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, findForbiddenContent, parseReportDocument, REPORT_DOCUMENT_MAX_BYTES, safeParseReportDocument, serializedReportDocumentBytes, type ReportDocumentV1, } 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; 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("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"); });