Files
writing-agent/tests/worker-set-view.test.ts
moranzhi 94f67fa744 引入上下文片段、固定槽与投影排序,并完善机遇裁定与游玩期 UI。
把创作产物收敛为可挂载片段与 play_slots/context_order,同步修订世界模拟器模块与运行时拼装。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 01:36:32 +08:00

174 lines
6.1 KiB
TypeScript
Raw Permalink 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 { parseWorkerSetYaml } from "../src/skills/worker-set-parse.js";
import { formatWorkerSetForUser } from "../src/skills/worker-set-view.js";
const FULL_SAMPLE = `
version: 1
form_summary: 西幻升级交互,带状态跟踪
play_morphology: action_reaction_loop
workers:
- ref: world-simulator
duty: 世界推进与裁决
when: 每轮用户输入后
context:
static:
- 设计.worker集
- 世界.蓝图.确认稿
dynamic:
- 用户.最新输入
- 运行.事件流
outputs:
- 运行.本轮.裁决
- 运行.事件流
- ref: narrator
duty: 组装用户可见回复
when: 中间产物齐后
presentation:
tone: 西幻
avoid: [空泛公告]
context:
static:
- 叙事.指南.确认稿
dynamic:
- 运行.事件流
outputs:
- 输出.用户展示
instantiate_hints:
invoke: [opening-generator]
tag_flow:
- "用户.最新输入 → 运行.本轮.裁决"
- "运行.本轮.裁决 → 输出.用户展示"
`;
describe("formatWorkerSetForUser", () => {
it("builds Chinese worker cards with context order", () => {
const parsed = parseWorkerSetYaml(FULL_SAMPLE)!;
const view = formatWorkerSetForUser(parsed)!;
expect(view.headline).toContain("西幻");
expect(view.playModeLabel).toBe("行动–反应循环");
expect(view.workers).toHaveLength(2);
expect(view.workers[0].displayName).toBe("主世界层");
expect(view.playSlots?.some((s) => s.id === "gm" && s.enabled)).toBe(true);
expect(view.contextOrder?.slots?.length).toBeGreaterThan(0);
expect(view.creationUnits?.map((u) => u.id)).toEqual(
expect.arrayContaining([
"fixed:aesthetics", // sample 含 narrator.presentation
"worker:world-simulator",
"worker:narrator",
]),
);
// 未写入的示例话题不出现(不是填空表)
expect(view.creationUnits?.some((u) => u.id === "fixed:input_protocol")).toBe(
false,
);
expect(view.workers[0].context.staticTags[0].tag).toBe("设计.worker集");
expect(view.workers[0].writes).toEqual(["运行.本轮.裁决", "运行.事件流"]);
expect(view.workers[0].context.explicit).toBe(true);
expect(view.workers[1].presentation?.[0].label).toBe("语气");
expect(view.tagFlow).toHaveLength(2);
expect(view.designTasks).toHaveLength(1);
expect(view.designTasks[0].id).toBe("opening-generator");
});
it("exposes contextTags as standalone cards with mount targets", () => {
const parsed = parseWorkerSetYaml(`
narrative_guide: 残酷、不有求必应
resident_context:
- id: tone
content: 文风克制
mount: [narrator]
workers:
- ref: world-simulator
duty: 裁决
- ref: narrator
duty: 转述
presentation:
tone: 冷峻
`)!;
const view = formatWorkerSetForUser(parsed)!;
expect(view.contextTags?.length).toBeGreaterThanOrEqual(2);
const guide = view.contextTags?.find((c) => c.id === "fixed:narrative_guide");
expect(guide?.mountSummary).toBe("全部 Worker");
expect(guide?.mounts.map((m) => m.workerId).sort()).toEqual([
"narrator",
"world-simulator",
]);
const tone = view.contextTags?.find((c) => c.id === "resident:tone");
expect(tone?.mounts).toHaveLength(1);
expect(tone?.mounts[0].workerId).toBe("narrator");
expect(tone?.mountSummary).toBe(tone!.mounts[0].workerName);
const aesthetics = view.contextTags?.find((c) =>
c.id.startsWith("fixed:aesthetics"),
);
expect(aesthetics?.mounts[0].workerId).toBe("narrator");
expect(view.workers[1].readsSummary).toMatch(/美学|叙事|常驻/);
});
it("surfaces interaction paradigm, core worker, and rationale", () => {
const parsed = parseWorkerSetYaml(`
form_summary: 囚徒困境旁观
interaction_paradigm: 旁观多角(推断)
core_worker: world-simulator
reasoning: 三个角色需信息隔绝,各用 role-decide陈述用 Markdown不必文学转述。
workers:
- ref: world-simulator
role: core
rationale: 裁决组合结果,兼任结构化陈述。
- ref: role-decide
role: auxiliary
rationale: 仅 3 个参与者,隔绝决策上下文。
merge_considered: 不为每个 NPC 各开一个 worker。
`)!;
const view = formatWorkerSetForUser(parsed)!;
expect(view.interactionParadigm).toContain("旁观");
expect(view.coreWorker).toBe("world-simulator");
expect(view.reasoning).toContain("信息隔绝");
// 推断 play_slots 后顺序为 perspective → gm角色用槽位中文名
const gm = view.workers.find((w) => w.id === "world-simulator");
const perspective = view.workers.find((w) => w.id === "role-decide");
expect(gm?.roleLabel).toBe("主世界层");
expect(gm?.rationale).toContain("裁决");
expect(perspective?.roleLabel).toBe("角色视角");
expect(perspective?.mergeConsidered).toContain("NPC");
});
it("merges default context when explicit context omitted", () => {
const parsed = parseWorkerSetYaml(`
workers:
- ref: variable-update
duty: 跟踪变量
when: 世界机之后
`)!;
const view = formatWorkerSetForUser(parsed)!;
expect(
view.workers[0].context.staticTags.some(
(t) =>
t.tag.includes("变量.目录") ||
t.tag.includes("变量设计") ||
t.tag.includes("设计.变量"),
),
).toBe(true);
expect(view.workers[0].context.explicit).toBe(false);
expect(view.workers[0].writes).toContain("变量.当前");
});
it("marks design tasks filled when blackboard has tags", () => {
const parsed = parseWorkerSetYaml(`
workers:
- ref: narrator
instantiate_hints:
invoke: [opening-generator]
`)!;
const view = formatWorkerSetForUser(parsed, {
filledTags: ["运行.初始变量", "输出.开场白"],
})!;
expect(view.designTasks[0].status).toBe("filled");
});
it("parses context and outputs fields", () => {
const parsed = parseWorkerSetYaml(FULL_SAMPLE)!;
expect(parsed.workers[0].context?.static).toContain("世界.蓝图.确认稿");
expect(parsed.workers[0].outputs).toContain("运行.事件流");
});
});