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
138 lines
5.8 KiB
TypeScript
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\(/);
|
|
}
|
|
});
|