Files
Jyotisha/frontend/tests/personal-report-contract.test.ts
T
Jesse_Chen 4ef4c4064f
Independent Staging Quality Gate / validate (push) Successful in 12m11s
Independent Staging Quality Gate / publish (push) Successful in 1m57s
fix(report): name final-parse guards and scrub blocked wording
Five-theme assemble cleared the chart and actionNotes caps, then failed
three anonymous guards. Log field path plus kind only, and strip
deterministic phrases from sections the producer already marked blocked.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 23:09:21 +08:00

433 lines
18 KiB
TypeScript

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 {
CHART_IDS,
findBlockedDeterministicClaims,
findChartSetViolations,
findDanglingEvidenceRefs,
findDuplicateEvidenceIds,
classifyReportDocumentGuardError,
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<string, unknown>).hometown = "上海";
assert.equal(safeParseReportDocument(extra).ok, false);
const missing = clone();
delete (missing as Partial<ReportDocumentV1>).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 = [
"<script>alert(1)</script>",
"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<string, unknown> = {};
for (const key of Object.keys(fixture).reverse()) {
reordered[key] = (fixture as Record<string, unknown>)[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);
const unsupportedWealth = cloneV2();
unsupportedWealth.blockedConflictDisclosure = unsupportedWealth.blockedConflictDisclosure.filter(
(section) => section.theme !== "wealth",
);
unsupportedWealth.thematicNarrative.push({
...structuredClone(unsupportedWealth.thematicNarrative[0]),
id: "theme-wealth",
theme: "wealth",
});
const wealthViolations = findThemeCoverageViolations(unsupportedWealth);
assert.ok(wealthViolations.some((error) => error.includes("requires structured D2")));
assert.ok(wealthViolations.some((error) => error.includes("requires structured D11")));
assert.equal(safeParseReportDocument(unsupportedWealth).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 chart cap and enum stay bound to CHART_IDS", () => {
const contractSource = readFileSync(
new URL("../src/lib/personal-report-contract.ts", import.meta.url),
"utf8",
);
assert.match(
contractSource,
/charts: z\.array\(reportDocumentV2ChartSchema\)\.min\(1\)\.max\(CHART_IDS\.length\)/,
);
assert.doesNotMatch(contractSource, /charts: z\.array\(reportDocumentV1ChartSchema\)\.min\(1\)\.max\(CHART_IDS/);
const schema = JSON.parse(readFileSync(
new URL("../../contracts/personal-report/report-document.v2.schema.json", import.meta.url),
"utf8",
)) as {
properties: { charts: { maxItems: number } };
definitions: { chart: { properties: { id: { enum: string[] } } } };
};
assert.equal(schema.properties.charts.maxItems, CHART_IDS.length);
assert.deepEqual(schema.definitions.chart.properties.id.enum, [...CHART_IDS]);
});
test("a document with every CHART_IDS entry passes the server parse", () => {
const document = cloneV2();
const template = document.charts[0];
const present = new Set(document.charts.map((chart) => chart.id));
for (const id of CHART_IDS) {
if (present.has(id)) continue;
document.charts.push({
...structuredClone(template),
id,
title: `${id} 分盘`,
});
}
assert.equal(document.charts.length, CHART_IDS.length);
const parsed = safeParseServerReportDocument(document);
assert.equal(parsed.ok, true);
});
test("one chart beyond CHART_IDS.length is still rejected", () => {
const document = cloneV2();
const template = document.charts[0];
while (document.charts.length < CHART_IDS.length + 1) {
document.charts.push({
...structuredClone(template),
id: template.id,
title: `${template.id} extra ${document.charts.length}`,
});
}
const result = safeParseReportDocument(document);
assert.equal(result.ok, false);
assert.ok(result.errors.some((error) => error.path === "charts" && error.code === "too_big"));
});
test("v2 guard rejects HTML and CSS while allowing ordinary Chinese prose", () => {
for (const poison of ["<div>报告</div>", "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);
});
test("guard parse errors expose field path and kind without the offending value", () => {
const missing = cloneV2();
missing.blockedConflictDisclosure = missing.blockedConflictDisclosure.filter(
(section) => section.theme !== "career",
);
const coverage = safeParseReportDocument(missing);
assert.equal(coverage.ok, false);
assert.ok(coverage.errors.some((error) => (
error.path === "requestedThemes" && error.code === "theme_coverage"
)));
const medical = cloneV2();
medical.actionNotes[0].note = "你已经患有糖尿病。";
const forbidden = safeParseReportDocument(medical);
assert.equal(forbidden.ok, false);
assert.ok(forbidden.errors.some((error) => (
error.path === "actionNotes[0].note" && error.code === "forbidden_medical_diagnosis"
)));
assert.ok(forbidden.errors.every((error) => !error.path.includes("糖尿病")));
assert.equal(
classifyReportDocumentGuardError("thematicNarrative[1]: blocked section contains deterministic prediction").code,
"blocked_deterministic",
);
});