"""Tests for agent/skill_commands.py — skill slash command scanning and platform filtering.""" import os from unittest.mock import patch import tools.skills_tool as skills_tool_module from agent.skill_commands import scan_skill_commands, build_skill_invocation_message def _make_skill( skills_dir, name, frontmatter_extra="", body="Do the thing.", category=None ): """Helper to create a minimal skill directory with SKILL.md.""" if category: skill_dir = skills_dir / category / name else: skill_dir = skills_dir / name skill_dir.mkdir(parents=True, exist_ok=True) content = f"""\ --- name: {name} description: Description for {name}. {frontmatter_extra}--- # {name} {body} """ (skill_dir / "SKILL.md").write_text(content) return skill_dir class TestScanSkillCommands: def test_finds_skills(self, tmp_path): with patch("tools.skills_tool.SKILLS_DIR", tmp_path): _make_skill(tmp_path, "my-skill") result = scan_skill_commands() assert "/my-skill" in result assert result["/my-skill"]["name"] == "my-skill" def test_empty_dir(self, tmp_path): with patch("tools.skills_tool.SKILLS_DIR", tmp_path): result = scan_skill_commands() assert result == {} def test_excludes_incompatible_platform(self, tmp_path): """macOS-only skills should not register slash commands on Linux.""" with ( patch("tools.skills_tool.SKILLS_DIR", tmp_path), patch("tools.skills_tool.sys") as mock_sys, ): mock_sys.platform = "linux" _make_skill(tmp_path, "imessage", frontmatter_extra="platforms: [macos]\n") _make_skill(tmp_path, "web-search") result = scan_skill_commands() assert "/web-search" in result assert "/imessage" not in result def test_includes_matching_platform(self, tmp_path): """macOS-only skills should register slash commands on macOS.""" with ( patch("tools.skills_tool.SKILLS_DIR", tmp_path), patch("tools.skills_tool.sys") as mock_sys, ): mock_sys.platform = "darwin" _make_skill(tmp_path, "imessage", frontmatter_extra="platforms: [macos]\n") result = scan_skill_commands() assert "/imessage" in result def test_universal_skill_on_any_platform(self, tmp_path): """Skills without platforms field should register on any platform.""" with ( patch("tools.skills_tool.SKILLS_DIR", tmp_path), patch("tools.skills_tool.sys") as mock_sys, ): mock_sys.platform = "win32" _make_skill(tmp_path, "generic-tool") result = scan_skill_commands() assert "/generic-tool" in result class TestBuildSkillInvocationMessage: def test_loads_skill_by_stored_path_when_frontmatter_name_differs(self, tmp_path): skill_dir = tmp_path / "mlops" / "audiocraft" skill_dir.mkdir(parents=True, exist_ok=True) (skill_dir / "SKILL.md").write_text( """\ --- name: audiocraft-audio-generation description: Generate audio with AudioCraft. --- # AudioCraft Generate some audio. """ ) with patch("tools.skills_tool.SKILLS_DIR", tmp_path): scan_skill_commands() msg = build_skill_invocation_message("/audiocraft-audio-generation", "compose") assert msg is not None assert "AudioCraft" in msg assert "compose" in msg def test_builds_message(self, tmp_path): with patch("tools.skills_tool.SKILLS_DIR", tmp_path): _make_skill(tmp_path, "test-skill") scan_skill_commands() msg = build_skill_invocation_message("/test-skill", "do stuff") assert msg is not None assert "test-skill" in msg assert "do stuff" in msg def test_returns_none_for_unknown(self, tmp_path): with patch("tools.skills_tool.SKILLS_DIR", tmp_path): scan_skill_commands() msg = build_skill_invocation_message("/nonexistent") assert msg is None def test_uses_shared_skill_loader_for_secure_setup(self, tmp_path, monkeypatch): monkeypatch.delenv("TENOR_API_KEY", raising=False) calls = [] def fake_secret_callback(var_name, prompt, metadata=None): calls.append((var_name, prompt, metadata)) os.environ[var_name] = "stored-in-test" return { "success": True, "stored_as": var_name, "validated": False, "skipped": False, } monkeypatch.setattr( skills_tool_module, "_secret_capture_callback", fake_secret_callback, raising=False, ) with patch("tools.skills_tool.SKILLS_DIR", tmp_path): _make_skill( tmp_path, "test-skill", frontmatter_extra=( "required_environment_variables:\n" " - name: TENOR_API_KEY\n" " prompt: Tenor API key\n" ), ) scan_skill_commands() msg = build_skill_invocation_message("/test-skill", "do stuff") assert msg is not None assert "test-skill" in msg assert len(calls) == 1 assert calls[0][0] == "TENOR_API_KEY" def test_gateway_still_loads_skill_but_returns_setup_guidance( self, tmp_path, monkeypatch ): monkeypatch.delenv("TENOR_API_KEY", raising=False) def fail_if_called(var_name, prompt, metadata=None): raise AssertionError( "gateway flow should not try secure in-band secret capture" ) monkeypatch.setattr( skills_tool_module, "_secret_capture_callback", fail_if_called, raising=False, ) with patch.dict( os.environ, {"HERMES_SESSION_PLATFORM": "telegram"}, clear=False ): with patch("tools.skills_tool.SKILLS_DIR", tmp_path): _make_skill( tmp_path, "test-skill", frontmatter_extra=( "required_environment_variables:\n" " - name: TENOR_API_KEY\n" " prompt: Tenor API key\n" ), ) scan_skill_commands() msg = build_skill_invocation_message("/test-skill", "do stuff") assert msg is not None assert "local cli" in msg.lower() def test_preserves_remaining_remote_setup_warning(self, tmp_path, monkeypatch): monkeypatch.setenv("TERMINAL_ENV", "ssh") monkeypatch.delenv("TENOR_API_KEY", raising=False) def fake_secret_callback(var_name, prompt, metadata=None): os.environ[var_name] = "stored-in-test" return { "success": True, "stored_as": var_name, "validated": False, "skipped": False, } monkeypatch.setattr( skills_tool_module, "_secret_capture_callback", fake_secret_callback, raising=False, ) with patch("tools.skills_tool.SKILLS_DIR", tmp_path): _make_skill( tmp_path, "test-skill", frontmatter_extra=( "required_environment_variables:\n" " - name: TENOR_API_KEY\n" " prompt: Tenor API key\n" ), ) scan_skill_commands() msg = build_skill_invocation_message("/test-skill", "do stuff") assert msg is not None assert "remote environment" in msg.lower() def test_supporting_file_hint_uses_file_path_argument(self, tmp_path): with patch("tools.skills_tool.SKILLS_DIR", tmp_path): skill_dir = _make_skill(tmp_path, "test-skill") references = skill_dir / "references" references.mkdir() (references / "api.md").write_text("reference") scan_skill_commands() msg = build_skill_invocation_message("/test-skill", "do stuff") assert msg is not None assert 'file_path=""' in msg