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