mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-21 23:20:21 +00:00
7160cc26b5
The first cut asserted against the REAL tools.process_registry.format_process_notification, which isn't importable in WebUI-only CI shards (shard 2 failed: returned None). Inject a stub agent module via sys.modules so the test validates the delegation WIRING independent of agent-module availability, plus a graceful-degradation test confirming async_delegation returns None (not raise) when the agent formatter can't be imported. Production behavior is unchanged: async_delegation events only occur when the agent is running, so the import resolves where the events actually exist.
140 lines
4.9 KiB
Python
140 lines
4.9 KiB
Python
from api.background_process import format_wakeup_prompt
|
|
|
|
|
|
def test_format_wakeup_prompt_skips_empty_event():
|
|
assert format_wakeup_prompt({}) is None
|
|
|
|
|
|
def test_format_wakeup_prompt_skips_non_dict_event():
|
|
assert format_wakeup_prompt(None) is None
|
|
assert format_wakeup_prompt(42) is None
|
|
|
|
|
|
def test_format_wakeup_prompt_skips_unknown_event_type():
|
|
evt = {"type": "unknown_event", "message": "do not inject me"}
|
|
assert format_wakeup_prompt(evt) is None
|
|
|
|
|
|
def test_format_wakeup_prompt_handles_async_delegation(monkeypatch):
|
|
"""#4912 — async_delegation completion events (from a background
|
|
delegate_task) must be rendered, not silently dropped, so the subagent
|
|
result re-enters the parent conversation. format_wakeup_prompt delegates
|
|
to the agent-side tools.process_registry.format_process_notification.
|
|
|
|
We inject a stub agent module so this validates the delegation WIRING
|
|
independent of whether the full hermes-agent package is importable in the
|
|
test environment (it isn't in WebUI-only CI shards).
|
|
"""
|
|
import sys
|
|
import types
|
|
|
|
seen = {}
|
|
|
|
def _fake_fmt(evt):
|
|
seen["evt"] = evt
|
|
return "[ASYNC DELEGATION COMPLETE — t_1]\nSubagent finished: the answer is 42."
|
|
|
|
fake_mod = types.ModuleType("tools.process_registry")
|
|
fake_mod.format_process_notification = _fake_fmt
|
|
fake_pkg = sys.modules.get("tools") or types.ModuleType("tools")
|
|
monkeypatch.setitem(sys.modules, "tools", fake_pkg)
|
|
monkeypatch.setitem(sys.modules, "tools.process_registry", fake_mod)
|
|
|
|
evt = {
|
|
"type": "async_delegation",
|
|
"session_id": "proc_deleg1",
|
|
"session_key": "webui-session",
|
|
"task_id": "t_1",
|
|
"summary": "Subagent finished: the answer is 42.",
|
|
"status": "completed",
|
|
}
|
|
result = format_wakeup_prompt(evt)
|
|
assert result is not None, (
|
|
"async_delegation completion must produce a wakeup prompt (#4912), "
|
|
"not be dropped"
|
|
)
|
|
assert "ASYNC DELEGATION" in result.upper()
|
|
# Confirm it actually delegated the event to the agent-side formatter.
|
|
assert seen.get("evt", {}).get("type") == "async_delegation"
|
|
|
|
|
|
def test_format_wakeup_prompt_async_delegation_drops_gracefully_without_agent(monkeypatch):
|
|
"""If the agent-side formatter is unavailable (import fails), async_delegation
|
|
degrades to None instead of raising — same safe behavior as before #4912."""
|
|
import builtins
|
|
|
|
real_import = builtins.__import__
|
|
|
|
def _blocking_import(name, *args, **kwargs):
|
|
if name == "tools.process_registry" or name.startswith("tools.process_registry"):
|
|
raise ImportError("simulated: agent module unavailable")
|
|
return real_import(name, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(builtins, "__import__", _blocking_import)
|
|
evt = {"type": "async_delegation", "session_id": "s", "task_id": "t"}
|
|
# Must not raise; returns None when the agent formatter can't be imported.
|
|
assert format_wakeup_prompt(evt) is None
|
|
|
|
|
|
def test_format_wakeup_prompt_handles_watch_disabled():
|
|
evt = {
|
|
"type": "watch_disabled",
|
|
"session_id": "proc_abc",
|
|
"session_key": "webui-session",
|
|
"command": "tail -f log",
|
|
"message": "Watch patterns disabled for process proc_abc.",
|
|
}
|
|
assert (
|
|
format_wakeup_prompt(evt)
|
|
== "[IMPORTANT: Watch patterns disabled for process proc_abc.]"
|
|
)
|
|
|
|
|
|
def test_format_wakeup_prompt_skips_blank_watch_disabled():
|
|
assert format_wakeup_prompt({"type": "watch_disabled"}) is None
|
|
assert format_wakeup_prompt({"type": "watch_disabled", "message": ""}) is None
|
|
|
|
|
|
def test_format_wakeup_prompt_handles_watch_overflow_tripped():
|
|
evt = {
|
|
"type": "watch_overflow_tripped",
|
|
"session_id": "",
|
|
"session_key": "",
|
|
"command": "",
|
|
"message": "Watch-pattern overflow: suppressing further watch_match events.",
|
|
}
|
|
assert (
|
|
format_wakeup_prompt(evt)
|
|
== "[IMPORTANT: Watch-pattern overflow: suppressing further watch_match events.]"
|
|
)
|
|
|
|
|
|
def test_format_wakeup_prompt_handles_watch_overflow_released():
|
|
evt = {
|
|
"type": "watch_overflow_released",
|
|
"session_id": "",
|
|
"session_key": "",
|
|
"command": "",
|
|
"suppressed": 3,
|
|
"message": "Watch-pattern notifications resumed. 3 match event(s) were suppressed.",
|
|
}
|
|
assert (
|
|
format_wakeup_prompt(evt)
|
|
== "[IMPORTANT: Watch-pattern notifications resumed. 3 match event(s) were suppressed.]"
|
|
)
|
|
|
|
|
|
def test_format_wakeup_prompt_keeps_normal_completion():
|
|
evt = {
|
|
"type": "completion",
|
|
"session_id": "proc_abc",
|
|
"command": "sleep 1",
|
|
"exit_code": 0,
|
|
"output": "done",
|
|
}
|
|
result = format_wakeup_prompt(evt)
|
|
assert result is not None
|
|
assert "Background process proc_abc completed" in result
|
|
assert "Command: sleep 1" in result
|
|
assert "Output:\ndone" in result
|