c1762ff11c
The rename sweep in the base commit missed the sibling-test blast radius (18 red files on CI). Three classes, all fixed: 1. Stale old names in tests (todo/cronjob/process/tour/tip) — updated to todo_list/cronjob_manage/process_manage/gui_tour/show_tip at every registry.get_entry/dispatch/coerce/preview/allowlist call site, plus the coding-brief sentence in agent/coding_context.py now names todo_list (and its gating test). 2. Missed rename in production: AGENT_RUNTIME_POST_HOOK_TOOL_NAMES still held 'tour' — post-hook ownership would have double-emitted for gui_tour via the bridge path. 3. Tests pinning pre-deferral assembly (blank-slate surface, modal sandbox resolution, desktop diet, HUD note) now pin their ACTUAL contract under the legacy defer:[] override, or assert on granted tool names instead of visible schemas. Also fixes a pre-existing ordering flake surfaced by the sweep: test_holds_exactly_the_gui_affordances depended on whether an earlier test had imported apply_layout_tool (registry-registered, not in the static desktop_ui list) — now forces discovery and pins the full set. 649 tests green locally across all touched files, both orderings.
125 lines
4.9 KiB
Python
125 lines
4.9 KiB
Python
"""GUI capability follows the SESSION's client, not the backend's process env.
|
|
|
|
The desktop app is a client. It can drive a backend that Electron spawned
|
|
locally, one reached over SSH, one behind a plain URL+token, or Hermes Cloud —
|
|
and only the first two run with ``HERMES_DESKTOP=1`` in their environment.
|
|
Gating the pane/browser/reaction tools on that env var therefore stripped every
|
|
one of them from URL and cloud gateways, while the same backend still told the
|
|
model "You are chatting inside the Hermes desktop app".
|
|
|
|
These tests pin the contract that replaced it: eligibility is resolved from the
|
|
session's own ``source`` (``session.create``'s ``source: 'desktop'``), so the
|
|
answer is identical on every connection topology.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
import tui_gateway.server as server
|
|
from toolsets import TOOLSETS, resolve_toolset
|
|
|
|
GUI_TOOLS = {
|
|
"annotate_preview",
|
|
"desktop_preview",
|
|
"drive_preview",
|
|
"close_terminal",
|
|
"focus_pane",
|
|
"read_terminal",
|
|
"read_window_below",
|
|
"react_to_message",
|
|
"setup_mcp",
|
|
"show_tip",
|
|
"gui_tour",
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def no_desktop_env(monkeypatch):
|
|
"""A backend nobody told about the desktop — i.e. every remote gateway."""
|
|
monkeypatch.delenv("HERMES_DESKTOP", raising=False)
|
|
monkeypatch.delenv("HERMES_DESKTOP_TERMINAL", raising=False)
|
|
monkeypatch.delenv("HERMES_TUI_TOOLSETS", raising=False)
|
|
return monkeypatch
|
|
|
|
|
|
class TestDesktopUiToolset:
|
|
def test_holds_exactly_the_gui_affordances(self):
|
|
# apply_layout registers into desktop_ui via the registry (not the
|
|
# static toolsets.py list), so force discovery first — otherwise the
|
|
# result depends on which earlier test imported tool modules
|
|
# (pre-existing ordering flake, surfaced by the #97979 test sweep).
|
|
from tools.registry import discover_builtin_tools
|
|
discover_builtin_tools()
|
|
assert set(resolve_toolset("desktop_ui")) == GUI_TOOLS | {"apply_layout"}
|
|
|
|
def test_stays_off_the_core_tool_list(self):
|
|
"""Core ships on every API call — a GUI-only tool must not be there."""
|
|
from toolsets import _HERMES_CORE_TOOLS
|
|
|
|
assert GUI_TOOLS.isdisjoint(_HERMES_CORE_TOOLS)
|
|
|
|
def test_no_platform_bundle_carries_it(self):
|
|
"""Messaging/CLI bundles must not pick these up by listing them."""
|
|
for name, spec in TOOLSETS.items():
|
|
if name == "desktop_ui":
|
|
continue
|
|
assert GUI_TOOLS.isdisjoint(set(spec.get("tools") or ())), name
|
|
|
|
|
|
class TestSurfaceResolution:
|
|
def test_desktop_session_gets_them_with_no_desktop_env(self, no_desktop_env):
|
|
"""THE regression: a desktop client on a remote/cloud backend."""
|
|
assert "desktop_ui" in server._gui_surface_toolsets("desktop")
|
|
|
|
def test_tui_session_does_not(self, no_desktop_env):
|
|
assert "desktop_ui" not in server._gui_surface_toolsets("tui")
|
|
|
|
def test_desktop_env_alone_does_not_grant_them(self, no_desktop_env):
|
|
"""A desktop-spawned backend serving a TUI session stays clean.
|
|
|
|
The embedded terminal pane runs `hermes --tui` against this same
|
|
backend; env-keyed gating handed it GUI tools it cannot answer.
|
|
"""
|
|
no_desktop_env.setenv("HERMES_DESKTOP", "1")
|
|
assert "desktop_ui" not in server._gui_surface_toolsets("tui")
|
|
|
|
def test_project_tools_ride_on_every_gui_surface(self, no_desktop_env):
|
|
for platform in ("desktop", "tui"):
|
|
assert "project" in server._gui_surface_toolsets(platform)
|
|
|
|
|
|
class TestResolverPlumbing:
|
|
def test_posture_path_folds_in_the_session_surface(self, no_desktop_env):
|
|
"""Focus-mode returns early — the surface toolsets must survive it."""
|
|
import agent.coding_context as cc
|
|
|
|
no_desktop_env.setattr(cc, "coding_selection", lambda **_: ["coding"])
|
|
|
|
assert server._load_enabled_toolsets("desktop") == [
|
|
"coding",
|
|
"desktop_ui",
|
|
"project",
|
|
]
|
|
assert server._load_enabled_toolsets("tui") == ["coding", "project"]
|
|
|
|
def test_config_path_folds_in_the_session_surface(self, no_desktop_env):
|
|
import agent.coding_context as cc
|
|
import hermes_cli.config as config_mod
|
|
|
|
no_desktop_env.setattr(cc, "coding_selection", lambda **_: None)
|
|
no_desktop_env.setattr(
|
|
config_mod, "load_config", lambda: {"platform_toolsets": {"cli": ["memory"]}}
|
|
)
|
|
|
|
desktop = server._load_enabled_toolsets("desktop")
|
|
tui = server._load_enabled_toolsets("tui")
|
|
|
|
assert desktop is not None and tui is not None
|
|
assert "desktop_ui" in desktop
|
|
assert "desktop_ui" not in tui
|
|
|
|
def test_explicit_env_pin_still_wins(self, no_desktop_env):
|
|
"""HERMES_TUI_TOOLSETS is an operator override; surface can't re-add."""
|
|
no_desktop_env.setenv("HERMES_TUI_TOOLSETS", "web,memory")
|
|
|
|
assert server._load_enabled_toolsets("desktop") == ["web", "memory"]
|