mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
Some checks are pending
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Docker Build and Publish / move-latest (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Nix / nix (macos-latest) (push) Waiting to run
Nix / nix (ubuntu-latest) (push) Waiting to run
OSV-Scanner / Scan lockfiles (push) Waiting to run
Tests / test (push) Waiting to run
Tests / e2e (push) Waiting to run
uv.lock check / uv lock --check (push) Waiting to run
199 lines
5.8 KiB
Python
199 lines
5.8 KiB
Python
import asyncio
|
|
import pytest
|
|
|
|
from pathlib import Path
|
|
from hermes_cli import kanban_db as kb
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.fixture
|
|
def kanban_home(tmp_path, monkeypatch):
|
|
home = tmp_path / ".hermes"
|
|
home.mkdir()
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
kb.init_db()
|
|
return home
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notifier_unsubs_after_completed_event(kanban_home):
|
|
"""
|
|
Subscription should be remove after completed event
|
|
"""
|
|
import hermes_cli.kanban_db as kb
|
|
from gateway.run import GatewayRunner
|
|
from gateway.config import Platform
|
|
|
|
conn = kb.connect()
|
|
try:
|
|
tid = kb.create_task(conn, title="test task", assignee="worker1")
|
|
kb.add_notify_sub(conn, task_id=tid, platform="telegram", chat_id="chat1")
|
|
kb.complete_task(conn, tid, result="completed by agent")
|
|
finally:
|
|
conn.close()
|
|
|
|
runner = object.__new__(GatewayRunner)
|
|
runner._running = True
|
|
runner._kanban_sub_fail_counts = {}
|
|
|
|
fake_adapter = MagicMock()
|
|
|
|
async def _send_and_stop(chat_id, msg, metadata=None):
|
|
runner._running = False
|
|
|
|
fake_adapter.send = AsyncMock(side_effect=_send_and_stop)
|
|
runner.adapters = {Platform.TELEGRAM: fake_adapter}
|
|
|
|
_orig_sleep = asyncio.sleep
|
|
|
|
async def _fast_sleep(_):
|
|
await _orig_sleep(0)
|
|
|
|
with patch("gateway.run.asyncio.sleep", side_effect=_fast_sleep):
|
|
await asyncio.wait_for(
|
|
runner._kanban_notifier_watcher(interval=1),
|
|
timeout=10.0,
|
|
)
|
|
|
|
fake_adapter.send.assert_called_once()
|
|
call_msg = fake_adapter.send.call_args[0][1]
|
|
assert "completed" in call_msg
|
|
|
|
conn = kb.connect()
|
|
try:
|
|
subs = kb.list_notify_subs(conn, tid)
|
|
finally:
|
|
conn.close()
|
|
assert subs == [], "Subscription should be unsub after completed event"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('kind', ["gave_up", "crashed", "timed_out"])
|
|
async def test_notifier_unsubs_after_abnormal_events(kind, kanban_home):
|
|
"""
|
|
Event kind of gave_up, crashed, time_out would be cover, and remove subscription
|
|
"""
|
|
import hermes_cli.kanban_db as kb
|
|
from gateway.run import GatewayRunner
|
|
from gateway.config import Platform
|
|
|
|
conn = kb.connect()
|
|
|
|
try:
|
|
tid = kb.create_task(conn, title=f"test {kind} task", assignee="worker1")
|
|
kb.add_notify_sub(conn, task_id=tid, platform="telegram", chat_id="chat1")
|
|
kb._append_event(conn, tid, kind=kind)
|
|
finally:
|
|
conn.close()
|
|
|
|
runner = object.__new__(GatewayRunner)
|
|
runner._running = True
|
|
runner._kanban_sub_fail_counts = {}
|
|
|
|
fake_adapter = MagicMock()
|
|
|
|
async def _send_and_stop(chat_id, msg, metadata=None):
|
|
runner._running = False
|
|
|
|
fake_adapter.send = AsyncMock(side_effect=_send_and_stop)
|
|
runner.adapters = {Platform.TELEGRAM: fake_adapter}
|
|
|
|
_orig_sleep = asyncio.sleep
|
|
|
|
async def _fast_sleep(_):
|
|
await _orig_sleep(0)
|
|
|
|
with patch("gateway.run.asyncio.sleep", side_effect=_fast_sleep):
|
|
await asyncio.wait_for(
|
|
runner._kanban_notifier_watcher(interval=1),
|
|
timeout=10.0,
|
|
)
|
|
|
|
fake_adapter.send.assert_called_once()
|
|
assert kind.replace('_', ' ') in fake_adapter.send.call_args[0][1]
|
|
|
|
conn = kb.connect()
|
|
try:
|
|
subs = kb.list_notify_subs(conn, tid)
|
|
finally:
|
|
conn.close()
|
|
assert subs == [], "Subscription should be unsub after abnormal crash"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notifier_second_blocked_delivers(kanban_home):
|
|
"""
|
|
After the first blocked, should receive second blocked notification.
|
|
"""
|
|
import hermes_cli.kanban_db as kb
|
|
from gateway.run import GatewayRunner
|
|
from gateway.config import Platform
|
|
|
|
runner = object.__new__(GatewayRunner)
|
|
runner._running = True
|
|
runner._kanban_sub_fail_counts = {}
|
|
|
|
delivered_msgs: list[str] = []
|
|
|
|
async def _capture_send(chat_id, msg, metadata=None):
|
|
delivered_msgs.append(msg)
|
|
|
|
fake_adapter = MagicMock()
|
|
fake_adapter.send = AsyncMock(side_effect=_capture_send)
|
|
runner.adapters = {Platform.TELEGRAM: fake_adapter}
|
|
|
|
_orig_sleep = asyncio.sleep
|
|
tick_count = 0
|
|
|
|
async def _fast_sleep(_):
|
|
nonlocal tick_count
|
|
await _orig_sleep(0)
|
|
tick_count += 1
|
|
if tick_count >= 6:
|
|
runner._running = False
|
|
|
|
conn = kb.connect()
|
|
try:
|
|
tid = kb.create_task(conn, title="test task", assignee="worker1")
|
|
kb.add_notify_sub(conn, task_id=tid, platform="telegram", chat_id="chat1")
|
|
|
|
# Cycle 1: blocked
|
|
kb.block_task(conn, tid, reason="first block")
|
|
finally:
|
|
conn.close()
|
|
|
|
with patch("gateway.run.asyncio.sleep", side_effect=_fast_sleep):
|
|
await asyncio.wait_for(
|
|
runner._kanban_notifier_watcher(interval=1),
|
|
timeout=10.0,
|
|
)
|
|
|
|
# Cycle 2: unblock → block run again
|
|
runner._running = True
|
|
tick_count = 0
|
|
|
|
conn = kb.connect()
|
|
try:
|
|
kb.unblock_task(conn, tid)
|
|
kb.block_task(conn, tid, reason="second block")
|
|
finally:
|
|
conn.close()
|
|
|
|
with patch("gateway.run.asyncio.sleep", side_effect=_fast_sleep):
|
|
await asyncio.wait_for(
|
|
runner._kanban_notifier_watcher(interval=1),
|
|
timeout=10.0,
|
|
)
|
|
|
|
blocked_deliveries = [m for m in delivered_msgs if "blocked" in m]
|
|
assert "second block" not in blocked_deliveries[0]
|
|
assert "second block" in blocked_deliveries[1]
|
|
assert len(blocked_deliveries) == 2, (
|
|
f"Should receive 2 blocked notification, but only get {len(blocked_deliveries)} count\n"
|
|
f"Message {delivered_msgs}"
|
|
)
|