import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { closeLocalPostgresDataPool, createLocalPostgresDataClient, } from "../src/lib/db/local-postgres-client-core.ts"; import { startPostgresFixture } from "./helpers/postgres-fixture.ts"; const runnerPath = fileURLToPath( new URL("../scripts/db-migrate.mjs", import.meta.url), ); const migrationSql = readFileSync( new URL("../supabase/migrations/20260814030000_rectification_event_decision_contract_v2.sql", import.meta.url), "utf8", ); function dockerAvailable(): boolean { return spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { encoding: "utf8", stdio: "ignore", }).status === 0; } const skipWithoutDocker = dockerAvailable() ? false : "docker unavailable on this host"; const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; function rpcError(error: unknown): string { if (!error || typeof error !== "object") return ""; const value = error as { message?: unknown }; return typeof value.message === "string" ? value.message : ""; } function candidateIds(value: unknown): string[] { if (!value || typeof value !== "object") return []; const rows = (value as { candidates?: unknown }).candidates; if (!Array.isArray(rows)) return []; return rows.flatMap((row) => { if (!row || typeof row !== "object") return []; const id = (row as { candidate_id?: unknown }).candidate_id; return typeof id === "string" ? [id] : []; }); } function objectField(value: unknown, field: string): Record { if (!value || typeof value !== "object") return {}; const nested = (value as Record)[field]; return nested && typeof nested === "object" && !Array.isArray(nested) ? (nested as Record) : {}; } test("PR-4 migration binds receipts to server candidate UUIDs and restores V2 result fields", () => { const persistStart = migrationSql.indexOf( "create or replace function public.persist_agentic_rectification_candidate_v2(", ); const caseStart = migrationSql.indexOf( "create or replace function public.get_agentic_rectification_case(", ); const dossierStart = migrationSql.indexOf( "create or replace function public.get_agentic_rectification_case_dossier(", ); const acceptStart = migrationSql.indexOf( "create or replace function public.accept_agentic_rectification_candidate_for_case_v2(", ); assert.ok(persistStart >= 0 && caseStart > persistStart && dossierStart > caseStart && acceptStart > dossierStart); const persistFunction = migrationSql.slice(persistStart, caseStart); assert.match(persistFunction, /p_decision_receipt - 'representative_candidate_id'/); assert.match(persistFunction, /v_representative_candidate_id := v_candidate_id/); assert.match(persistFunction, /\{representative_candidate_id\}/); assert.match(persistFunction, /decision_receipt = v_saved_decision_receipt/); assert.match(persistFunction, /'decision_receipt', v_saved_decision_receipt/); assert.match(persistFunction, /'decision_receipt', v_cached\.decision_receipt/); for (const functionSql of [ migrationSql.slice(caseStart, dossierStart), migrationSql.slice(dossierStart, acceptStart), ]) { for (const field of [ "event_contract_version", "decision_policy_version", "decision_receipt", "execution_ledger", "selected_candidate_id", ]) { assert.match(functionSql, new RegExp(`'${field}', v_result\.${field}`)); } } const dossierFunction = migrationSql.slice(dossierStart, acceptStart); assert.match(dossierFunction, /\(1, 'user'::text, recent\.user_message\)/); assert.match(dossierFunction, /\(2, 'assistant'::text, recent\.assistant_message\)/); }); test("PR-4 candidate decisions use server UUIDs, receipt-derived gates and separate acceptance/confirmation", { skip: skipWithoutDocker }, async () => { const fixture = startPostgresFixture(); const schemaUrl = fixture.connectionUrl("schema_owner", "schema-owner-test-password"); const serviceUrl = fixture.connectionUrl("service_runtime", "service-runtime-test-password"); try { const migration = spawnSync(process.execPath, [runnerPath], { encoding: "utf8", env: { ...process.env, SCHEMA_DATABASE_URL: schemaUrl }, }); assert.equal(migration.status, 0, migration.stderr); assert.match( migration.stdout, /applied 20260814030000_rectification_event_decision_contract_v2\.sql/, ); const userId = "10000000-0000-4000-8000-000000000001"; const sessionId = "20000000-0000-4000-8000-000000000001"; const secondSessionId = "20000000-0000-4000-8000-000000000002"; const caseId = "30000000-0000-4000-8000-000000000001"; const secondCaseId = "30000000-0000-4000-8000-000000000002"; const consentTurnId = "40000000-0000-4000-8000-000000000001"; const fakeCallerCandidateId = "50000000-0000-4000-8000-000000000001"; const fingerprint = "a".repeat(64); const snapshot = { birth_date: "1997-08-08", latitude: 36.420487, longitude: 114.209936, timezone_offset: 8, birth_time_source: "family_exact", reported_birth_time: "05:00", active_birth_time: null, birth_time_period: null, uncertainty_before_minutes: 10, uncertainty_after_minutes: 10, }; const range = { start_time: "04:50", end_time: "05:10" }; fixture.psqlAs("identity_runtime", "identity-runtime-test-password", ` insert into identity.users (id, name, email, email_verified, email_verified_at) values ('${userId}', 'PR4 Candidate Fixture', 'pr4-candidate@example.com', true, now()) `); fixture.psql(` update public.profiles set birth_date = '1997-08-08', reported_birth_time = '05:00', active_birth_time = null, birth_time = '05:00', birth_time_status = 'reported', birth_time_source = 'family_exact', birth_time_period = null, uncertainty_before_minutes = 10, uncertainty_after_minutes = 10, birth_place_label = '河北省邯郸市武安市', latitude = 36.420487, longitude = 114.209936, timezone_id = 'Asia/Shanghai', timezone_offset = 8 where id = '${userId}'; insert into public.chat_sessions (id, user_id, title, theme, session_type, messages) values ('${sessionId}', '${userId}', 'PR4 candidate case', 'general', 'birth_time_rectification', '[]'::jsonb), ('${secondSessionId}', '${userId}', 'PR4 other case', 'general', 'birth_time_rectification', '[]'::jsonb); insert into public.agentic_rectification_cases ( id, user_id, session_id, status, skill_name, skill_version, baseline_profile_fingerprint, baseline_birth_snapshot, candidate_range ) values ( '${caseId}', '${userId}', '${sessionId}', 'candidate_ready', 'jyotish-birth-time-rectification', '9.0.0', '${fingerprint}', '${JSON.stringify(snapshot)}'::jsonb, '${JSON.stringify(range)}'::jsonb ), ( '${secondCaseId}', '${userId}', '${secondSessionId}', 'candidate_ready', 'jyotish-birth-time-rectification', '9.0.0', '${"b".repeat(64)}', '${JSON.stringify(snapshot)}'::jsonb, '${JSON.stringify(range)}'::jsonb ); insert into public.agentic_rectification_turns ( id, case_id, request_id, user_message, assistant_message, status, model_name, completed_at ) values ( '${consentTurnId}', '${caseId}', '41000000-0000-4000-8000-000000000001', '我确认使用05:07作为出生时间', '已记录确认请求', 'completed', 'fixture-model', now() ); `); const service = createLocalPostgresDataClient(serviceUrl, null, "service_role"); const candidates = [ { candidate_id: fakeCallerCandidateId, rank: 1, time: "05:07", relative_support: 60, tied_minute_count: 1, }, { rank: 2, time: "05:08", relative_support: 40, tied_minute_count: 1 }, ]; const decisionReceipt = { display_allowed: true, accept_allowed: true, confirm_allowed: true, representative_time: "05:07", overall_confidence: "high", margin_percent: 20, representative_candidate_id: fakeCallerCandidateId, }; const executionLedger = [ { phase: "candidate.score", status: "completed", engine: "fixture-engine" }, { phase: "decision.evaluate", status: "completed", policy: "decision-policy-v1" }, ]; const persist = (engineResultId: string, policyVersion: string) => service.rpc("persist_agentic_rectification_candidate_v2", { p_user_id: userId, p_case_id: caseId, p_engine_result_id: engineResultId, p_evidence_ledger_fingerprint: "c".repeat(64), p_candidate_range_fingerprint: "d".repeat(64), p_skill_version: "9.0.0", p_algorithm_version: "rectification-pr4-fixture-v1", p_event_contract_version: "rectification-event-contract-v2", p_decision_policy_version: policyVersion, p_candidate_range: range, p_candidates: candidates, p_decision_receipt: decisionReceipt, p_execution_ledger: executionLedger, }); const first = await persist("pr4-engine-result-1", "decision-policy-v1"); assert.equal(first.error, null, rpcError(first.error)); const firstRow = first.data as Record; const firstResultId = String(firstRow.result_id); const firstCandidateIds = candidateIds(firstRow); assert.equal(firstRow.cached, false); assert.equal(firstCandidateIds.length, 2); assert.ok(firstCandidateIds.every((id) => UUID_PATTERN.test(id))); assert.ok(!firstCandidateIds.includes(fakeCallerCandidateId)); assert.equal( objectField(firstRow, "decision_receipt").representative_candidate_id, firstCandidateIds[0], ); const cached = await persist("pr4-engine-result-cache-ignored", "decision-policy-v1"); assert.equal(cached.error, null, rpcError(cached.error)); const cachedRow = cached.data as Record; assert.equal(cachedRow.cached, true); assert.equal(cachedRow.result_id, firstResultId); assert.deepEqual(candidateIds(cachedRow), firstCandidateIds); assert.equal( objectField(cachedRow, "decision_receipt").representative_candidate_id, firstCandidateIds[0], ); const changedPolicy = await persist("pr4-engine-result-2", "decision-policy-v2"); assert.equal(changedPolicy.error, null, rpcError(changedPolicy.error)); const changedPolicyRow = changedPolicy.data as Record; const resultId = String(changedPolicyRow.result_id); const ids = candidateIds(changedPolicyRow); const representativeCandidateId = ids[0]!; const nonRepresentativeCandidateId = ids[1]!; assert.equal(changedPolicyRow.cached, false); assert.notEqual(resultId, firstResultId); assert.equal( objectField(changedPolicyRow, "decision_receipt").representative_candidate_id, representativeCandidateId, ); assert.equal( fixture.psql(` select (r.decision_receipt ->> 'representative_candidate_id') || ':' || c.id from public.agentic_rectification_results r join public.agentic_rectification_candidates c on c.result_id = r.id and c.is_representative where r.id = '${resultId}' `), `${representativeCandidateId}:${representativeCandidateId}`, ); const restoredCase = await service.rpc("get_agentic_rectification_case", { p_user_id: userId, p_case_id: caseId, }); assert.equal(restoredCase.error, null, rpcError(restoredCase.error)); const restoredCaseResult = objectField(restoredCase.data, "latest_result"); assert.deepEqual( { event_contract_version: restoredCaseResult.event_contract_version, decision_policy_version: restoredCaseResult.decision_policy_version, representative_candidate_id: objectField( restoredCaseResult, "decision_receipt", ).representative_candidate_id, execution_ledger: restoredCaseResult.execution_ledger, selected_candidate_id: restoredCaseResult.selected_candidate_id, }, { event_contract_version: "rectification-event-contract-v2", decision_policy_version: "decision-policy-v2", representative_candidate_id: representativeCandidateId, execution_ledger: executionLedger, selected_candidate_id: null, }, ); const restoredDossier = await service.rpc("get_agentic_rectification_case_dossier", { p_user_id: userId, p_case_id: caseId, }); assert.equal(restoredDossier.error, null, rpcError(restoredDossier.error)); const restoredDossierResult = objectField(restoredDossier.data, "latest_result"); assert.equal(restoredDossierResult.event_contract_version, "rectification-event-contract-v2"); assert.equal(restoredDossierResult.decision_policy_version, "decision-policy-v2"); assert.equal( objectField(restoredDossierResult, "decision_receipt").representative_candidate_id, representativeCandidateId, ); assert.deepEqual(restoredDossierResult.execution_ledger, executionLedger); assert.equal(restoredDossierResult.selected_candidate_id, null); assert.deepEqual( (restoredDossier.data as { turns?: Array<{ role?: unknown }> }).turns?.map( (turn) => turn.role, ), ["user", "assistant"], ); assert.equal( fixture.psql(` select event_contract_version || ':' || decision_policy_version || ':' || display_allowed || ':' || selection_allowed || ':' || confirmation_allowed || ':' || (decision_receipt ->> 'confirm_allowed') || ':' || jsonb_array_length(execution_ledger) from public.agentic_rectification_results where id = '${resultId}' `), "rectification-event-contract-v2:decision-policy-v2:true:true:true:true:2", ); assert.equal( fixture.psql(` select has_function_privilege('service_role', 'public.persist_agentic_rectification_candidate(uuid,uuid,text,text,text,text,text,jsonb,jsonb,text,numeric,boolean,boolean,time without time zone)', 'EXECUTE') || ':' || has_function_privilege('service_role', 'public.accept_agentic_rectification_candidate(uuid,uuid,uuid,time without time zone)', 'EXECUTE') || ':' || has_function_privilege('service_role', 'public.accept_agentic_rectification_candidate_for_case(uuid,uuid,uuid,time without time zone)', 'EXECUTE') || ':' || has_function_privilege('service_role', 'public.confirm_agentic_rectification_birth_time(uuid,uuid,uuid,time without time zone,text,uuid)', 'EXECUTE') `), "f:f:f:f", ); const crossResult = await service.rpc("accept_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: firstResultId, p_candidate_id: representativeCandidateId, p_request_id: "60000000-0000-4000-8000-000000000001", }); assert.match(rpcError(crossResult.error), /agentic_rectification_candidate_not_found/); const crossCase = await service.rpc("accept_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: secondCaseId, p_result_id: resultId, p_candidate_id: representativeCandidateId, p_request_id: "60000000-0000-4000-8000-000000000002", }); assert.match(rpcError(crossCase.error), /agentic_rectification_candidate_not_found/); const forged = await service.rpc("accept_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: resultId, p_candidate_id: "50000000-0000-4000-8000-000000000099", p_request_id: "60000000-0000-4000-8000-000000000003", }); assert.match(rpcError(forged.error), /agentic_rectification_candidate_not_found/); const inexactConfirmation = await service.rpc("confirm_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: resultId, p_candidate_id: nonRepresentativeCandidateId, p_request_id: "70000000-0000-4000-8000-000000000001", p_consent_quote: "确认使用05:07", p_source_turn_id: consentTurnId, }); assert.match( rpcError(inexactConfirmation.error), /agentic_rectification_confirmation_exact_gate_blocked/, ); const acceptRequestId = "80000000-0000-4000-8000-000000000001"; const accepted = await service.rpc("accept_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: resultId, p_candidate_id: representativeCandidateId, p_request_id: acceptRequestId, }); assert.equal(accepted.error, null, rpcError(accepted.error)); assert.deepEqual( { status: (accepted.data as Record).status, case_status: (accepted.data as Record).case_status, saved_time: (accepted.data as Record).saved_time, candidate_id: (accepted.data as Record).candidate_id, }, { status: "accepted", case_status: "candidate_accepted", saved_time: "05:07", candidate_id: representativeCandidateId, }, ); assert.equal( fixture.psql(` select active_birth_time || ':' || birth_time_status || ':' || selection_kind || ':' || selected_candidate_id from public.profiles p join public.agentic_rectification_results r on r.user_id = p.id where r.id = '${resultId}' `), `05:07:00:accepted:user_accepted:${representativeCandidateId}`, ); for (const rpcName of [ "get_agentic_rectification_case", "get_agentic_rectification_case_dossier", ]) { const restoredAfterAccept = await service.rpc(rpcName, { p_user_id: userId, p_case_id: caseId, }); assert.equal(restoredAfterAccept.error, null, rpcError(restoredAfterAccept.error)); const latestResult = objectField(restoredAfterAccept.data, "latest_result"); assert.equal(latestResult.selected_candidate_id, representativeCandidateId); } const acceptReplay = await service.rpc("accept_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: resultId, p_candidate_id: representativeCandidateId, p_request_id: acceptRequestId, }); assert.equal(acceptReplay.error, null, rpcError(acceptReplay.error)); assert.equal((acceptReplay.data as Record).idempotent, true); const requestConflict = await service.rpc("accept_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: resultId, p_candidate_id: nonRepresentativeCandidateId, p_request_id: acceptRequestId, }); assert.match( rpcError(requestConflict.error), /agentic_rectification_candidate_request_conflict/, ); const ungrounded = await service.rpc("confirm_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: resultId, p_candidate_id: representativeCandidateId, p_request_id: "90000000-0000-4000-8000-000000000001", p_consent_quote: "这段话没有出现", p_source_turn_id: consentTurnId, }); assert.match(rpcError(ungrounded.error), /agentic_rectification_consent_not_grounded/); const confirmRequestId = "90000000-0000-4000-8000-000000000002"; const confirmed = await service.rpc("confirm_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: resultId, p_candidate_id: representativeCandidateId, p_request_id: confirmRequestId, p_consent_quote: "确认使用05:07", p_source_turn_id: consentTurnId, }); assert.equal(confirmed.error, null, rpcError(confirmed.error)); assert.deepEqual( { status: (confirmed.data as Record).status, case_status: (confirmed.data as Record).case_status, saved_time: (confirmed.data as Record).saved_time, }, { status: "confirmed", case_status: "confirmed", saved_time: "05:07" }, ); assert.equal( fixture.psql(` select p.birth_time_status || ':' || r.selection_kind || ':' || c.status || ':' || c.confirmed_time || ':' || r.selected_candidate_id from public.profiles p join public.agentic_rectification_results r on r.user_id = p.id join public.agentic_rectification_cases c on c.id = r.case_id where r.id = '${resultId}' `), `confirmed:engine_confirmed:confirmed:05:07:00:${representativeCandidateId}`, ); const confirmReplay = await service.rpc("confirm_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: resultId, p_candidate_id: representativeCandidateId, p_request_id: confirmRequestId, p_consent_quote: "确认使用05:07", p_source_turn_id: consentTurnId, }); assert.equal(confirmReplay.error, null, rpcError(confirmReplay.error)); assert.equal((confirmReplay.data as Record).idempotent, true); } finally { try { await closeLocalPostgresDataPool(serviceUrl); } finally { fixture.stop(); } } });