Files
hope-voice-api/tests/test_mcp_tools.py
T
Shane a1d59580f7 fix: ChatContext is not a sequence — use .items for len/list; add test suite
Bug fix:
- GemmaLLM.chat(): len(chat_ctx) → len(chat_ctx.items)
- GemmaLLM._compact_context(): list(chat_ctx) → list(chat_ctx.items)
  ChatContext in livekit-agents 1.7 is not iterable or sized directly;
  it exposes an .items list. The TypeError was silently killing all LLM
  responses (agent heard user but never spoke back).

Test suite (tests/):
- test_web_ui.py: static assets, token endpoint, LiveKit WS proxy
- test_llm_api.py: completion, tool calling, thinking-disabled latency, streaming
- test_mcp_tools.py: weather (Fahrenheit), time, memory, skills — all via
  docker exec + in-container MCP client
- test_agent_integration.py: process alive, worker registered, supervisord,
  container health, LiveKit API
- test_compaction.py: size-triggered compaction, system prompt preservation,
  short-context no-op
- conftest.py: shared fixtures (HTTPS client, token, LLM, docker exec helpers)
- Run with: .venv-tests/bin/python -m pytest tests/ -v
2026-08-22 17:09:29 -04:00

133 lines
4.3 KiB
Python

"""Tests for the MCP tool servers (weather, memory, skills) via docker exec.
Each test spawns the real MCP server script inside the container and drives it
with a JSON-RPC session using the mcp client library from the agent venv.
"""
import re
import pytest
def _extract_number(text: str) -> float | None:
"""Pull the first plausible temperature number out of a weather summary."""
m = re.search(r"(\d+)\s*degrees", text)
if m:
return float(m.group(1))
m = re.search(r"\b(\d{2,3})\b", text)
return float(m.group(1)) if m else None
@pytest.mark.slow
def test_weather_get(mcp_client_factory):
text, is_error = mcp_client_factory("weather_mcp.py", "get_weather", {"location": "Raleigh"})
assert not is_error
assert "degrees" in text.lower()
@pytest.mark.slow
def test_weather_fahrenheit(mcp_client_factory):
"""wttr.in reports Celsius; the tool must convert to Fahrenheit."""
text, is_error = mcp_client_factory("weather_mcp.py", "get_weather", {"location": "Raleigh"})
assert not is_error
temp = _extract_number(text)
assert temp is not None, f"no temperature found in: {text}"
# Fahrenheit for any inhabited place is > 50 (Celsius would be ~10-30).
assert temp > 50, f"temperature {temp} looks like Celsius, expected Fahrenheit"
@pytest.mark.slow
def test_time_get(mcp_client_factory):
text, is_error = mcp_client_factory("weather_mcp.py", "get_time", {})
assert not is_error
assert ("AM" in text or "PM" in text)
days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
assert any(d in text for d in days), f"no day name in: {text}"
@pytest.mark.slow
def test_memory_save_and_recall(mcp_client_factory):
topic = "test-suite-probe"
content = "zebra42 is the probe fact for the voice test suite"
saved, err = mcp_client_factory(
"memory_mcp.py", "memory_save", {"topic": topic, "content": content}
)
assert not err
assert "Saved" in saved
try:
recalled, err = mcp_client_factory("memory_mcp.py", "memory_recall", {"query": "zebra42"})
assert not err
assert "zebra42" in recalled
assert topic in recalled # recall output is tagged with the topic name
finally:
# Clean up: remove the probe file so repeated runs stay green.
from conftest import docker_exec
docker_exec("rm", "-f", "/memory/test-suite-probe.md")
def test_memory_list(mcp_client_factory):
from conftest import docker_exec
topic = "test-suite-list-probe"
try:
saved, err = mcp_client_factory(
"memory_mcp.py", "memory_save", {"topic": topic, "content": "listing probe"}
)
assert not err
listed, err = mcp_client_factory("memory_mcp.py", "memory_list", {})
assert not err
assert topic in listed
finally:
docker_exec("rm", "-f", "/memory/test-suite-list-probe.md")
@pytest.mark.slow
def test_skill_save_and_recall(mcp_client_factory):
name = "Test Suite Probe Skill"
description = "A probe skill used by the voice test suite."
steps = "Step 1: do the thing. Step 2: verify zebra42 was done."
saved, err = mcp_client_factory(
"skills_mcp.py",
"skill_save",
{"name": name, "description": description, "steps": steps},
)
assert not err
assert "Saved" in saved
try:
recalled, err = mcp_client_factory("skills_mcp.py", "skill_recall", {"query": "zebra42"})
assert not err
assert "zebra42" in recalled
assert "Steps" in recalled
finally:
from conftest import docker_exec
docker_exec("rm", "-f", "/skills/test-suite-probe-skill.md")
def test_skill_list(mcp_client_factory):
from conftest import docker_exec
name = "Test Suite List Skill"
try:
saved, err = mcp_client_factory(
"skills_mcp.py",
"skill_save",
{
"name": name,
"description": "listing probe skill",
"steps": "one step",
},
)
assert not err
listed, err = mcp_client_factory("skills_mcp.py", "skill_list", {})
assert not err
# list output uses the slugified filename
assert "test-suite-list-skill" in listed
finally:
docker_exec("rm", "-f", "/skills/test-suite-list-skill.md")