Files
Jyotisha/frontend/tests/ephemeris-route.test.ts
T
jesse-ux 62be52dacf
Independent Staging Quality Gate / validate (push) Canceled after 2m16s
Independent Staging Quality Gate / publish (push) Canceled after 0s
fix(chart): skip VedAstro on chart-page first paint
The natal /api/chart request from the chart page now sends skip_vedastro_main_entry_overview, matching the ephemeris page. Mapper and contract never read that evidence. BUG-718 recurs from BUG-161. Tests lock both foreground natal paths.
2026-09-16 00:22:00 +08:00

229 lines
9.3 KiB
TypeScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { parseEphemerisOkResponse } from "../src/lib/ephemeris-contract.ts";
import {
eventsFromEngine,
natalHouseFromSigns,
panchangaFactorsFromEngine,
transitsFromEngine,
} from "../src/lib/ephemeris-view.ts";
const routeSource = readFileSync(new URL("../src/app/api/ephemeris/route.ts", import.meta.url), "utf8");
const viewSource = readFileSync(new URL("../src/lib/ephemeris-view.ts", import.meta.url), "utf8");
const frontendRoot = fileURLToPath(new URL("../", import.meta.url));
const panchangaGolden = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-panchanga-range.golden.json", import.meta.url), "utf8"));
const natalGolden = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-natal.golden.json", import.meta.url), "utf8"));
const transitGolden = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-transit-2026-09-15.golden.json", import.meta.url), "utf8"));
const completeRow = {
name: "Example",
birth_date: "1990-04-09",
reported_birth_time: "13:24",
active_birth_time: "13:24",
birth_time: "13:24",
birth_time_status: "confirmed",
latitude: 31.19,
longitude: 121.44,
timezone_offset: 8,
timezone_id: "Asia/Shanghai",
ayanamsa: "raman",
};
function executeRoute(input: {
readonly user: boolean;
readonly row: Record<string, unknown> | null;
readonly date?: string;
readonly eventsStatus?: number;
}) {
const script = String.raw`
import { mock } from "node:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
const moduleUrl = (path) => pathToFileURL(process.cwd() + "/" + path).href;
const user = ${input.user ? "true" : "false"};
const row = ${JSON.stringify(input.row)};
const date = ${JSON.stringify(input.date ?? "2026-09-15")};
const eventsStatus = ${input.eventsStatus ?? 404};
const fixture = (name) => JSON.parse(readFileSync(join(process.cwd(), "tests/fixtures", name), "utf8"));
const panchanga = fixture("ephemeris-panchanga-range.golden.json");
const natal = fixture("ephemeris-chart-natal.golden.json");
const transit = fixture("ephemeris-chart-transit-2026-09-15.golden.json");
let billingImported = false;
mock.module("@/lib/consultation-billing", {
namedExports: {
charge() { billingImported = true; throw new Error("billing must not run"); },
},
});
mock.module("@/mastra", {
namedExports: {
getAgent() { throw new Error("mastra must not run"); },
},
});
mock.module("@/lib/request-rate-limit", {
namedExports: {
consumeUserRequestRateLimit: () => ({ ok: true }),
},
});
mock.module("@/lib/daily-starlanguage", {
namedExports: {
calendarDateInTimeZone: () => "2026-09-15",
},
});
mock.module("@/lib/supabase/server", {
namedExports: {
createServerSupabaseClient: async () => ({
auth: { getUser: async () => ({ data: { user: user ? { id: "user-1" } : null }, error: null }) },
from: () => ({
select() { return this; },
eq() { return this; },
async maybeSingle() { return { data: row, error: null }; },
}),
}),
},
});
const upstream = [];
globalThis.fetch = async (inputUrl, init) => {
const url = String(inputUrl);
const body = init?.body ? JSON.parse(String(init.body)) : {};
upstream.push({ url, body });
if (url.endsWith("/api/panchanga_range")) {
return new Response(JSON.stringify(panchanga), { status: 200, headers: { "content-type": "application/json" } });
}
if (url.endsWith("/api/chart")) {
const packet = Number(body.year) === 1990 ? natal : transit;
return new Response(JSON.stringify(packet), { status: 200, headers: { "content-type": "application/json" } });
}
if (url.endsWith("/api/ephemeris_events")) {
return new Response(JSON.stringify({ error: "not_found" }), { status: eventsStatus, headers: { "content-type": "application/json" } });
}
throw new Error("unexpected_upstream " + url);
};
const { GET } = await import(moduleUrl("src/app/api/ephemeris/route.ts"));
const response = await GET(new Request("https://staging.jyotisha.chat/api/ephemeris?date=" + date));
console.log(JSON.stringify({
status: response.status,
body: await response.json(),
upstream,
billingImported,
}));
`;
const result = spawnSync(process.execPath, [
"--experimental-test-module-mocks",
"--import",
"tsx",
"--input-type=module",
"--eval",
script,
], { cwd: frontendRoot, encoding: "utf8" });
assert.equal(result.status, 0, result.stderr || result.stdout);
const line = result.stdout.trim().split("\n").at(-1) ?? "{}";
return JSON.parse(line) as {
status: number;
body: Record<string, unknown>;
upstream: Array<{ url: string; body: Record<string, unknown> }>;
billingImported: boolean;
};
}
test("the ephemeris BFF does not import billing or mastra", () => {
assert.match(routeSource, /createServerSupabaseClient/);
assert.match(routeSource, /ACCOUNT_BIRTH_SELECT/);
assert.match(routeSource, /globalBirthProfileFromAccountRow/);
assert.match(routeSource, /consumeUserRequestRateLimit\("ephemeris"/);
assert.match(routeSource, /"\/api\/panchanga_range"/);
assert.match(routeSource, /"\/api\/chart"/);
assert.match(routeSource, /"\/api\/ephemeris_events"/);
assert.match(routeSource, /eventsFromEngine\(eventsRaw\)/);
assert.doesNotMatch(routeSource, /consultation-billing|@\/mastra/);
assert.doesNotMatch(viewSource, /consultation-billing|@\/mastra/);
});
test("ephemeris natal and transit chart requests skip VedAstro overview", () => {
assert.equal((routeSource.match(/skip_vedastro_main_entry_overview:\s*true/g) ?? []).length, 2);
const result = executeRoute({ user: true, row: completeRow });
const chartCalls = result.upstream.filter((item) => item.url.endsWith("/api/chart"));
assert.ok(chartCalls.length >= 2);
for (const call of chartCalls) {
assert.equal(call.body.skip_vedastro_main_entry_overview, true);
}
});
test("unauthenticated ephemeris requests are 401", () => {
const result = executeRoute({ user: false, row: null });
assert.equal(result.status, 401);
assert.equal(result.body.status, "unauthenticated");
assert.equal(result.upstream.length, 0);
assert.equal(result.billingImported, false);
});
test("panchanga still returns without a birth profile, and natal-relative stays empty", () => {
const result = executeRoute({ user: true, row: { timezone_id: "Asia/Shanghai" } });
assert.equal(result.status, 200);
const parsed = parseEphemerisOkResponse(result.body);
assert.ok(parsed);
assert.equal(parsed?.panchanga.available, true);
assert.equal(parsed?.panchanga.factors.length, 5);
assert.equal(parsed?.transits.available, false);
assert.equal(parsed?.transits.natalRelativeAvailable, false);
assert.equal(parsed?.transits.natalRelativeNote, "missing_natal");
assert.equal(parsed?.events.status, "unavailable");
assert.equal(result.upstream.some((item) => item.url.endsWith("/api/chart")), false);
assert.equal(result.billingImported, false);
});
test("ephemeris_events failure degrades instead of failing the page", () => {
const result = executeRoute({ user: true, row: completeRow, eventsStatus: 404 });
assert.equal(result.status, 200);
const parsed = parseEphemerisOkResponse(result.body);
assert.ok(parsed);
assert.equal(parsed?.panchanga.available, true);
assert.equal(parsed?.transits.available, true);
assert.equal(parsed?.transits.natalRelativeAvailable, true);
assert.equal(parsed?.events.status, "unavailable");
assert.equal(result.billingImported, false);
assert.ok(result.upstream.some((item) => item.url.endsWith("/api/ephemeris_events")));
});
test("golden engine packets pass the narrow response contract", () => {
const factors = panchangaFactorsFromEngine(panchangaGolden, "2026-09-15");
assert.equal(factors.length, 5);
assert.equal(factors.find((item) => item.key === "tithi")?.value, "Shukla Panchami");
assert.equal(factors.find((item) => item.key === "nakshatra")?.detail, "第 4 足");
const transits = transitsFromEngine({ transitChart: transitGolden, natalChart: natalGolden });
assert.equal(transits.bodies.length, 9);
assert.equal(transits.natalRelativeAvailable, true);
const sun = transits.bodies.find((item) => item.planet === "Sun");
assert.equal(sun?.signLabel, "狮子座");
assert.equal(sun?.natalHouse, natalHouseFromSigns("Leo", "Cancer"));
assert.equal(sun?.natalHouse, 2);
const parsed = parseEphemerisOkResponse({
status: "ok",
date: "2026-09-15",
today: "2026-09-15",
timezoneId: "Asia/Shanghai",
panchanga: { available: true, ayanamsaNote: "lahiri_not_account", factors },
transits: {
available: true,
natalRelativeAvailable: true,
natalRelativeNote: "shown",
ayanamsa: "raman",
bodies: transits.bodies,
},
events: { status: "unavailable" },
});
assert.ok(parsed);
assert.equal(eventsFromEngine(null).status, "unavailable");
assert.equal(eventsFromEngine({ error: "not_found" }).status, "unavailable");
});