Spaces:
Running
Running
File size: 7,284 Bytes
715e36f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | import sys
import importlib
from types import ModuleType
from pathlib import Path
import pytest
import reachy_cook.config as config_mod
def _reload_core_tools() -> ModuleType:
"""Reload core_tools after config object has been patched."""
for module_name in list(sys.modules):
if module_name.startswith("reachy_cook.tools."):
sys.modules.pop(module_name, None)
# External file-loaded modules are registered by bare tool name.
sys.modules.pop("ext_ping", None)
sys.modules.pop("sweep_look", None)
sys.modules.pop("ext_dup_a", None)
sys.modules.pop("ext_dup_b", None)
sys.modules.pop("reachy_cook.tools.core_tools", None)
core_tools_mod = importlib.import_module("reachy_cook.tools.core_tools")
core_tools_mod.initialize_tools()
return core_tools_mod
def test_external_profile_can_use_builtin_tools(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""External profile tools.txt can reference built-in src tools."""
profile_name = "ext_profile_test"
external_profiles_root = tmp_path / "external_profiles"
profile_dir = external_profiles_root / profile_name
profile_dir.mkdir(parents=True)
(profile_dir / "instructions.txt").write_text("hello\n", encoding="utf-8")
(profile_dir / "tools.txt").write_text("dance\n", encoding="utf-8")
monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", profile_name)
monkeypatch.setattr(config_mod.config, "PROFILES_DIRECTORY", external_profiles_root)
monkeypatch.setattr(config_mod.config, "TOOLS_DIRECTORY", None)
monkeypatch.setattr(config_mod.config, "AUTOLOAD_EXTERNAL_TOOLS", False)
core_tools_mod = _reload_core_tools()
assert "dance" in core_tools_mod.ALL_TOOLS
assert "dance" not in sys.modules
def test_external_tools_can_be_loaded_without_external_profile(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""External tools can be loaded with built-in profile via autoload mode."""
external_tools_root = tmp_path / "external_tools"
external_tools_root.mkdir(parents=True)
(external_tools_root / "ext_ping.py").write_text(
"\n".join(
[
"from typing import Any, Dict",
"from reachy_cook.tools.core_tools import Tool, ToolDependencies",
"",
"class ExtPingTool(Tool):",
' name = "ext_ping"',
' description = "External ping tool"',
' parameters_schema = {"type": "object", "properties": {}, "required": []}',
"",
" async def __call__(self, deps: ToolDependencies, **kwargs: Any) -> Dict[str, Any]:",
' return {"status": "ok"}',
"",
]
),
encoding="utf-8",
)
monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", "default")
monkeypatch.setattr(config_mod.config, "PROFILES_DIRECTORY", config_mod.DEFAULT_PROFILES_DIRECTORY)
monkeypatch.setattr(config_mod.config, "TOOLS_DIRECTORY", external_tools_root)
monkeypatch.setattr(config_mod.config, "AUTOLOAD_EXTERNAL_TOOLS", True)
core_tools_mod = _reload_core_tools()
assert "ext_ping" in core_tools_mod.ALL_TOOLS
def test_external_tools_fail_on_duplicate_tool_names(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Loading must fail if multiple tools declare the same Tool.name."""
external_tools_root = tmp_path / "external_tools"
external_tools_root.mkdir(parents=True)
duplicate_tool_source = "\n".join(
[
"from typing import Any, Dict",
"from reachy_cook.tools.core_tools import Tool, ToolDependencies",
"",
"class DupTool(Tool):",
' name = "dup_tool"',
' description = "Duplicate tool name"',
' parameters_schema = {"type": "object", "properties": {}, "required": []}',
"",
" async def __call__(self, deps: ToolDependencies, **kwargs: Any) -> Dict[str, Any]:",
' return {"status": "ok"}',
"",
]
)
(external_tools_root / "ext_dup_a.py").write_text(duplicate_tool_source, encoding="utf-8")
(external_tools_root / "ext_dup_b.py").write_text(duplicate_tool_source, encoding="utf-8")
monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", "default")
monkeypatch.setattr(config_mod.config, "PROFILES_DIRECTORY", config_mod.DEFAULT_PROFILES_DIRECTORY)
monkeypatch.setattr(config_mod.config, "TOOLS_DIRECTORY", external_tools_root)
monkeypatch.setattr(config_mod.config, "AUTOLOAD_EXTERNAL_TOOLS", True)
with pytest.raises(RuntimeError, match="Duplicate Tool.name values detected"):
_reload_core_tools()
def test_builtin_profile_can_load_profile_local_tools(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Built-in profile-local tools should load from the packaged profiles root."""
monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", "default")
monkeypatch.setattr(config_mod.config, "PROFILES_DIRECTORY", config_mod.DEFAULT_PROFILES_DIRECTORY)
monkeypatch.setattr(config_mod.config, "TOOLS_DIRECTORY", None)
monkeypatch.setattr(config_mod.config, "AUTOLOAD_EXTERNAL_TOOLS", False)
core_tools_mod = _reload_core_tools()
assert "sweep_look" in core_tools_mod.ALL_TOOLS
def test_tool_registry_reloads_when_profile_changes(monkeypatch: pytest.MonkeyPatch) -> None:
"""Runtime profile changes should refresh enabled tools without restarting Python."""
monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", "default")
monkeypatch.setattr(config_mod.config, "PROFILES_DIRECTORY", config_mod.DEFAULT_PROFILES_DIRECTORY)
monkeypatch.setattr(config_mod.config, "TOOLS_DIRECTORY", None)
monkeypatch.setattr(config_mod.config, "AUTOLOAD_EXTERNAL_TOOLS", False)
core_tools_mod = _reload_core_tools()
initial_tool_names = {spec["name"] for spec in core_tools_mod.get_tool_specs()}
assert "sweep_look" in initial_tool_names
assert "camera" in initial_tool_names
monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", "mars_rover")
reloaded_tool_names = {spec["name"] for spec in core_tools_mod.get_tool_specs()}
assert "camera" in reloaded_tool_names
assert "move_head" in reloaded_tool_names
assert "sweep_look" not in reloaded_tool_names
assert "camera" in core_tools_mod.ALL_TOOLS
assert "sweep_look" not in core_tools_mod.ALL_TOOLS
def test_forced_tool_registry_reload_does_not_duplicate_profile_local_tool(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Reloading a profile-local tool should ignore stale class objects from the previous load."""
monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", "default")
monkeypatch.setattr(config_mod.config, "PROFILES_DIRECTORY", config_mod.DEFAULT_PROFILES_DIRECTORY)
monkeypatch.setattr(config_mod.config, "TOOLS_DIRECTORY", None)
monkeypatch.setattr(config_mod.config, "AUTOLOAD_EXTERNAL_TOOLS", False)
core_tools_mod = _reload_core_tools()
core_tools_mod.initialize_tools(force=True)
assert "sweep_look" in core_tools_mod.ALL_TOOLS
|