fix: Restore all files lost during destructive rebase

A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,3 @@
"""
Tests for Breakpilot Agent Core
"""

View File

@@ -0,0 +1,57 @@
"""
Pytest configuration and fixtures for agent-core tests
"""
import pytest
import asyncio
import sys
from pathlib import Path
# Add agent-core to path
sys.path.insert(0, str(Path(__file__).parent.parent))
@pytest.fixture(scope="session")
def event_loop():
"""Create an instance of the default event loop for each test case."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture
def mock_redis():
"""Mock Redis client for testing"""
from unittest.mock import AsyncMock, MagicMock
redis = AsyncMock()
redis.get = AsyncMock(return_value=None)
redis.set = AsyncMock(return_value=True)
redis.setex = AsyncMock(return_value=True)
redis.delete = AsyncMock(return_value=True)
redis.keys = AsyncMock(return_value=[])
redis.publish = AsyncMock(return_value=1)
redis.pubsub = MagicMock()
return redis
@pytest.fixture
def mock_db_pool():
"""Mock PostgreSQL pool for testing"""
from unittest.mock import AsyncMock, MagicMock
from contextlib import asynccontextmanager
pool = AsyncMock()
@asynccontextmanager
async def acquire():
conn = AsyncMock()
conn.fetch = AsyncMock(return_value=[])
conn.fetchrow = AsyncMock(return_value=None)
conn.execute = AsyncMock(return_value="UPDATE 0")
yield conn
pool.acquire = acquire
return pool

View File

@@ -0,0 +1,201 @@
"""
Tests for Heartbeat Monitoring
Tests cover:
- Heartbeat registration and updates
- Timeout detection
- Pause/resume functionality
- Status reporting
"""
import pytest
import asyncio
from datetime import datetime, timezone, timedelta
from unittest.mock import AsyncMock
import sys
sys.path.insert(0, str(__file__).rsplit('/tests/', 1)[0])
from sessions.heartbeat import HeartbeatMonitor, HeartbeatClient, HeartbeatEntry
class TestHeartbeatMonitor:
"""Tests for HeartbeatMonitor"""
@pytest.fixture
def monitor(self):
"""Create a heartbeat monitor"""
return HeartbeatMonitor(
timeout_seconds=5,
check_interval_seconds=1,
max_missed_beats=2
)
def test_register_session(self, monitor):
"""Should register session for monitoring"""
monitor.register("session-1", "tutor-agent")
assert "session-1" in monitor.sessions
assert monitor.sessions["session-1"].agent_type == "tutor-agent"
def test_beat_updates_timestamp(self, monitor):
"""Beat should update last_beat timestamp"""
monitor.register("session-1", "agent")
original = monitor.sessions["session-1"].last_beat
import time
time.sleep(0.01)
result = monitor.beat("session-1")
assert result is True
assert monitor.sessions["session-1"].last_beat > original
assert monitor.sessions["session-1"].missed_beats == 0
def test_beat_nonexistent_session(self, monitor):
"""Beat should return False for unregistered session"""
result = monitor.beat("nonexistent")
assert result is False
def test_unregister_session(self, monitor):
"""Should unregister session from monitoring"""
monitor.register("session-1", "agent")
result = monitor.unregister("session-1")
assert result is True
assert "session-1" not in monitor.sessions
def test_pause_session(self, monitor):
"""Should pause monitoring for session"""
monitor.register("session-1", "agent")
result = monitor.pause("session-1")
assert result is True
assert "session-1" in monitor._paused_sessions
def test_resume_session(self, monitor):
"""Should resume monitoring for paused session"""
monitor.register("session-1", "agent")
monitor.pause("session-1")
result = monitor.resume("session-1")
assert result is True
assert "session-1" not in monitor._paused_sessions
def test_get_status(self, monitor):
"""Should return session status"""
monitor.register("session-1", "tutor-agent")
status = monitor.get_status("session-1")
assert status is not None
assert status["session_id"] == "session-1"
assert status["agent_type"] == "tutor-agent"
assert status["is_healthy"] is True
assert status["is_paused"] is False
def test_get_status_nonexistent(self, monitor):
"""Should return None for nonexistent session"""
status = monitor.get_status("nonexistent")
assert status is None
def test_get_all_status(self, monitor):
"""Should return status for all sessions"""
monitor.register("session-1", "agent-1")
monitor.register("session-2", "agent-2")
all_status = monitor.get_all_status()
assert len(all_status) == 2
assert "session-1" in all_status
assert "session-2" in all_status
def test_registered_count(self, monitor):
"""Should return correct registered count"""
assert monitor.registered_count == 0
monitor.register("s1", "a")
monitor.register("s2", "a")
assert monitor.registered_count == 2
def test_healthy_count(self, monitor):
"""Should return correct healthy count"""
monitor.register("s1", "a")
monitor.register("s2", "a")
# Both should be healthy initially
assert monitor.healthy_count == 2
# Simulate missed beat
monitor.sessions["s1"].missed_beats = 1
assert monitor.healthy_count == 1
class TestHeartbeatClient:
"""Tests for HeartbeatClient"""
@pytest.fixture
def monitor(self):
"""Create a monitor for the client"""
return HeartbeatMonitor(timeout_seconds=5)
def test_client_creation(self, monitor):
"""Client should be created with correct settings"""
client = HeartbeatClient(
session_id="session-1",
monitor=monitor,
interval_seconds=2
)
assert client.session_id == "session-1"
assert client.interval == 2
assert client._running is False
@pytest.mark.asyncio
async def test_client_start_stop(self, monitor):
"""Client should start and stop correctly"""
monitor.register("session-1", "agent")
client = HeartbeatClient(
session_id="session-1",
monitor=monitor,
interval_seconds=1
)
await client.start()
assert client._running is True
await asyncio.sleep(0.1)
await client.stop()
assert client._running is False
@pytest.mark.asyncio
async def test_client_context_manager(self, monitor):
"""Client should work as context manager"""
monitor.register("session-1", "agent")
async with HeartbeatClient("session-1", monitor, 1) as client:
assert client._running is True
assert client._running is False
class TestHeartbeatEntry:
"""Tests for HeartbeatEntry dataclass"""
def test_entry_creation(self):
"""Entry should be created with correct values"""
entry = HeartbeatEntry(
session_id="session-1",
agent_type="tutor-agent",
last_beat=datetime.now(timezone.utc)
)
assert entry.session_id == "session-1"
assert entry.agent_type == "tutor-agent"
assert entry.missed_beats == 0

View File

@@ -0,0 +1,207 @@
"""
Tests for Memory Store
Tests cover:
- Memory storage and retrieval
- TTL expiration
- Access counting
- Pattern-based search
"""
import pytest
import asyncio
from datetime import datetime, timezone, timedelta
import sys
sys.path.insert(0, str(__file__).rsplit('/tests/', 1)[0])
from brain.memory_store import MemoryStore, Memory
class TestMemory:
"""Tests for Memory dataclass"""
def test_memory_creation(self):
"""Memory should be created with correct values"""
memory = Memory(
key="test:key",
value={"data": "value"},
agent_id="tutor-agent"
)
assert memory.key == "test:key"
assert memory.value["data"] == "value"
assert memory.agent_id == "tutor-agent"
assert memory.access_count == 0
assert memory.expires_at is None
def test_memory_with_expiration(self):
"""Memory should track expiration"""
expires = datetime.now(timezone.utc) + timedelta(days=30)
memory = Memory(
key="temp:data",
value="temporary",
agent_id="agent",
expires_at=expires
)
assert memory.expires_at == expires
assert memory.is_expired() is False
def test_memory_expired(self):
"""Should detect expired memory"""
expires = datetime.now(timezone.utc) - timedelta(hours=1)
memory = Memory(
key="old:data",
value="expired",
agent_id="agent",
expires_at=expires
)
assert memory.is_expired() is True
def test_memory_serialization(self):
"""Memory should serialize correctly"""
memory = Memory(
key="test",
value={"nested": {"data": [1, 2, 3]}},
agent_id="test-agent",
metadata={"source": "unit_test"}
)
data = memory.to_dict()
restored = Memory.from_dict(data)
assert restored.key == memory.key
assert restored.value == memory.value
assert restored.agent_id == memory.agent_id
assert restored.metadata == memory.metadata
class TestMemoryStore:
"""Tests for MemoryStore"""
@pytest.fixture
def store(self):
"""Create a memory store without persistence"""
return MemoryStore(
redis_client=None,
db_pool=None,
namespace="test"
)
@pytest.mark.asyncio
async def test_remember_and_recall(self, store):
"""Should store and retrieve values"""
await store.remember(
key="math:formula",
value={"name": "pythagorean", "formula": "a² + b² = c²"},
agent_id="tutor-agent"
)
value = await store.recall("math:formula")
assert value is not None
assert value["name"] == "pythagorean"
@pytest.mark.asyncio
async def test_recall_nonexistent(self, store):
"""Should return None for nonexistent key"""
value = await store.recall("nonexistent:key")
assert value is None
@pytest.mark.asyncio
async def test_get_memory(self, store):
"""Should retrieve full Memory object"""
await store.remember(
key="test:memory",
value="test value",
agent_id="test-agent",
metadata={"category": "test"}
)
memory = await store.get_memory("test:memory")
assert memory is not None
assert memory.key == "test:memory"
assert memory.agent_id == "test-agent"
assert memory.access_count >= 1
@pytest.mark.asyncio
async def test_forget(self, store):
"""Should delete memory"""
await store.remember(
key="temporary",
value="will be deleted",
agent_id="agent"
)
result = await store.forget("temporary")
assert result is True
value = await store.recall("temporary")
assert value is None
@pytest.mark.asyncio
async def test_search_pattern(self, store):
"""Should search by pattern"""
await store.remember("eval:math:1", {"score": 80}, "grader")
await store.remember("eval:math:2", {"score": 90}, "grader")
await store.remember("eval:english:1", {"score": 85}, "grader")
math_results = await store.search("eval:math:*")
assert len(math_results) == 2
@pytest.mark.asyncio
async def test_get_by_agent(self, store):
"""Should filter by agent ID"""
await store.remember("data:1", "value1", "agent-1")
await store.remember("data:2", "value2", "agent-2")
await store.remember("data:3", "value3", "agent-1")
agent1_memories = await store.get_by_agent("agent-1")
assert len(agent1_memories) == 2
@pytest.mark.asyncio
async def test_get_recent(self, store):
"""Should get recently created memories"""
await store.remember("new:1", "value1", "agent")
await store.remember("new:2", "value2", "agent")
recent = await store.get_recent(hours=1)
assert len(recent) == 2
@pytest.mark.asyncio
async def test_access_count_increment(self, store):
"""Should increment access count on recall"""
await store.remember("counting", "value", "agent")
# Access multiple times
await store.recall("counting")
await store.recall("counting")
await store.recall("counting")
memory = await store.get_memory("counting")
assert memory.access_count >= 3
@pytest.mark.asyncio
async def test_cleanup_expired(self, store):
"""Should clean up expired memories"""
# Create an expired memory manually
expired_memory = Memory(
key="expired",
value="old data",
agent_id="agent",
expires_at=datetime.now(timezone.utc) - timedelta(hours=1)
)
store._local_cache["expired"] = expired_memory
count = await store.cleanup_expired()
assert count == 1
assert "expired" not in store._local_cache

View File

@@ -0,0 +1,224 @@
"""
Tests for Message Bus
Tests cover:
- Message publishing and subscription
- Request-response pattern
- Message priority
- Local delivery (without Redis)
"""
import pytest
import asyncio
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock
import sys
sys.path.insert(0, str(__file__).rsplit('/tests/', 1)[0])
from orchestrator.message_bus import (
MessageBus,
AgentMessage,
MessagePriority,
MessageType,
)
class TestAgentMessage:
"""Tests for AgentMessage dataclass"""
def test_message_creation_defaults(self):
"""Message should have default values"""
message = AgentMessage(
sender="agent-1",
receiver="agent-2",
message_type="test",
payload={"data": "value"}
)
assert message.sender == "agent-1"
assert message.receiver == "agent-2"
assert message.priority == MessagePriority.NORMAL
assert message.correlation_id is not None
assert message.timestamp is not None
def test_message_with_priority(self):
"""Message should accept custom priority"""
message = AgentMessage(
sender="alert-agent",
receiver="admin",
message_type="critical_alert",
payload={},
priority=MessagePriority.CRITICAL
)
assert message.priority == MessagePriority.CRITICAL
def test_message_serialization(self):
"""Message should serialize and deserialize correctly"""
original = AgentMessage(
sender="sender",
receiver="receiver",
message_type="test",
payload={"key": "value"},
priority=MessagePriority.HIGH
)
data = original.to_dict()
restored = AgentMessage.from_dict(data)
assert restored.sender == original.sender
assert restored.receiver == original.receiver
assert restored.message_type == original.message_type
assert restored.payload == original.payload
assert restored.priority == original.priority
assert restored.correlation_id == original.correlation_id
class TestMessageBus:
"""Tests for MessageBus"""
@pytest.fixture
def bus(self):
"""Create a message bus without Redis"""
return MessageBus(
redis_client=None,
db_pool=None,
namespace="test"
)
@pytest.mark.asyncio
async def test_start_stop(self, bus):
"""Bus should start and stop correctly"""
await bus.start()
assert bus._running is True
await bus.stop()
assert bus._running is False
@pytest.mark.asyncio
async def test_subscribe_unsubscribe(self, bus):
"""Should subscribe and unsubscribe handlers"""
handler = AsyncMock(return_value=None)
await bus.subscribe("agent-1", handler)
assert "agent-1" in bus._handlers
await bus.unsubscribe("agent-1")
assert "agent-1" not in bus._handlers
@pytest.mark.asyncio
async def test_local_message_delivery(self, bus):
"""Messages should be delivered locally without Redis"""
received = []
async def handler(message):
received.append(message)
return None
await bus.subscribe("agent-2", handler)
message = AgentMessage(
sender="agent-1",
receiver="agent-2",
message_type="test",
payload={"data": "hello"}
)
await bus.publish(message)
# Local delivery is synchronous
assert len(received) == 1
assert received[0].payload["data"] == "hello"
@pytest.mark.asyncio
async def test_request_response(self, bus):
"""Request should get response from handler"""
async def handler(message):
return {"result": "processed"}
await bus.subscribe("responder", handler)
message = AgentMessage(
sender="requester",
receiver="responder",
message_type="request",
payload={"query": "test"}
)
response = await bus.request(message, timeout=5.0)
assert response["result"] == "processed"
@pytest.mark.asyncio
async def test_request_timeout(self, bus):
"""Request should timeout if no response"""
async def slow_handler(message):
await asyncio.sleep(10)
return {"result": "too late"}
await bus.subscribe("slow-agent", slow_handler)
message = AgentMessage(
sender="requester",
receiver="slow-agent",
message_type="request",
payload={}
)
with pytest.raises(asyncio.TimeoutError):
await bus.request(message, timeout=0.1)
@pytest.mark.asyncio
async def test_broadcast(self, bus):
"""Broadcast should reach all subscribers"""
received_1 = []
received_2 = []
async def handler_1(message):
received_1.append(message)
return None
async def handler_2(message):
received_2.append(message)
return None
await bus.subscribe("agent-1", handler_1)
await bus.subscribe("agent-2", handler_2)
message = AgentMessage(
sender="broadcaster",
receiver="*",
message_type="announcement",
payload={"text": "Hello everyone"}
)
await bus.broadcast(message)
assert len(received_1) == 1
assert len(received_2) == 1
def test_connected_property(self, bus):
"""Connected should reflect running state"""
assert bus.connected is False
def test_subscriber_count(self, bus):
"""Should track subscriber count"""
assert bus.subscriber_count == 0
class TestMessagePriority:
"""Tests for MessagePriority enum"""
def test_priority_ordering(self):
"""Priorities should have correct ordering"""
assert MessagePriority.LOW.value < MessagePriority.NORMAL.value
assert MessagePriority.NORMAL.value < MessagePriority.HIGH.value
assert MessagePriority.HIGH.value < MessagePriority.CRITICAL.value
def test_priority_values(self):
"""Priorities should have expected values"""
assert MessagePriority.LOW.value == 0
assert MessagePriority.NORMAL.value == 1
assert MessagePriority.HIGH.value == 2
assert MessagePriority.CRITICAL.value == 3

View File

@@ -0,0 +1,270 @@
"""
Tests for Session Management
Tests cover:
- Session creation and retrieval
- State transitions
- Checkpoint management
- Heartbeat integration
"""
import pytest
import asyncio
from datetime import datetime, timezone, timedelta
from unittest.mock import AsyncMock, MagicMock, patch
import sys
sys.path.insert(0, str(__file__).rsplit('/tests/', 1)[0])
from sessions.session_manager import (
AgentSession,
SessionManager,
SessionState,
SessionCheckpoint,
)
class TestAgentSession:
"""Tests for AgentSession dataclass"""
def test_create_session_defaults(self):
"""Session should have default values"""
session = AgentSession()
assert session.session_id is not None
assert session.agent_type == ""
assert session.state == SessionState.ACTIVE
assert session.checkpoints == []
assert session.context == {}
def test_create_session_with_values(self):
"""Session should accept custom values"""
session = AgentSession(
agent_type="tutor-agent",
user_id="user-123",
context={"subject": "math"}
)
assert session.agent_type == "tutor-agent"
assert session.user_id == "user-123"
assert session.context["subject"] == "math"
def test_checkpoint_creation(self):
"""Session should create checkpoints correctly"""
session = AgentSession()
checkpoint = session.checkpoint("task_received", {"task_id": "123"})
assert len(session.checkpoints) == 1
assert checkpoint.name == "task_received"
assert checkpoint.data["task_id"] == "123"
assert checkpoint.timestamp is not None
def test_heartbeat_updates_timestamp(self):
"""Heartbeat should update last_heartbeat"""
session = AgentSession()
original = session.last_heartbeat
# Small delay to ensure time difference
import time
time.sleep(0.01)
session.heartbeat()
assert session.last_heartbeat > original
def test_pause_and_resume(self):
"""Session should pause and resume correctly"""
session = AgentSession()
session.pause()
assert session.state == SessionState.PAUSED
assert len(session.checkpoints) == 1 # Pause creates checkpoint
session.resume()
assert session.state == SessionState.ACTIVE
assert len(session.checkpoints) == 2 # Resume creates checkpoint
def test_complete_session(self):
"""Session should complete with result"""
session = AgentSession()
session.complete({"output": "success"})
assert session.state == SessionState.COMPLETED
last_cp = session.get_last_checkpoint()
assert last_cp.name == "session_completed"
assert last_cp.data["result"]["output"] == "success"
def test_fail_session(self):
"""Session should fail with error"""
session = AgentSession()
session.fail("Connection timeout", {"code": 504})
assert session.state == SessionState.FAILED
last_cp = session.get_last_checkpoint()
assert last_cp.name == "session_failed"
assert last_cp.data["error"] == "Connection timeout"
assert last_cp.data["details"]["code"] == 504
def test_get_last_checkpoint_by_name(self):
"""Should filter checkpoints by name"""
session = AgentSession()
session.checkpoint("step_1", {"data": 1})
session.checkpoint("step_2", {"data": 2})
session.checkpoint("step_1", {"data": 3})
last_step_1 = session.get_last_checkpoint("step_1")
assert last_step_1.data["data"] == 3
last_step_2 = session.get_last_checkpoint("step_2")
assert last_step_2.data["data"] == 2
def test_get_duration(self):
"""Should calculate session duration"""
session = AgentSession()
duration = session.get_duration()
assert duration.total_seconds() >= 0
assert duration.total_seconds() < 1 # Should be very fast
def test_serialization(self):
"""Session should serialize and deserialize correctly"""
session = AgentSession(
agent_type="grader-agent",
user_id="user-456",
context={"exam_id": "exam-1"}
)
session.checkpoint("grading_started", {"questions": 5})
# Serialize
data = session.to_dict()
# Deserialize
restored = AgentSession.from_dict(data)
assert restored.session_id == session.session_id
assert restored.agent_type == session.agent_type
assert restored.user_id == session.user_id
assert restored.context == session.context
assert len(restored.checkpoints) == 1
class TestSessionManager:
"""Tests for SessionManager"""
@pytest.fixture
def manager(self):
"""Create a session manager without persistence"""
return SessionManager(
redis_client=None,
db_pool=None,
namespace="test"
)
@pytest.mark.asyncio
async def test_create_session(self, manager):
"""Should create new sessions"""
session = await manager.create_session(
agent_type="tutor-agent",
user_id="user-789",
context={"grade": 10}
)
assert session.agent_type == "tutor-agent"
assert session.user_id == "user-789"
assert session.context["grade"] == 10
assert len(session.checkpoints) == 1 # session_created
@pytest.mark.asyncio
async def test_get_session_from_cache(self, manager):
"""Should retrieve session from local cache"""
created = await manager.create_session(
agent_type="grader-agent"
)
retrieved = await manager.get_session(created.session_id)
assert retrieved is not None
assert retrieved.session_id == created.session_id
@pytest.mark.asyncio
async def test_get_nonexistent_session(self, manager):
"""Should return None for nonexistent session"""
result = await manager.get_session("nonexistent-id")
assert result is None
@pytest.mark.asyncio
async def test_update_session(self, manager):
"""Should update session in cache"""
session = await manager.create_session(agent_type="alert-agent")
session.context["alert_count"] = 5
await manager.update_session(session)
retrieved = await manager.get_session(session.session_id)
assert retrieved.context["alert_count"] == 5
@pytest.mark.asyncio
async def test_delete_session(self, manager):
"""Should delete session from cache"""
session = await manager.create_session(agent_type="test-agent")
result = await manager.delete_session(session.session_id)
assert result is True
retrieved = await manager.get_session(session.session_id)
assert retrieved is None
@pytest.mark.asyncio
async def test_get_active_sessions(self, manager):
"""Should return active sessions filtered by type"""
await manager.create_session(agent_type="tutor-agent")
await manager.create_session(agent_type="tutor-agent")
await manager.create_session(agent_type="grader-agent")
tutor_sessions = await manager.get_active_sessions(
agent_type="tutor-agent"
)
assert len(tutor_sessions) == 2
@pytest.mark.asyncio
async def test_cleanup_stale_sessions(self, manager):
"""Should mark stale sessions as failed"""
# Create a session with old heartbeat
session = await manager.create_session(agent_type="test-agent")
session.last_heartbeat = datetime.now(timezone.utc) - timedelta(hours=50)
manager._local_cache[session.session_id] = session
count = await manager.cleanup_stale_sessions(max_age=timedelta(hours=48))
assert count == 1
assert session.state == SessionState.FAILED
class TestSessionCheckpoint:
"""Tests for SessionCheckpoint"""
def test_checkpoint_creation(self):
"""Checkpoint should store data correctly"""
checkpoint = SessionCheckpoint(
name="test_checkpoint",
timestamp=datetime.now(timezone.utc),
data={"key": "value"}
)
assert checkpoint.name == "test_checkpoint"
assert checkpoint.data["key"] == "value"
def test_checkpoint_serialization(self):
"""Checkpoint should serialize correctly"""
checkpoint = SessionCheckpoint(
name="test",
timestamp=datetime.now(timezone.utc),
data={"count": 42}
)
data = checkpoint.to_dict()
restored = SessionCheckpoint.from_dict(data)
assert restored.name == checkpoint.name
assert restored.data == checkpoint.data

View File

@@ -0,0 +1,203 @@
"""
Tests for Task Router
Tests cover:
- Intent-based routing
- Routing rules
- Fallback handling
- Routing statistics
"""
import pytest
import asyncio
from unittest.mock import MagicMock
import sys
sys.path.insert(0, str(__file__).rsplit('/tests/', 1)[0])
from orchestrator.task_router import (
TaskRouter,
RoutingRule,
RoutingResult,
RoutingStrategy,
)
class TestRoutingRule:
"""Tests for RoutingRule dataclass"""
def test_rule_creation(self):
"""Rule should be created correctly"""
rule = RoutingRule(
intent_pattern="learning_*",
agent_type="tutor-agent",
priority=10
)
assert rule.intent_pattern == "learning_*"
assert rule.agent_type == "tutor-agent"
assert rule.priority == 10
def test_rule_matches_exact(self):
"""Rule should match exact intent"""
rule = RoutingRule(
intent_pattern="grade_exam",
agent_type="grader-agent"
)
assert rule.matches("grade_exam", {}) is True
assert rule.matches("grade_quiz", {}) is False
def test_rule_matches_wildcard(self):
"""Rule should match wildcard patterns"""
rule = RoutingRule(
intent_pattern="learning_*",
agent_type="tutor-agent"
)
assert rule.matches("learning_math", {}) is True
assert rule.matches("learning_english", {}) is True
assert rule.matches("grading_math", {}) is False
def test_rule_matches_conditions(self):
"""Rule should check conditions"""
rule = RoutingRule(
intent_pattern="*",
agent_type="vip-agent",
conditions={"is_vip": True}
)
assert rule.matches("any_intent", {"is_vip": True}) is True
assert rule.matches("any_intent", {"is_vip": False}) is False
assert rule.matches("any_intent", {}) is False
class TestRoutingResult:
"""Tests for RoutingResult dataclass"""
def test_successful_result(self):
"""Should create successful routing result"""
result = RoutingResult(
success=True,
agent_id="tutor-1",
agent_type="tutor-agent",
reason="Primary agent selected"
)
assert result.success is True
assert result.agent_id == "tutor-1"
assert result.is_fallback is False
def test_fallback_result(self):
"""Should indicate fallback routing"""
result = RoutingResult(
success=True,
agent_id="backup-1",
agent_type="backup-agent",
is_fallback=True,
reason="Fallback used"
)
assert result.success is True
assert result.is_fallback is True
def test_failed_result(self):
"""Should create failed routing result"""
result = RoutingResult(
success=False,
reason="No agents available"
)
assert result.success is False
assert result.agent_id is None
class TestTaskRouter:
"""Tests for TaskRouter"""
@pytest.fixture
def router(self):
"""Create a task router without supervisor"""
return TaskRouter(supervisor=None)
def test_default_rules_exist(self, router):
"""Router should have default rules"""
rules = router.get_rules()
assert len(rules) > 0
def test_add_rule(self, router):
"""Should add new routing rule"""
original_count = len(router.rules)
router.add_rule(RoutingRule(
intent_pattern="custom_*",
agent_type="custom-agent",
priority=100
))
assert len(router.rules) == original_count + 1
def test_rules_sorted_by_priority(self, router):
"""Rules should be sorted by priority (high first)"""
router.add_rule(RoutingRule(
intent_pattern="low_*",
agent_type="low-agent",
priority=1
))
router.add_rule(RoutingRule(
intent_pattern="high_*",
agent_type="high-agent",
priority=100
))
# Highest priority should be first
assert router.rules[0].priority >= router.rules[-1].priority
def test_remove_rule(self, router):
"""Should remove routing rule"""
router.add_rule(RoutingRule(
intent_pattern="removable_*",
agent_type="temp-agent"
))
result = router.remove_rule("removable_*")
assert result is True
def test_find_matching_rules(self, router):
"""Should find rules matching intent"""
matching = router.find_matching_rules("learning_math")
assert len(matching) > 0
assert any(r["agent_type"] == "tutor-agent" for r in matching)
def test_get_routing_stats_empty(self, router):
"""Should return empty stats initially"""
stats = router.get_routing_stats()
assert stats["total_routes"] == 0
def test_set_default_route(self, router):
"""Should set default agent for type"""
router.set_default_route("tutor-agent", "tutor-primary")
assert router._default_routes["tutor-agent"] == "tutor-primary"
def test_clear_history(self, router):
"""Should clear routing history"""
# Add some history
router._routing_history.append({"test": "data"})
router.clear_history()
assert len(router._routing_history) == 0
class TestRoutingStrategy:
"""Tests for RoutingStrategy enum"""
def test_strategy_values(self):
"""Strategies should have expected values"""
assert RoutingStrategy.DIRECT.value == "direct"
assert RoutingStrategy.ROUND_ROBIN.value == "round_robin"
assert RoutingStrategy.LEAST_LOADED.value == "least_loaded"
assert RoutingStrategy.PRIORITY.value == "priority"