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

130 lines
4.6 KiB
Python

"""Tests for the conversations.title NOT NULL migration (s1a2b3c4d5e6).
Verifies that after upgrade the column is NOT NULL with a server default
of empty string, that NULL rows are back-filled, and that downgrade
restores nullable and converts empty strings back to NULL.
"""
from __future__ import annotations
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 (including s1a2b3c4d5e6)."""
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_title_column_is_not_nullable(db_engine: Engine) -> None:
"""After upgrade, conversations.title must be NOT NULL."""
cols = sa.inspect(db_engine).get_columns("conversations")
title_cols = [c for c in cols if c["name"] == "title"]
assert len(title_cols) == 1, "Expected exactly one 'title' column on conversations"
assert not title_cols[0]["nullable"], (
"conversations.title must be NOT NULL after the migration"
)
def test_title_defaults_empty_string_on_insert(db_engine: Engine) -> None:
"""An insert omitting title lands as '' (the server_default)."""
with db_engine.connect() as conn:
conn.execute(
sa.text(
"INSERT INTO conversations "
"(id, created_at, updated_at, kind, root_conversation_id) "
"VALUES (:id, :ts, :ts, 1, :id)"
),
{"id": "conv_title_default", "ts": 1700000000},
)
conn.commit()
value = conn.execute(
sa.text("SELECT title FROM conversations WHERE id = :id"),
{"id": "conv_title_default"},
).scalar_one()
assert value == "", f"Expected title to default to '' (empty string); got {value!r}."
def test_downgrade_restores_nullable_and_nullifies_empty_titles(tmp_path: Path) -> None:
"""
Downgrade to r1a2b3c4d5e6 makes title nullable and converts '' back to NULL.
"""
db_path = tmp_path / "downgrade.db"
uri = f"sqlite:///{db_path}"
engine = get_or_create_engine(uri)
# At head: insert a row with no title (will be ''). kind=1 is the
# "default" int code (conversations.kind is a SMALLINT at head).
with engine.connect() as conn:
conn.execute(
sa.text(
"INSERT INTO conversations "
"(id, created_at, updated_at, kind, root_conversation_id, title) "
"VALUES (:id, :ts, :ts, 1, :id, '')"
),
{"id": "conv_downgrade_test", "ts": 1700000001},
)
conn.commit()
# Also insert a row with a real title to verify it is not affected.
with engine.connect() as conn:
conn.execute(
sa.text(
"INSERT INTO conversations "
"(id, created_at, updated_at, kind, root_conversation_id, title) "
"VALUES (:id, :ts, :ts, 1, :id, :title)"
),
{"id": "conv_titled", "ts": 1700000002, "title": "My Session"},
)
conn.commit()
# Downgrade to r1a2b3c4d5e6 (below the title migration; passes back through
# the enums→SMALLINT downgrade that restores kind to a string).
config = _build_alembic_config(uri)
with engine.begin() as conn:
config.attributes["connection"] = conn
command.downgrade(config, "r1a2b3c4d5e6")
inspector = sa.inspect(engine)
title_col = next(c for c in inspector.get_columns("conversations") if c["name"] == "title")
assert title_col["nullable"], "conversations.title must be nullable after downgrade"
# Empty-string title should have been converted back to NULL.
with engine.connect() as conn:
value = conn.execute(
sa.text("SELECT title FROM conversations WHERE id = :id"),
{"id": "conv_downgrade_test"},
).scalar_one_or_none()
assert value is None, (
f"Expected empty-string title to be restored to NULL after downgrade; got {value!r}"
)
# A real title should be unchanged.
with engine.connect() as conn:
value = conn.execute(
sa.text("SELECT title FROM conversations WHERE id = :id"),
{"id": "conv_titled"},
).scalar_one_or_none()
assert value == "My Session", f"Real title should be unchanged after downgrade; got {value!r}"
engine.dispose()
clear_engine_cache()