249 lines
7.9 KiB
TypeScript
249 lines
7.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
assessBirthTime,
|
|
birthTimeAssessmentSchema,
|
|
candidateResultSchema,
|
|
journeySnapshotSchema,
|
|
withCandidateResult,
|
|
withConfirmedCandidate,
|
|
withRectificationScoring,
|
|
} from "../src/lib/birth-time-journey.ts";
|
|
|
|
const location = { lat: 31.2304, lon: 121.4737, tz: 8 } as const;
|
|
|
|
test("birth time journey sends a stable hospital record directly to charting", () => {
|
|
const assessment = birthTimeAssessmentSchema.parse({
|
|
date: "1993-04-17",
|
|
source: "hospital_record",
|
|
reportedTime: "08:16",
|
|
uncertaintyBeforeMinutes: 2,
|
|
uncertaintyAfterMinutes: 2,
|
|
location,
|
|
});
|
|
|
|
const snapshot = assessBirthTime(assessment, { kind: "stable" });
|
|
|
|
assert.equal(snapshot.state, "ready");
|
|
assert.equal(snapshot.route, "direct_chart");
|
|
assert.equal(snapshot.canApply, true);
|
|
assert.equal(snapshot.activeTime, "08:16");
|
|
assert.equal(snapshot.assistantIntent, "confirm_stable_record");
|
|
assert.equal(journeySnapshotSchema.safeParse(snapshot).success, true);
|
|
});
|
|
|
|
test("birth time journey fails a sensitive hospital record closed into rectification", () => {
|
|
const assessment = birthTimeAssessmentSchema.parse({
|
|
date: "1993-04-17",
|
|
source: "hospital_record",
|
|
reportedTime: "08:16",
|
|
uncertaintyBeforeMinutes: 2,
|
|
uncertaintyAfterMinutes: 2,
|
|
location,
|
|
});
|
|
|
|
const snapshot = assessBirthTime(assessment, { kind: "sensitive" });
|
|
|
|
assert.equal(snapshot.state, "rectifying");
|
|
assert.equal(snapshot.route, "rectification");
|
|
assert.equal(snapshot.canApply, false);
|
|
assert.equal(snapshot.activeTime, null);
|
|
assert.equal(snapshot.assistantIntent, "explain_sensitive_boundary");
|
|
});
|
|
|
|
test("birth time journey fails a scanner outage closed into rectification", () => {
|
|
const assessment = birthTimeAssessmentSchema.parse({
|
|
date: "1993-04-17",
|
|
source: "hospital_record",
|
|
reportedTime: "08:16",
|
|
uncertaintyBeforeMinutes: 2,
|
|
uncertaintyAfterMinutes: 2,
|
|
location,
|
|
});
|
|
|
|
const snapshot = assessBirthTime(assessment, { kind: "unavailable" });
|
|
|
|
assert.equal(snapshot.route, "rectification");
|
|
assert.equal(snapshot.canApply, false);
|
|
assert.equal(snapshot.assistantIntent, "explain_assessment_unavailable");
|
|
});
|
|
|
|
test("birth time journey routes family and approximate declarations to rectification", () => {
|
|
const family = birthTimeAssessmentSchema.parse({
|
|
date: "1993-04-17",
|
|
source: "family_exact",
|
|
reportedTime: "14:30",
|
|
uncertaintyBeforeMinutes: 10,
|
|
uncertaintyAfterMinutes: 10,
|
|
location,
|
|
});
|
|
const approximate = birthTimeAssessmentSchema.parse({
|
|
date: "1993-04-17",
|
|
source: "approximate",
|
|
reportedTime: "14:30",
|
|
uncertaintyBeforeMinutes: 30,
|
|
uncertaintyAfterMinutes: 30,
|
|
location,
|
|
});
|
|
|
|
assert.equal(assessBirthTime(family, { kind: "sensitive" }).assistantIntent, "start_light_rectification");
|
|
assert.equal(assessBirthTime(approximate, { kind: "sensitive" }).assistantIntent, "start_standard_rectification");
|
|
});
|
|
|
|
test("birth time journey accepts period-only and unknown declarations without inventing a clock time", () => {
|
|
const period = birthTimeAssessmentSchema.parse({
|
|
date: "1993-04-17",
|
|
source: "period_only",
|
|
period: "evening",
|
|
location,
|
|
});
|
|
const unknown = birthTimeAssessmentSchema.parse({
|
|
date: "1993-04-17",
|
|
source: "unknown",
|
|
clue: "家人只记得天黑以后",
|
|
location,
|
|
});
|
|
|
|
const periodSnapshot = assessBirthTime(period, { kind: "not_required" });
|
|
const unknownSnapshot = assessBirthTime(unknown, { kind: "not_required" });
|
|
|
|
assert.equal(periodSnapshot.reportedRange.label, "18:00—22:59");
|
|
assert.equal(periodSnapshot.activeTime, null);
|
|
assert.equal(periodSnapshot.assistantIntent, "start_period_rectification");
|
|
assert.equal(unknownSnapshot.reportedRange.label, "全天待确认");
|
|
assert.equal(unknownSnapshot.assistantIntent, "collect_time_clues");
|
|
});
|
|
|
|
test("birth time assessment rejects source-specific missing or invalid fields", () => {
|
|
const missingTime = birthTimeAssessmentSchema.safeParse({
|
|
date: "1993-04-17",
|
|
source: "hospital_record",
|
|
uncertaintyBeforeMinutes: 2,
|
|
uncertaintyAfterMinutes: 2,
|
|
location,
|
|
});
|
|
const invalidFamilyRange = birthTimeAssessmentSchema.safeParse({
|
|
date: "1993-04-17",
|
|
source: "family_exact",
|
|
reportedTime: "14:30",
|
|
uncertaintyBeforeMinutes: 30,
|
|
uncertaintyAfterMinutes: 30,
|
|
location,
|
|
});
|
|
|
|
assert.equal(missingTime.success, false);
|
|
assert.equal(invalidFamilyRange.success, false);
|
|
});
|
|
|
|
test("rectification scoring can save a candidate but never apply an exact minute", () => {
|
|
const assessment = birthTimeAssessmentSchema.parse({
|
|
date: "1993-04-17",
|
|
source: "approximate",
|
|
reportedTime: "14:30",
|
|
uncertaintyBeforeMinutes: 30,
|
|
uncertaintyAfterMinutes: 30,
|
|
location,
|
|
});
|
|
const initial = assessBirthTime(assessment, { kind: "sensitive" });
|
|
|
|
const scored = withRectificationScoring(initial, {
|
|
answeredCount: 3,
|
|
candidateClusterRankings: [{ cluster: "middle_candidate_cluster", score: 5 }],
|
|
});
|
|
|
|
assert.equal(scored.state, "candidate");
|
|
assert.equal(scored.canApply, false);
|
|
assert.equal(scored.activeTime, null);
|
|
assert.equal(scored.assistantIntent, "present_saved_candidate_range");
|
|
});
|
|
|
|
test("the completed questionnaire deterministically requests dated life events", () => {
|
|
const assessment = birthTimeAssessmentSchema.parse({
|
|
date: "1993-04-17",
|
|
source: "period_only",
|
|
period: "early_morning",
|
|
location,
|
|
});
|
|
const initial = assessBirthTime(assessment, { kind: "not_required" });
|
|
|
|
const completed = withRectificationScoring(initial, {
|
|
answeredCount: 8,
|
|
candidateClusterRankings: [{ cluster: "middle_candidate_cluster", score: 5 }],
|
|
nextRound: null,
|
|
nextRoundQuestions: [],
|
|
});
|
|
|
|
assert.equal(completed.state, "rectifying");
|
|
assert.equal(completed.input, "life_events");
|
|
assert.equal(completed.assistantIntent, "collect_dated_life_events");
|
|
assert.equal(completed.canApply, false);
|
|
assert.equal(completed.activeTime, null);
|
|
});
|
|
|
|
test("candidate confidence deterministically selects the next action", () => {
|
|
const assessment = birthTimeAssessmentSchema.parse({
|
|
date: "1993-04-17",
|
|
source: "approximate",
|
|
reportedTime: "14:30",
|
|
uncertaintyBeforeMinutes: 30,
|
|
uncertaintyAfterMinutes: 30,
|
|
location,
|
|
});
|
|
const eventSnapshot = withRectificationScoring(
|
|
assessBirthTime(assessment, { kind: "sensitive" }),
|
|
{
|
|
answeredCount: 8,
|
|
candidateClusterRankings: [{ cluster: "middle_candidate_cluster", score: 5 }],
|
|
nextRound: null,
|
|
nextRoundQuestions: [],
|
|
},
|
|
);
|
|
const base = {
|
|
resultId: "1d8ee348-61a3-433d-8907-ff6d281b9992",
|
|
winningSegment: {
|
|
startTime: "14:22",
|
|
endTime: "14:26",
|
|
representativeTime: "14:24",
|
|
widthMinutes: 5,
|
|
},
|
|
eventCount: 4,
|
|
domainCount: 3,
|
|
topScore: 16,
|
|
secondScore: 10,
|
|
marginPercent: 37.5,
|
|
reasons: [],
|
|
evidence: [],
|
|
algorithmVersion: "birth-time-event-scoring-v1",
|
|
} as const;
|
|
|
|
const low = withCandidateResult(eventSnapshot, candidateResultSchema.parse({
|
|
...base,
|
|
confidence: "low",
|
|
canApply: false,
|
|
}));
|
|
const medium = withCandidateResult(eventSnapshot, candidateResultSchema.parse({
|
|
...base,
|
|
confidence: "medium",
|
|
canApply: false,
|
|
}));
|
|
const highResult = candidateResultSchema.parse({
|
|
...base,
|
|
confidence: "high",
|
|
canApply: true,
|
|
});
|
|
const high = withCandidateResult(eventSnapshot, highResult);
|
|
|
|
assert.deepEqual(
|
|
[low.input, medium.input, high.input],
|
|
["life_events", "candidate_actions", "candidate_confirmation"],
|
|
);
|
|
assert.deepEqual([low.canApply, medium.canApply, high.canApply], [false, false, true]);
|
|
assert.equal(high.state, "confirming");
|
|
|
|
const ready = withConfirmedCandidate(high, highResult, "14:24");
|
|
assert.equal(ready.state, "ready");
|
|
assert.equal(ready.route, "direct_chart");
|
|
assert.equal(ready.activeTime, "14:24");
|
|
assert.equal(ready.canApply, false);
|
|
});
|