import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { resolveServerOwnedChartBirth } from "../src/lib/chart-birth-truth.ts"; import { prepareChartViewProfile } from "../src/lib/chart-view-profile-read.ts"; import { safeParseReportDocument } from "../src/lib/personal-report-contract.ts"; import { buildLongformCoverDocument } from "../src/lib/personal-report-longform-cover.ts"; import { loadReportWorkerProfile } from "../src/lib/personal-report-worker-subject.ts"; import { REPORT_BIRTH_FINGERPRINT_KEY, readReportBirthFingerprint, reportCoverSubject, reportDocumentBirthFingerprint, stampReportBirthFingerprint, subjectBirthFingerprint, withReportBirthFingerprint, } from "../src/lib/report-birth-binding.ts"; // Report cover binding, product decision A (TASK-birth-sky-followup-20260928 F1/F2). // Fictional sample birth only: 1994-05-18 07:40 UTC+8, 30.27N 120.15E. const storedSelf = { name: "虚构甲", birth_date: "1994-05-18", reported_birth_time: "07:40:00", birth_time_source: "family_exact", birth_time_status: "reported", birth_place_label: "虚构城", latitude: 30.27, longitude: 120.15, timezone_offset: 8, timezone_id: "Asia/Shanghai", ayanamsa: "lahiri", }; const routeSource = readFileSync(new URL("../src/app/api/reports/route.ts", import.meta.url), "utf8"); const itemRouteSource = readFileSync(new URL("../src/app/api/reports/[reportId]/route.ts", import.meta.url), "utf8"); const workerSource = readFileSync(new URL("../src/lib/personal-report-worker.ts", import.meta.url), "utf8"); const longformSource = readFileSync(new URL("../src/lib/personal-report-longform-generate.ts", import.meta.url), "utf8"); const routeCoreSource = readFileSync(new URL("../src/lib/personal-report-route-core.ts", import.meta.url), "utf8"); const generationSource = readFileSync(new URL("../src/lib/personal-report-generation.ts", import.meta.url), "utf8"); function fakeSupabase(rows: Readonly<{ profiles?: Record | null }>) { return { from(table: string) { const query = { select: () => query, eq: () => query, maybeSingle: async () => ({ data: table === "profiles" ? (rows.profiles ?? null) : { rectification_case_id: null }, error: null, }), }; return query; }, }; } test("the report fingerprint is the chart page's profileFingerprint (same function, stored row)", async () => { const withoutOffset = { ...storedSelf, timezone_offset: null }; const filled = { ...withoutOffset, timezone_offset: 8 }; const chartView = await prepareChartViewProfile({ row: withoutOffset, resolveTimezone: async () => filled }); assert.equal(chartView.ok, true); assert.equal(subjectBirthFingerprint(withoutOffset), chartView.birth.fingerprint); // The in-memory timezone fill changes the fingerprint, so it must be taken before the fill. assert.notEqual(resolveServerOwnedChartBirth(filled).fingerprint, chartView.birth.fingerprint); const stamped = stampReportBirthFingerprint(withoutOffset, filled); assert.equal(readReportBirthFingerprint(stamped), chartView.birth.fingerprint); assert.match(String((stamped as Record)[REPORT_BIRTH_FINGERPRINT_KEY]), /^[0-9a-f]{32}$/); }); test("worker profile loader stamps the stored-row fingerprint before the timezone fill", async () => { const profile = await loadReportWorkerProfile(fakeSupabase({ profiles: storedSelf }), "user-1", "self"); assert.equal(readReportBirthFingerprint(profile), subjectBirthFingerprint(storedSelf)); assert.equal((profile as Record).latitude, 30.27, "the generators still read the same row"); }); test("every generation path writes subject.birthFingerprint from the stamped profile", () => { // POST /api/reports (inline and queued create) stamps before the timezone fill. assert.match(routeSource, /profile = stampReportBirthFingerprint\(\s*resolved\.row,\s*await resolveMissingBirthTimezoneOffset\(resolved\.row/); // Inline route-core generation, the queued writer and the queued longform all pass it on. assert.match(routeCoreSource, /birthFingerprint: readReportBirthFingerprint\(profile\),/); assert.match(workerSource, /birthFingerprint: readReportBirthFingerprint\(profile\),/); assert.match(longformSource, /birthFingerprint: readReportBirthFingerprint\(profile\),/); // Both document assemblies (single-call and sectioned) stamp before the guard and the final parse. assert.equal(generationSource.match(/applyReportGuard\(withReportBirthFingerprint\(candidate, deps\.birthFingerprint\)/g)?.length, 2); // Never into the evidence bundle the model reads. assert.doesNotMatch(generationSource, /bundle[^\n]*birthFingerprint/); }); test("longform cover document carries the fingerprint and still parses; without it nothing is added", () => { const input = { report: { id: "123e4567-e89b-12d3-a456-426614174000", reportType: "personal_full" as const, presentationMode: "default" as const, depth: "standard" as const, requestedThemes: ["career"], skillName: "jyotish-vedic-astrology", skillVersion: "6.9.14", skillSourceCommit: null, skillSnapshotSha256: "ab".repeat(32), }, subject: { displayName: "虚构甲", birthTimeStatus: "reported" as const, birthPlaceLabel: "虚构城" }, markdown: "# 个人长报告\n\n## 摘要\n\n事业方向保持观察。\n", generatedAt: "2026-09-28T00:00:00.000Z", }; const fingerprint = subjectBirthFingerprint(storedSelf); const bound = buildLongformCoverDocument({ ...input, birthFingerprint: fingerprint }); assert.equal(bound.subject.birthFingerprint, fingerprint); assert.equal(reportDocumentBirthFingerprint(bound), fingerprint); const legacy = buildLongformCoverDocument(input); assert.equal("birthFingerprint" in legacy.subject, false); assert.equal(reportDocumentBirthFingerprint(legacy), null); }); test("documents without the field (old reports) still parse; a malformed fingerprint does not", () => { const document = buildLongformCoverDocument({ report: { id: "123e4567-e89b-12d3-a456-426614174000", reportType: "personal_full", presentationMode: "default", depth: "standard", requestedThemes: ["career"], skillName: "jyotish-vedic-astrology", skillVersion: "6.9.14", skillSourceCommit: null, skillSnapshotSha256: "ab".repeat(32), }, subject: { displayName: "虚构甲", birthTimeStatus: "reported", birthPlaceLabel: "虚构城" }, markdown: "# 个人长报告\n\n## 摘要\n\n事业方向保持观察。\n", generatedAt: "2026-09-28T00:00:00.000Z", }); assert.equal(safeParseReportDocument(document).ok, true); assert.equal(safeParseReportDocument(withReportBirthFingerprint(document, "a".repeat(32))).ok, true); assert.equal(safeParseReportDocument({ ...document, subject: { ...document.subject, birthFingerprint: "1994-05-18" } }).ok, false); assert.equal(withReportBirthFingerprint(document, undefined), document, "no fingerprint → untouched"); }); test("coverSubject: only a matching fingerprint yields the subject id, self when chart_profile_id is null", () => { const fp = "a".repeat(32); assert.equal(reportCoverSubject({ chartProfileId: null, storedFingerprint: fp, currentFingerprint: fp }), "self"); assert.equal(reportCoverSubject({ chartProfileId: "11111111-1111-4111-8111-111111111111", storedFingerprint: fp, currentFingerprint: fp }), "11111111-1111-4111-8111-111111111111"); assert.equal(reportCoverSubject({ chartProfileId: null, storedFingerprint: fp, currentFingerprint: "b".repeat(32) }), null); assert.equal(reportCoverSubject({ chartProfileId: null, storedFingerprint: null, currentFingerprint: fp }), null); assert.equal(reportCoverSubject({ chartProfileId: null, storedFingerprint: fp, currentFingerprint: null }), null); }); test("GET /api/reports/:id compares on the server through the chart page's subject loader", () => { assert.match(itemRouteSource, /loadCurrentBirthFingerprint: async \(ownerId: string, subjectId: string\) => \(\s*subjectBirthFingerprint\(\(await loadSubjectBirth\(supabase, ownerId, subjectId\)\)\.row\)/); assert.match(routeCoreSource, /reportDocumentBirthFingerprint\(row\.reportDocument\)/); });