mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
142 lines
5.6 KiB
Python
142 lines
5.6 KiB
Python
"""Tests for feishu_comment — event filtering, access control integration, wiki reverse lookup."""
|
|
|
|
import asyncio
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
from plugins.platforms.feishu.feishu_comment import (
|
|
parse_drive_comment_event,
|
|
_ALLOWED_NOTICE_TYPES,
|
|
_sanitize_comment_text,
|
|
)
|
|
|
|
|
|
def _make_event(
|
|
comment_id="c1",
|
|
reply_id="r1",
|
|
notice_type="add_reply",
|
|
file_token="docx_token",
|
|
file_type="docx",
|
|
from_open_id="ou_user",
|
|
to_open_id="ou_bot",
|
|
is_mentioned=True,
|
|
):
|
|
"""Build a minimal drive comment event SimpleNamespace."""
|
|
return SimpleNamespace(event={
|
|
"event_id": "evt_1",
|
|
"comment_id": comment_id,
|
|
"reply_id": reply_id,
|
|
"is_mentioned": is_mentioned,
|
|
"timestamp": "1713200000",
|
|
"notice_meta": {
|
|
"file_token": file_token,
|
|
"file_type": file_type,
|
|
"notice_type": notice_type,
|
|
"from_user_id": {"open_id": from_open_id},
|
|
"to_user_id": {"open_id": to_open_id},
|
|
},
|
|
})
|
|
|
|
|
|
class TestParseEvent(unittest.TestCase):
|
|
def test_parse_valid_event(self):
|
|
evt = _make_event()
|
|
parsed = parse_drive_comment_event(evt)
|
|
self.assertIsNotNone(parsed)
|
|
self.assertEqual(parsed["comment_id"], "c1")
|
|
self.assertEqual(parsed["file_type"], "docx")
|
|
self.assertEqual(parsed["from_open_id"], "ou_user")
|
|
self.assertEqual(parsed["to_open_id"], "ou_bot")
|
|
|
|
|
|
class TestEventFiltering(unittest.TestCase):
|
|
"""Test the filtering logic in handle_drive_comment_event."""
|
|
|
|
def _run(self, coro):
|
|
return asyncio.get_event_loop().run_until_complete(coro)
|
|
|
|
@patch("plugins.platforms.feishu.feishu_comment_rules.load_config")
|
|
@patch("plugins.platforms.feishu.feishu_comment_rules.resolve_rule")
|
|
@patch("plugins.platforms.feishu.feishu_comment_rules.is_user_allowed")
|
|
def test_self_reply_filtered(self, mock_allowed, mock_resolve, mock_load):
|
|
"""Events where from_open_id == self_open_id should be dropped."""
|
|
from plugins.platforms.feishu.feishu_comment import handle_drive_comment_event
|
|
|
|
evt = _make_event(from_open_id="ou_bot", to_open_id="ou_bot")
|
|
self._run(handle_drive_comment_event(Mock(), evt, self_open_id="ou_bot"))
|
|
mock_load.assert_not_called()
|
|
|
|
@patch("plugins.platforms.feishu.feishu_comment_rules.load_config")
|
|
@patch("plugins.platforms.feishu.feishu_comment_rules.resolve_rule")
|
|
@patch("plugins.platforms.feishu.feishu_comment_rules.is_user_allowed")
|
|
def test_wrong_receiver_filtered(self, mock_allowed, mock_resolve, mock_load):
|
|
"""Events where to_open_id != self_open_id should be dropped."""
|
|
from plugins.platforms.feishu.feishu_comment import handle_drive_comment_event
|
|
|
|
evt = _make_event(to_open_id="ou_other_bot")
|
|
self._run(handle_drive_comment_event(Mock(), evt, self_open_id="ou_bot"))
|
|
mock_load.assert_not_called()
|
|
|
|
|
|
class TestAccessControlIntegration(unittest.TestCase):
|
|
def _run(self, coro):
|
|
return asyncio.get_event_loop().run_until_complete(coro)
|
|
|
|
@patch("plugins.platforms.feishu.feishu_comment_rules.has_wiki_keys", return_value=False)
|
|
@patch("plugins.platforms.feishu.feishu_comment_rules.is_user_allowed", return_value=False)
|
|
@patch("plugins.platforms.feishu.feishu_comment_rules.resolve_rule")
|
|
@patch("plugins.platforms.feishu.feishu_comment_rules.load_config")
|
|
def test_denied_user_no_side_effects(self, mock_load, mock_resolve, mock_allowed, mock_wiki_keys):
|
|
"""Denied user should not trigger typing reaction or agent."""
|
|
from plugins.platforms.feishu.feishu_comment import handle_drive_comment_event
|
|
from plugins.platforms.feishu.feishu_comment_rules import ResolvedCommentRule
|
|
|
|
mock_resolve.return_value = ResolvedCommentRule(True, "allowlist", frozenset(), "top")
|
|
mock_load.return_value = Mock()
|
|
|
|
client = Mock()
|
|
evt = _make_event()
|
|
self._run(handle_drive_comment_event(client, evt, self_open_id="ou_bot"))
|
|
|
|
# No API calls should be made for denied users
|
|
client.request.assert_not_called()
|
|
|
|
|
|
class TestSanitizeCommentText(unittest.TestCase):
|
|
def test_angle_brackets_escaped(self):
|
|
self.assertEqual(_sanitize_comment_text("List<String>"), "List<String>")
|
|
|
|
def test_ampersand_escaped_first(self):
|
|
self.assertEqual(_sanitize_comment_text("a & b"), "a & b")
|
|
|
|
def test_ampersand_not_double_escaped(self):
|
|
result = _sanitize_comment_text("a < b & c > d")
|
|
self.assertEqual(result, "a < b & c > d")
|
|
self.assertNotIn("&lt;", result)
|
|
self.assertNotIn("&gt;", result)
|
|
|
|
|
|
class TestWikiReverseLookup(unittest.TestCase):
|
|
def _run(self, coro):
|
|
return asyncio.get_event_loop().run_until_complete(coro)
|
|
|
|
@patch("plugins.platforms.feishu.feishu_comment._exec_request")
|
|
def test_reverse_lookup_success(self, mock_exec):
|
|
from plugins.platforms.feishu.feishu_comment import _reverse_lookup_wiki_token
|
|
|
|
mock_exec.return_value = (0, "Success", {
|
|
"node": {"node_token": "WIKI_TOKEN_123", "obj_token": "docx_abc"},
|
|
})
|
|
result = self._run(_reverse_lookup_wiki_token(Mock(), "docx", "docx_abc"))
|
|
self.assertEqual(result, "WIKI_TOKEN_123")
|
|
# Verify correct API params
|
|
call_args = mock_exec.call_args
|
|
queries = call_args[1].get("queries") or call_args[0][3]
|
|
query_dict = dict(queries)
|
|
self.assertEqual(query_dict["token"], "docx_abc")
|
|
self.assertEqual(query_dict["obj_type"], "docx")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|