test_relaunch.py
1 """Tests for hermes_cli.relaunch — unified self-relaunch utility.""" 2 3 import sys 4 5 import pytest 6 7 from hermes_cli import relaunch as relaunch_mod 8 9 10 class TestResolveHermesBin: 11 def test_prefers_absolute_argv0_when_executable(self, monkeypatch): 12 fake = "/nix/store/abc/bin/hermes" 13 monkeypatch.setattr(sys, "argv", [fake]) 14 monkeypatch.setattr(relaunch_mod.os.path, "isfile", lambda p: p == fake) 15 monkeypatch.setattr(relaunch_mod.os, "access", lambda p, mode: p == fake) 16 assert relaunch_mod.resolve_hermes_bin() == fake 17 18 def test_resolves_relative_argv0(self, monkeypatch, tmp_path): 19 fake = tmp_path / "hermes" 20 fake.write_text("#!/bin/sh\n") 21 fake.chmod(0o755) 22 monkeypatch.setattr(sys, "argv", [str(fake.name)]) 23 monkeypatch.chdir(tmp_path) 24 # Ensure we don't accidentally match a real 'hermes' on PATH 25 monkeypatch.setattr(relaunch_mod.shutil, "which", lambda _name: None) 26 assert relaunch_mod.resolve_hermes_bin() == str(fake) 27 28 def test_falls_back_to_path_which(self, monkeypatch): 29 monkeypatch.setattr(sys, "argv", ["-c"]) # not a real path 30 monkeypatch.setattr( 31 relaunch_mod.shutil, "which", lambda name: "/usr/bin/hermes" if name == "hermes" else None 32 ) 33 assert relaunch_mod.resolve_hermes_bin() == "/usr/bin/hermes" 34 35 def test_returns_none_when_unresolvable(self, monkeypatch): 36 monkeypatch.setattr(sys, "argv", ["-c"]) 37 monkeypatch.setattr(relaunch_mod.shutil, "which", lambda _name: None) 38 assert relaunch_mod.resolve_hermes_bin() is None 39 40 41 class TestExtractInheritedFlags: 42 def test_extracts_tui_and_dev(self): 43 argv = ["--tui", "--dev", "chat"] 44 assert relaunch_mod._extract_inherited_flags(argv) == ["--tui", "--dev"] 45 46 def test_extracts_profile_with_value(self): 47 argv = ["--profile", "work", "chat"] 48 assert relaunch_mod._extract_inherited_flags(argv) == ["--profile", "work"] 49 50 def test_extracts_short_p_with_value(self): 51 argv = ["-p", "work"] 52 assert relaunch_mod._extract_inherited_flags(argv) == ["-p", "work"] 53 54 def test_extracts_equals_form(self): 55 argv = ["--profile=work", "--model=anthropic/claude-sonnet-4"] 56 assert relaunch_mod._extract_inherited_flags(argv) == [ 57 "--profile=work", 58 "--model=anthropic/claude-sonnet-4", 59 ] 60 61 def test_skips_unknown_flags(self): 62 argv = ["--foo", "bar", "--tui"] 63 assert relaunch_mod._extract_inherited_flags(argv) == ["--tui"] 64 65 def test_does_not_consume_flag_like_value(self): 66 argv = ["--tui", "--resume", "abc123"] 67 assert relaunch_mod._extract_inherited_flags(argv) == ["--tui"] 68 69 def test_preserves_multiple_skills(self): 70 argv = ["-s", "foo", "-s", "bar", "--tui"] 71 assert relaunch_mod._extract_inherited_flags(argv) == ["-s", "foo", "-s", "bar", "--tui"] 72 73 74 class TestInheritedFlagTable: 75 """Sanity-check the argparse-introspected table that drives extraction.""" 76 77 def test_short_and_long_aliases_are_paired(self): 78 table = dict(relaunch_mod._INHERITED_FLAGS_TABLE) 79 # Each pair declared together in the parser shares takes_value. 80 for short, long_ in [ 81 ("-p", "--profile"), 82 ("-m", "--model"), 83 ("-s", "--skills"), 84 ]: 85 assert table[short] == table[long_], f"{short}/{long_} disagree" 86 87 def test_store_true_flags_do_not_take_value(self): 88 table = dict(relaunch_mod._INHERITED_FLAGS_TABLE) 89 for flag in ["--tui", "--dev", "--yolo", "--ignore-user-config", "--ignore-rules"]: 90 assert table[flag] is False, f"{flag} should not take a value" 91 92 def test_value_flags_take_value(self): 93 table = dict(relaunch_mod._INHERITED_FLAGS_TABLE) 94 for flag in ["--profile", "--model", "--provider", "--skills"]: 95 assert table[flag] is True, f"{flag} should take a value" 96 97 def test_excluded_flags_are_not_inherited(self): 98 table = dict(relaunch_mod._INHERITED_FLAGS_TABLE) 99 # --worktree creates a new worktree per process; inheriting would 100 # orphan the parent's. Chat-only flags (--quiet/-Q, --verbose/-v, 101 # --source) can't be in argv at the existing relaunch callsites. 102 for flag in ["-w", "--worktree", "-Q", "--quiet", "-v", "--verbose", "--source"]: 103 assert flag not in table, f"{flag} should not be inherited" 104 105 106 class TestBuildRelaunchArgv: 107 def test_uses_bin_when_available(self, monkeypatch): 108 monkeypatch.setattr(relaunch_mod, "resolve_hermes_bin", lambda: "/usr/bin/hermes") 109 argv = relaunch_mod.build_relaunch_argv(["--resume", "abc"]) 110 assert argv[0] == "/usr/bin/hermes" 111 112 def test_falls_back_to_python_module(self, monkeypatch): 113 monkeypatch.setattr(relaunch_mod, "resolve_hermes_bin", lambda: None) 114 argv = relaunch_mod.build_relaunch_argv(["--resume", "abc"]) 115 assert argv == [sys.executable, "-m", "hermes_cli.main", "--resume", "abc"] 116 117 def test_preserves_inherited_flags(self, monkeypatch): 118 monkeypatch.setattr(relaunch_mod, "resolve_hermes_bin", lambda: "/usr/bin/hermes") 119 original = ["--tui", "--dev", "--profile", "work", "sessions", "browse"] 120 argv = relaunch_mod.build_relaunch_argv(["--resume", "abc"], original_argv=original) 121 assert "--tui" in argv 122 assert "--dev" in argv 123 assert "--profile" in argv 124 assert "work" in argv 125 assert "--resume" in argv 126 assert "abc" in argv 127 # The original subcommand should not survive 128 assert "sessions" not in argv 129 assert "browse" not in argv 130 131 def test_can_disable_preserve(self, monkeypatch): 132 monkeypatch.setattr(relaunch_mod, "resolve_hermes_bin", lambda: "/usr/bin/hermes") 133 original = ["--tui", "chat"] 134 argv = relaunch_mod.build_relaunch_argv( 135 ["--resume", "abc"], preserve_inherited=False, original_argv=original 136 ) 137 assert "--tui" not in argv 138 assert argv == ["/usr/bin/hermes", "--resume", "abc"] 139 140 141 class TestRelaunch: 142 def test_calls_execvp(self, monkeypatch): 143 calls = [] 144 145 def fake_execvp(path, argv): 146 calls.append((path, argv)) 147 raise SystemExit(0) 148 149 monkeypatch.setattr(relaunch_mod.os, "execvp", fake_execvp) 150 monkeypatch.setattr(relaunch_mod, "resolve_hermes_bin", lambda: "/usr/bin/hermes") 151 152 with pytest.raises(SystemExit): 153 relaunch_mod.relaunch(["--resume", "abc"]) 154 155 assert calls == [("/usr/bin/hermes", ["/usr/bin/hermes", "--resume", "abc"])]