import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { assembleChartView } from "../src/lib/chart-view-load.ts"; import { chartViewOkSchema, chartViewResponseSchema } from "../src/lib/chart-view-contract.ts"; import { COORDINATE_BOUNDARY } from "../src/lib/chart-view-labels.ts"; const golden = JSON.parse( readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"), ) as { chart: Record; varga_full: Record; chara: Record; western: Record; }; const routeSource = readFileSync(new URL("../src/app/api/chart-view/route.ts", import.meta.url), "utf8"); const serviceSource = readFileSync(new URL("../src/lib/chart-view-service.ts", import.meta.url), "utf8"); const loadSource = readFileSync(new URL("../src/lib/chart-view-load.ts", import.meta.url), "utf8"); const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); 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" as const, }; function engine(overrides: Record | null> = {}) { const calls: string[] = []; const postEngine = async (path: string) => { calls.push(path); if (path in overrides) return overrides[path]; if (path === "/api/chart") return golden.chart; if (path === "/api/varga_full") return golden.varga_full; if (path === "/api/dasha/chara") return golden.chara; if (path === "/api/western") return null; if (path === "/api/qizheng") return null; return null; }; return { calls, postEngine }; } test("the chart-view BFF never bills and never calls a model", () => { for (const source of [routeSource, serviceSource, loadSource]) { assert.doesNotMatch(source, /consultation-billing/); assert.doesNotMatch(source, /@\/mastra/); assert.doesNotMatch(source, /authorizeUsage|completeUsage|releaseUsage/); } assert.match(routeSource, /export async function GET/); assert.match(serviceSource, /createServerSupabaseClient/); assert.match(serviceSource, /ACCOUNT_BIRTH_SELECT/); assert.match(loadSource, /\/api\/chart/); assert.match(loadSource, /\/api\/dasha\/chara/); assert.match(loadSource, /\/api\/varga_full/); assert.match(loadSource, /planets/); assert.match(loadSource, /ascendant/); assert.match(loadSource, /houses/); }); test("unauthenticated chart-view requests are 401", async () => { const { calls, postEngine } = engine(); const result = await assembleChartView({ userId: null, profile, postEngine, asOf: "2026-09-15", }); assert.equal(result.httpStatus, 401); assert.equal(result.body.status, "unauthenticated"); assert.equal(result.body.billed, false); assert.equal(calls.length, 0); }); test("an incomplete birth profile returns a structured message, not 500", async () => { const { calls, postEngine } = engine(); const result = await assembleChartView({ userId: "user-1", profile: null, postEngine, asOf: "2026-09-15", }); assert.equal(result.httpStatus, 200); assert.equal(result.body.status, "birth_profile_incomplete"); assert.notEqual(result.httpStatus, 500); assert.match(result.body.message, /出生资料/); assert.equal(calls.length, 0); }); test("western and qizheng stay unavailable without failing the Indian tabs", async () => { const { calls, postEngine } = engine(); const result = await assembleChartView({ userId: "user-1", profile, postEngine, asOf: "2026-09-15", }); assert.equal(result.httpStatus, 200); const body = chartViewOkSchema.parse(result.body); assert.equal(body.billed, false); assert.ok(body.vedic.vargas.some((item) => item.id === "D1")); assert.ok(body.vedic.planets.length >= 9); assert.equal(body.dasha.vimshottari.method, "Vimshottari"); assert.equal(body.dasha.chara.title, "Chara Dasha(kn_rao 变体)"); assert.equal(body.western.status, "unavailable"); assert.equal(body.qizheng.status, "unavailable"); assert.ok(body.vedic.vargas.some((item) => item.id === "D9")); assert.equal(calls.includes("/api/western"), true); assert.equal(calls.includes("/api/qizheng"), true); assert.doesNotMatch(calls.join(" "), /billing|mastra|consult/); }); test("a real western engine packet lights the tropical tab without mixing sidereal longitudes", async () => { const { postEngine } = engine({ "/api/western": golden.western, "/api/qizheng": null }); const result = await assembleChartView({ userId: "user-1", profile, postEngine, asOf: "2026-09-15", }); const body = chartViewOkSchema.parse(result.body); assert.equal(body.western.status, "ok"); if (body.western.status !== "ok") throw new Error("expected western ok"); assert.equal(body.western.zodiac, "tropical"); assert.equal(body.western.houses.length, 12); assert.match(body.western.boundary, /回归黄道/); assert.notEqual(body.western.ascendantLongitude, body.vedic.planets[0]?.degreeInSign); assert.equal(body.qizheng.status, "unavailable"); }); test("qizheng ketuMode follows the engine field, not a request echo", async () => { const qizheng = { coordinate_system: "qizheng_mansion_degrees_from_jiao", calculation: { ketu_mode: "descending-node", engine_ketu_mode: "apogee" }, palaces: [{ name: "午", branch: "午", lifePalace: "命宫", mansions: ["星"], occupants: [] }], bodies: { sun: { mansion: "奎", siderealLon: 177.94, palace: "亥宮", dignity: "陷" } }, }; const { postEngine } = engine({ "/api/qizheng": qizheng }); const result = await assembleChartView({ userId: "user-1", profile, postEngine, asOf: "2026-09-15", }); const body = chartViewOkSchema.parse(result.body); assert.equal(body.qizheng.status, "ok"); if (body.qizheng.status !== "ok") throw new Error("expected qizheng ok"); assert.equal(body.qizheng.ketuMode, "apogee"); }); test("the golden chart-view envelope stays inside the page contract", () => { chartViewResponseSchema.parse({ status: "birth_profile_incomplete", billed: false, message: "还没有可用来排盘的出生资料。", }); assert.match(COORDINATE_BOUNDARY.vedic, /不能换算/); assert.match(COORDINATE_BOUNDARY.western, /不能换算/); assert.match(COORDINATE_BOUNDARY.qizheng, /角宿/); }); test("page.tsx does not grow to host the chart page", () => { assert.ok((pageSource.match(/\n/g) ?? []).length <= 1951); assert.doesNotMatch(pageSource, /chart-page|ChartPageView|\/api\/chart-view/); });