From 5c6f82cb74446717c79baab33e657c0fd7d74fe8 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 21 Sep 2026 11:48:14 +0200 Subject: [PATCH] test(intake): background markdown prompt tests and truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/lib/intake/interview.ts | 4 +- src/lib/intake/prompt.test.ts | 100 +++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/lib/intake/interview.ts b/src/lib/intake/interview.ts index ae222c6..ace8c11 100644 --- a/src/lib/intake/interview.ts +++ b/src/lib/intake/interview.ts @@ -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 ( diff --git a/src/lib/intake/prompt.test.ts b/src/lib/intake/prompt.test.ts index 818439e..4b9190c 100644 --- a/src/lib/intake/prompt.test.ts +++ b/src/lib/intake/prompt.test.ts @@ -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(""); + }); });