Applied before the longform snapshot is hashed, Chinese and English bodies. The engine payload is unchanged; legacy and unrectified reports store the engine text byte-for-byte. Chart cards keep their grid (the line becomes the card caption). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
120 lines
8.1 KiB
TypeScript
120 lines
8.1 KiB
TypeScript
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/);
|
|
});
|