Files
writing-agent/tests/creation-plan.test.ts
T
chenmoranzhiandCursor 1cc4ae557a 创作一轮改为先选题再填写,减少工具空转和重复覆盖。
上下文按规则、历史、产物分段拼装,便于吃提示缓存并核对费用。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 23:48:20 +08:00

306 lines
11 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
CREATION_PLAN_TAG,
buildDictateSystemPrompt,
formatDictateRoundTail,
creationPlanModules,
formatCreationPlanCapabilityCatalog,
formatCreationPlanProgress,
runDictateTurn,
validateCreationPlan,
type CreationPlan,
} from "../src/dictate/index.js";
import { moduleBrowseGroup } from "../src/dictate/creation-plan.js";
import {
catalogModulesForIntake,
formatModuleCatalogForDictate,
loadModuleCatalog,
} from "../src/skills/creation-flow.js";
import type { AgentDriver } from "../src/runtime/driver.js";
async function fixture() {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
expect(catalog).toBeTruthy();
const modules = creationPlanModules(catalog!);
const plan: CreationPlan = {
schema: "creation-plan.v1",
技能: "本局创作方案",
brief: "以封闭雨夜中的身份猜疑兑现持续压迫感",
recipe: {
primary: "封闭空间悬疑",
secondary_traits: ["关系拉扯"],
method: ["让线索公开程度与人物关系共同改变每轮选择"],
references: [],
},
items: modules.map((module, index) => ({
module_id: module.id,
tier: index === 0 ? "必须" : "有一定效果",
why: `${module.name}对本局的作用`,
missing: "",
instances: [],
})),
};
return { catalog: catalog!, modules, plan };
}
describe("creation plan", () => {
it("covers all current content modules and excludes approved mechanical or legacy nodes", async () => {
const { modules } = await fixture();
expect(modules).toHaveLength(14);
expect(modules.map((module) => module.id)).not.toEqual(
expect.arrayContaining([
"creation-plan",
"narrative",
"variable-context",
"context-order",
"worker-spec",
"refine",
"user-requirements",
]),
);
expect(modules.map((module) => module.id)).toEqual(
expect.arrayContaining([
"aesthetics-interaction",
"generation-rules",
"reply-format",
"opening-setup",
]),
);
});
it("groups dictate nodes by runtime role and omits the recipe-only merge", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
expect(catalog).toBeTruthy();
const dictate = catalogModulesForIntake(catalog!, "dictate");
const ids = dictate.map((module) => module.id);
expect(ids).not.toContain("narrative");
expect(moduleBrowseGroup(dictate.find((module) => module.id === "creation-plan")!)).toBe(
"plan",
);
expect(moduleBrowseGroup(dictate.find((module) => module.id === "narrative-guide")!)).toBe(
"content",
);
expect(moduleBrowseGroup(dictate.find((module) => module.id === "reply-format")!)).toBe(
"present",
);
expect(moduleBrowseGroup(dictate.find((module) => module.id === "zero-layer-status")!)).toBe(
"present",
);
expect(moduleBrowseGroup(dictate.find((module) => module.id === "refine")!)).toBe(
"program",
);
expect(new Set(dictate.map((module) => moduleBrowseGroup(module)))).toEqual(
new Set(["plan", "content", "present", "program"]),
);
});
it("selects exactly one presentation chain", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
expect(catalog).toBeTruthy();
const zeroModules = creationPlanModules(catalog!, "zero_layer");
const ids = zeroModules.map((module) => module.id);
expect(zeroModules).toHaveLength(14);
expect(ids).toEqual(
expect.arrayContaining([
"zero-layer-status",
"zero-layer-reply-format",
"zero-layer-opening-setup",
]),
);
expect(ids).not.toEqual(
expect.arrayContaining(["status-bar", "reply-format", "opening-setup"]),
);
const plan: CreationPlan = {
schema: "creation-plan.v1",
技能: "本局创作方案",
brief: "持久终端体验",
presentation_mode: "zero_layer",
recipe: {
primary: "终端操作",
secondary_traits: [],
method: ["原地更新持久卡"],
references: [],
},
items: zeroModules.map((module) => ({
module_id: module.id,
tier: "必须",
why: module.name,
missing: "",
instances: [],
})),
};
expect(validateCreationPlan(JSON.stringify(plan), catalog).ok).toBe(true);
plan.items[plan.items.findIndex((item) => item.module_id === "zero-layer-status")] = {
module_id: "status-bar",
tier: "必须",
why: "错误混入普通楼层",
missing: "",
instances: [],
};
const mixed = validateCreationPlan(JSON.stringify(plan), catalog);
expect(mixed.ok).toBe(false);
expect(mixed.missingIds).toContain("zero-layer-status");
expect(mixed.unknownIds).toContain("status-bar");
});
it("rejects missing module ids and reports them", async () => {
const { catalog, plan } = await fixture();
const omitted = plan.items.at(-1)!.module_id;
plan.items.pop();
const result = validateCreationPlan(JSON.stringify(plan), catalog);
expect(result.ok).toBe(false);
expect(result.missingIds).toEqual([omitted]);
expect(result.error).toContain(omitted);
});
it("uses a short catalog before the plan and grouped state after it", async () => {
const { catalog, plan } = await fixture();
const shortCatalog = formatCreationPlanCapabilityCatalog(catalog);
expect(shortCatalog).toContain("items 必须覆盖");
expect(shortCatalog).toContain("presentation_mode=`message|zero_layer`");
expect(shortCatalog).toContain("0层回复呈现");
expect(shortCatalog).not.toContain("creation-plan(本局创作方案)");
const rules = plan.items.find((item) => item.module_id === "generation-rules");
expect(rules).toBeTruthy();
rules!.tier = "必须";
const unused = plan.items.find((item) => item.module_id === "topology");
expect(unused).toBeTruthy();
unused!.tier = "没有意义";
const progress = formatCreationPlanProgress({
plan,
catalog,
products: [
{ tag: catalog.modules.find((m) => m.id === "aesthetics-interaction")!.artifact, content: "{}" },
{ tag: "设计.生成规则#tenants", content: "{}" },
],
});
expect(progress).toContain("【本局创作方案 · 当前决策】");
expect(progress).toContain("不是本轮待办");
expect(progress).toContain("何时用 / 何时不用");
expect(progress).toContain("### 必须");
expect(progress).toContain("〔已落〕");
expect(progress).toContain("要落两份或以上同类对象");
expect(progress).toContain("字段很少、也不承载玩法");
const unusedSection = progress.split("### 没有意义")[1] ?? "";
expect(unusedSection).toContain("topology");
expect(unusedSection).not.toContain("何时用:");
const oldFullGuide = formatModuleCatalogForDictate(catalog);
const fixed = buildDictateSystemPrompt();
const before = formatDictateRoundTail({
capabilityCatalog: shortCatalog,
recipeExamples: "- text-generator:文本生成案例",
});
const after = formatDictateRoundTail({ creationPlanState: progress });
expect(fixed).not.toContain("当前决策");
expect(before).toContain("尚无本局创作方案");
expect(after).toContain("当前决策");
expect(after).toContain("收口前的核对清单");
expect(after).not.toContain("旧配方案例目录");
expect(after.length).toBeLessThan(
formatDictateRoundTail({ moduleGuide: oldFullGuide }).length,
);
});
it("blocks an incomplete plan insert before writing", async () => {
const { catalog, plan } = await fixture();
const missingId = plan.items.at(-1)!.module_id;
plan.items.pop();
const products: Array<{ tag: string; content: string }> = [];
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "p1",
name: "insert",
arguments: JSON.stringify({
position: CREATION_PLAN_TAG,
content: JSON.stringify(plan),
}),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content) as {
error?: string;
missing_ids?: string[];
};
expect(payload.error).toContain("能力覆盖校验失败");
expect(payload.missing_ids).toContain(missingId);
}
return { iterations: 1, stop: { kind: "text", content: "方案未写入。" } };
},
};
const result = await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "做一个封闭空间悬疑" }],
moduleCatalog: catalog,
driver,
handlers: {
listProducts: () => products,
writeProduct: (tag, content) => products.push({ tag, content }),
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(result.wroteTags).toEqual([]);
expect(products).toEqual([]);
});
it("warns but does not reject an opening with required gaps", async () => {
const { catalog, plan } = await fixture();
const requiredId = plan.items[0]!.module_id;
const products = [{ tag: CREATION_PLAN_TAG, content: JSON.stringify(plan) }];
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "o1",
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;
missing_required_module_ids?: string[];
};
expect(payload.ok).toBe(true);
expect(payload.missing_required_module_ids).toContain(requiredId);
}
return { iterations: 1, stop: { kind: "text", content: "开场已写入,并提示缺项。" } };
},
};
await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "先写开场" }],
moduleCatalog: catalog,
driver,
handlers: {
listProducts: () => products,
writeProduct: (tag, content) => products.push({ tag, content }),
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(products.some((product) => product.tag === "设计.开场白")).toBe(true);
});
});