1138 lines
56 KiB
TypeScript
1138 lines
56 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import test from "node:test";
|
|
|
|
const migration = readFileSync(
|
|
new URL(
|
|
"../supabase/migrations/20260812010000_agentic_rectification_v9_runtime.sql",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
const dbMigrationsCopy = fileURLToPath(
|
|
new URL("../db/migrations/20260812010000_agentic_rectification_v9_runtime.sql", import.meta.url),
|
|
);
|
|
|
|
test("v9 runtime migration sorts after the newest business migration and stays unique", () => {
|
|
assert.ok(
|
|
"20260812010000_agentic_rectification_v9_runtime.sql" >
|
|
"20260811010000_consultation_status_service_role_read.sql",
|
|
);
|
|
assert.match(migration, /^begin;[\s\S]*^commit;$/m);
|
|
});
|
|
|
|
test("v9 runtime migration must never be duplicated into the identity foundation", () => {
|
|
assert.equal(
|
|
existsSync(dbMigrationsCopy),
|
|
false,
|
|
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
|
);
|
|
});
|
|
|
|
test("v9 runtime migration creates the five durable tables with required columns", () => {
|
|
assert.match(migration, /create table if not exists public\.agentic_rectification_cases \(/);
|
|
assert.match(migration, /user_id uuid not null references auth\.users\(id\) on delete cascade/);
|
|
assert.match(migration, /session_id uuid not null unique references public\.chat_sessions\(id\) on delete cascade/);
|
|
assert.match(migration, /baseline_profile_fingerprint text not null/);
|
|
assert.match(migration, /baseline_birth_snapshot jsonb not null/);
|
|
assert.match(migration, /skill_version text not null/);
|
|
assert.match(migration, /accepted_time time without time zone/);
|
|
assert.match(migration, /confirmed_time time without time zone/);
|
|
assert.match(migration, /completed_at timestamptz/);
|
|
assert.match(migration, /closed_reason text/);
|
|
|
|
assert.match(migration, /create table if not exists public\.agentic_rectification_evidence \(/);
|
|
assert.match(migration, /source_turn_id uuid not null references public\.agentic_rectification_turns\(id\)/);
|
|
assert.match(migration, /user_quote text not null/);
|
|
assert.match(migration, /event_kind text not null/);
|
|
assert.match(migration, /date_precision text not null check \(date_precision in \('year', 'month', 'day', 'range', 'unknown'\)\)/);
|
|
assert.match(migration, /supersedes_evidence_id uuid references public\.agentic_rectification_evidence\(id\)/);
|
|
|
|
assert.match(migration, /create table if not exists public\.agentic_rectification_turns \(/);
|
|
assert.match(migration, /status text not null check \(status in \('pending', 'completed', 'failed', 'retryable'\)\)/);
|
|
assert.match(migration, /model_name text not null/);
|
|
const turnsTable = migration.slice(
|
|
migration.indexOf("create table if not exists public.agentic_rectification_turns"),
|
|
migration.indexOf("-- 3. Evidence"),
|
|
);
|
|
assert.doesNotMatch(turnsTable, /reasoning/);
|
|
|
|
assert.match(migration, /create table if not exists public\.agentic_rectification_tool_receipts \(/);
|
|
assert.match(migration, /input_fingerprint text/);
|
|
assert.match(migration, /result_fingerprint text/);
|
|
assert.match(migration, /safe_error_code text/);
|
|
assert.doesNotMatch(migration, /create table if not exists public\.agentic_rectification_tool_receipts \([\s\S]*payload/);
|
|
|
|
assert.match(migration, /create table if not exists public\.agentic_rectification_open_ledger \(/);
|
|
assert.match(migration, /primary key \(user_id, request_id\)/);
|
|
});
|
|
|
|
test("v9 runtime migration enforces one resumable case per user at the database level", () => {
|
|
assert.match(
|
|
migration,
|
|
/create unique index if not exists agentic_rectification_cases_one_resumable_per_user[\s\S]*where status in \([\s\S]*'draft'[\s\S]*'collecting_evidence'[\s\S]*'candidate_ready'[\s\S]*'candidate_accepted'[\s\S]*'needs_rebaseline'[\s\S]*'paused'[\s\S]*\)/,
|
|
);
|
|
});
|
|
|
|
test("v9 runtime migration keeps Case/Session bidirectional consistency", () => {
|
|
assert.match(migration, /alter table public\.chat_sessions\s+add column if not exists agentic_rectification_case_id uuid/);
|
|
assert.match(migration, /create unique index if not exists chat_sessions_agentic_rectification_case_unique/);
|
|
assert.match(migration, /agentic_rectification_cases_sync_session/);
|
|
assert.match(migration, /agentic_rectification_case_session_mismatch/);
|
|
assert.match(migration, /agentic_rectification_case_owner_mismatch/);
|
|
});
|
|
|
|
test("v9 runtime migration extends results with case_id and fingerprints", () => {
|
|
assert.match(migration, /add column if not exists case_id uuid/);
|
|
assert.match(migration, /add column if not exists evidence_ledger_fingerprint text/);
|
|
assert.match(migration, /add column if not exists candidate_range_fingerprint text/);
|
|
assert.match(migration, /add column if not exists skill_version text/);
|
|
});
|
|
|
|
test("v9 runtime migration grants only service_role on the new tables", () => {
|
|
for (const table of [
|
|
"agentic_rectification_cases",
|
|
"agentic_rectification_turns",
|
|
"agentic_rectification_evidence",
|
|
"agentic_rectification_tool_receipts",
|
|
"agentic_rectification_open_ledger",
|
|
]) {
|
|
assert.match(migration, new RegExp(`revoke all on table public\\.${table} from public, anon, authenticated, service_role`));
|
|
assert.match(migration, new RegExp(`grant all on table public\\.${table} to service_role`));
|
|
assert.doesNotMatch(migration, new RegExp(`grant select on table public\\.${table} to authenticated`));
|
|
}
|
|
});
|
|
|
|
test("v9 RPCs are security definer and service-role only", () => {
|
|
for (const functionName of [
|
|
"open_agentic_rectification_case",
|
|
"get_agentic_rectification_entry_summary",
|
|
"get_agentic_rectification_case",
|
|
"close_agentic_rectification_case",
|
|
"upgrade_agentic_rectification_skill",
|
|
"append_agentic_rectification_turn",
|
|
"insert_agentic_rectification_tool_receipt",
|
|
"propose_agentic_rectification_evidence",
|
|
"confirm_agentic_rectification_evidence",
|
|
"revise_agentic_rectification_evidence",
|
|
]) {
|
|
assert.match(migration, new RegExp(`create or replace function public\\.${functionName}\\(`));
|
|
assert.match(migration, new RegExp(`grant execute on function public\\.${functionName}\\([\\s\\S]*?to service_role`));
|
|
}
|
|
assert.doesNotMatch(
|
|
migration,
|
|
/grant execute on function public\.open_agentic_rectification_case\([\s\S]*?to authenticated/,
|
|
);
|
|
});
|
|
|
|
test("open RPC serializes same-user requests and forbids silent supersede", () => {
|
|
assert.match(migration, /pg_catalog\.pg_advisory_xact_lock\(/);
|
|
assert.match(migration, /hashtext\('agentic_rectification_open:' \|\| p_user_id::text\)/);
|
|
assert.match(migration, /agentic_rectification_active_case_conflict/);
|
|
assert.doesNotMatch(migration, /supersede_active/);
|
|
assert.doesNotMatch(migration, /p_supersede/);
|
|
});
|
|
|
|
test("open RPC creates the case and session atomically and derives shouldStartOpening", () => {
|
|
const open = migration.slice(
|
|
migration.indexOf("create or replace function public.open_agentic_rectification_case"),
|
|
migration.indexOf("-- 12. Entry summary"),
|
|
);
|
|
assert.match(open, /insert into public\.chat_sessions/);
|
|
assert.match(open, /insert into public\.agentic_rectification_cases/);
|
|
assert.match(open, /insert into public\.agentic_rectification_open_ledger/);
|
|
assert.match(open, /exception when unique_violation/);
|
|
assert.match(open, /'should_start_opening', true/);
|
|
// Snapshot/range validation lives inside the create block, before the
|
|
// inserts -- a session/view-only open never needs a complete profile.
|
|
const createBlock = open.slice(
|
|
open.indexOf("Create a new case + a new session atomically"),
|
|
open.lastIndexOf("return jsonb_build_object("),
|
|
);
|
|
assert.match(createBlock, /agentic_rectification_profile_incomplete/);
|
|
assert.match(createBlock, /agentic_rectification_invalid_range/);
|
|
});
|
|
|
|
test("open RPC never creates a case for an incomplete profile", () => {
|
|
assert.match(migration, /agentic_rectification_profile_incomplete/);
|
|
assert.match(migration, /p_baseline_birth_snapshot ->> 'birth_date' is null/);
|
|
});
|
|
|
|
test("terminal cases reject evidence and turn writes", () => {
|
|
const propose = migration.slice(
|
|
migration.indexOf("create or replace function public.propose_agentic_rectification_evidence"),
|
|
migration.indexOf("create or replace function public.confirm_agentic_rectification_evidence"),
|
|
);
|
|
assert.match(propose, /agentic_rectification_case_terminal/);
|
|
const append = migration.slice(
|
|
migration.indexOf("create or replace function public.append_agentic_rectification_turn"),
|
|
migration.indexOf("-- 17. Tool receipt"),
|
|
);
|
|
assert.match(append, /agentic_rectification_case_terminal/);
|
|
});
|
|
|
|
test("evidence proposals require quote grounding in the source turn", () => {
|
|
assert.match(migration, /agentic_rectification_quote_not_grounded/);
|
|
assert.match(migration, /agentic_rectification_normalize_quote\(p_user_quote\)/);
|
|
assert.match(migration, /agentic_rectification_normalize_quote\(v_turn\.user_message\)/);
|
|
});
|
|
|
|
test("evidence revisions are append-only and never generate confirmed evidence from text", () => {
|
|
assert.match(migration, /set status = 'superseded'/);
|
|
assert.match(migration, /supersedes_evidence_id[\s\S]*'pending_confirmation', v_target\.id/);
|
|
const backfill = migration.slice(
|
|
migration.indexOf("create or replace function public.backfill_agentic_rectification_legacy_cases"),
|
|
migration.indexOf("-- 20. Backfill verification"),
|
|
);
|
|
assert.doesNotMatch(backfill, /insert into public\.agentic_rectification_evidence/);
|
|
});
|
|
|
|
test("legacy backfill maps every documented status and keeps one resumable per user", () => {
|
|
const backfill = migration.slice(
|
|
migration.indexOf("create or replace function public.backfill_agentic_rectification_legacy_cases"),
|
|
migration.indexOf("-- 20. Backfill verification"),
|
|
);
|
|
assert.match(backfill, /when result_kind = 'engine_confirmed' then 'confirmed'/);
|
|
assert.match(backfill, /when result_kind = 'user_accepted' then 'candidate_accepted'/);
|
|
assert.match(backfill, /when has_messages then case when has_results then 'candidate_ready' else 'collecting_evidence' end/);
|
|
assert.match(backfill, /then 'superseded'/);
|
|
assert.match(backfill, /then 'draft'/);
|
|
assert.match(backfill, /agentic_rectification_legacy_fingerprint/);
|
|
assert.match(backfill, /update public\.agentic_rectification_results\s+set case_id = v_case_id/);
|
|
assert.match(backfill, /not exists \(\s*select 1 from public\.agentic_rectification_cases c where c\.session_id = s\.id\s*\)/);
|
|
});
|
|
|
|
test("backfill apply runs inside the migration and verification is re-runnable", () => {
|
|
assert.match(migration, /select public\.backfill_agentic_rectification_legacy_cases\(\) into v_result/);
|
|
assert.match(migration, /create or replace function public\.verify_agentic_rectification_backfill\(\)/);
|
|
assert.match(migration, /resumable_conflicts/);
|
|
assert.match(migration, /orphan_cases/);
|
|
});
|
|
|
|
test("v9 fingerprint uses the already-installed pgcrypto digest", () => {
|
|
assert.match(migration, /public\.digest\(/);
|
|
assert.match(migration, /'sha256'/);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 20260813010000_agentic_rectification_v9_agent_api.sql
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const agentApiMigration = readFileSync(
|
|
new URL(
|
|
"../supabase/migrations/20260813010000_agentic_rectification_v9_agent_api.sql",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
|
|
test("v9 agent api migration sorts after the v9 runtime and stays a single transaction", () => {
|
|
assert.ok(
|
|
"20260813010000_agentic_rectification_v9_agent_api.sql" >
|
|
"20260812010000_agentic_rectification_v9_runtime.sql",
|
|
);
|
|
assert.match(agentApiMigration, /^begin;[\s\S]*^commit;$/m);
|
|
});
|
|
|
|
test("v9 agent api migration adds the durable run phases table for skill evidence", () => {
|
|
assert.match(agentApiMigration, /create table if not exists public\.agentic_rectification_run_phases \(/);
|
|
assert.match(agentApiMigration, /phase text not null check \(\s*phase in \(/);
|
|
assert.match(agentApiMigration, /'skill\.started', 'skill\.loaded'/);
|
|
assert.match(agentApiMigration, /alter table public\.agentic_rectification_run_phases enable row level security/);
|
|
assert.match(agentApiMigration, /grant all on table public\.agentic_rectification_run_phases to service_role/);
|
|
});
|
|
|
|
test("v9 agent api migration adds dossier, finalize, candidate and consent RPCs", () => {
|
|
assert.match(agentApiMigration, /create or replace function public\.get_agentic_rectification_case_dossier\(/);
|
|
assert.match(agentApiMigration, /create or replace function public\.get_agentic_rectification_case_compute\(/);
|
|
assert.match(agentApiMigration, /create or replace function public\.finalize_agentic_rectification_turn\(/);
|
|
assert.match(agentApiMigration, /create or replace function public\.persist_agentic_rectification_candidate\(/);
|
|
assert.match(agentApiMigration, /create or replace function public\.accept_agentic_rectification_candidate_for_case\(/);
|
|
assert.match(agentApiMigration, /create or replace function public\.confirm_agentic_rectification_birth_time\(/);
|
|
assert.match(agentApiMigration, /create or replace function public\.transition_agentic_rectification_case_status\(/);
|
|
assert.match(agentApiMigration, /create or replace function public\.insert_agentic_rectification_run_phase\(/);
|
|
assert.match(agentApiMigration, /create or replace function public\.get_agentic_rectification_turn_receipt\(/);
|
|
});
|
|
|
|
test("candidate fingerprint cache reuse and terminal/skill-version guards are enforced", () => {
|
|
assert.match(agentApiMigration, /evidence_ledger_fingerprint = p_evidence_ledger_fingerprint/);
|
|
assert.match(agentApiMigration, /candidate_range_fingerprint = p_candidate_range_fingerprint/);
|
|
assert.match(agentApiMigration, /agentic_rectification_skill_version_mismatch/);
|
|
assert.match(agentApiMigration, /agentic_rectification_case_terminal/);
|
|
assert.match(agentApiMigration, /agentic_rectification_confirmation_blocked/);
|
|
assert.match(agentApiMigration, /agentic_rectification_confirm_time_mismatch/);
|
|
});
|
|
|
|
test("accept replay idempotency precedes the profile baseline check", () => {
|
|
// Regression guard: a second accept of the same candidate must return
|
|
// idempotent=true, not fail with candidate_profile_changed. The profile
|
|
// legitimately diverges from the baseline snapshot after the first accept,
|
|
// so the replay branch (selected_time already set) must come BEFORE the
|
|
// baseline comparison. Verified against a real PostgreSQL 17 run.
|
|
const acceptFn = agentApiMigration.slice(
|
|
agentApiMigration.indexOf("create or replace function public.accept_agentic_rectification_candidate_for_case"),
|
|
agentApiMigration.indexOf("-- ---------------------------------------------------------------------------\n-- 6. Confirm birth time"),
|
|
);
|
|
const idempotentBranch = acceptFn.indexOf("if v_result.selected_time is not null then");
|
|
const profileCheck = acceptFn.indexOf("agentic_rectification_candidate_profile_changed");
|
|
assert.ok(idempotentBranch >= 0, "idempotent replay branch must exist");
|
|
assert.ok(profileCheck >= 0, "profile baseline check must exist");
|
|
assert.ok(
|
|
idempotentBranch < profileCheck,
|
|
"idempotent replay must be evaluated before the profile baseline check",
|
|
);
|
|
// The replay validates the profile against the accepted selection, not the
|
|
// stale baseline snapshot.
|
|
const replayProfileCheck = acceptFn.slice(idempotentBranch, acceptFn.indexOf("v_snapshot := v_case.baseline_birth_snapshot;"));
|
|
assert.match(replayProfileCheck, /v_profile\.active_birth_time is distinct from v_result\.selected_time/);
|
|
});
|
|
|
|
test("confirmation gate requires a consent quote grounded in the source turn", () => {
|
|
assert.match(agentApiMigration, /agentic_rectification_consent_not_grounded/);
|
|
assert.match(agentApiMigration, /agentic_rectification_normalize_quote\(p_consent_quote\)/);
|
|
assert.match(agentApiMigration, /agentic_rectification_normalize_quote\(v_turn\.user_message\)/);
|
|
assert.match(agentApiMigration, /reaches 'confirmed'/);
|
|
assert.match(agentApiMigration, /birth_time_status = 'confirmed'/);
|
|
});
|
|
|
|
test("needs_rebaseline guard flips resumable cases on profile change", () => {
|
|
assert.match(agentApiMigration, /create or replace function public\.agentic_rectification_profiles_rebaseline_guard\(\)/);
|
|
assert.match(agentApiMigration, /create trigger agentic_rectification_profiles_rebaseline_guard_trigger/);
|
|
assert.match(agentApiMigration, /status = 'needs_rebaseline'/);
|
|
assert.match(agentApiMigration, /agentic_rectification_resumable_statuses\(\)/);
|
|
});
|
|
|
|
test("v9 agent api migration seeds the DB-driven runtime selector flag", () => {
|
|
assert.match(agentApiMigration, /'rectification_runtime_version'/);
|
|
assert.match(agentApiMigration, /\"version\":\"v9\",\"legacy_mode\":\"readonly\"/);
|
|
assert.match(agentApiMigration, /on conflict \(flag_key, version\) do nothing/);
|
|
});
|
|
|
|
test("v9 agent api migration must never be duplicated into the identity foundation", () => {
|
|
const copy = fileURLToPath(
|
|
new URL("../db/migrations/20260813010000_agentic_rectification_v9_agent_api.sql", import.meta.url),
|
|
);
|
|
assert.equal(
|
|
existsSync(copy),
|
|
false,
|
|
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 20260813020000_rectification_birth_context_activity.sql
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const birthContextActivityMigration = readFileSync(
|
|
new URL(
|
|
"../supabase/migrations/20260813020000_rectification_birth_context_activity.sql",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
|
|
const birthContextActivityMigrationCopy = fileURLToPath(
|
|
new URL(
|
|
"../db/migrations/20260813020000_rectification_birth_context_activity.sql",
|
|
import.meta.url,
|
|
),
|
|
);
|
|
|
|
test("birth-context activity migration follows the existing V9 API migration in one transaction", () => {
|
|
assert.ok(
|
|
"20260813020000_rectification_birth_context_activity.sql" >
|
|
"20260813010000_agentic_rectification_v9_agent_api.sql",
|
|
);
|
|
assert.match(birthContextActivityMigration, /^-- Created 2026-08-12\.[\s\S]*\nbegin;[\s\S]*^commit;$/m);
|
|
});
|
|
|
|
test("birth-context activity migration persists only allowlisted completed tool methods", () => {
|
|
assert.match(
|
|
birthContextActivityMigration,
|
|
/add column if not exists executed_methods jsonb not null default '\[\]'::jsonb/,
|
|
);
|
|
assert.match(
|
|
birthContextActivityMigration,
|
|
/drop function if exists public\.insert_agentic_rectification_tool_receipt\(\s*uuid, uuid, uuid, text, text, text, text, text, text, text\s*\)/,
|
|
);
|
|
const insertReceipt = birthContextActivityMigration.slice(
|
|
birthContextActivityMigration.indexOf("create or replace function public.insert_agentic_rectification_tool_receipt"),
|
|
birthContextActivityMigration.indexOf("create or replace function public.get_agentic_rectification_turn_receipt"),
|
|
);
|
|
assert.match(insertReceipt, /p_executed_methods jsonb/);
|
|
assert.match(insertReceipt, /uuid, uuid, uuid, text, text, text, text, text, text, text, jsonb/);
|
|
for (const method of [
|
|
"d1-rashi", "d2-hora", "d4-chaturthamsha", "d9-navamsa", "d10-dashamsa",
|
|
"d11-labhamsha", "d24-chaturvimshamsha", "d30-trimshamsha",
|
|
"vimshottari-dasha", "narayana-dasha", "gochara", "ashtakavarga",
|
|
"shadbala", "arudha-pada", "functional-benefic-malefic",
|
|
]) {
|
|
assert.match(insertReceipt, new RegExp(`'${method}'`));
|
|
}
|
|
|
|
const getReceipt = birthContextActivityMigration.slice(
|
|
birthContextActivityMigration.indexOf("create or replace function public.get_agentic_rectification_turn_receipt"),
|
|
birthContextActivityMigration.indexOf("create temporary table rectification_birth_context_affected_cases"),
|
|
);
|
|
assert.equal((getReceipt.match(/tr\.status = 'completed'/g) ?? []).length, 2);
|
|
assert.match(getReceipt, /'methods', v_methods/);
|
|
});
|
|
|
|
test("birth-context profile changes rebaseline resumable cases and invalidate active results", () => {
|
|
assert.match(birthContextActivityMigration, /baseline_birth_snapshot[\s\S]*'birth_place_label'[\s\S]*'timezone_id'/);
|
|
assert.match(
|
|
birthContextActivityMigration,
|
|
/update public\.agentic_rectification_cases c\s+set status = 'needs_rebaseline'[\s\S]*from rectification_birth_context_affected_cases affected\s+where c\.id = affected\.id;/,
|
|
);
|
|
assert.match(birthContextActivityMigration, /status = any \(public\.agentic_rectification_resumable_statuses\(\)\)/);
|
|
assert.match(birthContextActivityMigration, /update public\.agentic_rectification_results r[\s\S]*r\.invalidated_at is null/);
|
|
assert.ok((birthContextActivityMigration.match(/old\.birth_place_label is distinct from new\.birth_place_label/g) ?? []).length >= 2);
|
|
assert.ok((birthContextActivityMigration.match(/old\.timezone_id is distinct from new\.timezone_id/g) ?? []).length >= 2);
|
|
assert.ok((birthContextActivityMigration.match(/after update of birth_date, birth_place_label/g) ?? []).length >= 2);
|
|
});
|
|
|
|
test("birth-context activity migration stays out of the identity migration tree", () => {
|
|
assert.equal(
|
|
existsSync(birthContextActivityMigrationCopy),
|
|
false,
|
|
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 20260813030000_rectification_turn_message_projection.sql
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const turnMessageProjectionMigration = readFileSync(
|
|
new URL(
|
|
"../supabase/migrations/20260813030000_rectification_turn_message_projection.sql",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
|
|
const turnMessageProjectionMigrationCopy = fileURLToPath(
|
|
new URL(
|
|
"../db/migrations/20260813030000_rectification_turn_message_projection.sql",
|
|
import.meta.url,
|
|
),
|
|
);
|
|
|
|
test("rectification turn projection migration follows the V9 activity migration", () => {
|
|
assert.ok(
|
|
"20260813030000_rectification_turn_message_projection.sql" >
|
|
"20260813020000_rectification_birth_context_activity.sql",
|
|
);
|
|
assert.match(turnMessageProjectionMigration, /^-- Preserve both logical messages[\s\S]*\nbegin;[\s\S]*^commit;$/m);
|
|
});
|
|
|
|
test("rectification dossier expands each physical turn into user and assistant messages", () => {
|
|
assert.match(
|
|
turnMessageProjectionMigration,
|
|
/cross join lateral \(\s*values\s*\(1, 'user'::text, t\.user_message\),\s*\(2, 'assistant'::text, t\.assistant_message\)\s*\) as message\(ordinal, role, text\)/,
|
|
);
|
|
assert.match(turnMessageProjectionMigration, /'role', message\.role/);
|
|
assert.match(turnMessageProjectionMigration, /'text', message\.text/);
|
|
assert.match(turnMessageProjectionMigration, /order by t\.created_at, message\.ordinal/);
|
|
assert.match(turnMessageProjectionMigration, /and message\.text is not null/);
|
|
assert.doesNotMatch(
|
|
turnMessageProjectionMigration,
|
|
/coalesce\(t\.user_message, t\.assistant_message\)/,
|
|
);
|
|
});
|
|
|
|
test("rectification turn projection migration stays out of the identity migration tree", () => {
|
|
assert.equal(
|
|
existsSync(turnMessageProjectionMigrationCopy),
|
|
false,
|
|
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 20260813040000_allow_parallel_rectification_cases.sql
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const parallelCasesMigration = readFileSync(
|
|
new URL(
|
|
"../supabase/migrations/20260813040000_allow_parallel_rectification_cases.sql",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
|
|
const parallelCasesMigrationCopy = fileURLToPath(
|
|
new URL(
|
|
"../db/migrations/20260813040000_allow_parallel_rectification_cases.sql",
|
|
import.meta.url,
|
|
),
|
|
);
|
|
|
|
test("parallel rectification migration follows the turn projection and stays transactional", () => {
|
|
assert.ok(
|
|
"20260813040000_allow_parallel_rectification_cases.sql" >
|
|
"20260813030000_rectification_turn_message_projection.sql",
|
|
);
|
|
assert.match(parallelCasesMigration, /^begin;[\s\S]*^commit;$/m);
|
|
});
|
|
|
|
test("parallel rectification migration removes the single-active-case restriction", () => {
|
|
assert.match(
|
|
parallelCasesMigration,
|
|
/drop index if exists public\.agentic_rectification_cases_one_resumable_per_user/,
|
|
);
|
|
assert.doesNotMatch(parallelCasesMigration, /agentic_rectification_active_case_conflict/);
|
|
assert.doesNotMatch(
|
|
parallelCasesMigration,
|
|
/if p_intent = 'homepage'[\s\S]*?'disposition', 'resumed'/,
|
|
);
|
|
});
|
|
|
|
test("parallel rectification open keeps request idempotency and exact-session recovery", () => {
|
|
assert.match(parallelCasesMigration, /pg_catalog\.pg_advisory_xact_lock\(/);
|
|
assert.match(parallelCasesMigration, /l\.request_id = p_request_id/);
|
|
assert.match(parallelCasesMigration, /if p_intent = 'session' then/);
|
|
assert.match(parallelCasesMigration, /where id = p_session_id and user_id = p_user_id/);
|
|
assert.match(parallelCasesMigration, /insert into public\.chat_sessions/);
|
|
assert.match(parallelCasesMigration, /insert into public\.agentic_rectification_cases/);
|
|
assert.match(parallelCasesMigration, /insert into public\.agentic_rectification_open_ledger/);
|
|
});
|
|
|
|
test("parallel rectification migration stays out of the identity migration tree", () => {
|
|
assert.equal(
|
|
existsSync(parallelCasesMigrationCopy),
|
|
false,
|
|
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 20260813050000_allow_rectification_candidate_reselection.sql
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const candidateReselectionMigration = readFileSync(
|
|
new URL(
|
|
"../supabase/migrations/20260813050000_allow_rectification_candidate_reselection.sql",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
|
|
const candidateReselectionMigrationCopy = fileURLToPath(
|
|
new URL(
|
|
"../db/migrations/20260813050000_allow_rectification_candidate_reselection.sql",
|
|
import.meta.url,
|
|
),
|
|
);
|
|
|
|
test("candidate reselection migration follows parallel cases and stays transactional", () => {
|
|
assert.ok(
|
|
"20260813050000_allow_rectification_candidate_reselection.sql" >
|
|
"20260813040000_allow_parallel_rectification_cases.sql",
|
|
);
|
|
assert.match(candidateReselectionMigration, /^begin;[\s\S]*^commit;$/m);
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/create or replace function public\.accept_agentic_rectification_candidate_for_case\(/,
|
|
);
|
|
});
|
|
|
|
test("candidate reselection preserves replay and safely changes only accepted candidates", () => {
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/v_result\.selected_time is not distinct from p_time[\s\S]*v_case\.accepted_time is not distinct from p_time[\s\S]*'idempotent', true/,
|
|
);
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/v_case\.status is distinct from 'candidate_accepted'/,
|
|
);
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/v_result\.selection_kind is distinct from 'user_accepted'/,
|
|
);
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/v_profile\.active_birth_time is distinct from v_result\.selected_time/,
|
|
);
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/v_profile\.birth_time_status is distinct from 'accepted'/,
|
|
);
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/set selected_time = p_time,[\s\S]*selection_kind = 'user_accepted'/,
|
|
);
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/set active_birth_time = p_time,[\s\S]*birth_time = p_time,[\s\S]*birth_time_status = 'accepted'/,
|
|
);
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/set status = 'candidate_accepted',[\s\S]*accepted_time = p_time/,
|
|
);
|
|
assert.doesNotMatch(
|
|
candidateReselectionMigration,
|
|
/selected_time is distinct from p_time[\s\S]{0,160}agentic_rectification_candidate_already_selected/,
|
|
);
|
|
});
|
|
|
|
test("candidate reselection keeps terminal and confirmed selections immutable", () => {
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/v_case\.status in \('confirmed', 'closed', 'abandoned', 'superseded'\)[\s\S]*agentic_rectification_case_terminal/,
|
|
);
|
|
assert.match(
|
|
candidateReselectionMigration,
|
|
/v_result\.selection_kind is distinct from 'user_accepted'[\s\S]*agentic_rectification_candidate_selection_blocked/,
|
|
);
|
|
assert.match(candidateReselectionMigration, /birth_time_status = 'accepted'/);
|
|
assert.doesNotMatch(
|
|
candidateReselectionMigration,
|
|
/set selected_time = p_time,[\s\S]{0,180}selection_kind = 'engine_confirmed'/,
|
|
);
|
|
});
|
|
|
|
test("candidate reselection migration stays out of the identity migration tree", () => {
|
|
assert.equal(
|
|
existsSync(candidateReselectionMigrationCopy),
|
|
false,
|
|
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 20260813060000_rectification_turn_regeneration.sql
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const turnRegenerationMigration = readFileSync(
|
|
new URL(
|
|
"../supabase/migrations/20260813060000_rectification_turn_regeneration.sql",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
const turnRegenerationMigrationCopy = fileURLToPath(
|
|
new URL(
|
|
"../db/migrations/20260813060000_rectification_turn_regeneration.sql",
|
|
import.meta.url,
|
|
),
|
|
);
|
|
|
|
test("turn regeneration migration is forward-only, transactional and business-tree only", () => {
|
|
assert.ok(
|
|
"20260813060000_rectification_turn_regeneration.sql"
|
|
> "20260813050000_allow_rectification_candidate_reselection.sql",
|
|
);
|
|
assert.match(turnRegenerationMigration, /^begin;[\s\S]*^commit;$/m);
|
|
assert.equal(
|
|
existsSync(turnRegenerationMigrationCopy),
|
|
false,
|
|
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
|
);
|
|
});
|
|
|
|
test("turn regeneration is ownership-bound, latest-only and request-id idempotent", () => {
|
|
assert.match(turnRegenerationMigration, /where id = p_case_id and user_id = p_user_id/);
|
|
assert.match(turnRegenerationMigration, /if v_case\.session_id <> p_session_id/);
|
|
assert.match(turnRegenerationMigration, /where request_id = p_request_id/);
|
|
assert.match(turnRegenerationMigration, /agentic_rectification_regeneration_request_conflict/);
|
|
assert.match(turnRegenerationMigration, /pg_advisory_xact_lock/);
|
|
assert.match(turnRegenerationMigration, /v_case\.status in \('confirmed', 'closed', 'abandoned', 'superseded'\)/);
|
|
assert.match(turnRegenerationMigration, /where id = p_turn_id and case_id = p_case_id/);
|
|
assert.match(turnRegenerationMigration, /v_turn\.status <> 'completed'/);
|
|
assert.match(turnRegenerationMigration, /order by created_at desc, id desc[\s\S]*limit 1/);
|
|
assert.match(turnRegenerationMigration, /v_latest_turn_id is distinct from p_turn_id/);
|
|
});
|
|
|
|
test("turn regeneration updates only the existing Assistant text and timestamp", () => {
|
|
const update = turnRegenerationMigration.slice(
|
|
turnRegenerationMigration.indexOf("update public.agentic_rectification_turns"),
|
|
turnRegenerationMigration.indexOf("insert into public.agentic_rectification_turn_regenerations"),
|
|
);
|
|
const setClause = update.slice(update.indexOf("set "), update.indexOf("where "));
|
|
assert.match(update, /set assistant_message = v_message,[\s\S]*updated_at = pg_catalog\.now\(\)/);
|
|
assert.doesNotMatch(setClause, /user_message|status\s*=|case_id\s*=|session_id\s*=/);
|
|
assert.doesNotMatch(
|
|
turnRegenerationMigration,
|
|
/update public\.(?:agentic_rectification_evidence|agentic_rectification_results|agentic_rectification_cases|profiles)/,
|
|
);
|
|
assert.match(turnRegenerationMigration, /create or replace function public\.get_agentic_rectification_turn_regeneration/);
|
|
assert.match(turnRegenerationMigration, /create or replace function public\.regenerate_agentic_rectification_turn/);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 20260814020000_rectification_v10_runtime.sql
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const rectificationV10MigrationFilename = "20260814020000_rectification_v10_runtime.sql";
|
|
const rectificationV10Migration = readFileSync(
|
|
new URL(`../supabase/migrations/${rectificationV10MigrationFilename}`, import.meta.url),
|
|
"utf8",
|
|
);
|
|
const rectificationV10MigrationCopy = fileURLToPath(
|
|
new URL(`../db/migrations/${rectificationV10MigrationFilename}`, import.meta.url),
|
|
);
|
|
|
|
const rectificationV10Phases = [
|
|
"run.started",
|
|
"skill.started",
|
|
"skill.loaded",
|
|
"skill.bound",
|
|
"case.loaded",
|
|
"intent.classified",
|
|
"evidence.proposed",
|
|
"evidence.confirmed",
|
|
"candidates.comparing",
|
|
"candidates.updated",
|
|
"diagnostics.completed",
|
|
"candidate.accepted",
|
|
"birth_time.confirmed",
|
|
"answer.composed",
|
|
"billing.settled",
|
|
"answer.delta",
|
|
"run.completed",
|
|
"run.failed",
|
|
];
|
|
const rectificationV10Tools = [
|
|
"rectification-read-case",
|
|
"rectification-set-focus",
|
|
"rectification-resolve-focus",
|
|
"rectification-record-evidence-batch",
|
|
"rectification-propose-evidence",
|
|
"rectification-confirm-evidence",
|
|
"rectification-revise-evidence",
|
|
"rectification-compare-candidates",
|
|
"rectification-read-diagnostics",
|
|
"rectification-offer-candidates",
|
|
"rectification-accept-candidate",
|
|
"rectification-confirm-birth-time",
|
|
"rectification-close-case",
|
|
];
|
|
const rectificationV10Methods = [
|
|
"d1-rashi",
|
|
"d2-hora",
|
|
"d4-chaturthamsha",
|
|
"d9-navamsa",
|
|
"d10-dashamsa",
|
|
"d11-labhamsha",
|
|
"d24-chaturvimshamsha",
|
|
"d30-trimshamsha",
|
|
"vimshottari-dasha",
|
|
"narayana-dasha",
|
|
"gochara",
|
|
"ashtakavarga",
|
|
"shadbala",
|
|
"arudha-pada",
|
|
"functional-benefic-malefic",
|
|
];
|
|
|
|
function quotedSqlValues(source: string, pattern: RegExp): string[] {
|
|
const match = source.match(pattern);
|
|
assert.ok(match?.[1], `missing SQL allowlist: ${pattern}`);
|
|
return [...match[1].matchAll(/'([^']+)'/g)].map((value) => value[1]);
|
|
}
|
|
|
|
function v10Function(name: string, nextMarker: string): string {
|
|
const start = rectificationV10Migration.indexOf(`create or replace function public.${name}`);
|
|
const end = rectificationV10Migration.indexOf(nextMarker, start);
|
|
assert.notEqual(start, -1, `missing V10 function ${name}`);
|
|
assert.notEqual(end, -1, `missing V10 function boundary after ${name}`);
|
|
return rectificationV10Migration.slice(start, end);
|
|
}
|
|
|
|
test("V10 runtime migration is forward-only, transactional and business-tree only", () => {
|
|
assert.ok(
|
|
rectificationV10MigrationFilename > "20260814010000_immutable_skill_registry.sql",
|
|
);
|
|
assert.match(rectificationV10Migration, /^-- Rectification V10[\s\S]*\nbegin;[\s\S]*^commit;$/m);
|
|
assert.equal(
|
|
existsSync(rectificationV10MigrationCopy),
|
|
false,
|
|
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
|
);
|
|
});
|
|
|
|
test("V10 runtime migration creates durable attempts, focus and server summary tables", () => {
|
|
const attemptsTable = rectificationV10Migration.slice(
|
|
rectificationV10Migration.indexOf("create table if not exists public.agentic_rectification_run_attempts"),
|
|
rectificationV10Migration.indexOf("create index if not exists agentic_rectification_run_attempts_turn_idx"),
|
|
);
|
|
assert.match(attemptsTable, /case_id uuid not null references public\.agentic_rectification_cases\(id\) on delete cascade/);
|
|
assert.match(attemptsTable, /turn_id uuid not null references public\.agentic_rectification_turns\(id\) on delete cascade/);
|
|
assert.match(attemptsTable, /attempt_number integer not null check \(attempt_number > 0\)/);
|
|
assert.match(attemptsTable, /status in \('started', 'completed', 'failed', 'retryable', 'aborted'\)/);
|
|
assert.match(attemptsTable, /usage jsonb not null default '\{\}'::jsonb check \(jsonb_typeof\(usage\) = 'object'\)/);
|
|
assert.match(attemptsTable, /unique \(turn_id, attempt_number\)/);
|
|
assert.match(attemptsTable, /unique \(id, case_id, turn_id\)/);
|
|
assert.match(attemptsTable, /check \(status = 'started' or completed_at is not null\)/);
|
|
|
|
const focusTable = rectificationV10Migration.slice(
|
|
rectificationV10Migration.indexOf("create table if not exists public.agentic_rectification_conversation_focuses"),
|
|
rectificationV10Migration.indexOf("create unique index if not exists agentic_rectification_one_active_focus_per_case"),
|
|
);
|
|
assert.match(focusTable, /case_id uuid not null references public\.agentic_rectification_cases\(id\) on delete cascade/);
|
|
assert.match(focusTable, /target_evidence_id uuid references public\.agentic_rectification_evidence\(id\) on delete set null/);
|
|
assert.match(focusTable, /expected_answer_schema jsonb not null default '\{\}'::jsonb[\s\S]*jsonb_typeof\(expected_answer_schema\) = 'object'/);
|
|
assert.match(focusTable, /status in \('active', 'resolved', 'declined', 'skipped', 'superseded'\)/);
|
|
assert.match(focusTable, /unique \(case_id, question_id\)/);
|
|
assert.match(focusTable, /\(status = 'active' and resolved_at is null\)[\s\S]*\(status <> 'active' and resolved_at is not null\)/);
|
|
assert.match(rectificationV10Migration, /agentic_rectification_one_active_focus_per_case[\s\S]*where status = 'active'/);
|
|
|
|
const summaryTable = rectificationV10Migration.slice(
|
|
rectificationV10Migration.indexOf("create table if not exists public.agentic_rectification_case_conversation_summaries"),
|
|
rectificationV10Migration.indexOf("-- Batch idempotency"),
|
|
);
|
|
assert.match(summaryTable, /case_id uuid primary key references public\.agentic_rectification_cases\(id\) on delete cascade/);
|
|
for (const arrayColumn of [
|
|
"confirmed_evidence_summary",
|
|
"pending_revisions",
|
|
"declined_skipped_topics",
|
|
"missing_evidence_categories",
|
|
]) {
|
|
assert.match(
|
|
summaryTable,
|
|
new RegExp(`${arrayColumn} jsonb not null default '\\\[\\\]'::jsonb[\\s\\S]*jsonb_typeof\\(${arrayColumn}\\) = 'array'`),
|
|
);
|
|
}
|
|
assert.match(summaryTable, /summary_version integer not null default 1 check \(summary_version > 0\)/);
|
|
assert.match(summaryTable, /active_focus is null or jsonb_typeof\(active_focus\) = 'object'/);
|
|
assert.match(summaryTable, /candidate_divergence_summary is null or jsonb_typeof\(candidate_divergence_summary\) = 'object'/);
|
|
assert.match(summaryTable, /last_result_policy is null or jsonb_typeof\(last_result_policy\) = 'object'/);
|
|
});
|
|
|
|
test("V10 runtime migration adds attempt ownership and evidence idempotency columns", () => {
|
|
assert.match(
|
|
rectificationV10Migration,
|
|
/alter table public\.agentic_rectification_run_phases\s+add column if not exists attempt_id uuid references public\.agentic_rectification_run_attempts\(id\) on delete cascade/,
|
|
);
|
|
assert.match(
|
|
rectificationV10Migration,
|
|
/alter table public\.agentic_rectification_tool_receipts\s+add column if not exists attempt_id uuid references public\.agentic_rectification_run_attempts\(id\) on delete cascade/,
|
|
);
|
|
assert.match(
|
|
rectificationV10Migration,
|
|
/alter table public\.agentic_rectification_turns\s+add column if not exists successful_attempt_id uuid references public\.agentic_rectification_run_attempts\(id\) on delete set null/,
|
|
);
|
|
assert.match(
|
|
rectificationV10Migration,
|
|
/alter table public\.agentic_rectification_turns\s+add column if not exists request_id uuid/,
|
|
);
|
|
assert.match(
|
|
rectificationV10Migration,
|
|
/create unique index if not exists agentic_rectification_turns_case_request_idx\s+on public\.agentic_rectification_turns \(case_id, request_id\)\s+where request_id is not null/,
|
|
);
|
|
assert.match(
|
|
rectificationV10Migration,
|
|
/alter table public\.agentic_rectification_evidence\s+add column if not exists idempotency_key text/,
|
|
);
|
|
assert.match(
|
|
rectificationV10Migration,
|
|
/create unique index if not exists agentic_rectification_evidence_idempotency_idx\s+on public\.agentic_rectification_evidence \(case_id, idempotency_key\)\s+where idempotency_key is not null/,
|
|
);
|
|
});
|
|
|
|
test("V10 tables and RPCs remain service-role-only security-definer surfaces", () => {
|
|
for (const table of [
|
|
"agentic_rectification_run_attempts",
|
|
"agentic_rectification_conversation_focuses",
|
|
"agentic_rectification_case_conversation_summaries",
|
|
]) {
|
|
assert.match(rectificationV10Migration, new RegExp(`alter table public\\.${table} enable row level security`));
|
|
assert.match(rectificationV10Migration, new RegExp(`revoke all on table public\\.${table} from public, anon, authenticated, service_role`));
|
|
assert.doesNotMatch(
|
|
rectificationV10Migration,
|
|
new RegExp(`grant (?:all|insert|update|delete) on table public\\.${table} to service_role`),
|
|
);
|
|
}
|
|
|
|
const rpcSignatures = [
|
|
"refresh_agentic_rectification_case_conversation_summary\\(uuid\\)",
|
|
"set_agentic_rectification_conversation_focus\\(\\s*uuid, uuid, text, text, uuid, text, text, jsonb\\s*\\)",
|
|
"resolve_agentic_rectification_conversation_focus\\(\\s*uuid, uuid, uuid, text, uuid\\s*\\)",
|
|
"confirm_agentic_rectification_evidence_v10\\(uuid, uuid, uuid, uuid\\)",
|
|
"revise_agentic_rectification_evidence_v10\\(\\s*uuid, uuid, uuid, uuid, text, date, date, text, text\\s*\\)",
|
|
"record_agentic_rectification_evidence_batch\\(\\s*uuid, uuid, uuid, uuid, jsonb\\s*\\)",
|
|
"append_agentic_rectification_turn\\(\\s*uuid, uuid, text, text, text, text, text, uuid\\s*\\)",
|
|
"create_agentic_rectification_run_attempt\\(uuid, uuid, uuid, integer\\)",
|
|
"finalize_agentic_rectification_run_attempt\\(\\s*uuid, uuid, uuid, uuid, text, text, jsonb\\s*\\)",
|
|
"insert_agentic_rectification_run_phase\\(\\s*uuid, uuid, uuid, text, text, integer, uuid\\s*\\)",
|
|
"insert_agentic_rectification_tool_receipt\\(\\s*uuid, uuid, uuid, text, text, text, text, text, text, text, jsonb, uuid\\s*\\)",
|
|
"finalize_agentic_rectification_turn\\(\\s*uuid, uuid, uuid, uuid, text, text, uuid\\s*\\)",
|
|
"get_agentic_rectification_turn_receipt\\(uuid, uuid, uuid\\)",
|
|
"get_agentic_rectification_case_dossier\\(uuid, uuid\\)",
|
|
];
|
|
for (const signature of rpcSignatures) {
|
|
assert.match(
|
|
rectificationV10Migration,
|
|
new RegExp(`grant execute on function public\\.${signature}\\s+to service_role`),
|
|
);
|
|
}
|
|
for (const legacySignature of [
|
|
"append_agentic_rectification_turn\\(\\s*uuid, uuid, text, text, text, text, text\\s*\\)",
|
|
"finalize_agentic_rectification_turn\\(\\s*uuid, uuid, uuid, text, text\\s*\\)",
|
|
"finalize_agentic_rectification_turn\\(\\s*uuid, uuid, uuid, text, text, uuid\\s*\\)",
|
|
]) {
|
|
assert.match(
|
|
rectificationV10Migration,
|
|
new RegExp(`revoke execute on function public\\.${legacySignature}\\s+from service_role`),
|
|
);
|
|
assert.doesNotMatch(
|
|
rectificationV10Migration,
|
|
new RegExp(`grant execute on function public\\.${legacySignature}\\s+to service_role`),
|
|
);
|
|
}
|
|
assert.ok((rectificationV10Migration.match(/language plpgsql\s+security definer\s+set search_path = ''/g) ?? []).length >= 15);
|
|
});
|
|
|
|
test("V10 phase, tool and technique allowlists are exact", () => {
|
|
const insertPhase = v10Function(
|
|
"insert_agentic_rectification_run_phase",
|
|
"-- New overload. The eleven-argument V9 function remains untouched.",
|
|
);
|
|
assert.deepEqual(
|
|
quotedSqlValues(insertPhase, /p_phase not in \(\s*([\s\S]*?)\s*\) then/),
|
|
rectificationV10Phases,
|
|
);
|
|
|
|
const insertReceipt = v10Function(
|
|
"insert_agentic_rectification_tool_receipt",
|
|
"-- Attempt-aware finalization.",
|
|
);
|
|
assert.deepEqual(
|
|
quotedSqlValues(insertReceipt, /p_tool_name not in \(\s*([\s\S]*?)\s*\)\s+or p_public_phase/),
|
|
rectificationV10Tools,
|
|
);
|
|
assert.deepEqual(
|
|
quotedSqlValues(insertReceipt, /p_public_phase not in \(\s*([\s\S]*?)\s*\)\s+or p_status/),
|
|
rectificationV10Phases,
|
|
);
|
|
assert.deepEqual(
|
|
quotedSqlValues(insertReceipt, /method\.value not in \(\s*([\s\S]*?)\s*\)\s*\)/),
|
|
rectificationV10Methods,
|
|
);
|
|
});
|
|
|
|
test("V10 conversation summary is server-owned and included in the bounded dossier", () => {
|
|
const refreshSummary = v10Function(
|
|
"refresh_agentic_rectification_case_conversation_summary",
|
|
"create or replace function public.agentic_rectification_refresh_conversation_summary_trigger",
|
|
);
|
|
assert.match(refreshSummary, /language plpgsql\s+security definer\s+set search_path = ''/);
|
|
assert.match(refreshSummary, /insert into public\.agentic_rectification_case_conversation_summaries/);
|
|
assert.match(refreshSummary, /on conflict \(case_id\) do update set/);
|
|
assert.match(refreshSummary, /from public\.agentic_rectification_evidence e/);
|
|
assert.match(refreshSummary, /from public\.agentic_rectification_conversation_focuses f/);
|
|
assert.match(refreshSummary, /from public\.agentic_rectification_results/);
|
|
assert.doesNotMatch(refreshSummary, /user_quote|baseline_birth_snapshot|reasoning|score_breakdown|final_score/);
|
|
|
|
const dossier = v10Function("get_agentic_rectification_case_dossier", "commit;");
|
|
assert.match(dossier, /limit 50/);
|
|
assert.match(dossier, /from public\.agentic_rectification_case_conversation_summaries/);
|
|
assert.match(dossier, /perform public\.refresh_agentic_rectification_case_conversation_summary\(v_case\.id\)/);
|
|
for (const field of [
|
|
"confirmed_evidence_summary",
|
|
"pending_revisions",
|
|
"active_focus",
|
|
"declined_skipped_topics",
|
|
"candidate_divergence_summary",
|
|
"missing_evidence_categories",
|
|
"last_result_policy",
|
|
"summary_version",
|
|
"updated_at",
|
|
]) {
|
|
assert.match(dossier, new RegExp(`'${field}', v_summary\\.${field}`));
|
|
}
|
|
});
|
|
|
|
test("V10 focus and evidence writes reject terminal cases and retire active focus", () => {
|
|
const terminalStatuses = "'confirmed', 'closed', 'abandoned', 'superseded'";
|
|
const terminalGuard = v10Function(
|
|
"agentic_rectification_terminal_focus_guard",
|
|
"revoke all on function public.agentic_rectification_terminal_focus_guard",
|
|
);
|
|
assert.match(terminalGuard, new RegExp(`new\\.status in \\(${terminalStatuses}\\)`));
|
|
assert.match(terminalGuard, /set status = 'superseded',[\s\S]*where case_id = new\.id and status = 'active'/);
|
|
|
|
for (const [name, nextMarker] of [
|
|
["set_agentic_rectification_conversation_focus", "revoke all on function public.set_agentic_rectification_conversation_focus"],
|
|
["resolve_agentic_rectification_conversation_focus", "revoke all on function public.resolve_agentic_rectification_conversation_focus"],
|
|
["record_agentic_rectification_evidence_batch", "revoke all on function public.record_agentic_rectification_evidence_batch"],
|
|
] as const) {
|
|
assert.match(v10Function(name, nextMarker), new RegExp(`status in \\(${terminalStatuses}\\)`));
|
|
}
|
|
for (const [name, nextMarker] of [
|
|
["confirm_agentic_rectification_evidence_v10", "revoke all on function public.confirm_agentic_rectification_evidence_v10"],
|
|
["revise_agentic_rectification_evidence_v10", "revoke all on function public.revise_agentic_rectification_evidence_v10"],
|
|
] as const) {
|
|
const guardedFunction = v10Function(name, nextMarker);
|
|
assert.match(guardedFunction, /where id = p_case_id and user_id = p_user_id\s+for update/);
|
|
assert.match(guardedFunction, /agentic_rectification_case_not_found/);
|
|
assert.match(guardedFunction, new RegExp(`v_case\\.status in \\(${terminalStatuses}\\)`));
|
|
assert.match(guardedFunction, /agentic_rectification_case_terminal/);
|
|
assert.doesNotMatch(guardedFunction, /agentic_rectification_case_not_found_or_terminal/);
|
|
}
|
|
});
|
|
|
|
|
|
test("V10 batch focus resolves only one matching accepted evidence with its persisted id", () => {
|
|
const batch = v10Function(
|
|
"record_agentic_rectification_evidence_batch",
|
|
"revoke all on function public.record_agentic_rectification_evidence_batch",
|
|
);
|
|
assert.match(batch, /v_outcome = 'accepted'[\s\S]*v_focus\.target_domain is null or v_domain = v_focus\.target_domain/);
|
|
assert.match(batch, /v_focus\.target_kind is null or v_kind = v_focus\.target_kind/);
|
|
assert.match(batch, /v_focus_match_count := v_focus_match_count \+ 1/);
|
|
assert.match(batch, /v_focus_match_id := v_evidence_id/);
|
|
assert.match(batch, /elsif v_focus_match_count = 1 then[\s\S]*'resolved', v_focus_match_id/);
|
|
assert.match(batch, /elsif v_focus_match_count > 1 then[\s\S]*v_focus_resolution := 'ambiguous'/);
|
|
assert.match(batch, /else\s+v_focus_resolution := 'unmatched'/);
|
|
assert.match(batch, /'focus_evidence_id', v_focus_match_id/);
|
|
assert.match(batch, /'focus_resolution', v_focus_resolution/);
|
|
});
|
|
|
|
|
|
test("V10 request-aware turn append is case-serialized and replay-safe", () => {
|
|
const appendTurn = v10Function(
|
|
"append_agentic_rectification_turn",
|
|
"revoke all on function public.append_agentic_rectification_turn",
|
|
);
|
|
assert.match(appendTurn, /p_request_id uuid/);
|
|
assert.match(appendTurn, /where id = p_case_id and user_id = p_user_id\s+for update/);
|
|
const replayLookup = appendTurn.indexOf("where case_id = p_case_id and request_id = p_request_id");
|
|
const terminalGuard = appendTurn.indexOf("if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded')");
|
|
assert.ok(replayLookup >= 0 && terminalGuard > replayLookup);
|
|
assert.match(appendTurn, /v_turn\.user_message is distinct from p_user_message/);
|
|
assert.match(appendTurn, /v_turn\.model_name is distinct from p_model_name/);
|
|
assert.match(appendTurn, /v_turn\.model_version is distinct from p_model_version/);
|
|
assert.match(appendTurn, /agentic_rectification_request_mismatch/);
|
|
assert.match(appendTurn, /'should_execute', false/);
|
|
assert.match(appendTurn, /'already_in_progress', v_turn\.status = 'pending'/);
|
|
assert.match(appendTurn, /'idempotent', true/);
|
|
assert.match(appendTurn, /'should_execute', true/);
|
|
});
|
|
|
|
test("V10 attempt and turn completion stay monotonic across terminal cases and superseded attempts", () => {
|
|
const finalizeAttempt = v10Function(
|
|
"finalize_agentic_rectification_run_attempt",
|
|
"revoke all on function public.finalize_agentic_rectification_run_attempt",
|
|
);
|
|
assert.doesNotMatch(finalizeAttempt, /v_case\.status in \('confirmed', 'closed', 'abandoned', 'superseded'\)/);
|
|
assert.doesNotMatch(finalizeAttempt, /agentic_rectification_case_terminal/);
|
|
|
|
const legacyStart = rectificationV10Migration.indexOf(
|
|
"create or replace function public.finalize_agentic_rectification_turn(\n p_user_id uuid,\n p_case_id uuid,\n p_turn_id uuid,\n p_status text",
|
|
);
|
|
const legacyEnd = rectificationV10Migration.indexOf(
|
|
"revoke all on function public.finalize_agentic_rectification_turn",
|
|
legacyStart,
|
|
);
|
|
assert.ok(legacyStart >= 0 && legacyEnd > legacyStart);
|
|
const legacyFinalizeTurn = rectificationV10Migration.slice(legacyStart, legacyEnd);
|
|
assert.match(legacyFinalizeTurn, /if v_turn\.status = 'completed' then/);
|
|
assert.match(legacyFinalizeTurn, /agentic_rectification_turn_already_completed/);
|
|
assert.match(legacyFinalizeTurn, /where id = p_turn_id and status <> 'completed'/);
|
|
|
|
const attemptAwareStart = rectificationV10Migration.indexOf(
|
|
"-- Attempt-aware monotonic finalization used by the V10 runner.",
|
|
);
|
|
const attemptAwareEnd = rectificationV10Migration.indexOf(
|
|
"-- 8. Attempt-filtered turn receipt",
|
|
attemptAwareStart,
|
|
);
|
|
assert.ok(attemptAwareStart >= 0 && attemptAwareEnd > attemptAwareStart);
|
|
const attemptAwareFinalizeTurn = rectificationV10Migration.slice(attemptAwareStart, attemptAwareEnd);
|
|
assert.match(attemptAwareFinalizeTurn, /p_attempt_id uuid/);
|
|
assert.match(attemptAwareFinalizeTurn, /where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id\s+for update/);
|
|
assert.match(attemptAwareFinalizeTurn, /order by attempt_number desc\s+limit 1/);
|
|
assert.match(attemptAwareFinalizeTurn, /agentic_rectification_attempt_superseded/);
|
|
assert.match(attemptAwareFinalizeTurn, /p_successful_attempt_id is distinct from p_attempt_id/);
|
|
assert.match(attemptAwareFinalizeTurn, /v_attempt\.status <> 'completed'/);
|
|
assert.match(attemptAwareFinalizeTurn, /v_attempt\.status <> p_status/);
|
|
assert.match(attemptAwareFinalizeTurn, /agentic_rectification_turn_already_completed/);
|
|
assert.match(attemptAwareFinalizeTurn, /where id = p_turn_id and status <> 'completed'/);
|
|
});
|
|
|
|
test("V10 attempts, phases, receipts and completed turns are ownership-bound", () => {
|
|
const createAttempt = v10Function(
|
|
"create_agentic_rectification_run_attempt",
|
|
"revoke all on function public.create_agentic_rectification_run_attempt",
|
|
);
|
|
assert.match(createAttempt, /where id = p_case_id and user_id = p_user_id\s+for update/);
|
|
assert.match(createAttempt, /where id = p_turn_id and case_id = p_case_id\s+for update/);
|
|
const existingAttemptLookup = createAttempt.indexOf(
|
|
"where turn_id = p_turn_id and attempt_number = p_attempt_number",
|
|
);
|
|
const terminalAttemptGuard = createAttempt.indexOf(
|
|
"if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded')",
|
|
);
|
|
assert.ok(existingAttemptLookup >= 0 && terminalAttemptGuard > existingAttemptLookup);
|
|
assert.match(createAttempt, /'should_execute', false/);
|
|
assert.match(createAttempt, /'already_in_progress', v_attempt\.status = 'started'/);
|
|
assert.match(createAttempt, /agentic_rectification_case_terminal/);
|
|
assert.match(createAttempt, /on conflict \(turn_id, attempt_number\) do nothing/);
|
|
assert.match(createAttempt, /'should_execute', v_inserted/);
|
|
assert.match(createAttempt, /'already_in_progress', not v_inserted and v_attempt\.status = 'started'/);
|
|
|
|
const finalizeAttempt = v10Function(
|
|
"finalize_agentic_rectification_run_attempt",
|
|
"revoke all on function public.finalize_agentic_rectification_run_attempt",
|
|
);
|
|
assert.match(finalizeAttempt, /v_error_code text := nullif\(btrim\(coalesce\(p_error_code, ''\)\), ''\)/);
|
|
assert.match(finalizeAttempt, /where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id\s+for update/);
|
|
assert.match(finalizeAttempt, /v_attempt\.error_code is not distinct from v_error_code/);
|
|
assert.match(finalizeAttempt, /error_code = v_error_code/);
|
|
assert.match(finalizeAttempt, /phase = 'billing\.settled'/);
|
|
assert.match(finalizeAttempt, /phase = 'run\.completed'/);
|
|
assert.match(finalizeAttempt, /agentic_rectification_attempt_completion_receipt_missing/);
|
|
assert.match(finalizeAttempt, /from public\.agentic_rectification_skill_run_receipts receipt/);
|
|
for (const binding of [
|
|
/receipt\.case_id = p_case_id/,
|
|
/receipt\.turn_id = p_turn_id/,
|
|
/receipt\.request_id = p_attempt_id/,
|
|
/receipt\.run_kind = 'turn'/,
|
|
/receipt\.user_id = p_user_id/,
|
|
/receipt\.skill_name = v_case\.skill_name/,
|
|
/receipt\.skill_version = v_case\.skill_version/,
|
|
/receipt\.skill_sha256 = v_case\.skill_sha256/,
|
|
/receipt\.source_commit is not distinct from v_case\.skill_source_commit/,
|
|
]) {
|
|
assert.match(finalizeAttempt, binding);
|
|
}
|
|
assert.match(finalizeAttempt, /agentic_rectification_skill_receipt_missing/);
|
|
assert.match(finalizeAttempt, /agentic_rectification_attempt_already_finalized/);
|
|
|
|
const insertPhase = v10Function(
|
|
"insert_agentic_rectification_run_phase",
|
|
"-- New overload. The eleven-argument V9 function remains untouched.",
|
|
);
|
|
const insertReceipt = v10Function(
|
|
"insert_agentic_rectification_tool_receipt",
|
|
"-- Attempt-aware finalization.",
|
|
);
|
|
for (const attemptOwnedInsertion of [insertPhase, insertReceipt]) {
|
|
assert.match(attemptOwnedInsertion, /where id = p_turn_id and case_id = p_case_id/);
|
|
assert.match(attemptOwnedInsertion, /where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id/);
|
|
assert.match(attemptOwnedInsertion, /agentic_rectification_attempt_not_found/);
|
|
assert.match(attemptOwnedInsertion, /v_attempt\.status <> 'started'/);
|
|
assert.match(attemptOwnedInsertion, /agentic_rectification_attempt_not_started/);
|
|
}
|
|
|
|
const finalizeTurn = v10Function(
|
|
"finalize_agentic_rectification_turn",
|
|
"-- 8. Attempt-filtered turn receipt",
|
|
);
|
|
assert.match(finalizeTurn, /where id = p_turn_id and case_id = p_case_id\s+for update/);
|
|
assert.match(finalizeTurn, /where id = p_successful_attempt_id[\s\S]*case_id = p_case_id[\s\S]*turn_id = p_turn_id[\s\S]*status = 'completed'/);
|
|
assert.match(finalizeTurn, /agentic_rectification_attempt_not_successful/);
|
|
assert.match(finalizeTurn, /successful_attempt_id = case when p_status = 'completed' then p_successful_attempt_id else null end/);
|
|
|
|
const receipt = v10Function(
|
|
"get_agentic_rectification_turn_receipt",
|
|
"revoke all on function public.get_agentic_rectification_turn_receipt",
|
|
);
|
|
assert.match(receipt, /v_attempt_id := v_turn\.successful_attempt_id/);
|
|
assert.match(receipt, /where turn_id = p_turn_id and status = 'completed'[\s\S]*order by completed_at desc, attempt_number desc/);
|
|
assert.ok((receipt.match(/attempt_id = v_attempt_id/g) ?? []).length >= 4);
|
|
});
|