import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { unified } from "unified"; import remarkParse from "remark-parse"; import remarkGfm from "remark-gfm"; import remarkRehype from "remark-rehype"; import { reportChartGrid } from "../src/lib/report-chart-grid-rehype.ts"; import { annotateUnreliableChartSections, CHART_CAVEAT_EN, CHART_CAVEAT_ZH } from "../src/lib/personal-report-chart-caveats.ts"; // Real engine reader output for explicitly fictional births (fixtureProvenance.fictional). const densityReader = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-reader.json", import.meta.url), "utf8")); const readerMain = JSON.parse(readFileSync(new URL("./fixtures/report-reader-main-fictional.json", import.meta.url), "utf8")); const readerMainEn = JSON.parse(readFileSync(new URL("./fixtures/report-reader-main-fictional-en.json", import.meta.url), "utf8")); function chartCards(markdown: string): { cards: number; captions: string[] } { const tree = unified().use(remarkParse).use(remarkGfm).use(remarkRehype).use(reportChartGrid) .runSync(unified().use(remarkParse).use(remarkGfm).parse(markdown)) as { children: unknown[] }; let cards = 0; const captions: string[] = []; const text = (node: { type?: string; value?: string; children?: unknown[] }): string => node.type === "text" ? String(node.value ?? "") : (node.children ?? []).map((child) => text(child as never)).join(""); const walk = (node: { properties?: { className?: unknown }; children?: unknown[]; tagName?: string }) => { const classes = node.properties?.className; if (Array.isArray(classes) && classes.includes("personal-report-chart-card")) { cards += 1; const caption = (node.children ?? []).find((child) => (child as { tagName?: string }).tagName === "p"); if (caption) captions.push(text(caption as never)); } for (const child of node.children ?? []) walk(child as never); }; walk(tree as never); return { cards, captions }; } test("legacy and empty tiers leave reader Markdown byte-identical", () => { for (const markdown of [densityReader.markdown, readerMain.markdown] as string[]) { assert.equal(annotateUnreliableChartSections(markdown, undefined, "zh"), markdown); assert.equal(annotateUnreliableChartSections(markdown, [], "zh"), markdown); } }); test("D9/D10 chart and table sections get one line each; charts and tables stay; idempotent", () => { const raw = readerMain.markdown as string; const annotated = annotateUnreliableChartSections(raw, ["D9", "D10"], "zh"); const lines = annotated.split("\n"); const flagged = lines.flatMap((line, index) => line === CHART_CAVEAT_ZH ? [lines[index - 2]] : []); assert.deepEqual(flagged, ["##### Navamsha", "##### D9 九分盘", "##### D10 Dasamsa", "#### D9 九分盘 数值位置表", "#### D10 Dasamsa 数值位置表"]); assert.equal(lines.filter((line) => line.startsWith("|")).length, raw.split("\n").filter((line) => line.startsWith("|")).length); assert.equal(annotateUnreliableChartSections(annotated, ["D9", "D10"], "zh"), annotated); // Only the named chart is touched: D1 / D10 / D11 / D12 headings stay bare when only D9 is unreliable. const d9 = annotateUnreliableChartSections(raw, ["D9"], "zh").split("\n"); assert.deepEqual(d9.flatMap((line, index) => line === CHART_CAVEAT_ZH ? [d9[index - 2]] : []), ["##### Navamsha", "##### D9 九分盘", "#### D9 九分盘 数值位置表"]); }); test("English edition gets the English line under its own chart labels", () => { const annotated = annotateUnreliableChartSections(readerMainEn.markdown, ["D9"], "en"); const lines = annotated.split("\n"); assert.deepEqual(lines.flatMap((line, index) => line === CHART_CAVEAT_EN ? [lines[index - 2]] : []), ["##### Navamsha", "##### D9 Navamsa", "#### D9 Navamsa Position Table"]); assert.ok(!/[㐀-鿿]/.test(CHART_CAVEAT_EN)); }); test("chart cards keep their grid: the line becomes the card's caption, never a split", () => { const raw = densityReader.markdown as string; const before = chartCards(raw); const after = chartCards(annotateUnreliableChartSections(raw, ["D9", "D10"], "zh")); assert.ok(before.cards >= 3); assert.equal(after.cards, before.cards); assert.equal(after.captions.filter((caption) => caption === CHART_CAVEAT_ZH).length, 3); }); test("longform generation: engine payload unchanged; stored body annotated only for segment-v1 tiers", () => { const script = ` import assert from "node:assert/strict"; import { mock } from "node:test"; import { readFileSync } from "node:fs"; mock.module("server-only", { defaultExport: {} }); const { generatePersonalReportLongform } = await import("./src/lib/personal-report-longform-generate.ts"); const { CHART_CAVEAT_ZH } = await import("./src/lib/personal-report-chart-caveats.ts"); const packet = JSON.parse(readFileSync("tests/fixtures/report-density-fictional-engine.json", "utf8")); const golden = JSON.parse(readFileSync("tests/fixtures/report-density-fictional-reader.json", "utf8")); async function run(candidateRange) { const rows = new Map(); let body = ""; const admin = { async rpc(name, args) { const table = "personal_report_sections"; const current = rows.get(table); if (name === "ensure_personal_report_section" && !current) rows.set(table, { user_id: args.p_user_id, request_id: args.p_request_id, section_id: args.p_section_id, status: "pending", payload: null }); if (name === "complete_personal_report_section" && current?.status === "pending") rows.set(table, { ...current, status: "ready", payload: structuredClone(args.p_payload) }); return { data: [], error: null }; }, from(table) { return { select() { return this; }, eq() { return this; }, async maybeSingle() { return { data: structuredClone(rows.get(table) ?? null), error: null }; }, async upsert(row) { rows.set(table, structuredClone(row)); return { error: null }; }, }; } }; const report = { id: "123e4567-e89b-12d3-a456-426614174000", userId: "123e4567-e89b-12d3-a456-426614174001", requestId: "123e4567-e89b-12d3-a456-426614174002", reportType: "personal_full", presentationMode: "default", depth: "standard", requestedThemes: ["career"], skillName: "jyotish-vedic-astrology", skillVersion: "6.9.14", skillSourceCommit: null, skillSnapshotSha256: "ab".repeat(32) }; const result = await generatePersonalReportLongform({ report, admin, candidateRange, displayName: "虚构测试", birthTimeStatus: "accepted", profile: { birth_date: "2000-01-01", active_birth_time: "09:00", birth_time_status: "accepted", latitude: 0, longitude: 0, timezone_offset: 0, ayanamsa: "lahiri" }, now: () => new Date("2026-10-01T12:00:00Z"), fetchImpl: async (_url, init) => { body = init.body; return Response.json({ format: "markdown", markdown: golden.markdown, fact_table_packet: packet, reader_dasha_applicability: golden.reader_dasha_applicability }); } }); return { body, stored: rows.get("personal_report_longform_appendices").markdown, charts: result.document.charts.length }; } const legacy = await run({ startTime: "08:00", endTime: "10:00" }); const segment = await run({ startTime: "08:00", endTime: "10:00", unreliableVargas: ["D9", "D10"] }); assert.equal(segment.body, legacy.body, "engine payload is identical"); assert.equal(legacy.stored, golden.markdown, "legacy stored body is the engine text byte-for-byte"); assert.equal(segment.stored.split("\\n").filter(line => line === CHART_CAVEAT_ZH).length, 3); assert.equal(segment.stored.replaceAll("\\n\\n" + CHART_CAVEAT_ZH, ""), golden.markdown); assert.equal(segment.charts, legacy.charts); console.log("OK"); `; const result = spawnSync(process.execPath, ["--import", "tsx", "--experimental-test-module-mocks", "--input-type=module", "-e", script], { cwd: fileURLToPath(new URL("..", import.meta.url)), encoding: "utf8", }); assert.equal(result.status, 0, result.stderr + result.stdout); assert.match(result.stdout, /OK/); });