Files
Jyotisha/frontend/tests/birth-time-dynamic-candidate-confirmation.test.ts
T
2026-07-21 15:50:02 +08:00

262 lines
11 KiB
TypeScript

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);
});