diff --git a/planfile/api/server.py b/planfile/api/server.py index 30b6563..948f18c 100644 --- a/planfile/api/server.py +++ b/planfile/api/server.py @@ -509,9 +509,12 @@ def _bounded_stale_index_response( offset: int, limit: int | None, view: Literal["full", "operational", "summary"], + allow_unbounded_summary: bool = False, ) -> Response | None: """Serve a recent, coherent projection while its source index is repaired.""" - if sprint != "all" or limit is None or view == "full": + archive_queue = sprint == "all" and limit is not None + legacy_summary = allow_unbounded_summary and view == "summary" + if view == "full" or not (archive_queue or legacy_summary): return None index = pf.store._sqlite_ticket_index() if not index.has_fresh_snapshot(_INDEX_REPAIR_STALE_WINDOW_SECONDS): @@ -557,6 +560,7 @@ def _ticket_list_response( limit: int | None, view: Literal["full", "operational", "summary"] = "full", allow_stale: bool = False, + allow_index_stale: bool = False, ) -> Response: # FastAPI runs this sync endpoint in a worker pool. Serialize cache misses so # a burst of websocket-driven dashboard refreshes builds one 5+ MB response, @@ -670,6 +674,7 @@ def _ticket_list_response( offset=offset, limit=limit, view=view, + allow_unbounded_summary=allow_stale or allow_index_stale, ) if stale_index_response is not None: return stale_index_response @@ -759,6 +764,7 @@ def list_tickets( limit=limit, view=effective_view, allow_stale=browser_client, + allow_index_stale=legacy_unbounded_request, ) diff --git a/tests/test_sqlite_ticket_index.py b/tests/test_sqlite_ticket_index.py index a1375a1..1815cad 100644 --- a/tests/test_sqlite_ticket_index.py +++ b/tests/test_sqlite_ticket_index.py @@ -405,6 +405,38 @@ def test_stale_archive_queue_reads_use_recent_index_without_query_cache( assert pf.store.ticket_index_status()["current"] is False +def test_legacy_archive_dashboard_uses_recent_current_index_without_cache( + tmp_path, monkeypatch +): + pf = Planfile(str(tmp_path)) + _disable_archive(pf) + ticket = pf.create_ticket(name="Last known good dashboard item") + pf.store.configure_ticket_index(True) + sprint_file = pf.store._sprint_file("current") + data = yaml.safe_load(sprint_file.read_text(encoding="utf-8")) + data["sprint"]["tickets"][ticket.id]["name"] = "New durable value" + sprint_file.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + mirror_path(sprint_file).unlink(missing_ok=True) + monkeypatch.setattr(server, "get_planfile", lambda: pf) + monkeypatch.setattr( + pf, + "list_tickets", + lambda **_kwargs: (_ for _ in ()).throw( + AssertionError("legacy request parsed durable sprint") + ), + ) + server._TICKET_LIST_RESPONSE_CACHE.clear() + server._TICKET_LIST_LATEST.clear() + client = TestClient(server.app) + + response = client.get("/tickets?sprint=all") + + assert response.status_code == 200 + assert response.json()[0]["name"] == "Last known good dashboard item" + assert response.headers["X-Planfile-View"] == "summary" + assert response.headers["X-Planfile-Index-State"] == "stale" + + def test_expired_stale_archive_index_remains_fail_closed(tmp_path, monkeypatch): pf = Planfile(str(tmp_path)) _disable_archive(pf)