- Move agent code from backend/src/agent/ to agent/src/ss_tools/agent/ - Extract shared stdlib-only utilities to shared/src/ss_tools/shared/ - Add #region/#endregion contracts to all ~140 functions (INV_1 compliance) - Update docker files, entrypoint, build scripts for new package layout - Backend now imports ss_tools.shared._llm_health (no gradio/langchain deps) - Add specs for 036-039 feature plans
135 lines
6.5 KiB
Python
135 lines
6.5 KiB
Python
# #region Test.IntentKeyword.Edges [C:2] [TYPE Module] [SEMANTICS test,metadata,invariants]
|
|
# @BRIEF Metadata invariant tests for agent tools — tool catalog consistency, docstring coverage.
|
|
# Keyword matching tests removed (LLM handles intent detection via LangGraph tool-calling).
|
|
# @RELATION BINDS_TO -> [AgentChat.Tools]
|
|
# @TEST_EDGE: all_tools_registered -> get_all_tools() returns consistent list
|
|
# @TEST_EDGE: docstrings_present -> every tool has a non-empty docstring
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# A — Tool catalog consistency
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
class TestToolCatalog:
|
|
"""Verify tool catalog is internally consistent."""
|
|
|
|
def test_get_all_tools_returns_all_24(self):
|
|
"""get_all_tools() returns at least 24 tools."""
|
|
with patch("ss_tools.agent.tools.logger", MagicMock()):
|
|
from ss_tools.agent.tools import get_all_tools
|
|
tools = get_all_tools()
|
|
tool_names = {t.name for t in tools}
|
|
assert len(tools) >= 24, f"Expected ≥24 tools, got {len(tools)}: {tool_names}"
|
|
# Core tools must be present
|
|
assert "show_capabilities" in tool_names
|
|
assert "search_dashboards" in tool_names
|
|
assert "get_health_summary" in tool_names
|
|
assert "start_maintenance" in tool_names
|
|
|
|
def test_tool_names_are_unique(self):
|
|
"""No duplicate tool names in get_all_tools()."""
|
|
with patch("ss_tools.agent.tools.logger", MagicMock()):
|
|
from ss_tools.agent.tools import get_all_tools
|
|
tools = get_all_tools()
|
|
names = [t.name for t in tools]
|
|
assert len(names) == len(set(names)), f"Duplicate tool names: {names}"
|
|
|
|
def test_every_tool_has_docstring(self):
|
|
"""Every tool MUST have a docstring — enforced by LangChain but verified here."""
|
|
with patch("ss_tools.agent.tools.logger", MagicMock()):
|
|
from ss_tools.agent.tools import get_all_tools
|
|
for t in get_all_tools():
|
|
desc = (t.description or "").strip()
|
|
assert desc, f"Tool '{t.name}' has empty description. LangChain @tool requires a docstring."
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# B — Embedding router metadata
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
class TestEmbeddingMetadata:
|
|
"""Verify embedding router is consistent with tool catalog."""
|
|
|
|
def test_embedding_top_k_returns_empty_for_empty_query(self):
|
|
"""Empty query → empty result."""
|
|
try:
|
|
from ss_tools.agent._embedding_router import embedding_top_k
|
|
except ImportError:
|
|
pytest.skip("_embedding_router module not importable")
|
|
assert embedding_top_k("") == []
|
|
|
|
def test_embedding_is_available_returns_bool(self):
|
|
"""embedding_is_available returns bool, does not raise."""
|
|
try:
|
|
from ss_tools.agent._embedding_router import embedding_is_available
|
|
except ImportError:
|
|
pytest.skip("_embedding_router module not importable")
|
|
result = embedding_is_available()
|
|
assert isinstance(result, bool)
|
|
|
|
def test_get_descriptions_matches_all_tools(self):
|
|
"""_get_descriptions() covers every tool in get_all_tools() 1:1."""
|
|
with patch("ss_tools.agent.tools.logger", MagicMock()):
|
|
from ss_tools.agent.tools import get_all_tools
|
|
try:
|
|
from ss_tools.agent._embedding_router import _get_descriptions
|
|
except ImportError:
|
|
pytest.skip("_embedding_router module not importable")
|
|
|
|
descs, names = _get_descriptions()
|
|
tool_names = {t.name for t in get_all_tools()}
|
|
|
|
assert set(names) == tool_names, (
|
|
f"Mismatch: descriptions={set(names) - tool_names}, tools={tool_names - set(names)}"
|
|
)
|
|
assert len(descs) == len(tool_names), f"Expected {len(tool_names)} descriptions, got {len(descs)}"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# C — Tool resolver utilities (kept functions)
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
class TestToolResolverUtils:
|
|
"""Utility functions in _tool_resolver still work."""
|
|
|
|
def test_normalize_tool_args_handles_dict(self):
|
|
from ss_tools.agent._tool_resolver import normalize_tool_args
|
|
assert normalize_tool_args({"key": "value"}) == {"key": "value"}
|
|
|
|
def test_normalize_tool_args_handles_none(self):
|
|
from ss_tools.agent._tool_resolver import normalize_tool_args
|
|
assert normalize_tool_args(None) == {}
|
|
|
|
def test_coerce_tool_call_from_dict(self):
|
|
from ss_tools.agent._tool_resolver import coerce_tool_call
|
|
name, args = coerce_tool_call({"name": "test_tool", "args": {"x": 1}})
|
|
assert name == "test_tool"
|
|
assert args == {"x": 1}
|
|
|
|
def test_extract_tool_call_from_state_empty(self):
|
|
from ss_tools.agent._tool_resolver import extract_tool_call_from_state
|
|
from unittest.mock import MagicMock
|
|
state = MagicMock()
|
|
state.values = {"messages": []}
|
|
state.next = None
|
|
name, args = extract_tool_call_from_state(state)
|
|
assert name is None
|
|
assert args == {}
|
|
|
|
def test_known_agent_tool_names(self):
|
|
from ss_tools.agent._tool_resolver import known_agent_tool_names
|
|
names = known_agent_tool_names()
|
|
assert "show_capabilities" in names
|
|
assert "search_dashboards" in names
|
|
assert len(names) >= 24
|
|
|
|
|
|
# #endregion Test.IntentKeyword.Edges
|