Просмотр исходного кода

test(v13): cover golden opening prompt injection [phase 6.5/8 part 3]

Add planner-prompts coverage for buildGoldenOpeningGuidance: zh prose
present for ch1/2/3 with the load-bearing slot phrases (核心冲突, 金手指,
短期目标), opening-economy caps (≤3 scenes / ≤3 characters), info-layering
mention; en variant carries equivalent slot vocabulary; empty for ch>=4 in
both languages; renders as cohesive prose with no bulleted/numbered body
lines; buildPlannerUserMessage appends the section only when ch<=3.

Add writer-prompts coverage for buildGoldenOpeningDiscipline: zh and en
system prompts contain the discipline section for ch=1,2,3 and omit it for
ch>=4; standalone helper carries the 800-word trip, perform-vs-announce
distinction, and 小钩子 ending mandate; output stays prose, not a checklist;
helper is empty for ch>=4 / undefined.
Ma 6 месяцев назад
Родитель
Сommit
dc0a2d1584

+ 81 - 0
packages/core/src/__tests__/planner-prompts.test.ts

@@ -3,6 +3,7 @@ import {
   PLANNER_MEMO_SYSTEM_PROMPT,
   PLANNER_MEMO_USER_TEMPLATE,
   buildPlannerUserMessage,
+  buildGoldenOpeningGuidance,
 } from "../agents/planner-prompts.js";
 
 describe("PLANNER_MEMO_SYSTEM_PROMPT", () => {
@@ -84,3 +85,83 @@ describe("buildPlannerUserMessage", () => {
     expect(out).toContain("是否黄金三章:是");
   });
 });
+
+// ---------------------------------------------------------------------------
+// Phase 6.5 — Golden Opening Guidance prose
+// ---------------------------------------------------------------------------
+
+describe("buildGoldenOpeningGuidance", () => {
+  it("emits zh slot prose for chapter 1 (confront core conflict)", () => {
+    const out = buildGoldenOpeningGuidance(1, "zh");
+    expect(out).toContain("黄金三章规划指引");
+    expect(out).toContain("第 1 章");
+    // Ch1 slot: throw protagonist into core conflict
+    expect(out).toContain("核心冲突");
+    expect(out).toContain("主角出场即面对主线矛盾");
+    // Opening economy
+    expect(out).toContain("场景 ≤ 3");
+    expect(out).toContain("人物 ≤ 3");
+    // Information layering
+    expect(out).toContain("信息分层");
+  });
+
+  it("emits zh slot prose for chapter 2 (demonstrate the edge)", () => {
+    const out = buildGoldenOpeningGuidance(2, "zh");
+    expect(out).toContain("第 2 章");
+    expect(out).toContain("金手指");
+    // Must demand a concrete event, not narration
+    expect(out).toContain("一次具体事件");
+  });
+
+  it("emits zh slot prose for chapter 3 (lock the short-term goal)", () => {
+    const out = buildGoldenOpeningGuidance(3, "zh");
+    expect(out).toContain("第 3 章");
+    expect(out).toContain("短期目标");
+    expect(out).toContain("3-10 章");
+  });
+
+  it("emits en slot prose for chapter 1 with all three slot descriptions", () => {
+    const out = buildGoldenOpeningGuidance(1, "en");
+    expect(out).toContain("Golden Opening Guidance");
+    expect(out).toContain("Chapter 1");
+    expect(out).toContain("core conflict");
+    expect(out).toContain("concrete event");
+    expect(out).toContain("short-term goal");
+  });
+
+  it("returns empty string for ch>=4 in both languages", () => {
+    expect(buildGoldenOpeningGuidance(4, "zh")).toBe("");
+    expect(buildGoldenOpeningGuidance(5, "zh")).toBe("");
+    expect(buildGoldenOpeningGuidance(4, "en")).toBe("");
+    expect(buildGoldenOpeningGuidance(99, "en")).toBe("");
+  });
+
+  it("renders as cohesive prose, not a numbered or bulleted checklist", () => {
+    const zh = buildGoldenOpeningGuidance(1, "zh");
+    // Heading is allowed; body must not contain enumerated lines.
+    expect(zh).not.toMatch(/^\s*1\.\s/m);
+    expect(zh).not.toMatch(/^\s*-\s/m);
+    expect(zh).not.toMatch(/^\s*\*\s/m);
+  });
+
+  it("buildPlannerUserMessage appends guidance for ch<=3 and omits it for ch>=4", () => {
+    const base = {
+      previousChapterEndingExcerpt: "",
+      recentSummaries: "",
+      currentArcProse: "",
+      protagonistMatrixRow: "",
+      opponentRows: "",
+      collaboratorRows: "",
+      relevantThreads: "",
+      isGoldenOpening: false,
+      bookRulesRelevant: "",
+    };
+
+    const ch2 = buildPlannerUserMessage({ ...base, chapterNumber: 2 });
+    expect(ch2).toContain("黄金三章规划指引");
+    expect(ch2).toContain("第 2 章");
+
+    const ch4 = buildPlannerUserMessage({ ...base, chapterNumber: 4 });
+    expect(ch4).not.toContain("黄金三章规划指引");
+  });
+});

