Files
Jyotisha/frontend/tests/birth-sky-route.test.ts
T
Jesse_ChenandClaude Opus 5.5 d277cbd8ad feat(chart): 「那一刻的天空」封面 — engine, BFF, canvas painter, chart header entry
T1 scripts/birth_sky.py: real alt/az of the seven visible grahas (swe.calc_ut +
swe.azalt, geometric altitude, azimuth converted from swe's south origin to
north-east) and Yale BSC5 stars V<=4.0 (IAU 1976 precession); phase
day / twilight_morning / twilight_evening / night. Thin POST /api/birth_sky
registration in do_POST (no new handler method, no __new__).

T2 GET /api/birth-sky?subject=: birth truth read server side through
loadSubjectBirth + prepareChartViewProfile; zod contract; no billing, no
writes, no birth data in the response.

T3/T4 lib/birth-sky: sentence table, pure Canvas 2D drawBirthSky (night /
paper palettes from DESIGN tokens, east on the left, serif headline with
1.5 s font wait), PNG export via navigator.share or Blob + a.download.
Chart page prefetches and paints the cover once the chart is ok; the header
button appears only when the PNG is ready. Dialog is a lazy chunk.

T7 DESIGN §17, VOICE, CONTEXT glossary, CHANGELOG, real-device checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
2026-09-28 20:27:40 +08:00

138 lines
5.8 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { birthSkyResponseSchema } from "../src/lib/birth-sky/contract.ts";
import { assembleBirthSky, type BirthSkyDeps, type BirthSkyEngineBody } from "../src/lib/birth-sky/service-core.ts";
import { ConsultationSubjectError, resolveSubjectBirth } from "../src/lib/subject-birth.ts";
import { birthSkyGolden } from "./birth-sky-test-support.ts";
const routeSource = readFileSync(new URL("../src/app/api/birth-sky/route.ts", import.meta.url), "utf8");
const serviceSource = readFileSync(new URL("../src/lib/birth-sky/service.ts", import.meta.url), "utf8");
const coreSource = readFileSync(new URL("../src/lib/birth-sky/service-core.ts", import.meta.url), "utf8");
const otherId = "22222222-2222-4222-8222-222222222222";
// Fictional sample from the brief; never a real person.
const selfRow = {
name: "虚构甲",
birth_date: "1994-05-18",
reported_birth_time: "07:40:00",
birth_time_status: "reported",
latitude: 30.27,
longitude: 120.15,
timezone_offset: 8,
timezone_id: "Asia/Shanghai",
birth_place_label: "虚构地点",
ayanamsa: "lahiri",
};
function deps(overrides: Partial<BirthSkyDeps> = {}) {
const engineCalls: BirthSkyEngineBody[] = [];
const base: BirthSkyDeps = {
userId: "user-1",
subjectId: "self",
loadSubject: (userId, subjectId) => resolveSubjectBirth({
userId,
subjectId,
loadSelfRow: async () => selfRow,
loadOtherRow: async (id) => ({ ...selfRow, id, user_id: "someone-else", role: "other" }),
}),
postEngine: async (body) => {
engineCalls.push(body);
return birthSkyGolden.responses.day;
},
};
return { deps: { ...base, ...overrides }, engineCalls };
}
test("signed-out requests are 401 and never reach the engine", async () => {
const { deps: input, engineCalls } = deps({ userId: null });
const result = await assembleBirthSky(input);
assert.equal(result.httpStatus, 401);
assert.deepEqual(result.body, { status: "unauthenticated" });
assert.equal(engineCalls.length, 0);
});
test("someone else's subject is 404 and never reaches the engine", async () => {
const { deps: input, engineCalls } = deps({ subjectId: otherId });
const result = await assembleBirthSky(input);
assert.equal(result.httpStatus, 404);
assert.deepEqual(result.body, { status: "not_found" });
assert.equal(engineCalls.length, 0);
});
test("an incomplete profile says so, so the page shows no entry", async () => {
const { deps: incompleteSelf } = deps({
loadSubject: (userId, subjectId) => resolveSubjectBirth({
userId,
subjectId,
loadSelfRow: async () => ({ ...selfRow, latitude: null, longitude: null }),
loadOtherRow: async () => null,
}),
});
const self = await assembleBirthSky(incompleteSelf);
assert.equal(self.httpStatus, 200);
assert.deepEqual(self.body, { status: "birth_profile_incomplete" });
const { deps: incompleteOther, engineCalls } = deps({
loadSubject: async () => { throw new ConsultationSubjectError("subject_incomplete"); },
});
const other = await assembleBirthSky(incompleteOther);
assert.deepEqual(other.body, { status: "birth_profile_incomplete" });
assert.equal(engineCalls.length, 0);
});
test("a complete profile returns the contract and leaks no birth data", async () => {
const { deps: input, engineCalls } = deps();
const result = await assembleBirthSky(input);
assert.equal(result.httpStatus, 200);
assert.ok(birthSkyResponseSchema.safeParse(result.body).success);
assert.equal(result.body.status, "ok");
if (result.body.status !== "ok") return;
assert.equal(result.body.phase, "day");
assert.equal(result.body.timeBasis, "reported");
assert.deepEqual(engineCalls, [{ date: "1994-05-18", time: "07:40", timezone_offset: 8, latitude: 30.27, longitude: 120.15 }]);
const json = JSON.stringify(result.body);
for (const key of ["latitude", "longitude", "date", "time", "timezone", "birth_place_label", "placeLabel", "name"]) {
assert.doesNotMatch(json, new RegExp(`"${key}"`), key);
}
for (const value of ["1994", "07:40", "30.27", "120.15", "虚构", "Asia/Shanghai"]) {
assert.ok(!json.includes(value), value);
}
});
test("an adopted birth time is labelled adopted; engine failure shows nothing", async () => {
const { deps: adopted } = deps({
loadSubject: (userId, subjectId) => resolveSubjectBirth({
userId,
subjectId,
loadSelfRow: async () => ({ ...selfRow, birth_time_status: "confirmed", active_birth_time: "07:40:00" }),
loadOtherRow: async () => null,
}),
});
const ok = await assembleBirthSky(adopted);
assert.equal(ok.body.status === "ok" ? ok.body.timeBasis : null, "adopted");
const { deps: failing } = deps({ postEngine: async () => { throw new Error("down"); } });
const failed = await assembleBirthSky(failing);
assert.equal(failed.httpStatus, 503);
assert.deepEqual(failed.body, { status: "unavailable" });
const { deps: malformed } = deps({ postEngine: async () => ({ phase: "day" }) });
assert.deepEqual((await assembleBirthSky(malformed)).body, { status: "unavailable" });
});
test("the route reads only ?subject= and loads birth truth on the server; no billing", () => {
assert.match(routeSource, /export async function GET/);
assert.match(routeSource, /searchParams\.get\("subject"\)/);
assert.doesNotMatch(routeSource, /searchParams\.get\("(lat|lon|latitude|longitude|date|time|tz)/);
assert.doesNotMatch(routeSource, /request\.json\(\)/);
assert.match(serviceSource, /loadSubjectBirth/);
assert.match(serviceSource, /createServerSupabaseClient/);
assert.match(coreSource, /prepareChartViewProfile/);
for (const source of [routeSource, serviceSource, coreSource]) {
assert.doesNotMatch(source, /consultation-billing|authorizeUsage|completeUsage|@\/mastra|\.insert\(|\.update\(|\.upsert\(/);
}
});