import assert from "node:assert/strict"; import test from "node:test"; import { readFileSync } from "node:fs"; import { effectiveBirthDate } from "../src/lib/effective-birth-date.ts"; import { globalBirthProfileFromAccountRow } from "../src/lib/server-owned-birth-profile.ts"; import { resolveMissingBirthTimezoneOffset } from "../src/lib/birth-profile-timezone.ts"; import { accountProfilePatchSchema } from "../src/lib/account-profile-patch.ts"; import { applyPersistedBirthTime } from "../src/lib/birth-time-intake-model.ts"; import { dailyStarlanguageProfileKey } from "../src/lib/daily-starlanguage.ts"; import { readProfile } from "../src/lib/home-profile.ts"; import { prepareConsultationRoute } from "../src/lib/consultation-route-service.ts"; import { parseReportCandidateRange } from "../src/lib/report-candidate-range.ts"; import { resolveReportBirthTimeSensitivityInput } from "../src/lib/personal-report-route-core.ts"; // Synthetic boundary fixtures, not engine confirmation evidence. const profile = { birth_date: "2000-06-15", reported_birth_time: "00:10", active_birth_date: "2000-06-14", active_birth_time: "23:55", active_birth_timezone_offset: -5, birth_time_status: "accepted", latitude: 40, longitude: -74, timezone_id: "America/New_York", timezone_offset: -4, }; test("adopted chart date is independent of the declared date", () => { assert.equal(globalBirthProfileFromAccountRow(profile).date, "2000-06-14"); assert.equal(profile.birth_date, "2000-06-15"); assert.equal(globalBirthProfileFromAccountRow({ ...profile, birth_time_status: "reported" }).date, "2000-06-15"); assert.equal(globalBirthProfileFromAccountRow({ ...profile, active_birth_date: null }).date, "2000-06-15"); }); test("chart library summary uses effective date while its editable profile keeps the declaration", () => { const editable = readProfile(profile); assert.equal(effectiveBirthDate(editable), "2000-06-14"); assert.equal(editable.date, "2000-06-15"); assert.equal(effectiveBirthDate({ ...editable, birthTimeStatus: "reported" }), "2000-06-15"); // 原值 / 新值 / 原因:旧面板 record.profile / 人物页 selected.profile / 退役入口,保留有效日期合同。 const component = readFileSync(new URL("../src/components/people/people-page.tsx", import.meta.url), "utf8"); assert.match(component, /effectiveBirthDate\(selected\.profile\) \|\| "出生日期待补全"/); }); test("adopted chart reuses the persisted candidate calculation offset, not new DST rules", async () => { const result = await resolveMissingBirthTimezoneOffset(profile, { useActiveDate: true, preferredTime: "23:55", fetchImpl: async () => { throw new Error("must not reinterpret candidate timezone"); }, }) as Record; assert.equal(result.timezone_offset, -5); assert.equal(result.birth_date, "2000-06-15"); assert.equal(globalBirthProfileFromAccountRow(profile).timezoneOffset, -5); }); test("fixed-offset legacy profiles do not invent timezone rules", async () => { const fixed = { ...profile, active_birth_date: null, active_birth_timezone_offset: null, timezone_id: null }; assert.equal(await resolveMissingBirthTimezoneOffset(fixed, { useActiveDate: true, fetchImpl: async () => { throw new Error("must not fetch"); }, }), fixed); }); test("cross-day adopted date cannot silently lose its calculation offset", async () => { await assert.rejects(resolveMissingBirthTimezoneOffset({ ...profile, active_birth_timezone_offset: null }, { useActiveDate: true })); }); test("same-day adopted tuple must keep its offset while date-less legacy remains usable", async () => { const partial = { ...profile, active_birth_date: profile.birth_date, active_birth_timezone_offset: null }; assert.throws(() => globalBirthProfileFromAccountRow(partial), /adopted_birth_calculation_incomplete/); await assert.rejects(resolveMissingBirthTimezoneOffset(partial, { useActiveDate: true })); const legacy = { ...partial, active_birth_date: null }; assert.equal(globalBirthProfileFromAccountRow(legacy).timezoneOffset, -4); assert.equal(await resolveMissingBirthTimezoneOffset(legacy, { useActiveDate: true }), legacy); assert.notEqual(dailyStarlanguageProfileKey(readProfile(partial)), dailyStarlanguageProfileKey(readProfile(legacy))); }); test("account write hydration clears old adoption even when a new exact declaration is accepted", () => { const draft = readProfile(profile); assert.equal(draft.activeDate, "2000-06-14"); const updated = applyPersistedBirthTime(draft, { status: "accepted", activeTime: "00:10", activeDate: null, activeTimezoneOffset: null, }); assert.equal(updated.activeDate, undefined); assert.equal(updated.activeTimezoneOffset, undefined); assert.equal(updated.date, "2000-06-15"); assert.notEqual(dailyStarlanguageProfileKey(draft), dailyStarlanguageProfileKey(updated)); assert.notEqual(dailyStarlanguageProfileKey(draft), dailyStarlanguageProfileKey({ ...draft, activeDate: "2000-06-15" })); }); test("new consultation uses the complete adopted tuple and preserves explicit sensitivity intervals", async () => { const candidate_intervals = [{ start_at: "2000-06-14T23:55", end_at: "2000-06-15T00:11" }]; const range = parseReportCandidateRange({ candidate_intervals }); assert.ok(range); const prepared = await prepareConsultationRoute({ userId: "synthetic", mode: "verified_chart", reserve: async () => "reserved", loadProfile: async () => ({ ...profile, name: "Synthetic", birth_time_source: "family_exact", birth_place_label: "Synthetic place" }), loadCandidateRange: async () => range, }); assert.equal(prepared.serverChart?.toolInput.day, 14); assert.equal(prepared.serverChart?.toolInput.hour, 23); assert.equal(prepared.serverChart?.toolInput.tz, -5); assert.deepEqual(prepared.serverChart?.toolInput.candidate_range, { start_time: "23:55", end_time: "00:11", candidate_intervals }); assert.deepEqual(resolveReportBirthTimeSensitivityInput(profile, "accepted", "23:55", range).candidate_range, { start_time: "23:55", end_time: "00:11", candidate_intervals }); }); test("dated report intervals preserve separate segments and reject malformed calendar boundaries", () => { const candidate_intervals = [ { start_at: "2000-06-15T00:00", end_at: "2000-06-15T00:59" }, { start_at: "2000-06-15T23:00", end_at: "2000-06-15T23:59" }, ]; assert.deepEqual(parseReportCandidateRange({ candidate_intervals }), { startTime: "00:00", endTime: "23:59", candidate_intervals, }); for (const intervals of [ [], [...candidate_intervals].reverse(), [{ start_at: "2000-02-30T00:00", end_at: "2000-03-01T00:00" }], [{ start_at: "2000-06-15T24:00", end_at: "2000-06-16T00:00" }], [{ start_at: "2000-06-15T01:00", end_at: "2000-06-15T00:00" }], [candidate_intervals[0], { start_at: "2000-06-15T00:59", end_at: "2000-06-15T01:00" }], ]) assert.equal(parseReportCandidateRange({ candidate_intervals: intervals }), null); assert.equal(parseReportCandidateRange({ start_time: "23:55", end_time: "00:11" }), null, "legacy clock-only range must not guess a calendar date"); }); test("shared mapper fails closed if a cross-day adopted calculation loses its offset", () => { assert.throws(() => globalBirthProfileFromAccountRow({ ...profile, active_birth_timezone_offset: null }), /adopted_birth_calculation_incomplete/); }); test("ordinary account patch cannot claim adopted date or provenance", () => { for (const patch of [ { active_birth_date: "2000-06-14" }, { active_birth_timezone_offset: -5 }, { active_birth_provenance: { source: "fake" } }, ]) assert.equal(accountProfilePatchSchema.safeParse(patch).success, false); });