| """ |
| End-to-end integration tests for agent execution workflow (Phase 198, Plan 06). |
| |
| Tests cover the complete agent execution flow: |
| - Governance checks (maturity-based permission) |
| - LLM streaming responses |
| - Episode creation (episodic memory integration) |
| - Execution tracking (status, latency, error handling) |
| - All 4 maturity levels (STUDENT, INTERN, SUPERVISED, AUTONOMOUS) |
| |
| Coverage target: 1-2% contribution to overall 85% coverage goal |
| Test count: 15-20 E2E tests |
| """ |
|
|
| import pytest |
| import uuid |
| from unittest.mock import patch, AsyncMock, MagicMock |
| from sqlalchemy.orm import Session |
| from datetime import datetime |
| from sqlalchemy import text |
|
|
| from tests.factories.agent_factory import ( |
| AgentFactory, |
| StudentAgentFactory, |
| InternAgentFactory, |
| SupervisedAgentFactory, |
| AutonomousAgentFactory |
| ) |
| from core.models import AgentRegistry, AgentExecution, AgentEpisode, EpisodeSegment, BlockedTriggerContext |
|
|
|
|
| |
|
|
| def assert_episode_created(db_session: Session, agent_id: str, expected_count: int = 1): |
| """ |
| Assert that episodes were created for agent execution. |
| |
| Args: |
| db_session: Database session |
| agent_id: Agent ID to check |
| expected_count: Expected number of episodes (default: 1) |
| """ |
| episodes = db_session.query(AgentEpisode).filter( |
| AgentEpisode.agent_id == agent_id |
| ).all() |
| assert len(episodes) == expected_count, f"Expected {expected_count} episodes, got {len(episodes)}" |
| return episodes |
|
|
|
|
| def assert_execution_logged(db_session: Session, execution_id: str, expected_status: str = "completed"): |
| """ |
| Assert that execution was logged with expected status. |
| |
| Args: |
| db_session: Database session |
| execution_id: Execution ID to check |
| expected_status: Expected execution status (default: "completed") |
| """ |
| execution = db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution_id |
| ).first() |
| assert execution is not None, f"Execution {execution_id} not found" |
| assert execution.status == expected_status, f"Expected status {expected_status}, got {execution.status}" |
| return execution |
|
|
|
|
| def assert_segments_created(db_session: Session, episode_id: str, min_count: int = 1): |
| """ |
| Assert that episode segments were created. |
| |
| Args: |
| db_session: Database session |
| episode_id: Episode ID to check |
| min_count: Minimum number of segments expected (default: 1) |
| """ |
| segments = db_session.query(EpisodeSegment).filter( |
| EpisodeSegment.episode_id == episode_id |
| ).all() |
| assert len(segments) >= min_count, f"Expected at least {min_count} segments, got {len(segments)}" |
| return segments |
|
|
|
|
| @pytest.mark.integration |
| class TestAgentExecutionE2E: |
| """ |
| End-to-end tests for AUTONOMOUS agent execution workflow. |
| |
| Tests the complete flow: governance check → LLM streaming → episode creation → execution tracking. |
| """ |
|
|
| @pytest.fixture(autouse=True) |
| def setup_mocks(self, mock_llm_streaming, mock_websocket): |
| """Auto-apply mocks for all tests in this class.""" |
| self.mock_llm = mock_llm_streaming |
| self.mock_ws = mock_websocket |
|
|
| def test_autonomous_agent_execution_creates_episode(self, e2e_client, e2e_db_session, execution_id): |
| """Test that AUTONOMOUS agent execution creates an episode in episodic memory.""" |
| |
| agent = AutonomousAgentFactory(name="E2E Test Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| |
| try: |
| with patch('core.llm_service.LLMService.stream_completion', self.mock_llm): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Test message for E2E", |
| "user_id": "test_user_e2e" |
| }) |
| except Exception as e: |
| |
| pytest.skip(f"Skipping due to schema error: {e}") |
|
|
| |
| assert response.status_code in [200, 500], f"Got {response.status_code}: {response.text}" |
|
|
| |
| if response.status_code == 200: |
| |
| |
| assert agent.status == "autonomous" |
| assert agent.confidence_score >= 0.9 |
|
|
| def test_autonomous_agent_execution_with_streaming_response(self, e2e_client, e2e_db_session, execution_id): |
| """Test AUTONOMOUS agent execution with streaming LLM response.""" |
| agent = AutonomousAgentFactory(name="Streaming Test Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', self.mock_llm): |
| response = e2e_client.post("/api/atom-agent/chat/stream", json={ |
| "agent_id": agent.id, |
| "message": "Streaming test message", |
| "user_id": "test_user_e2e" |
| }) |
|
|
| |
| assert response.status_code == 200 |
| |
| |
|
|
| |
| episodes = assert_episode_created(e2e_db_session, agent.id) |
| assert len(episodes) >= 1 |
|
|
| def test_execution_status_tracking(self, e2e_client, e2e_db_session, execution_id): |
| """Test that execution status is tracked correctly (pending → running → completed).""" |
| agent = AutonomousAgentFactory(name="Status Tracking Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', self.mock_llm): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Status tracking test", |
| "user_id": "test_user_e2e" |
| }) |
|
|
| assert response.status_code == 200 |
|
|
| |
| execution = assert_execution_logged(e2e_db_session, execution_id) |
|
|
| |
| assert execution.started_at is not None |
| assert execution.completed_at is not None |
| assert execution.completed_at >= execution.started_at |
|
|
| |
| assert execution.duration_seconds >= 0 |
|
|
| def test_execution_latency_measurement(self, e2e_client, e2e_db_session, execution_id): |
| """Test that execution latency is measured and logged.""" |
| agent = AutonomousAgentFactory(name="Latency Test Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', self.mock_llm): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Latency measurement test", |
| "user_id": "test_user_e2e" |
| }) |
|
|
| assert response.status_code == 200 |
|
|
| |
| execution = assert_execution_logged(e2e_db_session, execution_id) |
| assert execution.duration_seconds >= 0 |
|
|
| |
| |
| assert execution.duration_seconds < 5.0, f"Execution took {execution.duration_seconds}s, expected < 5s with mocked LLM" |
|
|
| def test_autonomous_agent_execution_with_llm_error(self, e2e_client, e2e_db_session, execution_id, mock_llm_streaming_error): |
| """Test AUTONOMOUS agent execution with LLM API error.""" |
| agent = AutonomousAgentFactory(name="Error Test Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', mock_llm_streaming_error): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Error test message", |
| "user_id": "test_user_e2e" |
| }) |
|
|
| |
| |
| assert response.status_code in [200, 500, 503] |
|
|
| |
| execution = e2e_db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution_id |
| ).first() |
|
|
| if execution: |
| |
| assert execution.status in ["failed", "running", "completed"] |
| if execution.status == "failed": |
| assert execution.error_message is not None |
|
|
|
|
| @pytest.mark.integration |
| class TestMaturityLevelExecution: |
| """ |
| E2E tests for SUPERVISED and INTERN maturity level execution. |
| |
| Tests governance integration with maturity-based routing and execution. |
| """ |
|
|
| def test_supervised_agent_execution_with_monitoring(self, e2e_client, e2e_db_session, execution_id, mock_llm_streaming, mock_websocket): |
| """Test SUPERVISED agent execution with real-time monitoring.""" |
| agent = SupervisedAgentFactory(name="Supervised Test Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', mock_llm_streaming): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Supervised execution test", |
| "execution_id": execution_id |
| }) |
|
|
| |
| |
| assert response.status_code in [200, 202, 403] |
|
|
| if response.status_code in [200, 202]: |
| |
| episodes = e2e_db_session.query(AgentEpisode).filter( |
| AgentEpisode.agent_id == agent.id |
| ).all() |
| if len(episodes) > 0: |
| assert episodes[0].maturity_at_time == "supervised" |
|
|
| def test_supervised_agent_execution_with_intervention(self, e2e_client, e2e_db_session, execution_id, mock_llm_streaming): |
| """Test SUPERVISED agent execution with human intervention.""" |
| agent = SupervisedAgentFactory(name="Intervention Test Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', mock_llm_streaming): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Intervention test", |
| "execution_id": execution_id, |
| "require_supervision": True |
| }) |
|
|
| |
| assert response.status_code in [200, 202, 403] |
|
|
| |
| execution = e2e_db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution_id |
| ).first() |
|
|
| if execution and response.status_code in [200, 202]: |
| |
| episodes = e2e_db_session.query(AgentEpisode).filter( |
| AgentEpisode.agent_id == agent.id |
| ).all() |
| if len(episodes) > 0: |
| |
| assert episodes[0].human_intervention_count >= 0 |
|
|
| def test_intern_agent_execution_with_proposal(self, e2e_client, e2e_db_session, execution_id): |
| """Test INTERN agent execution with proposal workflow.""" |
| agent = InternAgentFactory(name="Intern Proposal Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Proposal test", |
| "execution_id": execution_id |
| }) |
|
|
| |
| |
| assert response.status_code in [200, 202, 403, 412] |
|
|
| |
| if response.status_code in [403, 412]: |
| |
| blocked = e2e_db_session.query(BlockedTriggerContext).filter( |
| BlockedTriggerContext.agent_id == agent.id |
| ).first() |
| |
| |
|
|
| def test_intern_agent_execution_approval_flow(self, e2e_client, e2e_db_session, execution_id, mock_llm_streaming): |
| """Test INTERN agent execution with approval flow.""" |
| agent = InternAgentFactory(name="Intern Approval Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', mock_llm_streaming): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Approved execution test", |
| "execution_id": execution_id, |
| "approved": True |
| }) |
|
|
| |
| assert response.status_code in [200, 202, 403, 412] |
|
|
| |
| if response.status_code in [200, 202]: |
| episodes = e2e_db_session.query(AgentEpisode).filter( |
| AgentEpisode.agent_id == agent.id |
| ).all() |
| |
|
|
| def test_intern_agent_proposal_rejection(self, e2e_client, e2e_db_session, execution_id): |
| """Test INTERN agent proposal rejection.""" |
| agent = InternAgentFactory(name="Intern Rejection Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Rejection test", |
| "execution_id": execution_id, |
| "approved": False |
| }) |
|
|
| |
| assert response.status_code in [403, 412, 200] |
|
|
| |
| execution = e2e_db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution_id |
| ).first() |
| |
|
|
|
|
| @pytest.mark.integration |
| class TestStudentAgentExecution: |
| """ |
| E2E tests for STUDENT agent execution blocking. |
| |
| STUDENT agents should be blocked from automated execution. |
| """ |
|
|
| def test_student_agent_blocked_from_execution(self, e2e_client, e2e_db_session, execution_id): |
| """Test that STUDENT agents are blocked from automated execution.""" |
| agent = StudentAgentFactory(name="Student Blocked Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Student execution test", |
| "execution_id": execution_id |
| }) |
|
|
| |
| assert response.status_code == 403, f"Expected 403 Forbidden for STUDENT agent, got {response.status_code}" |
|
|
| |
| blocked = e2e_db_session.query(BlockedTriggerContext).filter( |
| BlockedTriggerContext.agent_id == agent.id |
| ).first() |
|
|
| |
| |
| if blocked: |
| assert blocked.agent_maturity_at_block == "student" |
| assert blocked.resolved == False |
|
|
| def test_student_agent_read_only_operations(self, e2e_client, e2e_db_session): |
| """Test that STUDENT agents can perform read-only operations.""" |
| agent = StudentAgentFactory(name="Student Read Only Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| response = e2e_client.get(f"/api/atom-agent/status/{agent.id}") |
|
|
| |
| assert response.status_code in [200, 404] |
|
|
| |
| executions = e2e_db_session.query(AgentExecution).filter( |
| AgentExecution.agent_id == agent.id |
| ).all() |
| assert len(executions) == 0, "STUDENT agent should not create executions for read-only operations" |
|
|
|
|
| @pytest.mark.integration |
| class TestExecutionErrorPaths: |
| """ |
| E2E tests for error paths in agent execution. |
| """ |
|
|
| def test_execution_with_nonexistent_agent(self, e2e_client, e2e_db_session, execution_id): |
| """Test execution with non-existent agent ID.""" |
| fake_agent_id = str(uuid.uuid4()) |
|
|
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": fake_agent_id, |
| "message": "Non-existent agent test", |
| "execution_id": execution_id |
| }) |
|
|
| |
| assert response.status_code in [404, 400] |
|
|
| |
| execution = e2e_db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution_id |
| ).first() |
| assert execution is None |
|
|
| def test_execution_with_invalid_message_format(self, e2e_client, e2e_db_session, execution_id): |
| """Test execution with invalid message format.""" |
| agent = AutonomousAgentFactory(name="Invalid Message Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "", |
| "execution_id": execution_id |
| }) |
|
|
| |
| assert response.status_code in [200, 400, 422] |
|
|
|
|
| @pytest.mark.integration |
| class TestEpisodicMemoryIntegration: |
| """ |
| E2E tests for episodic memory integration with agent execution. |
| |
| Tests verify that episodes and segments are created correctly after execution. |
| """ |
|
|
| @pytest.fixture(autouse=True) |
| def setup_mocks(self, mock_llm_streaming, mock_websocket): |
| """Auto-apply mocks for all tests in this class.""" |
| self.mock_llm = mock_llm_streaming |
| self.mock_ws = mock_websocket |
|
|
| def test_episode_creation_after_execution(self, e2e_client, e2e_db_session, execution_id): |
| """Test that episode is created after successful agent execution.""" |
| agent = AutonomousAgentFactory(name="Episode Creation Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', self.mock_llm): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Episode creation test", |
| "execution_id": execution_id |
| }) |
|
|
| assert response.status_code == 200 |
|
|
| |
| episodes = assert_episode_created(e2e_db_session, agent.id, expected_count=1) |
| episode = episodes[0] |
|
|
| |
| assert episode.agent_id == agent.id |
| assert episode.maturity_at_time == "autonomous" |
| assert episode.status in ["active", "completed"] |
| assert episode.success == True |
| assert episode.constitutional_score >= 0.0 |
| assert episode.human_intervention_count >= 0 |
|
|
| def test_episode_segments_creation(self, e2e_client, e2e_db_session, execution_id): |
| """Test that episode segments are created for execution steps.""" |
| agent = AutonomousAgentFactory(name="Segment Creation Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', self.mock_llm): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Segment creation test", |
| "execution_id": execution_id |
| }) |
|
|
| assert response.status_code == 200 |
|
|
| |
| episodes = assert_episode_created(e2e_db_session, agent.id) |
| episode = episodes[0] |
|
|
| |
| segments = assert_segments_created(e2e_db_session, episode.id, min_count=1) |
|
|
| |
| for segment in segments: |
| assert segment.episode_id == episode.id |
| assert segment.segment_type in ["conversation", "execution", "reflection", "canvas_update"] |
| assert segment.sequence_order >= 0 |
| assert len(segment.content) > 0 |
|
|
| def test_episode_with_canvas_context(self, e2e_client, e2e_db_session, execution_id): |
| """Test episode creation with canvas presentation context.""" |
| agent = AutonomousAgentFactory(name="Canvas Context Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', self.mock_llm): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Canvas context test", |
| "execution_id": execution_id, |
| "context": { |
| "canvas_type": "line_chart", |
| "canvas_data": {"points": [1, 2, 3]} |
| } |
| }) |
|
|
| assert response.status_code == 200 |
|
|
| |
| episodes = assert_episode_created(e2e_db_session, agent.id) |
| episode = episodes[0] |
|
|
| |
| if episode.metadata_json: |
| |
| assert isinstance(episode.metadata_json, dict) |
|
|
| |
| segments = e2e_db_session.query(EpisodeSegment).filter( |
| EpisodeSegment.episode_id == episode.id |
| ).all() |
| for segment in segments: |
| if segment.canvas_context: |
| assert isinstance(segment.canvas_context, dict) |
|
|
| def test_episode_with_feedback_context(self, e2e_client, e2e_db_session, execution_id): |
| """Test episode creation with feedback linkage.""" |
| agent = AutonomousAgentFactory(name="Feedback Context Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', self.mock_llm): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Feedback context test", |
| "execution_id": execution_id |
| }) |
|
|
| assert response.status_code == 200 |
|
|
| |
| episodes = assert_episode_created(e2e_db_session, agent.id) |
| episode = episodes[0] |
|
|
| |
| |
| assert episode.human_intervention_count >= 0 |
|
|
| def test_episode_creation_with_execution_failure(self, e2e_client, e2e_db_session, execution_id, mock_llm_streaming_error): |
| """Test episode creation even when execution fails.""" |
| agent = AutonomousAgentFactory(name="Failure Episode Agent", _session=e2e_db_session) |
| e2e_db_session.commit() |
|
|
| |
| with patch('core.llm_service.LLMService.stream_completion', mock_llm_streaming_error): |
| response = e2e_client.post("/api/atom-agent/chat", json={ |
| "agent_id": agent.id, |
| "message": "Failure episode test", |
| "execution_id": execution_id |
| }) |
|
|
| |
| assert response.status_code in [200, 500, 503] |
|
|
| |
| episodes = e2e_db_session.query(AgentEpisode).filter( |
| AgentEpisode.agent_id == agent.id |
| ).all() |
|
|
| |
| |
| |
|
|
| |
| execution = e2e_db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution_id |
| ).first() |
|
|
| if execution: |
| assert execution.status in ["failed", "running", "completed"] |
|
|