183 lines
6.6 KiB
Python
183 lines
6.6 KiB
Python
"""Tests for the policies.scope migration (q1a2b3c4d5e6)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import sqlalchemy as sa
|
|
from alembic import command
|
|
from sqlalchemy.engine import Engine
|
|
|
|
from omnigent.db.utils import (
|
|
_build_alembic_config,
|
|
clear_engine_cache,
|
|
get_or_create_engine,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def db_engine(tmp_path: Path) -> Iterator[Engine]:
|
|
"""Fresh SQLite database with the full migration chain applied."""
|
|
db_path = tmp_path / "test.db"
|
|
uri = f"sqlite:///{db_path}"
|
|
engine = get_or_create_engine(uri)
|
|
try:
|
|
yield engine
|
|
finally:
|
|
clear_engine_cache()
|
|
|
|
|
|
def test_scope_column_exists_and_is_not_nullable(db_engine: Engine) -> None:
|
|
"""policies.scope is a NOT NULL column after the migration."""
|
|
columns = {c["name"]: c for c in sa.inspect(db_engine).get_columns("policies")}
|
|
assert "scope" in columns, "policies.scope column must exist after migration"
|
|
assert not columns["scope"]["nullable"], "policies.scope must be NOT NULL"
|
|
|
|
|
|
def test_ix_policies_name_cksum_index_exists(db_engine: Engine) -> None:
|
|
"""The default-name lookup index is present after the migration chain.
|
|
|
|
At head the partial unique index (``ix_policies_default_name_cksum``) has
|
|
been replaced by a plain non-unique ``name_cksum`` index (see
|
|
z5a2b3c4d5e6); default-name uniqueness lives in the store.
|
|
"""
|
|
indexes = {i["name"]: i for i in sa.inspect(db_engine).get_indexes("policies")}
|
|
assert "ix_policies_default_name_cksum" not in indexes
|
|
assert "ix_policies_name_cksum" in indexes
|
|
assert not indexes["ix_policies_name_cksum"]["unique"]
|
|
|
|
|
|
def test_backfill_sets_session_scope_for_session_policies(db_engine: Engine) -> None:
|
|
"""Rows with session_id set are back-filled with scope='session'."""
|
|
with db_engine.begin() as conn:
|
|
conn.execute(
|
|
sa.text(
|
|
# kind/type run at head, where they are int codes
|
|
# (1 = "default"/"python" per enum_codecs).
|
|
"INSERT INTO conversations"
|
|
" (id, created_at, updated_at, root_conversation_id, kind)"
|
|
" VALUES ('conv_sc1', 1, 1, 'conv_sc1', 1)"
|
|
)
|
|
)
|
|
conn.execute(
|
|
sa.text(
|
|
# scope runs at head, where it is an int code (2 = "session").
|
|
# name_cksum is NOT NULL at head (x1a2b3c4d5e6) — sha256(name).
|
|
"INSERT INTO policies"
|
|
" (id, name, name_cksum, session_id, scope, created_at, type, handler, enabled)"
|
|
" VALUES ('pol_sc1', 'sess_pol', :cksum, 'conv_sc1', 2, 1, 1, 'mod.f', 1)"
|
|
),
|
|
{"cksum": hashlib.sha256(b"sess_pol").digest()},
|
|
)
|
|
scope = conn.execute(
|
|
sa.text("SELECT scope FROM policies WHERE id = 'pol_sc1'")
|
|
).scalar_one()
|
|
assert scope == 2
|
|
|
|
|
|
def test_backfill_sets_default_scope_for_default_policies(db_engine: Engine) -> None:
|
|
"""Rows with session_id NULL are back-filled with scope='default'."""
|
|
with db_engine.begin() as conn:
|
|
conn.execute(
|
|
sa.text(
|
|
# scope runs at head, where it is an int code (1 = "default").
|
|
# name_cksum is NOT NULL at head (x1a2b3c4d5e6) — sha256(name).
|
|
"INSERT INTO policies"
|
|
" (id, name, name_cksum, session_id, scope, created_at, type, handler, enabled)"
|
|
" VALUES ('pol_def1', 'def_pol', :cksum, NULL, 1, 1, 1, 'mod.f', 1)"
|
|
),
|
|
{"cksum": hashlib.sha256(b"def_pol").digest()},
|
|
)
|
|
scope = conn.execute(
|
|
sa.text("SELECT scope FROM policies WHERE id = 'pol_def1'")
|
|
).scalar_one()
|
|
assert scope == 1
|
|
|
|
|
|
def test_scope_round_trip_via_store(db_engine: Engine) -> None:
|
|
"""scope is returned correctly by the policy store create methods."""
|
|
from omnigent.stores.conversation_store.sqlalchemy_store import SqlAlchemyConversationStore
|
|
from omnigent.stores.policy_store.sqlalchemy_store import SqlAlchemyPolicyStore
|
|
|
|
uri = str(db_engine.url)
|
|
conv_store = SqlAlchemyConversationStore(uri)
|
|
policy_store = SqlAlchemyPolicyStore(uri)
|
|
|
|
session_id = conv_store.create_conversation().id
|
|
|
|
session_pol = policy_store.create(
|
|
policy_id="pol_rt_sess",
|
|
session_id=session_id,
|
|
name="session_policy",
|
|
type="python",
|
|
handler="mod.func",
|
|
)
|
|
default_pol = policy_store.create_default(
|
|
policy_id="pol_rt_def",
|
|
name="default_policy",
|
|
type="python",
|
|
handler="mod.func",
|
|
)
|
|
|
|
assert session_pol.scope == "session"
|
|
assert default_pol.scope == "default"
|
|
|
|
|
|
def test_list_defaults_uses_scope_filter(db_engine: Engine) -> None:
|
|
"""list_defaults returns only scope='default' policies."""
|
|
from omnigent.stores.conversation_store.sqlalchemy_store import SqlAlchemyConversationStore
|
|
from omnigent.stores.policy_store.sqlalchemy_store import SqlAlchemyPolicyStore
|
|
|
|
uri = str(db_engine.url)
|
|
conv_store = SqlAlchemyConversationStore(uri)
|
|
policy_store = SqlAlchemyPolicyStore(uri)
|
|
|
|
session_id = conv_store.create_conversation().id
|
|
policy_store.create(
|
|
policy_id="pol_sess_x",
|
|
session_id=session_id,
|
|
name="sess_x",
|
|
type="python",
|
|
handler="mod.func",
|
|
)
|
|
policy_store.create_default(
|
|
policy_id="pol_def_x",
|
|
name="def_x",
|
|
type="python",
|
|
handler="mod.func",
|
|
)
|
|
|
|
defaults = policy_store.list_defaults()
|
|
assert len(defaults) == 1
|
|
assert defaults[0].id == "pol_def_x"
|
|
assert defaults[0].scope == "default"
|
|
|
|
|
|
def test_downgrade_removes_scope_column(tmp_path: Path) -> None:
|
|
"""Downgrade drops policies.scope and ix_policies_default_name."""
|
|
db_path = tmp_path / "downgrade.db"
|
|
uri = f"sqlite:///{db_path}"
|
|
engine = get_or_create_engine(uri)
|
|
|
|
# Start at head (includes the scope migration q1a2b3c4d5e6).
|
|
assert "scope" in {c["name"] for c in sa.inspect(engine).get_columns("policies")}
|
|
|
|
# Downgrade below the scope migration (through the later enums→SMALLINT
|
|
# migration that now sits above it).
|
|
config = _build_alembic_config(uri)
|
|
with engine.begin() as conn:
|
|
config.attributes["connection"] = conn
|
|
command.downgrade(config, "p1a2b3c4d5e6")
|
|
|
|
columns = {c["name"] for c in sa.inspect(engine).get_columns("policies")}
|
|
assert "scope" not in columns, "scope must be dropped by downgrade"
|
|
|
|
index_names = {i["name"] for i in sa.inspect(engine).get_indexes("policies")}
|
|
assert "ix_policies_default_name" not in index_names
|
|
|
|
engine.dispose()
|
|
clear_engine_cache()
|