Keep askable cards after exhaustion, explain each probe, read the adopted credible range in reports and chat, and compare declared periods before the minute grid when the clock is unknown. Co-authored-by: Cursor <cursoragent@cursor.com>
465 lines
16 KiB
TypeScript
465 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.doesNotMatch(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" },
|
|
);
|
|
});
|