diff --git a/scripts/self_healing.py b/scripts/self_healing.py index 8fa65f66..b33cd9bd 100644 --- a/scripts/self_healing.py +++ b/scripts/self_healing.py @@ -14,6 +14,7 @@ import subprocess import argparse import requests import datetime +from typing import Sequence # --- CONFIGURATION --- FLEET = { @@ -192,10 +193,10 @@ EXAMPLES: """ print(help_text) -def main(): +def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Self-healing infrastructure script (safe-by-default).", - add_help=False # We'll handle --help ourselves + add_help=False, ) parser.add_argument("--dry-run", action="store_true", default=False, help="Run in dry-run mode (default behavior).") @@ -209,25 +210,28 @@ def main(): help="Show detailed help about safety features.") parser.add_argument("--help", "-h", action="store_true", default=False, help="Show standard help.") + return parser - args = parser.parse_args() + +def main(argv: Sequence[str] | None = None): + parser = build_parser() + args = parser.parse_args(list(argv) if argv is not None else None) if args.help_safe: print_help_safe() - sys.exit(0) + raise SystemExit(0) if args.help: parser.print_help() - sys.exit(0) + raise SystemExit(0) - # Determine mode: if --execute is given, disable dry-run dry_run = not args.execute - # If --dry-run is explicitly given, ensure dry-run (redundant but clear) if args.dry_run: dry_run = True healer = SelfHealer(dry_run=dry_run, confirm_kill=args.confirm_kill, yes=args.yes) healer.run() + if __name__ == "__main__": main() \ No newline at end of file diff --git a/tests/test_self_healing.py b/tests/test_self_healing.py new file mode 100644 index 00000000..f1392a14 --- /dev/null +++ b/tests/test_self_healing.py @@ -0,0 +1,62 @@ +"""Tests for scripts/self_healing.py safe CLI behavior.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +REPO_ROOT = Path(__file__).parent.parent +spec = importlib.util.spec_from_file_location("self_healing", REPO_ROOT / "scripts" / "self_healing.py") +sh = importlib.util.module_from_spec(spec) +spec.loader.exec_module(sh) + + +class TestMainCli: + def test_help_exits_without_running_healer(self, monkeypatch, capsys): + healer_cls = MagicMock() + monkeypatch.setattr(sh, "SelfHealer", healer_cls) + + with pytest.raises(SystemExit) as excinfo: + sh.main(["--help"]) + + assert excinfo.value.code == 0 + healer_cls.assert_not_called() + out = capsys.readouterr().out + assert "--execute" in out + assert "--help-safe" in out + + def test_help_safe_exits_without_running_healer(self, monkeypatch, capsys): + healer_cls = MagicMock() + monkeypatch.setattr(sh, "SelfHealer", healer_cls) + + with pytest.raises(SystemExit) as excinfo: + sh.main(["--help-safe"]) + + assert excinfo.value.code == 0 + healer_cls.assert_not_called() + out = capsys.readouterr().out + assert "DRY-RUN" in out + assert "--confirm-kill" in out + + def test_default_invocation_runs_in_dry_run_mode(self, monkeypatch): + healer = MagicMock() + healer_cls = MagicMock(return_value=healer) + monkeypatch.setattr(sh, "SelfHealer", healer_cls) + + sh.main([]) + + healer_cls.assert_called_once_with(dry_run=True, confirm_kill=False, yes=False) + healer.run.assert_called_once_with() + + def test_execute_flag_disables_dry_run(self, monkeypatch): + healer = MagicMock() + healer_cls = MagicMock(return_value=healer) + monkeypatch.setattr(sh, "SelfHealer", healer_cls) + + sh.main(["--execute", "--yes", "--confirm-kill"]) + + healer_cls.assert_called_once_with(dry_run=False, confirm_kill=True, yes=True) + healer.run.assert_called_once_with()