286 lines
9.7 KiB
TypeScript
286 lines
9.7 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
import {
|
||
applyBirthTimeDraftPatch,
|
||
assistantIntentCopy,
|
||
birthTimeDisplayState,
|
||
birthTimePersistenceValues,
|
||
describeBirthTimeDraft,
|
||
formatBirthDate,
|
||
isDeclaredBirthProfileComplete,
|
||
isBirthTimeReadyForConsultation,
|
||
isBirthTimeDraftReady,
|
||
normalizePersistedBirthDate,
|
||
parseBirthDate,
|
||
type BirthTimeDraft,
|
||
} from "../src/lib/birth-time-intake-model.ts";
|
||
|
||
const emptyDraft: BirthTimeDraft = {
|
||
date: "1993-04-17",
|
||
time: "",
|
||
reportedTime: "",
|
||
birthTimeSource: "",
|
||
birthTimePeriod: "",
|
||
birthTimeClue: "",
|
||
uncertaintyBeforeMinutes: null,
|
||
uncertaintyAfterMinutes: null,
|
||
birthTimeStatus: "",
|
||
};
|
||
|
||
test("birth time intake requires only the fields selected by the source", () => {
|
||
const hospital = {
|
||
...emptyDraft,
|
||
birthTimeSource: "hospital_record",
|
||
reportedTime: "08:16",
|
||
uncertaintyBeforeMinutes: 2,
|
||
uncertaintyAfterMinutes: 2,
|
||
} satisfies BirthTimeDraft;
|
||
const period = {
|
||
...emptyDraft,
|
||
birthTimeSource: "period_only",
|
||
birthTimePeriod: "evening",
|
||
} satisfies BirthTimeDraft;
|
||
const incompleteApproximate = {
|
||
...emptyDraft,
|
||
birthTimeSource: "approximate",
|
||
reportedTime: "14:30",
|
||
} satisfies BirthTimeDraft;
|
||
|
||
assert.equal(isBirthTimeDraftReady(hospital), true);
|
||
assert.equal(isBirthTimeDraftReady(period), true);
|
||
assert.equal(isBirthTimeDraftReady(incompleteApproximate), false);
|
||
assert.equal(isBirthTimeDraftReady({ ...emptyDraft, birthTimeSource: "unknown" }), true);
|
||
});
|
||
|
||
test("an unconfirmed candidate working time remains in rectification onboarding", () => {
|
||
const candidate = {
|
||
...emptyDraft,
|
||
time: "04:53",
|
||
birthTimeStatus: "candidate",
|
||
} satisfies BirthTimeDraft;
|
||
|
||
assert.equal(isBirthTimeReadyForConsultation(candidate), false);
|
||
assert.equal(isBirthTimeReadyForConsultation({ ...candidate, time: "" }), false);
|
||
assert.equal(isBirthTimeReadyForConsultation({ ...candidate, birthTimeStatus: "rectifying" }), false);
|
||
assert.equal(isBirthTimeReadyForConsultation({ ...candidate, birthTimeStatus: "confirmed" }), true);
|
||
});
|
||
|
||
test("declared birth data completes onboarding without an active or confirmed minute", () => {
|
||
const declaredExact = {
|
||
...emptyDraft,
|
||
reportedTime: "05:30",
|
||
birthTimeSource: "family_exact",
|
||
uncertaintyBeforeMinutes: 10,
|
||
uncertaintyAfterMinutes: 10,
|
||
birthTimeStatus: "reported",
|
||
} satisfies BirthTimeDraft;
|
||
const declaredPeriod = {
|
||
...emptyDraft,
|
||
birthTimeSource: "period_only",
|
||
birthTimePeriod: "early_morning",
|
||
birthTimeStatus: "reported",
|
||
} satisfies BirthTimeDraft;
|
||
|
||
assert.equal(isDeclaredBirthProfileComplete(declaredExact), true);
|
||
assert.equal(isBirthTimeReadyForConsultation(declaredExact), false);
|
||
assert.equal(isDeclaredBirthProfileComplete(declaredPeriod), true);
|
||
assert.equal(isBirthTimeReadyForConsultation(declaredPeriod), false);
|
||
});
|
||
|
||
test("declared completeness validates the actual calendar date, clock, source fields, and bounds", () => {
|
||
const exact = {
|
||
...emptyDraft,
|
||
birthTimeSource: "family_exact",
|
||
reportedTime: "05:30",
|
||
uncertaintyBeforeMinutes: 10,
|
||
uncertaintyAfterMinutes: 10,
|
||
} satisfies BirthTimeDraft;
|
||
|
||
assert.equal(isDeclaredBirthProfileComplete({ ...exact, date: "2000-02-29" }), true);
|
||
assert.equal(isDeclaredBirthProfileComplete({ ...exact, date: "2001-02-29" }), false);
|
||
assert.equal(isDeclaredBirthProfileComplete({ ...exact, date: "1899-12-31" }), false);
|
||
assert.equal(isDeclaredBirthProfileComplete({ ...exact, date: "2101-01-01" }), false);
|
||
assert.equal(isDeclaredBirthProfileComplete({ ...exact, reportedTime: "24:00" }), false);
|
||
assert.equal(isDeclaredBirthProfileComplete({ ...exact, reportedTime: "5:30" }), false);
|
||
assert.equal(isDeclaredBirthProfileComplete({ ...exact, uncertaintyBeforeMinutes: 7 }), false);
|
||
assert.equal(isDeclaredBirthProfileComplete({
|
||
...emptyDraft,
|
||
birthTimeSource: "hospital_record",
|
||
reportedTime: "05:30",
|
||
}), false);
|
||
assert.equal(isDeclaredBirthProfileComplete({
|
||
...emptyDraft,
|
||
birthTimeSource: "period_only",
|
||
birthTimePeriod: "",
|
||
}), false);
|
||
assert.equal(isDeclaredBirthProfileComplete({
|
||
...emptyDraft,
|
||
birthTimeSource: "period_only",
|
||
birthTimePeriod: "morning",
|
||
uncertaintyBeforeMinutes: 30,
|
||
}), false);
|
||
assert.equal(isDeclaredBirthProfileComplete({
|
||
...emptyDraft,
|
||
birthTimeSource: "unknown",
|
||
reportedTime: "05:30",
|
||
}), false);
|
||
assert.equal(isDeclaredBirthProfileComplete({
|
||
...emptyDraft,
|
||
birthTimeSource: "unknown",
|
||
birthTimeClue: "x".repeat(241),
|
||
}), false);
|
||
assert.equal(isDeclaredBirthProfileComplete(exact, {
|
||
label: "中国 · 河北省 · 邯郸市",
|
||
lat: 36.62,
|
||
lon: 114.49,
|
||
tz: 8,
|
||
}), true);
|
||
assert.equal(isDeclaredBirthProfileComplete(exact, {
|
||
label: "中国 · 河北省 · 邯郸市 · 峰峰矿区",
|
||
lat: 36.420487,
|
||
lon: 114.209936,
|
||
tz: null,
|
||
timezoneId: "Asia/Shanghai",
|
||
}), true);
|
||
assert.equal(isDeclaredBirthProfileComplete(exact, {
|
||
label: "缺少时区的地点",
|
||
lat: 36.420487,
|
||
lon: 114.209936,
|
||
tz: null,
|
||
timezoneId: "",
|
||
}), false);
|
||
assert.equal(isDeclaredBirthProfileComplete(exact, {
|
||
label: "越界地点",
|
||
lat: 91,
|
||
lon: 114.49,
|
||
tz: 8,
|
||
}), false);
|
||
assert.equal(isDeclaredBirthProfileComplete(exact, null), false);
|
||
});
|
||
|
||
test("editing an unconfirmed declaration invalidates an old candidate but preserves confirmed active time", () => {
|
||
const candidate = {
|
||
...emptyDraft,
|
||
time: "05:18",
|
||
reportedTime: "05:30",
|
||
birthTimeSource: "approximate",
|
||
uncertaintyBeforeMinutes: 30,
|
||
uncertaintyAfterMinutes: 30,
|
||
birthTimeStatus: "candidate",
|
||
} satisfies BirthTimeDraft;
|
||
|
||
assert.deepEqual(applyBirthTimeDraftPatch(candidate, { reportedTime: "06:10" }), {
|
||
...candidate,
|
||
time: "",
|
||
reportedTime: "06:10",
|
||
birthTimeStatus: "reported",
|
||
});
|
||
assert.deepEqual(applyBirthTimeDraftPatch(candidate, {
|
||
uncertaintyBeforeMinutes: 60,
|
||
uncertaintyAfterMinutes: 60,
|
||
}), {
|
||
...candidate,
|
||
time: "",
|
||
uncertaintyBeforeMinutes: 60,
|
||
uncertaintyAfterMinutes: 60,
|
||
birthTimeStatus: "reported",
|
||
});
|
||
|
||
const confirmed = { ...candidate, birthTimeStatus: "confirmed" } satisfies BirthTimeDraft;
|
||
assert.deepEqual(applyBirthTimeDraftPatch(confirmed, { reportedTime: "06:10" }), {
|
||
...confirmed,
|
||
reportedTime: "06:10",
|
||
});
|
||
});
|
||
|
||
test("a persisted candidate working time takes precedence over the reported range", () => {
|
||
// Given: rectification saved a candidate minute while preserving the user's original period.
|
||
const candidate = {
|
||
...emptyDraft,
|
||
time: "04:53",
|
||
birthTimeSource: "period_only",
|
||
birthTimePeriod: "early_morning",
|
||
birthTimeStatus: "candidate",
|
||
} satisfies BirthTimeDraft;
|
||
|
||
// When: a profile surface asks what birth-time state to display.
|
||
const display = birthTimeDisplayState(candidate);
|
||
|
||
// Then: the candidate minute is primary and the original period remains secondary.
|
||
assert.deepEqual(display, {
|
||
kind: "candidate",
|
||
activeTime: "04:53",
|
||
reportedLabel: "凌晨 / 清晨(04:00—07:59)",
|
||
});
|
||
});
|
||
|
||
test("birth time declaration payload cannot write deterministic application fields", () => {
|
||
const draft = {
|
||
...emptyDraft,
|
||
time: "14:24",
|
||
reportedTime: "14:30",
|
||
birthTimeSource: "approximate",
|
||
uncertaintyBeforeMinutes: 30,
|
||
uncertaintyAfterMinutes: 30,
|
||
birthTimeStatus: "confirmed",
|
||
} satisfies BirthTimeDraft;
|
||
|
||
assert.deepEqual(birthTimePersistenceValues(draft), {
|
||
reported_birth_time: "14:30",
|
||
birth_time_source: "approximate",
|
||
birth_time_period: null,
|
||
birth_time_clue: null,
|
||
uncertainty_before_minutes: 30,
|
||
uncertainty_after_minutes: 30,
|
||
});
|
||
});
|
||
|
||
test("birth time intake describes uncertainty without claiming false precision", () => {
|
||
const approximate = {
|
||
...emptyDraft,
|
||
birthTimeSource: "approximate",
|
||
reportedTime: "14:30",
|
||
uncertaintyBeforeMinutes: 30,
|
||
uncertaintyAfterMinutes: 30,
|
||
} satisfies BirthTimeDraft;
|
||
|
||
assert.equal(describeBirthTimeDraft(approximate), "1993年4月17日,约 14:30(前后 30 分钟)");
|
||
assert.equal(
|
||
assistantIntentCopy("present_saved_candidate_range"),
|
||
"目前只能保存候选范围,还没有足够证据应用到具体分钟。",
|
||
);
|
||
});
|
||
|
||
test("birth date parsing preserves the local calendar day", () => {
|
||
const parsed = parseBirthDate("1993-04-17");
|
||
|
||
assert.equal(parsed?.getFullYear(), 1993);
|
||
assert.equal(parsed?.getMonth(), 3);
|
||
assert.equal(parsed?.getDate(), 17);
|
||
});
|
||
|
||
test("birth date values round trip leap days and reject invalid input", () => {
|
||
const leapDay = parseBirthDate("2000-02-29");
|
||
|
||
assert.equal(leapDay === undefined ? undefined : formatBirthDate(leapDay), "2000-02-29");
|
||
assert.equal(parseBirthDate(""), undefined);
|
||
assert.equal(parseBirthDate("2001-02-29"), undefined);
|
||
});
|
||
|
||
test("persisted birth dates normalize database ISO values without accepting invalid dates", () => {
|
||
assert.equal(normalizePersistedBirthDate(new Date("1997-08-08T00:00:00.000Z")), "1997-08-08");
|
||
assert.equal(normalizePersistedBirthDate("1997-08-08T00:00:00.000Z"), "1997-08-08");
|
||
assert.equal(normalizePersistedBirthDate("1997-08-08"), "1997-08-08");
|
||
assert.equal(normalizePersistedBirthDate(new Date("invalid")), "");
|
||
assert.equal(normalizePersistedBirthDate("1997-02-30T00:00:00.000Z"), "");
|
||
assert.equal(normalizePersistedBirthDate("1997-08-08junk"), "");
|
||
});
|
||
|
||
test("candidate copy does not claim an unconfirmed minute is automatically in use", () => {
|
||
const source = readFileSync(new URL("../src/components/birth-time-intake.tsx", import.meta.url), "utf8");
|
||
|
||
assert.doesNotMatch(source, /已用于当前排盘/);
|
||
assert.match(source, /birthTimeConsultationOptionsCopy\(value\)/);
|
||
});
|