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:
+26
-10
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user