muse-agent-example/web/tests/旅程/探索草稿.spec.ts
zizi 224df00706 W13 渐进探索、分层规划与近期细化:私人探索、明确选定、分层规划与近期细化;含竞态修复新用例。
按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
2026-09-10 19:25:41 +08:00

353 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** 实际浏览器和安装包;私人探索保存不产生正式作品或模型调用。 */
import { readFileSync } from "node:fs";
import { expect, test } from "@playwright/test";
test.use({ trace: "off" });
test("NC-browser-private-exploration:中文缓冲恢复冲突与独立私人保存", async ({
page,
context,
}, info) => {
if (!process.env.MUSE_WORKBENCH_URL || !process.env.MUSE_AUTHOR_PASSWORD_FILE)
throw new Error("需要明确的隔离后端和作者口令文件。");
await page.goto("/");
await page
.getByLabel("访问口令")
.fill(readFileSync(process.env.MUSE_AUTHOR_PASSWORD_FILE, "utf8").trim());
await page.getByRole("button", { name: "进入工作台" }).click();
const before = await (await page.request.get("/api/v1/works")).json();
await page.getByRole("link", { name: "先记下一个想法 →" }).click();
await page.getByRole("link", { name: "记下新想法" }).click();
await page.getByLabel("稿件名称").fill("雨夜的伞(合成探索)");
await page
.getByLabel("我的想法")
.fill(" 妹妹握着一把湿伞。\n“哥哥还没回来。”e\u0301🧑‍🏽");
await page.reload();
await expect(page.getByLabel("我的想法")).toHaveValue(
" 妹妹握着一把湿伞。\n“哥哥还没回来。”e\u0301🧑‍🏽",
);
await page.getByRole("button", { name: "保存私人稿" }).click();
await expect(page.getByText("私人稿已保存。", { exact: true })).toBeVisible();
await expect(
page.getByRole("button", { name: "检查整理稿格式" }),
).toBeEnabled();
await page.getByRole("button", { name: "检查整理稿格式" }).click();
await expect(
page.getByRole("heading", { name: "这些地方还需要整理" }),
).toBeVisible();
const id = new URL(page.url()).pathname.split("/").at(-1)!;
const api = `/api/v1/explorations/${id}`;
const original = await (await page.request.get(api)).json();
const other = await context.newPage();
await other.goto(page.url());
await expect(other.getByLabel("我的想法")).toHaveValue(original.content);
await page.getByLabel("我的想法").fill("妹妹撑开了伞。");
await page.getByRole("button", { name: "保存私人稿" }).click();
await expect
.poll(async () => (await (await page.request.get(api)).json()).revision)
.toBe(2);
await other.getByLabel("我的想法").fill("妹妹把伞收了起来。");
await other.getByRole("button", { name: "保存私人稿" }).click();
await expect(
other.getByRole("heading", { name: "另一处保存了新内容" }),
).toBeVisible();
await expect(other.getByLabel("我的想法")).toHaveValue("妹妹把伞收了起来。");
expect((await (await page.request.get(api)).json()).content).toBe(
"妹妹撑开了伞。",
);
await other
.getByRole("button", { name: "保留我的改稿,基于当前稿保存" })
.click();
await other.getByRole("button", { name: "保存私人稿" }).click();
await expect
.poll(async () => (await (await page.request.get(api)).json()).revision)
.toBe(3);
await other.screenshot({
path: info.outputPath("私人探索与保存.png"),
fullPage: true,
});
await other.close();
// 未修改的旧缓存不会盖过另一页已经保存的新稿。
await page.reload();
await expect(page.getByLabel("我的想法")).toHaveValue("妹妹把伞收了起来。");
expect(await (await page.request.get("/api/v1/works")).json()).toEqual(
before,
);
});
test("NC-browser-exploration-selection:明确选定后保留不可变来源", async ({
page,
}, info) => {
if (!process.env.MUSE_AUTHOR_PASSWORD_FILE)
throw new Error("需要作者口令引用。");
await page.goto("/");
await page
.getByLabel("访问口令")
.fill(readFileSync(process.env.MUSE_AUTHOR_PASSWORD_FILE, "utf8").trim());
await page.getByRole("button", { name: "进入工作台" }).click();
await page.getByRole("button", { name: "新建作品" }).click();
await page.getByLabel("名称", { exact: true }).fill("浮岛规划(合成验收)");
const workId = new URL(page.url()).pathname.split("/")[2];
await page.getByRole("button", { name: "保存档案" }).click();
await expect(page.getByLabel("新章节标题")).toBeVisible();
await page.goto("/explorations");
await page.getByRole("link", { name: "记下新想法" }).click();
const headings = [
"根设定",
"一、作品定位与核心命题",
"二、故事世界与社会系统",
"三、角色与关系",
"四、故事机制与关键实体",
"五、阶段与状态变化",
"六、情节架构与全书大纲",
"七、主题与读者体验弧线",
"八、信息、悬念与伏笔管理",
"九、叙事执行与一致性",
"待决与取舍",
];
const source =
"# 浮岛|作品探索\n\n" +
headings.map((h) => `## ${h}\n未探索:先保留浮岛停电的画面。`).join("\n\n");
await page.getByLabel("稿件名称").fill("浮岛的夜晚");
await page.getByLabel("我的想法").fill(source);
await page.getByRole("button", { name: "保存私人稿" }).click();
await expect(
page.getByRole("button", { name: "检查整理稿格式" }),
).toBeEnabled();
await page.getByRole("button", { name: "检查整理稿格式" }).click();
await expect(
page.getByRole("heading", { name: "整理稿格式完整" }),
).toBeVisible();
await page.getByLabel("交给哪部作品").selectOption(workId);
const button = page.getByRole("button", { name: "选定本稿作为规划依据" });
await expect(button).toBeEnabled();
await button.click();
await expect(
page.getByText("已选定为作品规划依据。正式规划仍需单独确认。", {
exact: true,
}),
).toBeVisible();
const path = `/api/v1/works/${workId}/exploration-selection`;
const chosen = await (await page.request.get(path)).json();
expect(chosen.revision).toBe(1);
expect(chosen.selection.snapshot.content).toBe(source);
await page.screenshot({
path: info.outputPath("探索方向交接.png"),
fullPage: true,
});
await page.getByLabel("我的想法").fill(source + "\n尚未选定的新分歧。");
await page.getByRole("button", { name: "保存私人稿" }).click();
await expect(page.getByText("私人稿已保存。", { exact: true })).toBeVisible();
expect(await (await page.request.get(path)).json()).toEqual(chosen);
expect(
(await (await page.request.get(`/api/v1/works/${workId}/chapters`)).json())
.chapters,
).toEqual([]);
});
test("NC-browser-plan-approval:动态大纲候选确认与双页冲突", async ({
page,
context,
}, info) => {
if (!process.env.MUSE_AUTHOR_PASSWORD_FILE)
throw new Error("需要作者口令引用。");
await page.goto("/");
await page
.getByLabel("访问口令")
.fill(readFileSync(process.env.MUSE_AUTHOR_PASSWORD_FILE, "utf8").trim());
await page.getByRole("button", { name: "进入工作台" }).click();
await page.getByRole("button", { name: "新建作品" }).click();
await page.getByLabel("名称", { exact: true }).fill("雪中的信(规划验收)");
const work = new URL(page.url()).pathname.split("/")[2];
await page.getByRole("button", { name: "保存档案" }).click();
await page.getByRole("link", { name: "查看作品规划" }).click();
await page.getByRole("button", { name: "另拟大纲" }).click();
await page.getByLabel("名称", { exact: true }).fill("雪中的信");
await page
.getByLabel("主线一句话", { exact: true })
.fill("邮差寻找一封无人认领的信。");
await page.getByRole("button", { name: "保存规划候选" }).click();
const plans = `/api/v1/works/${work}/plans`;
await expect(
page.getByText("规划候选已保存,请打开审阅。", { exact: true }),
).toBeVisible();
expect(await (await page.request.get(plans)).json()).toEqual([]);
await page.getByRole("button", { name: /候选 \d+ · 待决定/ }).click();
await page.getByRole("button", { name: "继续修改这份候选" }).click();
await page.getByLabel("名称", { exact: true }).fill("雪后的信");
await page.getByRole("button", { name: "保存规划候选" }).click();
await expect(
page.getByText("规划候选已保存,请打开审阅。", { exact: true }),
).toBeVisible();
await expect
.poll(
async () =>
(await (await page.request.get(`${plans}/candidates`)).json())[0]
.revision,
)
.toBe(2);
const revised = (
await (await page.request.get(`${plans}/candidates`)).json()
)[0];
expect(revised.revision).toBe(2);
const original = await (
await page.request.get(
`/api/v1/plan-candidates/${revised.candidate_id}?version=1`,
)
).json();
expect(original.content["名称"]).toBe("雪中的信");
await page.getByRole("button", { name: /候选 \d+ · 待决定/ }).click();
await page.getByRole("button", { name: "确认这份规划" }).click();
await expect(
page.getByText("这份规划已确认。", { exact: true }),
).toBeVisible();
expect((await (await page.request.get(plans)).json())[0].revision).toBe(1);
await page.getByRole("button", { name: "另拟大纲" }).click();
await page
.getByLabel("主线一句话", { exact: true })
.fill("邮差发现寄信人始终住在隔壁。");
await page.getByRole("button", { name: "保存规划候选" }).click();
await page.getByRole("button", { name: /候选 \d+ · 待决定/ }).click();
const other = await context.newPage();
await other.goto(page.url());
await other.getByRole("button", { name: /候选 \d+ · 待决定/ }).click();
await expect(
other.getByRole("button", { name: "确认这份规划" }),
).toBeVisible();
const drafting = await context.newPage();
await drafting.goto(page.url());
await drafting.getByRole("button", { name: "另拟大纲" }).click();
await drafting
.getByLabel("主线一句话", { exact: true })
.fill("邮差决定亲手递出这封信。");
await page.screenshot({
path: info.outputPath("大纲版本审阅.png"),
fullPage: true,
});
await page.getByRole("button", { name: "确认这份规划" }).click();
await expect
.poll(
async () => (await (await page.request.get(plans)).json())[0].revision,
)
.toBe(2);
await other.getByRole("button", { name: "确认这份规划" }).click();
await expect(other.getByRole("alert")).toBeVisible();
expect((await (await page.request.get(plans)).json())[0].revision).toBe(2);
await drafting.getByRole("button", { name: "保存规划候选" }).click();
await expect(
drafting.getByText("保存基线已变化。你的输入仍在下方,请先比较当前内容。", {
exact: true,
}),
).toBeVisible();
await expect(drafting.getByLabel("主线一句话", { exact: true })).toHaveValue(
"邮差决定亲手递出这封信。",
);
await drafting
.getByRole("button", { name: "保留我的改动,基于当前版本重看" })
.click();
await expect(drafting.getByLabel("主线一句话", { exact: true })).toHaveValue(
"邮差决定亲手递出这封信。",
);
// 重看会等待最新正式版本;冲突面板消失后才重建了基线。
await expect(
drafting.getByText("保存基线已变化。你的输入仍在下方,请先比较当前内容。", {
exact: true,
}),
).toBeHidden();
await drafting.getByRole("button", { name: "保存规划候选" }).click();
await expect(
drafting.getByText("规划候选已保存,请打开审阅。", { exact: true }),
).toBeVisible();
await drafting.getByRole("button", { name: /候选 \d+ · 待决定/ }).click();
await drafting.getByRole("button", { name: "确认这份规划" }).click();
await expect
.poll(
async () => (await (await page.request.get(plans)).json())[0].revision,
)
.toBe(3);
expect(
(await (await page.request.get(plans)).json())[0].content["主线一句话"],
).toBe("邮差决定亲手递出这封信。");
await drafting.screenshot({
path: info.outputPath("规划冲突恢复.png"),
fullPage: true,
});
await drafting.close();
await other.close();
});
test("NC-browser-plan-nodes:卷章场景目录接通动态场景规划", async ({
page,
}, info) => {
if (!process.env.MUSE_AUTHOR_PASSWORD_FILE)
throw new Error("需要作者口令引用。");
await page.goto("/");
await page
.getByLabel("访问口令")
.fill(readFileSync(process.env.MUSE_AUTHOR_PASSWORD_FILE, "utf8").trim());
await page.getByRole("button", { name: "进入工作台" }).click();
await page.getByRole("button", { name: "新建作品" }).click();
await page.getByLabel("名称", { exact: true }).fill("浮岛航线(节点验收)");
const work = new URL(page.url()).pathname.split("/")[2];
await page.getByRole("button", { name: "保存档案" }).click();
await page.getByRole("link", { name: "查看作品规划" }).click();
await page.getByText("卷、章与场景", { exact: true }).click();
await page.getByLabel("新节点名称", { exact: true }).fill("第一卷");
await page.getByRole("button", { name: "加入目录" }).click();
await page.getByLabel("新增层级").selectOption("chapter");
await page.getByLabel("新节点名称", { exact: true }).fill("起航");
await page
.getByRole("combobox", { name: "新节点归属", exact: true })
.selectOption({ label: "第一卷" });
await page.getByRole("button", { name: "加入目录" }).click();
await page.getByLabel("新增层级").selectOption("scene");
await page.getByLabel("新节点名称", { exact: true }).fill("门外访客");
await page
.getByRole("combobox", { name: "新节点归属", exact: true })
.selectOption({ label: "起航" });
await page.getByRole("button", { name: "加入目录" }).click();
await page.getByRole("button", { name: "保存节点目录" }).click();
await expect(
page.getByText("卷章与场景目录已保存。", { exact: true }),
).toBeVisible();
const directory = await (
await page.request.get(`/api/v1/works/${work}/plan-nodes`)
).json();
const chapter = directory.nodes.find(
(n: { kind: string }) => n.kind === "chapter",
),
scene = directory.nodes.find((n: { kind: string }) => n.kind === "scene");
expect(directory.revision).toBe(1);
expect(scene.parent_id).toBe(chapter.node_id);
await page.getByRole("link", { name: "填写门外访客的规划" }).click();
await page.getByRole("button", { name: "另拟节点计划" }).click();
await page
.getByLabel("目标推进收束", { exact: true })
.fill("访客想借一盏灯,船长让他进屋。");
await page.getByRole("button", { name: "保存规划候选" }).click();
await expect(
page.getByText("规划候选已保存,请打开审阅。", { exact: true }),
).toBeVisible();
await page.getByRole("button", { name: /候选 \d+ · 待决定/ }).click();
await page.getByRole("button", { name: "确认这份规划" }).click();
await expect(
page.getByText("这份规划已确认。", { exact: true }),
).toBeVisible();
const plans = await (
await page.request.get(`/api/v1/works/${work}/plans`)
).json();
expect(plans).toHaveLength(1);
expect(plans[0]).toMatchObject({
node_id: scene.node_id,
chapter_id: chapter.node_id,
type_id: "scene",
revision: 1,
});
expect(plans[0].content["目标推进收束"]).toBe(
"访客想借一盏灯,船长让他进屋。",
);
await page.screenshot({
path: info.outputPath("场景节点规划.png"),
fullPage: true,
});
});