136 lines
6.6 KiB
TypeScript
136 lines
6.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
const migration = readFileSync(
|
|
new URL("../supabase/migrations/20260814010000_immutable_skill_registry.sql", import.meta.url),
|
|
"utf8",
|
|
);
|
|
|
|
test("skill registry migration is additive and binds full case identity", () => {
|
|
assert.match(migration, /add column if not exists skill_sha256 text/);
|
|
assert.match(migration, /add column if not exists skill_source_commit text/);
|
|
assert.match(migration, /open_agentic_rectification_case_v2/);
|
|
assert.match(migration, /get_agentic_rectification_skill_identity/);
|
|
assert.match(migration, /get_agentic_rectification_skill_identity_status/);
|
|
assert.doesNotMatch(migration, /^\s*(?:drop table|drop column|truncate table)\b/im);
|
|
});
|
|
|
|
test("skill upgrades and legacy adoption are explicit, owner-scoped, and receipt-backed", () => {
|
|
assert.match(migration, /where id = p_case_id and user_id = p_user_id/);
|
|
assert.match(migration, /agentic_rectification_resumable_statuses\(\)/);
|
|
assert.match(migration, /upgrade_agentic_rectification_skill_v2/);
|
|
assert.match(migration, /adopt_agentic_rectification_skill_v1/);
|
|
assert.match(migration, /previous_skill_sha256 text check/);
|
|
assert.match(migration, /alter column previous_skill_sha256 drop not null/);
|
|
assert.match(migration, /previous_identity_status in \('verified', 'legacy_unverifiable'\)/);
|
|
assert.match(migration, /upgrade_kind in \('version_upgrade', 'legacy_adoption'\)/);
|
|
assert.match(migration, /where upgrade_kind = 'legacy_adoption'/);
|
|
assert.match(migration, /'previous_identity_status', 'legacy_unverifiable'/);
|
|
assert.match(migration, /'upgrade_kind', 'legacy_adoption'/);
|
|
assert.match(migration, /agentic_rectification_skill_identity_already_verified/);
|
|
assert.match(migration, /receipt_id/);
|
|
});
|
|
|
|
test("incomplete historical identities remain identifiable and fail closed for execution", () => {
|
|
assert.match(migration, /agentic_rectification_legacy_skill_identity_unverifiable/);
|
|
assert.match(migration, /'skill_identity_status',[\s\S]*'legacy_unverifiable'/);
|
|
assert.match(migration, /'requires_skill_adoption'/);
|
|
assert.match(migration, /v_case\.skill_sha256 is null/);
|
|
assert.match(migration, /if not v_is_new then/);
|
|
});
|
|
|
|
test("open request replays fail closed when the supplied full identity drifts", () => {
|
|
assert.match(migration, /v_case\.skill_name is distinct from p_skill_name/);
|
|
assert.match(migration, /v_case\.skill_version is distinct from p_skill_version/);
|
|
assert.match(migration, /v_case\.skill_sha256 is distinct from p_skill_sha256/);
|
|
assert.match(migration, /v_case\.skill_source_commit is distinct from p_skill_source_commit/);
|
|
assert.match(migration, /agentic_rectification_skill_identity_mismatch/);
|
|
});
|
|
|
|
test("run receipts are request-scoped and append-only across regeneration", () => {
|
|
assert.match(migration, /request_id uuid not null/);
|
|
assert.match(migration, /run_kind text not null/);
|
|
assert.match(migration, /run_kind in \('turn', 'regeneration'\)/);
|
|
assert.match(migration, /case_id, turn_id, request_id, run_kind/);
|
|
assert.match(migration, /on conflict \(case_id, turn_id, request_id, run_kind\) do nothing/);
|
|
assert.doesNotMatch(migration, /on conflict \(turn_id\) do update/);
|
|
assert.match(migration, /agentic_rectification_skill_receipt_conflict/);
|
|
assert.match(
|
|
migration,
|
|
/from public\.agentic_rectification_cases\s+where id = p_case_id and user_id = p_user_id\s+for update;/,
|
|
);
|
|
});
|
|
|
|
test("run receipt schema repair is safe on repeated execution", () => {
|
|
assert.match(migration, /add column if not exists request_id uuid/);
|
|
assert.match(migration, /add column if not exists run_kind text/);
|
|
assert.match(migration, /create unique index agentic_rectification_skill_run_receipts_request_idx/);
|
|
});
|
|
|
|
test("immutable Skill migration is restricted to schema_owner", () => {
|
|
assert.match(
|
|
migration,
|
|
/if current_user <> 'schema_owner' then[\s\S]*immutable_skill_registry_requires_schema_owner/,
|
|
);
|
|
});
|
|
|
|
test("migration transaction is owned by the migration runner", () => {
|
|
assert.doesNotMatch(migration, /^\s*begin\s*;/im);
|
|
assert.doesNotMatch(migration, /^\s*commit\s*;/im);
|
|
});
|
|
|
|
test("case and receipt tables expose no direct service-role DML", () => {
|
|
assert.match(
|
|
migration,
|
|
/revoke all on table public\.agentic_rectification_cases from service_role/,
|
|
);
|
|
assert.match(
|
|
migration,
|
|
/revoke all on table public\.agentic_rectification_skill_upgrade_receipts\s+from public, anon, authenticated, service_role/,
|
|
);
|
|
assert.match(
|
|
migration,
|
|
/revoke all on table public\.agentic_rectification_skill_run_receipts\s+from public, anon, authenticated, service_role/,
|
|
);
|
|
assert.doesNotMatch(
|
|
migration,
|
|
/grant (?:all|select|insert|update|delete) on table public\.agentic_rectification_skill_(?:upgrade|run)_receipts to service_role/,
|
|
);
|
|
});
|
|
|
|
test("append-only receipt triggers reject direct mutation but allow missing-parent cascades", () => {
|
|
assert.match(migration, /agentic_rectification_skill_upgrade_receipts_append_only/);
|
|
assert.match(migration, /agentic_rectification_skill_run_receipts_append_only/);
|
|
assert.match(migration, /agentic_rectification_skill_upgrade_receipts_truncate_guard/);
|
|
assert.match(migration, /agentic_rectification_skill_run_receipts_truncate_guard/);
|
|
assert.match(migration, /before update or delete/);
|
|
assert.match(
|
|
migration,
|
|
/before truncate on public\.agentic_rectification_skill_upgrade_receipts/,
|
|
);
|
|
assert.match(
|
|
migration,
|
|
/before truncate on public\.agentic_rectification_skill_run_receipts/,
|
|
);
|
|
assert.match(migration, /if tg_op = 'TRUNCATE' then/);
|
|
assert.match(migration, /if tg_op = 'UPDATE' then/);
|
|
assert.match(migration, /select 1 from public\.agentic_rectification_cases where id = old\.case_id/);
|
|
assert.match(migration, /select 1 from public\.agentic_rectification_turns where id = old\.turn_id/);
|
|
assert.match(migration, /select 1 from auth\.users where id = old\.user_id/);
|
|
assert.match(migration, /array\['turn_id'\]::text\[\]/);
|
|
assert.match(migration, /drop constraint %I/);
|
|
assert.match(migration, /drop index public\.%I/);
|
|
assert.match(migration, /agentic_rectification_skill_receipts_are_append_only/);
|
|
});
|
|
|
|
test("case identity guard remains defense in depth", () => {
|
|
assert.match(migration, /guard_agentic_rectification_case_skill_identity/);
|
|
assert.match(
|
|
migration,
|
|
/before update of skill_name, skill_version, skill_sha256, skill_source_commit/,
|
|
);
|
|
assert.match(migration, /current_user <> v_table_owner/);
|
|
assert.match(migration, /agentic_rectification_skill_identity_immutable/);
|
|
});
|