"""The citation view is cached by input fingerprint, not by a guessed TTL. Assembling it costs 20+ seconds of Voyage embeds and vector searches. A TTL would force a choice between staleness and cost: a chair who verifies a precedent must see it on the very next load, and a timer cannot promise that. Keying on a fingerprint of the inputs (arguments · attachments · corpus) makes freshness a property of the data rather than of the clock. """ import asyncio import pytest from legal_mcp.services import case_citation_verification as ccv @pytest.fixture(autouse=True) def _clean_cache(): ccv.invalidate() yield ccv.invalidate() def _stub(monkeypatch, fingerprint: str, calls: list, *, complete: bool = True): """Point build_view at a fake case, a controllable fingerprint, and a counted assembler, so these tests never touch the corpus.""" async def fake_case(_cn): return {"id": "00000000-0000-0000-0000-000000000001"} async def fake_fp(_cid): return fingerprint async def fake_build(cn): calls.append(cn) return {"status": "ok", "case_number": cn, "arguments": [], "retrieval_complete": complete, "summary": {}} monkeypatch.setattr(ccv.db, "get_case_by_number", fake_case) monkeypatch.setattr(ccv, "_inputs_fingerprint", fake_fp) monkeypatch.setattr(ccv, "_build_view_uncached", fake_build) @pytest.mark.asyncio async def test_second_load_is_served_from_cache(monkeypatch): calls: list = [] _stub(monkeypatch, "fp-1", calls) first = await ccv.build_view("1069-04-26") second = await ccv.build_view("1069-04-26") assert first["cached"] is False assert second["cached"] is True assert len(calls) == 1, "the expensive assembly must run once" @pytest.mark.asyncio async def test_changed_inputs_invalidate_without_anyone_calling_invalidate(monkeypatch): """The chair verifies a precedent → fingerprint moves → next load rebuilds. Correctness must not depend on a writer remembering to clear the cache.""" calls: list = [] _stub(monkeypatch, "fp-before", calls) await ccv.build_view("1069-04-26") _stub(monkeypatch, "fp-after", calls) # e.g. a new case_precedents row again = await ccv.build_view("1069-04-26") assert again["cached"] is False assert len(calls) == 2 @pytest.mark.asyncio async def test_concurrent_loads_do_not_stampede(monkeypatch): """Reload-during-a-slow-load is how the original outage compounded into Postgres deadlocks. The later readers must wait, not pile on.""" calls: list = [] async def fake_case(_cn): return {"id": "00000000-0000-0000-0000-000000000001"} async def fake_fp(_cid): return "fp-1" async def slow_build(cn): calls.append(cn) await asyncio.sleep(0.05) return {"status": "ok", "case_number": cn, "arguments": [], "retrieval_complete": True, "summary": {}} monkeypatch.setattr(ccv.db, "get_case_by_number", fake_case) monkeypatch.setattr(ccv, "_inputs_fingerprint", fake_fp) monkeypatch.setattr(ccv, "_build_view_uncached", slow_build) await asyncio.gather(*(ccv.build_view("1069-04-26") for _ in range(6))) assert len(calls) == 1, "six concurrent loads, one assembly" @pytest.mark.asyncio async def test_partial_view_expires_so_missing_suggestions_get_retried(monkeypatch): """A budget-exhausted view is cached to stop reload storms, but it must not become permanent — the arguments with no suggestions deserve another try.""" calls: list = [] _stub(monkeypatch, "fp-1", calls, complete=False) await ccv.build_view("1069-04-26") _fp, expires_at, _view = ccv._view_cache["1069-04-26"] assert expires_at is not None, "a partial view must carry an expiry" monkeypatch.setattr(ccv.time, "monotonic", lambda: expires_at + 1) after = await ccv.build_view("1069-04-26") assert after["cached"] is False assert len(calls) == 2 @pytest.mark.asyncio async def test_complete_view_does_not_expire_on_a_timer(monkeypatch): calls: list = [] _stub(monkeypatch, "fp-1", calls, complete=True) await ccv.build_view("1069-04-26") _fp, expires_at, _view = ccv._view_cache["1069-04-26"] assert expires_at is None, "freshness comes from the fingerprint, not a clock" @pytest.mark.asyncio async def test_use_cache_false_always_rebuilds(monkeypatch): calls: list = [] _stub(monkeypatch, "fp-1", calls) await ccv.build_view("1069-04-26") await ccv.build_view("1069-04-26", use_cache=False) assert len(calls) == 2