引入上下文片段、固定槽与投影排序,并完善机遇裁定与游玩期 UI。
把创作产物收敛为可挂载片段与 play_slots/context_order,同步修订世界模拟器模块与运行时拼装。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
216
tests/context-fragment.test.ts
Normal file
216
tests/context-fragment.test.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseContextFragment,
|
||||
projectFragmentContent,
|
||||
contextFragmentToView,
|
||||
extractFragmentAskSidecar,
|
||||
} from "../src/skills/context-fragment.js";
|
||||
import {
|
||||
parseContextOrder,
|
||||
contextOrderToSegments,
|
||||
slotOrderForRef,
|
||||
applyContextOrderEdit,
|
||||
mergeContextOrderIntoWorkerSetJson,
|
||||
serializeContextOrder,
|
||||
} from "../src/skills/context-order.js";
|
||||
import { assembleWorkerContext } from "../src/skills/context-segments.js";
|
||||
import { Blackboard } from "../src/blackboard/blackboard.js";
|
||||
import { buildDeclaredWorkerSkill } from "../src/skills/declared-worker.js";
|
||||
import { parseWorkerSetYaml } from "../src/skills/worker-set-parse.js";
|
||||
|
||||
describe("context-fragment.v1", () => {
|
||||
it("parses public header and projects summary", () => {
|
||||
const frag = parseContextFragment({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "特权求生紧张",
|
||||
mount: ["world-simulator", "narrator"],
|
||||
稳变: "stable",
|
||||
正文: { 体验: { 内核: "紧张" } },
|
||||
开放问题: [],
|
||||
})!;
|
||||
expect(frag.技能).toBe("美学纲领与交互范式");
|
||||
expect(projectFragmentContent(JSON.stringify(frag), "summary")).toBe(
|
||||
"特权求生紧张",
|
||||
);
|
||||
const view = contextFragmentToView(frag);
|
||||
expect(view.ok).toBe(true);
|
||||
expect(view.sections.some((s) => s.title === "概要")).toBe(true);
|
||||
});
|
||||
|
||||
it("extracts 自评 + 追问 into ask sidecar", () => {
|
||||
const side = extractFragmentAskSidecar({
|
||||
schema: "context-fragment.v1",
|
||||
brief: "孤立免疫",
|
||||
正文: { 美学纲领: { 体验内核: "…" } },
|
||||
自评: {
|
||||
维度: [
|
||||
{ 名: "交互范式", 分数: 70, 说明: "权限尚可" },
|
||||
{ 名: "美学纲领", 分数: 55, 说明: "边界未锁" },
|
||||
],
|
||||
薄弱点: "暴力尺度未定",
|
||||
},
|
||||
追问: {
|
||||
导语: "为贴近质感,还需确认:",
|
||||
题目: [
|
||||
{
|
||||
问: "暴力与血腥如何服务压迫感?",
|
||||
建议选项: ["偶发锋利高光", "常态压抑少见血", "其它"],
|
||||
示例: "远处尖叫,近处只留锈迹",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(side.assessment).toContain("交互范式 70%");
|
||||
expect(side.assessment).toContain("薄弱点:暴力尺度未定");
|
||||
expect(side.assessment).toContain("为贴近质感");
|
||||
expect(side.questions).toHaveLength(1);
|
||||
expect(side.questions[0]!.prompt).toContain("暴力与血腥");
|
||||
expect(side.questions[0]!.prompt).toContain("示例:远处尖叫");
|
||||
expect(side.questions[0]!.options?.map((o) => o.label)).toEqual([
|
||||
"偶发锋利高光",
|
||||
"常态压抑少见血",
|
||||
"其它",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("context-order.v1", () => {
|
||||
it("maps flat order to segments (history after stable)", () => {
|
||||
const doc = parseContextOrder({
|
||||
schema: "context-order.v1",
|
||||
slots: [
|
||||
{
|
||||
ref: "world-simulator",
|
||||
inserts: [
|
||||
{ order: 0, ref: "worker.persona", projection: "fixed" },
|
||||
{ order: 1, ref: "设计.实现机制", projection: "summary" },
|
||||
{ order: 2, ref: "对话.历史", projection: "summary" },
|
||||
{ order: 3, ref: "用户.最新输入", projection: "full" },
|
||||
],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const slot = slotOrderForRef(doc, "world-simulator")!;
|
||||
const segs = contextOrderToSegments({
|
||||
slot,
|
||||
personaText: "中立主世界层",
|
||||
});
|
||||
expect(segs[0].inline).toContain("中立主世界层");
|
||||
expect(segs[1].tags).toEqual(["设计.实现机制"]);
|
||||
expect(segs[2].tags).toEqual(["对话.历史"]);
|
||||
expect(segs[3].tier).toBe("dynamic");
|
||||
});
|
||||
|
||||
it("reorders flat list and merges into worker set JSON", () => {
|
||||
const doc = parseContextOrder({
|
||||
schema: "context-order.v1",
|
||||
slots: [
|
||||
{
|
||||
ref: "narrator",
|
||||
inserts: [
|
||||
{ order: 0, ref: "worker.persona", projection: "fixed" },
|
||||
{ order: 1, ref: "设计.叙事指南", projection: "summary" },
|
||||
{ order: 2, ref: "设计.美学纲领与交互范式", projection: "summary" },
|
||||
{ order: 3, ref: "对话.历史", projection: "summary" },
|
||||
{ order: 4, ref: "运行.本轮.裁决", projection: "full" },
|
||||
],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const moved = applyContextOrderEdit(doc, {
|
||||
action: "move",
|
||||
slotRef: "narrator",
|
||||
index: 2,
|
||||
delta: -1,
|
||||
});
|
||||
expect(moved.slots[0]!.inserts.map((i) => i.ref)).toEqual([
|
||||
"worker.persona",
|
||||
"设计.美学纲领与交互范式",
|
||||
"设计.叙事指南",
|
||||
"对话.历史",
|
||||
"运行.本轮.裁决",
|
||||
]);
|
||||
|
||||
const afterHist = applyContextOrderEdit(moved, {
|
||||
action: "set_anchor",
|
||||
slotRef: "narrator",
|
||||
index: 1,
|
||||
anchor: "post_history",
|
||||
});
|
||||
const refs = afterHist.slots[0]!.inserts.map((i) => i.ref);
|
||||
const histAt = refs.indexOf("对话.历史");
|
||||
expect(refs.indexOf("设计.美学纲领与交互范式")).toBeGreaterThan(histAt);
|
||||
|
||||
const merged = mergeContextOrderIntoWorkerSetJson(
|
||||
JSON.stringify({
|
||||
play_slots: { gm: true, narrator: true, perspective: false },
|
||||
workers: [],
|
||||
}),
|
||||
afterHist,
|
||||
);
|
||||
const again = JSON.parse(merged);
|
||||
expect(again.context_order.schema).toBe("context-order.v1");
|
||||
expect(serializeContextOrder(afterHist)).toContain("对话.历史");
|
||||
});
|
||||
|
||||
it("declared worker prefers context_order segments", () => {
|
||||
const parsed = parseWorkerSetYaml(
|
||||
JSON.stringify({
|
||||
play_slots: { gm: true, narrator: true, perspective: false },
|
||||
context_order: {
|
||||
schema: "context-order.v1",
|
||||
slots: [
|
||||
{
|
||||
ref: "world-simulator",
|
||||
inserts: [
|
||||
{ order: 0, ref: "worker.persona", projection: "fixed" },
|
||||
{ order: 1, ref: "对话.历史", projection: "summary" },
|
||||
{ order: 2, ref: "用户.最新输入", projection: "full" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
workers: [],
|
||||
}),
|
||||
);
|
||||
expect(parsed.parseError).toBeUndefined();
|
||||
const entry = parsed.workers.find((w) => w.ref === "world-simulator")!;
|
||||
const { worker } = buildDeclaredWorkerSkill({
|
||||
skillPackName: "world-simulator",
|
||||
entry,
|
||||
template: {
|
||||
id: "world-simulator",
|
||||
label: "主世界层",
|
||||
duty: "裁决",
|
||||
suggested_context: { static: ["设计.worker集"], dynamic: ["用户.最新输入"] },
|
||||
},
|
||||
workerSet: parsed,
|
||||
});
|
||||
expect(worker.contextSegments?.some((s) => s.inline?.includes("裁决"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
worker.contextSegments?.some(
|
||||
(s) => s.tier === "dynamic" && s.tags.includes("用户.最新输入"),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const bb = new Blackboard();
|
||||
bb.write({
|
||||
tag: "用户.最新输入",
|
||||
content: "推门进去",
|
||||
source: "test",
|
||||
});
|
||||
const text = assembleWorkerContext({
|
||||
inputs: { "用户.最新输入": "推门进去" },
|
||||
segments: worker.contextSegments,
|
||||
blackboard: bb,
|
||||
workerId: "world-simulator",
|
||||
workerName: "主世界层",
|
||||
outputTags: ["运行.本轮.裁决"],
|
||||
});
|
||||
expect(text).toContain("裁决");
|
||||
expect(text).toContain("推门进去");
|
||||
});
|
||||
});
|
||||
140
tests/dialogue-history.test.ts
Normal file
140
tests/dialogue-history.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
appendDialogueHistoryTurn,
|
||||
projectDialogueHistory,
|
||||
buildHistoryFallback,
|
||||
isDialogueHistoryRef,
|
||||
} from "../src/skills/dialogue-history.js";
|
||||
import { assembleWorkerContext } from "../src/skills/context-segments.js";
|
||||
import { Blackboard } from "../src/blackboard/blackboard.js";
|
||||
import {
|
||||
contextOrderToSegments,
|
||||
parseContextOrder,
|
||||
applyContextOrderEdit,
|
||||
synthesizeContextOrderFromWorkers,
|
||||
} from "../src/skills/context-order.js";
|
||||
|
||||
describe("dialogue history projection", () => {
|
||||
it("recognizes history refs and projects by level", () => {
|
||||
expect(isDialogueHistoryRef("对话.历史")).toBe(true);
|
||||
expect(isDialogueHistoryRef("history")).toBe(true);
|
||||
const long = Array.from({ length: 30 }, (_, i) => `## 用户\n\n第${i}句`).join(
|
||||
"\n\n",
|
||||
);
|
||||
const summary = projectDialogueHistory(long, "summary");
|
||||
expect(summary).toContain("仅最近");
|
||||
expect(projectDialogueHistory(long, "fixed")).toContain("不注入");
|
||||
});
|
||||
|
||||
it("assembles history tag in flat segment order", () => {
|
||||
const bb = new Blackboard();
|
||||
bb.write({
|
||||
tag: "对话.历史",
|
||||
content: appendDialogueHistoryTurn("", { role: "用户", text: "推门" }),
|
||||
source: "test",
|
||||
});
|
||||
bb.write({ tag: "用户.最新输入", content: "再看一眼", source: "test" });
|
||||
const text = assembleWorkerContext({
|
||||
inputs: {},
|
||||
segments: [
|
||||
{
|
||||
id: "p",
|
||||
tier: "static",
|
||||
tags: [],
|
||||
label: "## persona",
|
||||
inline: "GM",
|
||||
projection: "fixed",
|
||||
},
|
||||
{
|
||||
id: "h",
|
||||
tier: "static",
|
||||
tags: ["对话.历史"],
|
||||
label: "## 对话.历史",
|
||||
projection: "full",
|
||||
},
|
||||
{
|
||||
id: "u",
|
||||
tier: "dynamic",
|
||||
tags: ["用户.最新输入"],
|
||||
label: "## 用户.最新输入",
|
||||
projection: "full",
|
||||
},
|
||||
],
|
||||
blackboard: bb,
|
||||
workerId: "world-simulator",
|
||||
workerName: "主世界层",
|
||||
outputTags: [],
|
||||
});
|
||||
const iPersona = text.indexOf("GM");
|
||||
const iHist = text.indexOf("推门");
|
||||
const iUser = text.indexOf("再看一眼");
|
||||
expect(iPersona).toBeGreaterThanOrEqual(0);
|
||||
expect(iHist).toBeGreaterThan(iPersona);
|
||||
expect(iUser).toBeGreaterThan(iHist);
|
||||
});
|
||||
|
||||
it("falls back to event stream when history empty", () => {
|
||||
expect(
|
||||
buildHistoryFallback({
|
||||
eventStream: "门开了",
|
||||
latestUser: "走进去",
|
||||
}),
|
||||
).toContain("门开了");
|
||||
});
|
||||
});
|
||||
|
||||
describe("flat context order", () => {
|
||||
it("synthesizes history tag and moves across it", () => {
|
||||
const doc = synthesizeContextOrderFromWorkers(
|
||||
[
|
||||
{
|
||||
ref: "narrator",
|
||||
name: "叙事转述",
|
||||
context: {
|
||||
static: ["设计.叙事指南"],
|
||||
dynamic: ["运行.本轮.裁决", "用户.最新输入"],
|
||||
},
|
||||
},
|
||||
],
|
||||
{ gm: true, narrator: true, perspective: false },
|
||||
)!;
|
||||
expect(doc.slots[0]!.inserts.some((i) => i.ref === "对话.历史")).toBe(true);
|
||||
|
||||
const histIdx = doc.slots[0]!.inserts.findIndex((i) => i.ref === "对话.历史");
|
||||
const moved = applyContextOrderEdit(doc, {
|
||||
action: "set_anchor",
|
||||
slotRef: "narrator",
|
||||
index: doc.slots[0]!.inserts.length - 1,
|
||||
anchor: "pre_history",
|
||||
});
|
||||
const newHist = moved.slots[0]!.inserts.findIndex((i) => i.ref === "对话.历史");
|
||||
const lastUser = moved.slots[0]!.inserts.findIndex(
|
||||
(i) => i.ref === "用户.最新输入",
|
||||
);
|
||||
expect(lastUser).toBeLessThan(newHist);
|
||||
expect(histIdx).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("maps to segments in list order including history", () => {
|
||||
const doc = parseContextOrder({
|
||||
schema: "context-order.v1",
|
||||
slots: [
|
||||
{
|
||||
ref: "world-simulator",
|
||||
inserts: [
|
||||
{ order: 0, ref: "worker.persona", projection: "fixed" },
|
||||
{ order: 1, ref: "对话.历史", projection: "summary" },
|
||||
{ order: 2, ref: "用户.最新输入", projection: "full" },
|
||||
],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const segs = contextOrderToSegments({
|
||||
slot: doc.slots[0]!,
|
||||
personaText: "裁决",
|
||||
});
|
||||
expect(segs[0]!.inline).toContain("裁决");
|
||||
expect(segs[1]!.tags).toEqual(["对话.历史"]);
|
||||
expect(segs[2]!.tags).toEqual(["用户.最新输入"]);
|
||||
});
|
||||
});
|
||||
169
tests/play-slots.test.ts
Normal file
169
tests/play-slots.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
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 } from "../src/skills/settlement-packet.js";
|
||||
import {
|
||||
executeChance,
|
||||
parseChanceRequest,
|
||||
} from "../src/skills/chance-tools.js";
|
||||
|
||||
describe("play_slots", () => {
|
||||
it("expands default gm+narrator", () => {
|
||||
const slots = parsePlaySlots({ gm: true, narrator: true, perspective: false })!;
|
||||
expect(refsFromPlaySlots(slots)).toEqual(["world-simulator", "narrator"]);
|
||||
const workers = expandPlaySlotsToWorkers(slots);
|
||||
expect(workers.map((w) => w.ref)).toEqual(["world-simulator", "narrator"]);
|
||||
expect(workers[0].acceptance).toBe("continue");
|
||||
expect(workers[1].acceptance).toBe("review");
|
||||
});
|
||||
|
||||
it("orders perspective before gm", () => {
|
||||
const slots = parsePlaySlots({
|
||||
gm: true,
|
||||
narrator: true,
|
||||
perspective: true,
|
||||
})!;
|
||||
expect(refsFromPlaySlots(slots)).toEqual([
|
||||
"role-decide",
|
||||
"world-simulator",
|
||||
"narrator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("merges play_slots into worker set JSON", () => {
|
||||
const parsed = parseWorkerSetYaml(
|
||||
JSON.stringify({
|
||||
form_summary: "恋爱升温",
|
||||
play_slots: { gm: true, narrator: true, perspective: false },
|
||||
workers: [],
|
||||
}),
|
||||
);
|
||||
expect(parsed?.play_slots?.gm).toBe(true);
|
||||
expect(parsed?.workers).toHaveLength(2);
|
||||
expect(deriveRunWorkerScope(parsed)).toEqual([
|
||||
"world-simulator",
|
||||
"narrator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves custom context on merge", () => {
|
||||
const slots = parsePlaySlots({ 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({
|
||||
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: {
|
||||
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("画册");
|
||||
});
|
||||
});
|
||||
@@ -85,4 +85,38 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
expect(result.outputs["设计.worker集.草稿"]).toContain("narrator");
|
||||
expect(result.askUser).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lifts fragment 追问/自评 into askUser + askAssessment", () => {
|
||||
const result = parseWorkerResponseForTest(
|
||||
JSON.stringify({
|
||||
outputs: {
|
||||
"设计.美学纲领与交互范式": {
|
||||
schema: "context-fragment.v1",
|
||||
brief: "孤立免疫",
|
||||
正文: { 美学纲领: { 体验内核: "特权与惊惶" } },
|
||||
自评: {
|
||||
维度: [{ 名: "美学纲领", 分数: 60, 说明: "边界未锁" }],
|
||||
薄弱点: "尺度",
|
||||
},
|
||||
追问: {
|
||||
导语: "还需确认:",
|
||||
题目: [
|
||||
{
|
||||
问: "血腥如何用?",
|
||||
建议选项: ["少而锋利", "常态压抑"],
|
||||
示例: "锈迹即可",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
summary: "美学纲领 · 孤立免疫",
|
||||
}),
|
||||
["设计.美学纲领与交互范式"],
|
||||
);
|
||||
expect(result.askUser?.length).toBe(1);
|
||||
expect(result.askUser?.[0]?.options?.length).toBe(2);
|
||||
expect(result.askAssessment).toContain("美学纲领 60%");
|
||||
expect(result.askAssessment).toContain("还需确认");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,9 @@ describe("formatWorkerSetForUser", () => {
|
||||
expect(view.headline).toContain("西幻");
|
||||
expect(view.playModeLabel).toBe("行动–反应循环");
|
||||
expect(view.workers).toHaveLength(2);
|
||||
expect(view.workers[0].displayName).toBe("世界模拟");
|
||||
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
|
||||
@@ -121,9 +123,13 @@ workers:
|
||||
expect(view.interactionParadigm).toContain("旁观");
|
||||
expect(view.coreWorker).toBe("world-simulator");
|
||||
expect(view.reasoning).toContain("信息隔绝");
|
||||
expect(view.workers[0].roleLabel).toBe("核心");
|
||||
expect(view.workers[0].rationale).toContain("裁决");
|
||||
expect(view.workers[1].mergeConsidered).toContain("NPC");
|
||||
// 推断 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", () => {
|
||||
@@ -134,9 +140,14 @@ workers:
|
||||
when: 世界机之后
|
||||
`)!;
|
||||
const view = formatWorkerSetForUser(parsed)!;
|
||||
expect(view.workers[0].context.staticTags.some((t) => t.tag.includes("变量.目录"))).toBe(
|
||||
true,
|
||||
);
|
||||
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("变量.当前");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user