Files
writing-agent/tests/dictate-play-bind.test.ts
T
chenmoranzhiandCursor b864591afa 对话落盘同一轮填写失败后可以按次数补写。
游玩投影不再送进只给程序用的壳源码。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 08:29:38 +08:00

335 lines
13 KiB
TypeScript

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: "<div>", 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("手改排序");
});
});