import { describe, expect, it } from "vitest"; import { Blackboard } from "../src/blackboard/blackboard.js"; import { applyDictatePlayBind, buildDictateContextOrder, bindDictateProductsToPlaySpec, collectDictateBindProducts, DICTATE_PLAY_BIND_INPUT_META, isDictateStyleBindTag, } from "../src/dictate/play-bind.js"; import { DICTATE_ORDER_META_KEY } from "../src/dictate/types.js"; import { DIALOGUE_HISTORY_TAG, WORKER_PERSONA_REF } from "../src/skills/context-order.js"; import { defaultPlaySlots } from "../src/skills/play-slots.js"; describe("dictate play-bind", () => { it("classifies style tags", () => { expect(isDictateStyleBindTag("设计.模仿范例")).toBe(true); expect(isDictateStyleBindTag("设计.模仿要点")).toBe(true); expect(isDictateStyleBindTag("设计.叙事指南")).toBe(true); expect(isDictateStyleBindTag("设计.故事推进")).toBe(true); expect(isDictateStyleBindTag("设计.用户需求")).toBe(true); expect(isDictateStyleBindTag("设计.用户约束")).toBe(true); expect(isDictateStyleBindTag("设计.监控栏")).toBe(true); expect(isDictateStyleBindTag("用户.需求")).toBe(false); // 中转与壳不进游玩文风挂载 expect(isDictateStyleBindTag("设计.美学纲领")).toBe(false); expect(isDictateStyleBindTag("设计.正文组成")).toBe(false); }); it("excludes design_only and reply shell from play bind products", () => { const board = new Blackboard(); board.write({ tag: "设计.实现机制", content: "因果", source: "dictate", metadata: { [DICTATE_ORDER_META_KEY]: 10 }, }); board.write({ tag: "设计.本局创作方案", content: '{"schema":"creation-plan.v1","items":[]}', source: "dictate", }); board.write({ tag: "设计.美学纲领与交互范式", content: "体验", source: "dictate", }); board.write({ tag: "设计.正文组成", content: JSON.stringify({ schema: "context-fragment.v1", 正文: { frontend: { html: "
", css: "x", js: "y" }, 数据契约: { blocks: { body: { 类型: "markdown", 来源: "model_generated" } } }, }, }), source: "dictate", }); const tags = collectDictateBindProducts(board).map((p) => p.tag); expect(tags).toEqual(["设计.实现机制"]); }); it("builds gm context_order: persona → products → history → input", () => { const order = buildDictateContextOrder({ products: [ { tag: "用户.需求", content: "雨夜", order: -40 }, { tag: "设计.叙事指南", content: "冷色短句", order: -30 }, { tag: "设计.本局创作方案", content: "{}", order: -50 }, { tag: "设计.正文组成", content: "{}", order: 0 }, ], playSlots: defaultPlaySlots(), }); const gm = order.slots.find((s) => s.ref === "world-simulator"); expect(gm).toBeTruthy(); const refs = gm!.inserts.map((i) => i.ref); expect(refs[0]).toBe(WORKER_PERSONA_REF); expect(refs).toContain("用户.需求"); expect(refs).toContain("设计.叙事指南"); expect(refs).not.toContain("设计.本局创作方案"); expect(refs).not.toContain("设计.正文组成"); expect(refs).toContain(DIALOGUE_HISTORY_TAG); expect(refs[refs.length - 1]).toBe("用户.最新输入"); const histIdx = refs.indexOf(DIALOGUE_HISTORY_TAG); expect(refs.indexOf("用户.需求")).toBeLessThan(histIdx); expect(refs.indexOf("设计.叙事指南")).toBeLessThan(histIdx); }); it("keeps image prompt on the auditor and off the main world", () => { const image = JSON.stringify({ schema: "context-fragment.v1", 技能: "画面与生图", brief: "西幻", mount: ["auditor"], 稳变: "stable", 正文: { 工作流: "western-fantasy" }, }); const order = buildDictateContextOrder({ products: [ { tag: "设计.舞台骨架", content: "舞台", order: 1 }, { tag: "设计.画面与生图", content: image, order: 2 }, { tag: "设计.变量设计与更新规则", content: "真值", order: 3 }, ], playSlots: { ...defaultPlaySlots(), auditor: true }, }); const gm = order.slots .find((s) => s.ref === "world-simulator")! .inserts.map((i) => i.ref); const auditor = order.slots .find((s) => s.ref === "auditor")! .inserts.map((i) => i.ref); expect(gm).toContain("设计.舞台骨架"); expect(gm).not.toContain("设计.画面与生图"); expect(auditor).toContain("设计.画面与生图"); expect(auditor).toContain("设计.变量设计与更新规则"); expect(auditor).not.toContain("设计.舞台骨架"); }); it("groups context by change frequency before applying importance order", () => { const fragment = (skill: string, stability: "stable" | "semi") => JSON.stringify({ schema: "context-fragment.v1", 技能: skill, brief: skill, mount: ["world-simulator"], 稳变: stability, 正文: { value: skill }, 开放问题: [], }); const order = buildDictateContextOrder({ products: [ { tag: "设计.稳定约束", content: fragment("稳定约束", "stable"), order: 10 }, { tag: "设计.阶段备忘", content: fragment("阶段备忘", "semi"), order: -50 }, ], playSlots: defaultPlaySlots(), }); const refs = order.slots[0]!.inserts.map((i) => i.ref); const history = refs.indexOf(DIALOGUE_HISTORY_TAG); expect(refs.indexOf("设计.稳定约束")).toBeLessThan(history); expect(refs.indexOf("设计.阶段备忘")).toBeGreaterThan(history); expect(refs.indexOf("设计.阶段备忘")).toBeLessThan( refs.indexOf("用户.最新输入"), ); }); it("skips opening and worker-set tags; narrator only gets style", () => { const board = new Blackboard(); board.write({ tag: "用户.需求", content: "要压抑", source: "dictate", metadata: { [DICTATE_ORDER_META_KEY]: -40 }, }); board.write({ tag: "设计.叙事指南", content: "冷色短句", source: "dictate", metadata: { [DICTATE_ORDER_META_KEY]: -30 }, }); board.write({ tag: "设计.开场白", content: "雨落在檐上。", source: "dictate", }); board.write({ tag: "设计.worker集", content: JSON.stringify({ play_slots: { gm: true, narrator: true, auditor: false, perspective: false }, }), source: "test", }); const products = collectDictateBindProducts(board); expect(products.map((p) => p.tag)).toEqual(["用户.需求", "设计.叙事指南"]); expect(products.every((p) => p.tag !== "设计.开场白")).toBe(true); const result = bindDictateProductsToPlaySpec(board); const narr = result.contextOrder.slots.find((s) => s.ref === "narrator"); expect(narr).toBeTruthy(); const narrRefs = narr!.inserts.map((i) => i.ref); expect(narrRefs).toContain("设计.叙事指南"); expect(narrRefs).not.toContain("用户.需求"); expect(narrRefs).toContain("输出.用户展示"); expect(narrRefs).not.toContain("运行.本轮.裁决"); const gm = result.contextOrder.slots.find((s) => s.ref === "world-simulator"); expect(gm!.inserts.map((i) => i.ref)).toContain("用户.需求"); expect(result.workerSetJson).toContain("context_order"); }); it("forces narrator on for 文本生成器 and mounts creation-period style context", () => { const board = new Blackboard(); board.write({ tag: "创作.选用配方", content: JSON.stringify({ id: "文本生成器", name: "文本生成器" }), source: "user", }); board.write({ tag: "设计.模仿范例", content: "范例正文", source: "dictate", metadata: { [DICTATE_ORDER_META_KEY]: -28 }, }); board.write({ tag: "设计.模仿要点", content: "- 句式节奏\n- 感官密度", source: "dictate", metadata: { [DICTATE_ORDER_META_KEY]: -28 }, }); board.write({ tag: "设计.叙事指南", content: "冷硬短句,笔墨落在权力交换。", source: "dictate", metadata: { [DICTATE_ORDER_META_KEY]: -20 }, }); board.write({ tag: "设计.故事推进", content: "用户输入用法:大纲扩写。禁止续写与自动写入。", source: "dictate", metadata: { [DICTATE_ORDER_META_KEY]: -22 }, }); board.write({ tag: "用户.需求", content: "短篇", source: "dictate", }); board.write({ tag: "设计.worker集", content: JSON.stringify({ play_slots: { gm: true, narrator: false, auditor: false, perspective: false }, }), source: "test", }); const result = bindDictateProductsToPlaySpec(board); expect(result.contextOrder.play_slots?.narrator).toBe(true); const narr = result.contextOrder.slots.find((s) => s.ref === "narrator"); expect(narr).toBeTruthy(); const narrRefs = narr!.inserts.map((i) => i.ref); expect(narrRefs).toContain("设计.模仿范例"); expect(narrRefs).toContain("设计.模仿要点"); expect(narrRefs).toContain("设计.叙事指南"); expect(narrRefs).toContain("设计.故事推进"); expect(narrRefs).not.toContain("用户.需求"); expect(JSON.parse(result.workerSetJson).play_slots.narrator).toBe(true); expect(board.getContentByTag("设计.故事推进")).toBe( "用户输入用法:大纲扩写。禁止续写与自动写入。", ); }); it("does not invent variable mappings; binds declared map slots and catalog", () => { const board = new Blackboard(); board.write({ tag: "用户.需求", content: "x", source: "dictate" }); const without = bindDictateProductsToPlaySpec(board); const refsWithout = without.contextOrder.slots[0]!.inserts.map((i) => i.ref); expect(refsWithout).not.toContain("变量.当前"); expect(refsWithout).not.toContain("上下文.角色态度"); board.write({ tag: "设计.变量目录", content: JSON.stringify({ schema: "variable-catalog.v1", fields: [{ key: "好感", type: "number", initial: 10, user_visible: true }], }), source: "test", }); board.write({ tag: "设计.变量映射", content: JSON.stringify({ schema: "value-map.v1", maps: [ { id: "aff", field: "好感", target_tag: "上下文.角色态度", bands: [{ min: 0, content: "冷" }], }, ], }), source: "test", }); const withMap = bindDictateProductsToPlaySpec(board); const refs = withMap.contextOrder.slots[0]!.inserts.map((i) => i.ref); expect(refs).toContain("变量.当前"); expect(refs).toContain("上下文.角色态度"); expect(refs).not.toContain("设计.变量映射"); }); it("skips rebind on save when mounted products are unchanged", () => { const board = new Blackboard(); board.write({ tag: "用户.需求", content: "雨夜", source: "test" }); const first = applyDictatePlayBind(board); expect(first.skipped).toBe(false); const orderUpdated = board.getLatestByTag("设计.上下文投影排序")?.updatedAt; const workerUpdated = board.getLatestByTag("设计.worker集")?.updatedAt; expect(board.getLatestByTag("设计.上下文投影排序")?.metadata?.[DICTATE_PLAY_BIND_INPUT_META]).toEqual( board.getLatestByTag("设计.worker集")?.metadata?.[DICTATE_PLAY_BIND_INPUT_META], ); const second = applyDictatePlayBind(board); expect(second.skipped).toBe(true); expect(second.changed).toBe(false); expect(board.getLatestByTag("设计.上下文投影排序")?.updatedAt).toBe(orderUpdated); expect(board.getLatestByTag("设计.worker集")?.updatedAt).toBe(workerUpdated); }); it("keeps a hand-edited order until a mounted product changes", () => { const board = new Blackboard(); board.write({ tag: "用户.需求", content: "雨夜", source: "test" }); applyDictatePlayBind(board); const orderItem = board.getLatestByTag("设计.上下文投影排序")!; const doc = JSON.parse(orderItem.content) as { brief?: string }; doc.brief = "手改排序"; const handEdited = JSON.stringify(doc, null, 2); board.write({ tag: "设计.上下文投影排序", content: handEdited, source: "user", metadata: orderItem.metadata, }); const kept = applyDictatePlayBind(board); expect(kept.skipped).toBe(true); expect(board.getContentByTag("设计.上下文投影排序")).toBe(handEdited); board.write({ tag: "设计.叙事指南", content: "短句", source: "test" }); const rebound = applyDictatePlayBind(board); expect(rebound.skipped).toBe(false); expect(rebound.boundTags).toContain("设计.叙事指南"); const rewritten = JSON.parse( board.getContentByTag("设计.上下文投影排序") ?? "{}", ) as { brief?: string }; expect(rewritten.brief).not.toBe("手改排序"); }); });