diff --git a/api/models.py b/api/models.py index b53cd29d..c03ab058 100644 --- a/api/models.py +++ b/api/models.py @@ -4313,6 +4313,11 @@ def _state_projection_sidecar_metadata(sid: str) -> dict: (#4842). A rename/archive/edit bumps the signature and invalidates just that entry, so a stale title/archived flag is impossible without re-reading. Returns a COPY so callers can't mutate the cached dict. + + NOTE: this stat-gates on ``SESSION_DIR / f'{sid}.json'`` because that file is + ``Session.load_metadata_only``'s sole source for title+archived. If that ever + stops being true (metadata moves to another store), this gate would short- + circuit before the real source — update both together. """ default = {"title": None, "archived": False} if not is_safe_session_id(sid): diff --git a/tests/test_issue4385_cron_archive_reappears.py b/tests/test_issue4385_cron_archive_reappears.py index bf1fa547..76384521 100644 --- a/tests/test_issue4385_cron_archive_reappears.py +++ b/tests/test_issue4385_cron_archive_reappears.py @@ -109,16 +109,22 @@ def test_cron_state_projection_preserves_archived_sidecar(monkeypatch, tmp_path) (sid, "Cron Session", "test-model", 1, 20, "cron", "cron"), ) - class ArchivedSidecar: - title = "Cron Session" - archived = True - - monkeypatch.setattr( - models.Session, - "load_metadata_only", - staticmethod(lambda candidate: ArchivedSidecar() if candidate == sid else None), + # Write a real archived sidecar JSON under a patched SESSION_DIR so the + # projection's sidecar read exercises the production path (the #4842 perf + # fix stat-gates on SESSION_DIR/{sid}.json before consulting metadata, so a + # mock of load_metadata_only without a real file would never be reached — + # which mirrors production, where archived metadata only exists when the + # file does). + session_dir = tmp_path / "sessions" + session_dir.mkdir() + (session_dir / f"{sid}.json").write_text( + '{"session_id": "%s", "title": "Cron Session", "created_at": 1.0,' + ' "updated_at": 2.0, "archived": true, "messages": []}' % sid, + encoding="utf-8", ) + monkeypatch.setattr(models, "SESSION_DIR", session_dir) monkeypatch.setattr(models, "ensure_cron_project", lambda: "cron-project") + models.clear_sidecar_metadata_cache() rows = models._load_cli_sessions_uncached( tmp_path, @@ -195,15 +201,17 @@ def test_webhook_state_projection_preserves_archived_sidecar(monkeypatch, tmp_pa (sid,), ) - class ArchivedWebhookSidecar: - title = "Webhook Session" - archived = True - - monkeypatch.setattr( - models.Session, - "load_metadata_only", - staticmethod(lambda candidate: ArchivedWebhookSidecar() if candidate == sid else None), + # Real archived sidecar JSON under a patched SESSION_DIR (see the cron test + # above — the #4842 stat-gate requires the file to exist, matching prod). + session_dir = tmp_path / "sessions" + session_dir.mkdir() + (session_dir / f"{sid}.json").write_text( + '{"session_id": "%s", "title": "Webhook Session", "created_at": 1.0,' + ' "updated_at": 2.0, "archived": true, "messages": []}' % sid, + encoding="utf-8", ) + monkeypatch.setattr(models, "SESSION_DIR", session_dir) + models.clear_sidecar_metadata_cache() rows = models._load_cli_sessions_uncached( tmp_path,