Add capability-probed structured outputs and a prototype/instance creation graph.
Probe API profiles for json_schema/tools, flatten worker artifacts onto a strict schema, and let users spawn or revise graph nodes instead of a one-shot DAG. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
CAPABILITY_PROBE_REASONING_EFFORT,
|
||||
resolveStructuredDeliveryMode,
|
||||
type ProfileCapabilities,
|
||||
} from "../src/llm/capabilities.js";
|
||||
import { normalizeProfileReasoningEffort } from "../src/config/api-profiles.js";
|
||||
import {
|
||||
coerceToFlatArtifact,
|
||||
flatArtifactToFragmentDoc,
|
||||
FLAT_ARTIFACT_SCHEMA_ID,
|
||||
} from "../src/skills/flat-artifact.js";
|
||||
|
||||
describe("capability probe defaults", () => {
|
||||
it("always probes with low reasoning effort", () => {
|
||||
expect(CAPABILITY_PROBE_REASONING_EFFORT).toBe("low");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeProfileReasoningEffort", () => {
|
||||
it("keeps valid effort and drops auto/empty", () => {
|
||||
expect(normalizeProfileReasoningEffort("low")).toBe("low");
|
||||
expect(normalizeProfileReasoningEffort("MAX")).toBe("max");
|
||||
expect(normalizeProfileReasoningEffort("auto")).toBeUndefined();
|
||||
expect(normalizeProfileReasoningEffort("")).toBeUndefined();
|
||||
expect(normalizeProfileReasoningEffort("nope")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveStructuredDeliveryMode", () => {
|
||||
it("prefers json_schema then forced_tool then json_object", () => {
|
||||
const base: ProfileCapabilities = {
|
||||
chat: "ok",
|
||||
jsonObject: "ok",
|
||||
jsonSchema: "fail",
|
||||
tools: "ok",
|
||||
forcedTool: "ok",
|
||||
strictTool: "fail",
|
||||
};
|
||||
expect(resolveStructuredDeliveryMode(base)).toBe("forced_tool");
|
||||
expect(
|
||||
resolveStructuredDeliveryMode({ ...base, jsonSchema: "ok" }),
|
||||
).toBe("json_schema");
|
||||
expect(
|
||||
resolveStructuredDeliveryMode({
|
||||
...base,
|
||||
jsonSchema: "fail",
|
||||
forcedTool: "fail",
|
||||
}),
|
||||
).toBe("json_object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("flat artifact", () => {
|
||||
it("round-trips deep fragment body into sections", () => {
|
||||
const flat = coerceToFlatArtifact({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "点题",
|
||||
mount: ["narrator"],
|
||||
稳变: "stable",
|
||||
正文: {
|
||||
设定逻辑: { 变造: "丧尸" },
|
||||
美学纲领: "出租屋",
|
||||
},
|
||||
开放问题: ["后宫"],
|
||||
追问: {
|
||||
题目: [{ 问: "身份?", 建议选项: ["幸存者", "其他"] }],
|
||||
},
|
||||
});
|
||||
expect(flat?.sections.map((s) => s.id)).toEqual(["设定逻辑", "美学纲领"]);
|
||||
const frag = flatArtifactToFragmentDoc(flat!);
|
||||
expect(frag.schema).toBe("context-fragment.v1");
|
||||
expect(frag.brief).toBe("点题");
|
||||
expect((frag.追问 as { 题目: unknown[] }).题目[0]).toMatchObject({
|
||||
问: "身份?",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not coerce a creation-flow DAG (brief + steps) into a flat artifact", () => {
|
||||
const flow = {
|
||||
brief: "单角代入",
|
||||
status: "open",
|
||||
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
|
||||
};
|
||||
expect(coerceToFlatArtifact(flow)).toBeNull();
|
||||
});
|
||||
|
||||
it("does not coerce a brief-only object into an empty flat artifact", () => {
|
||||
expect(coerceToFlatArtifact({ brief: "点题" })).toBeNull();
|
||||
});
|
||||
|
||||
it("accepts native flat wire format", () => {
|
||||
const flat = coerceToFlatArtifact({
|
||||
schema: FLAT_ARTIFACT_SCHEMA_ID,
|
||||
skill: "x",
|
||||
brief: "b",
|
||||
mount: [],
|
||||
stability: "semi",
|
||||
sections: [{ id: "a", title: "A", text: "hello" }],
|
||||
open_questions: [],
|
||||
questions: [],
|
||||
summary: "s",
|
||||
});
|
||||
expect(flat?.sections[0]?.text).toBe("hello");
|
||||
});
|
||||
|
||||
it("round-trips nested body objects through flat stringify + fragment parse", () => {
|
||||
const nested = {
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "点题",
|
||||
mount: [],
|
||||
正文: {
|
||||
交互范式: { 前置配置: { 用户角色: "唯一免疫者" } },
|
||||
美学纲领: { 体验内核: "报复爽", 呈现要点: "内外对比" },
|
||||
设定逻辑: { 变造: { 原型: "丧尸" } },
|
||||
},
|
||||
开放问题: [],
|
||||
追问: { 题目: [] },
|
||||
};
|
||||
const flat = coerceToFlatArtifact(nested);
|
||||
expect(flat).not.toBeNull();
|
||||
const frag = flatArtifactToFragmentDoc(flat!);
|
||||
expect(frag.技能).toBe("美学纲领与交互范式");
|
||||
expect((frag.正文 as Record<string, unknown>).交互范式).toEqual({
|
||||
前置配置: { 用户角色: "唯一免疫者" },
|
||||
});
|
||||
expect(
|
||||
((frag.正文 as Record<string, unknown>).美学纲领 as Record<string, unknown>)
|
||||
.体验内核,
|
||||
).toBe("报复爽");
|
||||
});
|
||||
});
|
||||
@@ -48,6 +48,29 @@ describe("pruneContextTraces", () => {
|
||||
expect(pruned.map((m) => m.id)).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("pins design-flow traces while pruning others", () => {
|
||||
const messages = [
|
||||
{
|
||||
id: "flow",
|
||||
contextTrace: buildContextTrace({
|
||||
caller: "worker:design-flow",
|
||||
messages: [{ role: "user", content: "plan" }],
|
||||
}),
|
||||
},
|
||||
msg("1", true),
|
||||
msg("2", true),
|
||||
msg("3", true),
|
||||
];
|
||||
const pruned = pruneContextTraces(messages, 1);
|
||||
expect(pruned[0]?.contextTrace?.caller).toBe("worker:design-flow");
|
||||
expect(pruned.map((m) => Boolean(m.contextTrace))).toEqual([
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
]);
|
||||
});
|
||||
|
||||
it("is a no-op when under the limit", () => {
|
||||
const messages = [msg("1", true), msg("2", true)];
|
||||
const pruned = pruneContextTraces(messages, 5);
|
||||
|
||||
+393
-34
@@ -6,6 +6,8 @@ import {
|
||||
artifactTagForStep,
|
||||
extractModuleOpening,
|
||||
formatCreationFlowForUser,
|
||||
deriveFlowStepTitle,
|
||||
titleFromSuggestion,
|
||||
formatFlowProgressForAgent,
|
||||
formatModuleCatalogForAgent,
|
||||
formatRecipeCatalogForAgent,
|
||||
@@ -18,6 +20,13 @@ import {
|
||||
mergeCreationFlowPreservingAccepted,
|
||||
needsFlowExpansion,
|
||||
nextPendingStep,
|
||||
listReadySteps,
|
||||
computeStepLayers,
|
||||
spawnRepeatableCreationStep,
|
||||
spawnInstanceFromPrototype,
|
||||
hasSelectableCreationWork,
|
||||
removeUnstartedInstance,
|
||||
looksLikeCreationFlowDoc,
|
||||
parseCreationFlow,
|
||||
pickRecordedStepId,
|
||||
parseModuleCatalog,
|
||||
@@ -29,6 +38,7 @@ import {
|
||||
shouldSkipModuleOpening,
|
||||
isProgressPointerTag,
|
||||
stringifyCreationFlow,
|
||||
stripReviseSteps,
|
||||
creationFlowFromRecipeSeed,
|
||||
validateCreationFlow,
|
||||
validateStepReadyToRun,
|
||||
@@ -51,6 +61,10 @@ modules:
|
||||
declaration: 锚定实例
|
||||
artifact: 设计.具体实例
|
||||
repeatable: true
|
||||
- name: 开场白与开场变量
|
||||
declaration: 收口
|
||||
artifact: 设计.开场白与开场变量
|
||||
closer: true
|
||||
`)!;
|
||||
|
||||
describe("creation-flow", () => {
|
||||
@@ -145,7 +159,44 @@ describe("creation-flow", () => {
|
||||
acceptedStepIds: ["美学纲领与交互范式"],
|
||||
});
|
||||
expect(merged.restored).toEqual([]);
|
||||
expect(merged.raw).toBe(rewritten);
|
||||
expect(parseCreationFlow(merged.raw)!.steps.map((s) => s.id)).toEqual([
|
||||
"美学纲领与交互范式",
|
||||
"实现机制",
|
||||
]);
|
||||
});
|
||||
|
||||
it("drops planner revise steps when merging flow", () => {
|
||||
const prev = `{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }
|
||||
]
|
||||
}`;
|
||||
const rewritten = `{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{
|
||||
"id": "美学纲领与交互范式·改",
|
||||
"name": "美学纲领与交互范式",
|
||||
"mode": "revise",
|
||||
"revises": "美学纲领与交互范式",
|
||||
"depends_on": ["美学纲领与交互范式"]
|
||||
},
|
||||
{ "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式·改"] }
|
||||
]
|
||||
}`;
|
||||
const merged = mergeCreationFlowPreservingAccepted({
|
||||
prevRaw: prev,
|
||||
nextRaw: rewritten,
|
||||
acceptedStepIds: ["美学纲领与交互范式"],
|
||||
});
|
||||
const flow = parseCreationFlow(merged.raw)!;
|
||||
expect(flow.steps.map((s) => s.id)).toEqual([
|
||||
"美学纲领与交互范式",
|
||||
"实现机制",
|
||||
]);
|
||||
expect(flow.steps[1]?.depends_on).toEqual(["美学纲领与交互范式"]);
|
||||
});
|
||||
|
||||
it("allows duplicate capability names with distinct ids", () => {
|
||||
@@ -185,6 +236,24 @@ describe("creation-flow", () => {
|
||||
expect(flow?.steps[0]?.name).toBe("美学纲领与交互范式");
|
||||
});
|
||||
|
||||
it("extracts JSON from a code fence", () => {
|
||||
const flow = parseCreationFlow(
|
||||
"```json\n{\"brief\":\"单角代入\",\"steps\":[{\"name\":\"美学纲领与交互范式\",\"depends_on\":[]}]}\n```",
|
||||
);
|
||||
expect(flow?.brief).toBe("单角代入");
|
||||
expect(flow?.steps[0]?.name).toBe("美学纲领与交互范式");
|
||||
});
|
||||
|
||||
it("recognizes a DAG object and ignores brief-only objects", () => {
|
||||
expect(
|
||||
looksLikeCreationFlowDoc({
|
||||
brief: "单角代入",
|
||||
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(looksLikeCreationFlowDoc({ brief: "单角代入" })).toBe(false);
|
||||
});
|
||||
|
||||
it("validates deps must appear earlier", () => {
|
||||
const bad = parseCreationFlow(`{
|
||||
"steps": [
|
||||
@@ -250,13 +319,93 @@ describe("creation-flow", () => {
|
||||
depends_on: [],
|
||||
declaration: "站位与体验契约",
|
||||
});
|
||||
expect(view.steps[2]?.occurrence).toBe(2);
|
||||
expect(view.steps[2]?.role).toBe("instance");
|
||||
expect(view.steps[2]?.occurrence).toBe(1);
|
||||
expect(view.steps[1]?.role).toBe("prototype");
|
||||
expect(view.steps[2]?.repeatable).toBe(true);
|
||||
expect(view.steps[0]?.runState).toBe("current");
|
||||
expect(view.steps[0]?.runState).toBe("pending");
|
||||
expect(view.steps[0]?.layer).toBe(0);
|
||||
expect(view.steps[0]?.ready).toBe(true);
|
||||
expect(view.steps[0]?.selectable).toBe(true);
|
||||
expect(view.steps[1]?.runState).toBe("pending");
|
||||
expect(view.steps[1]?.layer).toBe(1);
|
||||
expect(view.steps[1]?.ready).toBe(false);
|
||||
expect(view.steps[2]?.runState).toBe("pending");
|
||||
});
|
||||
|
||||
it("shows a short title for 生成规则 / 具体实例 prototypes and instances", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"steps": [
|
||||
{
|
||||
"id": "生成规则",
|
||||
"name": "生成规则",
|
||||
"role": "prototype",
|
||||
"suggestion": "幸存者生成规则:锚定出租屋内的女性租客",
|
||||
"depends_on": []
|
||||
},
|
||||
{
|
||||
"id": "生成规则#1",
|
||||
"name": "生成规则",
|
||||
"role": "instance",
|
||||
"from": "生成规则",
|
||||
"depends_on": []
|
||||
},
|
||||
{
|
||||
"id": "具体实例",
|
||||
"name": "具体实例",
|
||||
"role": "prototype",
|
||||
"suggestion": "按幸存者规则生成开局女租客",
|
||||
"depends_on": ["生成规则"]
|
||||
},
|
||||
{
|
||||
"id": "具体实例#1",
|
||||
"name": "具体实例",
|
||||
"role": "instance",
|
||||
"from": "具体实例",
|
||||
"params": { "batch_goal": "开局女租客", "rule_id": "survivors" },
|
||||
"depends_on": ["生成规则#1"]
|
||||
}
|
||||
]
|
||||
}`)!;
|
||||
const untitled = formatCreationFlowForUser(flow, sampleCatalog);
|
||||
expect(untitled.steps.find((s) => s.id === "生成规则")?.title).toBe(
|
||||
"幸存者生成规则",
|
||||
);
|
||||
expect(untitled.steps.find((s) => s.id === "生成规则#1")?.title).toBe(
|
||||
"幸存者生成规则",
|
||||
);
|
||||
expect(untitled.steps.find((s) => s.id === "具体实例")?.title).toBe(
|
||||
"按幸存者规则生成开局女租客",
|
||||
);
|
||||
expect(untitled.steps.find((s) => s.id === "具体实例#1")?.title).toBe(
|
||||
"开局女租客",
|
||||
);
|
||||
|
||||
const accepted = formatCreationFlowForUser(flow, sampleCatalog, [], [], {
|
||||
"生成规则#1": { summary: "生成规则 · 幸存者 · 预生成并动态" },
|
||||
"具体实例#1": { summary: "具体实例 · 女租客 · 2条" },
|
||||
});
|
||||
expect(accepted.steps.find((s) => s.id === "生成规则#1")?.title).toBe(
|
||||
"幸存者",
|
||||
);
|
||||
expect(accepted.steps.find((s) => s.id === "具体实例#1")?.title).toBe(
|
||||
"女租客",
|
||||
);
|
||||
expect(
|
||||
deriveFlowStepTitle({
|
||||
name: "生成规则",
|
||||
hint: {
|
||||
content: {
|
||||
正文: { 本步参数: { target: "女租客" } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe("女租客");
|
||||
expect(titleFromSuggestion("怪物生成规则:按族群抽样")).toBe(
|
||||
"怪物生成规则",
|
||||
);
|
||||
});
|
||||
|
||||
it("marks accepted steps done and the next pending current", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"brief": "网恋回合",
|
||||
@@ -272,9 +421,10 @@ describe("creation-flow", () => {
|
||||
]);
|
||||
expect(view.steps.map((s) => s.runState)).toEqual([
|
||||
"done",
|
||||
"current",
|
||||
"pending",
|
||||
"pending",
|
||||
]);
|
||||
expect(view.steps[1]?.selectable).toBe(true);
|
||||
});
|
||||
|
||||
it("loads world-simulator catalog and formats for agent", async () => {
|
||||
@@ -317,7 +467,7 @@ describe("creation-flow", () => {
|
||||
expect(block).toContain("上下文投影排序〔程序步〕");
|
||||
expect(block).toContain("何时用");
|
||||
expect(block).toContain("何时不用");
|
||||
expect(block).toContain("规划产物字段");
|
||||
expect(block).toContain("role=prototype");
|
||||
expect(block).toContain("编排参数");
|
||||
expect(block).not.toContain("设计.美学纲领与交互范式");
|
||||
});
|
||||
@@ -403,6 +553,22 @@ steps:
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("creationFlowFromRecipeSeed follows the recipe steps, not a fixed first node", () => {
|
||||
const detail = parseRecipeYaml(
|
||||
`
|
||||
when: 其它剧本
|
||||
steps:
|
||||
- name: 舞台骨架
|
||||
depends_on: []
|
||||
- name: 实现机制
|
||||
depends_on: ["舞台骨架"]
|
||||
`,
|
||||
{ id: "other", name: "其它", declaration: "测" },
|
||||
);
|
||||
const seeded = creationFlowFromRecipeSeed(detail);
|
||||
expect(seeded?.steps.map((s) => s.name)).toEqual(["舞台骨架", "实现机制"]);
|
||||
});
|
||||
|
||||
it("falls back to legacy hint when methodology absent", () => {
|
||||
const detail = parseRecipeYaml(
|
||||
`
|
||||
@@ -478,7 +644,7 @@ recipes:
|
||||
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
||||
expect(loaded.promptBody).toContain("已验收");
|
||||
expect(loaded.promptBody).toContain("禁止再排新建");
|
||||
expect(loaded.promptBody).toContain("mode=revise");
|
||||
expect(loaded.promptBody).toContain("禁止排 mode=revise");
|
||||
expect(loaded.promptBody).toContain("生成规则");
|
||||
});
|
||||
|
||||
@@ -496,7 +662,36 @@ recipes:
|
||||
expect(text).toContain("草案已有、尚未验收");
|
||||
expect(text).toContain("美学纲领与交互范式");
|
||||
expect(text).toContain("不要当作新规划再写一遍");
|
||||
expect(text).toContain("可反复追加:生成规则、具体实例");
|
||||
expect(text).toContain("目录〔可反复〕:生成规则、具体实例");
|
||||
});
|
||||
|
||||
it("tells design-flow to insert nodes before an unaccepted closer", () => {
|
||||
const catalog = parseModuleCatalog(`
|
||||
modules:
|
||||
- name: 美学纲领与交互范式
|
||||
declaration: 站位
|
||||
artifact: 设计.美学纲领与交互范式
|
||||
- name: 开场白与开场变量
|
||||
declaration: 收口
|
||||
artifact: 设计.开场白与开场变量
|
||||
closer: true
|
||||
`)!;
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "closed",
|
||||
"steps": [
|
||||
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式"] }
|
||||
]
|
||||
}`)!;
|
||||
const text = formatFlowProgressForAgent({
|
||||
flow,
|
||||
acceptedStepIds: ["美学纲领与交互范式"],
|
||||
catalog,
|
||||
});
|
||||
expect(text).toContain("〔收口〕");
|
||||
expect(text).toContain("开场白与开场变量");
|
||||
expect(text).toContain("插在收口之前");
|
||||
expect(text).toContain("status 改回 open");
|
||||
});
|
||||
|
||||
it("injects aesthetics-interaction module prompt into design-step", async () => {
|
||||
@@ -637,7 +832,7 @@ recipes:
|
||||
expect(missingView.steps[0]?.paramsMissing).toBeUndefined();
|
||||
|
||||
const block = formatModuleCatalogForAgent(catalog!);
|
||||
expect(block).toContain("规划产物字段");
|
||||
expect(block).toContain("role=prototype");
|
||||
expect(block).toContain("target");
|
||||
});
|
||||
|
||||
@@ -662,8 +857,7 @@ recipes:
|
||||
acceptedStepNames: [],
|
||||
},
|
||||
);
|
||||
expect(loaded.promptBody).toContain("【规划产物】");
|
||||
expect(loaded.promptBody).not.toContain("勿再问「生成什么");
|
||||
expect(loaded.promptBody).toContain("【本步对象】");
|
||||
expect(loaded.promptBody).toContain("target: 怪物");
|
||||
expect(loaded.promptBody).toContain("rule_id: monsters");
|
||||
expect(loaded.worker.outputTags).toContain("设计.生成规则");
|
||||
@@ -693,14 +887,22 @@ modules:
|
||||
undefined,
|
||||
{
|
||||
flowRaw: JSON.stringify({
|
||||
steps: [{ id: "生成规则", name: "生成规则", depends_on: [] }],
|
||||
steps: [
|
||||
{
|
||||
id: "生成规则#1",
|
||||
name: "生成规则",
|
||||
role: "instance",
|
||||
from: "生成规则",
|
||||
depends_on: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
currentStepName: "生成规则",
|
||||
currentStepName: "生成规则#1",
|
||||
acceptedStepNames: [],
|
||||
},
|
||||
);
|
||||
expect(loaded.promptBody).toContain("【规划产物】");
|
||||
expect(loaded.promptBody).toContain("尚未规划本步具体写什么");
|
||||
expect(loaded.promptBody).toContain("【本步对象】");
|
||||
expect(loaded.promptBody).toContain("尚未钉本步具体写什么");
|
||||
});
|
||||
|
||||
it("nextPendingStep respects deps and accepted by id", () => {
|
||||
@@ -780,7 +982,7 @@ modules:
|
||||
expect(isCreationFlowComplete(legacy, ["美学纲领与交互范式"])).toBe(true);
|
||||
});
|
||||
|
||||
it("allows revising a non-repeatable skill and infers revises + id", () => {
|
||||
it("parses legacy revise steps then strips them from the user graph", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
@@ -795,10 +997,35 @@ modules:
|
||||
revises: "实现机制",
|
||||
});
|
||||
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
|
||||
const stripped = stripReviseSteps(flow);
|
||||
expect(stripped.steps.map((s) => s.id)).toEqual(["实现机制"]);
|
||||
const view = formatCreationFlowForUser(flow, sampleCatalog, ["实现机制"]);
|
||||
expect(view.steps[1]?.mode).toBe("revise");
|
||||
expect(view.steps[1]?.occurrence).toBeUndefined();
|
||||
expect(view.steps[1]?.runState).toBe("current");
|
||||
expect(view.steps).toHaveLength(1);
|
||||
expect(view.steps[0]?.id).toBe("实现机制");
|
||||
expect(view.steps[0]?.runState).toBe("done");
|
||||
expect(view.steps[0]?.mode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stripReviseSteps remaps dependents back to the origin step", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"steps": [
|
||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{
|
||||
"id": "美学纲领与交互范式·改",
|
||||
"name": "美学纲领与交互范式",
|
||||
"mode": "revise",
|
||||
"revises": "美学纲领与交互范式",
|
||||
"depends_on": ["美学纲领与交互范式"]
|
||||
},
|
||||
{ "id": "正文组成", "name": "实现机制", "depends_on": ["美学纲领与交互范式·改"] }
|
||||
]
|
||||
}`)!;
|
||||
const stripped = stripReviseSteps(flow);
|
||||
expect(stripped.steps.map((s) => s.id)).toEqual([
|
||||
"美学纲领与交互范式",
|
||||
"正文组成",
|
||||
]);
|
||||
expect(stripped.steps[1]?.depends_on).toEqual(["美学纲领与交互范式"]);
|
||||
});
|
||||
|
||||
it("still rejects two fresh copies of a non-repeatable skill", () => {
|
||||
@@ -849,7 +1076,7 @@ modules:
|
||||
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("nextPendingStep prefers a ready revise over a later fresh step", () => {
|
||||
it("nextPendingStep ignores orchestrated revise steps", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
@@ -863,31 +1090,26 @@ modules:
|
||||
}
|
||||
]
|
||||
}`)!;
|
||||
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.mode).toBe("revise");
|
||||
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.id).toBe(
|
||||
"美学纲领与交互范式·改",
|
||||
);
|
||||
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.mode).toBeUndefined();
|
||||
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.id).toBe("生成规则");
|
||||
expect(
|
||||
listReadySteps(flow, ["美学纲领与交互范式"]).map((s) => s.id),
|
||||
).toEqual(["生成规则"]);
|
||||
});
|
||||
|
||||
it("injects inherit draft and skips opening on a revise design-step", async () => {
|
||||
it("inherits existing artifact when reentering an accepted 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: "美学纲领与交互范式·改",
|
||||
currentStepName: "美学纲领与交互范式",
|
||||
acceptedStepNames: ["美学纲领与交互范式"],
|
||||
inheritExisting: true,
|
||||
});
|
||||
expect(binding?.opening).toBeNull();
|
||||
expect(binding?.inheritTag).toBe("设计.美学纲领与交互范式");
|
||||
@@ -898,16 +1120,153 @@ modules:
|
||||
undefined,
|
||||
{
|
||||
flowRaw,
|
||||
currentStepName: "美学纲领与交互范式·改",
|
||||
currentStepName: "美学纲领与交互范式",
|
||||
acceptedStepNames: ["美学纲领与交互范式"],
|
||||
inheritExisting: true,
|
||||
},
|
||||
);
|
||||
expect(loaded.promptBody).toContain("【回头修改】");
|
||||
expect(loaded.promptBody).not.toContain("程序开场");
|
||||
expect(loaded.worker.name).toContain("回头修改");
|
||||
expect(loaded.worker.inputTags).toContain("设计.美学纲领与交互范式");
|
||||
});
|
||||
|
||||
it("layers follow longest dependency path", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"steps": [
|
||||
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
||||
{ "name": "具体实例", "depends_on": ["生成规则"] },
|
||||
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式", "具体实例"] }
|
||||
]
|
||||
}`)!;
|
||||
const layers = computeStepLayers(flow);
|
||||
expect(layers.get("美学纲领与交互范式")).toBe(0);
|
||||
expect(layers.get("生成规则")).toBe(1);
|
||||
expect(layers.get("具体实例")).toBe(2);
|
||||
expect(layers.get("开场白与开场变量")).toBe(3);
|
||||
});
|
||||
|
||||
it("listReadySteps returns every unblocked pending step", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"steps": [
|
||||
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
||||
{ "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式"] }
|
||||
]
|
||||
}`)!;
|
||||
expect(listReadySteps(flow, []).map((s) => s.id)).toEqual([
|
||||
"美学纲领与交互范式",
|
||||
]);
|
||||
expect(
|
||||
loaded.worker.contextSegments?.some((s) => s.id === "inherit-existing"),
|
||||
listReadySteps(flow, ["美学纲领与交互范式"]).map((s) => s.id),
|
||||
).toEqual(["生成规则", "实现机制"]);
|
||||
});
|
||||
|
||||
it("spawns instance from prototype before closer and hangs instance on closer deps", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式"] }
|
||||
]
|
||||
}`)!;
|
||||
expect(
|
||||
hasSelectableCreationWork(flow, sampleCatalog, ["美学纲领与交互范式"]),
|
||||
).toBe(true);
|
||||
const spawned = spawnRepeatableCreationStep({
|
||||
flow,
|
||||
catalog: sampleCatalog,
|
||||
moduleName: "生成规则",
|
||||
acceptedStepIds: ["美学纲领与交互范式"],
|
||||
});
|
||||
expect("error" in spawned).toBe(false);
|
||||
if ("error" in spawned) return;
|
||||
expect(spawned.step.id).toBe("生成规则#1");
|
||||
expect(spawned.step.role).toBe("instance");
|
||||
expect(spawned.step.depends_on).toEqual(["美学纲领与交互范式"]);
|
||||
expect(spawned.flow.steps.map((s) => s.id)).toEqual([
|
||||
"美学纲领与交互范式",
|
||||
"生成规则",
|
||||
"生成规则#1",
|
||||
"开场白与开场变量",
|
||||
]);
|
||||
expect(
|
||||
spawned.flow.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
|
||||
).toEqual(["美学纲领与交互范式", "生成规则#1"]);
|
||||
|
||||
const second = spawnRepeatableCreationStep({
|
||||
flow: spawned.flow,
|
||||
catalog: sampleCatalog,
|
||||
moduleName: "生成规则",
|
||||
acceptedStepIds: ["美学纲领与交互范式"],
|
||||
});
|
||||
expect("error" in second).toBe(false);
|
||||
if ("error" in second) return;
|
||||
expect(second.step.id).toBe("生成规则#2");
|
||||
});
|
||||
|
||||
it("removeUnstartedInstance drops empty spawn and closer dep", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{ "id": "生成规则", "name": "生成规则", "role": "prototype", "depends_on": ["美学纲领与交互范式"] },
|
||||
{ "id": "生成规则#1", "name": "生成规则", "role": "instance", "from": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
||||
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式", "生成规则#1"] }
|
||||
]
|
||||
}`)!;
|
||||
const next = removeUnstartedInstance(flow, "生成规则#1", []);
|
||||
expect(next.steps.map((s) => s.id)).toEqual([
|
||||
"美学纲领与交互范式",
|
||||
"生成规则",
|
||||
"开场白与开场变量",
|
||||
]);
|
||||
expect(
|
||||
next.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
|
||||
).toEqual(["美学纲领与交互范式"]);
|
||||
const kept = removeUnstartedInstance(next, "美学纲领与交互范式", []);
|
||||
expect(kept.steps.map((s) => s.id)).toEqual(next.steps.map((s) => s.id));
|
||||
const accepted = removeUnstartedInstance(flow, "生成规则#1", ["生成规则#1"]);
|
||||
expect(accepted.steps.some((s) => s.id === "生成规则#1")).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks 具体实例 spawn until a 生成规则 instance is accepted", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"steps": [
|
||||
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{
|
||||
"id": "生成规则",
|
||||
"name": "生成规则",
|
||||
"role": "prototype",
|
||||
"depends_on": ["美学纲领与交互范式"]
|
||||
}
|
||||
]
|
||||
}`)!;
|
||||
const blocked = spawnRepeatableCreationStep({
|
||||
flow,
|
||||
catalog: sampleCatalog,
|
||||
moduleName: "具体实例",
|
||||
acceptedStepIds: ["美学纲领与交互范式"],
|
||||
});
|
||||
expect(blocked).toMatchObject({ error: "先完成一条生成规则" });
|
||||
const ruleSpawn = spawnInstanceFromPrototype({
|
||||
flow,
|
||||
catalog: sampleCatalog,
|
||||
prototypeId: "生成规则",
|
||||
acceptedStepIds: ["美学纲领与交互范式"],
|
||||
});
|
||||
expect("error" in ruleSpawn).toBe(false);
|
||||
if ("error" in ruleSpawn) return;
|
||||
const withRule = ruleSpawn.flow;
|
||||
const ok = spawnRepeatableCreationStep({
|
||||
flow: withRule,
|
||||
catalog: sampleCatalog,
|
||||
moduleName: "具体实例",
|
||||
acceptedStepIds: ["美学纲领与交互范式", "生成规则#1"],
|
||||
});
|
||||
expect("error" in ok).toBe(false);
|
||||
if ("error" in ok) return;
|
||||
expect(ok.step.depends_on).toEqual(["生成规则#1"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,12 +8,21 @@ import {
|
||||
import { parseWorkerSetYaml } from "../src/skills/worker-set-parse.js";
|
||||
|
||||
describe("resolveAcceptanceModeForWorker", () => {
|
||||
it("always confirms design-flow", () => {
|
||||
it("auto-continues design-flow without a confirm step", () => {
|
||||
const mode = resolveAcceptanceModeForWorker({
|
||||
session: createSession(),
|
||||
blackboard: new Blackboard(),
|
||||
workerId: "design-flow",
|
||||
});
|
||||
expect(mode).toBe("no_confirmation");
|
||||
});
|
||||
|
||||
it("still confirms design-step", () => {
|
||||
const mode = resolveAcceptanceModeForWorker({
|
||||
session: createSession(),
|
||||
blackboard: new Blackboard(),
|
||||
workerId: "design-step",
|
||||
});
|
||||
expect(mode).toBe("user_confirmed");
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
displayStageLabel,
|
||||
displayWorkerLabel,
|
||||
formatWorkerDisplayTitle,
|
||||
isFlowPlanReview,
|
||||
reviewComposerCopy,
|
||||
} from "../src/server/display-labels.js";
|
||||
|
||||
@@ -52,4 +53,25 @@ describe("display-labels", () => {
|
||||
expect(setup.acceptLabel).toBe("选定此开场");
|
||||
expect(setup.kind).toBe("opening");
|
||||
});
|
||||
|
||||
it("treats design-flow review as the pick-node surface", () => {
|
||||
expect(
|
||||
isFlowPlanReview({
|
||||
waitingReason: { kind: "review_artifact" },
|
||||
reviewArtifact: { workerId: "design-flow" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isFlowPlanReview({
|
||||
waitingReason: { kind: "review_artifact" },
|
||||
reviewArtifact: { workerId: "design-step" },
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isFlowPlanReview({
|
||||
waitingReason: { kind: "pick_creation_step" },
|
||||
reviewArtifact: { workerId: "design-flow" },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyOpenAiGeneration,
|
||||
applyRejectedGenerationField,
|
||||
classifyChatModel,
|
||||
fetchWithGenerationCompat,
|
||||
parseRejectedGenerationField,
|
||||
sanitizeReasoningEffort,
|
||||
sanitizeVerbosity,
|
||||
} from "../src/llm/generation-compat.js";
|
||||
import { buildRequestBody } from "../src/llm/client.js";
|
||||
import type { LlmConfig } from "../src/config/env.js";
|
||||
|
||||
const stGeneration = {
|
||||
temperature: 1,
|
||||
topP: 0.95,
|
||||
topK: 40,
|
||||
minP: 0.05,
|
||||
frequencyPenalty: 0,
|
||||
presencePenalty: 0,
|
||||
repetitionPenalty: 1.05,
|
||||
maxOutputTokens: 60000,
|
||||
seed: -1,
|
||||
reasoningEffort: "auto",
|
||||
verbosity: "auto",
|
||||
};
|
||||
|
||||
describe("sanitizeReasoningEffort", () => {
|
||||
it("omits SillyTavern auto and empty values", () => {
|
||||
expect(sanitizeReasoningEffort("auto")).toBeUndefined();
|
||||
expect(sanitizeReasoningEffort("AUTO")).toBeUndefined();
|
||||
expect(sanitizeReasoningEffort("")).toBeUndefined();
|
||||
expect(sanitizeReasoningEffort(" ")).toBeUndefined();
|
||||
expect(sanitizeReasoningEffort(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps values the provider enum accepts", () => {
|
||||
expect(sanitizeReasoningEffort("none")).toBe("none");
|
||||
expect(sanitizeReasoningEffort("minimal")).toBe("minimal");
|
||||
expect(sanitizeReasoningEffort("low")).toBe("low");
|
||||
expect(sanitizeReasoningEffort("medium")).toBe("medium");
|
||||
expect(sanitizeReasoningEffort("high")).toBe("high");
|
||||
expect(sanitizeReasoningEffort("xhigh")).toBe("xhigh");
|
||||
expect(sanitizeReasoningEffort("max")).toBe("max");
|
||||
});
|
||||
|
||||
it("maps SillyTavern min to minimal and ignores unknown values", () => {
|
||||
expect(sanitizeReasoningEffort("min")).toBe("minimal");
|
||||
expect(sanitizeReasoningEffort("MIN")).toBe("minimal");
|
||||
expect(sanitizeReasoningEffort("unknown")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeVerbosity", () => {
|
||||
it("omits auto and unknown values", () => {
|
||||
expect(sanitizeVerbosity("auto")).toBeUndefined();
|
||||
expect(sanitizeVerbosity("loud")).toBeUndefined();
|
||||
expect(sanitizeVerbosity("medium")).toBe("medium");
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyChatModel", () => {
|
||||
it("treats o-series, gpt-5, kimi-k2.5 and deepseek-reasoner as no-sampling", () => {
|
||||
expect(classifyChatModel("o3-mini").omitSampling).toBe(true);
|
||||
expect(classifyChatModel("openai/o4-mini").omitSampling).toBe(true);
|
||||
expect(classifyChatModel("gpt-5").omitSampling).toBe(true);
|
||||
expect(classifyChatModel("deepseek-reasoner").omitSampling).toBe(true);
|
||||
expect(classifyChatModel("kimi-k2.5").omitSampling).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps sampling for ordinary chat models", () => {
|
||||
expect(classifyChatModel("gpt-4o").omitSampling).toBe(false);
|
||||
expect(classifyChatModel("deepseek-v4-pro").omitSampling).toBe(false);
|
||||
expect(classifyChatModel("gpt-5-chat-latest").omitSampling).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyOpenAiGeneration", () => {
|
||||
it("does not send ST extras or auto sentinels on a chat model", () => {
|
||||
const body: Record<string, unknown> = { model: "deepseek-v4-pro" };
|
||||
applyOpenAiGeneration(body, stGeneration, "deepseek-v4-pro");
|
||||
expect(body.temperature).toBe(1);
|
||||
expect(body.top_p).toBe(0.95);
|
||||
expect(body.max_tokens).toBe(60000);
|
||||
expect(body.top_k).toBeUndefined();
|
||||
expect(body.min_p).toBeUndefined();
|
||||
expect(body.repetition_penalty).toBeUndefined();
|
||||
expect(body.reasoning_effort).toBeUndefined();
|
||||
expect(body.verbosity).toBeUndefined();
|
||||
expect(body.seed).toBeUndefined();
|
||||
});
|
||||
|
||||
it("drops sampling params for reasoning models and uses max_completion_tokens", () => {
|
||||
const body: Record<string, unknown> = { model: "o3-mini" };
|
||||
applyOpenAiGeneration(body, stGeneration, "o3-mini");
|
||||
expect(body.temperature).toBeUndefined();
|
||||
expect(body.top_p).toBeUndefined();
|
||||
expect(body.frequency_penalty).toBeUndefined();
|
||||
expect(body.presence_penalty).toBeUndefined();
|
||||
expect(body.max_tokens).toBeUndefined();
|
||||
expect(body.max_completion_tokens).toBe(60000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseRejectedGenerationField", () => {
|
||||
it("reads OpenAI unsupported parameter errors", () => {
|
||||
expect(
|
||||
parseRejectedGenerationField(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Unsupported parameter: 'temperature' is not supported with this model.",
|
||||
param: "temperature",
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toBe("temperature");
|
||||
});
|
||||
|
||||
it("reads strict JSON deserializer unknown variant errors", () => {
|
||||
expect(
|
||||
parseRejectedGenerationField(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message:
|
||||
"Failed to deserialize the JSON body into the target type: reasoning_effort: unknown variant `auto`, expected one of `none`, `minimal`",
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toBe("reasoning_effort");
|
||||
});
|
||||
|
||||
it("drops top_p when temperature and top_p are mutually exclusive", () => {
|
||||
expect(
|
||||
parseRejectedGenerationField(
|
||||
"temperature and top_p cannot both be specified for this model",
|
||||
),
|
||||
).toBe("top_p");
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyRejectedGenerationField", () => {
|
||||
it("renames max_completion_tokens to max_tokens once", () => {
|
||||
const body: Record<string, unknown> = { max_completion_tokens: 100 };
|
||||
const tried = new Set<string>();
|
||||
expect(applyRejectedGenerationField(body, "max_completion_tokens", tried)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(body.max_tokens).toBe(100);
|
||||
expect(body.max_completion_tokens).toBeUndefined();
|
||||
expect(applyRejectedGenerationField(body, "max_tokens", tried)).toBe(true);
|
||||
expect(body.max_completion_tokens).toBeUndefined();
|
||||
expect(body.max_tokens).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchWithGenerationCompat", () => {
|
||||
it("retries a 400 after stripping the rejected field", async () => {
|
||||
const sent: Array<Record<string, unknown>> = [];
|
||||
const response = await fetchWithGenerationCompat(async (body) => {
|
||||
sent.push({ ...body });
|
||||
if ("temperature" in body) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Unsupported parameter: 'temperature'",
|
||||
param: "temperature",
|
||||
},
|
||||
}),
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
||||
}, { model: "o3", temperature: 1, messages: [] });
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(sent).toHaveLength(2);
|
||||
expect(sent[0].temperature).toBe(1);
|
||||
expect(sent[1].temperature).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildRequestBody", () => {
|
||||
const messages = [{ role: "user" as const, content: "hi" }];
|
||||
|
||||
it("does not send reasoning_effort when the preset says auto", () => {
|
||||
const config: LlmConfig = {
|
||||
baseUrl: "https://example.test/v1",
|
||||
apiKey: "k",
|
||||
model: "deepseek-v4-pro",
|
||||
};
|
||||
const body = buildRequestBody(config, messages, {
|
||||
generation: { reasoningEffort: "auto", temperature: 0.5 },
|
||||
});
|
||||
expect(body.reasoning_effort).toBeUndefined();
|
||||
expect(body.temperature).toBe(0.5);
|
||||
});
|
||||
|
||||
it("sends a supported effort value", () => {
|
||||
const config: LlmConfig = {
|
||||
baseUrl: "https://example.test/v1",
|
||||
apiKey: "k",
|
||||
model: "gpt-5",
|
||||
};
|
||||
const body = buildRequestBody(config, messages, {
|
||||
generation: { reasoningEffort: "High" },
|
||||
});
|
||||
expect(body.reasoning_effort).toBe("high");
|
||||
expect(body.temperature).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses profile default reasoningEffort when generation omits it", () => {
|
||||
const config: LlmConfig = {
|
||||
baseUrl: "https://example.test/v1",
|
||||
apiKey: "k",
|
||||
model: "glm-5.3",
|
||||
reasoningEffort: "high",
|
||||
};
|
||||
const body = buildRequestBody(config, messages, {
|
||||
generation: { temperature: 1 },
|
||||
});
|
||||
expect(body.reasoning_effort).toBe("high");
|
||||
});
|
||||
|
||||
it("lets generation reasoningEffort override the profile default", () => {
|
||||
const config: LlmConfig = {
|
||||
baseUrl: "https://example.test/v1",
|
||||
apiKey: "k",
|
||||
model: "glm-5.3",
|
||||
reasoningEffort: "max",
|
||||
};
|
||||
const body = buildRequestBody(config, messages, {
|
||||
generation: { reasoningEffort: "low" },
|
||||
});
|
||||
expect(body.reasoning_effort).toBe("low");
|
||||
});
|
||||
|
||||
it("does not forward top_k from an imported ST preset", () => {
|
||||
const config: LlmConfig = {
|
||||
baseUrl: "https://example.test/v1",
|
||||
apiKey: "k",
|
||||
model: "deepseek-v4-pro",
|
||||
};
|
||||
const body = buildRequestBody(config, messages, {
|
||||
generation: stGeneration,
|
||||
});
|
||||
expect(body.top_k).toBeUndefined();
|
||||
expect(body.min_p).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
convertStructuralSmartQuotes,
|
||||
tryParseJsonDoc,
|
||||
} from "../web/json-doc.js";
|
||||
|
||||
describe("tryParseJsonDoc", () => {
|
||||
it("keeps interior Chinese quotes in an otherwise valid fragment", () => {
|
||||
const raw = JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
正文: {
|
||||
设定逻辑: {
|
||||
参与方式: {
|
||||
用户与user关系: {
|
||||
依据: "用户明确“只有我不会被感染”,且为爽文主角",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const parsed = tryParseJsonDoc(`## 设计.美学纲领与交互范式\n\n${raw}`);
|
||||
expect(parsed?.技能).toBe("美学纲领与交互范式");
|
||||
expect(parsed?.正文?.设定逻辑?.参与方式?.用户与user关系?.依据).toContain(
|
||||
"只有我不会被感染",
|
||||
);
|
||||
});
|
||||
|
||||
it("parses JSON that uses smart quotes as delimiters", () => {
|
||||
const raw = "{“schema”:“context-fragment.v1”,“brief”:“点题”}";
|
||||
expect(convertStructuralSmartQuotes(raw)).toBe(
|
||||
'{"schema":"context-fragment.v1","brief":"点题"}',
|
||||
);
|
||||
expect(tryParseJsonDoc(raw)).toEqual({
|
||||
schema: "context-fragment.v1",
|
||||
brief: "点题",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isOpeningSealArtifact,
|
||||
parseOpeningSealPayload,
|
||||
openingVariablesToTableDoc,
|
||||
reopenCreationFlowRaw,
|
||||
} from "../src/skills/opening-seal.js";
|
||||
|
||||
const fragment = JSON.stringify({
|
||||
@@ -71,4 +72,14 @@ describe("opening-seal", () => {
|
||||
const closed = JSON.parse(closeCreationFlowRaw(raw)!);
|
||||
expect(closed.status).toBe("closed");
|
||||
});
|
||||
|
||||
it("reopens a closed creation flow", () => {
|
||||
const raw = JSON.stringify({
|
||||
version: 1,
|
||||
status: "closed",
|
||||
steps: [{ id: "开场白与开场变量", name: "开场白与开场变量", depends_on: [] }],
|
||||
});
|
||||
const opened = JSON.parse(reopenCreationFlowRaw(raw)!);
|
||||
expect(opened.status).toBe("open");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
extractLastProductJson,
|
||||
selectJsonPayload,
|
||||
tryParseJsonDoc,
|
||||
} from "../src/parse/json-doc.js";
|
||||
|
||||
describe("selectJsonPayload", () => {
|
||||
it("keeps content when it is already the product", () => {
|
||||
const json = '{"schema":"context-fragment.v1","brief":"a","正文":{}}';
|
||||
expect(selectJsonPayload(json, "thinking...")).toBe(json);
|
||||
});
|
||||
|
||||
it("falls back to reasoning when content is not JSON", () => {
|
||||
const json = '{"schema":"context-fragment.v1","brief":"a","正文":{}}';
|
||||
expect(selectJsonPayload("Done.", json)).toBe(json);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractLastProductJson", () => {
|
||||
it("skips an earlier example object and takes the fragment", () => {
|
||||
const raw = `example { "foo": 1 } then ${JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
brief: "点题",
|
||||
正文: { x: 1 },
|
||||
})}`;
|
||||
const parsed = extractLastProductJson(raw) as { brief?: string };
|
||||
expect(parsed?.brief).toBe("点题");
|
||||
expect(tryParseJsonDoc(raw)).toMatchObject({ brief: "点题" });
|
||||
});
|
||||
});
|
||||
@@ -541,4 +541,160 @@ describe("phase machine", () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejecting the next step goes back to design-flow instead of the main agent", () => {
|
||||
let session = createSession("default");
|
||||
const decision = {
|
||||
id: "d1",
|
||||
action: "run_worker" as const,
|
||||
reason: "下一步开场白",
|
||||
workerId: "design-step",
|
||||
requiresApproval: true,
|
||||
statePatchAllowed: false as const,
|
||||
};
|
||||
session = {
|
||||
...session,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "approve_step", decisionId: decision.id },
|
||||
pendingDecision: decision,
|
||||
};
|
||||
|
||||
const rejected = applyEvent(session, {
|
||||
type: "user_rejected_next_step",
|
||||
payload: { decisionId: decision.id, reason: "还没做 NPC" },
|
||||
});
|
||||
expect(rejected.session.phase).toBe("running");
|
||||
expect(rejected.session.slots["用户.下一步意向"]).toBe("还没做 NPC");
|
||||
expect(rejected.session.pendingDecision).toBeUndefined();
|
||||
expect(rejected.effects).toEqual([
|
||||
{ type: "unseal_creation_opening" },
|
||||
{ type: "run_worker", workerId: "design-flow" },
|
||||
]);
|
||||
expect(rejected.effects.some((e) => e.type === "invoke_main_agent")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("replan from opening review runs design-flow without revising the opening", () => {
|
||||
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 },
|
||||
slots: { creationSealedByOpening: true },
|
||||
};
|
||||
|
||||
const replanned = applyEvent(session, {
|
||||
type: "user_requested_flow_replan",
|
||||
payload: { reason: "漏了开局 NPC" },
|
||||
});
|
||||
expect(replanned.session.phase).toBe("running");
|
||||
expect(replanned.session.pendingArtifactId).toBeUndefined();
|
||||
expect(replanned.session.slots.creationSealedByOpening).toBeUndefined();
|
||||
expect(replanned.session.artifacts[0]?.status).toBe("rejected");
|
||||
expect(replanned.session.slots["用户.修订说明"]).toBe("漏了开局 NPC");
|
||||
expect(replanned.effects).toEqual([
|
||||
{ type: "unseal_creation_opening" },
|
||||
{ type: "run_worker", workerId: "design-flow" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("awaits pick on the layered graph and starts the clicked step", () => {
|
||||
let session = createSession("default");
|
||||
session = {
|
||||
...session,
|
||||
phase: "running",
|
||||
};
|
||||
const awaited = applyEvent(session, {
|
||||
type: "creation_step_pick_awaited",
|
||||
payload: {},
|
||||
});
|
||||
expect(awaited.session.phase).toBe("waiting_user");
|
||||
expect(awaited.session.waitingReason?.kind).toBe("pick_creation_step");
|
||||
expect(awaited.session.currentWorkerId).toBeUndefined();
|
||||
|
||||
const picked = applyEvent(awaited.session, {
|
||||
type: "user_picked_creation_step",
|
||||
payload: { stepId: "生成规则" },
|
||||
});
|
||||
expect(picked.session.phase).toBe("running");
|
||||
expect(picked.session.currentWorkerId).toBe("design-step");
|
||||
expect(picked.effects).toEqual([
|
||||
{ type: "run_worker", workerId: "design-step" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaving a creation step returns to the pick graph", () => {
|
||||
let session = createSession("default");
|
||||
session = {
|
||||
...session,
|
||||
phase: "waiting_user",
|
||||
waitingReason: {
|
||||
kind: "worker_questions",
|
||||
workerId: "design-step",
|
||||
questions: [{ id: "module-opening", prompt: "引导" }],
|
||||
},
|
||||
currentWorkerId: "design-step",
|
||||
currentStepId: "生成规则#1",
|
||||
};
|
||||
const left = applyEvent(session, {
|
||||
type: "user_left_creation_step",
|
||||
payload: {},
|
||||
});
|
||||
expect(left.session.phase).toBe("waiting_user");
|
||||
expect(left.session.waitingReason?.kind).toBe("pick_creation_step");
|
||||
expect(left.session.currentWorkerId).toBeUndefined();
|
||||
expect(left.session.currentStepId).toBeUndefined();
|
||||
expect(left.session.resumeContext).toBeUndefined();
|
||||
expect(left.effects).toEqual([]);
|
||||
});
|
||||
|
||||
it("replan from the pick graph runs design-flow", () => {
|
||||
let session = createSession("default");
|
||||
session = {
|
||||
...session,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "pick_creation_step" },
|
||||
};
|
||||
const replanned = applyEvent(session, {
|
||||
type: "user_requested_flow_replan",
|
||||
payload: { reason: "补舞台骨架" },
|
||||
});
|
||||
expect(replanned.session.phase).toBe("running");
|
||||
expect(replanned.session.slots["用户.下一步意向"]).toBe("补舞台骨架");
|
||||
expect(replanned.effects).toEqual([
|
||||
{ type: "unseal_creation_opening" },
|
||||
{ type: "run_worker", workerId: "design-flow" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("design-flow without confirmation proposes the pick graph", () => {
|
||||
let session = createSession("default");
|
||||
session = {
|
||||
...session,
|
||||
phase: "running",
|
||||
acceptanceMode: "no_confirmation",
|
||||
};
|
||||
const artifact = createArtifact({
|
||||
workerId: "design-flow",
|
||||
outputTags: ["设计.创作流程"],
|
||||
});
|
||||
session = { ...session, artifacts: [artifact] };
|
||||
const completed = applyEvent(session, {
|
||||
type: "worker_completed",
|
||||
payload: { artifactId: artifact.id },
|
||||
});
|
||||
expect(completed.session.artifacts[0]?.status).toBe("accepted");
|
||||
expect(completed.session.pendingArtifactId).toBeUndefined();
|
||||
expect(completed.effects).toEqual([
|
||||
{ type: "propose_next_creation_step" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { PhaseRuntime, createDecision } from "../src/runtime/phase-runtime.js";
|
||||
import { createSession } from "../src/runtime/phase-machine.js";
|
||||
import { Blackboard } from "../src/blackboard/blackboard.js";
|
||||
import { parseCreationFlow } from "../src/skills/creation-flow.js";
|
||||
|
||||
describe("phase runtime", () => {
|
||||
it("auto-loads default orchestrator and awaits first user input", async () => {
|
||||
@@ -129,4 +130,167 @@ describe("phase runtime", () => {
|
||||
await runtime.start();
|
||||
await expect(runtime.retryStuckRun()).rejects.toThrow("当前没有可重试的执行");
|
||||
});
|
||||
|
||||
it("first input with seeded DAG waits for node pick instead of calling main agent", async () => {
|
||||
const llm = new MockLlmProvider(["Let me analyze the DAG in English prose"]);
|
||||
const runtime = new PhaseRuntime({ llm });
|
||||
await runtime.start();
|
||||
runtime.getBlackboard().write({
|
||||
tag: "设计.创作流程",
|
||||
content: JSON.stringify({
|
||||
version: 1,
|
||||
status: "open",
|
||||
steps: [
|
||||
{
|
||||
id: "美学纲领与交互范式",
|
||||
name: "美学纲领与交互范式",
|
||||
depends_on: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
source: "runtime",
|
||||
});
|
||||
await runtime.submitInput("丧尸世界,但只有我不会被感染");
|
||||
expect(runtime.getSession().phase).toBe("waiting_user");
|
||||
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
||||
});
|
||||
|
||||
it("enterSeededCreationPick skips first demand and waits on the seeded graph", async () => {
|
||||
const runtime = new PhaseRuntime();
|
||||
await runtime.start();
|
||||
runtime.getBlackboard().write({
|
||||
tag: "设计.创作流程",
|
||||
content: JSON.stringify({
|
||||
version: 1,
|
||||
status: "open",
|
||||
steps: [
|
||||
{
|
||||
id: "舞台骨架",
|
||||
name: "舞台骨架",
|
||||
depends_on: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
source: "runtime",
|
||||
});
|
||||
await runtime.enterSeededCreationPick();
|
||||
expect(runtime.getSession().slots.startupCompleted).toBe(true);
|
||||
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
||||
});
|
||||
|
||||
it("unusable main agent output without DAG starts design-flow", async () => {
|
||||
const llm = new MockLlmProvider(["Let me think about routing"]);
|
||||
const runtime = new PhaseRuntime({ llm, autoStubWorker: true });
|
||||
await runtime.start();
|
||||
await runtime.submitInput("网恋对象对话");
|
||||
expect(runtime.getSession().phase).not.toBe("running");
|
||||
expect(
|
||||
runtime.getSession().artifacts.some((a) => a.workerId === "design-flow"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("failRun leaves error phase instead of running", async () => {
|
||||
const runtime = new PhaseRuntime();
|
||||
await runtime.start();
|
||||
await runtime.submitInput("随便写点");
|
||||
expect(runtime.getSession().phase).toBe("running");
|
||||
await runtime.failRun("fetch failed");
|
||||
expect(runtime.getSession().phase).toBe("error");
|
||||
expect(runtime.getSession().waitingReason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("recoverOrphanedRun turns stuck running into node pick when DAG exists", async () => {
|
||||
const runtime = new PhaseRuntime();
|
||||
await runtime.start();
|
||||
runtime.getBlackboard().write({
|
||||
tag: "设计.创作流程",
|
||||
content: JSON.stringify({
|
||||
version: 1,
|
||||
status: "open",
|
||||
steps: [
|
||||
{
|
||||
id: "美学纲领与交互范式",
|
||||
name: "美学纲领与交互范式",
|
||||
depends_on: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
source: "runtime",
|
||||
});
|
||||
const session = runtime.getSession();
|
||||
session.phase = "running";
|
||||
session.waitingReason = undefined;
|
||||
expect(runtime.recoverOrphanedRun()).toBe(true);
|
||||
expect(runtime.getSession().phase).toBe("waiting_user");
|
||||
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
||||
});
|
||||
|
||||
it("leaveCreationStep returns to pick and unsawns an empty instance", async () => {
|
||||
const session = createSession("default");
|
||||
session.phase = "waiting_user";
|
||||
session.waitingReason = {
|
||||
kind: "worker_questions",
|
||||
workerId: "design-step",
|
||||
questions: [{ id: "module-opening", prompt: "生成规则引导" }],
|
||||
};
|
||||
session.currentWorkerId = "design-step";
|
||||
session.slots = { startupCompleted: true };
|
||||
const blackboard = new Blackboard();
|
||||
blackboard.write({
|
||||
tag: "设计.创作流程",
|
||||
content: JSON.stringify({
|
||||
version: 1,
|
||||
status: "open",
|
||||
steps: [
|
||||
{
|
||||
id: "美学纲领与交互范式",
|
||||
name: "美学纲领与交互范式",
|
||||
depends_on: [],
|
||||
},
|
||||
{
|
||||
id: "生成规则",
|
||||
name: "生成规则",
|
||||
role: "prototype",
|
||||
depends_on: ["美学纲领与交互范式"],
|
||||
},
|
||||
{
|
||||
id: "生成规则#1",
|
||||
name: "生成规则",
|
||||
role: "instance",
|
||||
from: "生成规则",
|
||||
depends_on: ["美学纲领与交互范式"],
|
||||
},
|
||||
{
|
||||
id: "开场白与开场变量",
|
||||
name: "开场白与开场变量",
|
||||
depends_on: ["美学纲领与交互范式", "生成规则#1"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
source: "runtime",
|
||||
});
|
||||
blackboard.write({
|
||||
tag: "创作.当前步骤",
|
||||
content: "生成规则#1",
|
||||
source: "runtime",
|
||||
});
|
||||
const runtime = new PhaseRuntime({
|
||||
initialSession: session,
|
||||
initialBlackboardItems: blackboard.exportItems(),
|
||||
});
|
||||
await runtime.leaveCreationStep();
|
||||
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
||||
expect(runtime.getSession().currentWorkerId).toBeUndefined();
|
||||
const flow = parseCreationFlow(
|
||||
runtime.getBlackboard().getContentByTag("设计.创作流程"),
|
||||
);
|
||||
expect(flow?.steps.map((s) => s.id)).toEqual([
|
||||
"美学纲领与交互范式",
|
||||
"生成规则",
|
||||
"开场白与开场变量",
|
||||
]);
|
||||
expect(
|
||||
flow?.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
|
||||
).toEqual(["美学纲领与交互范式"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,4 +127,32 @@ describe("session persistence", () => {
|
||||
|
||||
deleteBook(book.id);
|
||||
});
|
||||
|
||||
it("createForBook with recipe lands on that recipe's seeded pick graph", async () => {
|
||||
const { createBook, deleteBook } = await import("../src/book/store.js");
|
||||
const { SessionManager } = await import("../src/server/session-manager.js");
|
||||
|
||||
const book = createBook({ title: "配方开局" });
|
||||
const mgr = new SessionManager();
|
||||
const view = await mgr.createForBook(
|
||||
book.id,
|
||||
"world-simulator",
|
||||
"world-simulator",
|
||||
);
|
||||
expect(view.waitingReason?.kind).toBe("pick_creation_step");
|
||||
expect(view.startupCompleted).toBe(true);
|
||||
expect(view.uiPrompt).toBeUndefined();
|
||||
expect(view.openingGuide).toBeNull();
|
||||
const { loadAllRecipeDetails } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const details = await loadAllRecipeDetails("dialogue/world-simulator");
|
||||
const seedNames = details
|
||||
.find((d) => d.id === "world-simulator")
|
||||
?.seed?.steps.map((s) => s.name);
|
||||
expect(seedNames?.length).toBeGreaterThan(0);
|
||||
expect(view.creationFlowView?.steps.map((s) => s.name)).toEqual(seedNames);
|
||||
|
||||
deleteBook(book.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createMockToolCall,
|
||||
MockLlmProvider,
|
||||
} from "../src/llm/client.js";
|
||||
import { isUnusableMainAgentOutput } from "../src/main-agent/main-agent.js";
|
||||
import { runMainAgentToolLoop } from "../src/main-agent/tool-loop.js";
|
||||
import { MAIN_AGENT_TOOL_DEFINITIONS } from "../src/main-agent/tools.js";
|
||||
import { toolCallToDecision } from "../src/runtime/tool-registry.js";
|
||||
@@ -29,6 +30,15 @@ describe("main agent tool loop", () => {
|
||||
).toEqual(["read_blackboard", "run_worker"]);
|
||||
});
|
||||
|
||||
it("detects unusable main agent output", () => {
|
||||
expect(
|
||||
isUnusableMainAgentOutput(
|
||||
new Error("Main Agent returned invalid JSON: Let me analyze"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isUnusableMainAgentOutput(new Error("fetch failed"))).toBe(false);
|
||||
});
|
||||
|
||||
it("runs loop tools then returns terminal decision", async () => {
|
||||
const llm = new MockLlmProvider([
|
||||
createMockToolCall("read_blackboard", { tags: ["book.brief"] }),
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
parseWorkerResponseForTest,
|
||||
sanitizeWorkerSetOutputs,
|
||||
} from "../src/worker/executor.js";
|
||||
import { selectJsonPayload } from "../src/parse/json-doc.js";
|
||||
import { parseCreationFlow } from "../src/skills/creation-flow.js";
|
||||
|
||||
describe("worker-set prose rejection", () => {
|
||||
it("does not YAML-crash on Chinese prose; returns clear parseError", () => {
|
||||
@@ -155,6 +157,39 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
expect(result.askUser?.[0]?.prompt).not.toContain("为贴近你要的质感");
|
||||
});
|
||||
|
||||
it("keeps nested aesthetics 正文 objects for specialty cards", () => {
|
||||
const frag = {
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "孤立免疫",
|
||||
正文: {
|
||||
设定逻辑: {
|
||||
参与方式: {
|
||||
用户与user关系: {
|
||||
结论: "完全代入",
|
||||
完备度: "80%",
|
||||
依据: "用户明确只有我不会被感染",
|
||||
},
|
||||
},
|
||||
},
|
||||
交互范式: { 前置配置: { 人称: "你" } },
|
||||
美学纲领: { 体验内核: "特权与惊惶" },
|
||||
},
|
||||
自评: {
|
||||
维度: [{ 名: "美学纲领", 分数: 8, 说明: "点题" }],
|
||||
},
|
||||
};
|
||||
const result = parseWorkerResponseForTest(JSON.stringify(frag), [
|
||||
"设计.美学纲领与交互范式",
|
||||
]);
|
||||
const parsed = JSON.parse(result.outputs["设计.美学纲领与交互范式"]!);
|
||||
const body = parsed.正文;
|
||||
expect(typeof body.设定逻辑).toBe("object");
|
||||
expect(body.设定逻辑.参与方式.用户与user关系.结论).toBe("完全代入");
|
||||
expect(body.美学纲领.体验内核).toBe("特权与惊惶");
|
||||
expect(parsed.自评.维度[0].分数).toBe(8);
|
||||
});
|
||||
|
||||
it("recovers a root-level context-fragment into the artifact tag", () => {
|
||||
const frag = {
|
||||
schema: "context-fragment.v1",
|
||||
@@ -173,6 +208,46 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
expect(result.askUser).toBeUndefined();
|
||||
});
|
||||
|
||||
it("accepts unprefixed outputs keys and JSON sitting after thinking prose", () => {
|
||||
const frag = {
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "孤立免疫",
|
||||
正文: { 美学纲领: { 体验内核: "人人录我" } },
|
||||
};
|
||||
const prefixed = parseWorkerResponseForTest(
|
||||
JSON.stringify({
|
||||
outputs: { "美学纲领与交互范式": frag },
|
||||
summary: "ok",
|
||||
}),
|
||||
["设计.美学纲领与交互范式"],
|
||||
);
|
||||
expect(prefixed.outputs["设计.美学纲领与交互范式"]).toContain("人人录我");
|
||||
|
||||
const mixed = parseWorkerResponseForTest(
|
||||
`We must output JSON. Example { "foo": 1 }.\n${JSON.stringify(frag)}`,
|
||||
["设计.美学纲领与交互范式"],
|
||||
);
|
||||
expect(mixed.outputs["设计.美学纲领与交互范式"]).toContain("人人录我");
|
||||
});
|
||||
|
||||
it("uses product JSON in reasoning when content is a closing sentence", () => {
|
||||
const frag = {
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "点题",
|
||||
正文: { 美学纲领: { 体验内核: "内核" } },
|
||||
};
|
||||
const payload = selectJsonPayload(
|
||||
"The artifact is ready.",
|
||||
JSON.stringify(frag),
|
||||
);
|
||||
const result = parseWorkerResponseForTest(payload, [
|
||||
"设计.美学纲领与交互范式",
|
||||
]);
|
||||
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("内核");
|
||||
});
|
||||
|
||||
it("moves a fragment written to 创作.当前步骤 onto the artifact tag", () => {
|
||||
const frag = {
|
||||
schema: "context-fragment.v1",
|
||||
@@ -257,6 +332,51 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
expect(doc.play_slots.gm).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps a root-level creation flow DAG on 设计.创作流程", () => {
|
||||
const dag = {
|
||||
brief: "单角代入",
|
||||
status: "open",
|
||||
steps: [
|
||||
{ id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [] },
|
||||
{
|
||||
id: "生成规则",
|
||||
name: "生成规则",
|
||||
role: "prototype",
|
||||
depends_on: ["美学纲领与交互范式"],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = parseWorkerResponseForTest(JSON.stringify(dag), [
|
||||
"设计.创作流程",
|
||||
]);
|
||||
const parsed = parseCreationFlow(result.outputs["设计.创作流程"]);
|
||||
expect(parsed?.brief).toBe("单角代入");
|
||||
expect(parsed?.steps.map((s) => s.name)).toEqual([
|
||||
"美学纲领与交互范式",
|
||||
"生成规则",
|
||||
]);
|
||||
expect(result.outputs["设计.创作流程"]).not.toContain("context-fragment");
|
||||
expect(result.askUser).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps creation flow written under outputs bag", () => {
|
||||
const result = parseWorkerResponseForTest(
|
||||
JSON.stringify({
|
||||
outputs: {
|
||||
"设计.创作流程": {
|
||||
status: "open",
|
||||
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
|
||||
},
|
||||
},
|
||||
summary: "流程 · 1 步 · open",
|
||||
}),
|
||||
["设计.创作流程"],
|
||||
);
|
||||
expect(parseCreationFlow(result.outputs["设计.创作流程"])?.steps[0]?.name).toBe(
|
||||
"美学纲领与交互范式",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a whole-response truncated JSON as the artifact draft", () => {
|
||||
const raw =
|
||||
'{"outputs":{"设计.上下文投影排序":{"schema":"context-order.v1","brief":"旁观+转述","agents":[';
|
||||
|
||||
Reference in New Issue
Block a user