102 lines
3.6 KiB
Python
102 lines
3.6 KiB
Python
"""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
|