Files
Teknium ba030bc0db fix(test-seams): monkeypatch.setattr facade aliases — also patch the defining module (57 files)
Tests did monkeypatch.setattr(<facade module>, name) where name is now defined in a
sibling module and the production path reads the sibling's binding. Where production
reads through BOTH bindings the setattr is duplicated onto the defining module (import
added next to the existing alias import); where only the sibling reads it the target is
repointed. Seams whose production readers go through the facade are left alone.
2026-09-03 19:33:28 -07:00

226 lines
10 KiB
Python

"""Tests for ``hermes approvals test`` — dry-run approval verdict CLI.
The tester must compose the REAL runtime evaluators from ``tools.approval``
(detect_hardline_command, _match_user_deny_rule, detect_dangerous_command,
the container-skip gate, and the same ``_command_detection_variants``
normalization/de-obfuscation path) — never reimplement them. It is strictly
read-only: nothing is executed, no prompt fires, nothing is persisted.
Exit-code contract (script-friendly, documented in the CLI help):
0 = allow, 2 = ask-approval, 3 = deny (hardline / user deny rule).
"""
import argparse
import json
import pytest
import tools.approval as A
import tools.approval_prompt as approval_prompt
from tools import approval_context
from tools import approval_detection, approval_floors
from hermes_cli import approvals_test as at
def _args(command, env_type="local", as_json=False):
return argparse.Namespace(
command_words=list(command) if isinstance(command, (list, tuple)) else [command],
env_type=env_type,
json=as_json,
)
@pytest.fixture
def isolated_approvals(monkeypatch):
"""Isolate the evaluators from the dev machine's real config/state."""
monkeypatch.setattr(approval_context, "_get_approval_config", lambda: {"mode": "manual"})
monkeypatch.setattr(A, "_YOLO_MODE_FROZEN", False)
monkeypatch.setattr(A, "is_current_session_yolo_enabled", lambda: False)
monkeypatch.setattr(A, "load_permanent_allowlist", lambda: set())
saved = set(A._permanent_approved)
A._permanent_approved.clear()
# The tester must NEVER prompt or persist — make any attempt explode.
def _boom(*_a, **_kw): # pragma: no cover - failure path
raise AssertionError("read-only tester touched a prompt/persistence path")
monkeypatch.setattr(A, "prompt_dangerous_approval", _boom)
monkeypatch.setattr(approval_prompt, "prompt_dangerous_approval", _boom)
monkeypatch.setattr(A, "save_permanent_allowlist", _boom)
monkeypatch.setattr(A, "submit_pending", _boom, raising=False)
yield A
A._permanent_approved.clear()
A._permanent_approved.update(saved)
class TestVerdicts:
def test_benign_command_allows_with_exit_0(self, isolated_approvals, capsys):
rc = at.approvals_test_command(_args(["ls", "-la"]))
out = capsys.readouterr().out
assert rc == 0
assert "allow" in out
def test_hardline_command_denies_with_rule_name(self, isolated_approvals, capsys):
rc = at.approvals_test_command(_args(["sudo", "re" + "boot"]))
out = capsys.readouterr().out
assert rc == 3
assert "hardline-deny" in out
assert "system shutdown/reboot" in out
def test_dangerous_command_asks_with_exit_2(self, isolated_approvals, capsys):
rc = at.approvals_test_command(_args(["rm", "-rf", "~/project/build"]))
out = capsys.readouterr().out
assert rc == 2
assert "ask-approval" in out
assert "recursive delete" in out
def test_user_deny_rule_from_config_honored(self, isolated_approvals, capsys,
monkeypatch):
monkeypatch.setattr(
approval_context, "_get_approval_config",
lambda: {"mode": "manual", "deny": ["git push *"]})
rc = at.approvals_test_command(_args(["git", "push", "origin", "main"]))
out = capsys.readouterr().out
assert rc == 3
assert "user-deny" in out
assert "git push *" in out
def test_container_env_type_skips_guards_like_runtime(self, isolated_approvals,
capsys):
# Mirrors check_all_command_guards: isolated docker skips BEFORE the
# hardline floor, so even a catastrophic command reports allow.
rc = at.approvals_test_command(_args(["rm", "-rf", "/"], env_type="docker"))
out = capsys.readouterr().out
assert rc == 0
assert "allow" in out
assert "container" in out or "isolated" in out
def test_mode_off_bypasses_dangerous_but_not_hardline(self, isolated_approvals,
capsys, monkeypatch):
monkeypatch.setattr(approval_context, "_get_approval_config", lambda: {"mode": "off"})
rc = at.approvals_test_command(_args(["rm", "-rf", "~/project/build"]))
out = capsys.readouterr().out
assert rc == 0
assert "off" in out
rc = at.approvals_test_command(_args(["sudo", "re" + "boot"]))
assert rc == 3
class TestNormalizationParity:
"""The tester must run the same de-obfuscation path as the runtime."""
def test_obfuscated_command_matches_plain_verdict(self, isolated_approvals,
capsys):
rc_plain = at.approvals_test_command(_args(["rm", "-rf", "/"]))
out_plain = capsys.readouterr().out
rc_obf = at.approvals_test_command(_args(["r\\m", "-rf", "/"]))
out_obf = capsys.readouterr().out
assert rc_plain == rc_obf == 3
assert "recursive delete of root filesystem" in out_plain
assert "recursive delete of root filesystem" in out_obf
# The trace must show the de-obfuscated form the runtime evaluated.
assert "rm -rf /" in out_obf
def test_normalized_trace_shown_when_command_normalizes(self,
isolated_approvals,
capsys):
rc = at.approvals_test_command(_args(['git', 'st""atus']))
out = capsys.readouterr().out
assert rc == 0
assert "git status" in out
def test_composes_real_runtime_detectors(self, isolated_approvals, capsys,
monkeypatch):
"""Prove the tester calls the real evaluators, not a reimplementation."""
calls = {}
def _spy(name, real):
def wrapper(c):
calls[name] = c
return real(c)
return wrapper
monkeypatch.setattr(approval_detection, "detect_hardline_command",
_spy("hardline", approval_detection.detect_hardline_command))
monkeypatch.setattr(approval_detection, "detect_dangerous_command",
_spy("dangerous", approval_detection.detect_dangerous_command))
monkeypatch.setattr(approval_floors, "_match_user_deny_rule",
_spy("deny", approval_floors._match_user_deny_rule))
monkeypatch.setattr(approval_detection, "_command_detection_variants",
_spy("variants", approval_detection._command_detection_variants))
cmd = "rm -rf ~/project/build"
at.approvals_test_command(_args(cmd.split()))
capsys.readouterr()
assert calls.get("hardline") == cmd
assert calls.get("dangerous") == cmd
assert calls.get("deny") == cmd
assert calls.get("variants") == cmd
class TestReadOnly:
def test_nothing_executed(self, isolated_approvals, capsys, tmp_path):
sentinel = tmp_path / "must_not_exist"
rc = at.approvals_test_command(_args(["touch", str(sentinel)]))
capsys.readouterr()
assert rc == 0
assert not sentinel.exists()
def test_dangerous_command_never_prompts_or_persists(self, isolated_approvals,
capsys):
# isolated_approvals wires prompt/persistence to AssertionError; a
# dangerous command must complete without touching either.
rc = at.approvals_test_command(_args(["rm", "-rf", "~/project/build"]))
capsys.readouterr()
assert rc == 2
class TestOutputAndWiring:
def test_json_output_is_machine_readable(self, isolated_approvals, capsys):
rc = at.approvals_test_command(_args(["sudo", "re" + "boot"], as_json=True))
payload = json.loads(capsys.readouterr().out)
assert rc == 3
assert payload["verdict"] == "hardline-deny"
assert payload["exit_code"] == 3
assert payload["rule"] == "system shutdown/reboot"
assert payload["command"] == "sudo re" + "boot"
assert isinstance(payload["normalized_variants"], list)
def test_empty_command_is_usage_error(self, isolated_approvals, capsys):
rc = at.approvals_test_command(_args([]))
assert rc == 1
def test_dispatcher_routes_test_subcommand(self, isolated_approvals, capsys):
from hermes_cli.approvals_suggest import approvals_command
args = _args(["ls"])
args.approvals_command = "test"
rc = approvals_command(args)
out = capsys.readouterr().out
assert rc == 0
assert "allow" in out
def test_parser_wires_test_subcommand(self, isolated_approvals, capsys):
from hermes_cli.subcommands.approvals import build_approvals_parser
parser = argparse.ArgumentParser()
sub = parser.add_subparsers()
sentinel = []
build_approvals_parser(sub, cmd_approvals=lambda a: sentinel.append(a) or 0)
args = parser.parse_args(
["approvals", "test", "--env-type", "ssh", "--", "ls", "-la"])
assert args.approvals_command == "test"
assert args.env_type == "ssh"
# argparse REMAINDER keeps the leading "--"; the handler strips it.
# dest is command_words (NOT command) so main.py's startup path can
# keep reading args.command as the top-level subcommand name.
assert args.command_words == ["--", "ls", "-la"]
# The subparser must NOT claim the "command" dest — main.py's startup
# path reads args.command as the top-level subcommand name.
assert getattr(args, "command", None) != ["--", "ls", "-la"]
args.func(args)
assert sentinel
def test_leading_separator_stripped_from_command(self, isolated_approvals,
capsys):
rc = at.approvals_test_command(_args(["--", "ls", "-la"]))
out = capsys.readouterr().out
assert rc == 0
assert "ls -la" in out
assert "-- ls" not in out