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 = {}) { 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); }); // 原值: the JSON carries no date / time / place key and none of 1994, 07:40, 30.27, 120.15, 虚构, Asia/Shanghai. // 新值: date, time and city appear only inside the finished `caption` string; outside it the JSON is unchanged, // and no key or value anywhere carries coordinates, timezone, district, province or name. // 原因: TASK-birth-sky-polish-20260928 P2 — product decided the cover prints date + time + city. test("a complete profile returns the contract; birth data leaves only as the caption line", 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.equal(result.body.caption, "1994年5月18日 07:40 · 杭州"); 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 ["30.27", "120.15", "虚构", "Asia/Shanghai", "西湖区", "浙江", "UTC"]) { assert.ok(!json.includes(value), value); } const outside = JSON.stringify({ ...result.body, caption: null }); for (const value of ["1994", "07:40", "杭州"]) { assert.ok(!outside.includes(value), value); } }); test("an uncertain city stops the caption at the time", async () => { const { deps: input } = deps({ loadSubject: (userId, subjectId) => resolveSubjectBirth({ userId, subjectId, loadSelfRow: async () => ({ ...selfRow, birth_place_label: "虚构地点" }), loadOtherRow: async () => null, }), }); const result = await assembleBirthSky(input); assert.equal(result.body.status === "ok" ? result.body.caption : null, "1994年5月18日 07:40"); }); 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\(/); } });