911c6c50d3
The quiet sibling of `tour`, in the same `desktop_ui` toolset and reading the same `tour(action='targets')` discovery call: one bubble with an arrow, for a sentence that would be clearer with a finger on the thing it's about. Dimming the whole app to say "the model name is a button" is the wrong weight. Fire-and-forget rather than a round-trip, because a tip is not a question and blocking the turn on one would stall the reply it belongs to. The renderer enforces the user's opt-out itself, so a stale config read can never put a bubble on a screen that asked for none.
115 lines
3.5 KiB
Python
115 lines
3.5 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_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"]
|
|
|
|
|
|
class TestOptOut:
|
|
"""The user's switch gates the tool, and an absent key reads as ON."""
|
|
|
|
def _with_display(self, monkeypatch, display):
|
|
import hermes_cli.config as config_mod
|
|
|
|
monkeypatch.setattr(config_mod, "load_config_readonly", lambda: {"display": display})
|
|
|
|
def test_available_by_default(self, monkeypatch):
|
|
self._with_display(monkeypatch, {})
|
|
|
|
assert tt.check_tips_enabled() is True
|
|
|
|
def test_withdrawn_when_the_user_opts_out(self, monkeypatch):
|
|
self._with_display(monkeypatch, {"in_app_tips": False})
|
|
|
|
assert tt.check_tips_enabled() is False
|
|
|
|
def test_unreadable_config_leaves_the_tool_available(self, monkeypatch):
|
|
import hermes_cli.config as config_mod
|
|
|
|
def _boom():
|
|
raise OSError("no config here")
|
|
|
|
monkeypatch.setattr(config_mod, "load_config_readonly", _boom)
|
|
|
|
assert tt.check_tips_enabled() is True
|
|
|
|
def test_the_switch_is_what_gates_registration(self):
|
|
entry = registry.get_entry("tip")
|
|
|
|
assert entry is not None
|
|
assert entry.check_fn is tt.check_tips_enabled
|