引入游玩呈现壳与旁观维护槽,完善结算合并、十分制自评与技能产物 UI。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -45,8 +45,8 @@ describe("context-fragment.v1", () => {
|
||||
正文: { 美学纲领: { 体验内核: "…" } },
|
||||
自评: {
|
||||
维度: [
|
||||
{ 名: "交互范式", 分数: 70, 说明: "权限尚可" },
|
||||
{ 名: "美学纲领", 分数: 55, 说明: "边界未锁" },
|
||||
{ 名: "交互范式", 分数: 7, 说明: "权限尚可" },
|
||||
{ 名: "美学纲领", 分数: 5.5, 说明: "边界未锁" },
|
||||
],
|
||||
薄弱点: "暴力尺度未定",
|
||||
},
|
||||
@@ -61,7 +61,8 @@ describe("context-fragment.v1", () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(side.assessment).toContain("交互范式 70%");
|
||||
expect(side.assessment).toContain("交互范式 7/10");
|
||||
expect(side.assessment).toContain("美学纲领 5.5/10");
|
||||
expect(side.assessment).toContain("薄弱点:暴力尺度未定");
|
||||
expect(side.assessment).toContain("为贴近质感");
|
||||
expect(side.questions).toHaveLength(1);
|
||||
|
||||
@@ -11,53 +11,104 @@ import {
|
||||
deriveRunWorkerScope,
|
||||
parseWorkerSetYaml,
|
||||
} from "../src/skills/worker-set-parse.js";
|
||||
import { parseSettlementPacket } from "../src/skills/settlement-packet.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 gm+narrator", () => {
|
||||
const slots = parsePlaySlots({ gm: true, narrator: true, perspective: false })!;
|
||||
expect(refsFromPlaySlots(slots)).toEqual(["world-simulator", "narrator"]);
|
||||
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(["world-simulator", "narrator"]);
|
||||
expect(workers.map((w) => w.ref)).toEqual([
|
||||
"auditor",
|
||||
"world-simulator",
|
||||
"narrator",
|
||||
]);
|
||||
expect(workers[0].acceptance).toBe("continue");
|
||||
expect(workers[1].acceptance).toBe("review");
|
||||
expect(workers[1].acceptance).toBe("continue");
|
||||
expect(workers[2].acceptance).toBe("review");
|
||||
});
|
||||
|
||||
it("orders perspective before gm", () => {
|
||||
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: { gm: true, narrator: true, perspective: false },
|
||||
play_slots: {
|
||||
auditor: true,
|
||||
gm: true,
|
||||
narrator: true,
|
||||
perspective: false,
|
||||
},
|
||||
workers: [],
|
||||
}),
|
||||
);
|
||||
expect(parsed?.play_slots?.gm).toBe(true);
|
||||
expect(parsed?.workers).toHaveLength(2);
|
||||
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({ gm: true, narrator: true })!;
|
||||
const slots = parsePlaySlots({
|
||||
auditor: false,
|
||||
gm: true,
|
||||
narrator: true,
|
||||
})!;
|
||||
const merged = mergeWorkersWithPlaySlots(
|
||||
[
|
||||
{
|
||||
@@ -76,6 +127,7 @@ describe("play_slots", () => {
|
||||
|
||||
it("chance is on-demand and not in turn pipeline", () => {
|
||||
const slots = parsePlaySlots({
|
||||
auditor: false,
|
||||
gm: true,
|
||||
narrator: true,
|
||||
perspective: false,
|
||||
@@ -96,6 +148,7 @@ describe("play_slots", () => {
|
||||
const parsed = parseWorkerSetYaml(
|
||||
JSON.stringify({
|
||||
play_slots: {
|
||||
auditor: false,
|
||||
gm: true,
|
||||
narrator: true,
|
||||
perspective: false,
|
||||
@@ -166,4 +219,91 @@ describe("settlement packet", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
91
tests/present-packet.test.ts
Normal file
91
tests/present-packet.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isPresentLikeObject,
|
||||
parsePresentPacket,
|
||||
parseShellAdaptationFromReplyFormat,
|
||||
presentFromPlainText,
|
||||
shellDefaultRegions,
|
||||
} from "../src/skills/present-packet.js";
|
||||
|
||||
describe("present.v1", () => {
|
||||
it("parses present packet with blocks", () => {
|
||||
const view = parsePresentPacket(
|
||||
JSON.stringify({
|
||||
schema: "present.v1",
|
||||
shell: "chat_monitor",
|
||||
blocks: {
|
||||
monitor: { 好感: "暖络" },
|
||||
body: "她回了一句短谢。",
|
||||
},
|
||||
meta: { suggested_actions: ["回一句关心"] },
|
||||
}),
|
||||
);
|
||||
expect(view.ok).toBe(true);
|
||||
expect(view.fallbackPlain).toBe(false);
|
||||
expect(view.packet.shell).toBe("chat_monitor");
|
||||
expect(view.packet.blocks.body).toContain("短谢");
|
||||
expect(view.packet.meta?.suggested_actions?.[0]).toBe("回一句关心");
|
||||
});
|
||||
|
||||
it("falls back plain text to prose body", () => {
|
||||
const view = parsePresentPacket("林晚点头收下了画册。");
|
||||
expect(view.fallbackPlain).toBe(true);
|
||||
expect(view.packet.shell).toBe("prose");
|
||||
expect(view.packet.blocks.body).toContain("画册");
|
||||
});
|
||||
|
||||
it("does not treat settlement as present", () => {
|
||||
const view = parsePresentPacket(
|
||||
JSON.stringify({
|
||||
schema: "settlement.v1",
|
||||
visible_now: "场面",
|
||||
player_action: "送礼",
|
||||
}),
|
||||
);
|
||||
expect(view.fallbackPlain).toBe(true);
|
||||
expect(isPresentLikeObject({ schema: "settlement.v1" })).toBe(false);
|
||||
});
|
||||
|
||||
it("presentFromPlainText helper", () => {
|
||||
const p = presentFromPlainText("hello", "chapter_reader");
|
||||
expect(p.shell).toBe("chapter_reader");
|
||||
expect(p.blocks.body).toBe("hello");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shell adaptation from reply-format", () => {
|
||||
it("reads 呈现壳 from context-fragment", () => {
|
||||
const adapt = parseShellAdaptationFromReplyFormat(
|
||||
JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "正文组成",
|
||||
正文: {
|
||||
呈现壳: {
|
||||
shell_id: "turn_panel",
|
||||
为何选它: "机制重",
|
||||
微调: {
|
||||
tone_chrome: "terminal",
|
||||
show_suggested_actions: true,
|
||||
block_labels: { body: "场面" },
|
||||
},
|
||||
},
|
||||
可见块: [{ 块id: "body", 区域: "body" }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(adapt?.shell_id).toBe("turn_panel");
|
||||
expect(adapt?.tweaks.tone_chrome).toBe("terminal");
|
||||
expect(adapt?.tweaks.block_labels?.body).toBe("场面");
|
||||
expect(shellDefaultRegions("turn_panel")).toContain("footer");
|
||||
});
|
||||
|
||||
it("defaults missing shell to prose", () => {
|
||||
const adapt = parseShellAdaptationFromReplyFormat(
|
||||
JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
正文: { 可见块: [{ 块id: "body" }] },
|
||||
}),
|
||||
);
|
||||
expect(adapt?.shell_id).toBe("prose");
|
||||
});
|
||||
});
|
||||
@@ -95,7 +95,7 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
brief: "孤立免疫",
|
||||
正文: { 美学纲领: { 体验内核: "特权与惊惶" } },
|
||||
自评: {
|
||||
维度: [{ 名: "美学纲领", 分数: 60, 说明: "边界未锁" }],
|
||||
维度: [{ 名: "美学纲领", 分数: 6, 说明: "边界未锁" }],
|
||||
薄弱点: "尺度",
|
||||
},
|
||||
追问: {
|
||||
@@ -116,7 +116,7 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
);
|
||||
expect(result.askUser?.length).toBe(1);
|
||||
expect(result.askUser?.[0]?.options?.length).toBe(2);
|
||||
expect(result.askAssessment).toContain("美学纲领 60%");
|
||||
expect(result.askAssessment).toContain("美学纲领 6/10");
|
||||
expect(result.askAssessment).toContain("还需确认");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user