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
133 lines
4.3 KiB
Python
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")
|