import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { classifyReportEnvelope } from "../src/components/personal-report/personal-report-page.tsx"; import { cssDeclarations } from "./css-contract-test-support.ts"; import { safeParseReportDocument } from "../src/lib/personal-report-contract.ts"; import type { LegacyReportDocumentV1, ReportDocumentV2 } from "../src/lib/personal-report-contract.ts"; const fixturePath = new URL("../../tests/fixtures/personal_report_document.v1.json", import.meta.url); const v2FixturePath = new URL("../../tests/fixtures/personal_report_document.v2.json", import.meta.url); const canonicalFixture = JSON.parse(readFileSync(fixturePath, "utf8")) as LegacyReportDocumentV1; const canonicalV2Fixture = JSON.parse(readFileSync(v2FixturePath, "utf8")) as ReportDocumentV2; // 原值: 用 PersonalReportDocumentView 渲染旧阅读页的用例留在本文件 // 新值: 那些用例随旧阅读页删除;解析、路由信封、阅读外壳、目录和打印留在下面 // 原因: 阅读页从 09 月起走 Markdown 视图,旧视图没有页面引用 test("stored v1 and current v2 fixtures pass the canonical contract parse", () => { const storedV1 = safeParseReportDocument(canonicalFixture); assert.equal(storedV1.ok, true); if (storedV1.ok) { assert.equal(storedV1.document.schemaVersion, "report_document.v1"); assert.equal(storedV1.document.reportId, canonicalFixture.reportId); } const currentV2 = safeParseReportDocument(canonicalV2Fixture); assert.equal(currentV2.ok, true); if (currentV2.ok) { assert.equal(currentV2.document.schemaVersion, "report_document.v2"); assert.equal(currentV2.document.reportId, canonicalV2Fixture.reportId); } }); test("GET envelope classification: 401/404/ready/generating/failed/invalid (real API shape)", () => { assert.deepEqual(classifyReportEnvelope(401, {}), { phase: "unauthorized" }); assert.deepEqual(classifyReportEnvelope(404, {}), { phase: "not-found" }); assert.deepEqual(classifyReportEnvelope(403, {}), { phase: "invalid", message: "无权访问该报告。" }); const view = (status: string, failureCode: string | null = null) => ({ id: "11111111-1111-1111-1111-111111111111", requestId: "22222222-2222-2222-2222-222222222222", reportType: "personal_full", presentationMode: "default", status, failureCode, createdAt: "2026-08-06T00:00:00Z", completedAt: null, }); const ready = classifyReportEnvelope(200, { report: view("ready"), longformMarkdown: "# 个人长报告\n\n### 解读摘要\n正文", }); assert.equal(ready.phase, "markdown-ready"); assert.ok(ready.phase === "markdown-ready" && ready.markdown.includes("解读摘要")); const readyV2 = classifyReportEnvelope(200, { report: view("ready"), longformMarkdown: "## 成品阅读导航\n导航", reportDocument: canonicalV2Fixture, }); assert.equal(readyV2.phase, "markdown-ready"); assert.deepEqual(classifyReportEnvelope(200, { report: view("generating") }), { phase: "generating" }); assert.deepEqual(classifyReportEnvelope(200, { report: view("failed", "model_unavailable") }), { phase: "failed", failureCode: "model_unavailable", }); assert.deepEqual( classifyReportEnvelope(200, { report: { ...view("failed", "report_schema_invalid"), failureSummary: "4 个主题中 4 个写作失败:输出被截断" }, }), { phase: "failed", failureCode: "report_schema_invalid", failureSummary: "4 个主题中 4 个写作失败:输出被截断", }, ); assert.deepEqual( classifyReportEnvelope(200, { report: { ...view("failed", "calculation_unavailable"), failureSummary: "报告正文未能保存,请稍后重试", appendixLastErrorCode: "appendix_persist_failed", }, }), { phase: "failed", failureCode: "appendix_persist_failed", failureSummary: "报告正文未能保存,请稍后重试", appendixLastErrorCode: "appendix_persist_failed", }, ); assert.deepEqual(classifyReportEnvelope(200, { report: view("failed") }), { phase: "failed", failureCode: null }); // Server-side error envelopes carry a stable code at the top level. assert.deepEqual(classifyReportEnvelope(503, { error: "x", code: "report_persistence_unavailable" }), { phase: "failed", failureCode: "report_persistence_unavailable", }); assert.deepEqual(classifyReportEnvelope(500, {}), { phase: "failed", failureCode: null }); const missingDocument = classifyReportEnvelope(200, { report: view("ready") }); assert.equal(missingDocument.phase, "legacy-unavailable"); const badDocument = classifyReportEnvelope(200, { report: view("ready"), reportDocument: { schemaVersion: "wrong" }, }); assert.equal(badDocument.phase, "legacy-unavailable"); const unknownStatus = classifyReportEnvelope(200, { report: view("mystery") }); assert.equal(unknownStatus.phase, "invalid"); assert.equal(classifyReportEnvelope(200, null).phase, "invalid"); assert.equal(classifyReportEnvelope(200, { error: "oops" }).phase, "invalid"); }); test("report center and detail surfaces prefer a readable failure summary over the bare schema code", () => { const center = readFileSync( new URL("../src/components/personal-report/personal-report-center.tsx", import.meta.url), "utf8", ); const page = readFileSync( new URL("../src/components/personal-report/personal-report-page.tsx", import.meta.url), "utf8", ); assert.match(center, /report\.failureSummary \?\? report\.failureCode/); assert.match(page, /state\.failureSummary \?\? "生成过程中出现问题/); assert.match(page, /错误码:\{state\.failureCode\}/); }); test("canonical contract rejects malformed documents", () => { assert.equal(safeParseReportDocument({}).ok, false); const noCharts = { ...structuredClone(canonicalFixture) }; delete (noCharts as Partial).charts; assert.equal(safeParseReportDocument(noCharts).ok, false); const badVersion = { ...structuredClone(canonicalFixture), schemaVersion: "report_document.v2" }; assert.equal(safeParseReportDocument(badVersion).ok, false); const badMode = { ...structuredClone(canonicalFixture), presentationMode: "ultra" }; assert.equal(safeParseReportDocument(badMode).ok, false); const badReportId = { ...structuredClone(canonicalFixture), reportId: "not-a-uuid" }; assert.equal(safeParseReportDocument(badReportId).ok, false); const badRefs = structuredClone(canonicalFixture) as LegacyReportDocumentV1; badRefs.thematicNarrative[0].evidenceRefs = ["ev-does-not-exist"]; assert.equal(safeParseReportDocument(badRefs).ok, false, "dangling evidenceRefs must fail guards"); }); test("ready reports expose only the back link and accessible export icon", () => { const actionsSource = readFileSync( new URL("../src/components/personal-report/report-actions.tsx", import.meta.url), "utf8", ); assert.match(actionsSource, /href="\/reports"/); assert.match(actionsSource, /返回报告中心/); assert.match(actionsSource, /ReportExportDrawer/); // 原值:`assert.match(actionsSource, /size="icon"/)` 与 `/title="导出"/` // 新值:不再是图标按钮(doesNotMatch size="icon"),可见文字「导出」,描边样式 // 原因:TASK-report-reader-polish-20260929 §3-3——图标按钮太不显眼,产品要求做明显 assert.doesNotMatch(actionsSource, /size="icon"/); assert.match(actionsSource, /variant="outline"/); assert.match(actionsSource, /