Add LLM settings integration
test / pytest (push) Successful in 1m13s

Add app_settings migration, settings UI, and OpenAI-compatible httpx LLM client with mocked tests.

Preserve API keys on blank form submissions, require a fresh key when base_url changes, and keep AI search settings untouched for step 3.

Update docs/design LLM integration and step 3 AI search notes, including prompt contract and extra-hints planning.
This commit is contained in:
2026-06-01 20:06:22 +02:00
parent 8b8bd9f38f
commit d36b940981
12 changed files with 1254 additions and 15 deletions
+26 -10
View File
@@ -28,6 +28,7 @@ from app.db import Base, SessionLocal, configure_database
from app.migrate import (
V1_REVISION,
_detect_db_state,
_make_alembic_config,
run_migrations,
verify_schema_is_current,
)
@@ -35,6 +36,18 @@ from app.main import create_app
from fastapi.testclient import TestClient
def _get_head_revision() -> str:
"""Resolve the current Alembic head revision from migration scripts."""
from alembic.script import ScriptDirectory
cfg = _make_alembic_config("sqlite:///") # URL is unused for script lookup
script = ScriptDirectory.from_config(cfg)
return script.get_current_head()
HEAD_REVISION = _get_head_revision()
@pytest.fixture()
def tmp_db_path(tmp_path):
"""Provide a temporary SQLite database path."""
@@ -55,7 +68,7 @@ def tmp_db_url(tmp_db_path):
class TestFreshDBMigration:
"""Empty database gets all tables created by migration."""
def test_creates_all_three_tables(self, tmp_db_url):
def test_creates_all_tables(self, tmp_db_url):
run_migrations(tmp_db_url)
eng = create_engine(tmp_db_url)
tables = set(inspect(eng).get_table_names())
@@ -63,6 +76,7 @@ class TestFreshDBMigration:
assert "boxes" in tables
assert "items" in tables
assert "subitems" in tables
assert "app_settings" in tables
def test_creates_alembic_version_table(self, tmp_db_url):
run_migrations(tmp_db_url)
@@ -77,7 +91,7 @@ class TestFreshDBMigration:
with eng.begin() as conn:
version = conn.execute(text("SELECT version_num FROM alembic_version")).scalar()
eng.dispose()
assert version == V1_REVISION
assert version == HEAD_REVISION
def test_boxes_table_has_all_columns(self, tmp_db_url):
run_migrations(tmp_db_url)
@@ -147,9 +161,11 @@ class TestUnmanagedDBAdoption2a:
"""Database with existing tables matching baseline gets adopted."""
def _create_old_db(self, db_url: str) -> None:
"""Simulate a pre-Alembic DB: create_all + insert data."""
"""Simulate a pre-Alembic DB: create V1 tables only + insert data."""
eng = create_engine(db_url)
Base.metadata.create_all(bind=eng)
# Only create V1 tables (boxes, items, subitems) — not app_settings
for table_name in ("boxes", "items", "subitems"):
Base.metadata.tables[table_name].create(bind=eng)
with eng.begin() as conn:
conn.execute(text(
"INSERT INTO boxes (name, room, status, created_at, updated_at) "
@@ -170,7 +186,7 @@ class TestUnmanagedDBAdoption2a:
with eng.begin() as conn:
version = conn.execute(text("SELECT version_num FROM alembic_version")).scalar()
eng.dispose()
assert version == V1_REVISION
assert version == HEAD_REVISION
def test_data_preserved_after_adoption(self, tmp_db_url):
self._create_old_db(tmp_db_url)
@@ -187,14 +203,14 @@ class TestUnmanagedDBAdoption2a:
assert item_count == 1
assert box_name == "Kitchen Box"
def test_no_extra_tables_created(self, tmp_db_url):
def test_no_extra_tables_beyond_migrations(self, tmp_db_url):
self._create_old_db(tmp_db_url)
run_migrations(tmp_db_url)
eng = create_engine(tmp_db_url)
tables = set(inspect(eng).get_table_names())
eng.dispose()
assert tables == {"alembic_version", "boxes", "items", "subitems"}
assert tables == {"alembic_version", "boxes", "items", "subitems", "app_settings"}
def test_adoption_is_idempotent(self, tmp_db_url):
"""Running run_migrations twice does not error or duplicate data."""
@@ -209,7 +225,7 @@ class TestUnmanagedDBAdoption2a:
eng.dispose()
assert box_count == 1
assert version == V1_REVISION
assert version == HEAD_REVISION
# ---------------------------------------------------------------------------
@@ -396,7 +412,7 @@ class TestManagedDBMigration:
with eng.begin() as conn:
version = conn.execute(text("SELECT version_num FROM alembic_version")).scalar()
eng.dispose()
assert version == V1_REVISION
assert version == HEAD_REVISION
# ---------------------------------------------------------------------------
@@ -639,7 +655,7 @@ class TestProdDBCopyAdoption:
subitems_after = conn.execute(text("SELECT COUNT(*) FROM subitems")).scalar()
eng.dispose()
assert version == V1_REVISION
assert version == HEAD_REVISION
assert boxes_after == boxes_before
assert items_after == items_before
assert subitems_after == subitems_before