fix: unify OpenClaw detection, add isatty guard, fix print_warning import

Combines detection from both PRs into _detect_openclaw_processes():
- Cross-platform process scan (pgrep/tasklist/PowerShell) from PR #8102
- systemd service check from PR #8555
- Returns list[str] with details about what's found

Fixes in cleanup warning (from PR #8555):
- print_warning -> print_error/print_info (print_warning not in import chain)
- Added isatty() guard for non-interactive sessions
- Removed duplicate _check_openclaw_running() in favor of shared function

Updated all tests to match new API.
This commit is contained in:
Teknium 2026-04-12 16:40:10 -07:00 committed by Teknium
parent 76f7411fca
commit c83674dd77
2 changed files with 108 additions and 91 deletions

View file

@ -1,6 +1,7 @@
"""Tests for hermes claw commands."""
from argparse import Namespace
import subprocess
from types import ModuleType
from unittest.mock import MagicMock, patch
@ -199,7 +200,7 @@ class TestCmdMigrate:
@pytest.fixture(autouse=True)
def _mock_openclaw_running(self):
with patch.object(claw_mod, "_is_openclaw_running", return_value=False):
with patch.object(claw_mod, "_detect_openclaw_processes", return_value=[]):
yield
def test_error_when_source_missing(self, tmp_path, capsys):
@ -633,81 +634,92 @@ class TestPrintMigrationReport:
assert "Nothing to migrate" in captured.out
class TestIsOpenclawRunning:
def test_returns_true_when_pgrep_finds_openclaw(self):
class TestDetectOpenclawProcesses:
def test_returns_match_when_pgrep_finds_openclaw(self):
with patch.object(claw_mod, "sys") as mock_sys:
mock_sys.platform = "linux"
with patch.object(claw_mod, "subprocess") as mock_subprocess:
# systemd check misses, pgrep finds openclaw
mock_subprocess.run.side_effect = [
MagicMock(returncode=1, stdout=""), # systemctl
MagicMock(returncode=0, stdout="1234\n"), # pgrep
]
mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired
result = claw_mod._detect_openclaw_processes()
assert len(result) == 1
assert "1234" in result[0]
def test_returns_empty_when_pgrep_finds_nothing(self):
with patch.object(claw_mod, "sys") as mock_sys:
mock_sys.platform = "darwin"
with patch.object(claw_mod, "subprocess") as mock_subprocess:
mock_subprocess.run.side_effect = [
MagicMock(returncode=0),
MagicMock(returncode=1, stdout=""), # systemctl (not found)
MagicMock(returncode=1, stdout=""), # pgrep
]
assert claw_mod._is_openclaw_running() is True
mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired
result = claw_mod._detect_openclaw_processes()
assert result == []
def test_returns_true_when_pgrep_finds_clawd(self):
def test_detects_systemd_service(self):
with patch.object(claw_mod, "sys") as mock_sys:
mock_sys.platform = "linux"
with patch.object(claw_mod, "subprocess") as mock_subprocess:
mock_subprocess.run.side_effect = [
MagicMock(returncode=1),
MagicMock(returncode=0),
MagicMock(returncode=0, stdout="active\n"), # systemctl
MagicMock(returncode=1, stdout=""), # pgrep
]
assert claw_mod._is_openclaw_running() is True
mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired
result = claw_mod._detect_openclaw_processes()
assert len(result) == 1
assert "systemd" in result[0]
def test_returns_false_when_pgrep_finds_nothing(self):
with patch.object(claw_mod, "sys") as mock_sys:
mock_sys.platform = "darwin"
with patch.object(claw_mod, "subprocess") as mock_subprocess:
mock_subprocess.run.side_effect = [
MagicMock(returncode=1),
MagicMock(returncode=1),
]
assert claw_mod._is_openclaw_running() is False
def test_returns_true_on_windows_when_openclaw_exe_running(self):
def test_returns_match_on_windows_when_openclaw_exe_running(self):
with patch.object(claw_mod, "sys") as mock_sys:
mock_sys.platform = "win32"
with patch.object(claw_mod, "subprocess") as mock_subprocess:
# First tasklist (openclaw.exe) matches
mock_subprocess.run.side_effect = [
MagicMock(returncode=0, stdout="openclaw.exe 1234 Console 1 45,056 K\n"),
]
assert claw_mod._is_openclaw_running() is True
result = claw_mod._detect_openclaw_processes()
assert len(result) >= 1
assert any("openclaw.exe" in r for r in result)
def test_returns_true_on_windows_when_node_exe_has_openclaw_in_cmdline(self):
def test_returns_match_on_windows_when_node_exe_has_openclaw_in_cmdline(self):
with patch.object(claw_mod, "sys") as mock_sys:
mock_sys.platform = "win32"
with patch.object(claw_mod, "subprocess") as mock_subprocess:
# tasklist for openclaw.exe and clawd.exe both miss,
# PowerShell finds a matching node.exe process.
mock_subprocess.run.side_effect = [
MagicMock(returncode=0, stdout=""),
MagicMock(returncode=0, stdout=""),
MagicMock(returncode=0, stdout="1234\n"),
MagicMock(returncode=0, stdout=""), # tasklist openclaw.exe
MagicMock(returncode=0, stdout=""), # tasklist clawd.exe
MagicMock(returncode=0, stdout="1234\n"), # PowerShell
]
assert claw_mod._is_openclaw_running() is True
result = claw_mod._detect_openclaw_processes()
assert len(result) >= 1
assert any("node.exe" in r for r in result)
def test_returns_false_on_windows_when_node_exe_has_no_openclaw_in_cmdline(self):
def test_returns_empty_on_windows_when_nothing_found(self):
with patch.object(claw_mod, "sys") as mock_sys:
mock_sys.platform = "win32"
with patch.object(claw_mod, "subprocess") as mock_subprocess:
# Neither dedicated exe nor PowerShell find anything.
mock_subprocess.run.side_effect = [
MagicMock(returncode=0, stdout=""),
MagicMock(returncode=0, stdout=""),
MagicMock(returncode=0, stdout=""),
]
assert claw_mod._is_openclaw_running() is False
result = claw_mod._detect_openclaw_processes()
assert result == []
class TestWarnIfOpenclawRunning:
def test_noop_when_not_running(self, capsys):
with patch.object(claw_mod, "_is_openclaw_running", return_value=False):
with patch.object(claw_mod, "_detect_openclaw_processes", return_value=[]):
claw_mod._warn_if_openclaw_running(auto_yes=False)
captured = capsys.readouterr()
assert captured.out == ""
def test_warns_and_exits_when_running_and_user_declines(self, capsys):
with patch.object(claw_mod, "_is_openclaw_running", return_value=True):
with patch.object(claw_mod, "_detect_openclaw_processes", return_value=["openclaw process(es) (PIDs: 1234)"]):
with patch.object(claw_mod, "prompt_yes_no", return_value=False):
with patch.object(claw_mod.sys.stdin, "isatty", return_value=True):
with pytest.raises(SystemExit) as exc_info:
@ -717,7 +729,7 @@ class TestWarnIfOpenclawRunning:
assert "OpenClaw appears to be running" in captured.out
def test_warns_and_continues_when_running_and_user_accepts(self, capsys):
with patch.object(claw_mod, "_is_openclaw_running", return_value=True):
with patch.object(claw_mod, "_detect_openclaw_processes", return_value=["openclaw process(es) (PIDs: 1234)"]):
with patch.object(claw_mod, "prompt_yes_no", return_value=True):
with patch.object(claw_mod.sys.stdin, "isatty", return_value=True):
claw_mod._warn_if_openclaw_running(auto_yes=False)
@ -725,13 +737,13 @@ class TestWarnIfOpenclawRunning:
assert "OpenClaw appears to be running" in captured.out
def test_warns_and_continues_in_auto_yes_mode(self, capsys):
with patch.object(claw_mod, "_is_openclaw_running", return_value=True):
with patch.object(claw_mod, "_detect_openclaw_processes", return_value=["openclaw process(es) (PIDs: 1234)"]):
claw_mod._warn_if_openclaw_running(auto_yes=True)
captured = capsys.readouterr()
assert "OpenClaw appears to be running" in captured.out
def test_warns_and_continues_in_non_interactive_session(self, capsys):
with patch.object(claw_mod, "_is_openclaw_running", return_value=True):
with patch.object(claw_mod, "_detect_openclaw_processes", return_value=["openclaw process(es) (PIDs: 1234)"]):
with patch.object(claw_mod.sys.stdin, "isatty", return_value=False):
claw_mod._warn_if_openclaw_running(auto_yes=False)
captured = capsys.readouterr()