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.
149 lines
5.8 KiB
Python
149 lines
5.8 KiB
Python
"""Phantom tool references: system-prompt blocks must not name tools the
|
|
session can't call (Blank Slate audit, Aug 2026).
|
|
|
|
Covers:
|
|
* HERMES_AGENT_HELP_GUIDANCE degrades to the docs-only variant when the
|
|
skill tools aren't loaded.
|
|
* execution_guidance_text() drops web_search lines when web tools are off.
|
|
* The coding operating brief drops the `todo` sentence when the todo tool
|
|
isn't loaded.
|
|
* ESSENTIAL_SKILLS can't be disabled via config, and the CLI writer strips
|
|
them from persisted disabled lists.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
class TestHermesAgentHelpGuidance:
|
|
def test_skill_variant_used_when_skill_view_present(self):
|
|
from agent.prompt_builder import HERMES_AGENT_HELP_GUIDANCE
|
|
assert "skill_view(name='hermes-agent')" in HERMES_AGENT_HELP_GUIDANCE
|
|
|
|
def test_no_skills_variant_has_no_skill_view_reference(self):
|
|
from agent.prompt_builder import HERMES_AGENT_HELP_GUIDANCE_NO_SKILLS
|
|
assert "skill_view" not in HERMES_AGENT_HELP_GUIDANCE_NO_SKILLS
|
|
assert "hermes-agent.nousresearch.com/docs" in HERMES_AGENT_HELP_GUIDANCE_NO_SKILLS
|
|
|
|
|
|
class TestExecutionGuidanceText:
|
|
def test_full_text_when_web_search_available(self):
|
|
from agent.prompt_builder import (
|
|
OPENAI_MODEL_EXECUTION_GUIDANCE,
|
|
execution_guidance_text,
|
|
)
|
|
assert execution_guidance_text({"web_search", "terminal"}) == (
|
|
OPENAI_MODEL_EXECUTION_GUIDANCE
|
|
)
|
|
|
|
def test_full_text_when_toolset_unknown(self):
|
|
from agent.prompt_builder import (
|
|
OPENAI_MODEL_EXECUTION_GUIDANCE,
|
|
execution_guidance_text,
|
|
)
|
|
assert execution_guidance_text(None) == OPENAI_MODEL_EXECUTION_GUIDANCE
|
|
|
|
def test_web_search_dropped_without_web_tools(self):
|
|
from agent.prompt_builder import execution_guidance_text
|
|
text = execution_guidance_text({"terminal", "read_file"})
|
|
assert "web_search" not in text
|
|
# The surrounding structure survives.
|
|
assert "<mandatory_tool_use>" in text
|
|
assert "<missing_context>" in text
|
|
assert "(search_files, read_file, etc.)" in text
|
|
|
|
|
|
class TestCodingBriefTodoGating:
|
|
def _brief(self, valid_tool_names):
|
|
from agent.coding_context import CODING_PROFILE, RuntimeMode
|
|
mode = RuntimeMode(
|
|
profile=CODING_PROFILE, surface="cli", cwd=Path.cwd(),
|
|
)
|
|
prefix, _ws, _tr = mode.system_prompt_parts(
|
|
valid_tool_names=valid_tool_names
|
|
)
|
|
assert prefix, "coding profile must emit an operating brief"
|
|
return prefix[0]
|
|
|
|
def test_todo_kept_when_tool_available(self):
|
|
brief = self._brief({"todo_list", "terminal", "read_file"})
|
|
assert "Track multi-step work with `todo_list`" in brief
|
|
|
|
def test_todo_dropped_when_tool_missing(self):
|
|
brief = self._brief({"terminal", "read_file"})
|
|
assert "`todo`" not in brief
|
|
# The path:line half of the merged bullet survives.
|
|
assert "path:line" in brief
|
|
|
|
def test_unknown_toolset_keeps_full_brief(self):
|
|
brief = self._brief(None)
|
|
assert "Track multi-step work with `todo_list`" in brief
|
|
|
|
|
|
class TestEssentialSkillsUndisableable:
|
|
def test_agent_side_reader_strips_essential(self, monkeypatch, tmp_path):
|
|
import agent.skill_utils as su
|
|
cfg = tmp_path / "config.yaml"
|
|
cfg.write_text(
|
|
"skills:\n disabled:\n - hermes-agent\n - some-other-skill\n",
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr(su, "get_config_path", lambda: cfg)
|
|
su._RAW_CONFIG_CACHE.clear()
|
|
disabled = su.get_disabled_skill_names(platform="cli")
|
|
assert "hermes-agent" not in disabled
|
|
assert "some-other-skill" in disabled
|
|
|
|
def test_cli_side_reader_strips_essential(self):
|
|
from hermes_cli.skills_config import get_disabled_skills
|
|
cfg = {"skills": {"disabled": ["hermes-agent", "other"]}}
|
|
disabled = get_disabled_skills(cfg)
|
|
assert "hermes-agent" not in disabled
|
|
assert "other" in disabled
|
|
|
|
def test_cli_side_writer_strips_essential(self, monkeypatch):
|
|
import hermes_cli.skills_config as sc
|
|
saved = {}
|
|
monkeypatch.setattr(sc, "save_config", lambda cfg: saved.update(cfg))
|
|
cfg = {}
|
|
sc.save_disabled_skills(cfg, {"hermes-agent", "other"})
|
|
assert cfg["skills"]["disabled"] == ["other"]
|
|
|
|
def test_skill_manage_delete_refused(self):
|
|
from tools.skill_manager_tool import _pinned_guard
|
|
msg = _pinned_guard("hermes-agent")
|
|
assert msg is not None
|
|
assert "essential" in msg.lower()
|
|
|
|
|
|
class TestEssentialOnlySync:
|
|
def test_opted_out_sync_seeds_only_essential(self, monkeypatch, tmp_path):
|
|
"""A profile with .no-bundled-skills still gets the hermes-agent skill."""
|
|
import tools.skills_sync as ss
|
|
|
|
home = tmp_path / ".hermes"
|
|
home.mkdir()
|
|
(home / ss.NO_BUNDLED_SKILLS_MARKER).write_text("", encoding="utf-8")
|
|
|
|
bundled = tmp_path / "bundled"
|
|
for cat, name in [
|
|
("autonomous-ai-agents", "hermes-agent"),
|
|
("media", "gif-search"),
|
|
]:
|
|
d = bundled / cat / name
|
|
d.mkdir(parents=True)
|
|
(d / "SKILL.md").write_text(
|
|
f"---\nname: {name}\ndescription: x\n---\nbody\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
monkeypatch.setattr(ss, "_hermes_home", lambda: home)
|
|
monkeypatch.setattr(ss, "_get_bundled_dir", lambda: bundled)
|
|
monkeypatch.setattr(ss, "_build_external_skill_index", lambda: set())
|
|
|
|
result = ss.sync_skills(quiet=True)
|
|
|
|
assert result["skipped_opt_out"] is True
|
|
assert result["copied"] == ["hermes-agent"]
|
|
assert (home / "skills" / "autonomous-ai-agents" / "hermes-agent" / "SKILL.md").exists()
|
|
assert not (home / "skills" / "media").exists()
|