338 lines
15 KiB
TypeScript
338 lines
15 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 {
|
|
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<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);
|
|
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 ["<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);
|
|
});
|