feat(mcp): enhance multi-platform notification resilience and reactive wakeups
- AgentApiNotifier: Sort candidate process sessions by creation time descending to prioritize the latest active IDE session over stale PIDs. Implement automatic retry across candidate address/token pairs and persist working credentials to shared environment files (~/.gemini/jetski/.jetski_env and ~/.artemis/.artemis_env). - DesktopNotifier: Enable native OS desktop toast notifications by default on macOS, Linux, and Windows unless explicitly disabled or running in CI. Ensure MCP installer sets ARTEMIS_DESKTOP_NOTIFY=true in generated IDE configurations. - ScriptNotifier: Add universal command hook adapter (ARTEMIS_NOTIFY_CMD / MCP_NOTIFY_COMMAND) for custom IDE and CLI integration. - Fault Isolation: Wrap CompositeNotifier and global notify entrypoint in strict try-except blocks to guarantee zero-crash execution even if any individual notifier fails. - Tests & Docs: Add unit tests for session recovery, fault tolerance, script hook execution, and document multi-platform notification architecture in README.
This commit is contained in:
1 parent
4927c4f0b9
commit
c1a4b49a21
9 files changed
+468
-110
No files matched your search
@@ -94,12 +94,40 @@ def test_webhook_notifier_not_configured(monkeypatch):
|
||||
assert notifier.notify("conv-1", "hello") is False
|
||||
|
||||
|
||||
def test_desktop_notifier_not_enabled(monkeypatch):
|
||||
monkeypatch.delenv("ARTEMIS_DESKTOP_NOTIFY", raising=False)
|
||||
def test_desktop_notifier_disabled(monkeypatch):
|
||||
monkeypatch.setenv("ARTEMIS_DESKTOP_NOTIFY", "false")
|
||||
notifier = DesktopNotifier()
|
||||
assert notifier.is_available() is False
|
||||
|
||||
|
||||
def test_desktop_notifier_enabled_by_default(monkeypatch):
|
||||
monkeypatch.delenv("ARTEMIS_DESKTOP_NOTIFY", raising=False)
|
||||
monkeypatch.delenv("CI", raising=False)
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
if sys.platform == "linux":
|
||||
monkeypatch.setattr(shutil, "which", lambda cmd: "/usr/bin/notify-send" if cmd == "notify-send" else None)
|
||||
elif sys.platform == "darwin":
|
||||
monkeypatch.setattr(shutil, "which", lambda cmd: "/usr/bin/osascript" if cmd == "osascript" else None)
|
||||
notifier = DesktopNotifier()
|
||||
assert notifier.is_available() is True
|
||||
|
||||
|
||||
def test_script_notifier(monkeypatch):
|
||||
from mcp_server.notifiers.script import ScriptNotifier
|
||||
|
||||
monkeypatch.delenv("ARTEMIS_NOTIFY_CMD", raising=False)
|
||||
monkeypatch.delenv("MCP_NOTIFY_COMMAND", raising=False)
|
||||
notifier = ScriptNotifier()
|
||||
assert notifier.is_available() is False
|
||||
|
||||
monkeypatch.setenv("ARTEMIS_NOTIFY_CMD", "echo 'Notify: {title} - {message}'")
|
||||
assert notifier.is_available() is True
|
||||
res = notifier.notify("conv-123", "Task done", title="Success", payload={"trace_id": "t-1"})
|
||||
assert res is True
|
||||
|
||||
|
||||
def test_composite_notifier_dispatch():
|
||||
d1 = DummyNotifier(available=True, return_val=True)
|
||||
d2 = DummyNotifier(available=False, return_val=False)
|
||||
@@ -122,6 +150,29 @@ def test_composite_notifier_dispatch():
|
||||
assert d3.called_with is not None
|
||||
|
||||
|
||||
class BrokenNotifier(BaseNotifier):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "broken"
|
||||
|
||||
def is_available(self) -> bool:
|
||||
raise RuntimeError("Catastrophic error in is_available")
|
||||
|
||||
def notify(self, conversation_id, message, title=None, event_type="completed", payload=None):
|
||||
raise RuntimeError("Catastrophic error in notify")
|
||||
|
||||
|
||||
def test_composite_notifier_fault_tolerance():
|
||||
broken = BrokenNotifier()
|
||||
d1 = DummyNotifier(available=True, return_val=True)
|
||||
composite = CompositeNotifier(notifiers=[broken, d1])
|
||||
|
||||
# Must never raise an exception even if a notifier crashes in is_available or notify
|
||||
assert composite.is_available() is True
|
||||
assert composite.notify("conv-test", "Message") is True
|
||||
assert d1.called_with is not None
|
||||
|
||||
|
||||
def test_global_notify_function():
|
||||
res = notify(
|
||||
conversation_id="conv-xyz",
|
||||
@@ -129,3 +180,52 @@ def test_global_notify_function():
|
||||
payload={"trace_id": "test-trace"},
|
||||
)
|
||||
assert isinstance(res, bool)
|
||||
|
||||
|
||||
def test_agentapi_notifier_candidate_recovery_and_retry(monkeypatch, tmp_path):
|
||||
from mcp_server.notifiers.agentapi import AgentApiNotifier
|
||||
import subprocess
|
||||
|
||||
monkeypatch.delenv("ANTIGRAVITY_LS_ADDRESS", raising=False)
|
||||
monkeypatch.delenv("ANTIGRAVITY_CSRF_TOKEN", raising=False)
|
||||
|
||||
shared_file = tmp_path / ".test_jetski_env"
|
||||
monkeypatch.setattr(
|
||||
AgentApiNotifier,
|
||||
"_get_candidate_envs",
|
||||
lambda self, force_proc_scan=False: [
|
||||
("localhost:9999", "bad-token"),
|
||||
("localhost:1234", "good-token"),
|
||||
],
|
||||
)
|
||||
|
||||
saved_sessions = []
|
||||
monkeypatch.setattr(
|
||||
AgentApiNotifier,
|
||||
"_save_shared_env",
|
||||
lambda self, addr, token: saved_sessions.append((addr, token)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
AgentApiNotifier,
|
||||
"_find_agentapi_path",
|
||||
lambda self: "/mock/bin/agentapi",
|
||||
)
|
||||
|
||||
calls = []
|
||||
|
||||
def mock_run(cmd, capture_output, text, check, timeout, env):
|
||||
calls.append(env.get("ANTIGRAVITY_LS_ADDRESS"))
|
||||
if env.get("ANTIGRAVITY_LS_ADDRESS") == "localhost:9999":
|
||||
raise subprocess.CalledProcessError(1, cmd)
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="success")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", mock_run)
|
||||
|
||||
notifier = AgentApiNotifier()
|
||||
success = notifier.notify("conv-test-123", "Task done!")
|
||||
assert success is True
|
||||
assert calls == ["localhost:9999", "localhost:1234"]
|
||||
assert saved_sessions == [("localhost:1234", "good-token")]
|
||||
assert os.environ["ANTIGRAVITY_LS_ADDRESS"] == "localhost:1234"
|
||||
assert os.environ["ANTIGRAVITY_CSRF_TOKEN"] == "good-token"
|
||||
|
||||
Reference in new issue
Block a user