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:
3
agent-core/tests/__init__.py
Normal file
3
agent-core/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Tests for Breakpilot Agent Core
|
||||
"""
|
||||
57
agent-core/tests/conftest.py
Normal file
57
agent-core/tests/conftest.py
Normal 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
|
||||
201
agent-core/tests/test_heartbeat.py
Normal file
201
agent-core/tests/test_heartbeat.py
Normal 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
|
||||
207
agent-core/tests/test_memory_store.py
Normal file
207
agent-core/tests/test_memory_store.py
Normal 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
|
||||
224
agent-core/tests/test_message_bus.py
Normal file
224
agent-core/tests/test_message_bus.py
Normal 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
|
||||
270
agent-core/tests/test_session_manager.py
Normal file
270
agent-core/tests/test_session_manager.py
Normal 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
|
||||
203
agent-core/tests/test_task_router.py
Normal file
203
agent-core/tests/test_task_router.py
Normal 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"
|
||||
Reference in New Issue
Block a user