import assert from "node:assert/strict"; import test from "node:test"; import { runBirthTimeScoringPoll } from "../src/lib/birth-time-guided-polling.ts"; import { claimMutation, publishCurrentJourney } from "../src/lib/birth-time-guided-effect-coordinator.ts"; import { storedDynamicJourneyResponse } from "../src/lib/birth-time-journey-response.ts"; import { dynamicCase } from "./birth-time-dynamic-persistence-fixture.ts"; import { parseJourneyResponse } from "../src/lib/birth-time-journey-client.ts"; import { highConfirmationTurn } from "./birth-time-journey-client-test-support.ts"; const completedTurn = parseJourneyResponse(highConfirmationTurn); function deferred() { let resolve!: (value: T | PromiseLike) => void; const promise = new Promise((settle) => { resolve = settle; }); return { promise, resolve }; } const pendingTurn = parseJourneyResponse({ ...highConfirmationTurn, snapshot: { ...highConfirmationTurn.snapshot, state: "rectifying", assistantIntent: "collect_dated_life_events", input: "life_events", confidence: null, canApply: false, }, candidateResult: null, nextAction: { kind: "score_pending", jobId: "c70ea014-f8b4-41f2-9305-e4ae60c0d4d1" }, progress: { ...highConfirmationTurn.progress, phase: "scoring" }, permissions: { canConfirmCandidate: false }, }); test("polling is sequential and returns the first completed turn", async () => { let active = 0; let peak = 0; let calls = 0; const entered = Array.from({ length: 3 }, () => deferred()); const gates = Array.from( { length: 3 }, () => deferred(), ); const running = runBirthTimeScoringPoll({ initial: pendingTurn, maxAttempts: 4, signal: new AbortController().signal, delay: async () => undefined, poll: async () => { const call = calls; calls += 1; active += 1; peak = Math.max(peak, active); entered[call]?.resolve(); const turn = await gates[call].promise; active -= 1; return turn; }, }); await entered[0].promise; assert.equal(calls, 1, "a second poll must not start while the first is unresolved"); gates[0].resolve(pendingTurn); await entered[1].promise; assert.equal(active, 1); assert.equal(calls, 2, "only one deferred poll may be active at a time"); gates[1].resolve(pendingTurn); await entered[2].promise; assert.equal(active, 1); assert.equal(calls, 3); gates[2].resolve(completedTurn); const result = await running; assert.equal(calls, 3); assert.equal(peak, 1); assert.equal(result.kind, "completed"); }); test("polling stops on cancellation and ignores later work", async () => { const controller = new AbortController(); let calls = 0; const result = await runBirthTimeScoringPoll({ initial: pendingTurn, maxAttempts: 5, signal: controller.signal, delay: async () => { controller.abort(); }, poll: async () => { calls += 1; return pendingTurn; }, }); assert.equal(calls, 1); assert.equal(result.kind, "cancelled"); }); test("bounded polling preserves the pending turn instead of inventing completion", async () => { const result = await runBirthTimeScoringPoll({ initial: pendingTurn, maxAttempts: 2, signal: new AbortController().signal, delay: async () => undefined, poll: async () => pendingTurn, }); assert.equal(result.kind, "exhausted"); assert.equal(result.turn.nextAction.kind, "score_pending"); }); test("duplicate option clicks publish one advanced turn", async () => { const gate = deferred(); const sent: string[] = []; const published: typeof completedTurn[] = []; const lock = { current: false }; const select = async (optionId: string) => { const release = claimMutation(lock); if (release === null) return; sent.push(optionId); const turn = await gate.promise; published.push(turn); release(); }; const first = select("primary-option"); const duplicate = select("primary-option"); gate.resolve(completedTurn); await Promise.all([first, duplicate]); assert.deepEqual(sent, ["primary-option"]); assert.deepEqual(published, [completedTurn]); }); test("a stale generated question cannot replace a newer turn", () => { const expected = parseJourneyResponse(storedDynamicJourneyResponse(dynamicCase())); const current = parseJourneyResponse({ ...expected, turnVersion: expected.turnVersion + 1 }); let published = 0; const accepted = publishCurrentJourney({ expected, current, next: expected, publish: () => { published += 1; }, }); assert.equal(accepted, false); assert.equal(current.turnVersion, 8); assert.equal(published, 0); });