1020 lines
39 KiB
Python
1020 lines
39 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
import uuid
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tests.docker_volume_lifecycle import (
|
|
POSTGRES_TMPFS_SPEC,
|
|
acquire_docker_volume_set_guard,
|
|
inspect_postgres_tmpfs_container,
|
|
release_docker_volume_set_guard,
|
|
remove_container_and_volumes,
|
|
)
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
MIGRATION = REPO_ROOT / "scripts" / "kb_apply_prereqs.sql"
|
|
POSTGRES_IMAGE = os.environ.get("LEOCLEAN_RUNTIME_POSTGRES_IMAGE", "postgres:16-alpine")
|
|
DATABASE = "teleo_clone"
|
|
LEGACY_APPROVED_ID = "aaaaaaaa-0000-4000-8000-000000000001"
|
|
LEGACY_APPLIED_ID = "aaaaaaaa-0000-4000-8000-000000000002"
|
|
FRESH_PROPOSAL_ID = "aaaaaaaa-0000-4000-8000-000000000003"
|
|
TIMEZONE_PROPOSAL_ID = "aaaaaaaa-0000-4000-8000-000000000004"
|
|
REVIEWER_ID = "11111111-1111-1111-1111-111111111111"
|
|
STALE_REVIEWER_ID = "22222222-2222-2222-2222-222222222222"
|
|
LEGACY_REVIEWER_HANDLE = "m3ta"
|
|
CANONICAL_REVIEWER_HANDLE = "m3taversal"
|
|
|
|
BOOTSTRAP_SQL = r"""
|
|
create role kb_gate_owner nologin inherit;
|
|
create role kb_review login inherit;
|
|
create role kb_apply login inherit;
|
|
create role broad_writer nologin;
|
|
create role rogue_owner nologin;
|
|
create role rogue_grantee nologin;
|
|
|
|
create schema kb_stage;
|
|
|
|
create table public.agents (
|
|
id uuid primary key,
|
|
handle text not null unique,
|
|
kind text not null
|
|
);
|
|
create table public.strategies (
|
|
id uuid primary key,
|
|
agent_id uuid not null,
|
|
active boolean not null default true
|
|
);
|
|
create table public.strategy_nodes (id uuid primary key);
|
|
create table public.claim_evidence (
|
|
id uuid primary key,
|
|
claim_id uuid not null,
|
|
source_id uuid not null,
|
|
role text not null
|
|
);
|
|
create table public.claim_edges (id uuid primary key);
|
|
create table public.claims (id uuid primary key);
|
|
create table public.sources (
|
|
id uuid primary key,
|
|
hash text not null
|
|
);
|
|
create table public.reasoning_tools (id uuid primary key);
|
|
|
|
insert into public.agents (id, handle, kind)
|
|
values ('11111111-1111-1111-1111-111111111111', 'm3ta', 'human');
|
|
|
|
create table kb_stage.kb_proposals (
|
|
id uuid primary key,
|
|
proposal_type text not null,
|
|
status text not null,
|
|
payload jsonb not null,
|
|
reviewed_by_handle text,
|
|
reviewed_by_agent_id uuid,
|
|
reviewed_at timestamptz,
|
|
review_note text,
|
|
applied_by_handle text,
|
|
applied_by_agent_id uuid,
|
|
applied_at timestamptz,
|
|
updated_at timestamptz not null default now()
|
|
);
|
|
create table kb_stage.kb_review_principals (
|
|
db_role name primary key,
|
|
reviewed_by_handle text not null,
|
|
reviewed_by_agent_id uuid not null references public.agents(id),
|
|
active boolean not null default true,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
create table kb_stage.kb_proposal_approvals (
|
|
proposal_id uuid primary key references kb_stage.kb_proposals(id),
|
|
proposal_type text not null,
|
|
payload jsonb not null,
|
|
reviewed_by_handle text not null,
|
|
reviewed_by_agent_id uuid not null references public.agents(id),
|
|
reviewed_by_db_role name not null,
|
|
reviewed_at timestamptz not null,
|
|
review_note text not null
|
|
);
|
|
|
|
insert into kb_stage.kb_proposals (
|
|
id, proposal_type, status, payload, reviewed_by_handle, reviewed_by_agent_id,
|
|
reviewed_at, review_note, applied_by_handle, applied_by_agent_id, applied_at, updated_at
|
|
) values
|
|
(
|
|
'aaaaaaaa-0000-4000-8000-000000000001', 'add_edge', 'approved',
|
|
'{"apply_payload":{"from_claim":"aaaaaaaa-1000-4000-8000-000000000001"}}'::jsonb,
|
|
'm3ta', '11111111-1111-1111-1111-111111111111', '2026-07-18T10:00:00Z',
|
|
'Exact still-approved legacy row.', null, null, null, '2026-07-18T10:00:01Z'
|
|
),
|
|
(
|
|
'aaaaaaaa-0000-4000-8000-000000000002', 'add_edge', 'applied',
|
|
'{"apply_payload":{"from_claim":"aaaaaaaa-1000-4000-8000-000000000002"}}'::jsonb,
|
|
'm3ta', '11111111-1111-1111-1111-111111111111', '2026-07-18T09:00:00Z',
|
|
'Applied before exact approved rows were retained.', 'kb-apply',
|
|
'44444444-4444-4444-4444-444444444444', '2026-07-18T09:01:00Z',
|
|
'2026-07-18T09:01:00Z'
|
|
);
|
|
|
|
insert into kb_stage.kb_proposal_approvals (
|
|
proposal_id, proposal_type, payload, reviewed_by_handle, reviewed_by_agent_id,
|
|
reviewed_by_db_role, reviewed_at, review_note
|
|
) values
|
|
(
|
|
'aaaaaaaa-0000-4000-8000-000000000001', 'add_edge',
|
|
'{"apply_payload":{"from_claim":"aaaaaaaa-1000-4000-8000-000000000001"}}'::jsonb,
|
|
'm3ta', '11111111-1111-1111-1111-111111111111', 'kb_review',
|
|
'2026-07-18T10:00:00Z', 'Exact still-approved legacy row.'
|
|
),
|
|
(
|
|
'aaaaaaaa-0000-4000-8000-000000000002', 'add_edge',
|
|
'{"apply_payload":{"from_claim":"aaaaaaaa-1000-4000-8000-000000000002"}}'::jsonb,
|
|
'm3ta', '11111111-1111-1111-1111-111111111111', 'kb_review',
|
|
'2026-07-18T09:00:00Z', 'Applied before exact approved rows were retained.'
|
|
);
|
|
|
|
create function kb_stage.approve_strict_proposal(uuid, text, text)
|
|
returns jsonb language sql security definer
|
|
as $function$ select '{}'::jsonb $function$;
|
|
create function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text)
|
|
returns jsonb language sql security definer
|
|
as $function$ select '{}'::jsonb $function$;
|
|
create function kb_stage.assert_approved_proposal(
|
|
uuid, text, jsonb, text, uuid, timestamptz, text
|
|
) returns void language sql security definer
|
|
as $function$ select $function$;
|
|
create function kb_stage.finish_approved_proposal(
|
|
uuid, text, jsonb, text, uuid, timestamptz, text, text
|
|
) returns jsonb language sql security definer
|
|
as $function$ select '{}'::jsonb $function$;
|
|
|
|
grant all privileges on all tables in schema public
|
|
to kb_gate_owner, kb_apply, kb_review;
|
|
grant all privileges on all tables in schema kb_stage
|
|
to public, kb_gate_owner, kb_apply, kb_review;
|
|
grant all privileges on all functions in schema kb_stage
|
|
to public, kb_gate_owner, kb_apply, kb_review;
|
|
grant update (review_note) on kb_stage.kb_proposals to kb_apply;
|
|
grant select (review_note) on kb_stage.kb_proposals to public;
|
|
"""
|
|
|
|
|
|
def _psql(container: str, sql: str, *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
completed = subprocess.run(
|
|
[
|
|
"docker",
|
|
"exec",
|
|
"-i",
|
|
container,
|
|
"psql",
|
|
"-X",
|
|
"-v",
|
|
"ON_ERROR_STOP=1",
|
|
"-U",
|
|
"postgres",
|
|
"-d",
|
|
DATABASE,
|
|
"-At",
|
|
"-F",
|
|
"|",
|
|
],
|
|
input=sql,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
if check and completed.returncode != 0:
|
|
raise AssertionError(
|
|
f"psql failed with exit {completed.returncode}\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
|
|
)
|
|
return completed
|
|
|
|
|
|
def _apply_migration(container: str, *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
return _psql(container, MIGRATION.read_text(), check=check)
|
|
|
|
|
|
@contextmanager
|
|
def _postgres_clone() -> Iterator[str]:
|
|
if shutil.which("docker") is None:
|
|
pytest.skip("Docker is required for the disposable PostgreSQL migration canary")
|
|
|
|
volume_guard = acquire_docker_volume_set_guard(
|
|
context="kb_apply_prereqs PostgreSQL fixture",
|
|
)
|
|
container = f"kb-apply-prereqs-{uuid.uuid4().hex[:12]}"
|
|
try:
|
|
started = subprocess.run(
|
|
[
|
|
"docker",
|
|
"run",
|
|
"--pull=never",
|
|
"-d",
|
|
"--name",
|
|
container,
|
|
"--tmpfs",
|
|
POSTGRES_TMPFS_SPEC,
|
|
"-e",
|
|
"POSTGRES_PASSWORD=postgres",
|
|
"-e",
|
|
f"POSTGRES_DB={DATABASE}",
|
|
POSTGRES_IMAGE,
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
if started.returncode != 0:
|
|
pytest.fail(f"could not start disposable PostgreSQL: {started.stderr}")
|
|
inspect_postgres_tmpfs_container(container)
|
|
|
|
for _ in range(120):
|
|
logs = subprocess.run(
|
|
["docker", "logs", container],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
query = _psql(container, "select 1;", check=False)
|
|
ready_transitions = (logs.stdout + logs.stderr).count("database system is ready to accept connections")
|
|
if ready_transitions >= 2 and query.returncode == 0:
|
|
break
|
|
time.sleep(0.25)
|
|
else:
|
|
pytest.fail("disposable PostgreSQL did not become ready")
|
|
|
|
_psql(container, BOOTSTRAP_SQL)
|
|
yield container
|
|
finally:
|
|
try:
|
|
remove_container_and_volumes(container)
|
|
finally:
|
|
release_docker_volume_set_guard(volume_guard)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def migrated_postgres() -> Iterator[str]:
|
|
with _postgres_clone() as container:
|
|
_apply_migration(container)
|
|
yield container
|
|
|
|
|
|
def _lines(completed: subprocess.CompletedProcess[str]) -> set[str]:
|
|
return {line for line in completed.stdout.splitlines() if line}
|
|
|
|
|
|
def test_legacy_only_reviewer_is_canonicalized_without_rewriting_history(
|
|
migrated_postgres: str,
|
|
) -> None:
|
|
identity_rows = _psql(
|
|
migrated_postgres,
|
|
f"""
|
|
select id::text || '|' || handle || '|' || kind
|
|
from public.agents
|
|
where handle in ('{LEGACY_REVIEWER_HANDLE}', '{CANONICAL_REVIEWER_HANDLE}')
|
|
order by handle;
|
|
""",
|
|
).stdout.splitlines()
|
|
assert identity_rows == [f"{REVIEWER_ID}|{CANONICAL_REVIEWER_HANDLE}|human"]
|
|
|
|
historical_rows = _psql(
|
|
migrated_postgres,
|
|
f"""
|
|
select 'proposal|' || id::text || '|' || reviewed_by_handle || '|' || reviewed_by_agent_id::text
|
|
from kb_stage.kb_proposals
|
|
where id in ('{LEGACY_APPROVED_ID}'::uuid, '{LEGACY_APPLIED_ID}'::uuid)
|
|
order by id;
|
|
select 'approval|' || proposal_id::text || '|' || reviewed_by_handle || '|' ||
|
|
reviewed_by_agent_id::text
|
|
from kb_stage.kb_proposal_approvals
|
|
where proposal_id in ('{LEGACY_APPROVED_ID}'::uuid, '{LEGACY_APPLIED_ID}'::uuid)
|
|
order by proposal_id;
|
|
""",
|
|
).stdout.splitlines()
|
|
assert historical_rows == [
|
|
f"proposal|{LEGACY_APPROVED_ID}|{LEGACY_REVIEWER_HANDLE}|{REVIEWER_ID}",
|
|
f"proposal|{LEGACY_APPLIED_ID}|{LEGACY_REVIEWER_HANDLE}|{REVIEWER_ID}",
|
|
f"approval|{LEGACY_APPROVED_ID}|{LEGACY_REVIEWER_HANDLE}|{REVIEWER_ID}",
|
|
f"approval|{LEGACY_APPLIED_ID}|{LEGACY_REVIEWER_HANDLE}|{REVIEWER_ID}",
|
|
]
|
|
|
|
|
|
def test_already_current_reviewer_migration_is_idempotent() -> None:
|
|
with _postgres_clone() as container:
|
|
_psql(
|
|
container,
|
|
f"update public.agents set handle = '{CANONICAL_REVIEWER_HANDLE}' "
|
|
f"where handle = '{LEGACY_REVIEWER_HANDLE}';",
|
|
)
|
|
_apply_migration(container)
|
|
state_sql = f"""
|
|
select principal.reviewed_by_handle || '|' || principal.reviewed_by_agent_id::text || '|' ||
|
|
principal.active::text || '|' || principal.created_at::text || '|' ||
|
|
(select count(*) from public.agents where handle = '{LEGACY_REVIEWER_HANDLE}')::text
|
|
from kb_stage.kb_review_principals principal
|
|
where principal.db_role = 'kb_review';
|
|
"""
|
|
first_state = _psql(container, state_sql).stdout.strip()
|
|
_apply_migration(container)
|
|
second_state = _psql(container, state_sql).stdout.strip()
|
|
|
|
expected_state = f"{CANONICAL_REVIEWER_HANDLE}|{REVIEWER_ID}|true|1970-01-01 00:00:00+00|0"
|
|
assert first_state == expected_state
|
|
assert second_state == first_state
|
|
|
|
|
|
def test_stale_reviewer_principal_is_repaired_without_resetting_created_at() -> None:
|
|
with _postgres_clone() as container:
|
|
_psql(
|
|
container,
|
|
f"""
|
|
insert into public.agents (id, handle, kind)
|
|
values ('{STALE_REVIEWER_ID}', 'stale-reviewer', 'human');
|
|
insert into kb_stage.kb_review_principals
|
|
(db_role, reviewed_by_handle, reviewed_by_agent_id, active, created_at)
|
|
values (
|
|
'kb_review', 'stale-reviewer', '{STALE_REVIEWER_ID}', false,
|
|
'2001-02-03T04:05:06Z'
|
|
);
|
|
""",
|
|
)
|
|
_apply_migration(container)
|
|
principal_state = _psql(
|
|
container,
|
|
"""
|
|
select reviewed_by_handle || '|' || reviewed_by_agent_id::text || '|' || active::text || '|' ||
|
|
created_at::text
|
|
from kb_stage.kb_review_principals
|
|
where db_role = 'kb_review';
|
|
""",
|
|
).stdout.strip()
|
|
|
|
expected_state = f"{CANONICAL_REVIEWER_HANDLE}|{REVIEWER_ID}|true|2001-02-03 04:05:06+00"
|
|
assert principal_state == expected_state
|
|
|
|
|
|
def test_migration_refuses_distinct_legacy_and_canonical_reviewers() -> None:
|
|
with _postgres_clone() as container:
|
|
_psql(
|
|
container,
|
|
f"""
|
|
insert into public.agents (id, handle, kind)
|
|
values ('{STALE_REVIEWER_ID}', '{CANONICAL_REVIEWER_HANDLE}', 'human');
|
|
""",
|
|
)
|
|
refused = _apply_migration(container, check=False)
|
|
identity_rows = _psql(
|
|
container,
|
|
f"""
|
|
select handle || '|' || id::text
|
|
from public.agents
|
|
where handle in ('{LEGACY_REVIEWER_HANDLE}', '{CANONICAL_REVIEWER_HANDLE}')
|
|
order by handle;
|
|
""",
|
|
).stdout.splitlines()
|
|
|
|
assert refused.returncode != 0
|
|
assert "both reviewer handles m3ta" in refused.stderr
|
|
assert "exist with different agent UUIDs" in refused.stderr
|
|
assert identity_rows == [
|
|
f"{LEGACY_REVIEWER_HANDLE}|{REVIEWER_ID}",
|
|
f"{CANONICAL_REVIEWER_HANDLE}|{STALE_REVIEWER_ID}",
|
|
]
|
|
|
|
|
|
def test_migration_refuses_to_fabricate_missing_human_reviewer() -> None:
|
|
with _postgres_clone() as container:
|
|
_psql(
|
|
container,
|
|
f"""
|
|
delete from kb_stage.kb_proposal_approvals;
|
|
delete from public.agents where handle = '{LEGACY_REVIEWER_HANDLE}';
|
|
""",
|
|
)
|
|
refused = _apply_migration(container, check=False)
|
|
remaining_agents = _psql(container, "select count(*) from public.agents;").stdout.strip()
|
|
|
|
assert refused.returncode != 0
|
|
assert "required human reviewer agent is missing" in refused.stderr
|
|
assert "handle m3ta or m3taversal" in refused.stderr
|
|
assert remaining_agents == "0"
|
|
|
|
|
|
def test_reviewer_principal_seed_timestamp_is_deterministic(migrated_postgres: str) -> None:
|
|
created_at = _psql(
|
|
migrated_postgres,
|
|
"select created_at::text from kb_stage.kb_review_principals where db_role = 'kb_review';",
|
|
).stdout.strip()
|
|
|
|
assert created_at == "1970-01-01 00:00:00+00"
|
|
|
|
|
|
def test_upgrade_backfills_only_still_exact_approved_rows(migrated_postgres: str) -> None:
|
|
snapshot_state = _psql(
|
|
migrated_postgres,
|
|
f"""
|
|
select proposal_id::text || '|' || (approved_proposal_snapshot is not null)::text || '|' ||
|
|
coalesce(approved_proposal_snapshot->>'status', 'unavailable') || '|' ||
|
|
coalesce(approved_proposal_snapshot->>'reviewed_by_handle', 'unavailable')
|
|
from kb_stage.kb_proposal_approvals
|
|
where proposal_id in ('{LEGACY_APPROVED_ID}'::uuid, '{LEGACY_APPLIED_ID}'::uuid)
|
|
order by proposal_id;
|
|
""",
|
|
).stdout.splitlines()
|
|
|
|
assert snapshot_state == [
|
|
f"{LEGACY_APPROVED_ID}|true|approved|{LEGACY_REVIEWER_HANDLE}",
|
|
f"{LEGACY_APPLIED_ID}|false|unavailable|unavailable",
|
|
]
|
|
unavailable = _psql(
|
|
migrated_postgres,
|
|
f"""
|
|
set session authorization kb_apply;
|
|
select kb_stage.export_applied_proposal_replay_rows('{LEGACY_APPLIED_ID}'::uuid);
|
|
""",
|
|
check=False,
|
|
)
|
|
assert unavailable.returncode != 0
|
|
assert "exact applied transition unavailable" in unavailable.stderr
|
|
|
|
|
|
def test_new_approval_preserves_exact_post_approval_row_for_apply_export(migrated_postgres: str) -> None:
|
|
payload = '{"apply_payload":{"from_claim":"aaaaaaaa-1000-4000-8000-000000000003"}}'
|
|
_psql(
|
|
migrated_postgres,
|
|
f"""
|
|
insert into kb_stage.kb_proposals (id, proposal_type, status, payload, updated_at)
|
|
values ('{FRESH_PROPOSAL_ID}'::uuid, 'add_edge', 'pending_review', '{payload}'::jsonb,
|
|
'2026-07-18T11:00:00Z');
|
|
set session authorization kb_review;
|
|
select kb_stage.approve_strict_proposal(
|
|
'{FRESH_PROPOSAL_ID}'::uuid, 'add_edge', '{payload}'::jsonb, '{CANONICAL_REVIEWER_HANDLE}',
|
|
'Fresh exact approval snapshot.'
|
|
);
|
|
""",
|
|
)
|
|
approval_identity = _psql(
|
|
migrated_postgres,
|
|
f"""
|
|
select proposal.reviewed_by_handle || '|' || proposal.reviewed_by_agent_id::text || '|' ||
|
|
approval.reviewed_by_handle || '|' || approval.reviewed_by_agent_id::text || '|' ||
|
|
(approval.approved_proposal_snapshot->>'reviewed_by_handle') || '|' ||
|
|
(approval.approved_proposal_snapshot->>'reviewed_by_agent_id')
|
|
from kb_stage.kb_proposals proposal
|
|
join kb_stage.kb_proposal_approvals approval on approval.proposal_id = proposal.id
|
|
where proposal.id = '{FRESH_PROPOSAL_ID}'::uuid;
|
|
""",
|
|
).stdout.strip()
|
|
assert approval_identity == (
|
|
f"{CANONICAL_REVIEWER_HANDLE}|{REVIEWER_ID}|"
|
|
f"{CANONICAL_REVIEWER_HANDLE}|{REVIEWER_ID}|"
|
|
f"{CANONICAL_REVIEWER_HANDLE}|{REVIEWER_ID}"
|
|
)
|
|
approval_state = _psql(
|
|
migrated_postgres,
|
|
f"""
|
|
select jsonb_build_object(
|
|
'matches', approval.approved_proposal_snapshot = to_jsonb(proposal),
|
|
'status', approval.approved_proposal_snapshot->>'status',
|
|
'updated_at_matches', approval.approved_proposal_snapshot->'updated_at' = to_jsonb(proposal)->'updated_at'
|
|
)::text
|
|
from kb_stage.kb_proposal_approvals approval
|
|
join kb_stage.kb_proposals proposal on proposal.id = approval.proposal_id
|
|
where proposal.id = '{FRESH_PROPOSAL_ID}'::uuid;
|
|
""",
|
|
).stdout.strip()
|
|
assert approval_state == '{"status": "approved", "matches": true, "updated_at_matches": true}'
|
|
|
|
direct_approval_read = _psql(
|
|
migrated_postgres,
|
|
"set session authorization kb_apply; select * from kb_stage.kb_proposal_approvals;",
|
|
check=False,
|
|
)
|
|
assert direct_approval_read.returncode != 0
|
|
assert "permission denied for table kb_proposal_approvals" in direct_approval_read.stderr
|
|
|
|
premature_export = _psql(
|
|
migrated_postgres,
|
|
f"""
|
|
set session authorization kb_apply;
|
|
select kb_stage.export_applied_proposal_replay_rows('{FRESH_PROPOSAL_ID}'::uuid);
|
|
""",
|
|
check=False,
|
|
)
|
|
assert premature_export.returncode != 0
|
|
assert "exact applied transition unavailable" in premature_export.stderr
|
|
|
|
_psql(
|
|
migrated_postgres,
|
|
f"""
|
|
update kb_stage.kb_proposal_approvals
|
|
set approved_proposal_snapshot =
|
|
approved_proposal_snapshot || '{{"rationale":"tampered after approval"}}'::jsonb
|
|
where proposal_id = '{FRESH_PROPOSAL_ID}'::uuid;
|
|
""",
|
|
)
|
|
for guard_call in (
|
|
"kb_stage.assert_approved_proposal("
|
|
"proposal.id, proposal.proposal_type, proposal.payload, proposal.reviewed_by_handle, "
|
|
"proposal.reviewed_by_agent_id, proposal.reviewed_at, proposal.review_note)",
|
|
"kb_stage.finish_approved_proposal("
|
|
"proposal.id, proposal.proposal_type, proposal.payload, proposal.reviewed_by_handle, "
|
|
"proposal.reviewed_by_agent_id, proposal.reviewed_at, proposal.review_note, 'kb-apply')",
|
|
):
|
|
refused = _psql(
|
|
migrated_postgres,
|
|
f"""
|
|
set session authorization kb_apply;
|
|
select {guard_call}
|
|
from kb_stage.kb_proposals proposal where proposal.id = '{FRESH_PROPOSAL_ID}'::uuid;
|
|
""",
|
|
check=False,
|
|
)
|
|
assert refused.returncode != 0
|
|
assert "immutable approved snapshot" in refused.stderr
|
|
_psql(
|
|
migrated_postgres,
|
|
f"""
|
|
update kb_stage.kb_proposal_approvals approval
|
|
set approved_proposal_snapshot = to_jsonb(proposal)
|
|
from kb_stage.kb_proposals proposal
|
|
where approval.proposal_id = proposal.id
|
|
and proposal.id = '{FRESH_PROPOSAL_ID}'::uuid;
|
|
""",
|
|
)
|
|
|
|
exported = _psql(
|
|
migrated_postgres,
|
|
f"""
|
|
set session authorization kb_apply;
|
|
select kb_stage.assert_approved_proposal(
|
|
proposal.id, proposal.proposal_type, proposal.payload, proposal.reviewed_by_handle,
|
|
proposal.reviewed_by_agent_id, proposal.reviewed_at, proposal.review_note
|
|
)
|
|
from kb_stage.kb_proposals proposal where proposal.id = '{FRESH_PROPOSAL_ID}'::uuid;
|
|
select kb_stage.finish_approved_proposal(
|
|
proposal.id, proposal.proposal_type, proposal.payload, proposal.reviewed_by_handle,
|
|
proposal.reviewed_by_agent_id, proposal.reviewed_at, proposal.review_note, 'kb-apply'
|
|
)
|
|
from kb_stage.kb_proposals proposal where proposal.id = '{FRESH_PROPOSAL_ID}'::uuid;
|
|
select jsonb_object_keys(kb_stage.export_applied_proposal_replay_rows('{FRESH_PROPOSAL_ID}'::uuid))
|
|
order by 1;
|
|
""",
|
|
).stdout.splitlines()
|
|
assert exported[-3:] == ["applied_proposal", "approval_snapshot", "approved_proposal"]
|
|
|
|
|
|
def test_gate_functions_are_timezone_independent_across_connections(
|
|
migrated_postgres: str,
|
|
) -> None:
|
|
payload = '{"apply_payload":{"from_claim":"aaaaaaaa-1000-4000-8000-000000000004"}}'
|
|
_psql(
|
|
migrated_postgres,
|
|
f"""
|
|
insert into kb_stage.kb_proposals (id, proposal_type, status, payload, updated_at)
|
|
values ('{TIMEZONE_PROPOSAL_ID}'::uuid, 'add_edge', 'pending_review', '{payload}'::jsonb,
|
|
'2026-07-18T12:00:00Z');
|
|
set timezone = 'America/Los_Angeles';
|
|
set session authorization kb_review;
|
|
select kb_stage.approve_strict_proposal(
|
|
'{TIMEZONE_PROPOSAL_ID}'::uuid, 'add_edge', '{payload}'::jsonb, '{CANONICAL_REVIEWER_HANDLE}',
|
|
'Cross-timezone exact approval snapshot.'
|
|
);
|
|
""",
|
|
)
|
|
|
|
asserted_and_finished = _psql(
|
|
migrated_postgres,
|
|
f"""
|
|
set timezone = 'Asia/Tokyo';
|
|
set session authorization kb_apply;
|
|
select kb_stage.assert_approved_proposal(
|
|
proposal.id, proposal.proposal_type, proposal.payload, proposal.reviewed_by_handle,
|
|
proposal.reviewed_by_agent_id, proposal.reviewed_at, proposal.review_note
|
|
)
|
|
from kb_stage.kb_proposals proposal where proposal.id = '{TIMEZONE_PROPOSAL_ID}'::uuid;
|
|
select kb_stage.finish_approved_proposal(
|
|
proposal.id, proposal.proposal_type, proposal.payload, proposal.reviewed_by_handle,
|
|
proposal.reviewed_by_agent_id, proposal.reviewed_at, proposal.review_note, 'kb-apply'
|
|
)
|
|
from kb_stage.kb_proposals proposal where proposal.id = '{TIMEZONE_PROPOSAL_ID}'::uuid;
|
|
""",
|
|
)
|
|
assert '"status": "applied"' in asserted_and_finished.stdout
|
|
|
|
exported = json.loads(
|
|
_psql(
|
|
migrated_postgres,
|
|
f"""
|
|
set timezone = 'Pacific/Auckland';
|
|
set session authorization kb_apply;
|
|
select kb_stage.export_applied_proposal_replay_rows('{TIMEZONE_PROPOSAL_ID}'::uuid)::text;
|
|
""",
|
|
).stdout.splitlines()[-1]
|
|
)
|
|
approved = exported["approved_proposal"]
|
|
applied = exported["applied_proposal"]
|
|
approval = exported["approval_snapshot"]
|
|
assert approved["reviewed_at"].endswith("+00:00")
|
|
assert applied["reviewed_at"] == approved["reviewed_at"]
|
|
assert approval["reviewed_at"] == approved["reviewed_at"]
|
|
assert applied["status"] == "applied"
|
|
|
|
|
|
def test_transition_export_rollback_preserves_evidence_and_reinstalls_cleanly() -> None:
|
|
with _postgres_clone() as container:
|
|
_apply_migration(container)
|
|
before = _psql(
|
|
container,
|
|
f"""
|
|
select approved_proposal_snapshot::text
|
|
from kb_stage.kb_proposal_approvals
|
|
where proposal_id = '{LEGACY_APPROVED_ID}'::uuid;
|
|
""",
|
|
).stdout.strip()
|
|
|
|
_psql(
|
|
container,
|
|
"""
|
|
revoke all on function kb_stage.export_applied_proposal_replay_rows(uuid)
|
|
from public, kb_gate_owner, kb_apply, kb_review;
|
|
drop function kb_stage.export_applied_proposal_replay_rows(uuid);
|
|
""",
|
|
)
|
|
rolled_back = _psql(
|
|
container,
|
|
f"""
|
|
select pg_catalog.to_regprocedure(
|
|
'kb_stage.export_applied_proposal_replay_rows(uuid)'
|
|
) is null;
|
|
select approved_proposal_snapshot::text
|
|
from kb_stage.kb_proposal_approvals
|
|
where proposal_id = '{LEGACY_APPROVED_ID}'::uuid;
|
|
""",
|
|
).stdout.splitlines()
|
|
assert rolled_back == ["t", before]
|
|
|
|
_apply_migration(container)
|
|
restored = _psql(
|
|
container,
|
|
f"""
|
|
select pg_catalog.to_regprocedure(
|
|
'kb_stage.export_applied_proposal_replay_rows(uuid)'
|
|
) is not null;
|
|
select approved_proposal_snapshot::text
|
|
from kb_stage.kb_proposal_approvals
|
|
where proposal_id = '{LEGACY_APPROVED_ID}'::uuid;
|
|
""",
|
|
).stdout.splitlines()
|
|
assert restored == ["t", before]
|
|
|
|
|
|
def _catalog_snapshot(container: str) -> str:
|
|
return _psql(
|
|
container,
|
|
r"""
|
|
select 'role|' || rolname || '|' || rolcanlogin || '|' || rolinherit || '|' || rolsuper
|
|
from pg_catalog.pg_roles
|
|
where rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
|
|
order by rolname;
|
|
select 'owner|' || namespace.nspname || '|' || relation.relname || '|' || owner_role.rolname
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = relation.relowner
|
|
where (namespace.nspname, relation.relname) in (
|
|
('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'),
|
|
('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'),
|
|
('public', 'sources'), ('public', 'reasoning_tools'),
|
|
('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'),
|
|
('kb_stage', 'kb_proposal_approvals')
|
|
)
|
|
order by namespace.nspname, relation.relname;
|
|
select 'function|' || procedure.proname || '(' || pg_catalog.oidvectortypes(procedure.proargtypes) ||
|
|
')|' || owner_role.rolname || '|' || procedure.prosecdef || '|' || procedure.proconfig::text
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
|
|
where namespace.nspname = 'kb_stage'
|
|
and procedure.proname in (
|
|
'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal',
|
|
'export_applied_proposal_replay_rows'
|
|
)
|
|
order by procedure.proname, procedure.proargtypes::text;
|
|
select 'column|' || attribute.attname || '|' || attribute.atttypid::regtype::text || '|' ||
|
|
attribute.attnotnull
|
|
from pg_catalog.pg_attribute attribute
|
|
where attribute.attrelid = 'kb_stage.kb_proposal_approvals'::regclass
|
|
and attribute.attname = 'approved_proposal_snapshot'
|
|
and not attribute.attisdropped;
|
|
select 'table_acl|' || namespace.nspname || '|' || relation.relname || '|' ||
|
|
case when acl.grantee = 0 then 'PUBLIC' else grantee_role.rolname end || '|' ||
|
|
pg_catalog.upper(acl.privilege_type) || '|' || acl.is_grantable
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(relation.relacl, pg_catalog.acldefault('r', relation.relowner))
|
|
) acl
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
where (namespace.nspname, relation.relname) in (
|
|
('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'),
|
|
('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'),
|
|
('public', 'sources'), ('public', 'reasoning_tools'),
|
|
('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'),
|
|
('kb_stage', 'kb_proposal_approvals')
|
|
)
|
|
and acl.grantee <> relation.relowner
|
|
and (acl.grantee = 0 or grantee_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply'))
|
|
order by namespace.nspname, relation.relname, grantee_role.rolname, acl.privilege_type;
|
|
select 'function_acl|' || procedure.proname || '(' ||
|
|
pg_catalog.oidvectortypes(procedure.proargtypes) || ')|' ||
|
|
case when acl.grantee = 0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(acl.grantee) end ||
|
|
'|' || pg_catalog.upper(acl.privilege_type) || '|' || acl.is_grantable
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
|
|
) acl
|
|
where namespace.nspname = 'kb_stage'
|
|
and procedure.proname in (
|
|
'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal',
|
|
'export_applied_proposal_replay_rows'
|
|
)
|
|
and acl.grantee <> procedure.proowner
|
|
order by procedure.proname, procedure.proargtypes::text, acl.grantee;
|
|
""",
|
|
).stdout
|
|
|
|
|
|
def test_upgrade_normalizes_exact_role_ownership_and_acl_matrix(migrated_postgres: str) -> None:
|
|
role_rows = _lines(
|
|
_psql(
|
|
migrated_postgres,
|
|
"""
|
|
select rolname || '|' || rolcanlogin || '|' || rolinherit || '|' || rolsuper || '|' ||
|
|
rolcreatedb || '|' || rolcreaterole || '|' || rolreplication || '|' || rolbypassrls
|
|
from pg_catalog.pg_roles
|
|
where rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
|
|
order by rolname;
|
|
""",
|
|
)
|
|
)
|
|
assert role_rows == {
|
|
"kb_apply|true|false|false|false|false|false|false",
|
|
"kb_gate_owner|false|false|false|false|false|false|false",
|
|
"kb_review|true|false|false|false|false|false|false",
|
|
}
|
|
|
|
assert (
|
|
_psql(
|
|
migrated_postgres,
|
|
"""
|
|
select count(*)
|
|
from pg_catalog.pg_auth_members membership
|
|
join pg_catalog.pg_roles member_role on member_role.oid = membership.member
|
|
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
|
where member_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
|
|
or granted_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply');
|
|
""",
|
|
).stdout.strip()
|
|
== "0"
|
|
)
|
|
|
|
schema_rows = _lines(
|
|
_psql(
|
|
migrated_postgres,
|
|
"""
|
|
select protected_role.rolname || '|' || protected_schema.nspname || '|' ||
|
|
pg_catalog.has_schema_privilege(protected_role.rolname, protected_schema.nspname, 'USAGE') || '|' ||
|
|
pg_catalog.has_schema_privilege(protected_role.rolname, protected_schema.nspname, 'CREATE')
|
|
from (values ('kb_gate_owner'), ('kb_review'), ('kb_apply')) protected_role(rolname)
|
|
cross join (values ('public'), ('kb_stage')) protected_schema(nspname);
|
|
""",
|
|
)
|
|
)
|
|
assert schema_rows == {
|
|
f"{role}|{schema}|true|false"
|
|
for role in ("kb_gate_owner", "kb_review", "kb_apply")
|
|
for schema in ("public", "kb_stage")
|
|
}
|
|
|
|
function_rows = _lines(
|
|
_psql(
|
|
migrated_postgres,
|
|
"""
|
|
select procedure.proname || '(' || pg_catalog.oidvectortypes(procedure.proargtypes) || ')|' ||
|
|
owner_role.rolname || '|' || procedure.prosecdef || '|' ||
|
|
(
|
|
pg_catalog.cardinality(procedure.proconfig) = 2
|
|
and procedure.proconfig @> array[
|
|
'search_path=pg_catalog, pg_temp'::text,
|
|
'TimeZone=UTC'::text
|
|
]
|
|
)
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
|
|
where namespace.nspname = 'kb_stage'
|
|
and procedure.proname in (
|
|
'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal',
|
|
'export_applied_proposal_replay_rows'
|
|
);
|
|
""",
|
|
)
|
|
)
|
|
assert function_rows == {
|
|
"approve_strict_proposal(uuid, text, jsonb, text, text)|kb_gate_owner|true|true",
|
|
"assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text)|"
|
|
"kb_gate_owner|true|true",
|
|
"finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text, text)|"
|
|
"kb_gate_owner|true|true",
|
|
"export_applied_proposal_replay_rows(uuid)|kb_gate_owner|true|true",
|
|
}
|
|
assert (
|
|
_psql(
|
|
migrated_postgres,
|
|
"select pg_catalog.to_regprocedure('kb_stage.approve_strict_proposal(uuid,text,text)') is null;",
|
|
).stdout.strip()
|
|
== "t"
|
|
)
|
|
assert (
|
|
_psql(
|
|
migrated_postgres,
|
|
"""
|
|
select count(*)
|
|
from pg_catalog.pg_attribute attribute
|
|
join pg_catalog.pg_class relation on relation.oid = attribute.attrelid
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
cross join lateral pg_catalog.aclexplode(attribute.attacl) acl
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
where (namespace.nspname, relation.relname) in (
|
|
('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'),
|
|
('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'),
|
|
('public', 'sources'), ('public', 'reasoning_tools'),
|
|
('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'),
|
|
('kb_stage', 'kb_proposal_approvals')
|
|
)
|
|
and (acl.grantee = 0 or grantee_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply'));
|
|
""",
|
|
).stdout.strip()
|
|
== "0"
|
|
)
|
|
|
|
for runtime_role, target_role in (
|
|
("kb_apply", "broad_writer"),
|
|
("kb_apply", "kb_gate_owner"),
|
|
("kb_review", "kb_gate_owner"),
|
|
):
|
|
denied = _psql(
|
|
migrated_postgres,
|
|
f"set session authorization {runtime_role}; set role {target_role};",
|
|
check=False,
|
|
)
|
|
assert denied.returncode != 0
|
|
assert "permission denied to set role" in denied.stderr
|
|
|
|
snapshot_lines = set(_catalog_snapshot(migrated_postgres).splitlines())
|
|
expected_table_acls = {
|
|
"table_acl|public|agents|kb_gate_owner|SELECT|false",
|
|
"table_acl|public|agents|kb_apply|SELECT|false",
|
|
"table_acl|public|agents|kb_review|SELECT|false",
|
|
"table_acl|public|strategies|kb_apply|SELECT|false",
|
|
"table_acl|public|strategies|kb_apply|INSERT|false",
|
|
"table_acl|public|strategies|kb_apply|UPDATE|false",
|
|
"table_acl|public|strategy_nodes|kb_apply|SELECT|false",
|
|
"table_acl|public|strategy_nodes|kb_apply|INSERT|false",
|
|
"table_acl|public|strategy_nodes|kb_apply|UPDATE|false",
|
|
"table_acl|public|claim_evidence|kb_apply|SELECT|false",
|
|
"table_acl|public|claim_evidence|kb_apply|INSERT|false",
|
|
"table_acl|public|claim_edges|kb_apply|SELECT|false",
|
|
"table_acl|public|claim_edges|kb_apply|INSERT|false",
|
|
"table_acl|public|claims|kb_apply|SELECT|false",
|
|
"table_acl|public|claims|kb_apply|INSERT|false",
|
|
"table_acl|public|sources|kb_apply|SELECT|false",
|
|
"table_acl|public|sources|kb_apply|INSERT|false",
|
|
"table_acl|public|reasoning_tools|kb_apply|SELECT|false",
|
|
"table_acl|public|reasoning_tools|kb_apply|INSERT|false",
|
|
"table_acl|kb_stage|kb_proposals|kb_gate_owner|SELECT|false",
|
|
"table_acl|kb_stage|kb_proposals|kb_gate_owner|UPDATE|false",
|
|
"table_acl|kb_stage|kb_proposals|kb_apply|SELECT|false",
|
|
"table_acl|kb_stage|kb_proposals|kb_review|SELECT|false",
|
|
}
|
|
assert {line for line in snapshot_lines if line.startswith("table_acl|")} == expected_table_acls
|
|
assert {line for line in snapshot_lines if line.startswith("function_acl|")} == {
|
|
"function_acl|approve_strict_proposal(uuid, text, jsonb, text, text)|kb_review|EXECUTE|false",
|
|
"function_acl|assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text)|"
|
|
"kb_apply|EXECUTE|false",
|
|
"function_acl|finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text, text)|"
|
|
"kb_apply|EXECUTE|false",
|
|
"function_acl|export_applied_proposal_replay_rows(uuid)|kb_apply|EXECUTE|false",
|
|
}
|
|
|
|
|
|
def test_migration_rerun_preserves_catalog_snapshot(migrated_postgres: str) -> None:
|
|
before = _catalog_snapshot(migrated_postgres)
|
|
_apply_migration(migrated_postgres)
|
|
assert _catalog_snapshot(migrated_postgres) == before
|
|
|
|
|
|
def _assert_migration_fails(container: str, expected_error: str) -> None:
|
|
completed = _apply_migration(container, check=False)
|
|
assert completed.returncode != 0
|
|
assert expected_error in completed.stderr
|
|
|
|
|
|
def test_migration_fails_closed_on_protected_role_membership(migrated_postgres: str) -> None:
|
|
_psql(migrated_postgres, "grant broad_writer to kb_apply;")
|
|
try:
|
|
_assert_migration_fails(migrated_postgres, "unexpected protected role membership")
|
|
finally:
|
|
_psql(migrated_postgres, "revoke broad_writer from kb_apply;")
|
|
|
|
|
|
def test_migration_fails_closed_on_ownership_drift(migrated_postgres: str) -> None:
|
|
_psql(migrated_postgres, "alter table kb_stage.kb_proposal_approvals owner to rogue_owner;")
|
|
try:
|
|
_assert_migration_fails(migrated_postgres, "unexpected protected table owner")
|
|
finally:
|
|
_psql(migrated_postgres, "alter table kb_stage.kb_proposal_approvals owner to kb_gate_owner;")
|
|
|
|
|
|
def test_migration_fails_closed_on_unexpected_table_grantee(migrated_postgres: str) -> None:
|
|
_psql(migrated_postgres, "grant select on kb_stage.kb_proposal_approvals to rogue_grantee;")
|
|
try:
|
|
_assert_migration_fails(migrated_postgres, "unexpected protected table grantee")
|
|
finally:
|
|
_psql(migrated_postgres, "revoke all on kb_stage.kb_proposal_approvals from rogue_grantee;")
|
|
|
|
|
|
def test_migration_refuses_rogue_canonical_insert_before_install_and_normalizes_after_removal() -> None:
|
|
with _postgres_clone() as container:
|
|
_psql(container, "grant usage on schema public to rogue_grantee;")
|
|
for grant_sql, revoke_sql in (
|
|
(
|
|
"grant insert on public.claims to rogue_grantee;",
|
|
"revoke insert on public.claims from rogue_grantee;",
|
|
),
|
|
(
|
|
"grant insert (id) on public.claims to rogue_grantee;",
|
|
"revoke insert (id) on public.claims from rogue_grantee;",
|
|
),
|
|
):
|
|
_psql(container, grant_sql)
|
|
refused = _apply_migration(container, check=False)
|
|
assert refused.returncode != 0
|
|
assert "unexpected protected table grantee" in refused.stderr
|
|
assert _psql(container, "select count(*) from public.claims;").stdout.strip() == "0"
|
|
_psql(container, revoke_sql)
|
|
|
|
_apply_migration(container)
|
|
rogue_insert = _psql(
|
|
container,
|
|
"set session authorization rogue_grantee; "
|
|
"insert into public.claims (id) values ('99999999-9999-4999-8999-999999999999');",
|
|
check=False,
|
|
)
|
|
assert rogue_insert.returncode != 0
|
|
assert "permission denied for table claims" in rogue_insert.stderr
|
|
assert _psql(container, "select count(*) from public.claims;").stdout.strip() == "0"
|
|
|
|
|
|
def test_migration_fails_closed_on_unexpected_column_grantee(migrated_postgres: str) -> None:
|
|
_psql(
|
|
migrated_postgres,
|
|
"grant select (review_note) on kb_stage.kb_proposal_approvals to rogue_grantee;",
|
|
)
|
|
try:
|
|
_assert_migration_fails(migrated_postgres, "unexpected protected table grantee")
|
|
finally:
|
|
_psql(
|
|
migrated_postgres,
|
|
"revoke select (review_note) on kb_stage.kb_proposal_approvals from rogue_grantee;",
|
|
)
|
|
|
|
|
|
def test_migration_fails_closed_on_unexpected_function_grantee(migrated_postgres: str) -> None:
|
|
signature = "kb_stage.assert_approved_proposal(uuid,text,jsonb,text,uuid,timestamptz,text)"
|
|
_psql(migrated_postgres, f"grant execute on function {signature} to rogue_grantee;")
|
|
try:
|
|
_assert_migration_fails(migrated_postgres, "unexpected protected function grantee")
|
|
finally:
|
|
_psql(migrated_postgres, f"revoke all on function {signature} from rogue_grantee;")
|
|
|
|
|
|
def test_migration_fails_closed_on_unexpected_gate_overload(migrated_postgres: str) -> None:
|
|
_psql(
|
|
migrated_postgres,
|
|
"""
|
|
create function kb_stage.assert_approved_proposal(text)
|
|
returns void language sql security definer as $function$ select $function$;
|
|
""",
|
|
)
|
|
try:
|
|
_assert_migration_fails(migrated_postgres, "unexpected protected function overload")
|
|
finally:
|
|
_psql(migrated_postgres, "drop function kb_stage.assert_approved_proposal(text);")
|