import assert from "node:assert/strict"; import { existsSync, readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { loadV9TurnReceipt, setV10ConversationFocus, } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { createRectificationActivityReceiptState, receiptFromRectificationActivityState, reduceRectificationActivityReceipt, } from "../src/lib/rectification-activity-receipt.ts"; import { CASE_ID, EVIDENCE_ID, FOCUS_ID, TURN_ID, USER_ID, activeFocusFixture, fakeAccounting, } from "./rectification-v9-test-support.ts"; const migrationFilename = "20260815010000_rectification_focus_receipt_consistency.sql"; const migration = readFileSync( new URL(`../supabase/migrations/${migrationFilename}`, import.meta.url), "utf8", ); const migrationCopy = fileURLToPath( new URL(`../db/migrations/${migrationFilename}`, import.meta.url), ); function migrationFunction(name: string, nextMarker: string): string { const start = migration.indexOf(`create or replace function public.${name}`); const end = migration.indexOf(nextMarker, start); assert.notEqual(start, -1, `missing migration function ${name}`); assert.notEqual(end, -1, `missing boundary after migration function ${name}`); return migration.slice(start, end); } test("focus/receipt consistency migration is forward-only and business-tree only", () => { assert.ok(migrationFilename > "20260814060000_product_feature_flags.sql"); assert.match(migration, /^-- Rectification focus RPC[\s\S]*\nbegin;[\s\S]*^commit;$/m); assert.equal(existsSync(migrationCopy), false); }); test("SQL-shaped focus contract returns the same complete row on create and idempotent replay", () => { const focusFunction = migrationFunction( "set_agentic_rectification_conversation_focus", "revoke all on function public.set_agentic_rectification_conversation_focus", ); assert.equal((focusFunction.match(/'focus', to_jsonb\(v_focus\)/g) ?? []).length, 2); assert.match(focusFunction, /'focus', to_jsonb\(v_focus\),\s*'idempotent', true/); assert.match(focusFunction, /'focus', to_jsonb\(v_focus\),\s*'idempotent', false/); assert.doesNotMatch(focusFunction, /'focus_id'/); }); test("tool service accepts the identical SQL focus envelope for create and replay", async () => { let callCount = 0; const focus = activeFocusFixture({ targetEvidenceId: EVIDENCE_ID, expectedAnswerSchema: { required: ["month"] }, }); const accounting = fakeAccounting({ set_agentic_rectification_conversation_focus: () => { callCount += 1; return { focus, idempotent: callCount === 2 }; }, }); const input = { questionId: "career-month-question", intent: "clarify_event_date", targetEvidenceId: EVIDENCE_ID, targetDomain: "career", targetKind: "career_entry", expectedAnswerSchema: { required: ["month"] }, } as const; const created = await setV10ConversationFocus(accounting.client, USER_ID, CASE_ID, input); const replayed = await setV10ConversationFocus(accounting.client, USER_ID, CASE_ID, input); assert.deepEqual(replayed.focus, created.focus); assert.equal(created.focus.id, FOCUS_ID); assert.equal(created.idempotent, false); assert.equal(replayed.idempotent, true); }); test("persisted receipt selects the latest attempt and exposes latest terminal tool activity", async () => { const receiptFunction = migrationFunction( "get_agentic_rectification_turn_receipt", "revoke all on function public.get_agentic_rectification_turn_receipt", ); assert.match(receiptFunction, /where turn_id = p_turn_id\s+order by attempt_number desc\s+limit 1/); assert.match(receiptFunction, /tr\.status in \('completed', 'failed'\)/); assert.match(receiptFunction, /select distinct on \(tr\.tool_name\)/); assert.match(receiptFunction, /'tool_activities', v_tool_activities/); assert.doesNotMatch(receiptFunction, /where tr\.turn_id = p_turn_id and tr\.status = 'completed'/); const accounting = fakeAccounting({ get_agentic_rectification_turn_receipt: () => ({ turn_id: TURN_ID, attempt_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", status: "retryable", skill_name: "jyotish-birth-time-rectification", skill_version: "9.0.0", engine_version: null, phases: [{ phase: "run.failed", tool: null }], tool_activities: [ { tool: "rectification-read-case", status: "completed", methods: [] }, { tool: "rectification-set-focus", status: "failed", methods: ["d1-rashi"] }, ], tools: ["rectification-read-case"], methods: [], started_at: "2026-08-15T01:00:00.000Z", completed_at: null, }), }); const receipt = await loadV9TurnReceipt(accounting.client, USER_ID, CASE_ID, TURN_ID); assert.ok(receipt); assert.deepEqual(receipt.toolActivities, [ { tool: "rectification-read-case", status: "completed", methods: [] }, { tool: "rectification-set-focus", status: "failed", methods: [] }, ]); const projectActivities = (activities: typeof receipt.toolActivities) => { let state = createRectificationActivityReceiptState(); for (const activity of activities) { state = reduceRectificationActivityReceipt(state, activity); } return receiptFromRectificationActivityState(state); }; const persistedView = projectActivities(receipt.toolActivities); const liveView = projectActivities([ { tool: "rectification-read-case", status: "completed", methods: [] }, { tool: "rectification-set-focus", status: "failed", methods: [] }, ]); assert.deepEqual(persistedView, liveView); assert.deepEqual(persistedView, { steps: ["rectification-read-case"], methods: [], failedTool: "rectification-set-focus", }); }); const caseRoute = readFileSync( new URL("../src/app/api/rectification/cases/[caseId]/route.ts", import.meta.url), "utf8", ); const chat = readFileSync( new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8", ); test("refresh projection rebuilds the same activity receipt from persisted terminal states", () => { assert.match(caseRoute, /tool_activities: receipt\.toolActivities\.map/); assert.match(chat, /Array\.isArray\(receipt\.tool_activities\)/); assert.match(chat, /reduceRectificationActivityReceipt\(state/); assert.match(chat, /receiptFromRectificationActivityState\(state\)/); });