Files
hermes-agent/tests/tools/test_tip_tool.py
Brooklyn Nicholson 0357982696 feat(desktop): make the idle tip rotation opt-in, ungate the tool
The two halves of tips were behind one switch, which meant the app
volunteering commentary at idle shipped on by default. Split them along
the line that matters: the rotation talks unprompted, so it now waits to
be asked for, while an agent tip stays ungated like the tour it mirrors
— Hermes raises one mid-conversation, in answer to something the user
said.

Drops the tool's config gate along with the config key it read. The
renderer mirrored that key with config.set, which has no branch for it
and answered "unknown config key" into a swallowed catch, so the opt-out
never reached the backend in the first place.
2026-08-27 21:50:18 -05:00

88 lines
2.6 KiB
Python

"""Tests for the GUI-surface ``tip`` tool."""
import json
import pytest
from tools import tip_tool as tt
from tools.registry import registry
@pytest.fixture
def emitted(monkeypatch):
"""Capture what the desktop bridge is asked to send, and say it landed."""
sent = []
def _emit(event, payload):
sent.append((event, payload))
return True
monkeypatch.setattr(tt.desktop_ui, "emit", _emit)
return sent
def test_lives_in_the_gui_surface_toolset(monkeypatch):
"""Scoped by toolset, not by the backend's env — see AGENTS.md."""
monkeypatch.delenv("HERMES_DESKTOP", raising=False)
entry = registry.get_entry("tip")
assert entry is not None
assert entry.toolset == "desktop_ui"
def test_is_ungated_like_tour():
"""The Appearance switch governs the app's idle rotation, not this."""
entry = registry.get_entry("tip")
assert entry is not None
assert entry.check_fn is None
def test_requires_the_desktop_bridge(monkeypatch):
"""Outside the desktop GUI there is no emitter — a clear error, no crash."""
monkeypatch.setattr(tt.desktop_ui, "emit", lambda _event, _payload: False)
result = json.loads(tt.tip_tool(text="Over here", selector="#composer"))
assert "desktop" in result["error"]
def test_needs_both_something_to_say_and_somewhere_to_point(emitted):
assert "tip needs text" in json.loads(tt.tip_tool(text="", selector="#a"))["error"]
assert "tip needs a selector" in json.loads(tt.tip_tool(text="Hi", selector=" "))["error"]
assert not emitted
def test_rejects_an_unknown_side(emitted):
result = json.loads(tt.tip_tool(text="Hi", selector="#a", side="diagonal"))
assert "side must be one of" in result["error"]
assert not emitted
def test_emits_the_renderer_event_omitting_unset_fields(emitted):
result = json.loads(tt.tip_tool(text=" The model name is a button ", selector=" #model "))
assert result == {"success": True, "selector": "#model"}
assert emitted == [("tip.show", {"selector": "#model", "text": "The model name is a button"})]
def test_carries_title_and_side_when_given(emitted):
tt.tip_tool(text="Type here", selector="#composer", title="Composer", side="top")
assert emitted[0][1] == {
"selector": "#composer",
"text": "Type here",
"title": "Composer",
"side": "top",
}
def test_bridge_failure_is_reported(monkeypatch):
def _boom(_event, _payload):
raise RuntimeError("renderer went away")
monkeypatch.setattr(tt.desktop_ui, "emit", _boom)
assert "renderer went away" in json.loads(tt.tip_tool(text="Hi", selector="#a"))["error"]