import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import React from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { PersonalReportDocumentView } from "../src/components/personal-report/personal-report-document-view.tsx"; import { classifyReportEnvelope } from "../src/components/personal-report/personal-report-page.tsx"; import { safeParseReportDocument } from "../src/lib/personal-report-contract.ts"; import type { LegacyReportDocumentV1, ReportDocument, ReportDocumentV2 } from "../src/lib/personal-report-contract.ts"; Object.assign(globalThis, { React }); 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; function render(document: ReportDocument): string { return renderToStaticMarkup(React.createElement(PersonalReportDocumentView, { document })); } function withPresentationMode(document: LegacyReportDocumentV1, presentationMode: "default" | "research"): LegacyReportDocumentV1 { return { ...structuredClone(document), presentationMode }; } function withCharts(document: LegacyReportDocumentV1, charts: LegacyReportDocumentV1["charts"]): LegacyReportDocumentV1 { return { ...structuredClone(document), charts }; } 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("renders the selected answer-first order: cover -> summary -> D1 -> themes -> appendix -> disclaimer", () => { const markup = render(canonicalFixture); const positions: Record = { subject: markup.indexOf(canonicalFixture.subject.displayName), d1: markup.indexOf('id="report-charts"'), summary: markup.indexOf('id="report-summary"'), theme: markup.indexOf('id="report-themes"'), appendix: markup.indexOf('id="report-appendix"'), disclaimer: markup.indexOf('id="report-disclaimer"'), }; for (const key of Object.keys(positions)) { assert.ok(positions[key] >= 0, `${key} must be rendered`); } assert.ok(positions.subject < positions.summary, "cover comes before the summary"); assert.ok(positions.summary < positions.d1, "answer-first summary comes before the D1 chart"); assert.ok(positions.d1 < positions.theme, "D1 chart comes before themes"); assert.ok(positions.theme < positions.appendix, "themes come before the appendix"); assert.ok(positions.appendix < positions.disclaimer, "appendix comes before the disclaimer"); }); test("accepted and confirmed birth times render as distinct report metadata states", () => { const accepted = structuredClone(canonicalFixture); accepted.subject.birthTimeStatus = "accepted"; const acceptedMarkup = render(accepted); assert.match(acceptedMarkup, /
生时状态<\/dt>
已采用时间(未确认)<\/dd>/); const confirmed = structuredClone(canonicalFixture); confirmed.subject.birthTimeStatus = "confirmed"; const confirmedMarkup = render(confirmed); assert.match(confirmedMarkup, /
生时状态<\/dt>
已确认时间<\/dd>/); }); test("v2 renders scope, natal foundation, themes, current phase, actions, blocked disclosures and provenance", () => { const markup = render(canonicalV2Fixture); const positions: Record = { summary: markup.indexOf('id="report-summary"'), natal: markup.indexOf('id="report-natal-foundation"'), d1: markup.indexOf('id="report-charts"'), themes: markup.indexOf('id="report-themes"'), currentPhase: markup.indexOf('id="report-current-phase"'), actions: markup.indexOf('id="report-action-notes"'), blocked: markup.indexOf('id="report-blocked-disclosures"'), appendix: markup.indexOf('id="report-appendix"'), disclaimer: markup.indexOf('id="report-disclaimer"'), }; for (const [key, position] of Object.entries(positions)) { assert.ok(position >= 0, `${key} must be rendered for v2`); } assert.ok(positions.summary < positions.natal); assert.ok(positions.natal < positions.d1); assert.ok(positions.d1 < positions.themes); assert.ok(positions.themes < positions.currentPhase); assert.ok(positions.currentPhase < positions.actions); assert.ok(positions.actions < positions.blocked); assert.ok(positions.blocked < positions.appendix); assert.ok(positions.appendix < positions.disclaimer); assert.match(markup, /data-report-schema-version="report_document\.v2"/); assert.match(markup, /报告深度<\/dt>
标准/); assert.match(markup, /请求主题<\/dt>
综合、事业、财富、应期/); assert.match(markup, new RegExp(canonicalV2Fixture.natalFoundation.keyFactors[0])); assert.match(markup, new RegExp(canonicalV2Fixture.currentPhase?.phaseLabel ?? "missing")); assert.match(markup, new RegExp(canonicalV2Fixture.actionNotes[0].title)); assert.match(markup, new RegExp(canonicalV2Fixture.blockedConflictDisclosure[0].missingEvidence[0])); assert.match(markup, new RegExp(canonicalV2Fixture.provenance.skillName)); assert.match(markup, new RegExp(canonicalV2Fixture.provenance.skillVersion.replaceAll(".", "\\."))); assert.match(markup, new RegExp(canonicalV2Fixture.provenance.evidenceHash)); assert.match(markup, new RegExp(canonicalV2Fixture.disclaimer.slice(0, 20))); }); test("v2 currentPhase is omitted when null, without hiding the remaining v2 sections", () => { const document = structuredClone(canonicalV2Fixture); document.currentPhase = null; const markup = render(document); assert.doesNotMatch(markup, /id="report-current-phase"/); assert.match(markup, /id="report-action-notes"/); assert.match(markup, /id="report-blocked-disclosures"/); }); test("v2 chart evidence refs link to the appendix and every real structured chart uses the SVG renderer", () => { const document = structuredClone(canonicalV2Fixture); document.charts.push({ ...structuredClone(document.charts[0]), id: "D2", title: "D2 财富分盘", }); const markup = render(document); assert.equal((markup.match(/ chart.evidenceRefs)) { assert.match(markup, new RegExp(`href="#evidence-${evidenceRef}"`)); } }); test("v2 model-authored text stays escaped and cannot inject HTML, CSS or SVG", () => { const document = structuredClone(canonicalV2Fixture); document.actionNotes[0].note = ''; const markup = render(document); assert.match(markup, /<style>body\{display:none\}<\/style>/); assert.match(markup, /<svg onload="alert\(1\)"><\/svg>/); assert.doesNotMatch(markup, /