diff --git a/tests/test_core.py b/tests/test_core.py index aaf2faf..978ca27 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -638,3 +638,159 @@ def test_args_secret_warning_env_not_triggered(): def test_args_secret_warning_empty(): assert c.args_secret_warning({}) is None assert c.args_secret_warning({"args": []}) is None + + +# --------------------------------------------------------------------------- # +# restart_claude_desktop / profile_targets_claude_desktop (issue #9) +# --------------------------------------------------------------------------- # +def test_profile_targets_claude_desktop_true_for_desktop_config(): + p = c.Profile(label="Claude", path="/x/Claude/claude_desktop_config.json", config_exists=True) + assert c.profile_targets_claude_desktop(p) + + +def test_profile_targets_claude_desktop_false_for_claude_code(): + p = c.Profile(label="Claude Code", path="/home/me/.claude.json", config_exists=True) + assert not c.profile_targets_claude_desktop(p) + + +def test_profile_targets_claude_desktop_false_for_legacy_settings(): + p = c.Profile( + label="Claude Code (legacy settings.json)", + path="/home/me/.claude/settings.json", + config_exists=True, + ) + assert not c.profile_targets_claude_desktop(p) + + +class _FakeCompletedProcess: + def __init__(self, returncode=0, stdout="", stderr=""): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +def test_restart_claude_desktop_macos_commands(monkeypatch): + """macOS: pkill -x "Claude" then open -a Claude, in that order.""" + calls = [] + + def fake_run(cmd, **kwargs): + calls.append(cmd) + return _FakeCompletedProcess(returncode=0) + + monkeypatch.setattr(c.sys, "platform", "darwin") + monkeypatch.setattr(c.subprocess, "run", fake_run) + result = c.restart_claude_desktop() + assert result.success + assert calls[0] == ["pkill", "-x", "Claude"] + assert calls[1] == ["open", "-a", "Claude"] + + +def test_restart_claude_desktop_macos_pkill_not_running_is_fine(monkeypatch): + """pkill exiting non-zero (nothing to kill) must NOT be treated as failure.""" + + def fake_run(cmd, **kwargs): + if cmd[0] == "pkill": + return _FakeCompletedProcess(returncode=1) # no matching process + return _FakeCompletedProcess(returncode=0) + + monkeypatch.setattr(c.sys, "platform", "darwin") + monkeypatch.setattr(c.subprocess, "run", fake_run) + result = c.restart_claude_desktop() + assert result.success + + +def test_restart_claude_desktop_macos_relaunch_failure_reported(monkeypatch): + def fake_run(cmd, **kwargs): + if cmd[0] == "open": + return _FakeCompletedProcess(returncode=1, stderr="Unable to find application") + return _FakeCompletedProcess(returncode=1) + + monkeypatch.setattr(c.sys, "platform", "darwin") + monkeypatch.setattr(c.subprocess, "run", fake_run) + result = c.restart_claude_desktop() + assert not result.success + assert "Unable to find application" in result.detail + + +def test_restart_claude_desktop_macos_pkill_binary_missing_does_not_raise(monkeypatch): + """pkill raising OSError (binary missing) must be swallowed, not propagated -- + relaunch is still attempted.""" + + def fake_run(cmd, **kwargs): + if cmd[0] == "pkill": + raise OSError("pkill not found") + return _FakeCompletedProcess(returncode=0) + + monkeypatch.setattr(c.sys, "platform", "darwin") + monkeypatch.setattr(c.subprocess, "run", fake_run) + result = c.restart_claude_desktop() + assert result.success + + +def test_restart_claude_desktop_windows_commands(monkeypatch): + """Windows: taskkill the process, then relaunch via the Start-menu shortcut.""" + calls = [] + + def fake_run(cmd, **kwargs): + calls.append(cmd) + return _FakeCompletedProcess(returncode=0) + + monkeypatch.setattr(c.sys, "platform", "win32") + monkeypatch.setattr(c.subprocess, "run", fake_run) + result = c.restart_claude_desktop() + assert result.success + assert calls[0] == ["taskkill", "/IM", "Claude.exe", "/F"] + assert calls[1][:3] == ["cmd", "/c", "start"] + assert calls[1][-1].endswith("Claude.lnk") + + +def test_restart_claude_desktop_windows_relaunch_failure_reported(monkeypatch): + def fake_run(cmd, **kwargs): + if cmd[0] == "cmd": + return _FakeCompletedProcess(returncode=1, stderr="not found") + return _FakeCompletedProcess(returncode=0) + + monkeypatch.setattr(c.sys, "platform", "win32") + monkeypatch.setattr(c.subprocess, "run", fake_run) + result = c.restart_claude_desktop() + assert not result.success + assert "not found" in result.detail + + +def test_restart_claude_desktop_linux_commands(monkeypatch): + """Linux: pkill claude, then relaunch via a detached Popen (no waiting).""" + run_calls = [] + popen_calls = [] + + def fake_run(cmd, **kwargs): + run_calls.append(cmd) + return _FakeCompletedProcess(returncode=0) + + class _FakePopen: + def __init__(self, cmd, **kwargs): + popen_calls.append((cmd, kwargs)) + + monkeypatch.setattr(c.sys, "platform", "linux") + monkeypatch.setattr(c.subprocess, "run", fake_run) + monkeypatch.setattr(c.subprocess, "Popen", _FakePopen) + result = c.restart_claude_desktop() + assert result.success + assert run_calls == [["pkill", "claude"]] + cmd, kwargs = popen_calls[0] + assert cmd == ["claude"] + assert kwargs.get("start_new_session") is True + + +def test_restart_claude_desktop_linux_popen_failure_reported(monkeypatch): + def fake_run(cmd, **kwargs): + return _FakeCompletedProcess(returncode=0) + + def fake_popen(cmd, **kwargs): + raise OSError("no such file or directory") + + monkeypatch.setattr(c.sys, "platform", "linux") + monkeypatch.setattr(c.subprocess, "run", fake_run) + monkeypatch.setattr(c.subprocess, "Popen", fake_popen) + result = c.restart_claude_desktop() + assert not result.success + assert "Claude Desktop" in result.detail