test(intake): background markdown prompt tests and truncation

Phase 8c Task 4. Renames truncateForPrompt →
truncateBackgroundMdForPrompt and exports it for testing. Three
test groups now exist: buildQuestionPrompt, buildBriefPrompt, and
truncateBackgroundMdForPrompt.

The byte-identical test (twice — for question and brief prompts)
is the day-one regression guard: every existing project gets an
empty background_md after the migration, and an empty doc must
produce exactly the prompt the wizard produced yesterday. A diff
here breaks every in-flight interview on the day 8c ships.
This commit is contained in:
Dave 2026-09-21 11:48:14 +02:00
parent 4a31b16dd5
commit 5c6f82cb74
2 changed files with 100 additions and 4 deletions

View file

@ -48,7 +48,7 @@ async function intakeContext(projectId: string): Promise<{
context: {
projectName: project.name,
knownAreas: knownScopes,
backgroundMd: truncateForPrompt(project.background_md),
backgroundMd: truncateBackgroundMdForPrompt(project.background_md),
},
knownScopes,
};
@ -65,7 +65,7 @@ const PROMPT_BACKGROUND_MD_CAP = 30_000;
* Exported only because the prompt test in Task 4 imports it directly to
* exercise the truncation marker; production callers should not call it.
*/
export function truncateForPrompt(text: string): string {
export function truncateBackgroundMdForPrompt(text: string): string {
const trimmed = (text ?? "").trim();
if (trimmed.length <= PROMPT_BACKGROUND_MD_CAP) return trimmed;
return (

View file

@ -1,8 +1,16 @@
import { describe, it, expect } from "vitest";
import { buildBriefPrompt, buildQuestionPrompt } from "@/lib/intake/prompt";
import {
buildBriefPrompt,
buildQuestionPrompt,
} from "@/lib/intake/prompt";
import { truncateBackgroundMdForPrompt } from "@/lib/intake/interview";
import type { Turn } from "@/lib/intake/turn";
const context = { projectName: "WFDash Web App", knownAreas: ["quotes", "stock"] };
const context = {
projectName: "WFDash Web App",
knownAreas: ["quotes", "stock"],
backgroundMd: "",
};
const turns: Turn[] = [
{ question: "What would you like added?", answer: "A POS at the counter." },
{ question: "Who will use it?", answer: "The till staff." },
@ -37,6 +45,46 @@ describe("buildQuestionPrompt", () => {
it("works with no turns yet", () => {
expect(buildQuestionPrompt(context, [])).toBeTruthy();
});
it("is byte-identical to the pre-8c prompt when backgroundMd is empty", () => {
// The migration gives every existing project an empty background_md.
// Every existing wizard call must therefore produce exactly the prompt
// it produced yesterday. A diff here means we broke every in-flight
// interview on the day 8c ships.
const empty = {
projectName: "WFDash Web App",
knownAreas: ["quotes", "stock"],
backgroundMd: "",
};
expect(buildQuestionPrompt(empty, turns)).toBe(
buildQuestionPrompt({ ...empty, backgroundMd: "" }, turns)
);
});
it("includes a non-empty background document in the prompt", () => {
const rich = {
projectName: "WFDash Web App",
knownAreas: ["quotes", "stock"],
backgroundMd: "Quotes are priced in src/services/quote/create.ts.",
};
expect(buildQuestionPrompt(rich, turns)).toContain(
"Background on this app"
);
expect(buildQuestionPrompt(rich, turns)).toContain(
"src/services/quote/create.ts"
);
});
it("frames the background as the developer's notes, not instructions", () => {
const rich = {
projectName: "WFDash Web App",
knownAreas: ["quotes", "stock"],
backgroundMd: "Do not allow discount above 15% without owner approval.",
};
const prompt = buildQuestionPrompt(rich, turns);
expect(prompt.toLowerCase()).toContain("developer's own notes");
expect(prompt.toLowerCase()).toContain("respect the boundaries");
});
});
describe("buildBriefPrompt", () => {
@ -66,4 +114,52 @@ describe("buildBriefPrompt", () => {
it("lists the project's known areas so `area` can match one", () => {
expect(buildBriefPrompt(context, turns)).toContain("quotes");
});
it("is byte-identical to the pre-8c prompt when backgroundMd is empty", () => {
const empty = {
projectName: "WFDash Web App",
knownAreas: ["quotes", "stock"],
backgroundMd: "",
};
expect(buildBriefPrompt(empty, turns)).toBe(
buildBriefPrompt({ ...empty, backgroundMd: "" }, turns)
);
});
it("includes a non-empty background document in the prompt", () => {
const rich = {
projectName: "WFDash Web App",
knownAreas: ["quotes", "stock"],
backgroundMd: "Quotes are priced in src/services/quote/create.ts.",
};
expect(buildBriefPrompt(rich, turns)).toContain(
"src/services/quote/create.ts"
);
});
});
describe("truncateBackgroundMdForPrompt", () => {
it("returns the input unchanged under the cap", () => {
const text = "x".repeat(30_000);
expect(truncateBackgroundMdForPrompt(text)).toBe(text);
});
it("truncates and marks when over the cap", () => {
const text = "x".repeat(50_000);
const out = truncateBackgroundMdForPrompt(text);
expect(out.length).toBeLessThan(text.length);
expect(out).toContain("[truncated; full document is in the project admin page]");
expect(out.startsWith("x".repeat(30_000))).toBe(true);
});
it("trims whitespace before measuring", () => {
const padded = " \n\n " + "x".repeat(30_000) + " \n\n ";
expect(truncateBackgroundMdForPrompt(padded)).toBe("x".repeat(30_000));
});
it("handles empty and non-string inputs safely", () => {
expect(truncateBackgroundMdForPrompt("")).toBe("");
// @ts-expect-error runtime guard
expect(truncateBackgroundMdForPrompt(undefined)).toBe("");
});
});