import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
CHART_SIZE,
HOUSE_NUMBERS,
HOUSE_POLYGONS,
pointInPolygon,
polygonArea,
} from "../src/lib/north-indian-chart-geometry.ts";
import {
parseReportChartBlock,
planetDisplayLabel,
stripReportChartBlocks,
toNorthIndianChart,
type ReportChartBlock,
} from "../src/lib/report-chart-block.ts";
const fixture = JSON.parse(
readFileSync(new URL("./fixtures/report-chart-blocks-golden.json", import.meta.url), "utf8"),
) as {
sourceCommand: string;
blocks: ReportChartBlock[];
};
function wrapMarkdown(blocks: readonly ReportChartBlock[]): string {
return blocks.map((block) => [
`#### ${block.title}`,
"",
``,
"",
"```jyotish-chart",
JSON.stringify(block),
"```",
"",
].join("\n")).join("\n");
}
test("golden fixture records the engine command that produced it", () => {
assert.match(fixture.sourceCommand, /test_report_chart_block|build_professional_report_reference_packet|render_pl9_markdown/);
});
test("all 22 golden blocks parse", () => {
assert.equal(fixture.blocks.length, 22);
for (const raw of fixture.blocks) {
const parsed = parseReportChartBlock(JSON.stringify(raw));
assert.ok(parsed, `block ${raw.id} should parse`);
}
});
test("D1 whole-sign houses start at the ascendant and wrap at 12", () => {
const d1 = fixture.blocks.find((block) => block.id === "D1");
assert.ok(d1);
const chart = toNorthIndianChart(d1);
assert.equal(chart.houses[0]?.sign, d1.ascendant.sign);
const signs = [
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
] as const;
const ascIndex = signs.indexOf(d1.ascendant.sign);
assert.equal(chart.houses[11]?.sign, signs[(ascIndex + 11) % 12]);
});
test("retrograde labels include 逆 after the planet letter", () => {
const sample = fixture.blocks.flatMap((block) => block.planets).find((planet) => planet.retrograde);
assert.ok(sample, "golden chart should contain at least one retrograde planet");
const label = planetDisplayLabel(sample.name, sample.degree, true);
assert.match(label, /逆 \d+°$/);
});
test("malicious title is not drawn; parse still succeeds", () => {
const d1 = fixture.blocks.find((block) => block.id === "D1");
assert.ok(d1);
const poisoned = parseReportChartBlock(JSON.stringify({
...d1,
title: '
',
}));
assert.ok(poisoned);
const chart = toNorthIndianChart(poisoned);
const serialized = JSON.stringify(chart);
assert.doesNotMatch(serialized, /
{
assert.equal(parseReportChartBlock("{"), null);
assert.equal(parseReportChartBlock(JSON.stringify({ ...fixture.blocks[0], extra: true })), null);
assert.equal(parseReportChartBlock(JSON.stringify({ ...fixture.blocks[0], ascendant: { ...fixture.blocks[0].ascendant, degree: 31 } })), null);
assert.equal(parseReportChartBlock(JSON.stringify({
...fixture.blocks[0],
planets: [{ name: "Uranus", sign: "Aries", degree: 1, retrograde: false }],
})), null);
});
test("stripReportChartBlocks removes fences and keeps engine SVGs", () => {
const markdown = wrapMarkdown(fixture.blocks);
const stripped = stripReportChartBlocks(markdown);
assert.doesNotMatch(stripped, /```jyotish-chart/);
assert.equal((stripped.match(/