prompt-template.test.js 4.12 KB
// 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 <Picture 1> and says <Picture 2> 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");
});