Longform report pages kept the D1/D9/Moon and varga headings but skipHtml stripped the engine's inline SVG. Emit a jyotish-chart JSON fence beside each SVG and render a diamond chart on the reader without relaxing HTML sanitization. BUG-607. Co-authored-by: Cursor <cursoragent@cursor.com>
114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
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}`,
|
|
"",
|
|
`<svg viewBox="0 0 420 480"></svg>`,
|
|
"",
|
|
"```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: '<img onerror="alert(1)" src=x>',
|
|
}));
|
|
assert.ok(poisoned);
|
|
const chart = toNorthIndianChart(poisoned);
|
|
const serialized = JSON.stringify(chart);
|
|
assert.doesNotMatch(serialized, /<img/);
|
|
assert.doesNotMatch(serialized, /onerror/);
|
|
});
|
|
|
|
test("invalid payloads return null", () => {
|
|
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(/<svg/g) ?? []).length, (markdown.match(/<svg/g) ?? []).length);
|
|
});
|
|
|
|
test("house polygons fill the square without overlapping interiors", () => {
|
|
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(",")}`);
|
|
}
|
|
}
|
|
});
|