Files
smart-support/backend/tests/unit/test_registry.py
Yaojia Wang 33488fd634 feat: complete phase 1 -- core framework with chat loop, agents, and React UI
Backend:
- FastAPI WebSocket /ws endpoint with streaming via LangGraph astream
- LangGraph Supervisor connecting 3 mock agents (order_lookup, order_actions, fallback)
- YAML Agent Registry with Pydantic validation and immutable configs
- PostgresSaver checkpoint persistence via langgraph-checkpoint-postgres
- Session TTL with 30-min sliding window and interrupt extension
- LLM provider abstraction (Anthropic/OpenAI/Google)
- Token usage + cost tracking callback handler
- Input validation: message size cap, thread_id format, content length
- Security: no hardcoded defaults, startup API key validation, no input reflection

Frontend:
- React 19 + TypeScript + Vite chat UI
- WebSocket hook with reconnect + exponential backoff
- Streaming token display with agent attribution
- Interrupt approval/reject UI for write operations
- Collapsible tool call viewer

Testing:
- 87 unit tests, 87% coverage (exceeds 80% requirement)
- Ruff lint + format clean

Infrastructure:
- Docker Compose (PostgreSQL 16 + backend)
- pyproject.toml with full dependency management
2026-03-30 00:54:21 +02:00

148 lines
5.5 KiB
Python

"""Tests for app.registry module."""
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
import yaml
from app.registry import AgentConfig, AgentRegistry, PersonalityConfig
if TYPE_CHECKING:
from pathlib import Path
@pytest.mark.unit
class TestPersonalityConfig:
def test_defaults(self) -> None:
p = PersonalityConfig()
assert p.tone == "professional and helpful"
assert "Hello" in p.greeting
assert "human agent" in p.escalation_message
def test_custom_values(self) -> None:
p = PersonalityConfig(tone="casual", greeting="Hey!", escalation_message="Hold on.")
assert p.tone == "casual"
def test_immutable(self) -> None:
p = PersonalityConfig()
with pytest.raises(Exception):
p.tone = "new tone"
@pytest.mark.unit
class TestAgentConfig:
def test_valid_config(self) -> None:
ac = AgentConfig(
name="test",
description="A test agent",
permission="read",
tools=["tool1"],
)
assert ac.name == "test"
assert ac.permission == "read"
def test_empty_name_rejected(self) -> None:
with pytest.raises(ValueError, match="must not be empty"):
AgentConfig(name=" ", description="d", permission="read", tools=["t"])
def test_empty_tools_rejected(self) -> None:
with pytest.raises(ValueError, match="at least one tool"):
AgentConfig(name="x", description="d", permission="read", tools=[])
def test_invalid_permission_rejected(self) -> None:
with pytest.raises(Exception):
AgentConfig(name="x", description="d", permission="admin", tools=["t"])
def test_name_stripped(self) -> None:
ac = AgentConfig(name=" test ", description="d", permission="read", tools=["t"])
assert ac.name == "test"
def test_immutable(self) -> None:
ac = AgentConfig(name="test", description="d", permission="read", tools=["t"])
with pytest.raises(Exception):
ac.name = "new"
@pytest.mark.unit
class TestAgentRegistry:
def test_load_valid_yaml(self, sample_yaml_path: Path) -> None:
registry = AgentRegistry.load(sample_yaml_path)
assert len(registry) == 3
assert registry.get_agent("test_reader").permission == "read"
assert registry.get_agent("test_writer").permission == "write"
def test_list_agents(self, sample_registry: AgentRegistry) -> None:
agents = sample_registry.list_agents()
assert len(agents) == 3
names = {a.name for a in agents}
assert names == {"test_reader", "test_writer", "test_fallback"}
def test_get_agents_by_permission(self, sample_registry: AgentRegistry) -> None:
readers = sample_registry.get_agents_by_permission("read")
assert len(readers) == 2
writers = sample_registry.get_agents_by_permission("write")
assert len(writers) == 1
def test_get_nonexistent_agent(self, sample_registry: AgentRegistry) -> None:
with pytest.raises(KeyError, match="not found"):
sample_registry.get_agent("nonexistent")
def test_personality_defaults_applied(self, sample_registry: AgentRegistry) -> None:
agent = sample_registry.get_agent("test_reader")
assert agent.personality.tone == "professional and helpful"
def test_personality_custom_applied(self, sample_registry: AgentRegistry) -> None:
agent = sample_registry.get_agent("test_writer")
assert agent.personality.tone == "formal"
assert agent.personality.greeting == "Greetings."
def test_file_not_found(self) -> None:
with pytest.raises(FileNotFoundError):
AgentRegistry.load("/nonexistent/path.yaml")
def test_empty_file(self, tmp_path: Path) -> None:
path = tmp_path / "empty.yaml"
path.write_text("", encoding="utf-8")
with pytest.raises(ValueError, match="empty"):
AgentRegistry.load(path)
def test_invalid_yaml_syntax(self, tmp_path: Path) -> None:
path = tmp_path / "bad.yaml"
path.write_text("agents:\n - name: [invalid\n", encoding="utf-8")
with pytest.raises(ValueError, match="Invalid YAML"):
AgentRegistry.load(path)
def test_missing_agents_key(self, tmp_path: Path) -> None:
path = tmp_path / "no_agents.yaml"
path.write_text(yaml.dump({"items": []}), encoding="utf-8")
with pytest.raises(ValueError, match="agents"):
AgentRegistry.load(path)
def test_duplicate_agent_names(self, tmp_path: Path) -> None:
data = {
"agents": [
{"name": "dup", "description": "a", "permission": "read", "tools": ["t1"]},
{"name": "dup", "description": "b", "permission": "read", "tools": ["t2"]},
]
}
path = tmp_path / "dups.yaml"
path.write_text(yaml.dump(data), encoding="utf-8")
with pytest.raises(ValueError, match="Duplicate"):
AgentRegistry.load(path)
def test_missing_required_fields(self, tmp_path: Path) -> None:
data = {"agents": [{"name": "x"}]}
path = tmp_path / "missing.yaml"
path.write_text(yaml.dump(data), encoding="utf-8")
with pytest.raises(ValueError, match="Invalid agent config"):
AgentRegistry.load(path)
def test_empty_agents_list(self, tmp_path: Path) -> None:
data = {"agents": []}
path = tmp_path / "empty_list.yaml"
path.write_text(yaml.dump(data), encoding="utf-8")
with pytest.raises(ValueError, match="non-empty"):
AgentRegistry.load(path)