- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具) - 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺 - 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口 - 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影) - 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威) - R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
472 lines
15 KiB
TypeScript
472 lines
15 KiB
TypeScript
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||
import {
|
||
cleanup,
|
||
fireEvent,
|
||
render,
|
||
screen,
|
||
waitFor,
|
||
within,
|
||
} from "@testing-library/react";
|
||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||
import { Ui改进提案页 } from "../../src/功能/作者经验/改进提案页";
|
||
import type {
|
||
规则改进行,
|
||
规则改进命令,
|
||
规则启停行动,
|
||
} from "../../src/功能/作者经验/数据";
|
||
|
||
const api = vi.hoisted(() => ({ GET: vi.fn(), POST: vi.fn() }));
|
||
vi.mock("../../src/接口/生成/客户端", () => ({ 客户端: api }));
|
||
vi.mock("../../src/功能/知识方法/数据", () => ({
|
||
useMuse方法列表: () => ({
|
||
data: [
|
||
{
|
||
title: "动作承接",
|
||
version_id: "method-v1",
|
||
current_version: 1,
|
||
state: "confirmed",
|
||
},
|
||
],
|
||
}),
|
||
}));
|
||
|
||
const target = {
|
||
kind: "diagnostic_rule" as const,
|
||
target_ref: "B06.rule:rule-1",
|
||
version: "2",
|
||
version_id: "rule-version-2",
|
||
content_hash: "server-content-hash",
|
||
rule_hash: "server-rule-hash",
|
||
examples_hash: "server-examples-hash",
|
||
};
|
||
const selection = {
|
||
improvement_id: "improvement",
|
||
revision: 1,
|
||
content_hash: "improvement-hash",
|
||
};
|
||
function ruleRow(): 规则改进行 {
|
||
return {
|
||
...selection,
|
||
payload: {
|
||
improvement_id: "improvement",
|
||
expected_revision: 0,
|
||
title: "减少重复结尾误报",
|
||
rationale: "根据作者反馈验证对白例外",
|
||
rule_id: "rule-1",
|
||
rule_version: 2,
|
||
rule_hash: target.rule_hash,
|
||
feedback_ids: ["feedback-1"],
|
||
proposal_kind: "rule",
|
||
target,
|
||
},
|
||
target_state: "candidate",
|
||
is_current_target: true,
|
||
rule_state: {
|
||
rule_id: "rule-1",
|
||
current: null,
|
||
consumable: false,
|
||
reason: null,
|
||
},
|
||
actions: [
|
||
{
|
||
action_id: "validation-1",
|
||
command_id: "validation-command",
|
||
kind: "validation",
|
||
revision: 1,
|
||
payload: { selection },
|
||
receipt: {
|
||
run_id: "actual-run",
|
||
target,
|
||
result_hash: "actual-result-hash",
|
||
},
|
||
},
|
||
],
|
||
consumption: [],
|
||
};
|
||
}
|
||
let current: 规则改进行;
|
||
function complete(command: 规则改进命令): 规则启停行动 {
|
||
const state = command.request.action === "enable" ? "active" : "disabled";
|
||
const action: 规则启停行动 = {
|
||
action_id: "state-action",
|
||
command_id: command.command_id,
|
||
revision: command.selection.revision,
|
||
kind: command.request.action as "enable" | "disable",
|
||
payload: { selection: command.selection, request: command.request },
|
||
receipt: {
|
||
command_id: "owner-command",
|
||
author_review_id: command.request.review_id,
|
||
results: [
|
||
{
|
||
rule_id: command.request.rule_id,
|
||
version_id: target.version_id,
|
||
activation_revision: command.request.expected_activation_revision + 1,
|
||
state,
|
||
credential_id: command.request.credential_id || null,
|
||
},
|
||
],
|
||
},
|
||
};
|
||
current = {
|
||
...current,
|
||
target_state: state,
|
||
rule_state: {
|
||
rule_id: "rule-1",
|
||
current: {
|
||
state,
|
||
activation_revision: command.request.expected_activation_revision + 1,
|
||
rule_version: command.request.rule_version,
|
||
version_id: target.version_id,
|
||
},
|
||
consumable: state === "active",
|
||
reason: null,
|
||
},
|
||
actions: [
|
||
...current.actions.filter((item) => item.kind === "validation"),
|
||
action,
|
||
],
|
||
};
|
||
return action;
|
||
}
|
||
function mount(path = "/improvements/improvement") {
|
||
render(
|
||
<MemoryRouter initialEntries={[path]}>
|
||
<QueryClientProvider
|
||
client={
|
||
new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||
}
|
||
>
|
||
<Routes>
|
||
<Route
|
||
path="/improvements/:improvement_id"
|
||
element={<Ui改进提案页 />}
|
||
/>
|
||
</Routes>
|
||
</QueryClientProvider>
|
||
</MemoryRouter>,
|
||
);
|
||
}
|
||
function disabled(name: string) {
|
||
return (screen.getByRole("button", { name }) as HTMLButtonElement).disabled;
|
||
}
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
current = ruleRow();
|
||
api.GET.mockImplementation(async (path: string) => {
|
||
if (path === "/api/v1/improvements/{improvement_id}")
|
||
return { data: current };
|
||
if (path === "/api/v1/observations")
|
||
return { data: { groups: [], observations: [] } };
|
||
if (path === "/api/v1/feedback/{feedback_id}")
|
||
return {
|
||
data: {
|
||
feedback_id: "feedback-1",
|
||
payload: {
|
||
work_id: "work",
|
||
quote: "人物口头禅应当保留",
|
||
decision: "revise",
|
||
},
|
||
before_text: "原稿",
|
||
after_text: "改稿",
|
||
},
|
||
};
|
||
if (path === "/api/v1/review-rules")
|
||
return {
|
||
data: {
|
||
fingerprint: "library",
|
||
samples: {},
|
||
rules: {
|
||
"rule-1": {
|
||
id: "rule-1",
|
||
name: "重复结尾",
|
||
version: 2,
|
||
status: "candidate",
|
||
layer: "mechanical",
|
||
fix_hint: "保留人物口头禅",
|
||
samples: {},
|
||
},
|
||
},
|
||
},
|
||
};
|
||
if (path === "/api/v1/rules/{rule_id}/versions/{version}/target")
|
||
return {
|
||
data: {
|
||
target,
|
||
candidate_status: "candidate",
|
||
is_latest: true,
|
||
},
|
||
};
|
||
throw new Error("未预期的读取:" + path);
|
||
});
|
||
api.POST.mockImplementation(
|
||
async (path: string, options: { body: 规则改进命令 }) => {
|
||
if (path === "/api/v1/rules/{rule_id}/activation/review")
|
||
return {
|
||
data: {
|
||
review_id: "actual-review",
|
||
review_hash: "actual-review-hash",
|
||
},
|
||
};
|
||
if (path === "/api/v1/improvements/{improvement_id}/rule-state")
|
||
return { data: complete(options.body) };
|
||
if (
|
||
path === "/api/v1/rule-improvements" ||
|
||
path === "/api/v1/improvements"
|
||
)
|
||
return { data: current };
|
||
throw new Error("未预期的写入:" + path);
|
||
},
|
||
);
|
||
});
|
||
afterEach(cleanup);
|
||
|
||
it("NC-规则改善-反馈提案与方法分流:使用真实反馈和服务端规则哈希,方法仍走原入口", async () => {
|
||
mount("/improvements/new?feedback=feedback-1");
|
||
await screen.findByText("人物口头禅应当保留");
|
||
fireEvent.change(screen.getByLabelText("改进目标"), {
|
||
target: { value: "rule" },
|
||
});
|
||
await screen.findByRole("option", { name: "重复结尾 · 第2版" });
|
||
fireEvent.change(screen.getByLabelText("具体规则版本"), {
|
||
target: { value: "rule-1" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("改进标题"), {
|
||
target: { value: "减少误报" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("需要验证的改变"), {
|
||
target: { value: "检验口头禅例外" },
|
||
});
|
||
await waitFor(() => expect(disabled("保存规则改进提案")).toBe(false));
|
||
expect(api.POST).not.toHaveBeenCalled();
|
||
fireEvent.click(screen.getByRole("button", { name: "保存规则改进提案" }));
|
||
await screen.findByRole("heading", { name: "减少重复结尾误报" });
|
||
expect(api.GET).toHaveBeenCalledWith(
|
||
"/api/v1/rules/{rule_id}/versions/{version}/target",
|
||
{
|
||
params: { path: { rule_id: "rule-1", version: 2 } },
|
||
},
|
||
);
|
||
expect(api.POST.mock.calls[0]).toEqual([
|
||
"/api/v1/rule-improvements",
|
||
{
|
||
body: {
|
||
proposal: {
|
||
improvement_id: expect.any(String),
|
||
expected_revision: 0,
|
||
title: "减少误报",
|
||
rationale: "检验口头禅例外",
|
||
rule_id: "rule-1",
|
||
rule_version: 2,
|
||
rule_hash: "server-rule-hash",
|
||
feedback_ids: ["feedback-1"],
|
||
},
|
||
},
|
||
},
|
||
]);
|
||
expect(screen.queryByRole("button", { name: "确认启用已测版本" })).toBeNull();
|
||
|
||
cleanup();
|
||
api.POST.mockClear();
|
||
mount("/improvements/new?feedback=feedback-1");
|
||
await screen.findByText("人物口头禅应当保留");
|
||
fireEvent.change(screen.getByLabelText("改进标题"), {
|
||
target: { value: "改进动作承接" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("需要验证的改变"), {
|
||
target: { value: "减少跳跃" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("具体方法版本"), {
|
||
target: { value: "method-v1" },
|
||
});
|
||
fireEvent.click(screen.getByRole("button", { name: "保存具体提案" }));
|
||
await waitFor(() => expect(api.POST).toHaveBeenCalledTimes(1));
|
||
expect(api.POST.mock.calls[0][0]).toBe("/api/v1/improvements");
|
||
expect(api.POST.mock.calls[0][1].body.proposal).toMatchObject({
|
||
method_version_id: "method-v1",
|
||
feedback_ids: ["feedback-1"],
|
||
});
|
||
});
|
||
|
||
it("NC-规则改善-先审后入与未知响应:作者再次确认才启用,重试复用完整原请求", async () => {
|
||
current.consumption = [
|
||
{
|
||
diagnosis_id: "actual-diagnosis",
|
||
work_id: "work",
|
||
target: { chapter_id: "chapter", branch_id: "draft" },
|
||
report_hash: "report-hash",
|
||
},
|
||
];
|
||
let attempts = 0;
|
||
const handler = api.POST.getMockImplementation()!;
|
||
api.POST.mockImplementation(async (path, options) => {
|
||
if (path.endsWith("/rule-state") && attempts++ === 0)
|
||
throw new Error("响应中断");
|
||
return handler(path, options);
|
||
});
|
||
mount();
|
||
await screen.findByText("actual-run");
|
||
expect(screen.getByText("actual-result-hash")).toBeTruthy();
|
||
expect(
|
||
screen
|
||
.getByRole("link", { name: "查看诊断 actual-diagnosis" })
|
||
.getAttribute("href"),
|
||
).toBe(
|
||
"/works/work/review?chapter=chapter&branch=draft&diagnosis=actual-diagnosis",
|
||
);
|
||
expect(api.POST).not.toHaveBeenCalled();
|
||
fireEvent.change(screen.getByLabelText("规则验证凭据编号"), {
|
||
target: { value: "actual-proof" },
|
||
});
|
||
fireEvent.click(
|
||
screen.getByRole("button", { name: "核对凭据并打开启用审阅" }),
|
||
);
|
||
await screen.findByRole("button", { name: "确认启用此规则版本" });
|
||
expect(api.POST).toHaveBeenCalledTimes(1);
|
||
expect(api.POST.mock.calls[0]).toEqual([
|
||
"/api/v1/rules/{rule_id}/activation/review",
|
||
{
|
||
params: { path: { rule_id: "rule-1" } },
|
||
body: {
|
||
request: {
|
||
rule_id: "rule-1",
|
||
version: 2,
|
||
action: "enable",
|
||
expected_activation_revision: 0,
|
||
credential_id: "actual-proof",
|
||
},
|
||
},
|
||
},
|
||
]);
|
||
fireEvent.click(screen.getByRole("button", { name: "确认启用此规则版本" }));
|
||
await screen.findByText("响应中断");
|
||
expect(screen.queryByRole("button", { name: "返回修改" })).toBeNull();
|
||
expect(disabled("核对凭据并打开启用审阅")).toBe(true);
|
||
const first = api.POST.mock.calls[1];
|
||
api.GET.mockRejectedValueOnce(new Error("状态读取中断"));
|
||
fireEvent.click(screen.getByRole("button", { name: "刷新实际结果" }));
|
||
await screen.findByText("状态读取中断");
|
||
fireEvent.click(screen.getByRole("button", { name: "重试同一启用请求" }));
|
||
await screen.findByRole("heading", { name: "启用回执已保存" });
|
||
expect(api.POST).toHaveBeenCalledTimes(3);
|
||
expect(api.POST.mock.calls[2]).toEqual(first);
|
||
expect(first).toEqual([
|
||
"/api/v1/improvements/{improvement_id}/rule-state",
|
||
{
|
||
params: { path: { improvement_id: "improvement" } },
|
||
body: {
|
||
command_id: expect.any(String),
|
||
selection,
|
||
request: {
|
||
action: "enable",
|
||
rule_id: "rule-1",
|
||
rule_version: 2,
|
||
rule_hash: target.rule_hash,
|
||
expected_activation_revision: 0,
|
||
review_id: "actual-review",
|
||
review_hash: "actual-review-hash",
|
||
credential_id: "actual-proof",
|
||
},
|
||
},
|
||
},
|
||
]);
|
||
expect(
|
||
within(screen.getByRole("region", { name: "实际诊断使用" })).getAllByRole(
|
||
"link",
|
||
),
|
||
).toHaveLength(1);
|
||
});
|
||
|
||
it("NC-规则改善-刷新恢复:保留的待处理请求沿用原定位和审阅,不重新审阅", async () => {
|
||
const command: 规则改进命令 = {
|
||
command_id: "persisted-command",
|
||
selection,
|
||
request: {
|
||
action: "enable",
|
||
rule_id: "rule-1",
|
||
rule_version: 2,
|
||
rule_hash: target.rule_hash,
|
||
expected_activation_revision: 0,
|
||
review_id: "persisted-review",
|
||
review_hash: "persisted-review-hash",
|
||
credential_id: "persisted-proof",
|
||
},
|
||
};
|
||
current.revision = 2;
|
||
current.content_hash = "new-improvement-hash";
|
||
current.actions.push({
|
||
action_id: "pending",
|
||
command_id: command.command_id,
|
||
revision: 1,
|
||
kind: "enable",
|
||
payload: { selection: command.selection, request: command.request },
|
||
receipt: null,
|
||
});
|
||
mount();
|
||
const restore = await screen.findByRole("button", {
|
||
name: "恢复这次启用请求",
|
||
});
|
||
expect(api.POST).not.toHaveBeenCalled();
|
||
expect(disabled("核对凭据并打开启用审阅")).toBe(true);
|
||
fireEvent.click(restore);
|
||
await screen.findByRole("heading", { name: "启用回执已保存" });
|
||
expect(api.POST).toHaveBeenCalledTimes(1);
|
||
expect(api.POST).toHaveBeenCalledWith(
|
||
"/api/v1/improvements/{improvement_id}/rule-state",
|
||
{
|
||
params: { path: { improvement_id: "improvement" } },
|
||
body: command,
|
||
},
|
||
);
|
||
});
|
||
|
||
it("NC-规则改善-旧版停用:可停用本提案仍生效的旧版,不得停其他版本或重新启用旧版", async () => {
|
||
current.is_current_target = false;
|
||
current.target_state = "active";
|
||
current.rule_state.current = {
|
||
state: "active",
|
||
activation_revision: 3,
|
||
rule_version: 2,
|
||
version_id: target.version_id,
|
||
};
|
||
mount();
|
||
await screen.findByRole("heading", { name: "减少重复结尾误报" });
|
||
expect(disabled("核对凭据并打开启用审阅")).toBe(true);
|
||
expect(disabled("核对停用依据")).toBe(false);
|
||
fireEvent.click(screen.getByRole("button", { name: "核对停用依据" }));
|
||
await screen.findByRole("button", { name: "确认停用此规则版本" });
|
||
expect(api.POST).toHaveBeenCalledTimes(1);
|
||
expect(api.POST.mock.calls[0][1].body.request).toEqual({
|
||
rule_id: "rule-1",
|
||
version: 2,
|
||
action: "disable",
|
||
expected_activation_revision: 3,
|
||
credential_id: "",
|
||
});
|
||
fireEvent.click(screen.getByRole("button", { name: "确认停用此规则版本" }));
|
||
await screen.findByText("本提案规则已停用。");
|
||
expect(api.POST.mock.calls[1][1].body.request).toMatchObject({
|
||
action: "disable",
|
||
rule_version: 2,
|
||
expected_activation_revision: 3,
|
||
});
|
||
fireEvent.change(screen.getByLabelText("规则验证凭据编号"), {
|
||
target: { value: "new-proof" },
|
||
});
|
||
expect(disabled("核对凭据并打开启用审阅")).toBe(true);
|
||
|
||
cleanup();
|
||
api.POST.mockClear();
|
||
current = ruleRow();
|
||
current.rule_state.current = {
|
||
state: "active",
|
||
activation_revision: 5,
|
||
rule_version: 3,
|
||
version_id: "another-version",
|
||
};
|
||
mount();
|
||
await screen.findByText("当前生效的是第3版,本提案不能停用其他版本。");
|
||
expect(disabled("核对停用依据")).toBe(true);
|
||
expect(api.POST).not.toHaveBeenCalled();
|
||
});
|