import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { App } from "../src/client/App.js"; import type { ComponentStageResponse, DocumentComparisonResponse, RunSummaryResponse, } from "../src/shared/api.js"; vi.mock("../src/client/DiffView.js", () => ({ DiffView: ({ beforeLabel, afterLabel }: { beforeLabel: string; afterLabel: string }) => (
{beforeLabel} / {afterLabel}
), })); const components = [ { component_position: 0, component_id: "paper.rule", version: "1.0.0", parameters: [], applicability: "替换测试单词。", change_count: 1, }, { component_position: 1, component_id: "paper.zero", version: "1.0.0", parameters: [], applicability: "不修改当前测试文档。", change_count: 0, }, ]; const documentSummary = { document_id: "paper", source_label: "inputs/paper.md", status: "success" as const, input_sha256: "1".repeat(64), current_sha256: "2".repeat(64), change_count: 1, source_available: true, output_available: true, availability_error: null, }; const runResponse: RunSummaryResponse = { schema_version: 1, run: { run_id: "review-run", run_date: "2026-08-23", status: "success", started_at_utc: "2026-08-23T01:00:00Z", completed_at_utc: "2026-08-23T01:01:00Z", retention_until: "2026-09-22T01:01:00Z", }, components, documents: [documentSummary], summary: { document_count: 1, success_count: 1, failed_count: 0, unstable_count: 0, change_count: 1, }, original_run_location_changed: false, }; const change = { component_id: "paper.rule", component_version: "1.0.0", component_position: 0, proposal_ref: { component_position: 0, snapshot_sha256: "1".repeat(64), proposal_index: 0, }, edit_index: 0, reason: "替换测试单词", location: { line: 1, column: 3 }, editor_range: { start: 0, end: 3 }, before: "old", after: "new", }; const documentResponse: DocumentComparisonResponse = { schema_version: 1, document: documentSummary, components, original_markdown: "old", cleaned_markdown: "new", changes: [change], errors: [], residual_proposals: [], }; const stageResponse: ComponentStageResponse = { schema_version: 1, document_id: "paper", component: components[0]!, before_sha256: "1".repeat(64), after_sha256: "2".repeat(64), before_markdown: "old", after_markdown: "new", changes: [change], }; function response(payload: unknown, status = 200): Response { return new Response(JSON.stringify(payload), { status, headers: { "Content-Type": "application/json" }, }); } describe("App", () => { beforeEach(() => { vi.restoreAllMocks(); }); it("shows the run, document comparison, component order and component stage", async () => { const fetchMock = vi.fn((input: string | URL | Request) => { const pathname = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (pathname === "/api/v1/run") { return Promise.resolve(response(runResponse)); } if (pathname.endsWith("/components/0")) { return Promise.resolve(response(stageResponse)); } return Promise.resolve(response(documentResponse)); }); vi.stubGlobal("fetch", fetchMock); render(); expect(await screen.findByRole("heading", { name: "review-run" })).toBeInTheDocument(); expect(await screen.findByTestId("diff-view")).toHaveTextContent("清洗前 / 清洗后"); expect(screen.getByText("paper.rule")).toBeInTheDocument(); expect(screen.getByText("paper.zero")).toBeInTheDocument(); expect(screen.getByText("替换测试单词")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: /paper\.rule/ })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining("components/0"), expect.anything()); }); expect(await screen.findByTestId("diff-view")).toHaveTextContent("组件 1 执行前 / 组件 1 执行后"); }); it("shows API failures without rendering document text", async () => { vi.stubGlobal( "fetch", vi.fn(() => Promise.resolve( response({ error: { code: "unsupported_schema", message: "不支持这个产物版本。" } }, 409), ), ), ); render(); expect(await screen.findByRole("alert")).toHaveTextContent("不支持这个产物版本"); expect(screen.queryByTestId("diff-view")).not.toBeInTheDocument(); }); it("shows failed audit evidence without inventing a cleaned result", async () => { const failedSummary = { ...documentSummary, status: "failed" as const, current_sha256: documentSummary.input_sha256, change_count: 0, output_available: false, }; const failedRun: RunSummaryResponse = { ...runResponse, run: { ...runResponse.run, status: "failed" }, documents: [failedSummary], summary: { document_count: 1, success_count: 0, failed_count: 1, unstable_count: 0, change_count: 0, }, }; const failedDocument: DocumentComparisonResponse = { schema_version: 1, document: failedSummary, components: components.map((component) => ({ ...component, change_count: 0 })), original_markdown: "原文", cleaned_markdown: null, changes: [], errors: [ { component_id: "paper.rule", component_version: "1.0.0", component_position: 0, stage: "transform", error_type: "SyntheticError", message: "测试组件失败。", }, ], residual_proposals: [], }; vi.stubGlobal( "fetch", vi.fn((input: string | URL | Request) => { const pathname = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; return Promise.resolve(response(pathname === "/api/v1/run" ? failedRun : failedDocument)); }), ); render(); expect(await screen.findByRole("heading", { name: "失败文档只展示审计证据" })).toBeInTheDocument(); expect(screen.getByText("测试组件失败。")).toBeInTheDocument(); expect(screen.queryByTestId("diff-view")).not.toBeInTheDocument(); }); it("keeps audit details visible when a successful run has lost its original source", async () => { const unavailableSummary = { ...documentSummary, source_available: false, availability_error: "原文路径已经失效。", }; const unavailableRun: RunSummaryResponse = { ...runResponse, documents: [unavailableSummary], }; const unavailableDocument: DocumentComparisonResponse = { ...documentResponse, document: unavailableSummary, original_markdown: null, changes: [{ ...change, editor_range: null }], }; vi.stubGlobal( "fetch", vi.fn((input: string | URL | Request) => { const pathname = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; return Promise.resolve( response(pathname === "/api/v1/run" ? unavailableRun : unavailableDocument), ); }), ); render(); expect(await screen.findByRole("alert")).toHaveTextContent("原文路径已经失效"); expect(screen.getByText("替换测试单词")).toBeInTheDocument(); expect(screen.queryByTestId("diff-view")).not.toBeInTheDocument(); }); });