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(/ { const total = HOUSE_NUMBERS.reduce((sum, houseNumber) => sum + polygonArea(HOUSE_POLYGONS[houseNumber]), 0); assert.ok(Math.abs(total - CHART_SIZE * CHART_SIZE) < 1e-6, `area ${total}`); const step = 5; for (let x = step; x < CHART_SIZE; x += step) { for (let y = step; y < CHART_SIZE; y += step) { const hits = HOUSE_NUMBERS.filter((houseNumber) => pointInPolygon({ x, y }, HOUSE_POLYGONS[houseNumber])); assert.ok(hits.length <= 1, `point ${x},${y} in ${hits.join(",")}`); } } });