mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-16 14:32:34 +00:00
323 lines
11 KiB
Python
323 lines
11 KiB
Python
import asyncio
|
|
import time
|
|
|
|
|
|
class _FakeProc:
|
|
def __init__(self, lines=None, returncode=0):
|
|
self.stdout = iter(lines or [])
|
|
self._returncode = returncode
|
|
self.terminated = False
|
|
self.killed = False
|
|
self.pid = 12345
|
|
|
|
def poll(self):
|
|
return None if not self.terminated and not self.killed else self._returncode
|
|
|
|
def wait(self, timeout=None):
|
|
return self._returncode
|
|
|
|
def terminate(self):
|
|
self.terminated = True
|
|
|
|
def kill(self):
|
|
self.killed = True
|
|
|
|
|
|
def test_whatsapp_pairing_watcher_records_qr_and_connected():
|
|
from hermes_cli import web_server as ws
|
|
|
|
proc = _FakeProc([
|
|
'{"event":"started","session":"/tmp/session"}\n',
|
|
'{"event":"qr","qr":"qr-payload"}\n',
|
|
'{"event":"connected","user":{"id":"15551234567:1@s.whatsapp.net","name":"Hermes Bot"}}\n',
|
|
])
|
|
record = ws._WhatsAppOnboardingSession(
|
|
proc=proc,
|
|
mode="bot",
|
|
allowed_users="",
|
|
session_path="/tmp/session",
|
|
expires_at="2099-01-01T00:00:00Z",
|
|
expires_at_ts=time.time() + 600,
|
|
)
|
|
ws._whatsapp_onboarding_sessions.clear()
|
|
ws._whatsapp_onboarding_sessions["pairing"] = record
|
|
|
|
ws._watch_whatsapp_pairing("pairing", proc)
|
|
|
|
assert record.status == "connected"
|
|
assert record.qr_payload == "qr-payload"
|
|
assert record.account_id == "15551234567:1@s.whatsapp.net"
|
|
assert record.account_name == "Hermes Bot"
|
|
assert record.account_phone == "15551234567"
|
|
assert record.error is None
|
|
ws._whatsapp_onboarding_sessions.clear()
|
|
|
|
|
|
def test_whatsapp_pairing_payload_includes_linked_account():
|
|
from hermes_cli import web_server as ws
|
|
|
|
record = ws._WhatsAppOnboardingSession(
|
|
proc=None,
|
|
mode="bot",
|
|
allowed_users="",
|
|
session_path="/tmp/session",
|
|
expires_at="2099-01-01T00:00:00Z",
|
|
expires_at_ts=time.time() + 600,
|
|
status="connected",
|
|
account_id="15551234567@s.whatsapp.net",
|
|
account_name="Hermes Bot",
|
|
account_phone="15551234567",
|
|
)
|
|
|
|
payload = ws._whatsapp_onboarding_payload("pairing", record)
|
|
|
|
assert payload["account_id"] == "15551234567@s.whatsapp.net"
|
|
assert payload["account_name"] == "Hermes Bot"
|
|
assert payload["account_phone"] == "15551234567"
|
|
|
|
|
|
def test_messaging_payload_includes_safe_whatsapp_setup(monkeypatch):
|
|
from hermes_cli import web_server as ws
|
|
|
|
entry = {
|
|
"id": "whatsapp",
|
|
"name": "WhatsApp",
|
|
"description": "WhatsApp bridge",
|
|
"docs_url": "",
|
|
"env_vars": ("WHATSAPP_MODE", "WHATSAPP_ALLOWED_USERS", "WHATSAPP_ENABLED"),
|
|
"required_env": (),
|
|
}
|
|
monkeypatch.setattr(ws, "get_running_pid", lambda: None)
|
|
monkeypatch.setattr(ws, "get_runtime_status_running_pid", lambda runtime: None)
|
|
monkeypatch.setattr(
|
|
ws,
|
|
"load_config",
|
|
lambda: {
|
|
"platforms": {
|
|
"whatsapp": {
|
|
"enabled": True,
|
|
"home_channel": {
|
|
"platform": "whatsapp",
|
|
"chat_id": "280912570925281@lid",
|
|
"name": "Home",
|
|
},
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
payload = ws._messaging_platform_payload(
|
|
entry,
|
|
{
|
|
"WHATSAPP_MODE": "self-chat",
|
|
"WHATSAPP_ALLOWED_USERS": "61405484224",
|
|
"WHATSAPP_ENABLED": "true",
|
|
},
|
|
runtime=None,
|
|
scoped=True,
|
|
)
|
|
|
|
assert payload["whatsapp_setup"] == {
|
|
"mode": "self-chat",
|
|
"allowed_users_set": True,
|
|
"home_channel_set": True,
|
|
}
|
|
assert "61405484224" not in str(payload["whatsapp_setup"])
|
|
|
|
|
|
def test_apply_whatsapp_onboarding_saves_pairing_policy(monkeypatch):
|
|
from hermes_cli import web_server as ws
|
|
|
|
saved = {}
|
|
removed = []
|
|
enabled = []
|
|
|
|
monkeypatch.setattr(ws, "save_env_value", lambda key, value: saved.setdefault(key, value))
|
|
monkeypatch.setattr(ws, "remove_env_value", lambda key: removed.append(key))
|
|
monkeypatch.setattr(ws, "_write_platform_enabled", lambda platform, value: enabled.append((platform, value)))
|
|
monkeypatch.setattr(
|
|
ws,
|
|
"_restart_gateway_after_whatsapp_onboarding",
|
|
lambda profile=None: {"restart_started": True, "restart_pid": 12345},
|
|
)
|
|
|
|
record = ws._WhatsAppOnboardingSession(
|
|
proc=None,
|
|
mode="bot",
|
|
allowed_users="",
|
|
session_path="/tmp/session",
|
|
expires_at="2099-01-01T00:00:00Z",
|
|
expires_at_ts=time.time() + 600,
|
|
status="connected",
|
|
)
|
|
ws._whatsapp_onboarding_sessions.clear()
|
|
ws._whatsapp_onboarding_sessions["pairing"] = record
|
|
|
|
result = asyncio.run(
|
|
ws.apply_whatsapp_onboarding(
|
|
"pairing",
|
|
ws.WhatsAppOnboardingApply(mode="bot", allowed_users=""),
|
|
)
|
|
)
|
|
|
|
assert result["ok"] is True
|
|
assert saved["WHATSAPP_MODE"] == "bot"
|
|
assert saved["WHATSAPP_DM_POLICY"] == "pairing"
|
|
assert saved["WHATSAPP_ENABLED"] == "true"
|
|
assert "WHATSAPP_ALLOWED_USERS" not in removed
|
|
assert enabled == [("whatsapp", True)]
|
|
assert "pairing" not in ws._whatsapp_onboarding_sessions
|
|
|
|
|
|
def test_apply_whatsapp_onboarding_self_chat_defaults_to_linked_account(monkeypatch):
|
|
from hermes_cli import web_server as ws
|
|
|
|
saved = {}
|
|
removed = []
|
|
|
|
monkeypatch.setattr(ws, "save_env_value", lambda key, value: saved.setdefault(key, value))
|
|
monkeypatch.setattr(ws, "remove_env_value", lambda key: removed.append(key))
|
|
monkeypatch.setattr(ws, "_write_platform_enabled", lambda platform, value: None)
|
|
monkeypatch.setattr(
|
|
ws,
|
|
"_restart_gateway_after_whatsapp_onboarding",
|
|
lambda profile=None: {"restart_started": True, "restart_pid": 12345},
|
|
)
|
|
|
|
record = ws._WhatsAppOnboardingSession(
|
|
proc=None,
|
|
mode="self-chat",
|
|
allowed_users="",
|
|
session_path="/tmp/session",
|
|
expires_at="2099-01-01T00:00:00Z",
|
|
expires_at_ts=time.time() + 600,
|
|
status="connected",
|
|
account_id="15551234567:1@s.whatsapp.net",
|
|
account_phone="15551234567",
|
|
)
|
|
ws._whatsapp_onboarding_sessions.clear()
|
|
ws._whatsapp_onboarding_sessions["pairing"] = record
|
|
|
|
result = asyncio.run(
|
|
ws.apply_whatsapp_onboarding(
|
|
"pairing",
|
|
ws.WhatsAppOnboardingApply(mode="self-chat", allowed_users=""),
|
|
)
|
|
)
|
|
|
|
assert result["ok"] is True
|
|
assert saved["WHATSAPP_MODE"] == "self-chat"
|
|
assert saved["WHATSAPP_ALLOWED_USERS"] == "15551234567"
|
|
assert "WHATSAPP_ALLOWED_USERS" not in removed
|
|
assert "pairing" not in ws._whatsapp_onboarding_sessions
|
|
|
|
|
|
def test_start_whatsapp_onboarding_existing_creds_returns_linked_account(monkeypatch, tmp_path):
|
|
from hermes_cli import web_server as ws
|
|
|
|
session_dir = tmp_path / "session"
|
|
session_dir.mkdir()
|
|
(session_dir / "creds.json").write_text(
|
|
'{"me":{"id":"15551234567:1@s.whatsapp.net","name":"Hermes Bot"}}',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
old_proc = _FakeProc(returncode=1)
|
|
old_record = ws._WhatsAppOnboardingSession(
|
|
proc=old_proc,
|
|
mode="bot",
|
|
allowed_users="",
|
|
session_path=str(session_dir),
|
|
expires_at="2099-01-01T00:00:00Z",
|
|
expires_at_ts=time.time() + 600,
|
|
)
|
|
ws._whatsapp_onboarding_sessions.clear()
|
|
ws._whatsapp_onboarding_sessions["old"] = old_record
|
|
monkeypatch.setattr(ws, "_whatsapp_session_path", lambda: session_dir)
|
|
monkeypatch.setattr(ws.secrets, "token_urlsafe", lambda size: "existing-creds")
|
|
|
|
result = asyncio.run(
|
|
ws.start_whatsapp_onboarding(
|
|
ws.WhatsAppOnboardingStart(mode="self-chat", allowed_users="")
|
|
)
|
|
)
|
|
|
|
assert result["pairing_id"] == "existing-creds"
|
|
assert result["status"] == "connected"
|
|
assert result["qr_payload"] is None
|
|
assert result["account_id"] == "15551234567:1@s.whatsapp.net"
|
|
assert result["account_name"] == "Hermes Bot"
|
|
assert result["account_phone"] == "15551234567"
|
|
assert old_record.status == "cancelled"
|
|
assert old_proc.terminated is True
|
|
assert ws._whatsapp_onboarding_sessions["existing-creds"].account_phone == "15551234567"
|
|
ws._whatsapp_onboarding_sessions.clear()
|
|
|
|
|
|
def test_start_whatsapp_onboarding_returns_before_bridge_spawn(monkeypatch, tmp_path):
|
|
from hermes_cli import web_server as ws
|
|
|
|
captured = {}
|
|
|
|
class FakeThread:
|
|
def __init__(self, *, target, args, daemon):
|
|
captured["target"] = target
|
|
captured["args"] = args
|
|
captured["daemon"] = daemon
|
|
|
|
def start(self):
|
|
captured["started"] = True
|
|
|
|
ws._whatsapp_onboarding_sessions.clear()
|
|
monkeypatch.setattr(ws, "_whatsapp_session_path", lambda: tmp_path / "session")
|
|
monkeypatch.setattr(ws.secrets, "token_urlsafe", lambda size: "pairing-start")
|
|
monkeypatch.setattr(ws.threading, "Thread", FakeThread)
|
|
|
|
result = asyncio.run(
|
|
ws.start_whatsapp_onboarding(
|
|
ws.WhatsAppOnboardingStart(mode="bot", allowed_users="")
|
|
)
|
|
)
|
|
|
|
assert result["pairing_id"] == "pairing-start"
|
|
assert result["status"] == "starting"
|
|
assert result["qr_payload"] is None
|
|
assert captured["target"] is ws._run_whatsapp_pairing
|
|
assert captured["args"] == ("pairing-start", tmp_path / "session", "bot")
|
|
assert captured["daemon"] is True
|
|
assert captured["started"] is True
|
|
assert ws._whatsapp_onboarding_sessions["pairing-start"].proc is None
|
|
ws._whatsapp_onboarding_sessions.clear()
|
|
|
|
|
|
def test_spawn_whatsapp_pairing_process_uses_json_mode(monkeypatch, tmp_path):
|
|
from gateway.platforms import whatsapp_common
|
|
from hermes_cli import web_server as ws
|
|
import hermes_constants
|
|
|
|
bridge_dir = tmp_path / "bridge"
|
|
bridge_dir.mkdir()
|
|
(bridge_dir / "bridge.js").write_text("// bridge", encoding="utf-8")
|
|
session_dir = tmp_path / "session"
|
|
captured = {}
|
|
|
|
monkeypatch.setattr(whatsapp_common, "resolve_whatsapp_bridge_dir", lambda: bridge_dir)
|
|
monkeypatch.setattr(hermes_constants, "find_node_executable", lambda command: "/usr/bin/node")
|
|
monkeypatch.setattr(hermes_constants, "with_hermes_node_path", lambda env=None: {})
|
|
monkeypatch.setattr(ws, "_ensure_whatsapp_bridge_dependencies", lambda bridge_dir: None)
|
|
|
|
def fake_popen(args, **kwargs):
|
|
captured["args"] = args
|
|
captured["kwargs"] = kwargs
|
|
return _FakeProc()
|
|
|
|
monkeypatch.setattr(ws.subprocess, "Popen", fake_popen)
|
|
|
|
proc = ws._spawn_whatsapp_pairing_process(session_dir, "bot")
|
|
|
|
assert isinstance(proc, _FakeProc)
|
|
assert "--pair-only" in captured["args"]
|
|
assert "--pair-json" in captured["args"]
|
|
assert str(session_dir) in captured["args"]
|
|
assert captured["kwargs"]["env"]["WHATSAPP_MODE"] == "bot"
|
|
assert captured["kwargs"]["env"]["WHATSAPP_DM_POLICY"] == "pairing"
|