import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { acceptV9Candidate, confirmV9BirthTime } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { CASE_ID, SESSION_ID, RESULT_ID, CANDIDATE_ID, TURN_ID, USER_ID, candidateSnapshotFixture, dossierFixture, fakeAccounting } from "./rectification-v9-test-support.ts"; // Synthetic RPC output boundary fixtures, not engine confirmation evidence. const civil = { saved_date: "2000-06-14", saved_timezone_offset: -4, day_offset: -1, date_changed: true }; const malformed = [ { ...civil, saved_date: "2000-02-30" }, { ...civil, saved_timezone_offset: "-4" }, { ...civil, saved_timezone_offset: null }, { ...civil, saved_timezone_offset: 15 }, { saved_date: civil.saved_date }, { ...civil, day_offset: 2 }, { ...civil, date_changed: false }, ]; for (const kind of ["accept", "confirm"] as const) { test(`${kind} validates saved civil output and preserves date-less legacy`, async () => { async function invoke(fields: Record) { const accounting = fakeAccounting({ get_agentic_rectification_case_dossier: () => dossierFixture({ latestResult: candidateSnapshotFixture() }), [`${kind}_agentic_rectification_candidate_for_case_v2`]: () => ({ success: true, saved_time: "23:55", status: kind === "accept" ? "accepted" : "confirmed", result_id: RESULT_ID, ...fields, }), }); return kind === "accept" ? acceptV9Candidate(accounting.client, USER_ID, CASE_ID, RESULT_ID, CANDIDATE_ID, TURN_ID) : confirmV9BirthTime(accounting.client, USER_ID, CASE_ID, { resultId: RESULT_ID, candidateId: CANDIDATE_ID, requestId: TURN_ID, consentQuote: "Confirm synthetic boundary", sourceTurnId: TURN_ID, }); } const valid = await invoke(civil); assert.equal(valid.savedDate, civil.saved_date); assert.equal(valid.savedTimezoneOffset, civil.saved_timezone_offset); assert.equal(valid.dayOffset, -1); assert.equal(valid.dateChanged, true); assert.equal(Object.hasOwn(await invoke({}), "savedDate"), false); for (const fields of malformed) await assert.rejects(invoke(fields), /invalid_saved_civil_date/); }); } function executeRouteRows() { const script = ` import { mock } from 'node:test'; import { pathToFileURL } from 'node:url'; let fields = {}; const accounting = { rpc: async (name) => ({ error: null, data: name === 'get_agentic_rectification_case' ? { session_id: ${JSON.stringify(SESSION_ID)} } : { success: true, saved_time: '23:55', status: 'accepted', result_id: ${JSON.stringify(RESULT_ID)}, ...fields } }) }; mock.module('server-only', { namedExports: {} }); mock.module('@/lib/supabase/server', { namedExports: { createServerSupabaseClient: async () => ({ auth: { getUser: async () => ({ data: { user: { id: ${JSON.stringify(USER_ID)} } }, error: null }) } }) } }); mock.module('@/lib/supabase/admin', { namedExports: { createAdminSupabaseClient: () => accounting } }); mock.module('@/lib/product-access', { namedExports: { isProductEnabled: async () => true } }); mock.module('@/lib/rectification-agentic/v9/request-cache', { namedExports: { withRectificationRequestCache: x => x } }); mock.module('@/lib/rectification-agentic/v9/tool-service', { namedExports: { RectificationToolServiceError: class extends Error {}, loadV9CaseDossier: async () => ({ evidence: [], latestResult: null }), evidenceLedgerFingerprint: () => 'synthetic' } }); mock.module('@/lib/rectification-agentic/v9/result-identity', { namedExports: { assertV9ResultWritable: async () => {} } }); mock.module('@/lib/rectification-agentic/v9/interview-state', { namedExports: { decideFromDossier: () => ({}), overlayPublicDecision: () => ({ can_adopt: true }) } }); mock.module('@/lib/rectification-agentic/core/rectification-decision', { namedExports: { publicDecisionFields: () => ({ can_adopt: true }) } }); mock.module('@/lib/rectification-agentic/v9/answer-choice', { namedExports: { persistNextInterviewIfIdle: async () => {} } }); const { POST } = await import(pathToFileURL(process.cwd() + '/src/app/api/rectification/cases/[caseId]/candidates/accept/route.ts').href); const results = []; for (fields of ${JSON.stringify([civil, {}, ...malformed])}) { const response = await POST(new Request('https://example.invalid/api/rectification/cases/synthetic/candidates/accept', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(${JSON.stringify({ sessionId: SESSION_ID, resultId: RESULT_ID, candidateId: CANDIDATE_ID, requestId: TURN_ID, })}) }), { params: Promise.resolve({ caseId: ${JSON.stringify(CASE_ID)} }) }); results.push({ status: response.status, body: await response.json() }); } console.log(JSON.stringify(results)); `; const result = spawnSync(process.execPath, ["--experimental-test-module-mocks", "--import", "tsx", "--input-type=module", "--eval", script], { cwd: fileURLToPath(new URL("../", import.meta.url)), encoding: "utf8", }); assert.equal(result.status, 0, result.stderr || result.stdout); return JSON.parse(result.stdout.trim().split("\n").at(-1)!) as { status: number; body: Record }[]; } let routeRows: ReturnType; test("UI adoption route preserves complete civil output and date-less legacy", () => { routeRows = executeRouteRows(); assert.equal(routeRows[0].status, 200); assert.equal(routeRows[0].body.ok, true); for (const [key, value] of Object.entries(civil)) assert.equal(routeRows[0].body[key], value, key); assert.equal(routeRows[1].status, 200); assert.equal(routeRows[1].body.ok, true); assert.equal(Object.hasOwn(routeRows[1].body, "saved_date"), false); }); test("UI adoption route cannot report success for malformed or partial saved civil output", () => { routeRows ??= executeRouteRows(); for (const row of routeRows.slice(2)) { assert.ok(row.status >= 400, JSON.stringify(row)); assert.notEqual(row.body.ok, true); } });