import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import React from "react"; import { renderToStaticMarkup } from "react-dom/server"; import test from "node:test"; import { ChartVedicTab, VargaTable } from "../src/components/chart-page/chart-vedic-tab.tsx"; import { assembleChartView } from "../src/lib/chart-view-load.ts"; import { chartViewOkSchema, chartViewVargaSchema, type ChartViewOk, type ChartViewVarga } from "../src/lib/chart-view-contract.ts"; import { buildChartView } from "../src/lib/chart-view-mapper.ts"; Object.assign(globalThis, { React }); // Real engine output (see the fixture's sourceCommand); nothing here is hand-shaped. const golden = JSON.parse( readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"), ) as { chart: Record; varga_full: { result: Record; planets: Record> }> }; chara: Record; }; const profile = { name: "示例", date: "1990-06-15", time: "12:00", placeLabel: "北京", latitude: 39.9042, longitude: 116.4074, timezoneOffset: 8, timezoneId: "Asia/Shanghai", ayanamsa: "raman", birthTimeStatus: "confirmed", }; async function vargaView(): Promise { const result = await assembleChartView({ userId: "user-1", profile, asOf: "2026-09-15", layers: ["varga"], postEngine: async (path) => { if (path === "/api/chart") return { status: "ok", payload: golden.chart }; if (path === "/api/varga_full") return { status: "ok", payload: golden.varga_full as unknown as Record }; return { status: "http_error", path, elapsedMs: 1, httpStatus: 500 }; }, }); assert.equal(result.body.status, "ok"); return result.body as ChartViewOk; } function render(view: ChartViewOk, vargaId: string): string { return renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId, onSelectVarga: () => {} })); } /** The table row whose header cell names this body. */ function row(markup: string, label: string): string { const tbody = markup.match(/([\s\S]*?)<\/tbody>/)?.[1] ?? ""; const hit = [...tbody.matchAll(/([\s\S]*?)<\/tr>/g)].map((match) => match[1]!).find((cells) => { const header = cells.match(/([\s\S]*?)<\/th>/)?.[1] ?? ""; return header.includes(label); }); assert.ok(hit, `no row for ${label}`); return hit.replace(//g, ""); } function degreeMinutes(degree: number): string { const whole = Math.floor(degree); const minutes = Math.floor((degree - whole) * 60); return `${whole}°${String(minutes).padStart(2, "0")}′`; } // BUG-1076: the D9 caption changed but every row stayed D1. test("a divisional chart's table carries that chart's signs, degrees and houses", async () => { const view = await vargaView(); const d9 = golden.varga_full.result.D9_Navamsa!; const d1Markup = render(view, "D1"); const d9Markup = render(view, "D9"); const sunD9 = row(d9Markup, "太阳"); const moonD9 = row(d9Markup, "月亮"); assert.match(sunD9, /天秤/, "D9 Sun is in Libra"); assert.ok(sunD9.includes(degreeMinutes(Number(d9.planets.Sun!.degree))), "D9 Sun degree is the in-varga degree"); assert.match(moonD9, /双鱼/, "D9 Moon is in Pisces"); assert.match(moonD9, />4宫 { const view = await vargaView(); const markup = render(view, "D1"); const table = markup.match(/
[\s\S]*?<\/table><\/div>/)?.[0]; // Rendered from the baseline commit 5dc977ef with the same golden. const baseline = readFileSync(new URL("./fixtures/chart-d1-table-baseline.html", import.meta.url), "utf8"); assert.equal(table, baseline); }); test("divisional rows carry engine dignity and vargottama, and drop nakshatra columns", async () => { const view = await vargaView(); const d9 = view.vedic.vargas.find((item) => item.id === "D9"); assert.ok(d9?.rows); const saturn = d9.rows.find((item) => item.name === "Saturn"); // D1 Saturn is in Capricorn and D9 Saturn is in Capricorn: own sign, vargottama. assert.equal(saturn?.sign, "Capricorn"); assert.equal(saturn?.dignity, "own"); assert.equal(saturn?.vargottama, true); const sun = d9.rows.find((item) => item.name === "Sun"); assert.equal(sun?.sign, "Libra"); assert.equal(sun?.dignity, "debilitated"); assert.equal(sun?.vargottama, false); const markup = render(view, "D9"); const header = markup.match(/([\s\S]*?)<\/thead>/)?.[1] ?? ""; for (const column of ["行星", "分盘星座", "宫位", "本命星座", "庙旺", "同位"]) assert.match(header, new RegExp(`>${column}<`)); assert.doesNotMatch(header, /星宿|宿主|pada/); assert.match(row(markup, "土星"), />自宫<[\s\S]*>同位落陷不适用([\s\S]*?)<\/thead>/)?.[1] ?? ""; assert.equal(header.includes(">庙旺<"), dignity); assert.equal(header.includes(">同位<"), vargottama); const count = 4 + Number(dignity) + Number(vargottama); assert.equal((header.match(/([\s\S]*?)<\/tbody>/)?.[1] ?? ""; const rows = [...body.matchAll(/([\s\S]*?)<\/tr>/g)]; assert.equal(rows.length, 10, "the ascendant and all nine planets remain"); for (const [, cells] of rows) assert.equal((cells!.match(/<(?:th|td)\b/g) ?? []).length, count); } test("server capabilities retain dignity for every Dn and vargottama only for D9", async () => { const view = await vargaView(); for (const varga of view.vedic.vargas) { if (varga.id === "D1") { assert.equal(varga.supportsDignity, undefined); assert.equal(varga.supportsVargottama, undefined); continue; } assert.equal(varga.supportsDignity, true, varga.id); assert.equal(varga.supportsVargottama, varga.id === "D9", varga.id); assertColumns(tableMarkup(varga), true, varga.id === "D9"); } }); test("capability columns are independent and absent declarations hide whole columns", async () => { const view = await vargaView(); const source = view.vedic.vargas.find((varga) => varga.id === "D9")!; for (const dignity of [undefined, false, true]) { for (const vargottama of [undefined, false, true]) { const copy = structuredClone(source); delete copy.supportsDignity; delete copy.supportsVargottama; if (dignity !== undefined) copy.supportsDignity = dignity; if (vargottama !== undefined) copy.supportsVargottama = vargottama; assertColumns(tableMarkup(chartViewVargaSchema.parse(copy)), dignity === true, vargottama === true); } } }); test("valid false and null results never turn supported columns off", async () => { const view = await vargaView(); const copy = structuredClone(view.vedic.vargas.find((varga) => varga.id === "D9")!); for (const item of copy.rows!) { item.dignity = null; item.vargottama = false; } copy.ascendant!.vargottama = false; const markup = tableMarkup(chartViewVargaSchema.parse(copy)); assertColumns(markup, true, true); assert.doesNotMatch(markup.match(/([\s\S]*?)<\/tbody>/)?.[1] ?? "", />同位<|>入旺<|>自宫<|>落陷不适用 { const view = await vargaView(); const source = view.vedic.vargas.find((varga) => varga.id === "D9")!; for (const field of ["supportsDignity", "supportsVargottama"] as const) { for (const value of [null, "true", "false", 1, 0, [], {}]) { const copy = structuredClone(source); Object.assign(copy, { [field]: value }); const parsed = chartViewVargaSchema.safeParse(copy); assert.equal(parsed.success, false, `${field} must not coerce ${JSON.stringify(value)}`); if (!parsed.success) assert.ok(parsed.error.issues.some((issue) => issue.path[0] === field && issue.code === "invalid_type")); assertColumns(tableMarkup(copy), field !== "supportsDignity", field !== "supportsVargottama"); } } }); test("missing or invalid source positions are not manufactured as negative same-sign results", () => { const chart = structuredClone(golden.chart); const varga = structuredClone(golden.varga_full); const d9 = varga.result.D9_Navamsa!; delete d9.planets.Sun!.sign; d9.planets.Moon!.sign = "not-a-sign"; const view = buildChartView({ chart, varga: varga as unknown as Record, chara: null, western: null, profile, asOf: "2026-09-15" }); const projected = view.vedic.vargas.find((item) => item.id === "D9")!; assert.equal(projected.supportsDignity, true); assert.equal(projected.supportsVargottama, true); assert.equal(projected.rows!.some((item) => item.name === "Sun" || item.name === "Moon"), false); // Destroy only the source positions on a golden copy, rather than making a // response shape. With no valid result source no capability is advertised. for (const planet of Object.values(d9.planets)) delete planet.sign; delete (chart.ascendant as Record).sign; const empty = buildChartView({ chart, varga: varga as unknown as Record, chara: null, western: null, profile, asOf: "2026-09-15" }); const emptyD9 = empty.vedic.vargas.find((item) => item.id === "D9")!; assert.deepEqual(emptyD9.rows, []); assert.equal(emptyD9.ascendant, undefined); assert.equal(emptyD9.supportsDignity, false); assert.equal(emptyD9.supportsVargottama, false); }); test("view projection omits engine branding but old profile fields still parse and raw metadata stays intact", async () => { const before = structuredClone(golden); const view = await vargaView(); assert.equal("engineName" in view.profile, false); const legacy = structuredClone(view); Object.assign(legacy.profile, { engineName: "Swiss Ephemeris(本站)" }); assert.equal("engineName" in chartViewOkSchema.parse(legacy).profile, false); assert.deepEqual(golden, before, "the mapper never strips or changes the raw engine packet"); }); test("divisional house cells do not override their header's left alignment", async () => { const view = await vargaView(); for (const varga of view.vedic.vargas.filter((item) => item.id !== "D1")) { const markup = tableMarkup(varga); const rows = [...markup.matchAll(/([\s\S]*?)<\/tr>/g)].slice(1); assert.equal(rows.length, 10); for (const [, cells] of rows) { const tds = [...cells!.matchAll(/]*)>([\s\S]*?)<\/td>/g)]; assert.equal(tds[1]![1], "", "house cells inherit the same alignment and padding as the header"); assert.match(tds[1]![2]!, /^\d+宫$/); assert.match(tds[3]![1]!, /chart-page-planet-mid/, "the dignity column's deliberate centering remains"); } } });