- 上下文清单、任务列表与详情、恢复控制、运行观察、创作对话与消息流、正式变更决策面板、正文写作与候选审阅、资料研究等页面同步本轮后端能力。 - 事件标签表补齐后端事件类型;查询键与生成客户端随 HTTP 契约重新生成。 - 依赖调整后 lockfile 同步;交互用例与浏览器旅程 spec 一并更新。 - 证据:make 前端检查(lint + typecheck + Pi 工具桥 tsc)exit 0;make 前端测试 37 文件 / 120 用例通过。
293 lines
11 KiB
TypeScript
293 lines
11 KiB
TypeScript
/** 组件传输替身只验证显示与切换隔离;PG和HTTP证据由后端用例提供。 */
|
||
import { afterEach, expect, it, vi } from "vitest";
|
||
import {
|
||
cleanup,
|
||
fireEvent,
|
||
render,
|
||
screen,
|
||
waitFor,
|
||
within,
|
||
} from "@testing-library/react";
|
||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||
import { Ui依赖装配 } from "../../src/应用/依赖装配";
|
||
import { Ui实验报告, Ui实验页 } from "../../src/功能/效果评测/实验页";
|
||
import type { 实验报告 } from "../../src/功能/效果评测/数据";
|
||
|
||
afterEach(() => {
|
||
cleanup();
|
||
vi.unstubAllGlobals();
|
||
});
|
||
|
||
function 报告(id = "sample-only"): 实验报告 {
|
||
return {
|
||
schema_version: "evaluation-report-v1",
|
||
experiment_id: id,
|
||
conditions_hash: "c".repeat(64),
|
||
dataset_hash: "d".repeat(64),
|
||
plan_hash: "p".repeat(64),
|
||
target: {
|
||
target_ref: "B05.generate",
|
||
version: "test-release",
|
||
content_hash: "a".repeat(64),
|
||
},
|
||
primary_pair: ["control", "treatment"],
|
||
adapter: "direct_writer_prompt_v1",
|
||
state: "reconciling",
|
||
activation_status: "not_evaluated",
|
||
evaluation_goal: "diagnostic",
|
||
validation_modes: ["offline_contract"],
|
||
coverage: { samples: 1, generated: 0, compared: 0 },
|
||
outcomes: { incomplete: 1 },
|
||
dimensions: { 清晰度: { incomplete: 1 } },
|
||
source_groups: { 独立来源: { samples: 1, outcomes: { incomplete: 1 } } },
|
||
runtime: { units: 3, states: { failed: 1, completed: 1, not_started: 1 } },
|
||
cost: {
|
||
known_total_usd: "0.125",
|
||
total_usd: null,
|
||
has_unknown: true,
|
||
has_pending: false,
|
||
over_budget: false,
|
||
sent_calls: 2,
|
||
released_reservations: 0,
|
||
},
|
||
literary_quality: { calibration: "unverified", metrics: null },
|
||
report_hash: "b".repeat(64),
|
||
samples: [
|
||
{
|
||
sample_id: id,
|
||
source_groups: ["独立来源"],
|
||
generation_complete: false,
|
||
comparison_complete: false,
|
||
detection_complete: null,
|
||
detections: {},
|
||
outcome: "incomplete",
|
||
comparisons: { "control:treatment": "incomplete" },
|
||
dimensions: { 清晰度: "incomplete" },
|
||
decisions: [],
|
||
units: [],
|
||
incomplete_units: ["missing-unit"],
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
// @muse-case {"case_id":"NC-ui-evaluation-report-null","environment":"前端组件,传输替身","given":"实验报告页面","when":"读取或切换实验并核对显示","then":["未知成本、固定覆盖分母和未标定状态准确呈现"],"contract":"docs/系统架构/新版设计/模块设计/B10-效果评测.md"}
|
||
it("NC-ui-evaluation-report-null:未知成本和未标定不显示为零或通过", () => {
|
||
render(<Ui实验报告 内容={报告()} />);
|
||
expect(screen.getByText("实际总费用").nextElementSibling?.textContent).toBe(
|
||
"待核对",
|
||
);
|
||
expect(screen.getByText("文学指标").nextElementSibling?.textContent).toBe(
|
||
"未标定",
|
||
);
|
||
expect(screen.getByText("正式启用").nextElementSibling?.textContent).toBe(
|
||
"尚未评定",
|
||
);
|
||
expect(screen.getByText("比较齐全").nextElementSibling?.textContent).toBe(
|
||
"0 / 1",
|
||
);
|
||
expect(
|
||
screen.getByRole("heading", { name: /sample-only.*比较未完成/ }),
|
||
).toBeTruthy();
|
||
expect(screen.getByText("比较未完成 1 例")).toBeTruthy();
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-evaluation-primary-pair","environment":"前端组件,传输替身","given":"ABC逐例报告","when":"呈现主比较与两组诊断结果","then":["明确显示A/C主比较,不以B在其他对照中的胜出覆盖主结果"],"contract":"docs/系统架构/新版设计/模块设计/B10-效果评测.md"}
|
||
it("NC-ui-evaluation-primary-pair:ABC主比较和诊断比较分开显示", () => {
|
||
const r = 报告();
|
||
r.primary_pair = ["A", "C"];
|
||
r.samples[0].comparisons = { "A:B": "B", "A:C": "C", "B:C": "B" };
|
||
r.samples[0].outcome = "C";
|
||
render(<Ui实验报告 内容={r} />);
|
||
expect(screen.getByText("主比较:A / C")).toBeTruthy();
|
||
expect(screen.getByText("A / B:B(卡片索引)胜出")).toBeTruthy();
|
||
expect(screen.getByText("A / C:C(索引与正文)胜出")).toBeTruthy();
|
||
expect(
|
||
screen.getByRole("heading", { name: /sample-only.*C(索引与正文)胜出/ }),
|
||
).toBeTruthy();
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-evaluation-report-switch","environment":"前端组件,传输替身","given":"实验报告页面","when":"读取或切换实验并核对显示","then":["查询隔离后迟到响应不覆盖当前实验"],"contract":"docs/系统架构/新版设计/模块设计/B10-效果评测.md"}
|
||
it("NC-ui-evaluation-report-switch:切换实验后迟到报告不能覆盖当前实验", async () => {
|
||
let finishA!: (response: Response) => void;
|
||
const delayed = new Promise<Response>((resolve) => {
|
||
finishA = resolve;
|
||
});
|
||
const respond = (value: unknown) =>
|
||
new Response(JSON.stringify(value), {
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
const transport = vi.fn(async (request: Request) => {
|
||
const path = new URL(request.url).pathname;
|
||
if (path === "/api/v1/session")
|
||
return respond({ author_id: "fixture-author" });
|
||
if (path.endsWith("/experiment-a/report")) return delayed;
|
||
if (path.endsWith("/experiment-b/report")) return respond(报告("sample-B"));
|
||
throw new Error(`未预期请求:${path}`);
|
||
});
|
||
vi.stubGlobal("fetch", transport);
|
||
render(
|
||
<Ui依赖装配>
|
||
<MemoryRouter initialEntries={["/evaluations/experiment-a"]}>
|
||
<Routes>
|
||
<Route path="/evaluations/:experiment_id?" element={<Ui实验页 />} />
|
||
</Routes>
|
||
</MemoryRouter>
|
||
</Ui依赖装配>,
|
||
);
|
||
await waitFor(() =>
|
||
expect(
|
||
transport.mock.calls.some(([r]) =>
|
||
r.url.endsWith("/experiment-a/report"),
|
||
),
|
||
).toBe(true),
|
||
);
|
||
fireEvent.change(screen.getByLabelText("实验编号"), {
|
||
target: { value: "experiment-b" },
|
||
});
|
||
fireEvent.click(screen.getByRole("button", { name: "查看报告" }));
|
||
await screen.findByRole("heading", { name: /sample-B/ });
|
||
finishA(respond(报告("sample-A")));
|
||
await waitFor(() =>
|
||
expect(screen.queryByRole("heading", { name: /sample-A/ })).toBeNull(),
|
||
);
|
||
expect(screen.getByRole("heading", { name: /sample-B/ })).toBeTruthy();
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-evaluation-quotes","environment":"前端组件传输替身","given":"两侧已经反向的定位报告","when":"展开判断理由和引文","then":["使用固定两侧映射显示实际C/A,保留换行;原文中的HTML按文本显示"],"contract":"docs/系统架构/新版设计/模块设计/B10-效果评测.md"}
|
||
it("NC-ui-evaluation-quotes:换序后的真实实验臂与原字引文正确对应", () => {
|
||
const r = 报告();
|
||
r.samples[0].decisions = [
|
||
{
|
||
unit_id: "judge-unit",
|
||
task_id: "judge-task",
|
||
call_id: "judge-call",
|
||
output_hash: "raw-hash",
|
||
model: "independent-judge",
|
||
pair: ["A", "C"],
|
||
choice: "C",
|
||
dimensions: { 清晰度: "C" },
|
||
sides: { left: "C", right: "A" },
|
||
judgment_hash: "located-hash",
|
||
judgment: {
|
||
choice: "left",
|
||
rationale: "动作的因果顺序更清楚。",
|
||
dimensions: [
|
||
{
|
||
dimension: "清晰度",
|
||
choice: "left",
|
||
rationale: "保留两侧原句,便于核查。",
|
||
evidence: [
|
||
{
|
||
side: "left",
|
||
quote: "她推门。\n门外有人。",
|
||
start: 3,
|
||
end: 13,
|
||
},
|
||
{
|
||
side: "right",
|
||
quote: "<script>只是原文</script>",
|
||
start: 0,
|
||
end: 24,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
},
|
||
];
|
||
render(<Ui实验报告 内容={r} />);
|
||
fireEvent.click(screen.getByText(/A \/ C · independent-judge/));
|
||
expect(screen.getByText("C 原文").nextElementSibling?.textContent).toBe(
|
||
"她推门。\n门外有人。",
|
||
);
|
||
expect(screen.getByText("A 原文").nextElementSibling?.textContent).toBe(
|
||
"<script>只是原文</script>",
|
||
);
|
||
expect(document.querySelector("script")).toBeNull();
|
||
expect(screen.getByText("动作的因果顺序更清楚。")).toBeTruthy();
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-evaluation-corrections","environment":"前端组件传输替身","given":"已发生拒绝且纠正上限为零的报告","when":"展开执行和费用记录","then":["零上限、拒绝与已发生费用仍显示,不显示为已启用"],"contract":"docs/系统架构/新版设计/模块设计/B10-效果评测.md"}
|
||
it("NC-ui-evaluation-corrections:零纠正上限和已发生拒绝均可见", () => {
|
||
const r = 报告();
|
||
r.samples[0].units = [
|
||
{
|
||
unit_id: "failed-judge",
|
||
task_id: "task",
|
||
state: "failed",
|
||
output_hash: null,
|
||
correction_limit: 0,
|
||
rejections: [
|
||
{
|
||
sequence: 1,
|
||
attempt_id: "attempt",
|
||
call_id: "rejected-call",
|
||
failure_code: "PAIRWISE_QUOTE_NOT_FOUND",
|
||
record_hash: "r".repeat(64),
|
||
side_count: 2,
|
||
dimension_count: 1,
|
||
},
|
||
],
|
||
calls: [
|
||
{
|
||
call_id: "rejected-call",
|
||
attempt_id: "attempt",
|
||
role_id: "judge",
|
||
state: "settled",
|
||
actual_amount: "0.125",
|
||
reserved_amount: "1",
|
||
over_budget: false,
|
||
},
|
||
],
|
||
},
|
||
];
|
||
render(<Ui实验报告 内容={r} />);
|
||
fireEvent.click(screen.getByText("执行与费用记录", { exact: true }));
|
||
expect(screen.getByText("引文纠正上限:0 次")).toBeTruthy();
|
||
expect(screen.getByText("已保留 1 次判断拒绝。")).toBeTruthy();
|
||
expect(screen.getByLabelText("引文校验记录").textContent).toContain(
|
||
"第 1 次:引文未在原文中出现",
|
||
);
|
||
expect(
|
||
within(screen.getByRole("region", { name: "逐例结果" })).getByText(
|
||
/0.125 USD/,
|
||
),
|
||
).toBeTruthy();
|
||
expect(screen.getByText("正式启用").nextElementSibling?.textContent).toBe(
|
||
"尚未评定",
|
||
);
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-evaluation-contract-failure","environment":"前端组件传输替身","given":"已持久化的非引文判断合同拒绝","when":"展开失败原因","then":["说明合同不完整且未自动纠正,不误称引文缺失"],"contract":"docs/系统架构/新版设计/模块设计/B10-效果评测.md"}
|
||
it("NC-ui-evaluation-contract-failure:非引文拒绝不误报为缺少引文", () => {
|
||
const r = 报告();
|
||
r.samples[0].units = [
|
||
{
|
||
unit_id: "contract-failed",
|
||
task_id: "task",
|
||
state: "failed",
|
||
output_hash: null,
|
||
correction_limit: 1,
|
||
calls: [],
|
||
rejections: [
|
||
{
|
||
sequence: 1,
|
||
attempt_id: "attempt",
|
||
call_id: "invalid-contract",
|
||
record_hash: "a".repeat(64),
|
||
failure_code: "PAIRWISE_NOT_EXECUTED",
|
||
side_count: 2,
|
||
dimension_count: 1,
|
||
},
|
||
],
|
||
},
|
||
];
|
||
render(<Ui实验报告 内容={r} />);
|
||
fireEvent.click(screen.getByText("执行与费用记录", { exact: true }));
|
||
expect(screen.getByLabelText("引文校验记录").textContent).toContain(
|
||
"判断合同不完整,未自动纠正",
|
||
);
|
||
expect(screen.queryByText(/引文未在原文中出现/)).toBeNull();
|
||
});
|