Files
omnigent-ai--omnigent/tests/db/test_migration_terminal_launch_args.py
2026-07-13 13:12:00 +08:00

124 lines
4.8 KiB
Python

"""Tests for the ``conversations.terminal_launch_args`` column.
Per ``designs/NATIVE_RUNNER_SERVER_LAUNCH.md``: the column holds a nullable
JSON-encoded list of pass-through CLI args for a native terminal wrapper
(claude / codex). NULL means no native launch args — the common case for
non-native sessions and for rows that pre-date the feature. The column is now
a binary (``BLOB``/``BYTEA``) type storing the value zstd-compressed
(``omnigent.db.compression``). These tests exercise the schema directly (raw
SQL, no ORM) so column drift is caught independently of the store wrapper.
"""
from __future__ import annotations
from collections.abc import Iterator
from pathlib import Path
import pytest
import sqlalchemy as sa
from sqlalchemy.engine import Engine
from omnigent.db.utils import clear_engine_cache, get_or_create_engine
@pytest.fixture
def db_engine(tmp_path: Path) -> Iterator[Engine]:
"""
Fresh SQLite DB with the full alembic chain applied; cleaned up
after.
:param tmp_path: Pytest-managed temp directory for the SQLite file.
:returns: Engine pointed at the migrated database.
"""
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_terminal_launch_args_column_present_and_nullable(db_engine: Engine) -> None:
"""
Verify the migration creates ``conversations.terminal_launch_args``
as a nullable binary column.
(1) The column must exist — proves the migration applied; without
it every code path mentioning ``terminal_launch_args`` crashes on
an ``AttributeError`` from the ORM mapping. (2) It must be nullable
— non-native and pre-feature rows have no launch args and would
otherwise be rejected on read. (3) The type must be binary
(``BLOB``/``BYTEA``): the column is stored zstd-compressed by
``omnigent.db.compression``, whose framed bytes can contain NUL and
would be rejected by a ``TEXT`` column on PostgreSQL.
"""
cols = sa.inspect(db_engine).get_columns("conversations")
matches = [c for c in cols if c["name"] == "terminal_launch_args"]
assert len(matches) == 1, (
f"Expected exactly one 'terminal_launch_args' column on "
f"conversations, got {len(matches)}. If 0, the migration didn't apply."
)
col = matches[0]
assert col["nullable"], (
"conversations.terminal_launch_args must be NULLABLE — non-native "
"and pre-feature rows have no launch args and would otherwise be "
"rejected on read."
)
assert isinstance(col["type"], sa.LargeBinary), (
f"Expected a binary (BLOB/BYTEA) type for the compressed column, got {col['type']!r}."
)
def test_terminal_launch_args_round_trip_null_and_json(db_engine: Engine) -> None:
"""
Round-trip a default insert (NULL) and a JSON-encoded arg list.
Exercises the schema with raw SQL (no ORM) so column drift is
caught independently of the store wrapper. NULL stays NULL; a
stored JSON string comes back byte-for-byte (the store layer is
what decodes it to a list — here we pin the raw column behaviour).
"""
with db_engine.connect() as conn:
# Default insert: terminal_launch_args omitted → NULL.
# root_conversation_id is NOT NULL (self-FK); a top-level row's
# root is its own id, so :id binds both.
conn.execute(
sa.text(
"INSERT INTO conversations "
"(id, created_at, updated_at, kind, root_conversation_id) "
"VALUES (:id, :ts, :ts, 1, :id)"
),
{"id": "conv_tla_null", "ts": 1700000000},
)
result = conn.execute(
sa.text("SELECT terminal_launch_args FROM conversations WHERE id = :id"),
{"id": "conv_tla_null"},
).scalar_one()
assert result is None, (
f"Expected NULL terminal_launch_args on default insert; got {result!r}."
)
# Native-launch insert: a JSON-encoded arg list.
conn.execute(
sa.text(
"INSERT INTO conversations "
"(id, created_at, updated_at, kind, terminal_launch_args, "
"root_conversation_id) "
"VALUES (:id, :ts, :ts, 1, :tla, :id)"
),
{
"id": "conv_tla_value",
"ts": 1700000000,
"tla": '["--dangerously-skip-permissions", "--model", "opus"]',
},
)
result = conn.execute(
sa.text("SELECT terminal_launch_args FROM conversations WHERE id = :id"),
{"id": "conv_tla_value"},
).scalar_one()
assert result == '["--dangerously-skip-permissions", "--model", "opus"]', (
f"Round-trip mismatch on terminal_launch_args; got {result!r}."
)
conn.commit()