feat(chat): invalidate saved chats on sources sync — versioned stamps, stale marker, Regenerate against the new index

This commit is contained in:
2026-08-30 23:39:15 -04:00
parent ea8e041189
commit 32b7bfd4b3
26 changed files with 2145 additions and 63 deletions
+101
View File
@@ -0,0 +1,101 @@
"""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