Files
Jyotisha/frontend/tests/birth-sky-route.test.ts
T
Jesse_ChenandClaude Opus 5.5 dec36bce01 feat(birth-sky): poster-only dialog, date/time/city caption, slower reveal, paywall on first refused send (P1–P5, BUG-1080)
TASK-birth-sky-polish-20260928.
- P1: chart page dialog shows only the picture on a dimmed stage, 「保存图片」 below, icon × in the corner.
- P2: BFF returns one server-built `caption` (「1994年5月18日 07:40 · 杭州」); printed under the fact line in all three places; adopted time adds 「 · 校正后」.
- P3: convergence 3 s + words 2.5 s + fade 1 s; settled frame still equals the exported cover.
- P4: redeem paywall no longer opens when the starter home appears; first send refused for credits opens it, later ones open billing; convergence dialog portalled to <body>. BUG-1080 investigating (Chrome repro rules out the overlay covering the convergence).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-09-29 00:03:46 +08:00

160 lines
7.0 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);
});
// 原值: 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\(/);
}
});