Files
Jyotisha/frontend/tests/rectification-v10-focus-receipt-consistency.test.ts

162 lines
6.3 KiB
TypeScript

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