diff --git a/tests/hermes_cli/test_sessions_delete.py b/tests/hermes_cli/test_sessions_delete.py index e763cacf8c..7b3b8a9add 100644 --- a/tests/hermes_cli/test_sessions_delete.py +++ b/tests/hermes_cli/test_sessions_delete.py @@ -12,7 +12,7 @@ def test_sessions_delete_accepts_unique_id_prefix(monkeypatch, capsys): captured["resolved_from"] = session_id return "20260315_092437_c9a6ff" - def delete_session(self, session_id): + def delete_session(self, session_id, **kwargs): captured["deleted"] = session_id return True @@ -45,7 +45,7 @@ def test_sessions_delete_reports_not_found_when_prefix_is_unknown(monkeypatch, c def resolve_session_id(self, session_id): return None - def delete_session(self, session_id): + def delete_session(self, session_id, **kwargs): raise AssertionError("delete_session should not be called when resolution fails") def close(self): @@ -73,7 +73,7 @@ def test_sessions_delete_handles_eoferror_on_confirm(monkeypatch, capsys): def resolve_session_id(self, session_id): return "20260315_092437_c9a6ff" - def delete_session(self, session_id): + def delete_session(self, session_id, **kwargs): raise AssertionError("delete_session should not be called when cancelled") def close(self): diff --git a/tests/test_hermes_state.py b/tests/test_hermes_state.py index 868a28c530..cdcf5c1473 100644 --- a/tests/test_hermes_state.py +++ b/tests/test_hermes_state.py @@ -1981,3 +1981,58 @@ class TestAutoMaintenance: # Should parse as a float timestamp close to now. assert abs(float(marker) - time.time()) < 60 + def test_auto_prune_deletes_transcript_files(self, db, tmp_path): + """Issue #3015: auto-prune must also delete on-disk transcript files.""" + sessions_dir = tmp_path / "sessions" + sessions_dir.mkdir() + + self._make_old_ended(db, "old1", days_old=100) + self._make_old_ended(db, "old2", days_old=100) + db.create_session(session_id="new", source="cli") # active + + # Transcript files mimicking real gateway/CLI layout + (sessions_dir / "old1.json").write_text("{}") + (sessions_dir / "old1.jsonl").write_text("{}\n") + (sessions_dir / "old2.jsonl").write_text("{}\n") + (sessions_dir / "request_dump_old1_001.json").write_text("{}") + (sessions_dir / "new.jsonl").write_text("{}\n") # active, must survive + + result = db.maybe_auto_prune_and_vacuum( + retention_days=90, sessions_dir=sessions_dir + ) + assert result["pruned"] == 2 + + # Pruned transcript files are gone + assert not (sessions_dir / "old1.json").exists() + assert not (sessions_dir / "old1.jsonl").exists() + assert not (sessions_dir / "old2.jsonl").exists() + assert not (sessions_dir / "request_dump_old1_001.json").exists() + # Active session's transcript is untouched + assert (sessions_dir / "new.jsonl").exists() + + def test_auto_prune_without_sessions_dir_preserves_files(self, db, tmp_path): + """Backward-compat: no sessions_dir = DB-only cleanup (legacy behavior).""" + sessions_dir = tmp_path / "sessions" + sessions_dir.mkdir() + self._make_old_ended(db, "old", days_old=100) + (sessions_dir / "old.jsonl").write_text("{}\n") + + result = db.maybe_auto_prune_and_vacuum(retention_days=90) + assert result["pruned"] == 1 + # File stays — caller didn't opt in + assert (sessions_dir / "old.jsonl").exists() + + def test_prune_sessions_deletes_files_for_pruned_only(self, db, tmp_path): + """Active-session transcripts must never be deleted by prune.""" + sessions_dir = tmp_path / "sessions" + sessions_dir.mkdir() + self._make_old_ended(db, "old", days_old=100) + db.create_session(session_id="active", source="cli") # not ended + (sessions_dir / "old.jsonl").write_text("{}\n") + (sessions_dir / "active.jsonl").write_text("{}\n") + + count = db.prune_sessions(older_than_days=90, sessions_dir=sessions_dir) + assert count == 1 + assert not (sessions_dir / "old.jsonl").exists() + assert (sessions_dir / "active.jsonl").exists() +