- 上下文清单、任务列表与详情、恢复控制、运行观察、创作对话与消息流、正式变更决策面板、正文写作与候选审阅、资料研究等页面同步本轮后端能力。 - 事件标签表补齐后端事件类型;查询键与生成客户端随 HTTP 契约重新生成。 - 依赖调整后 lockfile 同步;交互用例与浏览器旅程 spec 一并更新。 - 证据:make 前端检查(lint + typecheck + Pi 工具桥 tsc)exit 0;make 前端测试 37 文件 / 120 用例通过。
476 lines
17 KiB
TypeScript
476 lines
17 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);
|
||
|
||
// @muse-case {"case_id":"NC-规则改善-反馈提案与方法分流","environment":"离线","given":"固定请求、来源与作者;按用例环境准备独立测试输入","when":"通过实际公开入口执行并核对拒绝、状态及持久结果","then":["NC-规则改善-反馈提案与方法分流:使用真实反馈和服务端规则哈希,方法仍走原入口"],"contract":"docs/系统架构/新版设计/模块设计/B07-作者经验.md#有来源的运行观察"}
|
||
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"],
|
||
});
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-规则改善-先审后入与未知响应","environment":"离线","given":"固定请求、来源与作者;按用例环境准备独立测试输入","when":"通过实际公开入口执行并核对拒绝、状态及持久结果","then":["NC-规则改善-先审后入与未知响应:作者再次确认才启用,重试复用完整原请求"],"contract":"docs/系统架构/新版设计/模块设计/B07-作者经验.md#有来源的运行观察"}
|
||
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);
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-规则改善-刷新恢复","environment":"离线","given":"固定请求、来源与作者;按用例环境准备独立测试输入","when":"通过实际公开入口执行并核对拒绝、状态及持久结果","then":["NC-规则改善-刷新恢复:保留的待处理请求沿用原定位和审阅,不重新审阅"],"contract":"docs/系统架构/新版设计/模块设计/B07-作者经验.md#有来源的运行观察"}
|
||
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,
|
||
},
|
||
);
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-规则改善-旧版停用","environment":"离线","given":"固定请求、来源与作者;按用例环境准备独立测试输入","when":"通过实际公开入口执行并核对拒绝、状态及持久结果","then":["NC-规则改善-旧版停用:可停用本提案仍生效的旧版,不得停其他版本或重新启用旧版"],"contract":"docs/系统架构/新版设计/模块设计/B07-作者经验.md#有来源的运行观察"}
|
||
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();
|
||
});
|