引入游玩回合与开场收口,抽出可替换 agent 司机并支持运行中止;完善预设洞位、世界书拼装与试跑,并打磨会话运行时与 Web 交互。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -106,9 +106,127 @@ describe("contextSegments assembly", () => {
|
||||
outputTags: ["设计.美学纲领与交互范式"],
|
||||
});
|
||||
expect(text).toContain("待改底稿");
|
||||
expect(text).toContain("用户.worker答复");
|
||||
expect(text).toContain("用户要求");
|
||||
expect(text).toContain("冲突时以较新的为准");
|
||||
expect(text).toContain("偏冷");
|
||||
expect(text).toContain("再冷一点");
|
||||
expect(text.split("再冷一点").length - 1).toBe(1);
|
||||
});
|
||||
|
||||
it("stale revision note without a draft stays as a user requirement, not revision mode", () => {
|
||||
const bb = new Blackboard();
|
||||
const text = assembleWorkerContext({
|
||||
inputs: {
|
||||
"用户.修订说明": "上一步留下的意见",
|
||||
"用户.需求": "网恋对话",
|
||||
},
|
||||
segments: [
|
||||
{
|
||||
id: "user",
|
||||
tier: "dynamic",
|
||||
tags: ["用户.需求", "用户.修订说明"],
|
||||
label: "## 用户表述",
|
||||
},
|
||||
],
|
||||
blackboard: bb,
|
||||
workerId: "design-step",
|
||||
workerName: "创作 · 正文组成",
|
||||
outputTags: ["设计.正文组成"],
|
||||
});
|
||||
expect(text).not.toContain("修订模式");
|
||||
expect(text).not.toContain("待改底稿");
|
||||
expect(text).toContain("用户要求");
|
||||
expect(text).toContain("上一步留下的意见");
|
||||
expect(text).toContain("网恋对话");
|
||||
expect(text).toContain("按 SKILL 与上方分区完成任务");
|
||||
});
|
||||
|
||||
it("collapses duplicate user-demand tags in the same segment", () => {
|
||||
const bb = new Blackboard();
|
||||
const text = assembleWorkerContext({
|
||||
inputs: {
|
||||
"用户.需求": "开场需求\n\n需要正文和选项",
|
||||
"用户.worker答复": "需要正文和选项",
|
||||
"用户.最新输入": "需要正文和选项",
|
||||
},
|
||||
segments: [
|
||||
{
|
||||
id: "user",
|
||||
tier: "dynamic",
|
||||
tags: ["用户.需求", "用户.worker答复", "用户.最新输入"],
|
||||
label: "## 用户表述",
|
||||
},
|
||||
],
|
||||
blackboard: bb,
|
||||
workerId: "design-step",
|
||||
workerName: "创作 · 正文组成",
|
||||
outputTags: ["设计.正文组成"],
|
||||
});
|
||||
expect(text.split("需要正文和选项").length - 1).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps earlier user requirements and puts the newer revision last", () => {
|
||||
const bb = new Blackboard();
|
||||
bb.write({
|
||||
tag: "设计.正文组成",
|
||||
content: '{"shell":"split_board"}',
|
||||
source: "worker",
|
||||
});
|
||||
const text = assembleWorkerContext({
|
||||
inputs: {
|
||||
"用户.需求": "需要正文和监控",
|
||||
"用户.修订说明": "状态放到上面",
|
||||
"设计.正文组成": '{"shell":"split_board"}',
|
||||
},
|
||||
segments: [
|
||||
{
|
||||
id: "user",
|
||||
tier: "dynamic",
|
||||
tags: ["用户.需求", "用户.修订说明"],
|
||||
label: "## 用户表述",
|
||||
},
|
||||
],
|
||||
blackboard: bb,
|
||||
workerId: "design-step",
|
||||
workerName: "创作 · 正文组成",
|
||||
outputTags: ["设计.正文组成"],
|
||||
});
|
||||
expect(text).toContain("需要正文和监控");
|
||||
expect(text).toContain("状态放到上面");
|
||||
expect(text.indexOf("需要正文和监控")).toBeLessThan(text.indexOf("状态放到上面"));
|
||||
expect(text).toContain("冲突时以较新的为准");
|
||||
expect(text).toContain("待改底稿");
|
||||
expect(text).toContain("split_board");
|
||||
});
|
||||
|
||||
it("inherit-existing asks the worker to keep the draft without a revision note", () => {
|
||||
const bb = new Blackboard();
|
||||
bb.write({
|
||||
tag: "设计.美学纲领与交互范式",
|
||||
content: '{"tone":"warm"}',
|
||||
source: "worker",
|
||||
});
|
||||
const text = assembleWorkerContext({
|
||||
inputs: {
|
||||
"设计.美学纲领与交互范式": '{"tone":"warm"}',
|
||||
},
|
||||
segments: [
|
||||
{
|
||||
id: "inherit-existing",
|
||||
tier: "static",
|
||||
tags: ["设计.美学纲领与交互范式"],
|
||||
label: "## 【既有产物 · 继承修改】在下列正文上继续改",
|
||||
},
|
||||
],
|
||||
blackboard: bb,
|
||||
workerId: "design-step",
|
||||
workerName: "创作 · 回头修改 · 美学纲领与交互范式",
|
||||
outputTags: ["设计.美学纲领与交互范式"],
|
||||
});
|
||||
expect(text).toContain("既有产物 · 继承修改");
|
||||
expect(text).toContain("回头修改");
|
||||
expect(text).toContain("warm");
|
||||
expect(text).not.toContain("按 SKILL 与上方分区完成任务");
|
||||
});
|
||||
|
||||
it("falls back to JSON inputs without segments", () => {
|
||||
|
||||
@@ -77,6 +77,24 @@ describe("context-fragment.v1", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not surface empty 追问 导语 as sidecar assessment", () => {
|
||||
const side = extractFragmentAskSidecar({
|
||||
schema: "context-fragment.v1",
|
||||
brief: "返现都市",
|
||||
正文: { 美学纲领: { 体验内核: "被宠" } },
|
||||
自评: {
|
||||
维度: [{ 名: "交互范式", 分数: 9, 说明: "站位已清" }],
|
||||
},
|
||||
追问: {
|
||||
导语: "为贴近你要的质感,还需确认:",
|
||||
题目: [],
|
||||
},
|
||||
});
|
||||
expect(side.questions).toHaveLength(0);
|
||||
expect(side.assessment).toContain("交互范式 9/10");
|
||||
expect(side.assessment).not.toContain("还需确认");
|
||||
});
|
||||
|
||||
it("isUsableContextFragment requires non-empty 正文", () => {
|
||||
expect(
|
||||
isUsableContextFragment({
|
||||
@@ -125,6 +143,63 @@ describe("context-order.v1", () => {
|
||||
expect(segs[3].tier).toBe("dynamic");
|
||||
});
|
||||
|
||||
it("accepts agents[] roster and maps gm → world-simulator", () => {
|
||||
const doc = parseContextOrder({
|
||||
schema: "context-order.v1",
|
||||
brief: "旁观+主世界层+转述",
|
||||
agents: [
|
||||
{
|
||||
id: "auditor",
|
||||
label: "表格管理",
|
||||
enabled: true,
|
||||
when: "every_turn",
|
||||
inserts: [
|
||||
{ order: 0, ref: "worker.persona", projection: "fixed" },
|
||||
{ order: 1, ref: "设计.变量设计与更新规则", projection: "summary" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "gm",
|
||||
enabled: true,
|
||||
inserts: [
|
||||
{ order: 0, ref: "worker.persona", projection: "fixed" },
|
||||
{ order: 1, ref: "对话.历史", projection: "summary" },
|
||||
],
|
||||
},
|
||||
{ id: "narrator", enabled: true, inserts: [] },
|
||||
{ id: "perspective", enabled: false, inserts: [] },
|
||||
{ id: "chance", enabled: false, when: "on_demand", inserts: [] },
|
||||
],
|
||||
})!;
|
||||
expect(doc.play_slots?.auditor).not.toBe(false);
|
||||
expect(doc.play_slots?.gm).toBe(true);
|
||||
expect(doc.play_slots?.perspective).toBe(false);
|
||||
expect(doc.play_slots?.chance).toBe(false);
|
||||
expect(slotOrderForRef(doc, "auditor")?.label).toBe("表格管理");
|
||||
expect(slotOrderForRef(doc, "world-simulator")?.inserts.some((i) => i.ref === "对话.历史")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(slotOrderForRef(doc, "role-decide")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("index projection keeps brief, mount and 稳变 without 正文", () => {
|
||||
const text = projectFragmentContent(
|
||||
JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "免疫即权力",
|
||||
mount: ["world-simulator", "narrator"],
|
||||
稳变: "stable",
|
||||
正文: { 美学纲领: { 体验内核: "很长不应出现在 index 里的正文" } },
|
||||
}),
|
||||
"index",
|
||||
);
|
||||
expect(text).toContain("免疫即权力");
|
||||
expect(text).toContain("挂载:world-simulator、narrator");
|
||||
expect(text).toContain("稳变:stable");
|
||||
expect(text).not.toContain("很长不应出现在 index 里的正文");
|
||||
});
|
||||
|
||||
it("reorders flat list and merges into worker set JSON", () => {
|
||||
const doc = parseContextOrder({
|
||||
schema: "context-order.v1",
|
||||
|
||||
+192
-12
@@ -290,12 +290,21 @@ describe("creation-flow", () => {
|
||||
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();
|
||||
@@ -303,9 +312,12 @@ describe("creation-flow", () => {
|
||||
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("何时用");
|
||||
expect(block).toContain("何时不用");
|
||||
expect(block).toContain("规划产物字段");
|
||||
expect(block).toContain("编排参数");
|
||||
expect(block).not.toContain("设计.美学纲领与交互范式");
|
||||
});
|
||||
@@ -465,7 +477,8 @@ recipes:
|
||||
expect(loaded.promptBody).toContain("已完成:");
|
||||
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
||||
expect(loaded.promptBody).toContain("已验收");
|
||||
expect(loaded.promptBody).toContain("禁止再排一次执行");
|
||||
expect(loaded.promptBody).toContain("禁止再排新建");
|
||||
expect(loaded.promptBody).toContain("mode=revise");
|
||||
expect(loaded.promptBody).toContain("生成规则");
|
||||
});
|
||||
|
||||
@@ -506,6 +519,7 @@ recipes:
|
||||
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
||||
expect(loaded.promptBody).toContain("体验契约");
|
||||
expect(loaded.promptBody).toContain("程序开场");
|
||||
expect(loaded.promptBody).toContain("【本步参数】");
|
||||
expect(loaded.worker.inputTags).toContain("创作.能力开场白");
|
||||
});
|
||||
|
||||
@@ -606,24 +620,24 @@ recipes:
|
||||
expect(view.steps[0]?.params?.target).toBe("怪物");
|
||||
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
expect(
|
||||
catalog?.modules.find((m) => m.name === "生成规则")?.params?.some(
|
||||
(p) => p.key === "target" && p.required,
|
||||
),
|
||||
).toBe(true);
|
||||
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": [] }]
|
||||
}`)!;
|
||||
// 编排期允许空壳;必填 params 在确认开干时钉齐
|
||||
// 先验产物:编排期允许空壳,确认开干也不拦
|
||||
expect(validateCreationFlow(missing, catalog).ok).toBe(true);
|
||||
const stepReady = validateStepReadyToRun(missing.steps[0]!, catalog);
|
||||
expect(stepReady.ok).toBe(false);
|
||||
expect(stepReady.errors.some((e) => e.includes("target"))).toBe(true);
|
||||
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("编排参数");
|
||||
expect(block).toContain("规划产物字段");
|
||||
expect(block).toContain("target");
|
||||
});
|
||||
|
||||
@@ -648,12 +662,47 @@ recipes:
|
||||
acceptedStepNames: [],
|
||||
},
|
||||
);
|
||||
expect(loaded.promptBody).toContain("【本步参数】");
|
||||
expect(loaded.promptBody).toContain("【规划产物】");
|
||||
expect(loaded.promptBody).not.toContain("勿再问「生成什么");
|
||||
expect(loaded.promptBody).toContain("target: 怪物");
|
||||
expect(loaded.promptBody).toContain("rule_id: monsters");
|
||||
expect(loaded.worker.outputTags).toContain("设计.生成规则");
|
||||
});
|
||||
|
||||
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: "生成规则", name: "生成规则", depends_on: [] }],
|
||||
}),
|
||||
currentStepName: "生成规则",
|
||||
acceptedStepNames: [],
|
||||
},
|
||||
);
|
||||
expect(loaded.promptBody).toContain("【规划产物】");
|
||||
expect(loaded.promptBody).toContain("尚未规划本步具体写什么");
|
||||
});
|
||||
|
||||
it("nextPendingStep respects deps and accepted by id", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
@@ -730,4 +779,135 @@ recipes:
|
||||
}`)!;
|
||||
expect(isCreationFlowComplete(legacy, ["美学纲领与交互范式"])).toBe(true);
|
||||
});
|
||||
|
||||
it("allows revising a non-repeatable skill and infers revises + id", () => {
|
||||
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 view = formatCreationFlowForUser(flow, sampleCatalog, ["实现机制"]);
|
||||
expect(view.steps[1]?.mode).toBe("revise");
|
||||
expect(view.steps[1]?.occurrence).toBeUndefined();
|
||||
expect(view.steps[1]?.runState).toBe("current");
|
||||
});
|
||||
|
||||
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 prefers a ready revise over a later fresh step", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
||||
{
|
||||
"name": "美学纲领与交互范式",
|
||||
"mode": "revise",
|
||||
"revises": "美学纲领与交互范式",
|
||||
"depends_on": ["美学纲领与交互范式"]
|
||||
}
|
||||
]
|
||||
}`)!;
|
||||
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.mode).toBe("revise");
|
||||
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.id).toBe(
|
||||
"美学纲领与交互范式·改",
|
||||
);
|
||||
});
|
||||
|
||||
it("injects inherit draft and skips opening on a revise design-step", async () => {
|
||||
const flowRaw = JSON.stringify({
|
||||
status: "open",
|
||||
steps: [
|
||||
{ id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [] },
|
||||
{
|
||||
id: "美学纲领与交互范式·改",
|
||||
name: "美学纲领与交互范式",
|
||||
mode: "revise",
|
||||
revises: "美学纲领与交互范式",
|
||||
depends_on: ["美学纲领与交互范式"],
|
||||
},
|
||||
],
|
||||
});
|
||||
const binding = await resolveDesignStepBinding({
|
||||
skillPackRoot: "dialogue/world-simulator",
|
||||
flowRaw,
|
||||
currentStepName: "美学纲领与交互范式·改",
|
||||
acceptedStepNames: ["美学纲领与交互范式"],
|
||||
});
|
||||
expect(binding?.opening).toBeNull();
|
||||
expect(binding?.inheritTag).toBe("设计.美学纲领与交互范式");
|
||||
|
||||
const loaded = await loadWorkerSkillWithContext(
|
||||
"world-simulator",
|
||||
"design-step",
|
||||
undefined,
|
||||
{
|
||||
flowRaw,
|
||||
currentStepName: "美学纲领与交互范式·改",
|
||||
acceptedStepNames: ["美学纲领与交互范式"],
|
||||
},
|
||||
);
|
||||
expect(loaded.promptBody).toContain("【回头修改】");
|
||||
expect(loaded.promptBody).not.toContain("程序开场");
|
||||
expect(loaded.worker.name).toContain("回头修改");
|
||||
expect(loaded.worker.inputTags).toContain("设计.美学纲领与交互范式");
|
||||
expect(
|
||||
loaded.worker.contextSegments?.some((s) => s.id === "inherit-existing"),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("resolveAcceptanceModeForWorker", () => {
|
||||
expect(mode).toBe("user_confirmed");
|
||||
});
|
||||
|
||||
it("maps review → user_confirmed and continue → no_confirmation", () => {
|
||||
it("maps review → user_confirmed and continue → no_confirmation in design", () => {
|
||||
const bb = new Blackboard();
|
||||
const yaml = JSON.stringify({
|
||||
version: 1,
|
||||
@@ -32,7 +32,7 @@ describe("resolveAcceptanceModeForWorker", () => {
|
||||
slots: {
|
||||
...createSession().slots,
|
||||
designInstanceReady: true,
|
||||
uiLifecycleStage: "play",
|
||||
uiLifecycleStage: "design",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,6 +51,34 @@ describe("resolveAcceptanceModeForWorker", () => {
|
||||
}),
|
||||
).toBe("user_confirmed");
|
||||
});
|
||||
|
||||
it("play ignores worker-set review and auto-continues", () => {
|
||||
const bb = new Blackboard();
|
||||
bb.write({
|
||||
tag: "设计.worker集",
|
||||
content: JSON.stringify({
|
||||
version: 1,
|
||||
workers: [{ ref: "narrator", acceptance: "review" }],
|
||||
}),
|
||||
source: "test",
|
||||
});
|
||||
const session = {
|
||||
...createSession(),
|
||||
slots: {
|
||||
...createSession().slots,
|
||||
designInstanceReady: true,
|
||||
uiLifecycleStage: "play",
|
||||
playLayerActive: true,
|
||||
},
|
||||
};
|
||||
expect(
|
||||
resolveAcceptanceModeForWorker({
|
||||
session,
|
||||
blackboard: bb,
|
||||
workerId: "narrator",
|
||||
}),
|
||||
).toBe("no_confirmation");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDeclaredWorkerSkill", () => {
|
||||
@@ -92,5 +120,6 @@ describe("buildDeclaredWorkerSkill", () => {
|
||||
expect(promptBody).toContain("残酷但不虐主");
|
||||
expect(promptBody).toContain("主角免疫");
|
||||
expect(promptBody).toContain("声明驱动");
|
||||
expect(promptBody).toContain("500~2000");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,5 +45,11 @@ describe("display-labels", () => {
|
||||
const opening = reviewComposerCopy("opening-generator");
|
||||
expect(opening.acceptLabel).toBe("选定此开场");
|
||||
expect(opening.kicker).toBe("待选定");
|
||||
|
||||
const setup = reviewComposerCopy("design-step", {
|
||||
outputTags: ["设计.开场白与开场变量"],
|
||||
});
|
||||
expect(setup.acceptLabel).toBe("选定此开场");
|
||||
expect(setup.kind).toBe("opening");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createMockToolCall,
|
||||
MockLlmProvider,
|
||||
} from "../src/llm/client.js";
|
||||
import { createLocalLlmDriver } from "../src/runtime/driver.js";
|
||||
|
||||
describe("local LLM driver", () => {
|
||||
it("continues while tools return results, then stops on text", async () => {
|
||||
const llm = new MockLlmProvider([
|
||||
createMockToolCall("lookup", { q: "a" }, "c1"),
|
||||
"done",
|
||||
]);
|
||||
const driver = createLocalLlmDriver(llm);
|
||||
const seen: string[] = [];
|
||||
|
||||
const run = await driver.run({
|
||||
system: "sys",
|
||||
messages: [{ role: "user", content: "go" }],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "lookup",
|
||||
description: "lookup",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
],
|
||||
handleStep: (calls) => {
|
||||
seen.push(...calls.map((c) => c.name));
|
||||
return {
|
||||
kind: "continue",
|
||||
results: calls.map((c) => ({ callId: c.id, content: "ok" })),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(seen).toEqual(["lookup"]);
|
||||
expect(run.iterations).toBe(2);
|
||||
expect(run.stop).toEqual({ kind: "text", content: "done" });
|
||||
});
|
||||
|
||||
it("stops when handleStep concludes, without another model call", async () => {
|
||||
const llm = new MockLlmProvider([
|
||||
createMockToolCall("run_worker", { workerId: "x" }, "t1"),
|
||||
"should-not-be-used",
|
||||
]);
|
||||
const driver = createLocalLlmDriver(llm);
|
||||
|
||||
const run = await driver.run({
|
||||
system: "sys",
|
||||
messages: [{ role: "user", content: "go" }],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "run_worker",
|
||||
description: "run",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
],
|
||||
handleStep: (calls) => ({ kind: "conclude", call: calls[0]! }),
|
||||
});
|
||||
|
||||
expect(run.iterations).toBe(1);
|
||||
expect(run.stop.kind).toBe("conclude");
|
||||
if (run.stop.kind === "conclude") {
|
||||
expect(run.stop.call.name).toBe("run_worker");
|
||||
}
|
||||
});
|
||||
|
||||
it("throws when the iteration cap is hit", async () => {
|
||||
const llm = new MockLlmProvider([
|
||||
createMockToolCall("ping", {}, "p1"),
|
||||
]);
|
||||
const driver = createLocalLlmDriver(llm);
|
||||
|
||||
await expect(
|
||||
driver.run({
|
||||
system: "sys",
|
||||
messages: [{ role: "user", content: "go" }],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "ping",
|
||||
description: "ping",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
],
|
||||
maxIterations: 2,
|
||||
handleStep: (calls) => ({
|
||||
kind: "continue",
|
||||
results: calls.map((c) => ({ callId: c.id, content: "pong" })),
|
||||
}),
|
||||
}),
|
||||
).rejects.toThrow(/exceeded 2 iterations/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
closeCreationFlowRaw,
|
||||
isOpeningSealArtifact,
|
||||
parseOpeningSealPayload,
|
||||
openingVariablesToTableDoc,
|
||||
} from "../src/skills/opening-seal.js";
|
||||
|
||||
const fragment = JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "开场白与开场变量",
|
||||
brief: "宿舍缺水",
|
||||
mount: ["narrator"],
|
||||
正文: {
|
||||
开场白全文: "你在宿舍醒来,瓶里只剩一口水。",
|
||||
开场白候选: [
|
||||
"你在宿舍醒来,瓶里只剩一口水。",
|
||||
"门外有人砸门。你还没想好要不要开。",
|
||||
],
|
||||
开场变量: [{ 名: "口粮", 值: 1, 依据: "开场只剩一口水" }],
|
||||
},
|
||||
开放问题: [],
|
||||
});
|
||||
|
||||
describe("opening-seal", () => {
|
||||
it("detects opening-setup and opening-generator artifacts", () => {
|
||||
expect(
|
||||
isOpeningSealArtifact({
|
||||
workerId: "design-step",
|
||||
outputTags: ["设计.开场白与开场变量"],
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isOpeningSealArtifact({
|
||||
workerId: "opening-generator",
|
||||
outputTags: ["输出.开场白"],
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isOpeningSealArtifact({
|
||||
workerId: "design-step",
|
||||
outputTags: ["设计.美学纲领与交互范式"],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("parses 1+ opening candidates and selects by index", () => {
|
||||
const first = parseOpeningSealPayload(fragment, 0);
|
||||
expect(first?.candidates).toHaveLength(2);
|
||||
expect(first?.selectedText).toContain("宿舍醒来");
|
||||
const second = parseOpeningSealPayload(fragment, 1);
|
||||
expect(second?.selectedText).toContain("砸门");
|
||||
expect(second?.variables).toEqual([
|
||||
{ name: "口粮", value: 1, note: "开场只剩一口水" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds table rows from opening variables", () => {
|
||||
const payload = parseOpeningSealPayload(fragment, 0);
|
||||
const doc = openingVariablesToTableDoc(payload!.variables);
|
||||
expect(doc.rows[0]?.key).toBe("口粮");
|
||||
expect(doc.rows[0]?.value).toBe(1);
|
||||
});
|
||||
|
||||
it("closes an open creation flow", () => {
|
||||
const raw = JSON.stringify({
|
||||
version: 1,
|
||||
status: "open",
|
||||
steps: [{ id: "开场白与开场变量", name: "开场白与开场变量", depends_on: [] }],
|
||||
});
|
||||
const closed = JSON.parse(closeCreationFlowRaw(raw)!);
|
||||
expect(closed.status).toBe("closed");
|
||||
});
|
||||
});
|
||||
+227
-3
@@ -209,6 +209,72 @@ describe("phase machine", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("later revision notes accumulate instead of replacing earlier ones", () => {
|
||||
let session = createSession("default");
|
||||
const artifact = createArtifact({
|
||||
workerId: "design-step",
|
||||
outputTags: ["设计.正文组成"],
|
||||
});
|
||||
session = {
|
||||
...session,
|
||||
artifacts: [artifact],
|
||||
pendingArtifactId: artifact.id,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||
};
|
||||
|
||||
session = applyEvent(session, {
|
||||
type: "user_rejected_artifact",
|
||||
payload: { artifactId: artifact.id, reason: "不如原本的上中下样式" },
|
||||
}).session;
|
||||
|
||||
session = {
|
||||
...session,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||
pendingArtifactId: artifact.id,
|
||||
};
|
||||
|
||||
const second = applyEvent(session, {
|
||||
type: "user_rejected_artifact",
|
||||
payload: { artifactId: artifact.id, reason: "状态很短,放到上面就行了" },
|
||||
});
|
||||
|
||||
expect(second.session.slots["用户.修订说明"]).toContain("不如原本的上中下样式");
|
||||
expect(second.session.slots["用户.修订说明"]).toContain("状态很短,放到上面就行了");
|
||||
});
|
||||
|
||||
it("accepting a design-step artifact clears stale revision notes", () => {
|
||||
let session = createSession("default");
|
||||
const artifact = createArtifact({
|
||||
workerId: "design-step",
|
||||
outputTags: ["设计.监控栏"],
|
||||
});
|
||||
session = {
|
||||
...session,
|
||||
artifacts: [artifact],
|
||||
pendingArtifactId: artifact.id,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||
slots: {
|
||||
"用户.修订说明": "上一步的意见",
|
||||
revisionInstruction: "上一步的意见",
|
||||
"用户.worker答复": "监控对象是剩余食物",
|
||||
},
|
||||
};
|
||||
|
||||
session = applyEvent(session, {
|
||||
type: "user_accepted_artifact",
|
||||
payload: { artifactId: artifact.id },
|
||||
}).session;
|
||||
|
||||
expect(session.slots["用户.修订说明"]).toBeUndefined();
|
||||
expect(session.slots.revisionInstruction).toBeUndefined();
|
||||
expect(session.slots["用户.worker答复"]).toBeUndefined();
|
||||
expect(session.slots["用户.需求"]).toContain("监控对象是剩余食物");
|
||||
expect(session.slots["用户.需求"]).toContain("上一步的意见");
|
||||
});
|
||||
|
||||
it("failed revision rerun falls back to reviewing the previous artifact", () => {
|
||||
let session = createSession("default");
|
||||
const artifact = {
|
||||
@@ -281,7 +347,35 @@ describe("phase machine", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepting design-step enters next_intent instead of invoking main agent", () => {
|
||||
it("accepting design-step worker set proposes next step instead of invoking main agent", () => {
|
||||
let session = createSession("default");
|
||||
const artifact = createArtifact({
|
||||
workerId: "design-step",
|
||||
stepId: "细化终稿",
|
||||
outputTags: ["设计.worker集"],
|
||||
});
|
||||
session = {
|
||||
...session,
|
||||
artifacts: [artifact],
|
||||
pendingArtifactId: artifact.id,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||
};
|
||||
|
||||
const accepted = applyEvent(session, {
|
||||
type: "user_accepted_artifact",
|
||||
payload: { artifactId: artifact.id },
|
||||
});
|
||||
expect(accepted.session.slots.designInstanceReady).toBe(true);
|
||||
expect(accepted.session.phase).toBe("running");
|
||||
expect(accepted.session.waitingReason).toBeUndefined();
|
||||
expect(accepted.effects).toEqual([{ type: "propose_next_creation_step" }]);
|
||||
expect(accepted.effects.some((e) => e.type === "invoke_main_agent")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("accepting design-step proposes next step instead of invoking main agent", () => {
|
||||
let session = createSession("default");
|
||||
const artifact = createArtifact({
|
||||
workerId: "design-step",
|
||||
@@ -300,12 +394,22 @@ describe("phase machine", () => {
|
||||
type: "user_accepted_artifact",
|
||||
payload: { artifactId: artifact.id },
|
||||
});
|
||||
expect(accepted.session.waitingReason?.kind).toBe("next_intent");
|
||||
expect(accepted.session.phase).toBe("running");
|
||||
expect(accepted.effects).toEqual([{ type: "propose_next_creation_step" }]);
|
||||
expect(accepted.effects.some((e) => e.type === "invoke_main_agent")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
const continued = applyEvent(accepted.session, {
|
||||
it("legacy next_intent still proposes next step and records intent", () => {
|
||||
let session = createSession("default");
|
||||
session = {
|
||||
...session,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "next_intent", afterWorkerId: "design-step" },
|
||||
};
|
||||
|
||||
const continued = applyEvent(session, {
|
||||
type: "user_submitted_input",
|
||||
payload: { text: "接下来写怪物生成规则" },
|
||||
});
|
||||
@@ -317,4 +421,124 @@ describe("phase machine", () => {
|
||||
{ type: "propose_next_creation_step" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepting opening-setup seals creation instead of next_intent", () => {
|
||||
let session = createSession("default");
|
||||
const artifact = createArtifact({
|
||||
workerId: "design-step",
|
||||
stepId: "开场白与开场变量",
|
||||
outputTags: ["设计.开场白与开场变量"],
|
||||
});
|
||||
session = {
|
||||
...session,
|
||||
artifacts: [artifact],
|
||||
pendingArtifactId: artifact.id,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||
};
|
||||
|
||||
const accepted = applyEvent(session, {
|
||||
type: "user_accepted_artifact",
|
||||
payload: { artifactId: artifact.id },
|
||||
});
|
||||
expect(accepted.session.waitingReason?.kind).toBe("input");
|
||||
expect(accepted.session.waitingReason).toMatchObject({
|
||||
kind: "input",
|
||||
});
|
||||
expect(accepted.effects).toEqual([{ type: "seal_creation_opening" }]);
|
||||
expect(
|
||||
accepted.effects.some((e) => e.type === "invoke_main_agent"),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepting opening-generator also seals creation", () => {
|
||||
let session = createSession("default");
|
||||
const artifact = createArtifact({
|
||||
workerId: "opening-generator",
|
||||
outputTags: ["输出.开场白", "运行.初始变量"],
|
||||
});
|
||||
session = {
|
||||
...session,
|
||||
artifacts: [artifact],
|
||||
pendingArtifactId: artifact.id,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||
};
|
||||
|
||||
const accepted = applyEvent(session, {
|
||||
type: "user_accepted_artifact",
|
||||
payload: { artifactId: artifact.id },
|
||||
});
|
||||
expect(accepted.session.waitingReason?.kind).toBe("input");
|
||||
expect(accepted.effects).toEqual([{ type: "seal_creation_opening" }]);
|
||||
});
|
||||
|
||||
it("sealed creation still blocks input until play", () => {
|
||||
let session = createSession("default");
|
||||
session = {
|
||||
...session,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "input" },
|
||||
slots: { creationSealedByOpening: true, designInstanceReady: true },
|
||||
};
|
||||
const blocked = applyEvent(session, {
|
||||
type: "user_submitted_input",
|
||||
payload: { text: "我买金首饰" },
|
||||
});
|
||||
expect(blocked.effects).toEqual([
|
||||
{ type: "emit_message", message: "创作已收口并保存。请切换到「游玩」开始。" },
|
||||
]);
|
||||
expect(blocked.effects.some((e) => e.type === "run_play_turn")).toBe(false);
|
||||
});
|
||||
|
||||
it("play input starts a play turn even after opening seal", () => {
|
||||
let session = createSession("default");
|
||||
session = {
|
||||
...session,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "input" },
|
||||
slots: {
|
||||
creationSealedByOpening: true,
|
||||
designInstanceReady: true,
|
||||
uiLifecycleStage: "play",
|
||||
},
|
||||
};
|
||||
const play = applyEvent(session, {
|
||||
type: "user_submitted_input",
|
||||
payload: { text: "我买金首饰" },
|
||||
});
|
||||
expect(play.session.phase).toBe("running");
|
||||
expect(play.effects).toEqual([{ type: "run_play_turn" }]);
|
||||
});
|
||||
|
||||
it("accepting a play worker continues the play pipeline", () => {
|
||||
let session = createSession("default");
|
||||
const artifact = createArtifact({
|
||||
workerId: "narrator",
|
||||
outputTags: ["输出.用户展示"],
|
||||
});
|
||||
session = {
|
||||
...session,
|
||||
artifacts: [artifact],
|
||||
pendingArtifactId: artifact.id,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||
slots: {
|
||||
designInstanceReady: true,
|
||||
uiLifecycleStage: "play",
|
||||
playLayerActive: true,
|
||||
},
|
||||
};
|
||||
const accepted = applyEvent(session, {
|
||||
type: "user_accepted_artifact",
|
||||
payload: { artifactId: artifact.id },
|
||||
});
|
||||
expect(accepted.effects).toEqual([{ type: "continue_play_turn" }]);
|
||||
expect(accepted.effects.some((e) => e.type === "invoke_main_agent")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(accepted.effects.some((e) => e.type === "propose_next_creation_step")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
MockLlmProvider,
|
||||
} from "../src/llm/client.js";
|
||||
import { PhaseRuntime, createDecision } from "../src/runtime/phase-runtime.js";
|
||||
import { createSession } from "../src/runtime/phase-machine.js";
|
||||
import { Blackboard } from "../src/blackboard/blackboard.js";
|
||||
|
||||
describe("phase runtime", () => {
|
||||
it("auto-loads default orchestrator and awaits first user input", async () => {
|
||||
@@ -47,4 +49,82 @@ describe("phase runtime", () => {
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("play turn runs declared pipeline without review and waits for input", async () => {
|
||||
const session = createSession("default");
|
||||
session.phase = "waiting_user";
|
||||
session.waitingReason = { kind: "input" };
|
||||
session.slots = {
|
||||
designInstanceReady: true,
|
||||
uiLifecycleStage: "play",
|
||||
playLayerActive: true,
|
||||
startupCompleted: true,
|
||||
};
|
||||
const blackboard = new Blackboard();
|
||||
blackboard.write({
|
||||
tag: "设计.worker集",
|
||||
content: JSON.stringify({
|
||||
version: 1,
|
||||
play_slots: {
|
||||
auditor: false,
|
||||
gm: true,
|
||||
narrator: true,
|
||||
perspective: false,
|
||||
},
|
||||
workers: [
|
||||
{ ref: "world-simulator", acceptance: "continue" },
|
||||
{ ref: "narrator", acceptance: "review" },
|
||||
],
|
||||
}),
|
||||
source: "test",
|
||||
});
|
||||
const runtime = new PhaseRuntime({
|
||||
autoStubWorker: true,
|
||||
initialSession: session,
|
||||
initialBlackboardItems: blackboard.exportItems(),
|
||||
});
|
||||
await runtime.submitInput("我网购一个金首饰试试");
|
||||
expect(runtime.getSession().waitingReason?.kind).toBe("input");
|
||||
expect(runtime.getSession().artifacts.map((a) => a.workerId)).toEqual([
|
||||
"world-simulator",
|
||||
"narrator",
|
||||
]);
|
||||
expect(runtime.getSession().artifacts.every((a) => a.status === "accepted")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("retryStuckRun restores the in-flight worker snapshot and reruns", async () => {
|
||||
const runtime = new PhaseRuntime({ autoStubWorker: true });
|
||||
await runtime.start();
|
||||
await runtime.submitInput("网恋对象对话");
|
||||
await runtime.submitDecision(
|
||||
createDecision({
|
||||
action: "run_worker",
|
||||
reason: "编排创作流程",
|
||||
workerId: "design-flow",
|
||||
requiresApproval: false,
|
||||
}),
|
||||
);
|
||||
const snap = runtime.getLastWorkerRunSnapshot();
|
||||
expect(snap?.workerId).toBe("design-flow");
|
||||
const artifactsBefore = runtime.getSession().artifacts.length;
|
||||
|
||||
const session = runtime.getSession();
|
||||
session.phase = "running";
|
||||
session.waitingReason = undefined;
|
||||
session.currentWorkerId = "design-flow";
|
||||
|
||||
await runtime.retryStuckRun();
|
||||
expect(runtime.getSession().artifacts.length).toBeGreaterThan(artifactsBefore);
|
||||
expect(
|
||||
runtime.getSession().artifacts.filter((a) => a.workerId === "design-flow").length,
|
||||
).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("retryStuckRun throws when nothing is running", async () => {
|
||||
const runtime = new PhaseRuntime({ autoStubWorker: true });
|
||||
await runtime.start();
|
||||
await expect(runtime.retryStuckRun()).rejects.toThrow("当前没有可重试的执行");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { importSillyTavernPreset } from "../src/preset/importer.js";
|
||||
import { assemblePlayWorkerMessages } from "../src/preset/play-frame.js";
|
||||
import { assemblePresetMessages } from "../src/preset/assembler.js";
|
||||
import type { WorldInfoPack } from "../src/preset/world-info-pack.js";
|
||||
|
||||
const pack: WorldInfoPack = {
|
||||
worldBookBefore: [{ id: "lore", name: "舞台", content: "古镇夜雨" }],
|
||||
history: "用户:昨天来过",
|
||||
worldBookAfter: [{ id: "vars", name: "变量", content: "HP 3" }],
|
||||
turn: "我推门",
|
||||
postTurn: [{ id: "tail", name: "尾注", content: "勿剧透" }],
|
||||
};
|
||||
|
||||
describe("assemblePlayWorkerMessages", () => {
|
||||
it("walks extended prompt_order: ST pre, our before, history, our after, turn, ST post, postTurn", () => {
|
||||
const report = importSillyTavernPreset({
|
||||
prompts: [
|
||||
{ identifier: "main", name: "Main", role: "system", content: "MAIN" },
|
||||
{
|
||||
identifier: "charDescription",
|
||||
name: "Desc",
|
||||
role: "system",
|
||||
marker: true,
|
||||
},
|
||||
{
|
||||
identifier: "chatHistory",
|
||||
name: "History",
|
||||
role: "system",
|
||||
marker: true,
|
||||
},
|
||||
{ identifier: "jailbreak", name: "PHI", role: "system", content: "PHI" },
|
||||
],
|
||||
prompt_order: [
|
||||
{
|
||||
character_id: 1,
|
||||
order: [
|
||||
{ identifier: "main", enabled: true },
|
||||
{ identifier: "charDescription", enabled: true },
|
||||
{ identifier: "chatHistory", enabled: true },
|
||||
{ identifier: "jailbreak", enabled: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const messages = assemblePlayWorkerMessages({
|
||||
systemPrompt: "SKILL",
|
||||
preset: report.preset,
|
||||
pack,
|
||||
});
|
||||
|
||||
expect(messages.map((m) => [m.role, m.content])).toEqual([
|
||||
["system", "SKILL"],
|
||||
["system", "MAIN"],
|
||||
["system", "### 舞台\n\n古镇夜雨"],
|
||||
["system", "用户:昨天来过"],
|
||||
["system", "### 变量\n\nHP 3"],
|
||||
["user", "我推门"],
|
||||
["system", "PHI"],
|
||||
["system", "### 尾注\n\n勿剧透"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not fill ST charDescription even if resolver returns content", () => {
|
||||
const report = importSillyTavernPreset({
|
||||
prompts: [
|
||||
{
|
||||
identifier: "charDescription",
|
||||
name: "Desc",
|
||||
role: "system",
|
||||
marker: true,
|
||||
},
|
||||
{
|
||||
identifier: "chatHistory",
|
||||
name: "History",
|
||||
role: "system",
|
||||
marker: true,
|
||||
},
|
||||
],
|
||||
prompt_order: [
|
||||
{
|
||||
character_id: 1,
|
||||
order: [
|
||||
{ identifier: "charDescription", enabled: true },
|
||||
{ identifier: "chatHistory", enabled: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const messages = assemblePresetMessages(report.preset, (id) => {
|
||||
if (id === "charDescription") return "CARD";
|
||||
if (id === "chatHistory") return "HIST";
|
||||
return null;
|
||||
});
|
||||
expect(messages.map((m) => m.content)).toEqual(["HIST"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
SLOT_PLAY_LAYER_ACTIVE,
|
||||
SLOT_PLAY_TURN_QUEUE,
|
||||
isPlayLayerActive,
|
||||
readPlayTurnQueue,
|
||||
withPlayTurnQueue,
|
||||
} from "../src/skills/play-turn.js";
|
||||
|
||||
describe("play-turn slots", () => {
|
||||
it("reads a trimmed queue and ignores junk", () => {
|
||||
expect(readPlayTurnQueue(undefined)).toEqual([]);
|
||||
expect(
|
||||
readPlayTurnQueue({ [SLOT_PLAY_TURN_QUEUE]: [" auditor ", "", 3, "gm"] }),
|
||||
).toEqual(["auditor", "gm"]);
|
||||
});
|
||||
|
||||
it("writes queue or clears when empty", () => {
|
||||
const withIds = withPlayTurnQueue({ a: 1 }, ["gm", "narrator"]);
|
||||
expect(withIds[SLOT_PLAY_TURN_QUEUE]).toEqual(["gm", "narrator"]);
|
||||
expect(withPlayTurnQueue(withIds, []).playTurnQueue).toBeUndefined();
|
||||
});
|
||||
|
||||
it("detects play layer flag", () => {
|
||||
expect(isPlayLayerActive(undefined)).toBe(false);
|
||||
expect(isPlayLayerActive({ [SLOT_PLAY_LAYER_ACTIVE]: true })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -51,6 +51,31 @@ describe("present.v1", () => {
|
||||
expect(p.shell).toBe("chapter_reader");
|
||||
expect(p.blocks.body).toBe("hello");
|
||||
});
|
||||
|
||||
it("strips 输出.用户展示 heading from plain body", () => {
|
||||
const view = parsePresentPacket("### 输出.用户展示\n\n叮!账户来了。");
|
||||
expect(view.packet.blocks.body).toBe("叮!账户来了。");
|
||||
expect(String(view.packet.blocks.body)).not.toContain("用户展示");
|
||||
});
|
||||
|
||||
it("strips heading then parses present JSON", () => {
|
||||
const view = parsePresentPacket(
|
||||
'### 输出.用户展示\n\n{"schema":"present.v1","shell":"prose","blocks":{"body":"场面很长"}}',
|
||||
);
|
||||
expect(view.fallbackPlain).toBe(false);
|
||||
expect(view.packet.blocks.body).toBe("场面很长");
|
||||
});
|
||||
|
||||
it("strips heading inside blocks.body", () => {
|
||||
const view = parsePresentPacket(
|
||||
JSON.stringify({
|
||||
schema: "present.v1",
|
||||
shell: "prose",
|
||||
blocks: { body: "### 输出.用户展示\n正文" },
|
||||
}),
|
||||
);
|
||||
expect(view.packet.blocks.body).toBe("正文");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shell adaptation from reply-format", () => {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { importSillyTavernPreset } from "../src/preset/importer.js";
|
||||
import {
|
||||
assemblePresetProbe,
|
||||
DEFAULT_PROBE_CONTEXT,
|
||||
PROBE_SYSTEM_PROMPT,
|
||||
runPresetProbe,
|
||||
} from "../src/preset/probe.js";
|
||||
|
||||
function samplePreset() {
|
||||
return importSillyTavernPreset({
|
||||
prompts: [
|
||||
{ identifier: "main", name: "Main", role: "system", content: "MAIN" },
|
||||
{
|
||||
identifier: "charDescription",
|
||||
name: "Desc",
|
||||
role: "system",
|
||||
marker: true,
|
||||
},
|
||||
{
|
||||
identifier: "chatHistory",
|
||||
name: "History",
|
||||
role: "system",
|
||||
marker: true,
|
||||
},
|
||||
{ identifier: "jailbreak", name: "PHI", role: "system", content: "PHI" },
|
||||
],
|
||||
prompt_order: [
|
||||
{
|
||||
character_id: 1,
|
||||
order: [
|
||||
{ identifier: "main", enabled: true },
|
||||
{ identifier: "charDescription", enabled: true },
|
||||
{ identifier: "chatHistory", enabled: true },
|
||||
{ identifier: "jailbreak", enabled: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}).preset;
|
||||
}
|
||||
|
||||
describe("assemblePresetProbe", () => {
|
||||
it("fills the sandwich with sample lore, history, and the current turn", () => {
|
||||
const messages = assemblePresetProbe(samplePreset(), {
|
||||
message: "我推开门。",
|
||||
});
|
||||
|
||||
expect(messages.map((m) => [m.role, m.content])).toEqual([
|
||||
["system", PROBE_SYSTEM_PROMPT],
|
||||
["system", "MAIN"],
|
||||
["system", `### 试跑设定\n\n${DEFAULT_PROBE_CONTEXT.loreBefore}`],
|
||||
["system", DEFAULT_PROBE_CONTEXT.history],
|
||||
["system", `### 本轮状态\n\n${DEFAULT_PROBE_CONTEXT.loreAfter}`],
|
||||
["user", "我推开门。"],
|
||||
["system", "PHI"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("omits empty optional bags", () => {
|
||||
const messages = assemblePresetProbe(samplePreset(), {
|
||||
message: "你好",
|
||||
loreBefore: "",
|
||||
history: "",
|
||||
loreAfter: "",
|
||||
postTurn: "",
|
||||
});
|
||||
|
||||
expect(messages.map((m) => m.content)).toEqual([
|
||||
PROBE_SYSTEM_PROMPT,
|
||||
"MAIN",
|
||||
"你好",
|
||||
"PHI",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runPresetProbe", () => {
|
||||
it("returns assembled messages without calling a model", async () => {
|
||||
const result = await runPresetProbe({
|
||||
preset: samplePreset(),
|
||||
input: { message: "我推开门。" },
|
||||
});
|
||||
|
||||
expect(result.completed).toBe(false);
|
||||
expect(result.reply).toBeNull();
|
||||
expect(result.messages.some((m) => m.content === "我推开门。")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns the model reply when complete is provided", async () => {
|
||||
const result = await runPresetProbe({
|
||||
preset: samplePreset(),
|
||||
input: { message: "我推开门。" },
|
||||
complete: async () => ({ content: "请进。", reasoning: "短回复" }),
|
||||
});
|
||||
|
||||
expect(result.completed).toBe(true);
|
||||
expect(result.reply).toBe("请进。");
|
||||
expect(result.reasoning).toBe("短回复");
|
||||
});
|
||||
|
||||
it("keeps messages when generate fails", async () => {
|
||||
const result = await runPresetProbe({
|
||||
preset: samplePreset(),
|
||||
input: { message: "我推开门。" },
|
||||
complete: async () => {
|
||||
throw new Error("upstream down");
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.completed).toBe(false);
|
||||
expect(result.error).toBe("upstream down");
|
||||
expect(result.messages.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("rejects an empty turn", async () => {
|
||||
await expect(
|
||||
runPresetProbe({
|
||||
preset: samplePreset(),
|
||||
input: { message: " " },
|
||||
}),
|
||||
).rejects.toThrow("缺少本轮输入");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type {
|
||||
ChatMessage,
|
||||
CompleteResult,
|
||||
LlmProvider,
|
||||
} from "../src/llm/client.js";
|
||||
import { wrapLlmForSession } from "../src/llm/preset-wrapper.js";
|
||||
import type { LlmTrackingContext } from "../src/llm/token-tracker.js";
|
||||
import type { PresetPackage } from "../src/types/preset.js";
|
||||
|
||||
const samplePreset: PresetPackage = {
|
||||
id: "p1",
|
||||
name: "test-preset",
|
||||
source: "native",
|
||||
prompts: [
|
||||
{
|
||||
id: "sys",
|
||||
name: "系统",
|
||||
enabled: true,
|
||||
role: "system",
|
||||
content: "PRESET_RULES",
|
||||
marker: false,
|
||||
sourceIdentifier: "sys",
|
||||
},
|
||||
],
|
||||
promptOrder: [{ promptId: "sys", enabled: true, orderIndex: 0 }],
|
||||
generation: { temperature: 0.91, maxOutputTokens: 1234 },
|
||||
unsupported: [],
|
||||
importedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
function recordingProvider(): {
|
||||
provider: LlmProvider;
|
||||
received: Array<{ messages: ChatMessage[]; generation?: unknown }>;
|
||||
} {
|
||||
const received: Array<{ messages: ChatMessage[]; generation?: unknown }> = [];
|
||||
const provider: LlmProvider = {
|
||||
async complete(messages, options): Promise<CompleteResult> {
|
||||
received.push({ messages, generation: options?.generation });
|
||||
return { content: "ok", model: "rec-model" };
|
||||
},
|
||||
async completeStream(messages, options, callbacks) {
|
||||
received.push({ messages, generation: options?.generation });
|
||||
callbacks?.onContentDelta?.("ok");
|
||||
return { content: "ok", model: "rec-model" };
|
||||
},
|
||||
async completeWithTools(messages, options) {
|
||||
received.push({ messages, generation: options.generation });
|
||||
return { content: "ok", toolCalls: [], model: "rec-model" };
|
||||
},
|
||||
};
|
||||
return { provider, received };
|
||||
}
|
||||
|
||||
describe("wrapLlmForSession", () => {
|
||||
it("context trace matches the exact messages and generation sent to the inner LLM", async () => {
|
||||
const { provider, received } = recordingProvider();
|
||||
const ctx: LlmTrackingContext = {};
|
||||
const llm = wrapLlmForSession(provider, { current: ctx }, () => samplePreset);
|
||||
|
||||
await llm.complete(
|
||||
[{ role: "user", content: "TASK" }],
|
||||
{ caller: "worker:design-step" },
|
||||
);
|
||||
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0].messages).toEqual([{ role: "user", content: "TASK" }]);
|
||||
expect(received[0].generation).toMatchObject({
|
||||
temperature: 0.91,
|
||||
maxOutputTokens: 1234,
|
||||
});
|
||||
expect(ctx.pendingContextTrace?.messages).toEqual([
|
||||
{ role: "user", content: "TASK" },
|
||||
]);
|
||||
expect(ctx.pendingContextTrace?.generation).toMatchObject({
|
||||
temperature: 0.91,
|
||||
maxOutputTokens: 1234,
|
||||
});
|
||||
expect(ctx.pendingContextTrace?.caller).toBe("worker:design-step");
|
||||
expect(ctx.pendingContextTrace?.model).toBe("rec-model");
|
||||
});
|
||||
|
||||
it("completeStream also records the merged request", async () => {
|
||||
const { provider, received } = recordingProvider();
|
||||
const ctx: LlmTrackingContext = {};
|
||||
const llm = wrapLlmForSession(provider, { current: ctx }, () => samplePreset);
|
||||
const chunks: string[] = [];
|
||||
|
||||
await llm.completeStream!(
|
||||
[{ role: "user", content: "TASK" }],
|
||||
{ caller: "worker:stream" },
|
||||
{ onContentDelta: (d) => chunks.push(d) },
|
||||
);
|
||||
|
||||
expect(chunks.join("")).toBe("ok");
|
||||
expect(received[0].messages).toEqual([{ role: "user", content: "TASK" }]);
|
||||
expect(ctx.pendingContextTrace?.messages).toEqual(received[0].messages);
|
||||
});
|
||||
});
|
||||
+59
-2
@@ -52,16 +52,73 @@ const samplePreset = {
|
||||
describe("importSillyTavernPreset", () => {
|
||||
it("picks prompt_order with most enabled content", () => {
|
||||
const report = importSillyTavernPreset(samplePreset, { name: "test" });
|
||||
expect(report.enabledCount).toBe(3);
|
||||
expect(report.preset.promptOrder.map((o) => o.promptId)).toEqual([
|
||||
"custom-a",
|
||||
"custom-b",
|
||||
"main",
|
||||
"worldBookBefore",
|
||||
"chatHistory",
|
||||
"worldBookAfter",
|
||||
"currentTurn",
|
||||
"postTurn",
|
||||
]);
|
||||
expect(report.enabledCount).toBe(8);
|
||||
expect(report.preset.generation.temperature).toBe(1);
|
||||
expect(report.preset.generation.maxOutputTokens).toBe(60000);
|
||||
expect(report.warnings.some((w) => w.includes("prompt_order"))).toBe(true);
|
||||
});
|
||||
|
||||
it("inserts app markers around chatHistory and leaves ST card/WI empty", () => {
|
||||
const report = importSillyTavernPreset({
|
||||
prompts: [
|
||||
{ identifier: "main", name: "Main", role: "system", content: "MAIN" },
|
||||
{
|
||||
identifier: "charDescription",
|
||||
name: "Char Description",
|
||||
role: "system",
|
||||
marker: true,
|
||||
},
|
||||
{
|
||||
identifier: "chatHistory",
|
||||
name: "Chat History",
|
||||
role: "system",
|
||||
marker: true,
|
||||
},
|
||||
{
|
||||
identifier: "jailbreak",
|
||||
name: "Post-History",
|
||||
role: "system",
|
||||
content: "PHI",
|
||||
},
|
||||
],
|
||||
prompt_order: [
|
||||
{
|
||||
character_id: 1,
|
||||
order: [
|
||||
{ identifier: "main", enabled: true },
|
||||
{ identifier: "charDescription", enabled: true },
|
||||
{ identifier: "chatHistory", enabled: true },
|
||||
{ identifier: "jailbreak", enabled: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(report.preset.promptOrder.map((o) => o.promptId)).toEqual([
|
||||
"main",
|
||||
"charDescription",
|
||||
"worldBookBefore",
|
||||
"chatHistory",
|
||||
"worldBookAfter",
|
||||
"currentTurn",
|
||||
"jailbreak",
|
||||
"postTurn",
|
||||
]);
|
||||
const messages = assemblePresetMessages(report.preset);
|
||||
expect(messages).toEqual([
|
||||
{ role: "system", content: "MAIN" },
|
||||
{ role: "system", content: "PHI" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("assemblePresetMessages", () => {
|
||||
@@ -79,7 +136,7 @@ describe("listEnabledPresetEntries", () => {
|
||||
it("lists enabled order with inject flags", () => {
|
||||
const report = importSillyTavernPreset(samplePreset);
|
||||
const entries = listEnabledPresetEntries(report.preset);
|
||||
expect(entries.length).toBe(3);
|
||||
expect(entries.length).toBe(8);
|
||||
expect(countInjectingEntries(entries)).toBe(2);
|
||||
expect(entries.filter((e) => e.willInject).map((e) => e.content)).toEqual([
|
||||
"instruction A",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MockLlmProvider } from "../src/llm/client.js";
|
||||
import {
|
||||
isAbortError,
|
||||
runWithAbortSignal,
|
||||
throwIfAborted,
|
||||
} from "../src/llm/run-abort.js";
|
||||
|
||||
describe("run abort", () => {
|
||||
it("isAbortError detects AbortError name", () => {
|
||||
const err = new Error("This operation was aborted");
|
||||
err.name = "AbortError";
|
||||
expect(isAbortError(err)).toBe(true);
|
||||
expect(isAbortError(new Error("LLM request failed"))).toBe(false);
|
||||
});
|
||||
|
||||
it("throwIfAborted raises AbortError when the signal is aborted", () => {
|
||||
const ac = new AbortController();
|
||||
ac.abort();
|
||||
expect(() => throwIfAborted(ac.signal)).toThrowError(/aborted/i);
|
||||
});
|
||||
|
||||
it("mock LLM respects the active abort signal", async () => {
|
||||
const llm = new MockLlmProvider(["ok"]);
|
||||
const ac = new AbortController();
|
||||
ac.abort();
|
||||
await expect(
|
||||
runWithAbortSignal(ac.signal, () => llm.complete([{ role: "user", content: "x" }])),
|
||||
).rejects.toMatchObject({ name: "AbortError" });
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
filterBlackboardForInstance,
|
||||
isRunPhaseBlackboardTag,
|
||||
materializeInstanceSnapshotPayload,
|
||||
} from "../src/book/snapshot-filters.js";
|
||||
@@ -12,6 +11,10 @@ describe("snapshot-filters", () => {
|
||||
expect(isRunPhaseBlackboardTag("角色.A.设定")).toBe(false);
|
||||
expect(isRunPhaseBlackboardTag("博弈.规则.草稿")).toBe(false);
|
||||
expect(isRunPhaseBlackboardTag("运行.事件流")).toBe(true);
|
||||
expect(isRunPhaseBlackboardTag("输出.开场白")).toBe(false);
|
||||
expect(isRunPhaseBlackboardTag("运行.初始变量")).toBe(false);
|
||||
expect(isRunPhaseBlackboardTag("变量.当前")).toBe(false);
|
||||
expect(isRunPhaseBlackboardTag("对话.历史")).toBe(true);
|
||||
expect(isRunPhaseBlackboardTag("角色.A.思考")).toBe(true);
|
||||
expect(isRunPhaseBlackboardTag("世界.当前状态")).toBe(true);
|
||||
});
|
||||
@@ -19,6 +22,8 @@ describe("snapshot-filters", () => {
|
||||
it("materializeInstanceSnapshotPayload removes run blackboard items", () => {
|
||||
const session = createSession("default");
|
||||
session.slots.startupCompleted = true;
|
||||
session.slots.playLayerActive = true;
|
||||
session.slots.playTurnQueue = ["narrator"];
|
||||
session.artifacts = [
|
||||
{
|
||||
id: "a1",
|
||||
@@ -44,6 +49,7 @@ describe("snapshot-filters", () => {
|
||||
{ id: "1", tag: "情境.实验.设定", content: "x", source: "worker", createdAt: "", updatedAt: "" },
|
||||
{ id: "2", tag: "运行.事件流", content: "y", source: "worker", createdAt: "", updatedAt: "" },
|
||||
{ id: "3", tag: "角色.A.设定", content: "z", source: "worker", createdAt: "", updatedAt: "" },
|
||||
{ id: "4", tag: "对话.历史", content: "## 用户\n\n买金", source: "runtime", createdAt: "", updatedAt: "" },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -53,6 +59,7 @@ describe("snapshot-filters", () => {
|
||||
]);
|
||||
expect(runtimeSession.artifacts).toHaveLength(1);
|
||||
expect(runtimeSession.artifacts[0].workerId).toBe("setup-scenario");
|
||||
expect(filterBlackboardForInstance).toBeDefined();
|
||||
expect(runtimeSession.slots.playLayerActive).toBeUndefined();
|
||||
expect(runtimeSession.slots.playTurnQueue).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,5 +98,14 @@ describe("worker-declaration", () => {
|
||||
},
|
||||
};
|
||||
expect(inferLifecycleStage(s2)).toBe("play");
|
||||
|
||||
const s3 = {
|
||||
...sessionWithAcceptedWorkerSet(),
|
||||
slots: {
|
||||
...sessionWithAcceptedWorkerSet().slots,
|
||||
playLayerActive: true,
|
||||
},
|
||||
};
|
||||
expect(inferLifecycleStage(s3)).toBe("play");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,7 +191,7 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("特权与惊惶");
|
||||
});
|
||||
|
||||
it("does not accept a fragment without 正文; keeps 追问 for the LLM loop", () => {
|
||||
it("keeps incomplete fragment (no 正文) for review; 追问 hangs as sidecar", () => {
|
||||
const result = parseWorkerResponseForTest(
|
||||
JSON.stringify({
|
||||
outputs: {
|
||||
@@ -210,12 +210,12 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
}),
|
||||
["设计.美学纲领与交互范式"],
|
||||
);
|
||||
expect(result.outputs["设计.美学纲领与交互范式"]).toBeUndefined();
|
||||
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("还差站位");
|
||||
expect(result.askUser?.length).toBeGreaterThan(0);
|
||||
expect(result.askUser?.[0]?.prompt).toContain("你代入吗");
|
||||
});
|
||||
|
||||
it("asks to continue the loop when fragment JSON is unusable", () => {
|
||||
it("keeps truncated fragment JSON as a reviewable draft", () => {
|
||||
const result = parseWorkerResponseForTest(
|
||||
JSON.stringify({
|
||||
outputs: {
|
||||
@@ -226,7 +226,54 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
}),
|
||||
["设计.美学纲领与交互范式"],
|
||||
);
|
||||
expect(result.outputs["设计.美学纲领与交互范式"]).toBeUndefined();
|
||||
expect(result.askUser?.[0]?.prompt).toContain("还没写出可验收的产物");
|
||||
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("孤立免疫");
|
||||
expect(result.askUser).toBeUndefined();
|
||||
});
|
||||
|
||||
it("canonicalizes context-order agents[] into slots on the artifact tag", () => {
|
||||
const result = parseWorkerResponseForTest(
|
||||
JSON.stringify({
|
||||
outputs: {
|
||||
"设计.上下文投影排序": {
|
||||
schema: "context-order.v1",
|
||||
brief: "主世界层",
|
||||
agents: [
|
||||
{
|
||||
id: "gm",
|
||||
enabled: true,
|
||||
inserts: [
|
||||
{ order: 0, ref: "worker.persona", projection: "fixed" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
summary: "ok",
|
||||
}),
|
||||
["设计.上下文投影排序"],
|
||||
);
|
||||
const doc = JSON.parse(result.outputs["设计.上下文投影排序"]!);
|
||||
expect(doc.slots[0].ref).toBe("world-simulator");
|
||||
expect(doc.play_slots.gm).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps a whole-response truncated JSON as the artifact draft", () => {
|
||||
const raw =
|
||||
'{"outputs":{"设计.上下文投影排序":{"schema":"context-order.v1","brief":"旁观+转述","agents":[';
|
||||
const result = parseWorkerResponseForTest(raw, ["设计.上下文投影排序"]);
|
||||
expect(result.outputs["设计.上下文投影排序"]).toContain("context-order.v1");
|
||||
expect(result.askUser).toBeUndefined();
|
||||
});
|
||||
|
||||
it("puts full 输出.用户展示 into preview without tag fence or 4000 cap", () => {
|
||||
const body = "场面".repeat(2100);
|
||||
const result = sanitizeWorkerSetOutputs({
|
||||
outputs: { "输出.用户展示": body },
|
||||
summary: "ok",
|
||||
preview: "旧",
|
||||
});
|
||||
expect(result.preview).toBe(body);
|
||||
expect(result.preview).not.toContain("### ");
|
||||
expect(result.preview.length).toBeGreaterThan(4000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Blackboard } from "../src/blackboard/blackboard.js";
|
||||
import { DIALOGUE_HISTORY_TAG } from "../src/skills/dialogue-history.js";
|
||||
import {
|
||||
CURRENT_TURN_TAG,
|
||||
resolveWorldInfoMarker,
|
||||
worldInfoPackFromSegments,
|
||||
} from "../src/preset/world-info-pack.js";
|
||||
import {
|
||||
CURRENT_TURN,
|
||||
WORLD_BOOK_AFTER,
|
||||
WORLD_BOOK_BEFORE,
|
||||
} from "../src/preset/markers.js";
|
||||
import type { ContextSegmentDef } from "../src/skills/context-segments.js";
|
||||
|
||||
describe("worldInfoPackFromSegments", () => {
|
||||
it("splits lore / history / after-history / turn by the two anchors", () => {
|
||||
const bb = new Blackboard();
|
||||
const segments: ContextSegmentDef[] = [
|
||||
{
|
||||
id: "stage",
|
||||
tier: "static",
|
||||
tags: [],
|
||||
inline: "古镇夜雨",
|
||||
label: "## 舞台骨架",
|
||||
},
|
||||
{
|
||||
id: "hist",
|
||||
tier: "dynamic",
|
||||
tags: [DIALOGUE_HISTORY_TAG],
|
||||
},
|
||||
{
|
||||
id: "vars",
|
||||
tier: "dynamic",
|
||||
tags: ["变量.当前"],
|
||||
label: "## 变量",
|
||||
},
|
||||
{
|
||||
id: "turn",
|
||||
tier: "dynamic",
|
||||
tags: [CURRENT_TURN_TAG],
|
||||
},
|
||||
{
|
||||
id: "note",
|
||||
tier: "dynamic",
|
||||
tags: [],
|
||||
inline: "尾注",
|
||||
label: "## 本轮后",
|
||||
},
|
||||
];
|
||||
const pack = worldInfoPackFromSegments({
|
||||
segments,
|
||||
inputs: {
|
||||
[DIALOGUE_HISTORY_TAG]: "用户:你好",
|
||||
"变量.当前": "HP 3",
|
||||
[CURRENT_TURN_TAG]: "我推门进去",
|
||||
},
|
||||
blackboard: bb,
|
||||
});
|
||||
expect(pack.worldBookBefore.map((e) => e.content)).toEqual(["古镇夜雨"]);
|
||||
expect(pack.history).toContain("你好");
|
||||
expect(pack.worldBookAfter.map((e) => e.content)).toEqual(["HP 3"]);
|
||||
expect(pack.turn).toBe("我推门进去");
|
||||
expect(pack.postTurn.map((e) => e.content)).toEqual(["尾注"]);
|
||||
expect(resolveWorldInfoMarker(pack, WORLD_BOOK_BEFORE)).toContain("古镇夜雨");
|
||||
expect(resolveWorldInfoMarker(pack, WORLD_BOOK_AFTER)).toContain("HP 3");
|
||||
expect(resolveWorldInfoMarker(pack, CURRENT_TURN)).toBe("我推门进去");
|
||||
expect(resolveWorldInfoMarker(pack, "charDescription")).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user