Files
Jyotisha/frontend/tests/adopted-birth-date.test.ts
T
jesse-ux b85c4a686a
Independent Staging Quality Gate / validate (push) Successful in 13m27s
Independent Staging Quality Gate / publish (push) Failing after 1h0m1s
fix(rectification): anchor candidate windows to civil dates across midnight
Carry explicit local date intervals instead of inferring the day from clock
order. Cluster width, delivery, adoption, and reports keep the actual civil
date; adopted date is stored separately from the reported birth_date.

Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation
thresholds, and Skill version are unchanged. Isolated Linux final-3 gates
passed; four pre-existing Python failures remain. This is not a production
release.
2026-09-21 02:55:00 +08:00

133 lines
7.4 KiB
TypeScript

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");
const component = readFileSync(new URL("../src/components/chart-library-panel.tsx", import.meta.url), "utf8");
assert.match(component, /effectiveBirthDate\(record\.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<string, unknown>;
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);
});