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
This commit is contained in:
82
backend/tests/unit/test_agents.py
Normal file
82
backend/tests/unit/test_agents.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Tests for agent tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.agents.fallback import fallback_respond
|
||||
from app.agents.order_lookup import get_order_status, get_tracking_info
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestOrderLookup:
|
||||
def test_get_order_status_existing(self) -> None:
|
||||
result = get_order_status.invoke({"order_id": "1042"})
|
||||
assert result["order_id"] == "1042"
|
||||
assert result["status"] == "shipped"
|
||||
|
||||
def test_get_order_status_not_found(self) -> None:
|
||||
result = get_order_status.invoke({"order_id": "9999"})
|
||||
assert "error" in result
|
||||
assert "9999" in result["error"]
|
||||
|
||||
def test_get_tracking_info_existing(self) -> None:
|
||||
result = get_tracking_info.invoke({"order_id": "1042"})
|
||||
assert result["carrier"] == "FedEx"
|
||||
assert result["tracking_number"] == "FX-9876543210"
|
||||
|
||||
def test_get_tracking_info_not_found(self) -> None:
|
||||
result = get_tracking_info.invoke({"order_id": "1043"})
|
||||
assert "error" in result
|
||||
|
||||
def test_all_mock_orders_have_required_fields(self) -> None:
|
||||
from app.agents.order_lookup import MOCK_ORDERS
|
||||
|
||||
for oid, order in MOCK_ORDERS.items():
|
||||
assert "order_id" in order
|
||||
assert "status" in order
|
||||
assert order["order_id"] == oid
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestFallback:
|
||||
def test_fallback_respond_returns_help(self) -> None:
|
||||
result = fallback_respond.invoke({"query": "random question"})
|
||||
assert "order" in result.lower()
|
||||
assert "help" in result.lower() or "can do" in result.lower()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestOrderActions:
|
||||
def test_cancel_order_approved(self) -> None:
|
||||
with patch("app.agents.order_actions.interrupt", return_value=True):
|
||||
from app.agents.order_actions import cancel_order
|
||||
|
||||
result = cancel_order.invoke({"order_id": "1042"})
|
||||
assert result["status"] == "cancelled"
|
||||
assert "1042" in result["message"]
|
||||
|
||||
def test_cancel_order_rejected(self) -> None:
|
||||
with patch("app.agents.order_actions.interrupt", return_value=False):
|
||||
from app.agents.order_actions import cancel_order
|
||||
|
||||
result = cancel_order.invoke({"order_id": "1042"})
|
||||
assert result["status"] == "kept"
|
||||
assert "declined" in result["message"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestToolBridge:
|
||||
def test_get_tools_by_names(self) -> None:
|
||||
from app.agents import get_tools_by_names
|
||||
|
||||
tools = get_tools_by_names(["get_order_status", "cancel_order"])
|
||||
assert len(tools) == 2
|
||||
|
||||
def test_unknown_tool_raises(self) -> None:
|
||||
from app.agents import get_tools_by_names
|
||||
|
||||
with pytest.raises(ValueError, match="Unknown tool"):
|
||||
get_tools_by_names(["nonexistent_tool"])
|
||||
Reference in New Issue
Block a user