Files
writing-agent/tests/dictate.test.ts
T
chenmoranzhiandCursor dd0a9e87d8 接通主角绑定、开场封印与游玩辅助,让世界模拟器从创作连续落到本局。
直聘插入按模块回馈,并记录请求骨架便于排查。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 07:40:08 +08:00

622 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import {
buildDictateMessages,
buildDictateSystemPrompt,
buildDictateInsertFeedbackIndex,
defaultDictateOrder,
extractDictateDialogue,
formatDictateUserFacingBrief,
isAllowedProductTag,
isDictateModeValue,
resolveModuleForInsertTag,
runDictateTurn,
sortDictateProducts,
resolveRepeatableInsertTag,
lookupDictateInsertFeedback,
} from "../src/dictate/index.js";
import {
formatModuleCatalogForDictate,
loadModuleCatalog,
} from "../src/skills/creation-flow.js";
import type { AgentDriver } from "../src/runtime/driver.js";
describe("dictate context", () => {
it("assembles full dialogue + products with almost no filtering", () => {
const messages = buildDictateMessages({
systemPrompt: "SYS",
products: [{ tag: "用户.需求", content: "雨夜压抑" }],
dialogue: [
{ role: "user", text: "我想要压抑雨夜" },
{ role: "assistant", text: "已写入用户.需求" },
],
});
expect(messages[0]).toEqual({ role: "system", content: "SYS" });
expect(messages[1]?.role).toBe("user");
const body = messages[1]?.content ?? "";
expect(body).toContain("用户.需求");
expect(body).toContain("雨夜压抑");
expect(body).toContain("我想要压抑雨夜");
expect(body).toContain("已写入用户.需求");
});
it("after clear (empty dialogue) still carries products", () => {
const messages = buildDictateMessages({
systemPrompt: "SYS",
products: [{ tag: "设计.美学纲领", content: "冷色短句" }],
dialogue: [],
});
const body = messages[1]?.content ?? "";
expect(body).toContain("冷色短句");
expect(body).toContain("尚无对话");
});
it("extracts only user + dictate_reply turns", () => {
const turns = extractDictateDialogue([
{ kind: "orchestrator_prompt", text: "欢迎" },
{ kind: "user_input", text: "你好" },
{ kind: "agent_tool", text: "insert" },
{ kind: "dictate_reply", text: "已记下" },
]);
expect(turns).toEqual([
{ role: "user", text: "你好" },
{ role: "assistant", text: "已记下" },
]);
});
it("validates product tags and mode value", () => {
expect(isAllowedProductTag("用户.需求")).toBe(true);
expect(isAllowedProductTag("设计.美学纲领")).toBe(true);
expect(isAllowedProductTag("设计.正文组成")).toBe(true);
expect(isAllowedProductTag("设计.开场白")).toBe(true);
expect(isAllowedProductTag("变量.当前")).toBe(false);
expect(isDictateModeValue("dictate")).toBe(true);
expect(isDictateModeValue("recipe")).toBe(false);
});
it("system prompt requires insert toolcall and relative order", () => {
const prompt = buildDictateSystemPrompt({
recipeName: "数据化跑团体验",
recipeBrief:
"设定 + 真值 + 分档映射\n怎么做:\n- declare_variable\n- declare_map",
moduleGuide:
"【能力 · 何时落盘】\n- 美学纲领与交互范式:…\n 落盘:insert 「设计.美学纲领与交互范式」\n 何时用:还不能回答站位",
});
expect(prompt).toContain("设计.正文组成");
expect(prompt).toContain("设计.开场白");
expect(prompt).toContain("输出.开场白");
expect(prompt).toContain("insert");
expect(prompt).toContain("delete");
expect(prompt).toContain("declare_variable");
expect(prompt).toContain("undeclare_variable");
expect(prompt).toContain("declare_map");
expect(prompt).toContain("remove_map");
expect(prompt).toContain("同 position 再 insert");
expect(prompt).toContain("数据化跑团体验");
expect(prompt).toMatch(/order/);
expect(prompt).toContain("仅允许 toolcall");
expect(prompt).toContain("设计.模仿范例");
expect(prompt).toContain("设计.模仿要点");
expect(prompt).toContain("设计.故事推进");
expect(prompt).toContain("设计.叙事指南");
expect(prompt).toContain("大纲扩写");
expect(prompt).toContain("扮演停笔关掉");
expect(prompt).toContain("只挂载这些产物");
expect(prompt).toContain("【能力 · 何时落盘】");
expect(prompt).toContain("设计.美学纲领与交互范式");
expect(prompt).toContain("同轮可多次 insert");
expect(prompt).toContain("不抢跑未齐条件的下游");
expect(prompt).toContain("也不自动开跑 D");
expect(prompt).toContain("快穿短局");
expect(prompt).toContain("一核短局");
expect(prompt).toMatch(/舞台骨架/);
expect(prompt).toMatch(/覆盖 C/);
expect(prompt).toContain("设计.主角设定");
expect(prompt).toContain("reply_module");
expect(prompt).toContain("对用户可见回复");
expect(prompt).toContain("有 N 个 reply_module");
expect(prompt).toContain("〔可反复〕须拆分");
expect(prompt).toContain("设计.具体实例#");
});
it("formats module catalog for dictate with land tags and when", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
expect(catalog).toBeTruthy();
const block = formatModuleCatalogForDictate(catalog!);
expect(block).toContain("【能力 · 何时落盘】");
expect(block).toContain("美学纲领与交互范式");
expect(block).toContain("落盘:insert 「设计.美学纲领与交互范式」");
expect(block).toContain("何时用");
expect(block).toContain("何时不用");
expect(block).toContain("declare_variable");
expect(block).toContain("设计.开场白");
expect(block).toContain("〔落档程序〕");
expect(block).toContain("设计.生成规则#");
expect(block).toContain("设计.具体实例#");
expect(block).toContain("可增殖");
expect(block).not.toContain("role=prototype");
expect(block).toContain("落盘:insert 「设计.叙事指南」");
expect(block).toContain("落盘:insert 「设计.故事推进」");
expect(block).not.toMatch(/^- 叙事指南与故事推进/m);
});
it("sorts products by relative order: neg then 0 then pos", () => {
const sorted = sortDictateProducts([
{ tag: "设计.开场白", content: "开", order: 20 },
{ tag: "用户.需求", content: "需", order: -40 },
{ tag: "设计.正文组成", content: "格", order: 0 },
]);
expect(sorted.map((p) => p.tag)).toEqual([
"用户.需求",
"设计.正文组成",
"设计.开场白",
]);
});
it("default order puts stable/important before volatile", () => {
expect(defaultDictateOrder("用户.需求")).toBeLessThan(
defaultDictateOrder("设计.正文组成"),
);
expect(defaultDictateOrder("设计.正文组成")).toBeLessThan(
defaultDictateOrder("设计.开场白"),
);
});
it("context lists products in order", () => {
const body =
buildDictateMessages({
systemPrompt: "SYS",
products: [
{ tag: "设计.开场白", content: "尾", order: 10 },
{ tag: "用户.需求", content: "头", order: -10 },
],
dialogue: [],
})[1]?.content ?? "";
expect(body.indexOf("用户.需求")).toBeLessThan(body.indexOf("设计.开场白"));
expect(body).toContain("order=-10");
expect(body).toContain("order=10");
});
it("resolves insert tag to module and builds user-facing brief", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
expect(catalog).toBeTruthy();
const mod = resolveModuleForInsertTag(
"设计.美学纲领与交互范式",
catalog!,
);
expect(mod?.id).toBe("aesthetics-interaction");
expect(
resolveModuleForInsertTag("设计.开场白", catalog!)?.id,
).toBe("opening-setup");
expect(
resolveModuleForInsertTag("设计.具体实例#lei-ying", catalog!)?.id,
).toBe("concrete-instances");
expect(
resolveModuleForInsertTag("设计.生成规则#ex-girlfriend-card", catalog!)?.id,
).toBe("generation-rules");
expect(resolveModuleForInsertTag("设计.叙事指南", catalog!)?.id).toBe(
"narrative-guide",
);
expect(resolveModuleForInsertTag("设计.故事推进", catalog!)?.id).toBe(
"story-progression",
);
expect(resolveModuleForInsertTag("设计.文风", catalog!)?.id).toBe(
"narrative-guide",
);
const index = await buildDictateInsertFeedbackIndex({
catalog: catalog!,
skillPackRoot: "dialogue/world-simulator",
});
const brief = index.get("设计.美学纲领与交互范式");
expect(brief).toBeTruthy();
expect(brief!).toContain("# 模块 · 美学纲领与交互范式");
expect(brief!).toContain("回答时请附带本模块");
expect(brief!).toContain("本模块内容包括");
expect(brief!).toContain("## 写入用户回复时做什么");
expect(brief!).toContain("## 探测与追问(本模块口径)");
expect(brief!).toContain("展示优于提问");
expect(brief!).toMatch(/自评关注|交互范式|美学纲领/);
const alias = index.get("设计.开场白");
expect(alias).toContain("开场白与开场变量");
});
it("splits repeatable insert tags and rejects bare overwrite", async () => {
const families = new Set(["设计.生成规则", "设计.具体实例"]);
const instanceBody = JSON.stringify({
schema: "context-fragment.v1",
技能: "具体实例",
正文: {
rule_id: "ex-girlfriend-card",
batch_id: "lei-ying",
records: [{ 姓名: "雷樱" }],
},
});
const first = resolveRepeatableInsertTag({
tag: "设计.具体实例",
content: instanceBody,
existingTags: [],
repeatableFamilies: families,
});
expect("tag" in first && first.tag).toBe("设计.具体实例#lei-ying");
const second = resolveRepeatableInsertTag({
tag: "设计.具体实例",
content: JSON.stringify({
schema: "context-fragment.v1",
正文: { records: [{ 姓名: "谢明澜" }] },
}),
existingTags: ["设计.具体实例#lei-ying"],
repeatableFamilies: families,
});
expect("tag" in second && second.tag).toBe("设计.具体实例#谢明澜");
const overwriteBare = resolveRepeatableInsertTag({
tag: "设计.具体实例",
content: "不是JSON就抽不出槽",
existingTags: ["设计.具体实例#lei-ying"],
repeatableFamilies: families,
});
expect("error" in overwriteBare).toBe(true);
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const index = await buildDictateInsertFeedbackIndex({
catalog: catalog!,
skillPackRoot: "dialogue/world-simulator",
});
expect(
lookupDictateInsertFeedback(index, "设计.具体实例#lei-ying"),
).toContain("具体实例");
});
it("runDictateTurn remaps bare repeatable insert to #slot", async () => {
const products: { tag: string; content: string }[] = [];
const body = JSON.stringify({
schema: "context-fragment.v1",
正文: {
batch_id: "lei-ying",
records: [{ 姓名: "雷樱" }],
},
});
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "c1",
name: "insert",
arguments: JSON.stringify({
position: "设计.具体实例",
content: body,
}),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content) as {
ok?: boolean;
position?: string;
error?: string;
};
expect(payload.ok).toBe(true);
expect(payload.position).toBe("设计.具体实例#lei-ying");
}
return {
iterations: 1,
stop: { kind: "text", content: "已落实例" },
};
},
};
const result = await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "再生成一张雷樱" }],
driver,
repeatableFamilies: ["设计.具体实例", "设计.生成规则"],
handlers: {
listProducts: () => products,
writeProduct: (tag, content) => {
products.push({ tag, content });
},
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(result.wroteTags).toEqual(["设计.具体实例#lei-ying"]);
expect(products.map((p) => p.tag)).toEqual(["设计.具体实例#lei-ying"]);
});
it("insert tool result includes reply_module from lookup", async () => {
let capturedSpec: string | undefined;
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "c1",
name: "insert",
arguments: JSON.stringify({
position: "设计.美学纲领与交互范式",
content: "傀儡帝体验核",
}),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content) as {
ok?: boolean;
reply_module?: string;
};
expect(payload.ok).toBe(true);
expect(payload.reply_module).toContain("回答时请附带本模块");
capturedSpec = payload.reply_module;
}
return {
iterations: 1,
stop: { kind: "text", content: "已落美学;下一刀建议主角名片。" },
};
},
};
const products: { tag: string; content: string }[] = [];
const result = await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "傀儡帝恋爱" }],
driver,
handlers: {
listProducts: () => products,
writeProduct: (tag, content) => {
products.push({ tag, content });
},
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
lookupInsertFeedback: () =>
formatDictateUserFacingBrief({
tag: "设计.美学纲领与交互范式",
module: {
id: "aesthetics-interaction",
name: "美学纲领与交互范式",
declaration: "x",
artifact: "设计.美学纲领与交互范式",
},
promptMd: [
"```probe",
"缺其中一件:追问 1~2 点",
"```",
"```principles",
"展示优于提问、大胆优于保守",
"```",
"```output",
'{"自评":{"维度":[{"名":"交互范式"},{"名":"美学纲领"}]}}',
"```",
].join("\n"),
}),
},
});
expect(capturedSpec).toContain("探测与追问");
expect(result.wroteTags).toContain("设计.美学纲领与交互范式");
expect(result.reply).toContain("已落美学");
});
it("delete removes a product tag", async () => {
const products = new Map<string, string>([
["设计.开场白", "旧开场"],
["用户.需求", "雨夜"],
]);
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "d1",
name: "delete",
arguments: JSON.stringify({
position: "设计.开场白",
reason: "重做开场",
}),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content) as {
ok?: boolean;
deleted?: string;
};
expect(payload.ok).toBe(true);
expect(payload.deleted).toBe("设计.开场白");
}
return {
iterations: 1,
stop: { kind: "text", content: "已删开场白。" },
};
},
};
const result = await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "删掉开场白" }],
driver,
handlers: {
listProducts: () =>
[...products.entries()].map(([tag, content]) => ({ tag, content })),
writeProduct: (tag, content) => {
products.set(tag, content);
},
deleteProduct: (tag) => products.delete(tag),
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(products.has("设计.开场白")).toBe(false);
expect(products.has("用户.需求")).toBe(true);
expect(result.deletedTags).toContain("设计.开场白");
});
it("undeclare_variable and remove_map prune catalog/map docs", async () => {
let catalogRaw =
'{"schema":"variable-catalog.v1","fields":[{"key":"好感","type":"number","initial":10,"user_visible":true},{"key":"章节","type":"number","initial":1,"user_visible":true}]}';
let mapRaw =
'{"schema":"value-map.v1","maps":[{"id":"aff","field":"好感","target_tag":"上下文.角色态度","bands":[{"min":0,"content":"冷"}]}]}';
let currentRaw =
'{"rows":[{"key":"好感","value":10,"rev":1},{"key":"章节","value":1,"rev":1}]}';
const projection = new Map<string, string>([
["上下文.角色态度", "冷"],
]);
const tags = new Map<string, string>([
["设计.变量目录", catalogRaw],
["设计.变量映射", mapRaw],
["变量.当前", currentRaw],
]);
const driver: AgentDriver = {
async run(input) {
const r1 = await input.handleStep([
{
id: "u1",
name: "undeclare_variable",
arguments: JSON.stringify({ key: "好感" }),
},
]);
expect(r1.kind).toBe("continue");
const r2 = await input.handleStep([
{
id: "r1",
name: "remove_map",
arguments: JSON.stringify({ id: "aff" }),
},
]);
expect(r2.kind).toBe("continue");
if (r2.kind === "continue") {
const payload = JSON.parse(r2.results[0]!.content) as {
cleared_target?: string | null;
};
expect(payload.cleared_target).toBe("上下文.角色态度");
}
return {
iterations: 2,
stop: { kind: "text", content: "已移除好感与映射。" },
};
},
};
const result = await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "去掉好感" }],
driver,
handlers: {
listProducts: () => [],
writeProduct: () => undefined,
deleteProduct: () => false,
readTag: (tag) => tags.get(tag) ?? projection.get(tag),
writeTag: (tag, content) => {
tags.set(tag, content);
if (tag === "设计.变量目录") catalogRaw = content;
if (tag === "设计.变量映射") mapRaw = content;
if (tag === "变量.当前") currentRaw = content;
},
deleteTag: (tag) => {
const a = tags.delete(tag);
const b = projection.delete(tag);
return a || b;
},
clearDialogue: () => undefined,
},
});
expect(catalogRaw).toContain("章节");
expect(catalogRaw).not.toContain('"好感"');
expect(currentRaw).not.toContain('"好感"');
expect(tags.has("设计.变量映射")).toBe(false);
expect(projection.has("上下文.角色态度")).toBe(false);
expect(result.deletedTags.some((t) => t.includes("好感"))).toBe(true);
expect(result.deletedTags.some((t) => t.includes("aff"))).toBe(true);
});
it("parallel inserts each get a reply_module and multi-module hint", async () => {
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "c1",
name: "insert",
arguments: JSON.stringify({
position: "设计.美学纲领与交互范式",
content: "美学",
}),
},
{
id: "c2",
name: "insert",
arguments: JSON.stringify({
position: "设计.主角设定",
content: "主角",
}),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const a = JSON.parse(outcome.results[0]!.content) as {
reply_module?: string;
reply_hint?: string;
};
const b = JSON.parse(outcome.results[1]!.content) as {
reply_module?: string;
reply_hint?: string;
};
expect(a.reply_module).toContain("美学纲领");
expect(b.reply_module).toContain("主角设定");
expect(a.reply_hint).toMatch(/多个 reply_module|本步已带回模块/);
expect(b.reply_hint).toContain("本步已带回模块");
}
return {
iterations: 1,
stop: { kind: "text", content: "## 模块 · 美学\n\n## 模块 · 主角" },
};
},
};
await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "都要" }],
driver,
handlers: {
listProducts: () => [],
writeProduct: () => undefined,
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
lookupInsertFeedback: (tag) =>
tag.includes("美学")
? formatDictateUserFacingBrief({
tag,
module: {
id: "aesthetics-interaction",
name: "美学纲领与交互范式",
declaration: "x",
artifact: tag,
},
promptMd: "```probe\n美学追问\n```",
})
: formatDictateUserFacingBrief({
tag,
module: {
id: "protagonist",
name: "主角设定",
declaration: "x",
artifact: tag,
},
promptMd: "```probe\n主角追问\n```",
}),
},
});
});
});