feat(chat): invalidate saved chats on sources sync — versioned stamps, stale marker, Regenerate against the new index
This commit is contained in:
@@ -39,6 +39,15 @@ real ``LLMClient`` the probe is stubbed (:func:`_stub_probe`) so no
|
||||
test ever hits the network; the ``_real_llm`` tests get a passing
|
||||
probe from ``FakeEmbedder.embed_one``/``chat``.
|
||||
|
||||
Phase 53 (task 02): the sources-version bump — a sync whose import
|
||||
changed the KB (added + updated + pruned > 0) advances the single-row
|
||||
``sources_meta`` counter exactly once (the new generation lands in the
|
||||
``/api/sync/status`` detail as ``sources_version``); an unchanged
|
||||
re-sync never bumps (the detail still reports the current generation),
|
||||
and every failure path (git error, model down) never bumps. The
|
||||
counter is pinned to the migration-0010 seed (0) around every test by
|
||||
:func:`_reset_sources_version`.
|
||||
|
||||
The git / import / overview layers are monkeypatched in ``app.api.sync``
|
||||
(same fake style as ``test_import_docs_git.py``) — no real git, no LLM:
|
||||
the runner's state machine and HTTP surface are under test.
|
||||
@@ -66,11 +75,13 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api import sync as sync_api
|
||||
from app.config import Settings
|
||||
from app.db import SessionLocal, db_available
|
||||
from app.main import app as fastapi_app
|
||||
from app.models import GitSource
|
||||
from app.rag import git_sources as git_sources_resolver
|
||||
from app.rag.importer import ImportSummary
|
||||
from app.rag.llm import EmbeddingError, LLMClient, ModelUnavailableError
|
||||
from app.rag.sources_meta import current_sources_version
|
||||
from scripts.git_sync import GitSyncError
|
||||
from tests.conftest import ADMIN_PASSWORD
|
||||
from tests.fakes import FakeEmbedder
|
||||
@@ -100,6 +111,26 @@ def clean_git_sources(db: Session) -> Iterator[None]:
|
||||
db.commit()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_sources_version() -> Iterator[None]:
|
||||
"""Phase 53: the sources version counter is global mutable state —
|
||||
pin it to the migration-0010 seed (0) around every sync test so the
|
||||
bump assertions start from a known generation (own session: the
|
||||
runner bumps through its own short-lived ``SessionLocal``).
|
||||
Skips like the ``db`` fixture when Postgres is down."""
|
||||
if not db_available():
|
||||
pytest.skip("Postgres not reachable — run `podman compose up -d db` first")
|
||||
session = SessionLocal()
|
||||
try:
|
||||
session.execute(text("UPDATE sources_meta SET version = 0 WHERE id = 1"))
|
||||
session.commit()
|
||||
yield
|
||||
finally:
|
||||
session.execute(text("UPDATE sources_meta SET version = 0 WHERE id = 1"))
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def sync_client() -> Iterator[TestClient]:
|
||||
"""Context-managed TestClient — one app event loop across requests
|
||||
@@ -310,7 +341,10 @@ def test_admin_sync_success_reports_full_detail(
|
||||
"files": 5, "added": 1, "updated": 2, "unchanged": 2, "pruned": 3,
|
||||
"errors": 0, "chunks": 11, "summaries": 1, "summary_errors": 0,
|
||||
"overview": True,
|
||||
"sources_version": 1, # phase 53: changed KB → exactly one bump (0 → 1)
|
||||
}
|
||||
# The bump committed: the counter advanced exactly once, not twice.
|
||||
assert current_sources_version(db) == 1
|
||||
# Git: the configured repo was cloned into BOR_SOURCES_DIR/<repo-name>/.
|
||||
assert clone_calls == [(repo_url, tmp_path / "bor" / "repo")]
|
||||
# Import: exactly the checkouts, with prune=True (the button is the
|
||||
@@ -354,6 +388,10 @@ def test_unchanged_kb_skips_overview_refresh(
|
||||
assert body["detail"]["overview"] is False
|
||||
assert fake_overview.llms == [] # no wasted model call
|
||||
assert len(fake_import.llms) == 1 # the import itself ran
|
||||
# Phase 53: an unchanged re-sync never bumps — the detail reports
|
||||
# the current (unadvanced) generation.
|
||||
assert body["detail"]["sources_version"] == 0
|
||||
assert current_sources_version(db) == 0
|
||||
|
||||
|
||||
# --- admin: concurrency ----------------------------------------------------
|
||||
@@ -435,6 +473,8 @@ def test_git_failure_marks_failed_and_skips_import(
|
||||
assert body["finished_at"] is not None
|
||||
assert fake_import.sources == [] # no partial import
|
||||
assert fake_overview.llms == []
|
||||
# Phase 53: a FAILED sync never bumps — the version is untouched.
|
||||
assert current_sources_version(db) == 0
|
||||
|
||||
# A failed run leaves the system restartable: a new POST is accepted.
|
||||
assert sync_client.post("/api/sync").status_code == 202
|
||||
@@ -773,6 +813,8 @@ def test_model_down_fails_fast_before_any_clone(
|
||||
assert body["detail"] == {}
|
||||
assert clone_calls == [] # fail fast: before any clone
|
||||
assert fake_import.sources == [] # and before any import
|
||||
# Phase 53: a FAILED sync (model down) never bumps.
|
||||
assert current_sources_version(db) == 0
|
||||
|
||||
|
||||
def test_probe_names_dead_embed_model_and_masks_credentials(
|
||||
|
||||
Reference in New Issue
Block a user