// Checks the Stage2 prompt assembler (dev-plan.md T7) against the // mechanical items from MVCP M.15.1, plus the truncation behavior and the // footwear fix (S1 spike finding). Run: node --test prompt-template.test.js const test = require("node:test"); const assert = require("node:assert/strict"); const { buildStage2Prompt, truncateWords, wordCount, extractSection, MAX_DESCRIPTOR_WORDS } = require("./prompt-template.js"); const SAMPLE = { character: { appearanceDescriptor: "shoulder-length straight black hair, a slender build, natural fair skin, and a friendly neutral expression" }, garment: { appearanceDescriptor: "a fitted red qipao-style dress with gold trim embroidery and a side slit" }, danceRef: { motionDescriptor: "a lively hip-hop inspired routine with hip sways and raised-arm turns" }, }; test("truncateWords/wordCount behave as expected", () => { assert.equal(wordCount("one two three"), 3); assert.equal(truncateWords("one two three", 5), "one two three"); assert.equal(truncateWords("one two three four five six", 3), "one two three"); }); test("M.15.1 H6: detailed_description is 350-500 words for a representative fill", () => { const { prompt } = buildStage2Prompt(SAMPLE); const n = wordCount(extractSection(prompt, "detailed_description", "overall_soundscape")); assert.ok(n >= 350 && n <= 500, `detailed_description word count ${n} outside 350-500`); }); test("M.15.1 H1: total prompt stays well under the 7000-character cap", () => { const { prompt } = buildStage2Prompt(SAMPLE); assert.ok(prompt.length < 7000, `prompt is ${prompt.length} chars`); }); test("M.15.1 H9: retention_analysis only uses whitelisted relation markers", () => { const { prompt } = buildStage2Prompt(SAMPLE); const analysis = extractSection(prompt, "retention_analysis", "detailed_description"); const WHITELIST = ["fully_preserved", "weak_reference", "fully_copy", "attribute_transfer"]; const found = analysis.match(/- ([a-z_]+) -|: ([a-z_]+) -/g) || []; assert.ok(found.length > 0, "expected to find relation markers in retention_analysis"); for (const marker of WHITELIST) assert.ok(analysis.includes(marker), `expected marker "${marker}" to appear`); }); test("M.15.1 H5: single-shot clip (every [Shot N] reference is shot 1), no MM:SS timestamps", () => { const { prompt } = buildStage2Prompt(SAMPLE); const shotRefs = prompt.match(/\[Shot (\d+)\]/g) || []; assert.ok(shotRefs.length > 0, "expected at least one [Shot N] reference"); assert.ok(shotRefs.every((s) => s === "[Shot 1]"), `expected every shot reference to be [Shot 1], got: ${shotRefs}`); assert.doesNotMatch(prompt, /At \d{2}:\d{2}\.\d{3}/); }); test("footwear fix (S1 spike finding): prompt pins footwear to and says doesn't affect it", () => { const { prompt } = buildStage2Prompt(SAMPLE); assert.match(prompt, /footwear.*retained unchanged/); assert.match(prompt, /does not affect her footwear/); }); test("appearanceDescriptor/garmentDescriptor over the word budget get truncated", () => { const longDescriptor = Array.from({ length: 100 }, (_, i) => `word${i}`).join(" "); const { prompt, appearanceDescriptor, garmentDescriptor } = buildStage2Prompt({ character: { appearanceDescriptor: longDescriptor }, garment: { appearanceDescriptor: longDescriptor }, danceRef: SAMPLE.danceRef, }); assert.equal(wordCount(appearanceDescriptor), MAX_DESCRIPTOR_WORDS); assert.equal(wordCount(garmentDescriptor), MAX_DESCRIPTOR_WORDS); assert.ok(!prompt.includes("word99"), "expected the tail of the long descriptor to be gone"); }); test("an oversized motionDescriptor is truncated so detailed_description stays within the 500-word H6 cap", () => { const longMotion = Array.from({ length: 300 }, (_, i) => `move${i}`).join(" "); const { prompt, motionDescriptor } = buildStage2Prompt({ ...SAMPLE, danceRef: { motionDescriptor: longMotion } }); const n = wordCount(extractSection(prompt, "detailed_description", "overall_soundscape")); assert.ok(n <= 500, `detailed_description word count ${n} exceeds 500 after truncation`); assert.ok(wordCount(motionDescriptor) < 300, "expected motionDescriptor itself to have been shortened"); });