install Echo Core as systemd service, update CLI for systemctl
- Created echo-core.service and echo-whatsapp-bridge.service (user units) - CLI status/doctor now use systemctl --user show instead of PID file - CLI restart uses kill+start pattern for reliability - Added echo stop command - CLI shebang uses venv python directly for keyring support - Updated tests to mock _get_service_status instead of PID file - 440 tests pass Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import signal
|
||||
import time
|
||||
from contextlib import ExitStack
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
@@ -53,31 +53,18 @@ def iso(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
class TestStatus:
|
||||
def test_offline_no_pid(self, iso, capsys):
|
||||
cli.cmd_status(_args())
|
||||
out = capsys.readouterr().out
|
||||
assert "OFFLINE" in out
|
||||
assert "no PID file" in out
|
||||
def _mock_service(self, active="active", pid="1234", ts="Fri 2026-02-13 22:00:00 UTC"):
|
||||
return {"ActiveState": active, "MainPID": pid, "ActiveEnterTimestamp": ts}
|
||||
|
||||
def test_offline_invalid_pid(self, iso, capsys):
|
||||
iso["pid_file"].write_text("garbage")
|
||||
cli.cmd_status(_args())
|
||||
out = capsys.readouterr().out
|
||||
assert "OFFLINE" in out
|
||||
assert "invalid PID file" in out
|
||||
|
||||
def test_offline_stale_pid(self, iso, capsys):
|
||||
iso["pid_file"].write_text("999999")
|
||||
with patch("os.kill", side_effect=OSError):
|
||||
def test_offline(self, iso, capsys):
|
||||
with patch("cli._get_service_status", return_value=self._mock_service(active="inactive", pid="0")):
|
||||
cli.cmd_status(_args())
|
||||
out = capsys.readouterr().out
|
||||
assert "OFFLINE" in out
|
||||
assert "not running" in out
|
||||
|
||||
def test_online(self, iso, capsys):
|
||||
iso["pid_file"].write_text("1234")
|
||||
iso["sessions_file"].write_text(json.dumps({"ch1": {}}))
|
||||
with patch("os.kill"):
|
||||
with patch("cli._get_service_status", return_value=self._mock_service()):
|
||||
cli.cmd_status(_args())
|
||||
out = capsys.readouterr().out
|
||||
assert "ONLINE" in out
|
||||
@@ -85,10 +72,20 @@ class TestStatus:
|
||||
assert "1 active" in out
|
||||
|
||||
def test_sessions_count_zero(self, iso, capsys):
|
||||
cli.cmd_status(_args())
|
||||
with patch("cli._get_service_status", return_value=self._mock_service(active="inactive")):
|
||||
cli.cmd_status(_args())
|
||||
out = capsys.readouterr().out
|
||||
assert "0 active" in out
|
||||
|
||||
def test_bridge_status(self, iso, capsys):
|
||||
with patch("cli._get_service_status", side_effect=[
|
||||
self._mock_service(),
|
||||
self._mock_service(pid="5678"),
|
||||
]):
|
||||
cli.cmd_status(_args())
|
||||
out = capsys.readouterr().out
|
||||
assert "WA Bridge: ONLINE" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cmd_doctor
|
||||
@@ -119,14 +116,13 @@ class TestDoctor:
|
||||
patch("os.statvfs", return_value=stat),
|
||||
patch("subprocess.run", return_value=mock_proc),
|
||||
patch("urllib.request.urlopen", return_value=mock_resp),
|
||||
patch("cli._get_service_status", return_value={"ActiveState": "active", "MainPID": "123"}),
|
||||
]
|
||||
|
||||
if setup_full:
|
||||
# Create .gitignore with required entries
|
||||
gi_path = cli.PROJECT_ROOT / ".gitignore"
|
||||
gi_path.write_text("sessions/\nlogs/\n.env\n*.sqlite\n")
|
||||
# Create PID file with current PID
|
||||
iso["pid_file"].write_text(str(_os.getpid()))
|
||||
# Set config.json not world-readable
|
||||
iso["config_file"].chmod(0o600)
|
||||
# Create sessions dir not world-readable
|
||||
@@ -205,33 +201,33 @@ class TestDoctor:
|
||||
|
||||
|
||||
class TestRestart:
|
||||
def test_no_pid_file(self, iso, capsys):
|
||||
with pytest.raises(SystemExit):
|
||||
cli.cmd_restart(_args())
|
||||
assert "no PID file" in capsys.readouterr().out
|
||||
def test_restart_success(self, iso, capsys):
|
||||
with patch("cli._systemctl", return_value=(0, "")), \
|
||||
patch("cli._get_service_status", return_value={"ActiveState": "active", "MainPID": "999"}), \
|
||||
patch("time.sleep"):
|
||||
cli.cmd_restart(_args(bridge=False))
|
||||
out = capsys.readouterr().out
|
||||
assert "restarted" in out.lower()
|
||||
assert "999" in out
|
||||
|
||||
def test_invalid_pid(self, iso, capsys):
|
||||
iso["pid_file"].write_text("nope")
|
||||
with pytest.raises(SystemExit):
|
||||
cli.cmd_restart(_args())
|
||||
assert "invalid PID" in capsys.readouterr().out
|
||||
|
||||
def test_dead_process(self, iso, capsys):
|
||||
iso["pid_file"].write_text("99999")
|
||||
with patch("os.kill", side_effect=OSError):
|
||||
with pytest.raises(SystemExit):
|
||||
cli.cmd_restart(_args())
|
||||
assert "not running" in capsys.readouterr().out
|
||||
|
||||
def test_sends_sigterm(self, iso, capsys):
|
||||
iso["pid_file"].write_text("42")
|
||||
def test_restart_with_bridge(self, iso, capsys):
|
||||
calls = []
|
||||
with patch("os.kill", side_effect=lambda p, s: calls.append((p, s))):
|
||||
cli.cmd_restart(_args())
|
||||
assert (42, 0) in calls
|
||||
assert (42, signal.SIGTERM) in calls
|
||||
assert "SIGTERM" in capsys.readouterr().out
|
||||
assert "42" in capsys.readouterr().out or True # already consumed above
|
||||
def mock_ctl(*args):
|
||||
calls.append(args)
|
||||
return (0, "")
|
||||
with patch("cli._systemctl", side_effect=mock_ctl), \
|
||||
patch("cli._get_service_status", return_value={"ActiveState": "active", "MainPID": "100"}), \
|
||||
patch("time.sleep"):
|
||||
cli.cmd_restart(_args(bridge=True))
|
||||
# Should have called kill+start for both bridge and core
|
||||
assert len(calls) == 4
|
||||
|
||||
def test_restart_fails(self, iso, capsys):
|
||||
with patch("cli._systemctl", return_value=(0, "")), \
|
||||
patch("cli._get_service_status", return_value={"ActiveState": "failed"}), \
|
||||
patch("time.sleep"):
|
||||
with pytest.raises(SystemExit):
|
||||
cli.cmd_restart(_args(bridge=False))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user