muse-agent-example/web/tests/交互/规划重看基线.test.tsx
zizi cf0f4fb985 W20–W24:保存知识方法、迁移框架、审校修订与案例行为的集成成果
接续 88dd570,保存 W20–W24 已实现的共享接口、业务入口、迁移、工作台、测试与文档。
W20/W22/W23 保持 in_progress,W21/W24 保持 verified;此提交不宣称方法或规则正式启用、多轮返修、真实角色评测完成。

W25 新增实验、标定、逐调用交付与角色执行及其迁移/测试/索引留在实施工作树,原有私人和旧实现保留项不纳入。

验证:离线 571、前端 33 通过;PG 469 项通过、2 项浏览器未启用,2 项误带入的 W25 用例已移出本提交;最终任务与交付边界 37 项通过。make 检查、最终类型、84 项资源及 diff 检查通过。未重跑浏览器或 Pi 宿主,不以合成调用认证外部模型效果。
独立整体审查四维通过;证据保存在 R2-20260909/提交W20-W24。
2026-09-13 16:24:42 +08:00

199 lines
7.1 KiB
TypeScript
Raw 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: [] };
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);
});
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();
});