merge: resolve conflicts with origin/main (SSH preflight check)
This commit is contained in:
commit
01e62c067b
33 changed files with 374 additions and 85 deletions
123
tests/agent/test_display_emoji.py
Normal file
123
tests/agent/test_display_emoji.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
"""Tests for get_tool_emoji in agent/display.py — skin + registry integration."""
|
||||
|
||||
from unittest.mock import patch as mock_patch, MagicMock
|
||||
|
||||
from agent.display import get_tool_emoji
|
||||
|
||||
|
||||
class TestGetToolEmoji:
|
||||
"""Verify the skin → registry → fallback resolution chain."""
|
||||
|
||||
def test_returns_registry_emoji_when_no_skin(self):
|
||||
"""Registry-registered emoji is used when no skin is active."""
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_emoji.return_value = "🎨"
|
||||
with mock_patch("agent.display._get_skin", return_value=None), \
|
||||
mock_patch("agent.display.registry", mock_registry, create=True):
|
||||
# Need to patch the import inside get_tool_emoji
|
||||
pass
|
||||
# Direct test: patch the lazy import path
|
||||
with mock_patch("agent.display._get_skin", return_value=None):
|
||||
# get_tool_emoji will try to import registry — mock that
|
||||
mock_reg = MagicMock()
|
||||
mock_reg.get_emoji.return_value = "📖"
|
||||
with mock_patch.dict("sys.modules", {}):
|
||||
import sys
|
||||
# Patch tools.registry module
|
||||
mock_module = MagicMock()
|
||||
mock_module.registry = mock_reg
|
||||
with mock_patch.dict(sys.modules, {"tools.registry": mock_module}):
|
||||
result = get_tool_emoji("read_file")
|
||||
assert result == "📖"
|
||||
|
||||
def test_skin_override_takes_precedence(self):
|
||||
"""Skin tool_emojis override registry defaults."""
|
||||
skin = MagicMock()
|
||||
skin.tool_emojis = {"terminal": "⚔"}
|
||||
with mock_patch("agent.display._get_skin", return_value=skin):
|
||||
result = get_tool_emoji("terminal")
|
||||
assert result == "⚔"
|
||||
|
||||
def test_skin_empty_dict_falls_through(self):
|
||||
"""Empty skin tool_emojis falls through to registry."""
|
||||
skin = MagicMock()
|
||||
skin.tool_emojis = {}
|
||||
mock_reg = MagicMock()
|
||||
mock_reg.get_emoji.return_value = "💻"
|
||||
import sys
|
||||
mock_module = MagicMock()
|
||||
mock_module.registry = mock_reg
|
||||
with mock_patch("agent.display._get_skin", return_value=skin), \
|
||||
mock_patch.dict(sys.modules, {"tools.registry": mock_module}):
|
||||
result = get_tool_emoji("terminal")
|
||||
assert result == "💻"
|
||||
|
||||
def test_fallback_default(self):
|
||||
"""When neither skin nor registry has an emoji, use the default."""
|
||||
skin = MagicMock()
|
||||
skin.tool_emojis = {}
|
||||
mock_reg = MagicMock()
|
||||
mock_reg.get_emoji.return_value = ""
|
||||
import sys
|
||||
mock_module = MagicMock()
|
||||
mock_module.registry = mock_reg
|
||||
with mock_patch("agent.display._get_skin", return_value=skin), \
|
||||
mock_patch.dict(sys.modules, {"tools.registry": mock_module}):
|
||||
result = get_tool_emoji("unknown_tool")
|
||||
assert result == "⚡"
|
||||
|
||||
def test_custom_default(self):
|
||||
"""Custom default is returned when nothing matches."""
|
||||
with mock_patch("agent.display._get_skin", return_value=None):
|
||||
mock_reg = MagicMock()
|
||||
mock_reg.get_emoji.return_value = ""
|
||||
import sys
|
||||
mock_module = MagicMock()
|
||||
mock_module.registry = mock_reg
|
||||
with mock_patch.dict(sys.modules, {"tools.registry": mock_module}):
|
||||
result = get_tool_emoji("x", default="⚙️")
|
||||
assert result == "⚙️"
|
||||
|
||||
def test_skin_override_only_for_matching_tool(self):
|
||||
"""Skin override for one tool doesn't affect others."""
|
||||
skin = MagicMock()
|
||||
skin.tool_emojis = {"terminal": "⚔"}
|
||||
mock_reg = MagicMock()
|
||||
mock_reg.get_emoji.return_value = "🔍"
|
||||
import sys
|
||||
mock_module = MagicMock()
|
||||
mock_module.registry = mock_reg
|
||||
with mock_patch("agent.display._get_skin", return_value=skin), \
|
||||
mock_patch.dict(sys.modules, {"tools.registry": mock_module}):
|
||||
assert get_tool_emoji("terminal") == "⚔" # skin override
|
||||
assert get_tool_emoji("web_search") == "🔍" # registry fallback
|
||||
|
||||
|
||||
class TestSkinConfigToolEmojis:
|
||||
"""Verify SkinConfig handles tool_emojis field correctly."""
|
||||
|
||||
def test_skin_config_has_tool_emojis_field(self):
|
||||
from hermes_cli.skin_engine import SkinConfig
|
||||
skin = SkinConfig(name="test")
|
||||
assert skin.tool_emojis == {}
|
||||
|
||||
def test_skin_config_accepts_tool_emojis(self):
|
||||
from hermes_cli.skin_engine import SkinConfig
|
||||
emojis = {"terminal": "⚔", "web_search": "🔮"}
|
||||
skin = SkinConfig(name="test", tool_emojis=emojis)
|
||||
assert skin.tool_emojis == emojis
|
||||
|
||||
def test_build_skin_config_includes_tool_emojis(self):
|
||||
from hermes_cli.skin_engine import _build_skin_config
|
||||
data = {
|
||||
"name": "custom",
|
||||
"tool_emojis": {"terminal": "🗡️", "patch": "⚒️"},
|
||||
}
|
||||
skin = _build_skin_config(data)
|
||||
assert skin.tool_emojis == {"terminal": "🗡️", "patch": "⚒️"}
|
||||
|
||||
def test_build_skin_config_empty_tool_emojis_default(self):
|
||||
from hermes_cli.skin_engine import _build_skin_config
|
||||
data = {"name": "minimal"}
|
||||
skin = _build_skin_config(data)
|
||||
assert skin.tool_emojis == {}
|
||||
|
|
@ -612,6 +612,25 @@ class TestBuildApiKwargs:
|
|||
kwargs = agent._build_api_kwargs(messages)
|
||||
assert kwargs["extra_body"]["reasoning"] == {"enabled": False}
|
||||
|
||||
def test_reasoning_not_sent_for_unsupported_openrouter_model(self, agent):
|
||||
agent.model = "minimax/minimax-m2.5"
|
||||
messages = [{"role": "user", "content": "hi"}]
|
||||
kwargs = agent._build_api_kwargs(messages)
|
||||
assert "reasoning" not in kwargs.get("extra_body", {})
|
||||
|
||||
def test_reasoning_sent_for_supported_openrouter_model(self, agent):
|
||||
agent.model = "qwen/qwen3.5-plus-02-15"
|
||||
messages = [{"role": "user", "content": "hi"}]
|
||||
kwargs = agent._build_api_kwargs(messages)
|
||||
assert kwargs["extra_body"]["reasoning"]["effort"] == "medium"
|
||||
|
||||
def test_reasoning_sent_for_nous_route(self, agent):
|
||||
agent.base_url = "https://inference-api.nousresearch.com/v1"
|
||||
agent.model = "minimax/minimax-m2.5"
|
||||
messages = [{"role": "user", "content": "hi"}]
|
||||
kwargs = agent._build_api_kwargs(messages)
|
||||
assert kwargs["extra_body"]["reasoning"]["effort"] == "medium"
|
||||
|
||||
def test_max_tokens_injected(self, agent):
|
||||
agent.max_tokens = 4096
|
||||
messages = [{"role": "user", "content": "hi"}]
|
||||
|
|
@ -942,6 +961,19 @@ class TestHandleMaxIterations:
|
|||
assert "error" in result.lower()
|
||||
assert "API down" in result
|
||||
|
||||
def test_summary_skips_reasoning_for_unsupported_openrouter_model(self, agent):
|
||||
agent.model = "minimax/minimax-m2.5"
|
||||
resp = _mock_response(content="Summary")
|
||||
agent.client.chat.completions.create.return_value = resp
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
messages = [{"role": "user", "content": "do stuff"}]
|
||||
|
||||
result = agent._handle_max_iterations(messages, 60)
|
||||
|
||||
assert result == "Summary"
|
||||
kwargs = agent.client.chat.completions.create.call_args.kwargs
|
||||
assert "reasoning" not in kwargs.get("extra_body", {})
|
||||
|
||||
|
||||
class TestRunConversation:
|
||||
"""Tests for the main run_conversation method.
|
||||
|
|
|
|||
|
|
@ -232,6 +232,48 @@ class TestCheckFnExceptionHandling:
|
|||
assert any(u["name"] == "crashes" for u in unavailable)
|
||||
|
||||
|
||||
class TestEmojiMetadata:
|
||||
"""Verify per-tool emoji registration and lookup."""
|
||||
|
||||
def test_emoji_stored_on_entry(self):
|
||||
reg = ToolRegistry()
|
||||
reg.register(
|
||||
name="t", toolset="s", schema=_make_schema(),
|
||||
handler=_dummy_handler, emoji="🔥",
|
||||
)
|
||||
assert reg._tools["t"].emoji == "🔥"
|
||||
|
||||
def test_get_emoji_returns_registered(self):
|
||||
reg = ToolRegistry()
|
||||
reg.register(
|
||||
name="t", toolset="s", schema=_make_schema(),
|
||||
handler=_dummy_handler, emoji="🎯",
|
||||
)
|
||||
assert reg.get_emoji("t") == "🎯"
|
||||
|
||||
def test_get_emoji_returns_default_when_unset(self):
|
||||
reg = ToolRegistry()
|
||||
reg.register(
|
||||
name="t", toolset="s", schema=_make_schema(),
|
||||
handler=_dummy_handler,
|
||||
)
|
||||
assert reg.get_emoji("t") == "⚡"
|
||||
assert reg.get_emoji("t", default="🔧") == "🔧"
|
||||
|
||||
def test_get_emoji_returns_default_for_unknown_tool(self):
|
||||
reg = ToolRegistry()
|
||||
assert reg.get_emoji("nonexistent") == "⚡"
|
||||
assert reg.get_emoji("nonexistent", default="❓") == "❓"
|
||||
|
||||
def test_emoji_empty_string_treated_as_unset(self):
|
||||
reg = ToolRegistry()
|
||||
reg.register(
|
||||
name="t", toolset="s", schema=_make_schema(),
|
||||
handler=_dummy_handler, emoji="",
|
||||
)
|
||||
assert reg.get_emoji("t") == "⚡"
|
||||
|
||||
|
||||
class TestSecretCaptureResultContract:
|
||||
def test_secret_request_result_does_not_include_secret_value(self):
|
||||
result = {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from unittest.mock import MagicMock
|
|||
import pytest
|
||||
|
||||
from tools.environments.ssh import SSHEnvironment
|
||||
from tools.environments import ssh as ssh_env
|
||||
|
||||
_SSH_HOST = os.getenv("TERMINAL_SSH_HOST", "")
|
||||
_SSH_USER = os.getenv("TERMINAL_SSH_USER", "")
|
||||
|
|
@ -93,6 +94,41 @@ class TestTerminalToolConfig:
|
|||
assert _get_env_config()["ssh_persistent"] is False
|
||||
|
||||
|
||||
class TestSSHPreflight:
|
||||
def test_ensure_ssh_available_raises_clear_error_when_missing(self, monkeypatch):
|
||||
monkeypatch.setattr(ssh_env.shutil, "which", lambda _name: None)
|
||||
|
||||
with pytest.raises(RuntimeError, match="SSH is not installed or not in PATH"):
|
||||
ssh_env._ensure_ssh_available()
|
||||
|
||||
def test_ssh_environment_checks_availability_before_connect(self, monkeypatch):
|
||||
monkeypatch.setattr(ssh_env.shutil, "which", lambda _name: None)
|
||||
monkeypatch.setattr(
|
||||
ssh_env.SSHEnvironment,
|
||||
"_establish_connection",
|
||||
lambda self: pytest.fail("_establish_connection should not run when ssh is missing"),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="openssh-client"):
|
||||
ssh_env.SSHEnvironment(host="example.com", user="alice")
|
||||
|
||||
def test_ssh_environment_connects_when_ssh_exists(self, monkeypatch):
|
||||
called = {"count": 0}
|
||||
|
||||
monkeypatch.setattr(ssh_env.shutil, "which", lambda _name: "/usr/bin/ssh")
|
||||
|
||||
def _fake_establish(self):
|
||||
called["count"] += 1
|
||||
|
||||
monkeypatch.setattr(ssh_env.SSHEnvironment, "_establish_connection", _fake_establish)
|
||||
|
||||
env = ssh_env.SSHEnvironment(host="example.com", user="alice")
|
||||
|
||||
assert called["count"] == 1
|
||||
assert env.host == "example.com"
|
||||
assert env.user == "alice"
|
||||
|
||||
|
||||
def _setup_ssh_env(monkeypatch, persistent: bool):
|
||||
monkeypatch.setenv("TERMINAL_ENV", "ssh")
|
||||
monkeypatch.setenv("TERMINAL_SSH_HOST", _SSH_HOST)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue