import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { computeEvidenceHash, safeParseServerReportDocument, parseServerReportDocument, } from "../src/lib/personal-report-contract.server-core.ts"; import { findBlockedDeterministicClaims, findChartSetViolations, findDanglingEvidenceRefs, findDuplicateEvidenceIds, CURRENT_REPORT_DOCUMENT_SCHEMA_VERSION, LEGACY_REPORT_DOCUMENT_SCHEMA_VERSION, REPORT_DOCUMENT_SCHEMA_VERSION, findForbiddenContent, findThemeCoverageViolations, findUnsupportedDateClaims, parseReportDocument, REPORT_DOCUMENT_MAX_BYTES, safeParseReportDocument, serializedReportDocumentBytes, type ReportDocumentV1, type ReportDocumentV2, } from "../src/lib/personal-report-contract.ts"; import { ReportDocumentValidationError } from "../src/lib/personal-report-contract.ts"; const fixtureText = readFileSync( new URL("../../tests/fixtures/personal_report_document.v1.json", import.meta.url), "utf8", ); const fixture: ReportDocumentV1 = JSON.parse(fixtureText); const clone = () => structuredClone(fixture) as ReportDocumentV1; const v2FixtureText = readFileSync( new URL("../../tests/fixtures/personal_report_document.v2.json", import.meta.url), "utf8", ); const v2Fixture: ReportDocumentV2 = JSON.parse(v2FixtureText); const cloneV2 = () => structuredClone(v2Fixture) as ReportDocumentV2; test("fixture passes isomorphic parse and stays under the 1.5 MiB cap", () => { const result = safeParseReportDocument(fixture); assert.equal(result.ok, true); assert.ok(serializedReportDocumentBytes(fixture) <= REPORT_DOCUMENT_MAX_BYTES); assert.ok(serializedReportDocumentBytes(fixture) < 100_000); }); test("isomorphic parse rejects extra keys, missing keys, and bad enums", () => { const extra = clone(); (extra.subject as Record).hometown = "上海"; assert.equal(safeParseReportDocument(extra).ok, false); const missing = clone(); delete (missing as Partial).disclaimer; assert.equal(safeParseReportDocument(missing).ok, false); const badEnum = clone(); badEnum.subject.birthTimeStatus = "guessed" as ReportDocumentV1["subject"]["birthTimeStatus"]; assert.equal(safeParseReportDocument(badEnum).ok, false); }); test("charts must contain exactly one D1 with all twelve houses", () => { const zeroD1 = clone(); zeroD1.charts = zeroD1.charts.filter((chart) => chart.id !== "D1"); assert.deepEqual(findChartSetViolations(zeroD1), ["charts must contain exactly one D1 chart, found 0"]); assert.equal(safeParseReportDocument(zeroD1).ok, false); const twoD1 = clone(); twoD1.charts.push(structuredClone(twoD1.charts[0])); const violations = findChartSetViolations(twoD1); assert.ok(violations.some((v) => v.includes("duplicate chart id"))); assert.ok(violations.some((v) => v.includes("exactly one D1"))); assert.equal(safeParseReportDocument(twoD1).ok, false); const elevenHouses = clone(); elevenHouses.charts[0].houses = elevenHouses.charts[0].houses.slice(0, 11); assert.ok(findChartSetViolations(elevenHouses).some((v) => v.includes("all twelve house numbers"))); assert.equal(safeParseReportDocument(elevenHouses).ok, false); }); test("duplicate house numbers are rejected per chart", () => { const duplicated = clone(); duplicated.charts[0].houses[11].houseNumber = 1; const result = safeParseReportDocument(duplicated); assert.equal(result.ok, false); }); test("longitude is [0, 360)", () => { const at360 = clone(); at360.charts[0].planets![0].longitudeDegrees = 360; assert.equal(safeParseReportDocument(at360).ok, false); const near360 = clone(); near360.charts[0].planets![0].longitudeDegrees = 359.999; assert.equal(safeParseReportDocument(near360).ok, true); }); test("evidence ids must be globally unique across the appendix", () => { const duplicated = clone(); duplicated.evidenceAppendix.conflicts[0].id = duplicated.evidenceAppendix.techniqueAudit[0].id; assert.ok(findDuplicateEvidenceIds(duplicated).some((v) => v.includes("ev-mevg-web"))); assert.equal(safeParseReportDocument(duplicated).ok, false); }); test("dangling evidenceRefs are rejected", () => { const dangling = clone(); dangling.thematicNarrative[0].evidenceRefs = ["ev-no-such-evidence"]; assert.deepEqual(findDanglingEvidenceRefs(dangling), ["career:ev-no-such-evidence"]); assert.equal(safeParseReportDocument(dangling).ok, false); }); test("blocked sections reject deterministic predictions", () => { const deterministic = clone(); deterministic.thematicNarrative[3].narrative = "这个事件必然会发生在明年,一定会成功。"; assert.ok(findBlockedDeterministicClaims(deterministic).length > 0); assert.equal(safeParseReportDocument(deterministic).ok, false); const nonDeterministic = clone(); nonDeterministic.thematicNarrative[3].narrative = "需要更多历史事件校准后才能评估,具体应期暂不提供。"; assert.equal(findBlockedDeterministicClaims(nonDeterministic).length, 0); assert.equal(safeParseReportDocument(nonDeterministic).ok, true); }); const forbiddenSamples = [ "", "javascript:alert(1)", "file:///Users/jesse/private/chart.json", "参考 ${process.env.SUPABASE_SERVICE_ROLE_KEY}", "onerror=alert(1)", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.abc", "node:internal/modules/cjs/loader", "Traceback (most recent call last)", "__dirname/secret", "__proto__ pollution", "C:\\Users\\jesse\\chart.json", "tool_call_id: call_123", ]; test("forbidden content patterns reject executable and internal material", () => { for (const poison of forbiddenSamples) { const poisoned = clone(); poisoned.disclaimer = poison; assert.ok( findForbiddenContent(poison).length > 0, `expected ${poison} to be flagged`, ); assert.equal(safeParseReportDocument(poisoned).ok, false, `poison: ${poison}`); } }); test("ordinary Chinese report text is not flagged as forbidden", () => { assert.equal(findForbiddenContent("事业与财富主题的多系统证据较一致。").length, 0); assert.equal(findForbiddenContent("A < B 的比较关系不属于 HTML 标签").length, 0); }); test("serialization size cap rejects oversized documents", () => { const oversized = clone(); oversized.disclaimer = "字".repeat(REPORT_DOCUMENT_MAX_BYTES); assert.equal(safeParseReportDocument(oversized).ok, false); }); test("JSON object key order is not validated (display order is a typed contract)", () => { const reordered: Record = {}; for (const key of Object.keys(fixture).reverse()) { reordered[key] = (fixture as Record)[key]; } assert.equal(safeParseReportDocument(reordered).ok, true); }); test("ReportDocument v1 keeps legacy Skill identity compatibility", () => { assert.equal(fixture.provenance.skillName, undefined); assert.equal(fixture.provenance.skillVersion, undefined); assert.equal(safeParseReportDocument(fixture).ok, true); }); test("optional Skill name and version accept registry identity and reject malformed values", () => { const current = clone(); current.provenance.skillName = "jyotish-vedic-astrology"; current.provenance.skillVersion = "6.9.14"; assert.equal(safeParseReportDocument(current).ok, true); const badName = clone(); badName.provenance.skillName = "Bad Package"; assert.equal(safeParseReportDocument(badName).ok, false); const badVersion = clone(); badVersion.provenance.skillVersion = "v6"; assert.equal(safeParseReportDocument(badVersion).ok, false); }); test("server parse recomputes the evidence hash and rejects self-reported mismatches", () => { // The fixture hash is the canonical cross-language value; recomputation must agree. assert.equal(computeEvidenceHash(fixture.evidenceAppendix), fixture.provenance.evidenceHash); assert.equal( computeEvidenceHash(fixture.evidenceAppendix), "a830bcb22ce287d2637ffc91f9f951d5f24b19cb067712db2687fd23437f13f4", ); assert.equal(safeParseServerReportDocument(fixture).ok, true); // Tampered evidence with the self-reported hash left unchanged must fail. const tampered = clone(); tampered.evidenceAppendix.calculationEvidence[0].value = "篡改后的证据值"; const result = safeParseServerReportDocument(tampered); assert.equal(result.ok, false); assert.ok(result.errors.some((error) => error.path === "provenance.evidenceHash")); assert.throws(() => parseServerReportDocument(tampered), ReportDocumentValidationError); // The isomorphic parse alone does not verify the hash (server-only duty). assert.equal(safeParseReportDocument(tampered).ok, true); }); test("parseReportDocument throws a typed validation error on guard failures", () => { const bad = clone(); bad.thematicNarrative[0].evidenceRefs = ["ev-missing"]; assert.throws(() => parseReportDocument(bad), ReportDocumentValidationError); const parsed = parseReportDocument(fixture); assert.equal(parsed.schemaVersion, "report_document.v1"); }); test("ReportDocument v2 is canonical and fixture passes all guards", () => { assert.equal(LEGACY_REPORT_DOCUMENT_SCHEMA_VERSION, "report_document.v1"); assert.equal(REPORT_DOCUMENT_SCHEMA_VERSION, "report_document.v2"); assert.equal(CURRENT_REPORT_DOCUMENT_SCHEMA_VERSION, REPORT_DOCUMENT_SCHEMA_VERSION); assert.equal(v2Fixture.schemaVersion, REPORT_DOCUMENT_SCHEMA_VERSION); assert.equal(v2Fixture.depth, "standard"); assert.deepEqual(findThemeCoverageViolations(v2Fixture), []); assert.deepEqual(findDanglingEvidenceRefs(v2Fixture), []); assert.deepEqual(findUnsupportedDateClaims(v2Fixture), []); assert.equal(safeParseReportDocument(v2Fixture).ok, true); assert.ok(serializedReportDocumentBytes(v2Fixture) <= REPORT_DOCUMENT_MAX_BYTES); }); test("ReportDocument v2 server read recomputes evidence hash", () => { assert.equal(computeEvidenceHash(v2Fixture.evidenceAppendix), v2Fixture.provenance.evidenceHash); assert.equal( computeEvidenceHash(v2Fixture.evidenceAppendix), "b4f6d95916b5253054a9c64958eb3deec6ee5b54b0286465e7a986ba701132cf", ); assert.equal(safeParseServerReportDocument(v2Fixture).ok, true); const tampered = cloneV2(); tampered.evidenceAppendix.calculationEvidence[0].value = "tampered-v2"; const result = safeParseServerReportDocument(tampered); assert.equal(result.ok, false); assert.ok(result.errors.some((error) => error.path === "provenance.evidenceHash")); }); test("every requested v2 theme has exactly one section or blocked disclosure", () => { const missing = cloneV2(); missing.blockedConflictDisclosure = missing.blockedConflictDisclosure.filter( (section) => section.theme !== "career", ); assert.ok(findThemeCoverageViolations(missing).some((error) => error.includes("career") && error.includes("found 0"))); assert.equal(safeParseReportDocument(missing).ok, false); const doubleCovered = cloneV2(); doubleCovered.thematicNarrative.push({ ...structuredClone(doubleCovered.thematicNarrative[0]), id: "theme-career", theme: "career", }); assert.ok(findThemeCoverageViolations(doubleCovered).some((error) => error.includes("career") && error.includes("found 2"))); assert.equal(safeParseReportDocument(doubleCovered).ok, false); const duplicatedRequest = cloneV2(); duplicatedRequest.requestedThemes.push("general"); assert.ok(findThemeCoverageViolations(duplicatedRequest).some((error) => error.includes("requestedThemes must contain general exactly once, found 2"))); assert.equal(safeParseReportDocument(duplicatedRequest).ok, false); }); test("v2 thematic sections require their real structured divisional charts", () => { const unsupportedCareer = cloneV2(); unsupportedCareer.blockedConflictDisclosure = unsupportedCareer.blockedConflictDisclosure.filter( (section) => section.theme !== "career", ); unsupportedCareer.thematicNarrative.push({ ...structuredClone(unsupportedCareer.thematicNarrative[0]), id: "theme-career", theme: "career", }); assert.ok(findThemeCoverageViolations(unsupportedCareer).some((error) => error.includes("requires structured D10"))); assert.equal(safeParseReportDocument(unsupportedCareer).ok, false); assert.equal(safeParseReportDocument(v2Fixture).ok, true); }); test("v2 evidence references close across all new sections and charts", () => { const mutations: Array<[string, (document: ReportDocumentV2) => void]> = [ ["executiveSummary", (document) => { document.executiveSummary.evidenceRefs = ["ev-missing"]; }], ["natalFoundation", (document) => { document.natalFoundation.evidenceRefs = ["ev-missing"]; }], ["currentPhase", (document) => { document.currentPhase!.evidenceRefs = ["ev-missing"]; }], ["actionNotes", (document) => { document.actionNotes[0].evidenceRefs = ["ev-missing"]; }], ["charts", (document) => { document.charts[0].evidenceRefs = ["ev-missing"]; }], ["thematicNarrative", (document) => { document.thematicNarrative[0].evidenceRefs = ["ev-missing"]; }], ["blockedConflictDisclosure", (document) => { document.blockedConflictDisclosure[0].evidenceRefs = ["ev-missing"]; }], ]; for (const [label, mutate] of mutations) { const dangling = cloneV2(); mutate(dangling); assert.ok(findDanglingEvidenceRefs(dangling).some((entry) => entry.includes("ev-missing")), label); assert.equal(safeParseReportDocument(dangling).ok, false, label); } }); test("v2 guard rejects unsupported dates, medical diagnoses, and deterministic financial promises", () => { const unsupportedDate = cloneV2(); unsupportedDate.blockedConflictDisclosure[0].reason = "结论将在2031年5月发生。"; unsupportedDate.blockedConflictDisclosure[0].evidenceRefs = []; assert.ok(findUnsupportedDateClaims(unsupportedDate).some((error) => error.includes("blockedConflictDisclosure[0]"))); assert.equal(safeParseReportDocument(unsupportedDate).ok, false); const medical = cloneV2(); medical.actionNotes[0].note = "你已经患有糖尿病。"; assert.ok(findForbiddenContent(medical.actionNotes[0].note).includes("medical_diagnosis")); assert.equal(safeParseReportDocument(medical).ok, false); const financial = cloneV2(); financial.actionNotes[0].note = "这个方案保证收益并且稳赚不赔。"; assert.ok(findForbiddenContent(financial.actionNotes[0].note).includes("deterministic_financial_promise")); assert.equal(safeParseReportDocument(financial).ok, false); }); test("v2 guard rejects HTML and CSS while allowing ordinary Chinese prose", () => { for (const poison of ["
报告
", "body { color: red; }", 'style="color:red"']) { const document = cloneV2(); document.actionNotes[0].note = poison; assert.ok(findForbiddenContent(poison).length > 0, poison); assert.equal(safeParseReportDocument(document).ok, false, poison); } const ordinary = cloneV2(); ordinary.actionNotes[0].note = "记录真实事件日期,并把观察与证据逐项核对。"; assert.equal(findForbiddenContent(ordinary.actionNotes[0].note).length, 0); assert.equal(safeParseReportDocument(ordinary).ok, true); });