import { describe, expect, it } from "vitest"; import { expandPlaySlotsToWorkers, mergeWorkersWithPlaySlots, onDemandRefsFromPlaySlots, parsePlaySlots, refsFromPlaySlots, } from "../src/skills/play-slots.js"; import { deriveOnDemandWorkerScope, deriveRunWorkerScope, parseWorkerSetYaml, } from "../src/skills/worker-set-parse.js"; import { parseSettlementPacket, settlementChangesToTablePatch, } from "../src/skills/settlement-packet.js"; import { emptyMaintainPacket, isMaintainPacketEmpty, maintainOpsToTablePatch, parseMaintainPacket, } from "../src/skills/maintain-packet.js"; import { createTableFromValues, mergeTableCells, } from "../src/blackboard/table-cells.js"; import { executeChance, parseChanceRequest, } from "../src/skills/chance-tools.js"; import { synthesizeContextOrderFromWorkers } from "../src/skills/context-order.js"; import { DIALOGUE_HISTORY_TAG } from "../src/skills/dialogue-history.js"; describe("play_slots", () => { it("expands default auditor+gm+narrator", () => { const slots = parsePlaySlots({ gm: true, narrator: true, perspective: false, })!; expect(slots.auditor).toBe(true); expect(refsFromPlaySlots(slots)).toEqual([ "auditor", "world-simulator", "narrator", ]); const workers = expandPlaySlotsToWorkers(slots); expect(workers.map((w) => w.ref)).toEqual([ "auditor", "world-simulator", "narrator", ]); expect(workers[0].acceptance).toBe("continue"); expect(workers[1].acceptance).toBe("continue"); expect(workers[2].acceptance).toBe("review"); }); it("orders auditor then perspective before gm", () => { const slots = parsePlaySlots({ auditor: true, gm: true, narrator: true, perspective: true, })!; expect(refsFromPlaySlots(slots)).toEqual([ "auditor", "role-decide", "world-simulator", "narrator", ]); }); it("can disable auditor", () => { const slots = parsePlaySlots({ auditor: false, gm: true, narrator: true, perspective: false, })!; expect(refsFromPlaySlots(slots)).toEqual(["world-simulator", "narrator"]); }); it("merges play_slots into worker set JSON", () => { const parsed = parseWorkerSetYaml( JSON.stringify({ form_summary: "恋爱升温", play_slots: { auditor: true, gm: true, narrator: true, perspective: false, }, workers: [], }), ); expect(parsed?.play_slots?.auditor).toBe(true); expect(parsed?.workers).toHaveLength(3); expect(deriveRunWorkerScope(parsed)).toEqual([ "auditor", "world-simulator", "narrator", ]); }); it("preserves custom context on merge", () => { const slots = parsePlaySlots({ auditor: false, gm: true, narrator: true, })!; const merged = mergeWorkersWithPlaySlots( [ { ref: "world-simulator", name: "自定义世界", context: { dynamic: ["变量.当前"] }, outputs: ["运行.本轮.裁决"], }, ], slots, ); expect(merged[0].name).toBe("自定义世界"); expect(merged[0].context?.dynamic).toEqual(["变量.当前"]); expect(merged[1].ref).toBe("narrator"); }); it("chance is on-demand and not in turn pipeline", () => { const slots = parsePlaySlots({ auditor: false, gm: true, narrator: true, perspective: false, chance: true, })!; expect(refsFromPlaySlots(slots)).toEqual(["world-simulator", "narrator"]); expect(onDemandRefsFromPlaySlots(slots)).toEqual(["chance"]); const workers = expandPlaySlotsToWorkers(slots); expect(workers.map((w) => w.ref)).toEqual([ "world-simulator", "narrator", "chance", ]); expect(workers.find((w) => w.ref === "chance")?.invocation).toBe( "on_demand", ); const parsed = parseWorkerSetYaml( JSON.stringify({ play_slots: { auditor: false, gm: true, narrator: true, perspective: false, chance: true, }, workers: [], }), ); expect(deriveRunWorkerScope(parsed)).toEqual([ "world-simulator", "narrator", ]); expect(deriveOnDemandWorkerScope(parsed)).toEqual(["chance"]); }); }); describe("chance tools", () => { it("parses and rolls dice expression", () => { const req = parseChanceRequest({ op: "roll", expression: "2d6+1" }); expect(req?.op).toBe("roll"); const result = executeChance(req!); expect(result.ok).toBe(true); expect(result.detail.total).toBeTypeOf("number"); }); it("compares scores", () => { const result = executeChance({ op: "compare", left: 15, right: 12, mode: "gte", }); expect(result.ok).toBe(true); expect(result.detail.win).toBe(true); }); it("draws from pool", () => { const result = executeChance({ op: "draw", pool: ["甲", "乙", "丙"], count: 2, unique: true, }); expect(result.ok).toBe(true); expect((result.detail.drawn as string[]).length).toBe(2); }); }); describe("settlement packet", () => { it("parses settlement.v1 into sections", () => { const view = parseSettlementPacket( JSON.stringify({ schema: "settlement.v1", player_action: "送画册", resolved: ["林晚收下"], visible_now: "短谢,未邀留下", variable_changes: [{ key: "好感", from: 10, to: 18, delta: 8 }], do_not_say: ["好感数字"], }), ); expect(view.ok).toBe(true); expect(view.sections.map((s) => s.title)).toContain("玩家行动"); expect(view.sections.map((s) => s.title)).toContain("变量变更"); }); it("treats prose as visible_now fallback", () => { const view = parseSettlementPacket("林晚点头收下了画册。"); expect(view.ok).toBe(true); expect(view.packet?.visible_now).toContain("画册"); }); it("merges variable_changes into table patch with delta", () => { const current = createTableFromValues({ 好感: 10 }, "system"); const patch = settlementChangesToTablePatch( [{ key: "好感", delta: 8 }], current, )!; const { doc } = mergeTableCells({ current, patch, actor: "worker:world-simulator", }); expect(doc.rows.find((r) => r.key === "好感")?.value).toBe(18); }); it("merges variable_changes with absolute to", () => { const current = createTableFromValues({ 阶段: "相识" }, "system"); const patch = settlementChangesToTablePatch( [{ key: "阶段", to: "暧昧" }], current, )!; const { doc } = mergeTableCells({ current, patch, actor: "worker:world-simulator", }); expect(doc.rows.find((r) => r.key === "阶段")?.value).toBe("暧昧"); }); }); describe("maintain packet", () => { it("defaults to empty ops", () => { const view = parseMaintainPacket( JSON.stringify({ schema: "maintain.v1", need_generate: false, table_ops: [], notes: [], }), ); expect(view.ok).toBe(true); expect(view.empty).toBe(true); expect(isMaintainPacketEmpty(emptyMaintainPacket())).toBe(true); }); it("accepts noop shorthand", () => { expect(parseMaintainPacket("无需").empty).toBe(true); }); it("applies table_ops delta to 变量.当前", () => { const current = createTableFromValues({ 好感: 5 }, "system"); const view = parseMaintainPacket( JSON.stringify({ schema: "maintain.v1", table_ops: [{ op: "delta", key: "好感", delta: 3 }], }), ); const patch = maintainOpsToTablePatch( view.packet!.table_ops!, current, "变量.当前", )!; const { doc } = mergeTableCells({ current, patch, actor: "worker:auditor", }); expect(doc.rows.find((r) => r.key === "好感")?.value).toBe(8); }); }); describe("context order synth", () => { it("skips dialogue history for auditor", () => { const order = synthesizeContextOrderFromWorkers( [ { ref: "auditor", name: "旁观维护" }, { ref: "world-simulator", name: "主世界层" }, ], parsePlaySlots({ auditor: true, gm: true, narrator: false }), )!; const auditor = order.slots.find((s) => s.ref === "auditor")!; const gm = order.slots.find((s) => s.ref === "world-simulator")!; expect(auditor.inserts.some((i) => i.ref === DIALOGUE_HISTORY_TAG)).toBe( false, ); expect(gm.inserts.some((i) => i.ref === DIALOGUE_HISTORY_TAG)).toBe(true); }); });