Open /chart from the sidebar without billing or a model. Five tabs; Western and Qizheng stay unavailable until those endpoints land. Ephemeris nav points at /ephemeris (404 until that page ships).
153 lines
5.8 KiB
TypeScript
153 lines
5.8 KiB
TypeScript
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 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("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/);
|
||
});
|