8ed6118b9a
Users pick a clock range instead of a coarse period plus notes, so rectification and window consult scan that range instead of a leftover afternoon bucket. Co-authored-by: Cursor <cursoragent@cursor.com>
463 lines
16 KiB
TypeScript
463 lines
16 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import {
|
|
applyBirthTimeDraftPatch,
|
|
applyPersistedBirthTime,
|
|
assistantIntentCopy,
|
|
birthTimeDisplayState,
|
|
birthTimeDraftReadyHint,
|
|
birthTimePersistenceValues,
|
|
birthTimeSourceDefaults,
|
|
birthTimeSourceOptions,
|
|
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: "",
|
|
declaredWindowStart: "",
|
|
declaredWindowEnd: "",
|
|
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 customWindow = {
|
|
...emptyDraft,
|
|
birthTimeSource: "period_only",
|
|
declaredWindowStart: "14:30",
|
|
declaredWindowEnd: "15:00",
|
|
} 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(customWindow), true);
|
|
assert.equal(isBirthTimeDraftReady({
|
|
...customWindow,
|
|
declaredWindowEnd: "14:30",
|
|
}), false);
|
|
assert.equal(isBirthTimeDraftReady(incompleteApproximate), true);
|
|
assert.equal(isBirthTimeDraftReady({ ...emptyDraft, birthTimeSource: "unknown" }), true);
|
|
});
|
|
|
|
test("family exact intake defaults and persists as reported 0/0 without engine confirmation", () => {
|
|
assert.deepEqual(birthTimeSourceDefaults.family_exact, {
|
|
birthTimePeriod: "",
|
|
declaredWindowStart: "",
|
|
declaredWindowEnd: "",
|
|
birthTimeClue: "",
|
|
uncertaintyBeforeMinutes: 0,
|
|
uncertaintyAfterMinutes: 0,
|
|
});
|
|
|
|
const declaredExact = {
|
|
...emptyDraft,
|
|
reportedTime: "05:00",
|
|
birthTimeSource: "family_exact",
|
|
uncertaintyBeforeMinutes: 0,
|
|
uncertaintyAfterMinutes: 0,
|
|
birthTimeStatus: "reported",
|
|
} satisfies BirthTimeDraft;
|
|
|
|
assert.equal(isBirthTimeDraftReady(declaredExact), true);
|
|
assert.equal(isBirthTimeReadyForConsultation(declaredExact), false);
|
|
assert.deepEqual(birthTimePersistenceValues(declaredExact), {
|
|
reported_birth_time: "05:00",
|
|
birth_time_source: "family_exact",
|
|
birth_time_period: null,
|
|
declared_window_start: null,
|
|
declared_window_end: null,
|
|
birth_time_clue: null,
|
|
uncertainty_before_minutes: 0,
|
|
uncertainty_after_minutes: 0,
|
|
});
|
|
});
|
|
|
|
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: 0,
|
|
uncertaintyAfterMinutes: 0,
|
|
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: 0,
|
|
uncertaintyAfterMinutes: 0,
|
|
} 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 clock window 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,
|
|
declared_window_start: null,
|
|
declared_window_end: 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");
|
|
assert.equal(
|
|
describeBirthTimeDraft({
|
|
...emptyDraft,
|
|
birthTimeSource: "period_only",
|
|
declaredWindowStart: "14:30",
|
|
declaredWindowEnd: "15:00",
|
|
}),
|
|
"1993年4月17日,14:30—15:00",
|
|
);
|
|
assert.equal(
|
|
describeBirthTimeDraft({
|
|
...emptyDraft,
|
|
birthTimeSource: "period_only",
|
|
birthTimePeriod: "evening",
|
|
}),
|
|
"1993年4月17日,18:00—22:59",
|
|
);
|
|
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("fresh intake copy stays user-facing and explains the next missing field", () => {
|
|
assert.equal(
|
|
birthTimeSourceOptions.find((option) => option.value === "family_exact")?.hint,
|
|
"按医院记录或家人记得的时间填写,精确到分钟",
|
|
);
|
|
assert.equal(
|
|
birthTimeSourceOptions.find((option) => option.value === "period_only")?.hint,
|
|
"选择一段开始到结束的时间,或完全不清楚",
|
|
);
|
|
assert.equal(birthTimeSourceOptions.every((option) => !/引擎确认|初始化填报/.test(option.hint)), true);
|
|
assert.equal(birthTimeDraftReadyHint({ ...emptyDraft, date: "" }), "请先选择出生日期");
|
|
assert.equal(birthTimeDraftReadyHint(emptyDraft), "请选择你对出生时间了解多少");
|
|
assert.equal(birthTimeDraftReadyHint({
|
|
...emptyDraft,
|
|
birthTimeSource: "family_exact",
|
|
uncertaintyBeforeMinutes: 0,
|
|
uncertaintyAfterMinutes: 0,
|
|
}), "请填写时和分");
|
|
assert.equal(birthTimeDraftReadyHint({
|
|
...emptyDraft,
|
|
birthTimeSource: "period_only",
|
|
}), "请选择开始和结束时间");
|
|
assert.equal(birthTimeDraftReadyHint({
|
|
...emptyDraft,
|
|
birthTimeSource: "period_only",
|
|
declaredWindowStart: "14:30",
|
|
declaredWindowEnd: "14:30",
|
|
}), "开始和结束不能是同一分钟");
|
|
});
|
|
|
|
test("fresh intake preserves exact, approximate-period, and unknown-time paths without auto-confirming", () => {
|
|
const source = readFileSync(new URL("../src/components/birth-time-intake.tsx", import.meta.url), "utf8");
|
|
|
|
assert.doesNotMatch(source, /引擎确认/);
|
|
assert.deepEqual(
|
|
birthTimeSourceOptions.map((option) => option.value),
|
|
["family_exact", "period_only"],
|
|
);
|
|
assert.doesNotMatch(source, /已用于当前排盘/);
|
|
assert.doesNotMatch(source, /birthTimePeriodOptions/);
|
|
assert.match(source, /birthTimeSource: "unknown"/);
|
|
assert.match(source, /选择你记得的开始和结束时间/);
|
|
assert.match(source, /完全不清楚,跳过出生时间/);
|
|
assert.match(source, /我可以选一段时间范围/);
|
|
assert.doesNotMatch(source, /补充描述|请选择最接近的时间范围|我可以描述一个时间范围/);
|
|
assert.match(source, /birthTimeStatus: "reported"/);
|
|
assert.match(source, /birthTimeConsultationOptionsCopy\(value\)/);
|
|
});
|
|
|
|
test("a custom clock window persists as the scan bounds without a leftover period bucket", () => {
|
|
const draft = {
|
|
...emptyDraft,
|
|
birthTimeSource: "period_only",
|
|
birthTimePeriod: "afternoon",
|
|
declaredWindowStart: "14:30",
|
|
declaredWindowEnd: "15:00",
|
|
birthTimeStatus: "reported",
|
|
} satisfies BirthTimeDraft;
|
|
|
|
assert.deepEqual(birthTimePersistenceValues(draft), {
|
|
reported_birth_time: null,
|
|
birth_time_source: "period_only",
|
|
birth_time_period: null,
|
|
declared_window_start: "14:30",
|
|
declared_window_end: "15:00",
|
|
birth_time_clue: null,
|
|
uncertainty_before_minutes: null,
|
|
uncertainty_after_minutes: null,
|
|
});
|
|
});
|
|
|
|
test("a saved declaration adopts the birth-time truth the account write returned", () => {
|
|
const exactDeclaration = {
|
|
...emptyDraft,
|
|
birthTimeSource: "family_exact",
|
|
reportedTime: "05:00",
|
|
uncertaintyBeforeMinutes: 0,
|
|
uncertaintyAfterMinutes: 0,
|
|
birthTimeStatus: "reported",
|
|
} as const satisfies BirthTimeDraft;
|
|
|
|
// The server accepts a zero-uncertainty exact time as the active minute, so the
|
|
// submitted draft must not keep claiming the time is still only reported.
|
|
assert.deepEqual(
|
|
applyPersistedBirthTime(exactDeclaration, { status: "accepted", activeTime: "05:00" }),
|
|
{ ...exactDeclaration, time: "05:00", birthTimeStatus: "accepted" },
|
|
);
|
|
assert.deepEqual(
|
|
applyPersistedBirthTime({ ...exactDeclaration, time: "05:00", birthTimeStatus: "accepted" }, {
|
|
status: "reported",
|
|
activeTime: null,
|
|
}),
|
|
exactDeclaration,
|
|
);
|
|
assert.deepEqual(
|
|
applyPersistedBirthTime(exactDeclaration, { status: "accepted", activeTime: "05:00:00" }),
|
|
{ ...exactDeclaration, time: "05:00", birthTimeStatus: "accepted" },
|
|
);
|
|
|
|
// A response without usable birth-time truth must leave the draft untouched.
|
|
for (const applied of [
|
|
undefined,
|
|
null,
|
|
{},
|
|
{ status: "unknown_status", activeTime: "05:00" },
|
|
{ status: 7, activeTime: "05:00" },
|
|
]) {
|
|
assert.deepEqual(applyPersistedBirthTime(exactDeclaration, applied), exactDeclaration);
|
|
}
|
|
assert.deepEqual(
|
|
applyPersistedBirthTime(exactDeclaration, { status: "accepted", activeTime: "5:0" }),
|
|
{ ...exactDeclaration, time: "", birthTimeStatus: "accepted" },
|
|
);
|
|
});
|