hermes-agent/tests/gateway/test_shutdown_forensics.py
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00

144 lines
5.1 KiB
Python

"""Tests for gateway.shutdown_forensics — fast snapshot + async diag spawn."""
from __future__ import annotations
import json
import os
import signal
import sys
import time
from pathlib import Path
import pytest
from gateway import shutdown_forensics as sf
# ---------------------------------------------------------------------------
# _signal_name
# ---------------------------------------------------------------------------
class TestSignalName:
def test_unknown_int_returns_signal_num_token(self):
# Pick an integer extremely unlikely to ever be a real signal alias
assert sf._signal_name(9999) == "signal#9999"
# ---------------------------------------------------------------------------
# snapshot_shutdown_context
# ---------------------------------------------------------------------------
class TestSnapshotShutdownContext:
def test_handles_none_signal(self):
ctx = sf.snapshot_shutdown_context(None)
assert ctx["signal"] == "UNKNOWN"
assert ctx["signal_num"] is None
def test_includes_timestamps(self):
before = time.time()
ctx = sf.snapshot_shutdown_context(signal.SIGTERM)
after = time.time()
assert before <= ctx["ts"] <= after
assert isinstance(ctx["ts_monotonic"], float)
def test_under_systemd_false_without_invocation_id_and_normal_ppid(
self, monkeypatch
):
monkeypatch.delenv("INVOCATION_ID", raising=False)
# We can't actually change ppid; skip if we happen to be reaped
# by init (e.g. running under tini).
if os.getppid() == 1:
pytest.skip("test process is reaped by init")
ctx = sf.snapshot_shutdown_context(signal.SIGTERM)
assert ctx["under_systemd"] is False
def test_detects_takeover_marker_for_self(self, tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
marker = tmp_path / ".gateway-takeover.json"
marker.write_text(
f'{{"target_pid": {os.getpid()}, "replacer_pid": 99999}}',
encoding="utf-8",
)
ctx = sf.snapshot_shutdown_context(signal.SIGTERM)
assert "takeover_marker" in ctx
assert ctx["takeover_marker_for_self"] is True
# ---------------------------------------------------------------------------
# format_context_for_log / context_as_json
# ---------------------------------------------------------------------------
class TestFormatters:
def test_context_as_json_handles_unserialisable_values(self):
ctx = {"signal": "SIGTERM", "weird": object()}
payload = sf.context_as_json(ctx)
# default=str means objects get repr'd, JSON stays valid
decoded = json.loads(payload)
assert decoded["signal"] == "SIGTERM"
assert "weird" in decoded
# ---------------------------------------------------------------------------
# spawn_async_diagnostic
# ---------------------------------------------------------------------------
class TestSpawnAsyncDiagnostic:
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX-only diagnostic")
def test_spawns_subprocess_and_writes_output(self, tmp_path):
log_path = tmp_path / "diag.log"
pid = sf.spawn_async_diagnostic(log_path, "SIGTERM", timeout_seconds=3.0)
assert pid is not None and pid > 0
# Wait briefly for the subprocess to write — bounded by its own timeout.
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
if log_path.exists() and log_path.stat().st_size > 0:
# Wait a touch longer for the script to finish writing
time.sleep(0.2)
break
time.sleep(0.1)
# Reap the subprocess so it doesn't show up as a zombie.
try:
os.waitpid(pid, 0)
except (ChildProcessError, OSError):
pass
assert log_path.exists()
contents = log_path.read_text(encoding="utf-8", errors="replace")
assert "shutdown diagnostic" in contents
assert "SIGTERM" in contents
# ---------------------------------------------------------------------------
# _parse_systemd_duration_to_us
# ---------------------------------------------------------------------------
class TestParseSystemdDuration:
def test_seconds(self):
assert sf._parse_systemd_duration_to_us("90s") == 90 * 1_000_000
def test_minutes(self):
assert sf._parse_systemd_duration_to_us("3min") == 180 * 1_000_000
# ---------------------------------------------------------------------------
# check_systemd_timing_alignment
# ---------------------------------------------------------------------------
class TestCheckSystemdTimingAlignment:
def test_returns_none_when_unit_undeterminable(self, monkeypatch):
monkeypatch.setenv("INVOCATION_ID", "abc")
# /proc/self/cgroup likely doesn't end in .service for the test runner
result = sf.check_systemd_timing_alignment(180.0)
# Either None (we couldn't find a unit) or a dict with mismatch info
# for whatever unit pytest IS in. Both are valid; we just ensure
# the function doesn't raise.
assert result is None or isinstance(result, dict)