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
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user