Files
hermes-agent/tests/agent/test_bot_chat_toolset_refresh.py
teknium1 10708bc117 fix(bot-chat): rebuild a stale Bot Chat's tools through the surface builder (#124211, salvage #124307)
Trim the salvaged detector/delivery pair to the salvage bar and fix the
delivery half. The PR resolved the refresh selection as
platform_toolsets.<surface> (desktop/tui), a key no config path writes,
so _get_platform_tools fell back to the constructed hermes-<surface>
composite that resolves to 0 tools (a cold resume went 30 -> 1 tool).

Delivery now goes through the builder new desktop/TUI sessions use
(tui_gateway.server._load_enabled_toolsets(platform) +
_load_disabled_toolsets) as an explicit refresh_agent_mcp_tools
override, so the refreshed set equals a fresh session's on that surface;
only desktop/tui agents are long-lived, every other surface builds a
fresh agent per process/turn and is left alone. Drops the
toolsets.py/tui_gateway policy relocation and the raw-value fallback in
the fingerprint; keeps the detector (platform_toolsets +
agent.disabled_toolsets replace the dead tools.enabled_toolsets key)
and the tools re-pin after the refresh.

Tests: 2 invariants in tests/agent/test_bot_chat_toolset_refresh.py
(desktop -> builder consulted, disabled override passed, re-pinned;
cli -> untouched) and the existing fingerprint axis test now edits
the key `hermes tools enable/disable` writes.
2026-09-28 06:06:17 -07:00

77 lines
3.1 KiB
Python

"""#124211: a Bot Chat capability refresh must rebuild tools[] through the surface's own
builder (what a fresh desktop/TUI session gets), not just the system prompt."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from agent.conversation_loop import _restore_or_build_system_prompt
STORED_PROMPT = (
"SYSTEM PROMPT BODY\n\nConversation started: Monday, January 05, 2026\n"
"Model: test-model\nProvider: openrouter\nPlatform: desktop"
)
def _stale_bot_chat_agent(platform: str):
db = MagicMock()
db.get_session.return_value = {"system_prompt": STORED_PROMPT, "tool_names": None}
agent = MagicMock()
agent._cached_system_prompt = None
agent.session_id = "test-session-id"
agent.model = "test-model"
agent.provider = "openrouter"
agent.platform = platform
agent._session_db = db
agent._use_prompt_caching = False
agent._build_system_prompt = MagicMock(return_value="NEW_PROMPT")
agent.enabled_toolsets = ["web"]
agent.disabled_toolsets = None
agent.tools = [{"type": "function", "function": {"name": "web_search", "parameters": {}}}]
agent.valid_tool_names = {"web_search"}
agent._bot_mode_protocol = True
agent._session_title_hint = "Bot Chat"
agent._surface_switch_note = ""
agent._gateway_turn_context_notes = ""
return agent, db
def _run_stale_refresh(agent):
builder = MagicMock(return_value=["terminal", "file", "desktop_ui"])
with (
patch("tools.bot_mode_probe.stored_prompt_capability_stale", return_value=True),
patch("tui_gateway.server._load_enabled_toolsets", builder),
patch("tui_gateway.server._load_disabled_toolsets", return_value=["computer_use"]),
patch("tools.mcp_tool_agent.refresh_agent_mcp_tools", return_value=set()) as refresh,
):
_restore_or_build_system_prompt(agent, None, [{"role": "user", "content": "hi"}])
assert agent._cached_system_prompt == "NEW_PROMPT"
return builder, refresh
def test_stale_epoch_rebuilds_tools_through_the_surface_builder():
"""The selection handed to the rebuild is the desktop builder's answer for THIS surface
(never a `platform_toolsets.<surface>` lookup, which resolves to no tools), a disabled
toolset is not carried forward, and the result is re-pinned for the next turn."""
agent, db = _stale_bot_chat_agent("desktop")
builder, refresh = _run_stale_refresh(agent)
builder.assert_called_once_with("desktop")
kwargs = refresh.call_args.kwargs
assert kwargs["enabled_override"] == ["terminal", "file", "desktop_ui"]
assert kwargs["disabled_override"] == ["computer_use"]
assert not kwargs.get("preserve_prefix")
assert db.update_session_tool_names.called
def test_stale_epoch_leaves_a_per_process_surface_alone():
"""A CLI resume builds a fresh agent from config every process; the desktop builder's
fold-in (client-surface toolsets) must not be applied to it."""
agent, _db = _stale_bot_chat_agent("cli")
builder, refresh = _run_stale_refresh(agent)
builder.assert_not_called()
refresh.assert_not_called()