/** * 创作流程 parse / validate / catalog / recipes */ import { describe, expect, it } from "vitest"; import { artifactTagForStep, extractModuleOpening, formatCreationFlowForUser, deriveFlowStepTitle, titleFromSuggestion, formatFlowProgressForAgent, listCallableCatalogModules, formatModuleCatalogForAgent, formatModuleCatalogForDictate, catalogModulesForIntake, formatRecipeCatalogForAgent, formatDictateRecipeBrief, formatRecipeExamplesForCreationPlan, formatSelectedRecipeForAgent, isCreationFlowComplete, loadAllRecipeDetails, loadModuleCatalog, loadModulePrompt, loadRecipeCatalog, loadRecipeDetail, mergeCreationFlowPreservingAccepted, needsFlowExpansion, nextPendingStep, listReadySteps, computeStepLayers, spawnRepeatableCreationStep, repeatableInstanceProductTag, spawnInstanceFromPrototype, hasSelectableCreationWork, hasAcceptedCloserStep, removeUnstartedInstance, removeRepeatableInstance, pruneRepeatableArtifactContent, looksLikeCreationFlowDoc, parseCreationFlow, pickRecordedStepId, parseModuleCatalog, parseModuleOpeningState, parseRecipeCatalog, parseRecipeYaml, parseSelectedRecipeRef, resolveDesignStepBinding, isProgressPointerTag, stringifyCreationFlow, stripReviseSteps, creationFlowFromRecipeSeed, validateCreationFlow, validateStepReadyToRun, } from "../src/skills/creation-flow.js"; import { loadWorkerSkillWithContext } from "../src/skills/loader.js"; const sampleCatalog = parseModuleCatalog(` modules: - name: 美学纲领与交互范式 declaration: 站位与体验契约 artifact: 设计.美学纲领与交互范式 - name: 实现机制 declaration: worker 与表 artifact: 设计.实现机制 - name: 生成规则 declaration: 可执行规则 artifact: 设计.生成规则 repeatable: true - name: 具体实例 declaration: 锚定实例 artifact: 设计.具体实例 repeatable: true - name: 开场白与开场变量 declaration: 收口 artifact: 设计.开场白与开场变量 closer: true `)!; describe("creation-flow", () => { it("parses minimal flow JSON and fills step ids", () => { const flow = parseCreationFlow(`{ "brief": "单角代入", "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] } ] }`); expect(flow).toEqual({ version: 1, brief: "单角代入", status: undefined, steps: [ { id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [], }, ], }); }); it("keeps accepted steps intact when the planner rewrites the flow", () => { const prev = `{ "status": "open", "steps": [ { "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] } ] }`; const rewritten = `{ "status": "open", "steps": [ { "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": ["实现机制"] }, { "id": "实现机制", "name": "实现机制", "depends_on": [] } ] }`; const merged = mergeCreationFlowPreservingAccepted({ prevRaw: prev, nextRaw: rewritten, acceptedStepIds: ["美学纲领与交互范式"], }); expect(merged.restored).toEqual(["美学纲领与交互范式"]); const flow = parseCreationFlow(merged.raw)!; expect(flow.steps[0]?.depends_on).toEqual([]); expect(flow.steps.map((s) => s.id)).toEqual([ "美学纲领与交互范式", "实现机制", ]); }); it("re-inserts an accepted step the planner dropped", () => { const prev = `{ "status": "open", "steps": [ { "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] } ] }`; const rewritten = `{ "status": "open", "steps": [{ "id": "实现机制", "name": "实现机制", "depends_on": [] }] }`; const merged = mergeCreationFlowPreservingAccepted({ prevRaw: prev, nextRaw: rewritten, acceptedStepIds: ["美学纲领与交互范式"], }); expect(parseCreationFlow(merged.raw)!.steps.map((s) => s.id)).toEqual([ "美学纲领与交互范式", "实现机制", ]); }); it("leaves an untouched rewrite alone", () => { const prev = `{ "status": "open", "steps": [ { "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] } ] }`; const rewritten = `{ "status": "open", "steps": [ { "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }, { "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式"] } ] }`; const merged = mergeCreationFlowPreservingAccepted({ prevRaw: prev, nextRaw: rewritten, acceptedStepIds: ["美学纲领与交互范式"], }); expect(merged.restored).toEqual([]); expect(parseCreationFlow(merged.raw)!.steps.map((s) => s.id)).toEqual([ "美学纲领与交互范式", "实现机制", ]); }); it("drops planner revise steps when merging flow", () => { const prev = `{ "status": "open", "steps": [ { "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] } ] }`; const rewritten = `{ "status": "open", "steps": [ { "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }, { "id": "美学纲领与交互范式·改", "name": "美学纲领与交互范式", "mode": "revise", "revises": "美学纲领与交互范式", "depends_on": ["美学纲领与交互范式"] }, { "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式·改"] } ] }`; const merged = mergeCreationFlowPreservingAccepted({ prevRaw: prev, nextRaw: rewritten, acceptedStepIds: ["美学纲领与交互范式"], }); const flow = parseCreationFlow(merged.raw)!; expect(flow.steps.map((s) => s.id)).toEqual([ "美学纲领与交互范式", "实现机制", ]); expect(flow.steps[1]?.depends_on).toEqual(["美学纲领与交互范式"]); }); it("allows duplicate capability names with distinct ids", () => { const flow = parseCreationFlow(`{ "status": "open", "steps": [ { "id": "生成规则", "name": "生成规则", "depends_on": [] }, { "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] }, { "id": "具体实例", "name": "具体实例", "depends_on": ["生成规则#2"] } ] }`)!; expect(flow.status).toBe("open"); expect(flow.steps.map((s) => s.id)).toEqual([ "生成规则", "生成规则#2", "具体实例", ]); expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true); }); it("auto-numbers duplicate names when id omitted", () => { const flow = parseCreationFlow(`{ "steps": [ { "name": "生成规则", "depends_on": [] }, { "name": "生成规则", "depends_on": ["生成规则"] } ] }`)!; expect(flow.steps[0]?.id).toBe("生成规则"); expect(flow.steps[1]?.id).toBe("生成规则#2"); expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true); }); it("extracts JSON from surrounding text", () => { const flow = parseCreationFlow( `说明如下\n{"steps":[{"name":"美学纲领与交互范式","depends_on":[]}]}\n完`, ); expect(flow?.steps[0]?.name).toBe("美学纲领与交互范式"); }); it("extracts JSON from a code fence", () => { const flow = parseCreationFlow( "```json\n{\"brief\":\"单角代入\",\"steps\":[{\"name\":\"美学纲领与交互范式\",\"depends_on\":[]}]}\n```", ); expect(flow?.brief).toBe("单角代入"); expect(flow?.steps[0]?.name).toBe("美学纲领与交互范式"); }); it("recognizes a DAG object and ignores brief-only objects", () => { expect( looksLikeCreationFlowDoc({ brief: "单角代入", steps: [{ name: "美学纲领与交互范式", depends_on: [] }], }), ).toBe(true); expect(looksLikeCreationFlowDoc({ brief: "单角代入" })).toBe(false); }); it("validates deps must appear earlier", () => { const bad = parseCreationFlow(`{ "steps": [ { "name": "实现机制", "depends_on": ["美学纲领与交互范式"] }, { "name": "美学纲领与交互范式", "depends_on": [] } ] }`)!; const result = validateCreationFlow(bad, sampleCatalog); expect(result.ok).toBe(false); expect(result.errors.some((e) => e.includes("未排在其前面"))).toBe(true); }); it("rejects unknown module names when catalog given", () => { const flow = parseCreationFlow(`{ "steps": [{ "name": "不存在的工序", "depends_on": [] }] }`)!; const result = validateCreationFlow(flow, sampleCatalog); expect(result.ok).toBe(false); expect(result.errors[0]).toContain("不在模块目录"); }); it("rejects non-repeatable duplicate names", () => { const flow = parseCreationFlow(`{ "steps": [ { "name": "实现机制", "depends_on": [] }, { "id": "实现机制#2", "name": "实现机制", "depends_on": ["实现机制"] } ] }`)!; const result = validateCreationFlow(flow, sampleCatalog); expect(result.ok).toBe(false); expect(result.errors.some((e) => e.includes("repeatable"))).toBe(true); }); it("accepts a valid ordered flow", () => { const flow = parseCreationFlow(`{ "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "实现机制", "depends_on": ["美学纲领与交互范式"] } ] }`)!; expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true); expect(artifactTagForStep("美学纲领与交互范式", sampleCatalog)).toBe( "设计.美学纲领与交互范式", ); }); it("formats user view without English tags", () => { const flow = parseCreationFlow(`{ "brief": "网恋回合", "status": "open", "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "生成规则", "depends_on": ["美学纲领与交互范式"] }, { "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] } ] }`)!; const view = formatCreationFlowForUser(flow, sampleCatalog); expect(view.brief).toBe("网恋回合"); expect(view.status).toBe("open"); expect(view.steps[0]).toMatchObject({ order: 1, name: "美学纲领与交互范式", depends_on: [], declaration: "站位与体验契约", }); expect(view.steps[2]?.role).toBe("instance"); expect(view.steps[2]?.occurrence).toBe(1); expect(view.steps[1]?.role).toBe("prototype"); expect(view.steps[2]?.repeatable).toBe(true); expect(view.steps[0]?.runState).toBe("pending"); expect(view.steps[0]?.layer).toBe(0); expect(view.steps[0]?.ready).toBe(true); expect(view.steps[0]?.selectable).toBe(true); expect(view.availableModules?.map((m) => m.name)).toEqual([ "实现机制", "具体实例", "开场白与开场变量", ]); expect(view.steps[1]?.runState).toBe("pending"); expect(view.steps[1]?.layer).toBe(1); expect(view.steps[1]?.ready).toBe(false); expect(view.steps[2]?.runState).toBe("pending"); }); it("shows a short title for 生成规则 / 具体实例 prototypes and instances", () => { const flow = parseCreationFlow(`{ "steps": [ { "id": "生成规则", "name": "生成规则", "role": "prototype", "suggestion": "幸存者生成规则:锚定出租屋内的女性租客", "depends_on": [] }, { "id": "生成规则#1", "name": "生成规则", "role": "instance", "from": "生成规则", "depends_on": [] }, { "id": "具体实例", "name": "具体实例", "role": "prototype", "suggestion": "按幸存者规则生成开局女租客", "depends_on": ["生成规则"] }, { "id": "具体实例#1", "name": "具体实例", "role": "instance", "from": "具体实例", "params": { "batch_goal": "开局女租客", "rule_id": "survivors" }, "depends_on": ["生成规则#1"] } ] }`)!; const untitled = formatCreationFlowForUser(flow, sampleCatalog); expect(untitled.steps.find((s) => s.id === "生成规则")?.title).toBe( "幸存者生成规则", ); expect(untitled.steps.find((s) => s.id === "生成规则#1")?.title).toBe( "幸存者生成规则", ); expect(untitled.steps.find((s) => s.id === "具体实例")?.title).toBe( "按幸存者规则生成开局女租客", ); expect(untitled.steps.find((s) => s.id === "具体实例#1")?.title).toBe( "开局女租客", ); const accepted = formatCreationFlowForUser(flow, sampleCatalog, [], [], { "生成规则#1": { summary: "生成规则 · 幸存者 · 预生成并动态" }, "具体实例#1": { summary: "具体实例 · 女租客 · 2条" }, }); expect(accepted.steps.find((s) => s.id === "生成规则#1")?.title).toBe( "幸存者", ); expect(accepted.steps.find((s) => s.id === "具体实例#1")?.title).toBe( "女租客", ); expect( deriveFlowStepTitle({ name: "生成规则", hint: { content: { 正文: { 本步参数: { target: "女租客" } }, }, }, }), ).toBe("女租客"); expect(titleFromSuggestion("怪物生成规则:按族群抽样")).toBe( "怪物生成规则", ); }); it("marks accepted steps done and the next pending current", () => { const flow = parseCreationFlow(`{ "brief": "网恋回合", "status": "open", "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "生成规则", "depends_on": ["美学纲领与交互范式"] }, { "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] } ] }`)!; const view = formatCreationFlowForUser(flow, sampleCatalog, [ "美学纲领与交互范式", ]); expect(view.steps.map((s) => s.runState)).toEqual([ "done", "pending", "pending", ]); expect(view.steps[1]?.selectable).toBe(true); }); it("loads world-simulator catalog and formats for agent", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); expect(catalog?.modules.some((m) => m.name === "美学纲领与交互范式")).toBe( true, ); expect(catalog?.modules.some((m) => m.name === "主角设定")).toBe(true); expect(catalog?.modules.some((m) => m.name === "交互范式")).toBe(false); expect(catalog?.modules.some((m) => m.name === "美学纲领")).toBe(false); expect( catalog?.modules.find((m) => m.name === "生成规则")?.repeatable, ).toBe(true); expect( catalog?.modules.find((m) => m.name === "具体实例")?.repeatable, ).toBe(true); expect( catalog?.modules.find((m) => m.name === "生成规则")?.kind, ).toBe("prior-artifact"); expect( catalog?.modules.find((m) => m.name === "具体实例")?.kind, ).toBe("prior-artifact"); expect( catalog?.modules.find((m) => m.name === "生成规则")?.params?.[0]?.key, ).toBe("target"); expect( catalog?.modules.find((m) => m.name === "具体实例")?.params?.[0]?.key, ).toBe("rule_id"); expect( catalog?.modules.find((m) => m.name === "上下文排序")?.auto, ).toBe(true); const gen = catalog?.modules.find((m) => m.name === "生成规则"); expect(gen?.when).toBeTruthy(); expect(gen?.when_not).toBeTruthy(); expect(gen?.boundary).toBeTruthy(); const block = formatModuleCatalogForAgent(catalog!); expect(block).toContain("【能力"); expect(block).toContain("美学纲领与交互范式:"); expect(block).toContain("生成规则〔可反复〕〔先验产物〕"); expect(block).toContain("上下文排序〔程序步〕"); expect(block).toContain("何时用"); expect(block).toContain("何时不用"); expect(block).toContain("role=prototype"); expect(block).toContain("编排参数"); expect(block).not.toContain("设计.美学纲领与交互范式"); expect(block).toContain("- 叙事指南与故事推进:"); expect(block).not.toMatch(/^- 叙事指南:/m); expect(block).not.toMatch(/^- 故事推进:/m); const dictateBlock = formatModuleCatalogForDictate(catalog!); expect(dictateBlock).toMatch(/^- 叙事指南:/m); expect(dictateBlock).toMatch(/^- 故事推进:/m); expect(dictateBlock).toContain("开场白与开场变量〔可反复〕〔收口〕"); expect(dictateBlock).toContain("信息可见范围〔落档程序〕"); expect(dictateBlock).not.toMatch(/^- 叙事指南与故事推进/m); expect( catalog?.modules.find((m) => m.name === "叙事指南与故事推进")?.intake, ).toBe("recipe"); expect(catalog?.modules.find((m) => m.name === "叙事指南")?.intake).toBe( "dictate", ); expect(catalog?.modules.find((m) => m.name === "故事推进")?.intake).toBe( "dictate", ); expect(catalog?.modules.find((m) => m.name === "故事推进")?.libraries).toEqual([ "preferences", ]); expect(catalog?.modules.some((m) => m.id === "user-requirements")).toBe(false); expect( catalog?.modules.find((m) => m.name === "叙事指南")?.libraries, ).toEqual(["style-packs"]); expect( catalog?.modules.find((m) => m.name === "叙事指南与故事推进")?.libraries, ).toEqual(["style-packs"]); expect(dictateBlock).not.toContain("【偏好库 · 可选用】"); expect( catalogModulesForIntake(catalog!, "recipe").some( (m) => m.name === "叙事指南", ), ).toBe(false); expect( catalogModulesForIntake(catalog!, "dictate").some( (m) => m.name === "叙事指南与故事推进", ), ).toBe(false); const leftover = listCallableCatalogModules({ catalog: catalog! }); expect(leftover.some((m) => m.name === "叙事指南与故事推进")).toBe(true); expect(leftover.some((m) => m.name === "叙事指南")).toBe(false); expect(leftover.some((m) => m.name === "故事推进")).toBe(false); const splitFlow = parseCreationFlow(`{ "steps": [{ "name": "叙事指南", "depends_on": [] }] }`)!; expect(validateCreationFlow(splitFlow, catalog).ok).toBe(false); }); it("loads recipe catalog for user director selection UI", async () => { const recipes = await loadRecipeCatalog("dialogue/world-simulator"); expect(recipes?.recipes.some((r) => r.name === "回合推演")).toBe(true); expect(recipes?.recipes.some((r) => r.name === "扩写助手")).toBe(true); expect(recipes?.recipes.some((r) => r.name === "文本生成器")).toBe(false); const block = formatRecipeCatalogForAgent(recipes!); expect(block).toContain("须由用户手动选择"); expect(block).toContain("回合推演"); const details = await loadAllRecipeDetails("dialogue/world-simulator"); expect(details.length).toBeGreaterThanOrEqual(2); const ws = details.find((d) => d.id === "world-simulator"); expect(ws?.when).toBeTruthy(); expect(ws?.core).toBeTruthy(); expect(ws?.process).toBeTruthy(); expect(ws?.principles).toBeTruthy(); expect( ws?.seed?.steps.some((s) => s.name === "美学纲领与交互范式"), ).toBe(true); expect(ws?.seed?.status).toBe("open"); const formatted = formatSelectedRecipeForAgent(ws!); expect(formatted).toContain("核心思路"); expect(formatted).toContain("设计流程"); expect(formatted).toContain("原则"); expect(formatted).not.toContain("调味提示"); }); it("loads dictate recipe catalog separately with Chinese ids", async () => { const dictate = await loadRecipeCatalog( "dialogue/world-simulator", undefined, "dictate", ); expect(dictate?.recipes.some((r) => r.id === "文本生成器")).toBe(true); expect(dictate?.recipes.some((r) => r.name === "文本生成器")).toBe(true); expect(dictate?.recipes.some((r) => r.name === "交互式长文生成器")).toBe(true); expect(dictate?.recipes.some((r) => r.name === "数据化跑团体验")).toBe(true); expect(dictate?.recipes.some((r) => r.id === "快穿短局")).toBe(true); expect(dictate?.recipes.some((r) => r.name === "快穿短局")).toBe(true); expect(dictate?.recipes.some((r) => r.id === "正文组成")).toBe(false); expect(dictate?.recipes).toHaveLength(4); expect(dictate?.recipes[0]?.id).toBe("快穿短局"); const entry = dictate!.recipes.find((r) => r.id === "文本生成器")!; expect(entry.family).toBe("dictate"); const detail = await loadRecipeDetail("dialogue/world-simulator", entry); expect(detail.method?.join("")).toMatch(/范例|写法|输入/); expect(detail.tierTendencies?.some((item) => item.module_id === "narrative-guide")).toBe(true); expect(detail.seed).toBeNull(); const longform = dictate!.recipes.find((r) => r.id === "交互式长文生成器")!; const longDetail = await loadRecipeDetail("dialogue/world-simulator", longform); expect(longDetail.brief).toMatch(/长连续/); expect(longDetail.method?.join("")).toMatch(/变量|动态表/); expect(longDetail.method?.join("")).toMatch(/硬机制|数据化跑团/); const rpg = dictate!.recipes.find((r) => r.id === "数据化跑团体验")!; const rpgDetail = await loadRecipeDetail("dialogue/world-simulator", rpg); expect(rpgDetail.brief).toMatch(/硬判断|结构化状态/); expect(rpgDetail.method?.join("")).toMatch(/长短|连续性/); expect(rpgDetail.tierTendencies?.some((item) => item.module_id === "mechanism")).toBe(true); const skip = dictate!.recipes.find((r) => r.id === "快穿短局")!; const skipDetail = await loadRecipeDetail("dialogue/world-simulator", skip); expect(skipDetail.seed).toBeNull(); expect(skipDetail.brief).toMatch(/体验核|短反馈/); expect(skipDetail.signals?.join("")).toMatch(/快穿|一次性|短任务/); const examples = formatRecipeExamplesForCreationPlan([ detail, longDetail, rpgDetail, skipDetail, ]); expect(examples).toContain("适用信号"); expect(examples).toContain("档位倾向"); expect(examples).toContain("variable-design=有必要"); expect(examples).not.toContain("固定路径"); }); it("parses selected recipe ref", () => { expect(parseSelectedRecipeRef("world-simulator")).toBe("world-simulator"); expect(parseSelectedRecipeRef('{"id":"expand-assistant","name":"扩写助手"}')).toBe( "expand-assistant", ); expect(parseSelectedRecipeRef("")).toBeNull(); }); it("parses recipe yaml with methodology fields", () => { const detail = parseRecipeYaml( ` when: 测试适用 core: 核心一句话 process: - 先美学 - 再按缺口选型 principles: - 正推 brief: 测试 brief steps: - name: 美学纲领与交互范式 depends_on: [] `, { id: "t", name: "测试配方", declaration: "测" }, ); expect(detail.when).toBe("测试适用"); expect(detail.core).toContain("核心一句话"); expect(detail.process).toEqual(["先美学", "再按缺口选型"]); expect(detail.principles).toEqual(["正推"]); expect(detail.seed?.status).toBe("open"); expect(detail.seed?.steps).toEqual([ { id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [], }, ]); const formatted = formatSelectedRecipeForAgent(detail); expect(formatted).toContain("用户已选配方"); expect(formatted).toContain("核心思路"); expect(formatted).toContain("开局 steps"); const seeded = creationFlowFromRecipeSeed(detail); expect(seeded?.steps[0]?.name).toBe("美学纲领与交互范式"); expect(seeded?.status).toBe("open"); const raw = stringifyCreationFlow(seeded!); expect(parseCreationFlow(raw)?.steps[0]?.id).toBe("美学纲领与交互范式"); expect(creationFlowFromRecipeSeed({ seed: null })).toBeNull(); expect( creationFlowFromRecipeSeed({ seed: { version: 1, status: "open", steps: [] }, }), ).toBeNull(); }); it("creationFlowFromRecipeSeed follows the recipe steps, not a fixed first node", () => { const detail = parseRecipeYaml( ` when: 其它剧本 steps: - name: 舞台骨架 depends_on: [] - name: 实现机制 depends_on: ["舞台骨架"] `, { id: "other", name: "其它", declaration: "测" }, ); const seeded = creationFlowFromRecipeSeed(detail); expect(seeded?.steps.map((s) => s.name)).toEqual(["舞台骨架", "实现机制"]); }); it("falls back to legacy hint when methodology absent", () => { const detail = parseRecipeYaml( ` when: 旧配方 hint: 可调味 steps: - name: 美学纲领与交互范式 depends_on: [] `, { id: "legacy", name: "旧", declaration: "测" }, ); expect(detail.hint).toBe("可调味"); expect(formatSelectedRecipeForAgent(detail)).toContain("调味提示(旧字段)"); }); it("parseRecipeCatalog skips incomplete rows", () => { const cat = parseRecipeCatalog(` recipes: - id: ok name: 好配方 declaration: 有声明 - id: bad name: 缺声明 `); expect(cat?.recipes).toEqual([ { id: "ok", name: "好配方", declaration: "有声明", family: "recipe" }, ]); }); it("injects selected recipe into design-flow", async () => { const selected = await loadWorkerSkillWithContext( "world-simulator", "design-flow", undefined, { selectedRecipeRef: "world-simulator" }, ); expect(selected.worker.outputTags).toContain("设计.创作流程"); expect(selected.promptBody).toContain("【用户已选配方 · 回合推演】"); expect(selected.promptBody).toContain("回合推演"); expect(selected.promptBody).toContain("【能力"); expect(selected.promptBody).toContain("美学纲领与交互范式"); expect(selected.promptBody).toContain("核心思路"); expect(selected.promptBody).toContain("何时用"); expect(selected.promptBody).toContain("开局 steps"); expect(selected.promptBody).toContain("【流程进度】"); expect(selected.promptBody).not.toContain("用户尚未手动选择"); }); it("tells design-flow which steps are done and not to reschedule them", async () => { const flowRaw = JSON.stringify({ steps: [{ name: "美学纲领与交互范式", depends_on: [] }], }); const loaded = await loadWorkerSkillWithContext( "world-simulator", "design-flow", undefined, { selectedRecipeRef: "world-simulator", flowRaw, acceptedStepNames: ["美学纲领与交互范式"], filledArtifactTags: ["设计.美学纲领与交互范式"], }, ); expect(loaded.promptBody).toContain("【流程进度】"); expect(loaded.promptBody).toContain("已完成:"); expect(loaded.promptBody).toContain("美学纲领与交互范式"); expect(loaded.promptBody).toContain("已验收"); expect(loaded.promptBody).toContain("这一面已覆盖"); expect(loaded.promptBody).toContain("要改则用户点该节点重进"); expect(loaded.promptBody).toContain("生成规则"); }); it("formatFlowProgressForAgent keeps seed steps as draft not new work", () => { const flow = parseCreationFlow(`{ "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] } ] }`)!; const text = formatFlowProgressForAgent({ flow, acceptedStepIds: [], catalog: sampleCatalog, }); expect(text).toContain("草案已有、尚未验收"); expect(text).toContain("美学纲领与交互范式"); expect(text).toContain("保留原 id"); expect(text).toContain("目录〔可反复〕:生成规则、具体实例"); expect(text).toContain("尚未编入、仍可调用"); expect(text).toContain("实现机制"); const leftoverAt = text.indexOf("尚未编入、仍可调用"); expect(leftoverAt).toBeGreaterThan(-1); expect(text.slice(leftoverAt)).not.toContain("美学纲领与交互范式"); }); it("listCallableCatalogModules skips scheduled names and filled non-repeatable artifacts", () => { const flow = parseCreationFlow(`{ "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "生成规则", "role": "prototype", "depends_on": ["美学纲领与交互范式"] } ] }`)!; const leftover = listCallableCatalogModules({ flow, catalog: sampleCatalog, filledArtifactTags: ["设计.实现机制"], }); expect(leftover.map((m) => m.name)).toEqual([ "具体实例", "开场白与开场变量", ]); }); it("tells design-flow to insert nodes before an unaccepted closer", () => { const catalog = parseModuleCatalog(` modules: - name: 美学纲领与交互范式 declaration: 站位 artifact: 设计.美学纲领与交互范式 - name: 开场白与开场变量 declaration: 收口 artifact: 设计.开场白与开场变量 closer: true `)!; const flow = parseCreationFlow(`{ "status": "closed", "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式"] } ] }`)!; const text = formatFlowProgressForAgent({ flow, acceptedStepIds: ["美学纲领与交互范式"], catalog, }); expect(text).toContain("〔收口〕"); expect(text).toContain("开场白与开场变量"); expect(text).toContain("插在收口之前"); expect(text).toContain("status 改回 open"); }); it("injects aesthetics-interaction module prompt into design-step", async () => { const flowRaw = JSON.stringify({ steps: [{ name: "美学纲领与交互范式", depends_on: [] }], }); const loaded = await loadWorkerSkillWithContext( "world-simulator", "design-step", undefined, { flowRaw, currentStepName: "美学纲领与交互范式", acceptedStepNames: [], }, ); expect(loaded.worker.outputTags).toContain("设计.美学纲领与交互范式"); expect(loaded.worker.outputTags).not.toContain("创作.当前步骤"); expect(loaded.worker.inputTags).toContain("创作.当前步骤"); expect(loaded.promptBody).toContain("美学纲领与交互范式"); expect(loaded.promptBody).toContain("体验契约"); expect(loaded.promptBody).toContain("程序开场"); expect(loaded.promptBody).toContain("【本步参数】"); expect(loaded.worker.inputTags).toContain("创作.能力开场白"); }); it("extracts ```opening default question from module prompt", async () => { const prompt = await loadModulePrompt( "dialogue/world-simulator", "aesthetics-interaction", ); expect(prompt).toBeTruthy(); const opening = extractModuleOpening(prompt!); expect(opening).toContain("反复获得什么体验"); expect(opening).toContain("站在什么位置"); expect(opening).not.toContain("nail清站位"); const { parseModulePromptSections, formatModulePromptForLlm, MODULE_SECTION_IDS, } = await import("../src/skills/creation-flow.js"); const sections = parseModulePromptSections(prompt!); for (const id of ["meta", "opening", "task", "output", "checklist"] as const) { expect(sections.blocks[id]?.length).toBeGreaterThan(0); } expect(MODULE_SECTION_IDS).toContain("opening"); const forLlm = formatModulePromptForLlm(prompt!); expect(forLlm).toContain("```task"); expect(forLlm).not.toContain("原型世界是什么"); expect( extractModuleOpening("## opening\n\n```opening\nhello world\n```\n"), ).toBe("hello world"); expect(parseModuleOpeningState('{"美学纲领与交互范式":"shown"}')).toEqual({ 美学纲领与交互范式: "shown", }); expect(isProgressPointerTag("创作.当前步骤")).toBe(true); expect(isProgressPointerTag("设计.美学纲领与交互范式")).toBe(false); expect(isProgressPointerTag("设计.创作流程")).toBe(false); const binding = await resolveDesignStepBinding({ skillPackRoot: "dialogue/world-simulator", flowRaw: JSON.stringify({ steps: [{ name: "美学纲领与交互范式", depends_on: [] }], }), currentStepName: "美学纲领与交互范式", }); expect(binding?.opening).toContain("反复获得什么体验"); expect(binding?.modulePrompt).toContain("```task"); expect(binding?.modulePrompt).not.toContain("最想反复感受到的是什么"); }); it("loads the migrated world-blueprint contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find((item) => item.id === "world-blueprint"); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator"]); const prompt = await loadModulePrompt( "dialogue/world-simulator", "world-blueprint", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toContain("稳定反馈"); expect(sections.blocks.output).toContain('"核心运转"'); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.output).not.toContain('"追问"'); expect(sections.blocks.score).toContain("支撑度"); expect(sections.blocks.score).toContain("克制度"); expect(sections.blocks.score).not.toContain("0–10"); }); it("loads the migrated generation-rules contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find((item) => item.id === "generation-rules"); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator", "auditor"]); expect(module?.repeatable).toBe(true); const prompt = await loadModulePrompt( "dialogue/world-simulator", "generation-rules", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.meta).toContain("两份或以上"); expect(sections.blocks.task).toContain("填写工作流"); expect(sections.blocks.output).toContain('"字段"'); expect(sections.blocks.output).toContain('"填写工作流"'); expect(sections.blocks.output).toContain('"依赖关系"'); expect(sections.blocks.output).not.toContain('"rules"'); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.score).toContain("字段妥当"); expect(sections.blocks.score).toContain("池与提示"); expect(sections.blocks.score).toContain("填写工作流"); }); it("loads the migrated concrete-instances contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find( (item) => item.id === "concrete-instances", ); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator"]); expect(module?.repeatable).toBe(true); const prompt = await loadModulePrompt( "dialogue/world-simulator", "concrete-instances", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.meta).toContain("字段很少"); expect(sections.blocks.task).toContain("一份产物只含一条记录"); expect(sections.blocks.output).toContain('"名称"'); expect(sections.blocks.output).toContain('"记录"'); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.score).toContain("工作流符合度"); expect(sections.blocks.score).not.toContain("充实度"); }); it("loads the migrated narrative-guide contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find((item) => item.id === "narrative-guide"); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["narrator", "world-simulator"]); expect(module?.libraries).toEqual(["style-packs"]); const prompt = await loadModulePrompt( "dialogue/world-simulator", "narrative-guide", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toContain("read_library_entry"); expect(sections.blocks.task).toContain("风格推荐"); expect(sections.blocks.output).toContain('"选用文风"'); expect(sections.blocks.output).toContain('"写法要求"'); expect(sections.blocks.output).not.toContain('"示范"'); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.score).toContain("遣词可执行"); expect(sections.blocks.score).toContain("文风选用"); expect(sections.blocks.score).toContain("本局化"); }); it("loads the migrated story-progression contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find( (item) => item.id === "story-progression", ); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator", "narrator"]); expect(module?.libraries).toEqual(["preferences"]); const prompt = await loadModulePrompt( "dialogue/world-simulator", "story-progression", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toContain("只选定一种"); expect(sections.blocks.task).toContain("read_library_entry"); expect(sections.blocks.output).toContain('"用户输入用法"'); expect(sections.blocks.output).toContain('"扩写要求"'); expect(sections.blocks.output).toContain('"续写要求"'); expect(sections.blocks.output).toContain('"单轮篇幅"'); expect(sections.blocks.output).toContain('"目标字数"'); expect(sections.blocks.output).toContain('"扩写占比"'); expect(sections.blocks.output).toContain('"续写占比"'); expect(sections.blocks.output).toContain('"OOC处理"'); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.score).toContain("唯一性与切分"); expect(sections.blocks.score).toContain("可执行性"); expect(sections.blocks.score).toContain("偏好本局化"); }); it("loads the migrated mechanism contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find((item) => item.id === "mechanism"); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator"]); const prompt = await loadModulePrompt( "dialogue/world-simulator", "mechanism", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toContain("多条共同作用"); expect(sections.blocks.task).toContain("缺失后果"); expect(sections.blocks.output).toContain('"既成机制"'); expect(sections.blocks.output).toContain('"因果影响"'); expect(sections.blocks.output).toContain('"推演口径"'); expect(sections.blocks.output).toContain('"共同作用"'); expect(sections.blocks.output).not.toContain('"缺失后果"'); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.score).toContain("骨架必要度"); expect(sections.blocks.score).toContain("因果清晰度"); expect(sections.blocks.score).toContain("推演开放度"); expect(sections.blocks.examples).toContain("灵气复苏"); expect(sections.blocks.examples).toContain("全民直播求生"); }); it("loads the migrated topology contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find((item) => item.id === "topology"); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator", "auditor"]); expect(module?.repeatable).toBe(true); const prompt = await loadModulePrompt( "dialogue/world-simulator", "topology", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toContain("一张图"); expect(sections.blocks.task).toContain("static"); expect(sections.blocks.task).toContain("record"); expect(sections.blocks.task).toContain("expand"); expect(sections.blocks.output).toContain('"节点字段"'); expect(sections.blocks.output).toContain('"连接字段"'); expect(sections.blocks.output).toContain('"初始图"'); expect(sections.blocks.output).toContain('"维护规则"'); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.output).not.toContain("SVG"); expect(sections.blocks.score).toContain("对象适配"); expect(sections.blocks.score).toContain("字段合同"); expect(sections.blocks.score).toContain("初始图有效性"); expect(sections.blocks.score).toContain("维护可执行性"); expect(sections.blocks.score).toContain("长期一致性"); }); it("loads the migrated variable-design contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find((item) => item.id === "variable-design"); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator", "auditor"]); const prompt = await loadModulePrompt( "dialogue/world-simulator", "variable-design", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toContain("deterministic"); expect(sections.blocks.task).toContain("narrative_inference"); expect(sections.blocks.task).toContain("inline_tool"); expect(sections.blocks.task).toContain("auditor_bundle"); expect(sections.blocks.output).toContain('"允许写入者"'); expect(sections.blocks.output).toContain('"更新规则"'); expect(sections.blocks.output).toContain('"派生映射"'); expect(sections.blocks.output).toContain('"触发规则"'); expect(sections.blocks.output).toContain('"维护复杂度"'); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.output).not.toContain("maintain.v1"); expect(sections.blocks.output).not.toContain("隐藏段"); expect(sections.blocks.score).toContain("真值必要性"); expect(sections.blocks.score).toContain("字段妥当性"); expect(sections.blocks.score).toContain("更新可执行性"); expect(sections.blocks.score).toContain("权限与公开性"); expect(sections.blocks.score).toContain("映射与触发一致性"); }); it("loads the migrated status-bar contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find((item) => item.id === "status-bar"); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator", "narrator"]); const prompt = await loadModulePrompt( "dialogue/world-simulator", "status-bar", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toContain("语义区域"); expect(sections.blocks.task).toContain("inline"); expect(sections.blocks.output).toContain('"语义区域"'); expect(sections.blocks.output).toContain("program_projection"); expect(sections.blocks.output).toContain("maintenance_channel"); expect(sections.blocks.output).toContain("state_ops"); expect(sections.blocks.output).not.toContain('"启用"'); expect(sections.blocks.output).not.toContain("1000~2000"); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.score).toContain("区域必要性"); expect(sections.blocks.score).toContain("来源一致性"); expect(sections.blocks.score).toContain("内容可执行性"); expect(sections.blocks.score).toContain("信息负担"); expect(sections.blocks.score).toContain("职责边界"); }); it("loads the migrated random-range contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find((item) => item.id === "random-range"); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator"]); const prompt = await loadModulePrompt( "dialogue/world-simulator", "random-range", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toContain("名称.随机数"); expect(sections.blocks.task).toContain("只剩 1 个"); expect(sections.blocks.output).toContain("敏捷.随机数"); expect(sections.blocks.output).not.toContain("票 id"); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.score).toContain("覆盖度"); expect(sections.blocks.score).toContain("克制"); }); it("loads the migrated reply-format contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find((item) => item.id === "reply-format"); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator", "narrator"]); const prompt = await loadModulePrompt( "dialogue/world-simulator", "reply-format", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toContain("普通楼层"); expect(sections.blocks.task).toContain("present.onData"); expect(sections.blocks.output).toContain('"数据契约"'); expect(sections.blocks.output).toContain('"区域落点"'); expect(sections.blocks.output).toContain('"frontend"'); expect(sections.blocks.output).toContain('"html"'); expect(sections.blocks.output).toContain('"css"'); expect(sections.blocks.output).toContain('"js"'); expect(sections.blocks.output).toContain('"示例灌数"'); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.score).toContain("区域落实"); expect(sections.blocks.score).toContain("权威绑定"); expect(sections.blocks.score).toContain("实现完整"); expect(sections.blocks.score).toContain("本局化"); }); it("loads the migrated opening-setup contract", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const module = catalog?.modules.find((item) => item.id === "opening-setup"); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator", "narrator"]); expect(module?.closer).toBe(true); expect(module?.repeatable).toBe(true); const prompt = await loadModulePrompt( "dialogue/world-simulator", "opening-setup", ); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toContain("默认只生成 1 条"); expect(sections.blocks.task).toContain("@玩家"); expect(sections.blocks.task).toContain("随机"); expect(module?.artifact).toBe("设计.开场白"); expect(sections.blocks.task).toContain("设计.开场白#opening-cycle2"); expect(sections.blocks.task).not.toContain("写入 `设计.开场白与开场变量`"); expect(sections.blocks.output).toContain("设计.开场白#场景短码"); expect(sections.blocks.output).toContain('"开场变量"'); expect(sections.blocks.output).not.toContain('"开场白候选"'); expect(sections.blocks.output).not.toContain('"开场白全文"'); expect(sections.blocks.output).toContain("present.v1"); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.score).toContain("开场必要"); expect(sections.blocks.score).toContain("前端符合"); expect(sections.blocks.score).toContain("状态自洽"); expect(sections.blocks.score).toContain("角色占位"); expect(sections.blocks.score).toContain("收口可切换"); }); it("loads the three zero-layer presentation contracts", async () => { const catalog = await loadModuleCatalog("dialogue/world-simulator"); const ids = [ "zero-layer-status", "zero-layer-reply-format", "zero-layer-opening-setup", ]; for (const id of ids) { const module = catalog?.modules.find((item) => item.id === id); expect(module?.layer).toBe("final"); expect(module?.mount).toEqual(["world-simulator", "narrator"]); const prompt = await loadModulePrompt("dialogue/world-simulator", id); const { parseModulePromptSections } = await import( "../src/skills/creation-flow.js" ); const sections = parseModulePromptSections(prompt!); expect(sections.blocks.task).toBeTruthy(); expect(sections.blocks.output).not.toContain('"自评"'); expect(sections.blocks.score).toBeTruthy(); } expect( catalog?.modules.find((item) => item.id === "zero-layer-opening-setup") ?.closer, ).toBe(true); expect( catalog?.modules.find((item) => item.id === "zero-layer-opening-setup") ?.repeatable, ).toBe(true); const formatPrompt = await loadModulePrompt( "dialogue/world-simulator", "zero-layer-reply-format", ); expect(formatPrompt).toContain("zero-layer.update.v1"); expect(formatPrompt).toContain("slot_updates"); expect(formatPrompt).toContain("base_revision"); const openingPrompt = await loadModulePrompt( "dialogue/world-simulator", "zero-layer-opening-setup", ); expect(openingPrompt).toContain("zero-layer.card.v1"); expect(openingPrompt).toContain("@玩家"); expect(openingPrompt).toContain("不输出任何 `名称.随机数`"); }); it("parses and validates step params from catalog", async () => { const flow = parseCreationFlow(`{ "status": "open", "steps": [ { "id": "生成规则·怪物", "name": "生成规则", "params": { "target": "怪物", "lifecycle_intent": "runtime_only" }, "depends_on": [] } ] }`)!; expect(flow.steps[0]?.params).toEqual({ target: "怪物", lifecycle_intent: "runtime_only", }); const view = formatCreationFlowForUser(flow); expect(view.steps[0]?.params?.target).toBe("怪物"); const catalog = await loadModuleCatalog("dialogue/world-simulator"); const rules = catalog?.modules.find((m) => m.name === "生成规则"); expect(rules?.kind).toBe("prior-artifact"); expect(rules?.params?.some((p) => p.key === "target")).toBe(true); expect(validateCreationFlow(flow, catalog).ok).toBe(true); const missing = parseCreationFlow(`{ "steps": [{ "name": "生成规则", "depends_on": [] }] }`)!; // 先验产物:编排期允许空壳,确认开干也不拦 expect(validateCreationFlow(missing, catalog).ok).toBe(true); const stepReady = validateStepReadyToRun(missing.steps[0]!, catalog); expect(stepReady.ok).toBe(true); const missingView = formatCreationFlowForUser(missing, catalog); expect(missingView.steps[0]?.kind).toBe("prior-artifact"); expect(missingView.steps[0]?.paramsMissing).toBeUndefined(); const block = formatModuleCatalogForAgent(catalog!); expect(block).toContain("role=prototype"); expect(block).toContain("target"); }); it("injects step params into design-step prompt", async () => { const flowRaw = JSON.stringify({ steps: [ { id: "生成规则·怪物", name: "生成规则", params: { target: "怪物", rule_id: "monsters" }, depends_on: [], }, ], }); const loaded = await loadWorkerSkillWithContext( "world-simulator", "design-step", undefined, { flowRaw, currentStepName: "生成规则·怪物", acceptedStepNames: [], }, ); expect(loaded.promptBody).toContain("【本步对象】"); expect(loaded.promptBody).toContain("target: 怪物"); expect(loaded.promptBody).toContain("rule_id: monsters"); expect(loaded.worker.outputTags).toEqual(["设计.生成规则#monsters"]); }); it("lands each rule, instance, and opening on its own tag", () => { const catalog = parseModuleCatalog(` modules: - id: generation-rules name: 生成规则 declaration: 一条规则一份产物 artifact: 设计.生成规则 repeatable: true - id: concrete-instances name: 具体实例 declaration: 一条记录一份产物 artifact: 设计.具体实例 repeatable: true - id: opening-setup name: 开场白与开场变量 declaration: 一条开场一份产物 artifact: 设计.开场白与开场变量 repeatable: true closer: true - id: aesthetics-interaction name: 美学纲领与交互范式 declaration: 一次写完 artifact: 设计.美学纲领与交互范式 `)!; const rule = catalog.modules.find((m) => m.id === "generation-rules")!; const row = catalog.modules.find((m) => m.id === "concrete-instances")!; const opening = catalog.modules.find((m) => m.id === "opening-setup")!; const once = catalog.modules.find((m) => m.id === "aesthetics-interaction")!; expect( repeatableInstanceProductTag({ module: rule, step: { id: "生成规则·怪物", role: "instance", params: { rule_id: "女租客" } }, }), ).toBe("设计.生成规则#女租客"); expect( repeatableInstanceProductTag({ module: row, step: { id: "具体实例#1", role: "instance", params: { batch_goal: "雷樱" } }, }), ).toBe("设计.具体实例#1"); expect( repeatableInstanceProductTag({ module: row, step: { id: "具体实例·雷樱", role: "instance", params: { batch_goal: "雷樱" } }, }), ).toBe("设计.具体实例#雷樱"); expect( repeatableInstanceProductTag({ module: opening, step: { id: "开场白与开场变量#dorm", role: "instance" }, }), ).toBe("设计.开场白#dorm"); expect( repeatableInstanceProductTag({ module: once, step: { id: "美学纲领与交互范式", role: "instance" }, }), ).toBeNull(); }); it("injects empty prior-artifact plan and still lets the step run", async () => { const catalog = parseModuleCatalog(` modules: - name: 生成规则 declaration: 可执行规则 artifact: 设计.生成规则 kind: prior-artifact params: - key: target label: 生成对象 required: true `)!; const empty = parseCreationFlow(`{ "steps": [{ "name": "生成规则", "depends_on": [] }] }`)!; expect(catalog.modules[0]?.kind).toBe("prior-artifact"); expect(validateStepReadyToRun(empty.steps[0]!, catalog).ok).toBe(true); const loaded = await loadWorkerSkillWithContext( "world-simulator", "design-step", undefined, { flowRaw: JSON.stringify({ steps: [ { id: "生成规则#1", name: "生成规则", role: "instance", from: "生成规则", depends_on: [], }, ], }), currentStepName: "生成规则#1", acceptedStepNames: [], }, ); expect(loaded.promptBody).toContain("【本步对象】"); expect(loaded.promptBody).toContain("尚未钉本步具体写什么"); }); it("nextPendingStep respects deps and accepted by id", () => { const flow = parseCreationFlow(`{ "status": "open", "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "生成规则", "depends_on": ["美学纲领与交互范式"] }, { "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] } ] }`)!; expect(nextPendingStep(flow, [])?.id).toBe("美学纲领与交互范式"); expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.id).toBe("生成规则"); expect(nextPendingStep(flow, ["美学纲领与交互范式", "生成规则"])?.id).toBe( "生成规则#2", ); expect( nextPendingStep(flow, ["美学纲领与交互范式", "生成规则", "生成规则#2"]), ).toBeNull(); }); it("pickRecordedStepId ignores LLM junk on 创作.当前步骤", () => { const flow = parseCreationFlow(`{ "status": "open", "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "生成规则", "depends_on": ["美学纲领与交互范式"] } ] }`)!; expect( pickRecordedStepId({ flow, alreadyAccepted: [], pinnedUnitId: "美学纲领与交互范式", writtenCurrentStep: JSON.stringify({ schema: "context-fragment.v1", 正文: { 交互范式: {}, 美学纲领: {} }, }), }), ).toBe("美学纲领与交互范式"); expect( pickRecordedStepId({ flow, alreadyAccepted: [], pinnedUnitId: "flow", writtenCurrentStep: "交互范式", }), ).toBe("美学纲领与交互范式"); expect( pickRecordedStepId({ flow, alreadyAccepted: ["美学纲领与交互范式"], pinnedUnitId: "", writtenCurrentStep: "", }), ).toBe("生成规则"); }); it("open flow needs expansion after listed steps accepted", () => { const flow = parseCreationFlow(`{ "status": "open", "steps": [{ "name": "美学纲领与交互范式", "depends_on": [] }] }`)!; expect(isCreationFlowComplete(flow, ["美学纲领与交互范式"])).toBe(false); expect(needsFlowExpansion(flow, ["美学纲领与交互范式"])).toBe(true); const closed = parseCreationFlow(`{ "status": "closed", "steps": [{ "name": "美学纲领与交互范式", "depends_on": [] }] }`)!; expect(isCreationFlowComplete(closed, ["美学纲领与交互范式"])).toBe(true); expect(needsFlowExpansion(closed, ["美学纲领与交互范式"])).toBe(false); const legacy = parseCreationFlow(`{ "steps": [{ "name": "美学纲领与交互范式", "depends_on": [] }] }`)!; expect(isCreationFlowComplete(legacy, ["美学纲领与交互范式"])).toBe(true); }); it("parses legacy revise steps then strips them from the user graph", () => { const flow = parseCreationFlow(`{ "status": "open", "steps": [ { "name": "实现机制", "depends_on": [] }, { "name": "实现机制", "mode": "回头修改", "depends_on": ["实现机制"] } ] }`)!; expect(flow.steps[1]).toMatchObject({ id: "实现机制·改", name: "实现机制", mode: "revise", revises: "实现机制", }); expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true); const stripped = stripReviseSteps(flow); expect(stripped.steps.map((s) => s.id)).toEqual(["实现机制"]); const view = formatCreationFlowForUser(flow, sampleCatalog, ["实现机制"]); expect(view.steps).toHaveLength(1); expect(view.steps[0]?.id).toBe("实现机制"); expect(view.steps[0]?.runState).toBe("done"); expect(view.steps[0]?.mode).toBeUndefined(); }); it("stripReviseSteps remaps dependents back to the origin step", () => { const flow = parseCreationFlow(`{ "steps": [ { "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }, { "id": "美学纲领与交互范式·改", "name": "美学纲领与交互范式", "mode": "revise", "revises": "美学纲领与交互范式", "depends_on": ["美学纲领与交互范式"] }, { "id": "正文组成", "name": "实现机制", "depends_on": ["美学纲领与交互范式·改"] } ] }`)!; const stripped = stripReviseSteps(flow); expect(stripped.steps.map((s) => s.id)).toEqual([ "美学纲领与交互范式", "正文组成", ]); expect(stripped.steps[1]?.depends_on).toEqual(["美学纲领与交互范式"]); }); it("still rejects two fresh copies of a non-repeatable skill", () => { const flow = parseCreationFlow(`{ "steps": [ { "name": "实现机制", "depends_on": [] }, { "id": "实现机制#2", "name": "实现机制", "depends_on": ["实现机制"] } ] }`)!; const result = validateCreationFlow(flow, sampleCatalog); expect(result.ok).toBe(false); expect(result.errors.some((e) => e.includes("repeatable"))).toBe(true); }); it("treats a second generation-rules step as fresh, not revise", () => { const flow = parseCreationFlow(`{ "steps": [ { "id": "生成规则·怪物", "name": "生成规则", "params": { "target": "怪物" }, "depends_on": [] }, { "id": "生成规则·NPC", "name": "生成规则", "params": { "target": "NPC" }, "depends_on": ["生成规则·怪物"] } ] }`)!; expect(flow.steps[1]?.mode).toBeUndefined(); expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true); }); it("revise of generation-rules inherits params from the origin step", () => { const flow = parseCreationFlow(`{ "steps": [ { "id": "生成规则·怪物", "name": "生成规则", "params": { "target": "怪物", "rule_id": "monsters" }, "depends_on": [] }, { "id": "生成规则·怪物·改", "name": "生成规则", "mode": "revise", "revises": "生成规则·怪物", "depends_on": ["生成规则·怪物"] } ] }`)!; expect(flow.steps[1]?.params).toEqual({ target: "怪物", rule_id: "monsters", }); expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true); }); it("nextPendingStep ignores orchestrated revise steps", () => { const flow = parseCreationFlow(`{ "status": "open", "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "生成规则", "depends_on": ["美学纲领与交互范式"] }, { "name": "美学纲领与交互范式", "mode": "revise", "revises": "美学纲领与交互范式", "depends_on": ["美学纲领与交互范式"] } ] }`)!; expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.mode).toBeUndefined(); expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.id).toBe("生成规则"); expect( listReadySteps(flow, ["美学纲领与交互范式"]).map((s) => s.id), ).toEqual(["生成规则"]); }); it("inherits existing artifact when reentering an accepted step", async () => { const flowRaw = JSON.stringify({ status: "open", steps: [ { id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [] }, ], }); const binding = await resolveDesignStepBinding({ skillPackRoot: "dialogue/world-simulator", flowRaw, currentStepName: "美学纲领与交互范式", acceptedStepNames: ["美学纲领与交互范式"], inheritExisting: true, }); expect(binding?.opening).toBeNull(); expect(binding?.inheritTag).toBe("设计.美学纲领与交互范式"); const loaded = await loadWorkerSkillWithContext( "world-simulator", "design-step", undefined, { flowRaw, currentStepName: "美学纲领与交互范式", acceptedStepNames: ["美学纲领与交互范式"], inheritExisting: true, }, ); expect(loaded.promptBody).toContain("【回头修改】"); expect(loaded.promptBody).not.toContain("程序开场"); expect(loaded.worker.name).toContain("回头修改"); expect(loaded.worker.inputTags).toContain("设计.美学纲领与交互范式"); }); it("layers follow longest dependency path", () => { const flow = parseCreationFlow(`{ "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "生成规则", "depends_on": ["美学纲领与交互范式"] }, { "name": "具体实例", "depends_on": ["生成规则"] }, { "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式", "具体实例"] } ] }`)!; const layers = computeStepLayers(flow); expect(layers.get("美学纲领与交互范式")).toBe(0); expect(layers.get("生成规则")).toBe(1); expect(layers.get("具体实例")).toBe(2); expect(layers.get("开场白与开场变量")).toBe(3); }); it("listReadySteps returns every unblocked pending step", () => { const flow = parseCreationFlow(`{ "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "生成规则", "depends_on": ["美学纲领与交互范式"] }, { "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式"] } ] }`)!; expect(listReadySteps(flow, []).map((s) => s.id)).toEqual([ "美学纲领与交互范式", ]); expect( listReadySteps(flow, ["美学纲领与交互范式"]).map((s) => s.id), ).toEqual(["生成规则", "实现机制"]); }); it("spawns instance from prototype before closer and hangs instance on closer deps", () => { const flow = parseCreationFlow(`{ "status": "open", "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式"] } ] }`)!; expect( hasSelectableCreationWork(flow, sampleCatalog, ["美学纲领与交互范式"]), ).toBe(true); const spawned = spawnRepeatableCreationStep({ flow, catalog: sampleCatalog, moduleName: "生成规则", acceptedStepIds: ["美学纲领与交互范式"], }); expect("error" in spawned).toBe(false); if ("error" in spawned) return; expect(spawned.step.id).toBe("生成规则#1"); expect(spawned.step.role).toBe("instance"); expect(spawned.step.depends_on).toEqual(["美学纲领与交互范式"]); expect(spawned.flow.steps.map((s) => s.id)).toEqual([ "美学纲领与交互范式", "生成规则", "生成规则#1", "开场白与开场变量", ]); expect( spawned.flow.steps.find((s) => s.name === "开场白与开场变量")?.depends_on, ).toEqual(["美学纲领与交互范式", "生成规则#1"]); const second = spawnRepeatableCreationStep({ flow: spawned.flow, catalog: sampleCatalog, moduleName: "生成规则", acceptedStepIds: ["美学纲领与交互范式"], }); expect("error" in second).toBe(false); if ("error" in second) return; expect(second.step.id).toBe("生成规则#2"); }); it("spawns an opening instance from its own prototype without depending on itself", () => { const catalog = parseModuleCatalog(` modules: - name: 开场白与开场变量 declaration: 可增殖收口 artifact: 设计.开场白与开场变量 closer: true repeatable: true `)!; const flow = parseCreationFlow(`{ "status": "open", "steps": [ { "id": "开场白与开场变量", "name": "开场白与开场变量", "role": "prototype", "depends_on": [] } ] }`)!; const spawned = spawnInstanceFromPrototype({ flow, catalog, prototypeId: "开场白与开场变量", acceptedStepIds: [], }); expect("error" in spawned).toBe(false); if ("error" in spawned) return; expect(spawned.step.role).toBe("instance"); expect(spawned.step.from).toBe("开场白与开场变量"); expect(spawned.step.depends_on).toEqual([]); const proto = spawned.flow.steps.find((s) => s.role === "prototype"); expect(proto?.depends_on).toEqual([]); expect(spawned.flow.steps.map((s) => s.id)).toEqual([ "开场白与开场变量", "开场白与开场变量#1", ]); }); it("removeUnstartedInstance drops empty spawn and closer dep", () => { const flow = parseCreationFlow(`{ "status": "open", "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "id": "生成规则", "name": "生成规则", "role": "prototype", "depends_on": ["美学纲领与交互范式"] }, { "id": "生成规则#1", "name": "生成规则", "role": "instance", "from": "生成规则", "depends_on": ["美学纲领与交互范式"] }, { "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式", "生成规则#1"] } ] }`)!; const next = removeUnstartedInstance(flow, "生成规则#1", []); expect(next.steps.map((s) => s.id)).toEqual([ "美学纲领与交互范式", "生成规则", "开场白与开场变量", ]); expect( next.steps.find((s) => s.name === "开场白与开场变量")?.depends_on, ).toEqual(["美学纲领与交互范式"]); const kept = removeUnstartedInstance(next, "美学纲领与交互范式", []); expect(kept.steps.map((s) => s.id)).toEqual(next.steps.map((s) => s.id)); const accepted = removeUnstartedInstance(flow, "生成规则#1", ["生成规则#1"]); expect(accepted.steps.some((s) => s.id === "生成规则#1")).toBe(true); }); it("removeRepeatableInstance drops accepted spawn and closer dep", () => { const flow = parseCreationFlow(`{ "status": "open", "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "id": "生成规则", "name": "生成规则", "role": "prototype", "depends_on": ["美学纲领与交互范式"] }, { "id": "生成规则#1", "name": "生成规则", "role": "instance", "from": "生成规则", "depends_on": ["美学纲领与交互范式"], "params": { "rule_id": "core-female-roles" } }, { "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式", "生成规则#1"] } ] }`)!; const dropped = removeRepeatableInstance({ flow, stepId: "生成规则#1", catalog: sampleCatalog, }); expect("error" in dropped).toBe(false); if ("error" in dropped) return; expect(dropped.flow.steps.map((s) => s.id)).toEqual([ "美学纲领与交互范式", "生成规则", "开场白与开场变量", ]); expect( dropped.flow.steps.find((s) => s.name === "开场白与开场变量")?.depends_on, ).toEqual(["美学纲领与交互范式"]); }); it("removeRepeatableInstance blocks when another instance depends on it", () => { const flow = parseCreationFlow(`{ "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "id": "生成规则", "name": "生成规则", "role": "prototype", "depends_on": ["美学纲领与交互范式"] }, { "id": "生成规则#1", "name": "生成规则", "role": "instance", "from": "生成规则", "depends_on": ["美学纲领与交互范式"], "params": { "target": "4位核心女性", "rule_id": "core-female-roles" } }, { "id": "具体实例", "name": "具体实例", "role": "prototype", "depends_on": ["生成规则"] }, { "id": "具体实例#1", "name": "具体实例", "role": "instance", "from": "具体实例", "depends_on": ["生成规则#1"] } ] }`)!; const blocked = removeRepeatableInstance({ flow, stepId: "生成规则#1", catalog: sampleCatalog, }); expect(blocked).toMatchObject({ error: expect.stringContaining("具体实例") }); const ok = removeRepeatableInstance({ flow, stepId: "具体实例#1", catalog: sampleCatalog, }); expect("error" in ok).toBe(false); }); it("pruneRepeatableArtifactContent strips the deleted rule_id", () => { const raw = JSON.stringify({ schema: "context-fragment.v1", 技能: "生成规则", brief: "两条规则", 正文: { rules: [ { rule_id: "core-female-roles", 对象: "女臣" }, { rule_id: "zombies", 对象: "丧尸" }, ], }, }); const step = { id: "生成规则#1", name: "生成规则", role: "instance" as const, depends_on: [], params: { rule_id: "core-female-roles" }, }; const pruned = JSON.parse( pruneRepeatableArtifactContent({ raw, step, remainingSameModule: 1, }), ); expect(pruned.正文.rules.map((r) => r.rule_id)).toEqual(["zombies"]); expect( pruneRepeatableArtifactContent({ raw, step, remainingSameModule: 0, }), ).toBe(""); }); it("blocks 具体实例 spawn until a 生成规则 instance is accepted", () => { const flow = parseCreationFlow(`{ "steps": [ { "name": "美学纲领与交互范式", "depends_on": [] }, { "id": "生成规则", "name": "生成规则", "role": "prototype", "depends_on": ["美学纲领与交互范式"] } ] }`)!; const blocked = spawnRepeatableCreationStep({ flow, catalog: sampleCatalog, moduleName: "具体实例", acceptedStepIds: ["美学纲领与交互范式"], }); expect(blocked).toMatchObject({ error: "先完成一条生成规则" }); const ruleSpawn = spawnInstanceFromPrototype({ flow, catalog: sampleCatalog, prototypeId: "生成规则", acceptedStepIds: ["美学纲领与交互范式"], }); expect("error" in ruleSpawn).toBe(false); if ("error" in ruleSpawn) return; const withRule = ruleSpawn.flow; const ok = spawnRepeatableCreationStep({ flow: withRule, catalog: sampleCatalog, moduleName: "具体实例", acceptedStepIds: ["美学纲领与交互范式", "生成规则#1"], }); expect("error" in ok).toBe(false); if ("error" in ok) return; expect(ok.step.depends_on).toEqual(["生成规则#1"]); }); it("detects accepted closer step", () => { const flow = parseCreationFlow( JSON.stringify({ steps: [ { id: "开场白与开场变量", name: "开场白与开场变量", depends_on: [], }, ], }), ); expect(hasAcceptedCloserStep(flow, [])).toBe(false); expect(hasAcceptedCloserStep(flow, ["开场白与开场变量"])).toBe(true); expect(hasAcceptedCloserStep(null, ["开场白与开场变量"])).toBe(true); }); });