"""Unit: sources-version counter helpers (phase 53, task 01). ``app.rag.sources_meta`` runs against the local compose Postgres (``podman compose up -d db``) — the house DB-test pattern: the helpers are thin session wrappers whose contract (seeded single row, flush-not commit, defensive absence) only holds against a real database. Skips with clear instructions when the stack is not up. A fixture resets ``sources_meta`` to the migration-0010 seed state (id 1, version 0) before and after every test, so the suite leaves the dev DB exactly as the migration left it. """ from __future__ import annotations import pytest from sqlalchemy import text from sqlalchemy.orm import Session from app.db import SessionLocal from app.models import SourcesMeta from app.rag.sources_meta import bump_sources_version, current_sources_version @pytest.fixture() def seeded_sources_meta(db: Session): """Reset the counter to the migration-0010 seed (id 1, version 0).""" db.execute(text("DELETE FROM sources_meta")) db.add(SourcesMeta(id=1, version=0)) db.commit() yield db.execute(text("DELETE FROM sources_meta")) db.add(SourcesMeta(id=1, version=0)) db.commit() def test_current_absent_row_returns_zero_without_raising( db: Session, seeded_sources_meta: None ) -> None: """Defensive: a deleted seed row reads as 0 — never an exception.""" db.execute(text("DELETE FROM sources_meta")) db.commit() assert current_sources_version(db) == 0 def test_first_bump_zero_to_one(db: Session, seeded_sources_meta: None) -> None: """Seeded at 0 (the pre-counter KB), the first bump returns 1 and ``current`` reflects it.""" assert current_sources_version(db) == 0 assert bump_sources_version(db) == 1 db.commit() # the caller commits — the helper only flushes assert current_sources_version(db) == 1 def test_second_bump_increments(db: Session, seeded_sources_meta: None) -> None: """Bumps are monotonic: 0 → 1 → 2, one step per KB-changing sync.""" assert bump_sources_version(db) == 1 db.commit() assert bump_sources_version(db) == 2 db.commit() assert current_sources_version(db) == 2 def test_bump_absent_row_upserts_to_one(db: Session, seeded_sources_meta: None) -> None: """Upsert semantics: a deleted seed row is recreated by the first bump (version 0 → 1), never left dangling.""" db.execute(text("DELETE FROM sources_meta")) db.commit() assert bump_sources_version(db) == 1 db.commit() row = db.get(SourcesMeta, 1) assert row is not None, "the single row must be recreated" assert row.id == 1 assert row.version == 1 assert row.updated_at is not None, "updated_at must be server-stamped" def test_bump_flushes_without_committing(db: Session, seeded_sources_meta: None) -> None: """The helper flushes, it does not commit: a rolled-back session must roll the bump back with it (each sync path owns its transaction).""" assert bump_sources_version(db) == 1 db.rollback() assert current_sources_version(db) == 0, "the uncommitted bump must roll back" def test_bumps_in_separate_sessions_progress(db: Session, seeded_sources_meta: None) -> None: """One writer at a time is the deployment reality, but two bumps in two sessions must not race to the same value: the second session sees the committed increment and lands on the next generation.""" assert bump_sources_version(db) == 1 db.commit() other = SessionLocal() try: assert bump_sources_version(other) == 2 other.commit() finally: other.close() db.expire_all() # drop the stale identity-map state assert current_sources_version(db) == 2