mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: add tests and update mds
This commit is contained in:
parent
f226e6be10
commit
9d8f9765c1
11 changed files with 6013 additions and 4 deletions
0
tests/tui_gateway/__init__.py
Normal file
0
tests/tui_gateway/__init__.py
Normal file
187
tests/tui_gateway/test_protocol.py
Normal file
187
tests/tui_gateway/test_protocol.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
"""Tests for tui_gateway JSON-RPC protocol plumbing."""
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
_original_stdout = sys.stdout
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _restore_stdout():
|
||||
yield
|
||||
sys.stdout = _original_stdout
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def server():
|
||||
with patch.dict("sys.modules", {
|
||||
"hermes_constants": MagicMock(get_hermes_home=MagicMock(return_value="/tmp/hermes_test")),
|
||||
"hermes_cli.env_loader": MagicMock(),
|
||||
"hermes_cli.banner": MagicMock(),
|
||||
"hermes_state": MagicMock(),
|
||||
}):
|
||||
import importlib
|
||||
mod = importlib.import_module("tui_gateway.server")
|
||||
yield mod
|
||||
mod._sessions.clear()
|
||||
mod._pending.clear()
|
||||
mod._answers.clear()
|
||||
mod._methods.clear()
|
||||
importlib.reload(mod)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def capture(server):
|
||||
"""Redirect server's real stdout to a StringIO and return (server, buf)."""
|
||||
buf = io.StringIO()
|
||||
server._real_stdout = buf
|
||||
return server, buf
|
||||
|
||||
|
||||
# ── JSON-RPC envelope ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_unknown_method(server):
|
||||
resp = server.handle_request({"id": "1", "method": "bogus"})
|
||||
assert resp["error"]["code"] == -32601
|
||||
|
||||
|
||||
def test_ok_envelope(server):
|
||||
assert server._ok("r1", {"x": 1}) == {
|
||||
"jsonrpc": "2.0", "id": "r1", "result": {"x": 1},
|
||||
}
|
||||
|
||||
|
||||
def test_err_envelope(server):
|
||||
assert server._err("r2", 4001, "nope") == {
|
||||
"jsonrpc": "2.0", "id": "r2", "error": {"code": 4001, "message": "nope"},
|
||||
}
|
||||
|
||||
|
||||
# ── write_json ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_write_json(capture):
|
||||
server, buf = capture
|
||||
assert server.write_json({"test": True})
|
||||
assert json.loads(buf.getvalue()) == {"test": True}
|
||||
|
||||
|
||||
def test_write_json_broken_pipe(server):
|
||||
class _Broken:
|
||||
def write(self, _): raise BrokenPipeError
|
||||
def flush(self): raise BrokenPipeError
|
||||
|
||||
server._real_stdout = _Broken()
|
||||
assert server.write_json({"x": 1}) is False
|
||||
|
||||
|
||||
# ── _emit ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_emit_with_payload(capture):
|
||||
server, buf = capture
|
||||
server._emit("test.event", "s1", {"key": "val"})
|
||||
msg = json.loads(buf.getvalue())
|
||||
|
||||
assert msg["method"] == "event"
|
||||
assert msg["params"]["type"] == "test.event"
|
||||
assert msg["params"]["session_id"] == "s1"
|
||||
assert msg["params"]["payload"]["key"] == "val"
|
||||
|
||||
|
||||
def test_emit_without_payload(capture):
|
||||
server, buf = capture
|
||||
server._emit("ping", "s2")
|
||||
|
||||
assert "payload" not in json.loads(buf.getvalue())["params"]
|
||||
|
||||
|
||||
# ── Blocking prompt round-trip ───────────────────────────────────────
|
||||
|
||||
|
||||
def test_block_and_respond(capture):
|
||||
server, _ = capture
|
||||
result = [None]
|
||||
|
||||
threading.Thread(
|
||||
target=lambda: result.__setitem__(0, server._block("test.prompt", "s1", {"q": "?"}, timeout=5)),
|
||||
).start()
|
||||
|
||||
for _ in range(100):
|
||||
if server._pending:
|
||||
break
|
||||
threading.Event().wait(0.01)
|
||||
|
||||
rid = next(iter(server._pending))
|
||||
server._answers[rid] = "my_answer"
|
||||
server._pending[rid].set()
|
||||
|
||||
threading.Event().wait(0.1)
|
||||
assert result[0] == "my_answer"
|
||||
|
||||
|
||||
def test_clear_pending(server):
|
||||
ev = threading.Event()
|
||||
server._pending["r1"] = ev
|
||||
server._clear_pending()
|
||||
|
||||
assert ev.is_set()
|
||||
assert server._answers["r1"] == ""
|
||||
|
||||
|
||||
# ── Session lookup ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_sess_missing(server):
|
||||
_, err = server._sess({"session_id": "nope"}, "r1")
|
||||
assert err["error"]["code"] == 4001
|
||||
|
||||
|
||||
def test_sess_found(server):
|
||||
server._sessions["abc"] = {"agent": MagicMock()}
|
||||
s, err = server._sess({"session_id": "abc"}, "r1")
|
||||
|
||||
assert s is not None
|
||||
assert err is None
|
||||
|
||||
|
||||
# ── Config I/O ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_config_load_missing(server, tmp_path):
|
||||
server._hermes_home = tmp_path
|
||||
assert server._load_cfg() == {}
|
||||
|
||||
|
||||
def test_config_roundtrip(server, tmp_path):
|
||||
server._hermes_home = tmp_path
|
||||
server._save_cfg({"model": "test/model"})
|
||||
assert server._load_cfg()["model"] == "test/model"
|
||||
|
||||
|
||||
# ── _cli_exec_blocked ────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("argv", [
|
||||
[],
|
||||
["setup"],
|
||||
["gateway"],
|
||||
["sessions", "browse"],
|
||||
["config", "edit"],
|
||||
])
|
||||
def test_cli_exec_blocked(server, argv):
|
||||
assert server._cli_exec_blocked(argv) is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("argv", [
|
||||
["version"],
|
||||
["sessions", "list"],
|
||||
])
|
||||
def test_cli_exec_allowed(server, argv):
|
||||
assert server._cli_exec_blocked(argv) is None
|
||||
67
tests/tui_gateway/test_render.py
Normal file
67
tests/tui_gateway/test_render.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""Tests for tui_gateway.render — rendering bridge fallback behavior."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from tui_gateway.render import make_stream_renderer, render_diff, render_message
|
||||
|
||||
|
||||
def _stub_rich(mock_mod):
|
||||
return patch.dict("sys.modules", {"agent.rich_output": mock_mod})
|
||||
|
||||
|
||||
def _no_rich():
|
||||
return patch.dict("sys.modules", {"agent.rich_output": None})
|
||||
|
||||
|
||||
# ── render_message ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_render_message_none_without_module():
|
||||
with _no_rich():
|
||||
assert render_message("hello") is None
|
||||
|
||||
|
||||
def test_render_message_formatted():
|
||||
mod = MagicMock()
|
||||
mod.format_response.return_value = "<b>hi</b>"
|
||||
|
||||
with _stub_rich(mod):
|
||||
assert render_message("hi", 100) == "<b>hi</b>"
|
||||
|
||||
|
||||
def test_render_message_type_error_fallback():
|
||||
mod = MagicMock()
|
||||
mod.format_response.side_effect = [TypeError, "fallback"]
|
||||
|
||||
with _stub_rich(mod):
|
||||
assert render_message("hi") == "fallback"
|
||||
|
||||
|
||||
def test_render_message_exception_returns_none():
|
||||
mod = MagicMock()
|
||||
mod.format_response.side_effect = RuntimeError
|
||||
|
||||
with _stub_rich(mod):
|
||||
assert render_message("hi") is None
|
||||
|
||||
|
||||
# ── render_diff / make_stream_renderer ───────────────────────────────
|
||||
|
||||
|
||||
def test_render_diff_none_without_module():
|
||||
with _no_rich():
|
||||
assert render_diff("+line") is None
|
||||
|
||||
|
||||
def test_stream_renderer_none_without_module():
|
||||
with _no_rich():
|
||||
assert make_stream_renderer() is None
|
||||
|
||||
|
||||
def test_stream_renderer_returns_instance():
|
||||
renderer = MagicMock()
|
||||
mod = MagicMock()
|
||||
mod.StreamingRenderer.return_value = renderer
|
||||
|
||||
with _stub_rich(mod):
|
||||
assert make_stream_renderer(120) is renderer
|
||||
Loading…
Add table
Add a link
Reference in a new issue