muse-agent-example/web/tests/交互/规划重看基线.test.tsx
zizi ae02fd1732 前端:工作台页面、事件标签与旅程用例
- 上下文清单、任务列表与详情、恢复控制、运行观察、创作对话与消息流、正式变更决策面板、正文写作与候选审阅、资料研究等页面同步本轮后端能力。
- 事件标签表补齐后端事件类型;查询键与生成客户端随 HTTP 契约重新生成。
- 依赖调整后 lockfile 同步;交互用例与浏览器旅程 spec 一并更新。
- 证据:make 前端检查(lint + typecheck + Pi 工具桥 tsc)exit 0;make 前端测试 37 文件 / 120 用例通过。
2026-09-18 01:15:07 +08:00

201 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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 { afterEach, expect, it, vi } from "vitest";
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
sessionStorage.clear();
});
const 空模式 = { type: "object", properties: {}, required: [] };
// @muse-case {"case_id":"NC-plan-rebase-await-fresh","environment":"Vitest真实组件;HTTP传输替身不算真实业务","given":"规划候选保存遇到版本冲突,最新表单刷新仍在途","when":"冲突提示刚出现就点击重看并再次保存","then":["重看等待最新正式版本,保存携带新基线且提示已保存"],"contract":"docs/系统架构/新版设计/接口契约/命令与作者决策.md"}
it("NC-plan-rebase-await-fresh:冲突后立即重看并保存,携带最新正式版本", async () => {
const 保存请求: unknown[] = [];
let 表单次数 = 0;
const 延迟 = (毫秒: number) => new Promise((r) => setTimeout(r, 毫秒));
vi.stubGlobal(
"fetch",
vi.fn(async (request: Request) => {
const 地址 = new URL(request.url).pathname;
const 应答 = (体: unknown, 状态 = 200) =>
new Response(JSON.stringify(体), {
status: 状态,
headers: { "Content-Type": "application/json" },
});
if (地址.startsWith("/api/v1/session")) return 应答({});
if (地址.includes("/plan-nodes"))
return 应答({ work_id: "work", revision: 1, nodes: [] });
if (地址.includes("/plans/form")) {
表单次数 += 1;
if (表单次数 === 1)
return 应答({
work_id: "work",
chapter_id: null,
node_id: null,
plan_id: "plan",
revision: 1,
schema_binding: {},
content: {},
visible_schema: 空模式,
editable_schema: 空模式,
});
// 冲突后的刷新故意延迟,放大“重看取到旧基线”的竞态窗口。
await 延迟(250);
return 应答({
work_id: "work",
chapter_id: null,
node_id: null,
plan_id: "plan",
revision: 2,
schema_binding: {},
content: {},
visible_schema: 空模式,
editable_schema: 空模式,
});
}
if (地址.includes("/plans/candidates") && request.method === "GET")
return 应答([]);
if (地址.includes("/plans/candidates")) {
保存请求.push(await request.clone().json());
if (保存请求.length === 1)
return 应答(
{ code: "REVISION_CONFLICT", message: "规划正式版本已变化" },
409,
);
return 应答({ receipt_id: "r2", action: "manual_save", results: [] });
}
if (地址.includes("/exploration-selection"))
return 应答({ work_id: "work", revision: 0, selection: null });
return 应答({});
}),
);
const { Ui规划工作区 } = await import("../../src/功能/作品规划/规划工作区");
const 客户端 = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={客户端}>
<MemoryRouter initialEntries={["/works/work/plans"]}>
<Ui规划工作区 />
</MemoryRouter>
</QueryClientProvider>,
);
fireEvent.click(await screen.findByRole("button", { name: "另拟大纲" }));
fireEvent.click(screen.getByRole("button", { name: "保存规划候选" }));
expect(
await screen.findByText(
"保存基线已变化。你的输入仍在下方,请先比较当前内容。",
),
).toBeTruthy();
// 冲突提示出现时最新表单仍在途;重看按钮等待新数据后才可点击。
const 重看 = screen.getByRole("button", {
name: "保留我的改动,基于当前版本重看",
});
await waitFor(() => expect((重看 as HTMLButtonElement).disabled).toBe(false));
fireEvent.click(重看);
await waitFor(() =>
expect(
screen.queryByText(
"保存基线已变化。你的输入仍在下方,请先比较当前内容。",
),
).toBeNull(),
);
fireEvent.click(screen.getByRole("button", { name: "保存规划候选" }));
expect(await screen.findByText("规划候选已保存,请打开审阅。")).toBeTruthy();
const 第二次 = 保存请求.at(-1) as { request: { expected_revision: number } };
expect(第二次.request.expected_revision).toBe(2);
});
// @muse-case {"case_id":"NC-plan-model-task-retry","environment":"Vitest真实组件;HTTP传输替身不算真实业务","given":"真实规划组件与传输替身;首次创建回执为未知503","when":"作者重试同一次创建","then":["重用相同命令、配置与基线;只显示已创建任务,不发送候选采纳"],"contract":"docs/系统架构/新版设计/模块设计/B01-作品规划.md"}
it("NC-plan-model-task-retry:规划创建回执未知时重试原命令,不自动采纳", async () => {
const 请求集: Record<string, unknown>[] = [];
let 正式写入 = 0;
const form = {
work_id: "work",
chapter_id: null,
node_id: null,
plan_id: "plan",
revision: 2,
schema_binding: {
schema_id: "outline",
base_version: 1,
effective_schema_hash: "schema",
projection_version: "view",
},
content: {},
visible_schema: 空模式,
editable_schema: 空模式,
};
vi.stubGlobal(
"fetch",
vi.fn(async (request: Request) => {
const path = new URL(request.url).pathname;
const reply = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
if (path.startsWith("/api/v1/session"))
return reply({ author_id: "author" });
if (path.includes("/plan-nodes"))
return reply({ work_id: "work", revision: 1, nodes: [] });
if (path.includes("/plans/form")) return reply(form);
if (path.includes("/plans/candidates")) {
if (request.method !== "GET") 正式写入++;
return reply([]);
}
if (path.includes("/exploration-selection"))
return reply({ work_id: "work", revision: 0, selection: null });
if (path.includes("/planning-tasks")) {
请求集.push(await request.clone().json());
return 请求集.length === 1
? reply({ code: "UNKNOWN", message: "回执暂不可知" }, 503)
: reply({ task_id: "task-plan" }, 201);
}
throw new Error("未约定的请求:" + path);
}),
);
const { Ui规划工作区 } = await import("../../src/功能/作品规划/规划工作区");
render(
<QueryClientProvider
client={
new QueryClient({ defaultOptions: { queries: { retry: false } } })
}
>
<MemoryRouter initialEntries={["/works/work/plans"]}>
<Ui规划工作区 />
</MemoryRouter>
</QueryClientProvider>,
);
fireEvent.change(await screen.findByLabelText("规划要求"), {
target: { value: "细化本章承接" },
});
fireEvent.change(screen.getByLabelText("规划模型配置"), {
target: { value: "planner-config" },
});
fireEvent.click(screen.getByRole("button", { name: "创建规划任务" }));
await screen.findByText("回执暂不可知");
const 重试 = screen.getByRole("button", { name: "重试同一规划任务" });
await waitFor(() => expect((重试 as HTMLButtonElement).disabled).toBe(false));
fireEvent.click(重试);
await screen.findByText(/任务已创建:task-plan/);
expect(请求集).toHaveLength(2);
expect(请求集[0]).toEqual(请求集[1]);
expect(请求集[1]).toMatchObject({
request: { work_id: "work", expected_revision: 2, content: {} },
config_id: "planner-config",
});
expect(正式写入).toBe(0);
expect(screen.queryByText("这份规划已确认。")).toBeNull();
});