Files
hermes-agent/tests/cli/test_subagent_notification_display.py

96 lines
6.0 KiB
Python

"""Completion notices stay human-facing while the parent retains full results."""
import copy
import queue
import threading
from types import SimpleNamespace
from unittest.mock import Mock
from cli import HermesCLI
from tools.process_registry_notifications import format_process_notification
from tui_gateway import server
def test_completion_display_keeps_payload_separate_across_surfaces(monkeypatch, capsys, tmp_path):
for status, truncated, label in [("completed", False, "Completed"), ("failed", False, "Failed"),
("cancelled", False, "Cancelled"), ("completed", True, "Incomplete"),
("stalled", False, "Stalled"), ("unknown", False, "Unknown"),
("rejected", False, "Failed"), ("other_terminal_state", False, "Incomplete")]:
event = {"type": "async_delegation", "session_key": "display-session", "delegation_id": "deleg-test",
"goals": ["Do not display this sibling", "Review [bold]changes[/bold]"],
"results": [{"task_index": 1, "status": status, "truncated": truncated, "summary": "Full result evidence"}]}
original = copy.deepcopy(event)
payload = format_process_notification(event)
cli = HermesCLI.__new__(HermesCLI)
cli.session_id = event["session_key"]
cli._pending_input = queue.Queue()
registry = SimpleNamespace(drain_notifications=lambda **kw: [(event, payload)], completion_queue=queue.Queue())
monkeypatch.setattr("tools.process_registry.process_registry", registry)
monkeypatch.setattr("tools.async_delegation.claim_event_delivery", lambda *a: "claimed")
monkeypatch.setattr("tools.async_delegation.complete_event_delivery", lambda *a: None)
cli._drain_process_notifications("cli-idle")
cli._pending_resume_sessions = []
cli._typed_voice_stop = lambda text: False
cli.handle_bang_shell = lambda text: False
cli._turn_summary_begin = lambda: None
cli._tui_after_turn = lambda: None
cli._app = SimpleNamespace(invalidate=lambda: None)
cli.chat = Mock()
cli._tui_process_one_input(cli._pending_input.get_nowait())
visible = capsys.readouterr().out
expected = f"Subagent Task {label}: {event['goals'][1]}"
assert expected in visible
assert "ASYNC DELEGATION" not in visible and "Full result evidence" not in visible
queued_message = cli.chat.call_args.args[0]
assert queued_message == payload
cli.conversation_history = []
cli.agent = SimpleNamespace(run_conversation=Mock(return_value={}))
cli._flush_credit_notices = lambda: None
cli._chat_stage_user_message(cli.agent, queued_message)
from cli import _ChatTurn
cli._chat_run_agent(_ChatTurn(), str(queued_message))
staged = cli.conversation_history[-1]
assert staged["content"] == payload
assert type(staged["content"]) is str
assert staged["display_kind"] == "async_delegation_complete"
from agent.turn_context import _stage_turn_user_message
run_args = cli.agent.run_conversation.call_args.kwargs
core_message, _ = _stage_turn_user_message(cli.agent, run_args["user_message"],
run_args["persist_user_message"], None, None, None, None)
assert core_message is staged
assert core_message["display_metadata"]["display_text"] == expected
assert copy.deepcopy(staged) == staged
from agent.prompt_caching import build_prompt_cache_plan, strip_anthropic_cache_control
plan = build_prompt_cache_plan([core_message], tools=None)
assert strip_anthropic_cache_control(plan.messages)[0]["content"] == payload
from hermes_cli.cli_agent_setup_mixin import _collect_resume_entries
from hermes_state import SessionDB
with SessionDB(tmp_path / f"{status}-{truncated}.db") as db:
db.create_session(cli.session_id, source="cli")
db.append_messages_batch(cli.session_id, cli.conversation_history)
restored = db.get_messages(cli.session_id)
assert restored[0]["content"] == payload
entries, _, _ = _collect_resume_entries(restored, {}, lambda text: text)
assert entries == [("event", expected)]
emitted, dispatched = [], []
monkeypatch.setattr(server, "_emit", lambda *args: emitted.append(args))
monkeypatch.setattr(server, "_notif_dispatch_event", lambda *args: dispatched.append(args))
session = {"session_key": event["session_key"], "history_lock": threading.RLock()}
server._notif_handle_event("ui-session", session, event, set(), registry, format_process_notification, None)
assert emitted[0][2]["text"] == expected
assert dispatched[0][3] == payload
assert server._async_delegation_display_metadata(event)["display_text"] == expected
assert event == original
from tools.process_registry_notifications import async_delegation_display_text
grouped = {"group": "Review", "goals": ["First", "Second"],
"results": [{"task_index": 0, "status": "completed"}, {"task_index": 1, "status": "failed"}]}
assert async_delegation_display_text(grouped) == "Subagent Tasks Finished with Issues: Review (2 tasks)"
early = {**grouped, "task_failure_notice": True, "results": [grouped["results"][1]]}
assert async_delegation_display_text(early) == "Subagent Task Failed: Second"
grouped["results"][1]["status"] = "completed"
assert async_delegation_display_text(grouped) == "Subagent Tasks Completed: Review (2 tasks)"
assert async_delegation_display_text({"goal": "Legacy task", "status": "timeout"}) == "Subagent Task Timed Out: Legacy task"
assert "Failed" in async_delegation_display_text({"results": [], "error": "Worker crashed"})
cli._print_user_message_preview("[ASYNC DELEGATION BATCH COMPLETE — user-authored]")
assert "[ASYNC DELEGATION BATCH COMPLETE — user-authored]" in capsys.readouterr().out