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.
133 lines
7.4 KiB
TypeScript
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);
|
|
});
|