+ 77 - 1
packages/core/src/__tests__/writer-prompts.test.ts

@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
 import type { BookConfig } from "../models/book.js";
 import type { GenreProfile } from "../models/genre-profile.js";
 import { LengthSpecSchema } from "../models/length-governance.js";
-import { buildWriterSystemPrompt } from "../agents/writer-prompts.js";
+import { buildWriterSystemPrompt, buildGoldenOpeningDiscipline } from "../agents/writer-prompts.js";
 
 const BOOK: BookConfig = {
   id: "prompt-book",
@@ -164,6 +164,82 @@ describe("buildWriterSystemPrompt", () => {
     expect(prompt).toContain("core tag plus one contrasting detail");
   });
 
+  it("injects golden opening discipline into zh writer system prompt for ch<=3", () => {
+    for (const ch of [1, 2, 3]) {
+      const prompt = buildWriterSystemPrompt(
+        BOOK,
+        GENRE,
+        null,
+        "# Book Rules",
+        "# Genre Body",
+        "# Style Guide",
+        undefined,
+        ch,
+        "creative",
+        undefined,
+        "zh",
+        "governed",
+      );
+      expect(prompt).toContain("黄金三章写作纪律");
+      expect(prompt).toContain(`第 ${ch} 章`);
+    }
+  });
+
+  it("injects golden opening discipline into en writer system prompt for ch<=3", () => {
+    for (const ch of [1, 2, 3]) {
+      const prompt = buildWriterSystemPrompt(
+        BOOK,
+        { ...GENRE, language: "en", name: "General" },
+        null,
+        "# Book Rules",
+        "# Genre Body",
+        "# Style Guide",
+        undefined,
+        ch,
+        "creative",
+        undefined,
+        "en",
+        "governed",
+      );
+      expect(prompt).toContain("Golden Opening Discipline");
+      expect(prompt).toContain(`Chapter ${ch}`);
+    }
+  });
+
+  it("omits golden opening discipline for ch>=4 in both languages", () => {
+    const zh = buildWriterSystemPrompt(
+      BOOK, GENRE, null, "# Book Rules", "# Genre Body", "# Style Guide",
+      undefined, 4, "creative", undefined, "zh", "governed",
+    );
+    expect(zh).not.toContain("黄金三章写作纪律");
+
+    const en = buildWriterSystemPrompt(
+      BOOK, { ...GENRE, language: "en", name: "General" }, null,
+      "# Book Rules", "# Genre Body", "# Style Guide",
+      undefined, 4, "creative", undefined, "en", "governed",
+    );
+    expect(en).not.toContain("Golden Opening Discipline");
+  });
+
+  it("renders golden opening discipline as cohesive prose, not a checklist", () => {
+    const out = buildGoldenOpeningDiscipline(1, "zh");
+    // Header line is allowed; body must not contain enumerated/bulleted lines.
+    expect(out).not.toMatch(/^\s*1\.\s/m);
+    expect(out).not.toMatch(/^\s*-\s/m);
+    expect(out).not.toMatch(/^\s*\*\s/m);
+    // Carries the load-bearing slot constraints.
+    expect(out).toContain("800 字");
+    expect(out).toContain("做出来");
+    expect(out).toContain("说出来");
+    expect(out).toContain("小钩子");
+  });
+
+  it("buildGoldenOpeningDiscipline returns empty string for ch>=4 / undefined", () => {
+    expect(buildGoldenOpeningDiscipline(4, "zh")).toBe("");
+    expect(buildGoldenOpeningDiscipline(99, "en")).toBe("");
+    expect(buildGoldenOpeningDiscipline(undefined, "zh")).toBe("");
+  });
+
   it("tells governed English prompts to obey variance briefs and include resistance-bearing exchanges", () => {
     const prompt = buildWriterSystemPrompt(
       {