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.
156 lines
5.5 KiB
Python
156 lines
5.5 KiB
Python
"""Tests for the todo tool module."""
|
|
|
|
import json
|
|
|
|
from tools.todo_tool import TodoStore, todo_tool
|
|
|
|
|
|
class TestWriteAndRead:
|
|
def test_write_replaces_list(self):
|
|
store = TodoStore()
|
|
items = [
|
|
{"id": "1", "content": "First task", "status": "pending"},
|
|
{"id": "2", "content": "Second task", "status": "in_progress"},
|
|
]
|
|
result = store.write(items)
|
|
assert len(result) == 2
|
|
assert result[0]["id"] == "1"
|
|
assert result[1]["status"] == "in_progress"
|
|
|
|
|
|
def test_write_deduplicates_duplicate_ids(self):
|
|
store = TodoStore()
|
|
result = store.write([
|
|
{"id": "1", "content": "First version", "status": "pending"},
|
|
{"id": "2", "content": "Other task", "status": "pending"},
|
|
{"id": "1", "content": "Latest version", "status": "in_progress"},
|
|
])
|
|
assert result == [
|
|
{"id": "2", "content": "Other task", "status": "pending"},
|
|
{"id": "1", "content": "Latest version", "status": "in_progress"},
|
|
]
|
|
|
|
|
|
class TestHasItems:
|
|
def test_empty_store(self):
|
|
store = TodoStore()
|
|
assert store.has_items() is False
|
|
|
|
def test_non_empty_store(self):
|
|
store = TodoStore()
|
|
store.write([{"id": "1", "content": "x", "status": "pending"}])
|
|
assert store.has_items() is True
|
|
|
|
|
|
class TestFormatForInjection:
|
|
def test_empty_returns_none(self):
|
|
store = TodoStore()
|
|
assert store.format_for_injection() is None
|
|
|
|
def test_non_empty_has_markers(self):
|
|
store = TodoStore()
|
|
store.write([
|
|
{"id": "1", "content": "Do thing", "status": "completed"},
|
|
{"id": "2", "content": "Next", "status": "pending"},
|
|
{"id": "3", "content": "Working", "status": "in_progress"},
|
|
])
|
|
text = store.format_for_injection()
|
|
# Completed items are filtered out of injection
|
|
assert "[x]" not in text
|
|
assert "Do thing" not in text
|
|
# Active items are included
|
|
assert "[ ]" in text
|
|
assert "[>]" in text
|
|
assert "Next" in text
|
|
assert "Working" in text
|
|
assert "context compression" in text.lower()
|
|
|
|
|
|
class TestMergeMode:
|
|
def test_update_existing_by_id(self):
|
|
store = TodoStore()
|
|
store.write([
|
|
{"id": "1", "content": "Original", "status": "pending"},
|
|
])
|
|
store.write(
|
|
[{"id": "1", "status": "completed"}],
|
|
merge=True,
|
|
)
|
|
items = store.read()
|
|
assert len(items) == 1
|
|
assert items[0]["status"] == "completed"
|
|
assert items[0]["content"] == "Original"
|
|
|
|
def test_merge_appends_new(self):
|
|
store = TodoStore()
|
|
store.write([{"id": "1", "content": "First", "status": "pending"}])
|
|
store.write(
|
|
[{"id": "2", "content": "Second", "status": "pending"}],
|
|
merge=True,
|
|
)
|
|
items = store.read()
|
|
assert len(items) == 2
|
|
|
|
|
|
class TestTodoToolFunction:
|
|
def test_read_mode(self):
|
|
store = TodoStore()
|
|
store.write([{"id": "1", "content": "Task", "status": "pending"}])
|
|
result = json.loads(todo_tool(store=store))
|
|
assert result["summary"]["total"] == 1
|
|
assert result["summary"]["pending"] == 1
|
|
|
|
|
|
def test_no_store_returns_error(self):
|
|
result = json.loads(todo_tool())
|
|
assert "error" in result
|
|
|
|
|
|
class TestTodoStoreBounds:
|
|
"""Bounds on persisted todo state (GHSA-5g4g-6jrg-mw3g hardening).
|
|
|
|
The todo list is re-injected into context after every compression event,
|
|
so an unbounded item — whether authored by the model or replayed from
|
|
caller-supplied history on the API server's _hydrate_todo_store path —
|
|
would defeat the compression it rides through. These pin the caps.
|
|
Not a security boundary (the API surface is authenticated and the caller
|
|
supplies their own history); this is footgun containment / parity.
|
|
"""
|
|
|
|
def test_oversized_content_is_truncated(self):
|
|
from tools.todo_tool import MAX_TODO_CONTENT_CHARS
|
|
store = TodoStore()
|
|
store.write([{"id": "1", "content": "A" * 50001, "status": "pending"}])
|
|
item = store.read()[0]
|
|
assert len(item["content"]) <= MAX_TODO_CONTENT_CHARS
|
|
assert item["content"].endswith("… [truncated]")
|
|
|
|
def test_injection_block_is_bounded(self):
|
|
from tools.todo_tool import MAX_TODO_CONTENT_CHARS
|
|
store = TodoStore()
|
|
store.write([{"id": "1", "content": "A" * 50001, "status": "pending"}])
|
|
inj = store.format_for_injection()
|
|
# Before the fix this was ~50085 chars; now it tracks the cap.
|
|
assert len(inj) < MAX_TODO_CONTENT_CHARS + 200
|
|
|
|
|
|
def test_item_count_is_bounded(self):
|
|
from tools.todo_tool import MAX_TODO_ITEMS
|
|
store = TodoStore()
|
|
store.write([
|
|
{"id": str(i), "content": f"task {i}", "status": "pending"}
|
|
for i in range(5000)
|
|
])
|
|
assert len(store.read()) == MAX_TODO_ITEMS
|
|
|
|
def test_normal_list_is_unchanged(self):
|
|
"""No regression: ordinary plans pass through untouched (no marker,
|
|
same content, same order)."""
|
|
store = TodoStore()
|
|
store.write([
|
|
{"id": "1", "content": "write the report", "status": "in_progress"},
|
|
{"id": "2", "content": "review PR", "status": "pending"},
|
|
])
|
|
items = store.read()
|
|
assert [i["content"] for i in items] == ["write the report", "review PR"]
|
|
assert "[truncated]" not in items[0]["content"]
|