162 lines
6.3 KiB
TypeScript
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\)/);
|
|
});
|