131 lines
4.8 KiB
TypeScript
131 lines
4.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
candidateResultSchema,
|
|
lifeEventSchema,
|
|
withConfirmedCandidate,
|
|
} from "../src/lib/birth-time-evidence.ts";
|
|
import { journeySnapshotSchema } from "../src/lib/birth-time-journey.ts";
|
|
import { evidenceDraftSchema } from "../src/lib/birth-time-journey-turn.ts";
|
|
|
|
const baseEvent = {
|
|
id: "5cb071d6-6d99-46be-85dc-a9bf59ef6ac5",
|
|
domain: "career",
|
|
} as const;
|
|
|
|
test("life event dates must match their declared precision", () => {
|
|
assert.equal(lifeEventSchema.safeParse({ ...baseEvent, precision: "year", date: "2011" }).success, true);
|
|
assert.equal(lifeEventSchema.safeParse({ ...baseEvent, precision: "month", date: "2011-09" }).success, true);
|
|
assert.equal(lifeEventSchema.safeParse({ ...baseEvent, precision: "day", date: "2011-09-24" }).success, true);
|
|
|
|
assert.equal(lifeEventSchema.safeParse({ ...baseEvent, precision: "year", date: "2011-09" }).success, false);
|
|
assert.equal(lifeEventSchema.safeParse({ ...baseEvent, precision: "month", date: "2011" }).success, false);
|
|
assert.equal(lifeEventSchema.safeParse({ ...baseEvent, precision: "day", date: "2011" }).success, false);
|
|
assert.equal(lifeEventSchema.safeParse({ ...baseEvent, precision: "year", date: "2011.5" }).success, false);
|
|
});
|
|
|
|
test("life event day precision rejects impossible calendar dates", () => {
|
|
assert.equal(lifeEventSchema.safeParse({ ...baseEvent, precision: "day", date: "2024-02-29" }).success, true);
|
|
assert.equal(lifeEventSchema.safeParse({ ...baseEvent, precision: "day", date: "2023-02-29" }).success, false);
|
|
assert.equal(lifeEventSchema.safeParse({ ...baseEvent, precision: "day", date: "2023-04-31" }).success, false);
|
|
});
|
|
|
|
test("evidence drafts carry one server-selected event and no confirmed events", () => {
|
|
const parsed = evidenceDraftSchema.parse({
|
|
draftId: baseEvent.id,
|
|
questionId: "baseline_career_1",
|
|
domain: "career",
|
|
precision: "month",
|
|
date: "2019-07",
|
|
status: "draft",
|
|
needsReview: false,
|
|
});
|
|
|
|
assert.equal(parsed.draftId, baseEvent.id);
|
|
assert.equal(parsed.questionId, "baseline_career_1");
|
|
assert.equal(parsed.status, "draft");
|
|
assert.equal("events" in parsed, false);
|
|
assert.equal("id" in parsed, false);
|
|
});
|
|
|
|
test("incomplete draft fields remain nullable and review-only", () => {
|
|
const parsed = evidenceDraftSchema.parse({
|
|
draftId: baseEvent.id,
|
|
questionId: "adaptive_career_1",
|
|
domain: "career",
|
|
precision: null,
|
|
date: null,
|
|
status: "draft",
|
|
needsReview: true,
|
|
});
|
|
|
|
assert.equal(parsed.precision, null);
|
|
assert.equal(parsed.date, null);
|
|
assert.equal(parsed.needsReview, true);
|
|
});
|
|
|
|
test("evidence drafts reject extra confirmed-event payloads", () => {
|
|
assert.equal(evidenceDraftSchema.safeParse({
|
|
draftId: baseEvent.id,
|
|
questionId: "baseline_career_1",
|
|
domain: "career",
|
|
precision: "year",
|
|
date: "2019",
|
|
status: "draft",
|
|
needsReview: false,
|
|
events: [{ ...baseEvent, precision: "year", date: "2019" }],
|
|
}).success, false);
|
|
});
|
|
|
|
const candidateBase = {
|
|
resultId: "8c48d5a8-cf2a-43a5-90f9-e39a726de265",
|
|
confidence: "high",
|
|
canApply: true,
|
|
winningSegment: {
|
|
startTime: "14:22",
|
|
endTime: "14:26",
|
|
representativeTime: "14:24",
|
|
widthMinutes: 5,
|
|
},
|
|
eventCount: 4,
|
|
domainCount: 3,
|
|
topScore: 18,
|
|
secondScore: 12,
|
|
marginPercent: 50,
|
|
reasons: ["One segment has consistent evidence."],
|
|
evidence: [],
|
|
algorithmVersion: "birth-time-event-scoring-v1",
|
|
} as const;
|
|
|
|
test("high candidate results require every deterministic safety gate", () => {
|
|
assert.equal(candidateResultSchema.safeParse(candidateBase).success, true);
|
|
assert.equal(candidateResultSchema.safeParse({ ...candidateBase, eventCount: 3 }).success, false);
|
|
assert.equal(candidateResultSchema.safeParse({ ...candidateBase, domainCount: 2 }).success, false);
|
|
assert.equal(candidateResultSchema.safeParse({ ...candidateBase, canApply: false, winningSegment: null }).success, false);
|
|
assert.equal(candidateResultSchema.safeParse({
|
|
...candidateBase,
|
|
winningSegment: { ...candidateBase.winningSegment, widthMinutes: 6 },
|
|
}).success, false);
|
|
assert.equal(candidateResultSchema.safeParse({ ...candidateBase, marginPercent: 19 }).success, false);
|
|
});
|
|
|
|
test("confirmation rejects a high result forged below the deterministic gates", () => {
|
|
const candidate = candidateResultSchema.parse(candidateBase);
|
|
const forged = { ...candidate, eventCount: 3 };
|
|
const snapshot = journeySnapshotSchema.parse({
|
|
state: "confirming",
|
|
assistantIntent: "confirm_candidate_time",
|
|
input: "candidate_confirmation",
|
|
route: "rectification",
|
|
confidence: "high",
|
|
canApply: true,
|
|
activeTime: null,
|
|
reportedRange: { label: "14:22—14:26", startTime: "14:22", endTime: "14:26" },
|
|
});
|
|
|
|
assert.throws(
|
|
() => withConfirmedCandidate(snapshot, forged, "14:24"),
|
|
{ name: "CandidateConfirmationError" },
|
|
);
|
|
});
|