- 上下文清单、任务列表与详情、恢复控制、运行观察、创作对话与消息流、正式变更决策面板、正文写作与候选审阅、资料研究等页面同步本轮后端能力。 - 事件标签表补齐后端事件类型;查询键与生成客户端随 HTTP 契约重新生成。 - 依赖调整后 lockfile 同步;交互用例与浏览器旅程 spec 一并更新。 - 证据:make 前端检查(lint + typecheck + Pi 工具桥 tsc)exit 0;make 前端测试 37 文件 / 120 用例通过。
228 lines
9.4 KiB
TypeScript
228 lines
9.4 KiB
TypeScript
/** 规划标题与摘要从载荷提取;作者动作经真实请求界面发出。 */
|
||
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";
|
||
import { 规划条目摘要 } from "../../src/功能/作品规划/数据";
|
||
import { Ui诊断报告 } from "../../src/功能/审校修订/问题清单";
|
||
|
||
afterEach(() => {
|
||
cleanup();
|
||
vi.unstubAllGlobals();
|
||
sessionStorage.clear();
|
||
});
|
||
|
||
// @muse-case {"case_id":"TC-00f820aca9ee","environment":"离线组件测试;正式写入另有真后端旅程","when":"渲染明确数据并执行作者动作。","contract":"docs/系统架构/新版设计/工作台设计.md"}
|
||
it("TC-00f820aca9ee:规划标题和摘要从载荷提取", () => {
|
||
const 载荷 = { targetChapter: 2, chapterGoal: "林深被隔离审讯", chapterEndHook: "茧准备出击" };
|
||
const { 标题, 摘要 } = 规划条目摘要(载荷);
|
||
expect(标题).toBe("第2章目标");
|
||
expect(摘要).toBe("林深被隔离审讯");
|
||
// 场景规划用目标推进收束,大纲用主线一句话,不写死题材字段。
|
||
const 场景 = 规划条目摘要({ 目标推进收束: "访客想借一盏灯" });
|
||
expect(场景.标题).toBe("场景目标");
|
||
expect(场景.摘要).toBe("访客想借一盏灯");
|
||
const 大纲 = 规划条目摘要({ 名称: "雪中的信", 主线一句话: "邮差寻找无人认领的信。" });
|
||
expect(大纲.标题).toBe("大纲方向");
|
||
expect(大纲.摘要).toBe("邮差寻找无人认领的信。");
|
||
expect(规划条目摘要(undefined).标题).toBe("规划候选");
|
||
});
|
||
|
||
// @muse-case {"case_id":"TC-00f820aca9ee-动作","environment":"离线组件测试;正式写入另有真后端旅程","when":"渲染明确数据并执行作者动作。","contract":"docs/系统架构/新版设计/工作台设计.md"}
|
||
it("TC-00f820aca9ee-动作:候选列表展示提取结果且决定动作发真实请求", async () => {
|
||
const 请求: { 方法: string; 地址: string; 体: unknown }[] = [];
|
||
const 候选载荷 = { targetChapter: 2, chapterGoal: "林深被隔离审讯" };
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (request: Request) => {
|
||
const 地址 = new URL(request.url).pathname + new URL(request.url).search;
|
||
请求.push({ 方法: request.method, 地址, 体: await request.clone().json().catch(() => null) });
|
||
const 应答 = (体: unknown) =>
|
||
new Response(JSON.stringify(体), { headers: { "Content-Type": "application/json" } });
|
||
if (地址.startsWith("/api/v1/session") || 地址.startsWith("/api/v1/works/list"))
|
||
return 应答([]);
|
||
if (地址.includes("/plan-nodes"))
|
||
return 应答({ work_id: "work", revision: 1, nodes: [] });
|
||
if (地址.includes("/plans/form"))
|
||
return 应答({
|
||
work_id: "work",
|
||
chapter_id: null,
|
||
node_id: null,
|
||
plan_id: "plan",
|
||
revision: 0,
|
||
schema_binding: {},
|
||
content: {},
|
||
visible_schema: { type: "object", properties: {}, required: [] },
|
||
editable_schema: { type: "object", properties: {}, required: [] },
|
||
});
|
||
if (地址.includes("/plans/candidates"))
|
||
return 应答([
|
||
{
|
||
candidate_id: "c1",
|
||
plan_id: "plan",
|
||
revision: 1,
|
||
candidate_hash: "hash",
|
||
work_id: "work",
|
||
chapter_id: null,
|
||
node_id: null,
|
||
expected_revision: 0,
|
||
schema_binding: {},
|
||
content: 候选载荷,
|
||
decision: "undecided",
|
||
visible_schema: { type: "object", properties: {}, required: [] },
|
||
editable_schema: { type: "object", properties: {}, required: [] },
|
||
},
|
||
]);
|
||
if (地址.includes("/explorations/selection")) return new Response("404", { status: 404 });
|
||
if (地址.includes("/review"))
|
||
return 应答({
|
||
review_id: "review-1",
|
||
review_hash: "review-hash",
|
||
snapshot: { 数据版本: 0 },
|
||
before: null,
|
||
candidate: {
|
||
candidate_id: "c1",
|
||
plan_id: "plan",
|
||
revision: 1,
|
||
candidate_hash: "hash",
|
||
work_id: "work",
|
||
chapter_id: null,
|
||
node_id: null,
|
||
expected_revision: 0,
|
||
schema_binding: {},
|
||
content: 候选载荷,
|
||
decision: "undecided",
|
||
visible_schema: { type: "object", properties: {}, required: [] },
|
||
editable_schema: { type: "object", properties: {}, required: [] },
|
||
},
|
||
changes: [],
|
||
});
|
||
if (地址.includes("/decisions"))
|
||
return 应答({ receipt_id: "receipt-1", action: "adopt", results: [] });
|
||
return 应答({});
|
||
}),
|
||
);
|
||
const { Ui规划工作区 } = await import("../../src/功能/作品规划/规划工作区");
|
||
const 客户端 = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||
render(
|
||
<QueryClientProvider client={客户端}>
|
||
<MemoryRouter initialEntries={["/works/work/plans?schema=fine_outline"]}>
|
||
<Ui规划工作区 />
|
||
</MemoryRouter>
|
||
</QueryClientProvider>,
|
||
);
|
||
expect(await screen.findByText("第2章目标")).toBeTruthy();
|
||
expect(screen.getByText("林深被隔离审讯")).toBeTruthy();
|
||
fireEvent.click(screen.getByRole("button", { name: /候选 1 ·/ }));
|
||
const 确认 = await screen.findByRole("button", { name: "确认这份规划" });
|
||
fireEvent.click(确认);
|
||
await waitFor(() => {
|
||
const 决定 = 请求.find((r) => r.地址.includes("/decisions"));
|
||
expect(决定).toBeTruthy();
|
||
expect(决定!.体).toMatchObject({
|
||
action: "adopt",
|
||
approved_changes: ["plan"],
|
||
author_review_id: "review-1",
|
||
review_hash: "review-hash",
|
||
});
|
||
});
|
||
});
|
||
|
||
|
||
// @muse-case {"case_id":"TC-f9bc3a8d21cf","environment":"离线组件测试;正式写入另有真后端旅程","when":"渲染明确数据并执行作者动作。","contract":"docs/系统架构/新版设计/工作台设计.md"}
|
||
it("TC-f9bc3a8d21cf:诊断结果展示正式正文汇总与来源引文", () => {
|
||
render(
|
||
<Ui诊断报告
|
||
value={{
|
||
diagnosis_id: "d",
|
||
report: {
|
||
mode: "candidate_preview",
|
||
findings: [
|
||
{ start: 3, end: 6, quote: "值得注意的是", decision_proposal: "ask", rule_id: "l002" },
|
||
],
|
||
executed_rules: ["l002", "m001"],
|
||
unreviewed_rules: [{ rule_id: "sem001", reason: "未审" }],
|
||
text_hash: "f".repeat(64),
|
||
calibration: "uncalibrated",
|
||
target: {
|
||
work_id: "w",
|
||
chapter_id: "c",
|
||
revision: 2,
|
||
document_hash: "f".repeat(64),
|
||
branch_id: "main",
|
||
},
|
||
},
|
||
}}
|
||
/>
|
||
);
|
||
expect(screen.getByText(/机器味诊断/)).toBeTruthy();
|
||
expect(screen.getByText(/候选规则预览 · 未标定/)).toBeTruthy();
|
||
expect(screen.getByText(/正文版本 2/)).toBeTruthy();
|
||
expect(screen.getByText(/确定性规则已执行 2 条/)).toBeTruthy();
|
||
expect(screen.getByText("值得注意的是")).toBeTruthy();
|
||
});
|
||
|
||
// @muse-case {"case_id":"TC-ea9587d92a04","environment":"离线组件测试;正式写入另有真后端旅程","when":"渲染明确数据并执行作者动作。","contract":"docs/系统架构/新版设计/工作台设计.md"}
|
||
it("TC-ea9587d92a04:正文依据变化时渲染失败关闭而非崩溃", () => {
|
||
render(
|
||
<Ui诊断报告
|
||
value={{
|
||
diagnosis_id: "d",
|
||
stale: true,
|
||
stale_reasons: ["正文版本已推进", "规则库已更新"],
|
||
report: {
|
||
mode: "candidate_preview",
|
||
findings: [],
|
||
executed_rules: [],
|
||
unreviewed_rules: [],
|
||
text_hash: "a",
|
||
calibration: "uncalibrated",
|
||
target: {
|
||
work_id: "w",
|
||
chapter_id: "c",
|
||
revision: 1,
|
||
document_hash: "a",
|
||
branch_id: "main",
|
||
},
|
||
},
|
||
}}
|
||
/>
|
||
);
|
||
const 告警 = screen.getByRole("alert");
|
||
expect(告警.textContent).toContain("依据已变化");
|
||
expect(告警.textContent).toContain("正文版本已推进");
|
||
expect(screen.queryByText(/NameError/)).toBeNull();
|
||
});
|
||
|
||
// @muse-case {"case_id":"TC-8098487cc24d","environment":"离线组件测试;正式写入另有真后端旅程","when":"渲染明确数据并执行作者动作。","contract":"docs/系统架构/新版设计/工作台设计.md"}
|
||
it("TC-8098487cc24d:作品定位与命中详情随诊断结果展示", () => {
|
||
render(
|
||
<Ui诊断报告
|
||
value={{
|
||
diagnosis_id: "d",
|
||
report: {
|
||
mode: "candidate_preview",
|
||
findings: [
|
||
{ start: 12, end: 18, quote: "嘴角微微上扬", decision_proposal: "retain", rule_id: "l003" },
|
||
],
|
||
executed_rules: ["l003"],
|
||
unreviewed_rules: [],
|
||
text_hash: "b".repeat(64),
|
||
calibration: "uncalibrated",
|
||
target: {
|
||
work_id: "机破星河",
|
||
chapter_id: "当年离歌",
|
||
revision: 5,
|
||
document_hash: "b".repeat(64),
|
||
branch_id: "main",
|
||
},
|
||
},
|
||
}}
|
||
/>
|
||
);
|
||
expect(screen.getByText(/正文版本 5/)).toBeTruthy();
|
||
expect(screen.getByText(/位置 12—18/)).toBeTruthy();
|
||
expect(screen.getByText("嘴角微微上扬")).toBeTruthy();
|
||
expect(screen.getByText("保留")).toBeTruthy();
|
||
});
|