完善创作节点循环与等待态交互,并大幅打磨 Web 壳与会话运行时。
- 节点循环:配方开局直坐 DAG 第一步;验收后先问下一步意向,再确认开干并可钉编排参数;「按意见修改」只重跑当前节点。 - 询问分流:能力 opening 走说话面引导,可跳过追问按题干去重;追问与产物同轮挂载,避免拆成两段历史。 - 运行时:补强 revision 重跑、创作流程合并/进度指针、工具循环与 worker 执行;新增运行日志与 web:watch。 - 前端:统一等待态文案与底栏主按钮,完善询问卡/产物验收/呈现壳样式与交互。 - 同步世界模拟器模块提示、编排文档与相关测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -79,6 +79,38 @@ describe("contextSegments assembly", () => {
|
||||
expect(text).not.toMatch(/^\s*\{/);
|
||||
});
|
||||
|
||||
it("revision mode asks the worker to apply sidecar answers and keep the draft", () => {
|
||||
const bb = new Blackboard();
|
||||
bb.write({
|
||||
tag: "设计.美学纲领与交互范式",
|
||||
content: '{"tone":"warm"}',
|
||||
source: "worker",
|
||||
});
|
||||
const text = assembleWorkerContext({
|
||||
inputs: {
|
||||
"用户.修订说明": "再冷一点",
|
||||
"用户.worker答复": "【追问作答】\n问:偏好?\n答:偏冷",
|
||||
"设计.美学纲领与交互范式": '{"tone":"warm"}',
|
||||
},
|
||||
segments: [
|
||||
{
|
||||
id: "user",
|
||||
tier: "dynamic",
|
||||
tags: ["用户.worker答复", "用户.修订说明"],
|
||||
label: "## 用户表述",
|
||||
},
|
||||
],
|
||||
blackboard: bb,
|
||||
workerId: "design-step",
|
||||
workerName: "创作 · 美学纲领与交互范式",
|
||||
outputTags: ["设计.美学纲领与交互范式"],
|
||||
});
|
||||
expect(text).toContain("待改底稿");
|
||||
expect(text).toContain("用户.worker答复");
|
||||
expect(text).toContain("偏冷");
|
||||
expect(text).toContain("再冷一点");
|
||||
});
|
||||
|
||||
it("falls back to JSON inputs without segments", () => {
|
||||
const bb = new Blackboard();
|
||||
const text = assembleWorkerContext({
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
projectFragmentContent,
|
||||
contextFragmentToView,
|
||||
extractFragmentAskSidecar,
|
||||
isUsableContextFragment,
|
||||
expectsContextFragmentTag,
|
||||
} from "../src/skills/context-fragment.js";
|
||||
import {
|
||||
parseContextOrder,
|
||||
@@ -74,6 +76,26 @@ describe("context-fragment.v1", () => {
|
||||
"其它",
|
||||
]);
|
||||
});
|
||||
|
||||
it("isUsableContextFragment requires non-empty 正文", () => {
|
||||
expect(
|
||||
isUsableContextFragment({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "孤立免疫",
|
||||
正文: { 美学纲领: { 体验内核: "特权" } },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isUsableContextFragment({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "孤立免疫",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(expectsContextFragmentTag("设计.美学纲领与交互范式")).toBe(true);
|
||||
expect(expectsContextFragmentTag("设计.创作流程")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("context-order.v1", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
artifactTagForStep,
|
||||
extractModuleOpening,
|
||||
formatCreationFlowForUser,
|
||||
formatFlowProgressForAgent,
|
||||
formatModuleCatalogForAgent,
|
||||
formatRecipeCatalogForAgent,
|
||||
formatSelectedRecipeForAgent,
|
||||
@@ -14,16 +15,23 @@ import {
|
||||
loadModuleCatalog,
|
||||
loadModulePrompt,
|
||||
loadRecipeCatalog,
|
||||
mergeCreationFlowPreservingAccepted,
|
||||
needsFlowExpansion,
|
||||
nextPendingStep,
|
||||
parseCreationFlow,
|
||||
pickRecordedStepId,
|
||||
parseModuleCatalog,
|
||||
parseModuleOpeningState,
|
||||
parseRecipeCatalog,
|
||||
parseRecipeYaml,
|
||||
parseSelectedRecipeRef,
|
||||
resolveDesignStepBinding,
|
||||
shouldSkipModuleOpening,
|
||||
isProgressPointerTag,
|
||||
stringifyCreationFlow,
|
||||
creationFlowFromRecipeSeed,
|
||||
validateCreationFlow,
|
||||
validateStepReadyToRun,
|
||||
} from "../src/skills/creation-flow.js";
|
||||
import { loadWorkerSkillWithContext } from "../src/skills/loader.js";
|
||||
|
||||
@@ -67,6 +75,79 @@ describe("creation-flow", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps accepted steps intact when the planner rewrites the flow", () => {
|
||||
const prev = `{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }
|
||||
]
|
||||
}`;
|
||||
const rewritten = `{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": ["实现机制"] },
|
||||
{ "id": "实现机制", "name": "实现机制", "depends_on": [] }
|
||||
]
|
||||
}`;
|
||||
const merged = mergeCreationFlowPreservingAccepted({
|
||||
prevRaw: prev,
|
||||
nextRaw: rewritten,
|
||||
acceptedStepIds: ["美学纲领与交互范式"],
|
||||
});
|
||||
expect(merged.restored).toEqual(["美学纲领与交互范式"]);
|
||||
const flow = parseCreationFlow(merged.raw)!;
|
||||
expect(flow.steps[0]?.depends_on).toEqual([]);
|
||||
expect(flow.steps.map((s) => s.id)).toEqual([
|
||||
"美学纲领与交互范式",
|
||||
"实现机制",
|
||||
]);
|
||||
});
|
||||
|
||||
it("re-inserts an accepted step the planner dropped", () => {
|
||||
const prev = `{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }
|
||||
]
|
||||
}`;
|
||||
const rewritten = `{
|
||||
"status": "open",
|
||||
"steps": [{ "id": "实现机制", "name": "实现机制", "depends_on": [] }]
|
||||
}`;
|
||||
const merged = mergeCreationFlowPreservingAccepted({
|
||||
prevRaw: prev,
|
||||
nextRaw: rewritten,
|
||||
acceptedStepIds: ["美学纲领与交互范式"],
|
||||
});
|
||||
expect(parseCreationFlow(merged.raw)!.steps.map((s) => s.id)).toEqual([
|
||||
"美学纲领与交互范式",
|
||||
"实现机制",
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves an untouched rewrite alone", () => {
|
||||
const prev = `{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }
|
||||
]
|
||||
}`;
|
||||
const rewritten = `{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{ "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式"] }
|
||||
]
|
||||
}`;
|
||||
const merged = mergeCreationFlowPreservingAccepted({
|
||||
prevRaw: prev,
|
||||
nextRaw: rewritten,
|
||||
acceptedStepIds: ["美学纲领与交互范式"],
|
||||
});
|
||||
expect(merged.restored).toEqual([]);
|
||||
expect(merged.raw).toBe(rewritten);
|
||||
});
|
||||
|
||||
it("allows duplicate capability names with distinct ids", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
@@ -171,6 +252,29 @@ describe("creation-flow", () => {
|
||||
});
|
||||
expect(view.steps[2]?.occurrence).toBe(2);
|
||||
expect(view.steps[2]?.repeatable).toBe(true);
|
||||
expect(view.steps[0]?.runState).toBe("current");
|
||||
expect(view.steps[1]?.runState).toBe("pending");
|
||||
expect(view.steps[2]?.runState).toBe("pending");
|
||||
});
|
||||
|
||||
it("marks accepted steps done and the next pending current", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"brief": "网恋回合",
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
||||
{ "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] }
|
||||
]
|
||||
}`)!;
|
||||
const view = formatCreationFlowForUser(flow, sampleCatalog, [
|
||||
"美学纲领与交互范式",
|
||||
]);
|
||||
expect(view.steps.map((s) => s.runState)).toEqual([
|
||||
"done",
|
||||
"current",
|
||||
"pending",
|
||||
]);
|
||||
});
|
||||
|
||||
it("loads world-simulator catalog and formats for agent", async () => {
|
||||
@@ -273,6 +377,18 @@ steps:
|
||||
expect(formatted).toContain("用户已选配方");
|
||||
expect(formatted).toContain("核心思路");
|
||||
expect(formatted).toContain("增量");
|
||||
|
||||
const seeded = creationFlowFromRecipeSeed(detail);
|
||||
expect(seeded?.steps[0]?.name).toBe("美学纲领与交互范式");
|
||||
expect(seeded?.status).toBe("open");
|
||||
const raw = stringifyCreationFlow(seeded!);
|
||||
expect(parseCreationFlow(raw)?.steps[0]?.id).toBe("美学纲领与交互范式");
|
||||
expect(creationFlowFromRecipeSeed({ seed: null })).toBeNull();
|
||||
expect(
|
||||
creationFlowFromRecipeSeed({
|
||||
seed: { version: 1, status: "open", steps: [] },
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to legacy hint when methodology absent", () => {
|
||||
@@ -326,9 +442,50 @@ recipes:
|
||||
expect(selected.promptBody).toContain("核心思路");
|
||||
expect(selected.promptBody).toContain("何时用");
|
||||
expect(selected.promptBody).toContain("增量");
|
||||
expect(selected.promptBody).toContain("【流程进度】");
|
||||
expect(selected.promptBody).not.toContain("【导演】用户尚未手动选择");
|
||||
});
|
||||
|
||||
it("tells design-flow which steps are done and not to reschedule them", async () => {
|
||||
const flowRaw = JSON.stringify({
|
||||
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
|
||||
});
|
||||
const loaded = await loadWorkerSkillWithContext(
|
||||
"world-simulator",
|
||||
"design-flow",
|
||||
undefined,
|
||||
{
|
||||
selectedRecipeRef: "world-simulator",
|
||||
flowRaw,
|
||||
acceptedStepNames: ["美学纲领与交互范式"],
|
||||
filledArtifactTags: ["设计.美学纲领与交互范式"],
|
||||
},
|
||||
);
|
||||
expect(loaded.promptBody).toContain("【流程进度】");
|
||||
expect(loaded.promptBody).toContain("已完成:");
|
||||
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
||||
expect(loaded.promptBody).toContain("已验收");
|
||||
expect(loaded.promptBody).toContain("禁止再排一次执行");
|
||||
expect(loaded.promptBody).toContain("生成规则");
|
||||
});
|
||||
|
||||
it("formatFlowProgressForAgent keeps seed steps as draft not new work", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"steps": [
|
||||
{ "name": "美学纲领与交互范式", "depends_on": [] }
|
||||
]
|
||||
}`)!;
|
||||
const text = formatFlowProgressForAgent({
|
||||
flow,
|
||||
acceptedStepIds: [],
|
||||
catalog: sampleCatalog,
|
||||
});
|
||||
expect(text).toContain("草案已有、尚未验收");
|
||||
expect(text).toContain("美学纲领与交互范式");
|
||||
expect(text).toContain("不要当作新规划再写一遍");
|
||||
expect(text).toContain("可反复追加:生成规则、具体实例");
|
||||
});
|
||||
|
||||
it("injects aesthetics-interaction module prompt into design-step", async () => {
|
||||
const flowRaw = JSON.stringify({
|
||||
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
|
||||
@@ -344,6 +501,8 @@ recipes:
|
||||
},
|
||||
);
|
||||
expect(loaded.worker.outputTags).toContain("设计.美学纲领与交互范式");
|
||||
expect(loaded.worker.outputTags).not.toContain("创作.当前步骤");
|
||||
expect(loaded.worker.inputTags).toContain("创作.当前步骤");
|
||||
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
||||
expect(loaded.promptBody).toContain("体验契约");
|
||||
expect(loaded.promptBody).toContain("程序开场");
|
||||
@@ -386,6 +545,35 @@ recipes:
|
||||
美学纲领与交互范式: "shown",
|
||||
});
|
||||
|
||||
expect(
|
||||
shouldSkipModuleOpening({
|
||||
demand: "丧尸世界只有我不会被感染",
|
||||
dependsOn: [],
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldSkipModuleOpening({
|
||||
demand: "丧尸世界只有我不会被感染",
|
||||
dependsOn: ["美学纲领与交互范式"],
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldSkipModuleOpening({
|
||||
demand: "",
|
||||
dependsOn: [],
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldSkipModuleOpening({
|
||||
demand: "已有需求",
|
||||
dependsOn: [],
|
||||
phase: "shown",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(isProgressPointerTag("创作.当前步骤")).toBe(true);
|
||||
expect(isProgressPointerTag("设计.美学纲领与交互范式")).toBe(false);
|
||||
expect(isProgressPointerTag("设计.创作流程")).toBe(false);
|
||||
|
||||
const binding = await resolveDesignStepBinding({
|
||||
skillPackRoot: "dialogue/world-simulator",
|
||||
flowRaw: JSON.stringify({
|
||||
@@ -428,9 +616,11 @@ recipes:
|
||||
const missing = parseCreationFlow(`{
|
||||
"steps": [{ "name": "生成规则", "depends_on": [] }]
|
||||
}`)!;
|
||||
const bad = validateCreationFlow(missing, catalog);
|
||||
expect(bad.ok).toBe(false);
|
||||
expect(bad.errors.some((e) => e.includes("target"))).toBe(true);
|
||||
// 编排期允许空壳;必填 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);
|
||||
|
||||
const block = formatModuleCatalogForAgent(catalog!);
|
||||
expect(block).toContain("编排参数");
|
||||
@@ -483,6 +673,43 @@ recipes:
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("pickRecordedStepId ignores LLM junk on 创作.当前步骤", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] }
|
||||
]
|
||||
}`)!;
|
||||
expect(
|
||||
pickRecordedStepId({
|
||||
flow,
|
||||
alreadyAccepted: [],
|
||||
pinnedUnitId: "美学纲领与交互范式",
|
||||
writtenCurrentStep: JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
正文: { 交互范式: {}, 美学纲领: {} },
|
||||
}),
|
||||
}),
|
||||
).toBe("美学纲领与交互范式");
|
||||
expect(
|
||||
pickRecordedStepId({
|
||||
flow,
|
||||
alreadyAccepted: [],
|
||||
pinnedUnitId: "flow",
|
||||
writtenCurrentStep: "交互范式",
|
||||
}),
|
||||
).toBe("美学纲领与交互范式");
|
||||
expect(
|
||||
pickRecordedStepId({
|
||||
flow,
|
||||
alreadyAccepted: ["美学纲领与交互范式"],
|
||||
pinnedUnitId: "",
|
||||
writtenCurrentStep: "",
|
||||
}),
|
||||
).toBe("生成规则");
|
||||
});
|
||||
|
||||
it("open flow needs expansion after listed steps accepted", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
displayStageLabel,
|
||||
displayWorkerLabel,
|
||||
formatWorkerDisplayTitle,
|
||||
reviewComposerCopy,
|
||||
} from "../src/server/display-labels.js";
|
||||
|
||||
describe("display-labels", () => {
|
||||
@@ -27,4 +28,22 @@ describe("display-labels", () => {
|
||||
);
|
||||
expect(displayWorkerLabel("worker:narrator")).toBe("执行单元 · 叙事转述");
|
||||
});
|
||||
|
||||
it("maps review composer copy by worker", () => {
|
||||
const flow = reviewComposerCopy("design-flow");
|
||||
expect(flow.acceptLabel).toBe("确认编排");
|
||||
expect(flow.submitLabel).toBe("按意见改编排");
|
||||
expect(flow.kicker).toBe("核对编排");
|
||||
expect(flow.tone).toBe("plan");
|
||||
|
||||
const step = reviewComposerCopy("design-step");
|
||||
expect(step.acceptLabel).toBe("接受");
|
||||
expect(step.submitLabel).toBe("按意见修改");
|
||||
expect(step.kicker).toBe("待验收");
|
||||
expect(step.tone).toBe("accept");
|
||||
|
||||
const opening = reviewComposerCopy("opening-generator");
|
||||
expect(opening.acceptLabel).toBe("选定此开场");
|
||||
expect(opening.kicker).toBe("待选定");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ describe("phase machine", () => {
|
||||
outputTags: ["设计.worker集.草稿"],
|
||||
});
|
||||
session = { ...session, artifacts: [artifact] };
|
||||
session = applyEvent(session, {
|
||||
const completed = applyEvent(session, {
|
||||
type: "worker_completed",
|
||||
payload: {
|
||||
artifactId: artifact.id,
|
||||
@@ -95,12 +95,22 @@ describe("phase machine", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
}).session;
|
||||
});
|
||||
session = completed.session;
|
||||
|
||||
expect(session.waitingReason?.kind).toBe("review_artifact");
|
||||
if (session.waitingReason?.kind !== "review_artifact") return;
|
||||
expect(session.waitingReason.questions?.length).toBe(1);
|
||||
expect(session.waitingReason.questions?.[0]?.required).toBe(false);
|
||||
// 追问只挂 waitingReason,不另发「可选追问」消息(与产物同面展示)
|
||||
expect(
|
||||
completed.effects.some(
|
||||
(e) =>
|
||||
e.type === "emit_message" &&
|
||||
typeof e.message === "string" &&
|
||||
/可选追问/.test(e.message),
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
// 不答追问,直接接受产物
|
||||
session = applyEvent(session, {
|
||||
@@ -159,4 +169,152 @@ describe("phase machine", () => {
|
||||
}).session;
|
||||
expect(session.slots.designInstanceReady).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejecting artifact with feedback immediately reruns the same worker", () => {
|
||||
let session = createSession("default");
|
||||
const artifact = createArtifact({
|
||||
workerId: "design-step",
|
||||
stepId: "step-aesthetics",
|
||||
outputTags: ["设计.美学纲领"],
|
||||
});
|
||||
session = {
|
||||
...session,
|
||||
artifacts: [artifact],
|
||||
pendingArtifactId: artifact.id,
|
||||
currentStepId: "step-aesthetics",
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||
slots: {
|
||||
"用户.worker答复": "【追问作答】\n问:偏好?\n答:偏冷",
|
||||
},
|
||||
};
|
||||
|
||||
const result = applyEvent(session, {
|
||||
type: "user_rejected_artifact",
|
||||
payload: { artifactId: artifact.id, reason: "语气再冷一点" },
|
||||
});
|
||||
|
||||
expect(result.session.phase).toBe("running");
|
||||
expect(result.session.waitingReason).toBeUndefined();
|
||||
expect(result.session.slots["用户.修订说明"]).toBe("语气再冷一点");
|
||||
// 追问答复不得被修订说明冲掉
|
||||
expect(result.session.slots["用户.worker答复"]).toContain("偏冷");
|
||||
expect(result.session.artifacts[0]?.status).toBe("revision_requested");
|
||||
// 只有这一份底稿可在重跑写不出新版时退回
|
||||
expect(result.session.slots.revisionTargetArtifactId).toBe(artifact.id);
|
||||
expect(result.session.currentWorkerId).toBe("design-step");
|
||||
expect(result.session.currentStepId).toBe("step-aesthetics");
|
||||
expect(result.effects).toEqual([
|
||||
{ type: "run_worker", workerId: "design-step" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("failed revision rerun falls back to reviewing the previous artifact", () => {
|
||||
let session = createSession("default");
|
||||
const artifact = {
|
||||
...createArtifact({
|
||||
workerId: "design-step",
|
||||
stepId: "step-aesthetics",
|
||||
outputTags: ["设计.美学纲领"],
|
||||
}),
|
||||
status: "revision_requested" as const,
|
||||
};
|
||||
session = {
|
||||
...session,
|
||||
artifacts: [artifact],
|
||||
currentWorkerId: "design-step",
|
||||
currentStepId: "step-aesthetics",
|
||||
phase: "running",
|
||||
slots: { revisionTargetArtifactId: artifact.id },
|
||||
};
|
||||
|
||||
const result = applyEvent(session, {
|
||||
type: "worker_revision_produced_nothing",
|
||||
payload: {
|
||||
artifactId: artifact.id,
|
||||
questions: ["这一步还没写出可验收的产物。请再补一点你最在意的体验。"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.session.phase).toBe("waiting_user");
|
||||
expect(result.session.waitingReason?.kind).toBe("review_artifact");
|
||||
expect(result.session.pendingArtifactId).toBe(artifact.id);
|
||||
expect(result.session.artifacts[0]?.status).toBe("under_review");
|
||||
// 重试题降级为可选追问,用户仍可直接接受上一版
|
||||
const reason = result.session.waitingReason;
|
||||
expect(reason?.kind === "review_artifact" && reason.questions?.[0]?.required).toBe(
|
||||
false,
|
||||
);
|
||||
// 退回后不再是重跑目标,避免下一步无产物时又被挂回来
|
||||
expect(result.session.slots.revisionTargetArtifactId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejecting artifact without feedback waits for revision instruction", () => {
|
||||
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 },
|
||||
};
|
||||
|
||||
const rejected = applyEvent(session, {
|
||||
type: "user_rejected_artifact",
|
||||
payload: { artifactId: artifact.id },
|
||||
});
|
||||
expect(rejected.session.waitingReason?.kind).toBe("revision");
|
||||
expect(rejected.session.artifacts[0]?.status).toBe("rejected");
|
||||
|
||||
const resumed = applyEvent(rejected.session, {
|
||||
type: "user_submitted_input",
|
||||
payload: { text: "加一点冲突感" },
|
||||
});
|
||||
expect(resumed.session.phase).toBe("running");
|
||||
expect(resumed.session.slots["用户.修订说明"]).toBe("加一点冲突感");
|
||||
expect(resumed.effects).toEqual([
|
||||
{ type: "run_worker", workerId: "design-step" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepting design-step enters next_intent instead of invoking main agent", () => {
|
||||
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("next_intent");
|
||||
expect(accepted.effects.some((e) => e.type === "invoke_main_agent")).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
const continued = applyEvent(accepted.session, {
|
||||
type: "user_submitted_input",
|
||||
payload: { text: "接下来写怪物生成规则" },
|
||||
});
|
||||
expect(continued.session.phase).toBe("running");
|
||||
expect(continued.session.slots["用户.下一步意向"]).toBe(
|
||||
"接下来写怪物生成规则",
|
||||
);
|
||||
expect(continued.effects).toEqual([
|
||||
{ type: "propose_next_creation_step" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
appendAskedQuestions,
|
||||
dropAlreadyAskedQuestions,
|
||||
formatQuestionAnswersForAi,
|
||||
formatQuestionAnswersForDisplay,
|
||||
isModuleOpeningQuestions,
|
||||
MODULE_OPENING_QUESTION_ID,
|
||||
mergeQuestionsPreferFragment,
|
||||
normalizeQuestions,
|
||||
parseAskedQuestions,
|
||||
} from "../src/skills/question-protocol.js";
|
||||
|
||||
describe("normalizeQuestions", () => {
|
||||
@@ -28,6 +34,131 @@ describe("normalizeQuestions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isModuleOpeningQuestions", () => {
|
||||
it("recognizes the single module-opening question", () => {
|
||||
const qs = normalizeQuestions([
|
||||
{
|
||||
id: MODULE_OPENING_QUESTION_ID,
|
||||
prompt: "本轮生成对象已钉死。请直接谈…",
|
||||
allowOther: true,
|
||||
required: true,
|
||||
},
|
||||
]);
|
||||
expect(isModuleOpeningQuestions(qs)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects ordinary worker questions", () => {
|
||||
const qs = normalizeQuestions([
|
||||
{ id: "q1", prompt: "要不要池?" },
|
||||
]);
|
||||
expect(isModuleOpeningQuestions(qs)).toBe(false);
|
||||
expect(isModuleOpeningQuestions([])).toBe(false);
|
||||
expect(isModuleOpeningQuestions(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
describe("mergeQuestionsPreferFragment", () => {
|
||||
it("keeps the 示例 version when askUser repeats the same options", () => {
|
||||
const ask = normalizeQuestions([
|
||||
{
|
||||
prompt: "为贴近你要的质感:你最想反复感受到的是哪一种?",
|
||||
options: [
|
||||
"隐于都市的日常从容:所有人都只是凡人,而你心里清楚世界是你的",
|
||||
"身份揭示的瞬间翻转:前一刻还被人平视甚至冒犯,后一刻万人噤声",
|
||||
],
|
||||
},
|
||||
{
|
||||
prompt: "在你亮明身份之前,你希望普通世界如何对待你?",
|
||||
options: [
|
||||
"基本礼貌但无特别敬畏,偶尔会有微小冒犯",
|
||||
"因你气质不凡而隐隐客气,但没人知道原因",
|
||||
],
|
||||
},
|
||||
]);
|
||||
const frag = normalizeQuestions([
|
||||
{
|
||||
prompt:
|
||||
"你最想反复感受到的是哪一种?\n示例:比如:走在现代街头,有人撞了你一下。",
|
||||
options: [
|
||||
"隐于都市的日常从容:所有人都只是凡人,而你心里清楚世界是你的",
|
||||
"身份揭示的瞬间翻转:前一刻还被人平视甚至冒犯,后一刻万人噤声",
|
||||
],
|
||||
},
|
||||
{
|
||||
prompt:
|
||||
"在你亮明身份之前,你希望普通世界如何对待你?\n示例:这样我才好确定叙事里普通人对你的默认距离。",
|
||||
options: [
|
||||
"基本礼貌但无特别敬畏,偶尔会有微小冒犯",
|
||||
"因你气质不凡而隐隐客气,但没人知道原因",
|
||||
],
|
||||
},
|
||||
]);
|
||||
const merged = mergeQuestionsPreferFragment(ask, frag);
|
||||
expect(merged).toHaveLength(2);
|
||||
expect(merged[0]?.prompt).toContain("示例:");
|
||||
expect(merged[1]?.prompt).toContain("示例:");
|
||||
});
|
||||
|
||||
it("keeps askUser-only questions that are not in the fragment", () => {
|
||||
const ask = normalizeQuestions([
|
||||
{ prompt: "人称?", options: ["第二人称", "第一人称"] },
|
||||
]);
|
||||
const frag = normalizeQuestions([
|
||||
{
|
||||
prompt: "暴力尺度?\n示例:锈迹即可",
|
||||
options: ["少而锋利", "常态压抑"],
|
||||
},
|
||||
]);
|
||||
const merged = mergeQuestionsPreferFragment(ask, frag);
|
||||
expect(merged).toHaveLength(2);
|
||||
expect(merged[0]?.prompt).toContain("暴力尺度");
|
||||
expect(merged[1]?.prompt).toBe("人称?");
|
||||
});
|
||||
});
|
||||
|
||||
describe("已问追问去重", () => {
|
||||
const aesthetics = normalizeQuestions([
|
||||
{
|
||||
id: "focus",
|
||||
prompt: "「永远有 1 万元余额」这个设定,你希望体验更偏向哪种?",
|
||||
options: [
|
||||
"用这 1 万元在城市里精打细算地活下去",
|
||||
"靠这 1 万元撬动关系、机会与影响力",
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
it("编排层重抛上一步问过的题时整题丢掉", () => {
|
||||
const history = appendAskedQuestions([], aesthetics);
|
||||
const replanned = normalizeQuestions([
|
||||
{
|
||||
id: "focus",
|
||||
prompt: "「永远有 1 万元余额」这个设定,你希望体验更偏向哪种?",
|
||||
options: [
|
||||
"用这 1 万元在城市里精打细算地活下去",
|
||||
"靠这 1 万元撬动关系、机会与影响力",
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(dropAlreadyAskedQuestions(replanned, history)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("新问题照常保留", () => {
|
||||
const history = appendAskedQuestions([], aesthetics);
|
||||
const fresh = normalizeQuestions([
|
||||
{ prompt: "下一步先排实现机制还是叙事指南?", options: ["实现机制"] },
|
||||
]);
|
||||
expect(dropAlreadyAskedQuestions(fresh, history)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("留痕可经 slots 往返,且同题不重复堆积", () => {
|
||||
const once = appendAskedQuestions([], aesthetics);
|
||||
const twice = appendAskedQuestions(once, aesthetics);
|
||||
expect(twice).toHaveLength(1);
|
||||
const roundTripped = parseAskedQuestions(JSON.stringify(twice));
|
||||
expect(dropAlreadyAskedQuestions(aesthetics, roundTripped)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatQuestionAnswers", () => {
|
||||
it("AI payload keeps Q+A; display omits prompts", () => {
|
||||
const questions = normalizeQuestions([
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
MockLlmProvider,
|
||||
} from "../src/llm/client.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";
|
||||
import { createSession } from "../src/runtime/phase-machine.js";
|
||||
|
||||
@@ -22,6 +23,12 @@ describe("main agent tool loop", () => {
|
||||
listArtifacts: () => [],
|
||||
};
|
||||
|
||||
it("exposes only DAG routing tools", () => {
|
||||
expect(
|
||||
MAIN_AGENT_TOOL_DEFINITIONS.map((tool) => tool.function.name),
|
||||
).toEqual(["read_blackboard", "run_worker"]);
|
||||
});
|
||||
|
||||
it("runs loop tools then returns terminal decision", async () => {
|
||||
const llm = new MockLlmProvider([
|
||||
createMockToolCall("read_blackboard", { tags: ["book.brief"] }),
|
||||
|
||||
@@ -119,4 +119,114 @@ describe("sanitizeWorkerSetOutputs", () => {
|
||||
expect(result.askAssessment).toContain("美学纲领 6/10");
|
||||
expect(result.askAssessment).toContain("还需确认");
|
||||
});
|
||||
|
||||
it("drops askUser copies of fragment 追问 and keeps the 示例 prompt", () => {
|
||||
const result = parseWorkerResponseForTest(
|
||||
JSON.stringify({
|
||||
outputs: {
|
||||
"设计.美学纲领与交互范式": {
|
||||
schema: "context-fragment.v1",
|
||||
brief: "孤立免疫",
|
||||
正文: { 美学纲领: { 体验内核: "特权与惊惶" } },
|
||||
追问: {
|
||||
导语: "还需确认:",
|
||||
题目: [
|
||||
{
|
||||
问: "你最想反复感受到的是哪一种?",
|
||||
建议选项: ["日常从容", "揭示翻转"],
|
||||
示例: "袖口唐纹",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
summary: "美学纲领 · 孤立免疫",
|
||||
askUser: [
|
||||
{
|
||||
prompt: "为贴近你要的质感:你最想反复感受到的是哪一种?",
|
||||
options: ["日常从容", "揭示翻转"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
["设计.美学纲领与交互范式"],
|
||||
);
|
||||
expect(result.askUser).toHaveLength(1);
|
||||
expect(result.askUser?.[0]?.prompt).toContain("示例:袖口唐纹");
|
||||
expect(result.askUser?.[0]?.prompt).not.toContain("为贴近你要的质感");
|
||||
});
|
||||
|
||||
it("recovers a root-level context-fragment into the artifact tag", () => {
|
||||
const frag = {
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "孤立免疫",
|
||||
正文: {
|
||||
设定逻辑: { 变造: { 原型: "丧尸", 变点: "唯我免疫" } },
|
||||
交互范式: { 代入: "完全" },
|
||||
美学纲领: { 体验内核: "人人录我" },
|
||||
},
|
||||
};
|
||||
const result = parseWorkerResponseForTest(JSON.stringify(frag), [
|
||||
"设计.美学纲领与交互范式",
|
||||
]);
|
||||
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("人人录我");
|
||||
expect(result.askUser).toBeUndefined();
|
||||
});
|
||||
|
||||
it("moves a fragment written to 创作.当前步骤 onto the artifact tag", () => {
|
||||
const frag = {
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "孤立免疫",
|
||||
正文: { 美学纲领: { 体验内核: "特权与惊惶" } },
|
||||
};
|
||||
const result = parseWorkerResponseForTest(
|
||||
JSON.stringify({
|
||||
outputs: { "创作.当前步骤": frag },
|
||||
summary: "误写入指针",
|
||||
}),
|
||||
["设计.美学纲领与交互范式", "创作.当前步骤"],
|
||||
);
|
||||
expect(result.outputs["创作.当前步骤"]).toBeUndefined();
|
||||
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("特权与惊惶");
|
||||
});
|
||||
|
||||
it("does not accept a fragment without 正文; keeps 追问 for the LLM loop", () => {
|
||||
const result = parseWorkerResponseForTest(
|
||||
JSON.stringify({
|
||||
outputs: {
|
||||
"设计.美学纲领与交互范式": {
|
||||
schema: "context-fragment.v1",
|
||||
技能: "美学纲领与交互范式",
|
||||
brief: "在唯一免疫的末日联接中体验绝望掌控感",
|
||||
mount: "world-simulator",
|
||||
追问: {
|
||||
导语: "还差站位",
|
||||
题目: [{ 问: "你代入吗?", 建议选项: ["完全代入", "旁观"] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
summary: "半残",
|
||||
}),
|
||||
["设计.美学纲领与交互范式"],
|
||||
);
|
||||
expect(result.outputs["设计.美学纲领与交互范式"]).toBeUndefined();
|
||||
expect(result.askUser?.length).toBeGreaterThan(0);
|
||||
expect(result.askUser?.[0]?.prompt).toContain("你代入吗");
|
||||
});
|
||||
|
||||
it("asks to continue the loop when fragment JSON is unusable", () => {
|
||||
const result = parseWorkerResponseForTest(
|
||||
JSON.stringify({
|
||||
outputs: {
|
||||
"设计.美学纲领与交互范式":
|
||||
'{"schema":"context-fragment.v1","brief":"孤立免疫","mount":"world-simulator"',
|
||||
},
|
||||
summary: "截断",
|
||||
}),
|
||||
["设计.美学纲领与交互范式"],
|
||||
);
|
||||
expect(result.outputs["设计.美学纲领与交互范式"]).toBeUndefined();
|
||||
expect(result.askUser?.[0]?.prompt).toContain("还没写出可验收的产物");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user