Files
Jyotisha/frontend/tests/chart-view-route.test.ts
T
jesse-ux a38a941522
Independent Staging Quality Gate / validate (push) Failing after 8m1s
Independent Staging Quality Gate / publish (push) Skipped
fix(qizheng): pass school options into the vendored chart and stabilize gates
BUG-710: call getSevenGovernorsChart with ketuMode/siderealMode so
calculation.ketu_mode is the engine school, not the request echo.
BUG-711: stop forbidding /ephemeris in the sidebar contract.
BUG-712: compare ephemeris event longitudes at 6 decimals and fail
closed when the golden is missing.
2026-09-15 17:54:20 +08:00

173 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, unknown>;
varga_full: Record<string, unknown>;
chara: Record<string, unknown>;
western: Record<string, unknown>;
};
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<string, Record<string, unknown> | 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 Dashakn_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/);
});