Files
Jyotisha/frontend/tests/birth-time-guided-candidate.test.ts
2026-07-18 18:40:23 +08:00

118 lines
5.1 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { candidateResultSchema } from "../src/lib/birth-time-evidence.ts";
import { createGuidedCandidateActions } from "../src/lib/birth-time-guided-candidate.ts";
import { StaleJourneyTurnError } from "../src/lib/birth-time-journey-turn-persistence.ts";
import type { StoredRectificationCase } from "../src/lib/birth-time-journey-service.ts";
import type { NextAction } from "../src/lib/birth-time-journey-turn.ts";
import {
draftActionId,
guidedCase,
journeyCaseId,
memoryStore,
secondActionId,
} from "./birth-time-journey-test-support.ts";
const mediumCandidate = candidateResultSchema.parse({
resultId: "345087cc-7e7f-4b37-90e5-f0c0a6e5b7a7",
confidence: "medium",
canApply: false,
winningSegment: { startTime: "05:18", endTime: "05:24", representativeTime: "05:21", widthMinutes: 7 },
eventCount: 4,
domainCount: 3,
topScore: 14.4,
secondScore: 12.1,
marginPercent: 15.97,
reasons: [],
evidence: [],
algorithmVersion: "birth-time-event-scoring-v1",
});
const highCandidate = candidateResultSchema.parse({
...mediumCandidate,
resultId: "8c48d5a8-cf2a-43a5-90f9-e39a726de265",
confidence: "high",
canApply: true,
winningSegment: { startTime: "05:19", endTime: "05:23", representativeTime: "05:21", widthMinutes: 5 },
eventCount: 5,
domainCount: 4,
topScore: 17.8,
secondScore: 13.5,
marginPercent: 24.16,
});
function candidateCase(candidate: typeof mediumCandidate | typeof highCandidate, version = 4) {
const stored = guidedCase({ version, candidateResult: candidate });
const baseTurn = stored.turnState;
if (!baseTurn) throw new Error("test fixture requires a guided turn");
const high = candidate.confidence === "high";
const nextAction: NextAction = high
? { kind: "request_candidate_confirmation", resultId: candidate.resultId }
: { kind: "present_medium_result", resultId: candidate.resultId };
return {
...stored,
snapshot: {
...stored.snapshot,
state: high ? "confirming" : "candidate",
assistantIntent: high ? "confirm_candidate_time" : "present_candidate_result",
input: high ? "candidate_confirmation" : "candidate_actions",
confidence: candidate.confidence,
canApply: high,
},
turnState: {
...baseTurn,
turnVersion: version,
nextAction,
progress: { ...baseTurn.progress, phase: "result" },
permissions: { canConfirmCandidate: high },
},
} satisfies StoredRectificationCase;
}
test("medium candidate save advances one version without applying a minute", async () => {
const memory = memoryStore(candidateCase(mediumCandidate));
const actions = createGuidedCandidateActions({ store: memory.store });
const result = await actions.save({ userId: "user-1", caseId: journeyCaseId, actionId: draftActionId, expectedVersion: 4, resultId: mediumCandidate.resultId });
assert.equal(result.turnVersion, 5);
assert.equal(result.nextAction.kind, "candidate_saved");
assert.equal(result.snapshot.activeTime, null);
assert.equal(memory.guidedCandidateWrites(), 1);
});
test("duplicate medium save replays once and stale new action fails", async () => {
const memory = memoryStore(candidateCase(mediumCandidate));
const actions = createGuidedCandidateActions({ store: memory.store });
const first = await actions.save({ userId: "user-1", caseId: journeyCaseId, actionId: draftActionId, expectedVersion: 4, resultId: mediumCandidate.resultId });
const duplicate = await actions.save({ userId: "user-1", caseId: journeyCaseId, actionId: draftActionId, expectedVersion: 4, resultId: mediumCandidate.resultId });
assert.deepEqual(duplicate, first);
assert.equal(memory.guidedCandidateWrites(), 1);
await assert.rejects(
actions.save({ userId: "user-1", caseId: journeyCaseId, actionId: secondActionId, expectedVersion: 4, resultId: mediumCandidate.resultId }),
StaleJourneyTurnError,
);
});
test("high confirmation requires matching result and representative time", async () => {
const memory = memoryStore(candidateCase(highCandidate));
const actions = createGuidedCandidateActions({ store: memory.store });
await assert.rejects(actions.confirm({ userId: "user-1", caseId: journeyCaseId, actionId: draftActionId, expectedVersion: 4, resultId: highCandidate.resultId, time: "05:22" }));
assert.equal(memory.guidedCandidateWrites(), 0);
const ready = await actions.confirm({ userId: "user-1", caseId: journeyCaseId, actionId: secondActionId, expectedVersion: 4, resultId: highCandidate.resultId, time: "05:21" });
assert.equal(ready.nextAction.kind, "ready");
assert.equal(ready.snapshot.activeTime, "05:21");
assert.equal(memory.guidedCandidateWrites(), 1);
});
test("medium result cannot use guided confirmation", async () => {
const memory = memoryStore(candidateCase(mediumCandidate));
const actions = createGuidedCandidateActions({ store: memory.store });
await assert.rejects(actions.confirm({ userId: "user-1", caseId: journeyCaseId, actionId: draftActionId, expectedVersion: 4, resultId: mediumCandidate.resultId, time: "05:21" }));
assert.equal(memory.guidedCandidateWrites(), 0);
});