- 节点循环:配方开局直坐 DAG 第一步;验收后先问下一步意向,再确认开干并可钉编排参数;「按意见修改」只重跑当前节点。 - 询问分流:能力 opening 走说话面引导,可跳过追问按题干去重;追问与产物同轮挂载,避免拆成两段历史。 - 运行时:补强 revision 重跑、创作流程合并/进度指针、工具循环与 worker 执行;新增运行日志与 web:watch。 - 前端:统一等待态文案与底栏主按钮,完善询问卡/产物验收/呈现壳样式与交互。 - 同步世界模拟器模块提示、编排文档与相关测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
197 lines
7.0 KiB
TypeScript
197 lines
7.0 KiB
TypeScript
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", () => {
|
||
it("accepts string[]", () => {
|
||
const qs = normalizeQuestions(["站位倾向?", " "]);
|
||
expect(qs).toHaveLength(1);
|
||
expect(qs[0]?.prompt).toBe("站位倾向?");
|
||
expect(qs[0]?.allowOther).toBe(true);
|
||
});
|
||
|
||
it("accepts structured items with options", () => {
|
||
const qs = normalizeQuestions([
|
||
{
|
||
id: "stance",
|
||
prompt: "你更想以什么身份玩?",
|
||
options: ["单角幸存者代入", { id: "B", label: "旁观调度" }],
|
||
},
|
||
]);
|
||
expect(qs[0]?.id).toBe("stance");
|
||
expect(qs[0]?.options).toHaveLength(2);
|
||
expect(qs[0]?.options?.[0]?.id).toBe("A");
|
||
expect(qs[0]?.options?.[0]?.label).toContain("幸存者");
|
||
});
|
||
});
|
||
|
||
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([
|
||
{ id: "q1", prompt: "有无其它幸存者?", options: ["没有,仅自然生物"] },
|
||
]);
|
||
const answers = [
|
||
{ questionId: "q1", optionId: "A", text: "没有,仅自然生物" },
|
||
];
|
||
const ai = formatQuestionAnswersForAi(questions, answers);
|
||
expect(ai).toContain("问:有无其它幸存者?");
|
||
expect(ai).toContain("答:没有,仅自然生物");
|
||
const display = formatQuestionAnswersForDisplay(questions, answers);
|
||
expect(display).toBe("没有,仅自然生物");
|
||
expect(display).not.toContain("问:");
|
||
});
|
||
|
||
it("appends free-text note to AI and display payloads", () => {
|
||
const questions = normalizeQuestions([
|
||
{ id: "q1", prompt: "节奏?", options: ["严格回合制"] },
|
||
]);
|
||
const answers = [
|
||
{ questionId: "q1", optionId: "A", text: "严格回合制" },
|
||
];
|
||
const ai = formatQuestionAnswersForAi(questions, answers, "希望偏慢热");
|
||
expect(ai).toContain("【补充】");
|
||
expect(ai).toContain("希望偏慢热");
|
||
const display = formatQuestionAnswersForDisplay(
|
||
questions,
|
||
answers,
|
||
"希望偏慢热",
|
||
);
|
||
expect(display).toContain("严格回合制");
|
||
expect(display).toContain("希望偏慢热");
|
||
});
|
||
});
|