hermes-agent/tests/hermes_cli/test_sessions_delete.py
Teknium 845a2d8152
feat(sessions): any prune filter matches all ages; preview shows age span (#59415)
Bare 'hermes sessions prune' keeps the historical 90-day default, but any
filter — now including --source — suppresses the implicit cutoff, so
'prune --source cron' targets ALL cron sessions instead of silently only
those older than 90 days (the surprise a user hit live: 'No sessions
match ... source cron' despite plenty of recent cron runs).

- CLI preview + confirmation now show the match count plus the oldest
  and newest matching session start times before deleting.
- Dashboard /api/sessions/prune mirrors the semantics: attribute filters
  without an explicit older_than_days match all ages (model_fields_set
  distinguishes an explicit 90 from the Pydantic default); dry_run
  responses gain oldest_started_at/newest_started_at.
- Docs + argparse help updated; tests for both surfaces.
2026-07-05 23:01:10 -07:00

222 lines
6.7 KiB
Python

import sys
import pytest
def test_sessions_delete_accepts_unique_id_prefix(monkeypatch, capsys):
import hermes_cli.main as main_mod
import hermes_state
captured = {}
class FakeDB:
def resolve_session_id(self, session_id):
captured["resolved_from"] = session_id
return "20260315_092437_c9a6ff"
def delete_session(self, session_id, **kwargs):
captured["deleted"] = session_id
return True
def close(self):
captured["closed"] = True
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(
sys,
"argv",
["hermes", "sessions", "delete", "20260315_092437_c9a6", "--yes"],
)
main_mod.main()
output = capsys.readouterr().out
assert captured == {
"resolved_from": "20260315_092437_c9a6",
"deleted": "20260315_092437_c9a6ff",
"closed": True,
}
assert "Deleted session '20260315_092437_c9a6ff'." in output
def test_sessions_delete_reports_not_found_when_prefix_is_unknown(monkeypatch, capsys):
import hermes_cli.main as main_mod
import hermes_state
class FakeDB:
def resolve_session_id(self, session_id):
return None
def delete_session(self, session_id, **kwargs):
raise AssertionError("delete_session should not be called when resolution fails")
def close(self):
pass
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(
sys,
"argv",
["hermes", "sessions", "delete", "missing-prefix", "--yes"],
)
main_mod.main()
output = capsys.readouterr().out
assert "Session 'missing-prefix' not found." in output
def test_sessions_delete_handles_eoferror_on_confirm(monkeypatch, capsys):
"""sessions delete should not crash when stdin is closed (non-TTY)."""
import hermes_cli.main as main_mod
import hermes_state
class FakeDB:
def resolve_session_id(self, session_id):
return "20260315_092437_c9a6ff"
def delete_session(self, session_id, **kwargs):
raise AssertionError("delete_session should not be called when cancelled")
def close(self):
pass
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(
sys, "argv",
["hermes", "sessions", "delete", "20260315_092437_c9a6"],
)
monkeypatch.setattr("builtins.input", lambda _prompt="": (_ for _ in ()).throw(EOFError))
main_mod.main()
output = capsys.readouterr().out
assert "Cancelled" in output
def test_sessions_prune_handles_eoferror_on_confirm(monkeypatch, capsys):
"""sessions prune should not crash when stdin is closed (non-TTY)."""
import hermes_cli.main as main_mod
import hermes_state
class FakeDB:
def list_prune_candidates(self, **kwargs):
return [
{
"id": "20260315_092437_c9a6ff",
"source": "cli",
"title": "old session",
"started_at": 0.0,
"ended_at": 1.0,
"message_count": 3,
"archived": 0,
}
]
def prune_sessions(self, **kwargs):
raise AssertionError("prune_sessions should not be called when cancelled")
def close(self):
pass
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(
sys, "argv",
["hermes", "sessions", "prune"],
)
monkeypatch.setattr("builtins.input", lambda _prompt="": (_ for _ in ()).throw(EOFError))
main_mod.main()
output = capsys.readouterr().out
assert "Cancelled" in output
def _run_prune(monkeypatch, capsys, argv_tail, candidates=None):
"""Run `hermes sessions prune <argv_tail>` against a FakeDB, capturing
the filter kwargs passed to list_prune_candidates. Auto-confirms."""
import hermes_cli.main as main_mod
import hermes_state
seen = {}
rows = candidates if candidates is not None else [
{
"id": "20260101_000000_aaaaaa",
"source": "cron",
"title": "oldest run",
"started_at": 1_600_000_000.0,
"ended_at": 1_600_000_100.0,
"message_count": 2,
"archived": 0,
},
{
"id": "20260601_000000_bbbbbb",
"source": "cron",
"title": "newest run",
"started_at": 1_700_000_000.0,
"ended_at": 1_700_000_100.0,
"message_count": 4,
"archived": 0,
},
]
class FakeDB:
def list_prune_candidates(self, **kwargs):
seen.update(kwargs)
return rows
def prune_sessions(self, **kwargs):
return len(rows)
def close(self):
pass
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(
sys, "argv", ["hermes", "sessions", "prune", *argv_tail]
)
monkeypatch.setattr("builtins.input", lambda _prompt="": "y")
main_mod.main()
return seen, capsys.readouterr().out
def test_sessions_prune_bare_keeps_90_day_default(monkeypatch, capsys):
"""A truly bare `hermes sessions prune` keeps the implicit 90-day cutoff."""
import time as _time
filters, _out = _run_prune(monkeypatch, capsys, [])
assert filters["started_before"] is not None
assert filters["started_before"] == pytest.approx(
_time.time() - 90 * 86400, abs=60
)
def test_sessions_prune_source_matches_all_ages(monkeypatch, capsys):
"""--source alone suppresses the implicit 90-day cutoff (all ages)."""
filters, _out = _run_prune(monkeypatch, capsys, ["--source", "cron"])
assert filters["started_before"] is None
assert filters["started_after"] is None
assert filters["source"] == "cron"
def test_sessions_prune_source_with_explicit_time_respected(monkeypatch, capsys):
"""--source + explicit --older-than keeps the user's bound."""
import time as _time
filters, _out = _run_prune(
monkeypatch, capsys, ["--source", "cron", "--older-than", "30"]
)
assert filters["started_before"] == pytest.approx(
_time.time() - 30 * 86400, abs=60
)
assert filters["source"] == "cron"
def test_sessions_prune_preview_shows_oldest_newest(monkeypatch, capsys):
"""Confirmation preview surfaces count + oldest/newest session times."""
from hermes_cli.session_filters import format_epoch
_filters, out = _run_prune(monkeypatch, capsys, ["--source", "cron"])
assert "2 session(s) match" in out
assert f"oldest {format_epoch(1_600_000_000.0)}" in out
assert f"newest {format_epoch(1_700_000_000.0)}" in out