import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { candidateResultSchema } from "../src/lib/birth-time-evidence.ts"; import { birthTimeJourneyRequestSchema } from "../src/lib/birth-time-journey-request.ts"; import { createBirthTimeJourneyService } from "../src/lib/birth-time-journey-service.ts"; import { StaleJourneyTurnError } from "../src/lib/birth-time-journey-turn-persistence.ts"; import { actionId, dynamicCase, legacyCase, ownerId } from "./birth-time-dynamic-persistence-fixture.ts"; import { memoryStore } from "./birth-time-journey-memory-store.ts"; const highCandidate = candidateResultSchema.parse({ resultId: "f8eb3bc5-80eb-40fc-b937-e62ea37c3236", confidence: "high", canApply: true, winningSegment: { startTime: "17:13", endTime: "17:17", representativeTime: "17:15", widthMinutes: 5, }, eventCount: 4, domainCount: 3, topScore: 18, secondScore: 8, marginPercent: 55, reasons: ["One segment has consistent evidence."], evidence: [], algorithmVersion: "birth-time-event-scoring-v1", }); const migration = readFileSync(new URL( "../supabase/migrations/20260720000000_chat_delete_and_dynamic_candidate_confirmation.sql", import.meta.url, ), "utf8"); function highConfidenceDynamicCase() { const current = dynamicCase(); return { ...current, candidateResult: highCandidate, snapshot: { ...current.snapshot, state: "confirming" as const, assistantIntent: "confirm_candidate_time" as const, input: "candidate_confirmation" as const, confidence: "high" as const, canApply: true, }, currentChoiceQuestion: null, dynamicTurnState: { ...current.dynamicTurnState, nextAction: { kind: "request_candidate_confirmation" as const, resultId: highCandidate.resultId, }, progress: { ...current.dynamicTurnState.progress, phase: "result" as const }, permissions: { canConfirmCandidate: true }, }, }; } test("dynamic confirmation atomically reaches ready", async () => { const current = highConfidenceDynamicCase(); const memory = memoryStore(current); const service = createBirthTimeJourneyService({ store: memory.store, engine: { async scan() { throw new Error("unexpected scan"); }, async score() { throw new Error("unexpected score"); }, async scoreEvents() { throw new Error("unexpected event score"); }, async buildDifferencePacket() { throw new Error("unexpected packet"); }, async scoreChoices() { throw new Error("unexpected choice score"); }, }, }); const result = await service.confirmDynamicCandidate({ userId: ownerId, caseId: current.id, actionId, expectedVersion: current.dynamicTurnState.turnVersion, resultId: current.candidateResult.resultId, time: current.candidateResult.winningSegment?.representativeTime ?? "", }); assert.equal(result.nextAction.kind, "ready"); assert.equal(result.snapshot.activeTime, "17:15"); }); test("dynamic confirmation replays only its exact receipt and rejects stale versions", async () => { const current = highConfidenceDynamicCase(); const memory = memoryStore(current); const service = createBirthTimeJourneyService({ store: memory.store, engine: { async scan() { throw new Error("unexpected scan"); }, async score() { throw new Error("unexpected score"); }, async scoreEvents() { throw new Error("unexpected event score"); }, async buildDifferencePacket() { throw new Error("unexpected packet"); }, async scoreChoices() { throw new Error("unexpected choice score"); }, }, }); const command = { userId: ownerId, caseId: current.id, actionId, expectedVersion: current.turnVersion, resultId: highCandidate.resultId, time: "17:15", }; const first = await service.confirmDynamicCandidate(command); const replay = await service.confirmDynamicCandidate(command); assert.deepEqual(replay, first); assert.equal(memory.committedTurnWrites(), 1); await assert.rejects(service.confirmDynamicCandidate({ ...command, actionId: "f3a64be6-65d3-498c-a86d-847cf104e594", }), StaleJourneyTurnError); }); test("dynamic confirmation rejects every binding mismatch without writing", async () => { const cases = [ { name: "wrong protocol", current: legacyCase(true), command: {}, }, { name: "wrong current action", current: { ...highConfidenceDynamicCase(), dynamicTurnState: { ...highConfidenceDynamicCase().dynamicTurnState, nextAction: { kind: "present_medium_result" as const, resultId: highCandidate.resultId } }, }, command: {}, }, { name: "wrong result", current: highConfidenceDynamicCase(), command: { resultId: "a3e41512-9fa0-4866-a187-e3b3aa07aee0" }, }, { name: "wrong representative time", current: highConfidenceDynamicCase(), command: { time: "17:14" }, }, { name: "confirmation disabled", current: { ...highConfidenceDynamicCase(), dynamicTurnState: { ...highConfidenceDynamicCase().dynamicTurnState, permissions: { canConfirmCandidate: false } }, }, command: {}, }, { name: "stale version", current: highConfidenceDynamicCase(), command: { expectedVersion: highConfidenceDynamicCase().turnVersion - 1 }, }, ] as const; for (const scenario of cases) { const memory = memoryStore(scenario.current); const service = createBirthTimeJourneyService({ store: memory.store, engine: { async scan() { throw new Error("unexpected scan"); }, async score() { throw new Error("unexpected score"); }, async scoreEvents() { throw new Error("unexpected event score"); }, async buildDifferencePacket() { throw new Error("unexpected packet"); }, async scoreChoices() { throw new Error("unexpected choice score"); }, } }); await assert.rejects(service.confirmDynamicCandidate({ userId: ownerId, caseId: scenario.current.id, actionId, expectedVersion: scenario.current.turnVersion ?? 0, resultId: highCandidate.resultId, time: "17:15", ...scenario.command, })); assert.equal(memory.committedTurnWrites(), 0, scenario.name); } }); test("dynamic confirmation rejects same-action payload mismatches after a successful receipt", async () => { const current = highConfidenceDynamicCase(); const memory = memoryStore(current); const service = createBirthTimeJourneyService({ store: memory.store, engine: { async scan() { throw new Error("unexpected scan"); }, async score() { throw new Error("unexpected score"); }, async scoreEvents() { throw new Error("unexpected event score"); }, async buildDifferencePacket() { throw new Error("unexpected packet"); }, async scoreChoices() { throw new Error("unexpected choice score"); }, } }); const command = { userId: ownerId, caseId: current.id, actionId, expectedVersion: current.turnVersion, resultId: highCandidate.resultId, time: "17:15" }; await service.confirmDynamicCandidate(command); await assert.rejects(service.confirmDynamicCandidate({ ...command, time: "17:14" }), StaleJourneyTurnError); await assert.rejects(service.confirmDynamicCandidate({ ...command, resultId: "a3e41512-9fa0-4866-a187-e3b3aa07aee0" }), StaleJourneyTurnError); assert.equal(memory.committedTurnWrites(), 1); }); test("dynamic confirmation rejects a non-representative minute before writing", async () => { const current = highConfidenceDynamicCase(); const memory = memoryStore(current); const service = createBirthTimeJourneyService({ store: memory.store, engine: { async scan() { throw new Error("unexpected scan"); }, async score() { throw new Error("unexpected score"); }, async scoreEvents() { throw new Error("unexpected event score"); }, async buildDifferencePacket() { throw new Error("unexpected packet"); }, async scoreChoices() { throw new Error("unexpected choice score"); }, }, }); await assert.rejects(service.confirmDynamicCandidate({ userId: ownerId, caseId: current.id, actionId, expectedVersion: current.turnVersion, resultId: highCandidate.resultId, time: "17:14", }), StaleJourneyTurnError); assert.equal(memory.committedTurnWrites(), 0); }); test("dynamic confirmation request accepts only its strict receipt, version, result, and time", () => { const valid = { type: "confirm_dynamic_candidate", caseId: dynamicCase().id, actionId, turnVersion: 7, resultId: highCandidate.resultId, time: "17:15", } as const; assert.equal(birthTimeJourneyRequestSchema.safeParse(valid).success, true); assert.equal(birthTimeJourneyRequestSchema.safeParse({ ...valid, time: "17:15:00" }).success, false); assert.equal(birthTimeJourneyRequestSchema.safeParse({ ...valid, turnVersion: -1 }).success, false); assert.equal(birthTimeJourneyRequestSchema.safeParse({ ...valid, candidateResult: highCandidate }).success, false); }); test("dynamic confirmation RPC locks the v2 case and is service-role only", () => { assert.match(migration, /create function public\.confirm_birth_time_dynamic_candidate\([\s\S]*?returns bigint/i); assert.match(migration, /for update[\s\S]*journey_protocol is distinct from 'dynamic-choice-v2'/i); assert.match(migration, /p_action_id = any\(v_case\.processed_action_ids\)/i); assert.match(migration, /candidate_result_id is distinct from p_result_id[\s\S]*representativeTime/i); assert.match(migration, /turn_version = p_expected_version \+ 1[\s\S]*turn_state = p_turn_state/i); assert.match(migration, /set active_birth_time = p_time,[\s\S]*birth_time_status = 'confirmed',[\s\S]*rectification_case_id = p_case_id/i); assert.match(migration, /revoke all on function public\.confirm_birth_time_dynamic_candidate\([\s\S]*?from public, anon, authenticated;[\s\S]*?grant execute on function public\.confirm_birth_time_dynamic_candidate\([\s\S]*?to service_role;/i); }); test("dynamic confirmation SQL binds replay and mutation to the exact guarded receipt", () => { assert.match(migration, /security definer\s+set search_path = ''/i); assert.match(migration, /from public\.birth_time_rectification_cases c[\s\S]*?where c\.id = p_case_id and c\.user_id = p_user_id[\s\S]*?for update/i); assert.match(migration, /from public\.birth_time_rectification_dynamic_state s[\s\S]*?where s\.case_id = p_case_id and s\.user_id = p_user_id[\s\S]*?for update/i); assert.match(migration, /v_case\.turn_version is distinct from p_expected_version \+ 1[\s\S]*?dynamic_control -> 'lastActionReceipt' is distinct from v_receipt/i); assert.match(migration, /v_case\.status is distinct from 'confirming'[\s\S]*?request_candidate_confirmation[\s\S]*?canConfirmCandidate/i); assert.match(migration, /update public\.birth_time_rectification_cases[\s\S]*?turn_version = p_expected_version \+ 1[\s\S]*?update public\.birth_time_rectification_dynamic_state[\s\S]*?lastActionReceipt[\s\S]*?update public\.profiles/i); assert.match(migration, /revoke all on function public\.confirm_birth_time_dynamic_candidate\([\s\S]*?from public, anon, authenticated;/i); assert.match(migration, /grant execute on function public\.confirm_birth_time_dynamic_candidate\([\s\S]*?to service_role;/i); });