hermes-agent/tests/hermes_cli/test_checkpoints_prune.py
Teknium 39975613b1
test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.

Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
  (DEFAULT_CONFIG smart-approval leaked in) — pinned approval
  mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
  silently probed live provider catalogs (~2s/test) — stubbed
  cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
  shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
  MagicMock agents — interrupt.side_effect now clears _running_agents
  (22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
  (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
  patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
  5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
  with rationale comment — the watchdog-dump assertion needs the loop
  blocked well past deadline+grace under parallel load (flaked once in
  the 40-worker verification run at a 0.2s margin).

Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
2026-07-29 13:39:40 -07:00

125 lines
4 KiB
Python

"""Tests for `hermes checkpoints prune`'s orphan confirmation flow.
Covers the P1 raised on PR #69141: the confirmation preview must cover
BOTH v2 projects (`store_status()["projects"]`) and pre-v2 shadow repos
(`store_status()["pre_v2_projects"]`), since `prune_checkpoints()` deletes
orphans from both layouts. Exercises decline / accept / --force across
pre-v2-only and mixed (v2 + pre-v2) stores.
"""
from __future__ import annotations
import argparse
import pytest
def _ns(**kwargs) -> argparse.Namespace:
defaults = {"retention_days": 7, "max_size_mb": 500, "keep_orphans": False, "force": False}
defaults.update(kwargs)
return argparse.Namespace(**defaults)
def _prune_result(**kwargs) -> dict:
result = {"scanned": 0, "deleted_orphan": 0, "deleted_stale": 0, "errors": 0, "bytes_freed": 0}
result.update(kwargs)
return result
_V2_ORPHAN_ONLY_STATUS = {
"projects": [],
"pre_v2_projects": [],
}
_PRE_V2_ONLY_STATUS = {
"projects": [],
"pre_v2_projects": [
{"path": "/home/user/.hermes/checkpoints/deadbeefcafebabe", "workdir": None, "exists": False},
],
}
_MIXED_STATUS = {
"projects": [
{"hash": "abc123", "workdir": "/gone/v2-project", "exists": False, "commits": 4},
],
"pre_v2_projects": [
{"path": "/home/user/.hermes/checkpoints/deadbeefcafebabe", "workdir": "/gone/pre-v2-project", "exists": False},
],
}
def _patch_checkpoint_manager(monkeypatch, status: dict, prune_calls: list):
import tools.checkpoint_manager as ckpt_mgr
monkeypatch.setattr(ckpt_mgr, "store_status", lambda *a, **k: status)
def _fake_prune(**kwargs):
prune_calls.append(kwargs)
return _prune_result(
deleted_orphan=len(status["projects"]) + len(status["pre_v2_projects"]),
)
monkeypatch.setattr(ckpt_mgr, "prune_checkpoints", _fake_prune)
# ─── pre-v2-only store ──────────────────────────────────────────────────────
# ─── mixed store (v2 + pre-v2) ──────────────────────────────────────────────
# ─── --keep-orphans skips the prompt entirely, on either layout ───────────
@pytest.mark.parametrize("status", [_PRE_V2_ONLY_STATUS, _MIXED_STATUS], ids=["pre_v2_only", "mixed"])
def test_keep_orphans_skips_prompt(monkeypatch, capsys, status):
import hermes_cli.checkpoints as checkpoints_cli
prune_calls: list = []
_patch_checkpoint_manager(monkeypatch, status, prune_calls)
def _unexpected_input(_prompt):
raise AssertionError("input() must not be called when --keep-orphans is passed")
monkeypatch.setattr("builtins.input", _unexpected_input)
rc = checkpoints_cli.cmd_prune(_ns(keep_orphans=True))
assert rc == 0
assert len(prune_calls) == 1
assert prune_calls[0]["delete_orphans"] is False
# ─── no orphans present: never prompts even without --force ───────────────
# ─── allowlist binding: preview set == deletion set, even when empty ───────
def test_empty_preview_binds_empty_allowlist(monkeypatch, capsys):
"""Zero-orphan-preview timing regression (PR #69141 review).
When the non-force preview shows zero orphans, no prompt runs — but the
later rescan inside prune_checkpoints() may discover a project that
became orphaned *after* the preview. That undisplayed, unconfirmed orphan
must not be deletable: the allowlist passed down must be the exact
(empty) displayed set, never the unrestricted None sentinel.
"""
import hermes_cli.checkpoints as checkpoints_cli
prune_calls: list = []
_patch_checkpoint_manager(monkeypatch, _V2_ORPHAN_ONLY_STATUS, prune_calls)
rc = checkpoints_cli.cmd_prune(_ns())
assert rc == 0
assert len(prune_calls) == 1
assert prune_calls[0]["orphan_allowlist"] == set()