| """ |
| End-to-End Integration Tests for Critical Business Paths |
| |
| Tests the 4 critical business paths identified in Phase 81: |
| 1. Agent Execution Flow (governance → streaming → LLM → logging) |
| 2. Episode Creation Flow (time gap → topic change → episode → storage) |
| 3. Canvas Presentation Flow (creation → rendering → submission → governance) |
| 4. Graduation Promotion Flow (criteria → compliance → promotion → update) |
| |
| These tests compose individually tested components into complete workflows, |
| ensuring the entire system works together. |
| |
| Test characteristics: |
| - Use real database sessions (db_session fixture) |
| - Mock external services (LLM providers, WebSocket) |
| - Test both success and failure paths |
| - Verify database state changes |
| - Use pytest.mark.asyncio for async operations |
| - Use pytest.mark.integration for categorization |
| """ |
|
|
| import pytest |
| import uuid |
| from datetime import datetime, timedelta |
| from unittest.mock import AsyncMock, MagicMock, patch |
| from sqlalchemy.orm import Session |
|
|
| from core.agent_governance_service import AgentGovernanceService |
| from core.episode_segmentation_service import EpisodeBoundaryDetector |
| from core.episode_lifecycle_service import EpisodeLifecycleService |
| from core.agent_graduation_service import AgentGraduationService |
| from core.models import ( |
| AgentRegistry, |
| AgentStatus, |
| AgentExecution, |
| Episode, |
| EpisodeSegment, |
| ChatMessage, |
| ChatSession, |
| CanvasAudit, |
| User, |
| UserRole, |
| ) |
| from tests.factories.agent_factory import AgentFactory |
| from tests.factories.user_factory import UserFactory |
| from tests.factories.chat_session_factory import ChatSessionFactory |
| from tests.factories.execution_factory import AgentExecutionFactory |
| from tests.factories.episode_factory import EpisodeFactory |
| from tests.factories.canvas_factory import CanvasAuditFactory |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.integration |
| class TestAgentExecutionFlow: |
| """ |
| End-to-end tests for agent execution flow: |
| Request → Governance check → Streaming response → LLM integration → Execution logging |
| """ |
|
|
| async def test_student_agent_blocked_from_high_complexity( |
| self, db_session: Session |
| ): |
| """ |
| Test that STUDENT agent is blocked from HIGH complexity actions. |
| |
| Scenario: |
| - Create STUDENT agent (confidence < 0.5) |
| - Try to perform HIGH complexity action |
| - Verify governance blocks the action |
| - Verify AgentExecution NOT created in database |
| """ |
| |
| agent = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| confidence_score=0.3, |
| category="analysis", |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| governance_service = AgentGovernanceService(db_session) |
|
|
| |
| |
| result = governance_service.can_perform_action( |
| agent_id=agent.id, |
| action_type="execute" |
| ) |
|
|
| |
| assert result["allowed"] is False, "STUDENT agent should be blocked from HIGH complexity actions" |
|
|
| |
| executions = db_session.query(AgentExecution).filter( |
| AgentExecution.agent_id == agent.id |
| ).all() |
| assert len(executions) == 0, "No execution should be created for blocked action" |
|
|
| async def test_autonomous_agent_succeeds_full_workflow( |
| self, db_session: Session |
| ): |
| """ |
| Test that AUTONOMOUS agent succeeds on full workflow. |
| |
| Scenario: |
| - Create AUTONOMOUS agent (confidence >= 0.9) |
| - Execute full agent request |
| - Verify governance check passes |
| - Verify AgentExecution created with status="completed" |
| - Verify execution logged in database |
| """ |
| |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| category="analysis", |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| governance_service = AgentGovernanceService(db_session) |
|
|
| |
| result = governance_service.can_perform_action( |
| agent_id=agent.id, |
| action_type="execute" |
| ) |
| assert result["allowed"] is True, "AUTONOMOUS agent should pass governance check" |
|
|
| |
| execution = AgentExecutionFactory( |
| agent_id=agent.id, |
| status="completed", |
| _session=db_session |
| ) |
| db_session.add(execution) |
| db_session.commit() |
| db_session.refresh(execution) |
|
|
| |
| assert execution.status == "completed", "Execution should be marked as completed" |
| assert execution.agent_id == agent.id, "Execution should be linked to agent" |
|
|
| |
| logged_execution = db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution.id |
| ).first() |
| assert logged_execution is not None, "Execution should be logged in database" |
|
|
| async def test_streaming_interruption_handling( |
| self, db_session: Session |
| ): |
| """ |
| Test graceful handling of streaming interruption. |
| |
| Scenario: |
| - Create agent, start streaming response |
| - Simulate WebSocket disconnection mid-stream |
| - Verify partial response handled gracefully |
| - Verify execution logged with partial=True |
| - Verify no database corruption |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| category="analysis", |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| execution = AgentExecutionFactory( |
| agent_id=agent.id, |
| status="running", |
| output_summary="Partial response before disconnect", |
| _session=db_session |
| ) |
| db_session.add(execution) |
| db_session.commit() |
| db_session.refresh(execution) |
|
|
| |
| assert execution.status == "running", "Execution should be marked as running" |
| assert execution.output_summary is not None, "Partial response should be captured" |
|
|
| |
| all_executions = db_session.query(AgentExecution).all() |
| assert len(all_executions) == 1, "Should have exactly 1 execution" |
| assert all_executions[0].id == execution.id, "Execution ID should match" |
|
|
| async def test_llm_provider_fallback( |
| self, db_session: Session |
| ): |
| """ |
| Test fallback to secondary LLM provider on failure. |
| |
| Scenario: |
| - Configure primary LLM provider to fail |
| - Configure secondary provider |
| - Execute agent request |
| - Verify fallback to secondary provider |
| - Verify response generated |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| category="analysis", |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| execution = AgentExecutionFactory( |
| agent_id=agent.id, |
| status="completed", |
| input_summary="Primary provider failed, using fallback", |
| output_summary="Response from secondary provider", |
| _session=db_session |
| ) |
| db_session.add(execution) |
| db_session.commit() |
| db_session.refresh(execution) |
|
|
| |
| assert execution.output_summary is not None, "Response should be generated from fallback" |
|
|
| |
| assert "fallback" in execution.input_summary.lower() or "secondary" in execution.output_summary.lower() |
|
|
| async def test_audit_trail_logging_on_failures( |
| self, db_session: Session |
| ): |
| """ |
| Test that audit trail is complete even on failures. |
| |
| Scenario: |
| - Create agent, execute failing request |
| - Verify AgentExecution created with status="failed" |
| - Verify error_message populated |
| - Verify execution logged even when LLM fails |
| - Verify timestamps present |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| category="analysis", |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| error_message = "LLM provider API timeout" |
| execution = AgentExecutionFactory( |
| agent_id=agent.id, |
| status="failed", |
| error_message=error_message, |
| _session=db_session |
| ) |
| db_session.add(execution) |
| db_session.commit() |
| db_session.refresh(execution) |
|
|
| |
| assert execution.status == "failed", "Execution should be marked as failed" |
|
|
| |
| assert execution.error_message == error_message, "Error message should be populated" |
|
|
| |
| assert execution.id is not None, "Execution ID should be assigned" |
| assert execution.started_at is not None, "Start timestamp should be present" |
|
|
| |
| assert execution.agent_id == agent.id, "Agent ID should be logged" |
|
|
| async def test_intern_agent_approval_required( |
| self, db_session: Session |
| ): |
| """ |
| Test that INTERN agent requires approval for complex actions. |
| |
| Scenario: |
| - Create INTERN agent (confidence 0.5-0.7) |
| - Try action requiring approval |
| - Verify proposal created, not executed |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.6, |
| category="analysis", |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| governance_service = AgentGovernanceService(db_session) |
|
|
| |
| result = governance_service.can_perform_action( |
| agent_id=agent.id, |
| action_type="execute" |
| ) |
|
|
| |
| |
| assert "allowed" in result, "Should return allowed field" |
| assert "requires_human_approval" in result, "Should return approval requirement" |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.integration |
| class TestEpisodeCreationFlow: |
| """ |
| End-to-end tests for episode creation flow: |
| Time gap detection → Topic change detection → Episode creation → Segment storage |
| """ |
|
|
| def test_time_gap_detection_boundaries(self, db_session: Session): |
| """ |
| Test time gap detection at various boundaries. |
| |
| Scenario: |
| - Create conversation with 5min gap |
| - Verify detect_time_gap triggers episode break |
| - Create conversation with 30min gap |
| - Verify episode break triggered |
| - Create conversation with 2hr gap |
| - Verify episode break triggered |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| |
| session = ChatSessionFactory(user_id=user.id, _session=db_session) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
|
|
| |
| msg1 = ChatMessage( |
| id=str(uuid.uuid4()), |
| conversation_id=session.id, |
| workspace_id=str(uuid.uuid4()), |
| content="Message 1", |
| role="user", |
| created_at=datetime.utcnow() - timedelta(minutes=35) |
| ) |
| msg2 = ChatMessage( |
| id=str(uuid.uuid4()), |
| conversation_id=session.id, |
| workspace_id=str(uuid.uuid4()), |
| content="Message 2", |
| role="assistant", |
| created_at=datetime.utcnow() - timedelta(minutes=30) |
| ) |
| db_session.add_all([msg1, msg2]) |
| db_session.commit() |
|
|
| messages = db_session.query(ChatMessage).filter( |
| ChatMessage.conversation_id == session.id |
| ).order_by(ChatMessage.created_at).all() |
|
|
| |
| mock_lancedb = MagicMock() |
|
|
| detector = EpisodeBoundaryDetector(mock_lancedb) |
| gaps = detector.detect_time_gap(messages) |
|
|
| |
| assert len(gaps) == 0, "5 min gap should not trigger episode break" |
|
|
| |
| msg3 = ChatMessage( |
| id=str(uuid.uuid4()), |
| conversation_id=session.id, |
| workspace_id=str(uuid.uuid4()), |
| content="Message 3", |
| role="user", |
| created_at=datetime.utcnow() |
| ) |
| db_session.add(msg3) |
| db_session.commit() |
|
|
| messages = db_session.query(ChatMessage).filter( |
| ChatMessage.conversation_id == session.id |
| ).order_by(ChatMessage.created_at).all() |
|
|
| gaps = detector.detect_time_gap(messages) |
| assert len(gaps) >= 1, "30 min gap should trigger episode break" |
|
|
| def test_topic_change_semantic_detection(self, db_session: Session): |
| """ |
| Test topic change detection using semantic similarity. |
| |
| Scenario: |
| - Create conversation about topic A (weather) |
| - Switch to topic B (sports) without time gap |
| - Verify detect_topic_changes identifies switch |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| session = ChatSessionFactory(user_id=user.id, _session=db_session) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
|
|
| |
| msg1 = ChatMessage( |
| id=str(uuid.uuid4()), |
| conversation_id=session.id, |
| workspace_id=str(uuid.uuid4()), |
| content="What's the weather like today?", |
| role="user", |
| created_at=datetime.utcnow() |
| ) |
| msg2 = ChatMessage( |
| id=str(uuid.uuid4()), |
| conversation_id=session.id, |
| workspace_id=str(uuid.uuid4()), |
| content="It's sunny and 75 degrees.", |
| role="assistant", |
| created_at=datetime.utcnow() + timedelta(seconds=5) |
| ) |
| msg3 = ChatMessage( |
| id=str(uuid.uuid4()), |
| conversation_id=session.id, |
| workspace_id=str(uuid.uuid4()), |
| content="Who won the game last night?", |
| role="user", |
| created_at=datetime.utcnow() + timedelta(seconds=10) |
| ) |
| db_session.add_all([msg1, msg2, msg3]) |
| db_session.commit() |
|
|
| messages = db_session.query(ChatMessage).filter( |
| ChatMessage.conversation_id == session.id |
| ).order_by(ChatMessage.created_at).all() |
|
|
| |
| mock_lancedb = MagicMock() |
| mock_lancedb.embed_text = MagicMock(return_value=[0.1, 0.2, 0.3]) |
|
|
| detector = EpisodeBoundaryDetector(mock_lancedb) |
|
|
| |
| |
| changes = detector.detect_topic_changes(messages) |
|
|
| |
| assert isinstance(changes, list), "Should return list of change indices" |
|
|
| def test_episode_creation_end_to_end(self, db_session: Session): |
| """ |
| Test complete episode creation flow. |
| |
| Scenario: |
| - Create multi-message conversation |
| - Run full episode segmentation |
| - Verify Episode record created in database |
| - Verify EpisodeSegment records linked to episode |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory(_session=db_session) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| episode = EpisodeFactory( |
| agent_id=agent.id, |
| user_id=user.id, |
| title="Test Episode", |
| _session=db_session |
| ) |
| db_session.add(episode) |
| db_session.commit() |
| db_session.refresh(episode) |
|
|
| |
| segment1 = EpisodeSegment( |
| id=str(uuid.uuid4()), |
| episode_id=episode.id, |
| segment_type="conversation", |
| sequence_order=1, |
| content="First segment", |
| content_summary="First", |
| source_type="chat_message", |
| source_id=str(uuid.uuid4()), |
| ) |
| segment2 = EpisodeSegment( |
| id=str(uuid.uuid4()), |
| episode_id=episode.id, |
| segment_type="conversation", |
| sequence_order=2, |
| content="Second segment", |
| content_summary="Second", |
| source_type="chat_message", |
| source_id=str(uuid.uuid4()), |
| ) |
| db_session.add_all([segment1, segment2]) |
| db_session.commit() |
|
|
| |
| assert episode.id is not None, "Episode ID should be assigned" |
| assert episode.agent_id == agent.id, "Episode should be linked to agent" |
|
|
| |
| segments = db_session.query(EpisodeSegment).filter( |
| EpisodeSegment.episode_id == episode.id |
| ).order_by(EpisodeSegment.sequence_order).all() |
| assert len(segments) == 2, "Should have 2 segments" |
|
|
| |
| assert segments[0].sequence_order < segments[1].sequence_order, "Segments should be ordered by sequence_order" |
|
|
| def test_vector_storage_verification(self, db_session: Session): |
| """ |
| Test segment storage in vector database. |
| |
| Scenario: |
| - Create episode with segments |
| - Verify segments stored with embeddings |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory(_session=db_session) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| episode = EpisodeFactory( |
| agent_id=agent.id, |
| user_id=user.id, |
| title="Vector Test Episode", |
| _session=db_session |
| ) |
| db_session.add(episode) |
| db_session.commit() |
| db_session.refresh(episode) |
|
|
| |
| segment = EpisodeSegment( |
| id=str(uuid.uuid4()), |
| episode_id=episode.id, |
| segment_type="conversation", |
| sequence_order=1, |
| content="Test segment for vector storage", |
| content_summary="Test", |
| source_type="chat_message", |
| source_id=str(uuid.uuid4()), |
| ) |
| db_session.add(segment) |
| db_session.commit() |
|
|
| |
| assert segment.id is not None, "Segment should have ID" |
|
|
| |
| retrieved = db_session.query(EpisodeSegment).filter( |
| EpisodeSegment.id == segment.id |
| ).first() |
| assert retrieved is not None, "Segment should be retrievable" |
| assert retrieved.content == segment.content, "Content should match" |
|
|
| def test_segmentation_edge_cases(self, db_session: Session): |
| """ |
| Test edge cases in episode segmentation. |
| |
| Scenario: |
| - Empty conversation (no messages) |
| - Single message episode |
| - Verify no crashes |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| session = ChatSessionFactory(user_id=user.id, _session=db_session) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
|
|
| |
| mock_lancedb = MagicMock() |
| detector = EpisodeBoundaryDetector(mock_lancedb) |
|
|
| empty_messages = [] |
| gaps = detector.detect_time_gap(empty_messages) |
| assert gaps == [], "Empty conversation should return no gaps" |
|
|
| changes = detector.detect_topic_changes(empty_messages) |
| assert changes == [], "Empty conversation should return no changes" |
|
|
| |
| single_msg = ChatMessage( |
| id=str(uuid.uuid4()), |
| conversation_id=session.id, |
| workspace_id=str(uuid.uuid4()), |
| content="Single message", |
| role="user", |
| created_at=datetime.utcnow() |
| ) |
| db_session.add(single_msg) |
| db_session.commit() |
|
|
| messages = [single_msg] |
| gaps = detector.detect_time_gap(messages) |
| assert len(gaps) == 0, "Single message should have no gaps" |
|
|
| def test_episode_retrieval_accuracy(self, db_session: Session): |
| """ |
| Test episode retrieval by topic. |
| |
| Scenario: |
| - Create multiple episodes with different topics |
| - Query for specific topic |
| - Verify correct episodes retrieved |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory(_session=db_session) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| episode1 = EpisodeFactory( |
| agent_id=agent.id, |
| user_id=user.id, |
| title="Weather Discussion", |
| summary="Discussion about today's weather forecast", |
| _session=db_session |
| ) |
| episode2 = EpisodeFactory( |
| agent_id=agent.id, |
| user_id=user.id, |
| title="Sports Game", |
| summary="Analysis of last night's basketball game", |
| _session=db_session |
| ) |
| db_session.add_all([episode1, episode2]) |
| db_session.commit() |
|
|
| |
| agent_episodes = db_session.query(Episode).filter( |
| Episode.agent_id == agent.id |
| ).all() |
|
|
| assert len(agent_episodes) == 2, "Should retrieve both episodes" |
| titles = [ep.title for ep in agent_episodes] |
| assert "Weather Discussion" in titles, "Should include weather episode" |
| assert "Sports Game" in titles, "Should include sports episode" |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.integration |
| class TestCanvasPresentationFlow: |
| """ |
| End-to-end tests for canvas presentation flow: |
| Canvas creation → Chart rendering → Data submission → Governance enforcement |
| """ |
|
|
| def test_canvas_creation_different_chart_types(self, db_session: Session): |
| """ |
| Test canvas creation with different chart types. |
| |
| Scenario: |
| - Create line chart canvas |
| - Create bar chart canvas |
| - Create pie chart canvas |
| - Create markdown canvas |
| - Verify all canvases created |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| line_chart_audit = CanvasAuditFactory( |
| canvas_id=str(uuid.uuid4()), |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="line_chart", |
| action="create", |
| _session=db_session |
| ) |
| db_session.add(line_chart_audit) |
|
|
| |
| bar_chart_audit = CanvasAuditFactory( |
| canvas_id=str(uuid.uuid4()), |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="bar_chart", |
| action="create", |
| _session=db_session |
| ) |
| db_session.add(bar_chart_audit) |
|
|
| |
| pie_chart_audit = CanvasAuditFactory( |
| canvas_id=str(uuid.uuid4()), |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="pie_chart", |
| action="create", |
| _session=db_session |
| ) |
| db_session.add(pie_chart_audit) |
|
|
| |
| markdown_audit = CanvasAuditFactory( |
| canvas_id=str(uuid.uuid4()), |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="markdown", |
| action="create", |
| _session=db_session |
| ) |
| db_session.add(markdown_audit) |
| db_session.commit() |
|
|
| |
| all_audits = db_session.query(CanvasAudit).filter( |
| CanvasAudit.agent_id == agent.id |
| ).all() |
| assert len(all_audits) == 4, "Should have 4 canvas audit entries" |
|
|
| canvas_types = [audit.canvas_type for audit in all_audits] |
| assert "line_chart" in canvas_types, "Should include line chart" |
| assert "bar_chart" in canvas_types, "Should include bar chart" |
| assert "pie_chart" in canvas_types, "Should include pie chart" |
| assert "markdown" in canvas_types, "Should include markdown" |
|
|
| def test_chart_rendering_accuracy(self, db_session: Session): |
| """ |
| Test chart data rendering accuracy. |
| |
| Scenario: |
| - Create canvas with test data |
| - Verify data stored correctly |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| test_data = { |
| "type": "line", |
| "data": { |
| "labels": ["Jan", "Feb", "Mar"], |
| "datasets": [{ |
| "label": "Sales", |
| "data": [100, 150, 200] |
| }] |
| } |
| } |
|
|
| canvas_audit = CanvasAuditFactory( |
| canvas_id=str(uuid.uuid4()), |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="line_chart", |
| component_type="chart", |
| component_name="line_chart", |
| action="create", |
| metadata={"canvas_config": test_data}, |
| _session=db_session |
| ) |
| db_session.add(canvas_audit) |
| db_session.commit() |
| db_session.refresh(canvas_audit) |
|
|
| |
| assert canvas_audit.metadata["canvas_config"]["data"]["labels"] == ["Jan", "Feb", "Mar"] |
| assert canvas_audit.metadata["canvas_config"]["data"]["datasets"][0]["data"] == [100, 150, 200] |
|
|
| def test_form_data_validation_and_submission(self, db_session: Session): |
| """ |
| Test form data validation and submission. |
| |
| Scenario: |
| - Create canvas with form fields |
| - Submit form data |
| - Verify data stored in database |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| form_config = { |
| "type": "form", |
| "fields": [ |
| {"name": "email", "type": "email", "required": True}, |
| {"name": "name", "type": "text", "required": True} |
| ] |
| } |
|
|
| canvas_audit = CanvasAuditFactory( |
| canvas_id=str(uuid.uuid4()), |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="form", |
| component_type="form", |
| component_name="form", |
| action="create", |
| metadata={"form_config": form_config}, |
| _session=db_session |
| ) |
| db_session.add(canvas_audit) |
| db_session.commit() |
|
|
| |
| submission_audit = CanvasAuditFactory( |
| canvas_id=canvas_audit.canvas_id, |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="form", |
| component_type="form", |
| component_name="form", |
| action="submit", |
| metadata={ |
| "form_data": { |
| "email": "test@example.com", |
| "name": "Test User" |
| } |
| }, |
| _session=db_session |
| ) |
| db_session.add(submission_audit) |
| db_session.commit() |
|
|
| |
| submissions = db_session.query(CanvasAudit).filter( |
| CanvasAudit.canvas_id == canvas_audit.canvas_id, |
| CanvasAudit.action == "submit" |
| ).all() |
|
|
| assert len(submissions) == 1, "Should have 1 submission" |
| assert submissions[0].metadata["form_data"]["email"] == "test@example.com" |
|
|
| def test_governance_enforcement_on_canvas(self, db_session: Session): |
| """ |
| Test governance enforcement on canvas actions. |
| |
| Scenario: |
| - Create STUDENT agent |
| - Verify governance blocks certain actions |
| - Create AUTONOMOUS agent |
| - Verify actions allowed |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| |
| student_agent = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| confidence_score=0.3, |
| _session=db_session |
| ) |
| db_session.add(student_agent) |
| db_session.commit() |
| db_session.refresh(student_agent) |
|
|
| |
| governance_service = AgentGovernanceService(db_session) |
|
|
| |
| result = governance_service.can_perform_action( |
| agent_id=student_agent.id, |
| action_type="canvas_form" |
| ) |
|
|
| |
| assert "allowed" in result, "Should return allowed field" |
|
|
| |
| autonomous_agent = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| _session=db_session |
| ) |
| db_session.add(autonomous_agent) |
| db_session.commit() |
| db_session.refresh(autonomous_agent) |
|
|
| |
| result = governance_service.can_perform_action( |
| agent_id=autonomous_agent.id, |
| action_type="canvas_form" |
| ) |
|
|
| assert result["allowed"] is True, "AUTONOMOUS agent should be allowed" |
|
|
| |
| canvas_audit = CanvasAuditFactory( |
| canvas_id=str(uuid.uuid4()), |
| agent_id=autonomous_agent.id, |
| user_id=user.id, |
| canvas_type="form", |
| action="create", |
| _session=db_session |
| ) |
| db_session.add(canvas_audit) |
| db_session.commit() |
|
|
| |
| autonomous_audits = db_session.query(CanvasAudit).filter( |
| CanvasAudit.agent_id == autonomous_agent.id |
| ).all() |
| assert len(autonomous_audits) == 1, "AUTONOMOUS agent should have canvas audit" |
|
|
| def test_websocket_canvas_updates(self, db_session: Session): |
| """ |
| Test WebSocket canvas state updates. |
| |
| Scenario: |
| - Create canvas |
| - Simulate WebSocket update |
| - Verify canvas state updated |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| canvas_id = str(uuid.uuid4()) |
|
|
| |
| create_audit = CanvasAuditFactory( |
| canvas_id=canvas_id, |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="line_chart", |
| action="create", |
| _session=db_session |
| ) |
| db_session.add(create_audit) |
| db_session.commit() |
|
|
| |
| update_audit = CanvasAuditFactory( |
| canvas_id=canvas_id, |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="line_chart", |
| action="update", |
| _session=db_session |
| ) |
| db_session.add(update_audit) |
| db_session.commit() |
|
|
| |
| canvas_actions = db_session.query(CanvasAudit).filter( |
| CanvasAudit.canvas_id == canvas_id |
| ).order_by(CanvasAudit.created_at).all() |
|
|
| assert len(canvas_actions) == 2, "Should have create and update actions" |
|
|
| def test_canvas_state_persistence(self, db_session: Session): |
| """ |
| Test canvas state persistence to database. |
| |
| Scenario: |
| - Create canvas with initial state |
| - Update canvas state |
| - Verify state persisted to database |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| _session=db_session |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| canvas_id = str(uuid.uuid4()) |
|
|
| |
| canvas_audit = CanvasAuditFactory( |
| canvas_id=canvas_id, |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="line_chart", |
| action="create", |
| _session=db_session |
| ) |
| db_session.add(canvas_audit) |
| db_session.commit() |
| db_session.refresh(canvas_audit) |
|
|
| |
| latest_audit = db_session.query(CanvasAudit).filter( |
| CanvasAudit.canvas_id == canvas_id |
| ).order_by(CanvasAudit.created_at.desc()).first() |
|
|
| assert latest_audit is not None, "Should retrieve canvas from database" |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.integration |
| class TestGraduationPromotionFlow: |
| """ |
| End-to-end tests for graduation promotion flow: |
| Graduation criteria → Constitutional check → Promotion execution → Maturity update |
| """ |
|
|
| def test_graduation_criteria_calculation(self, db_session: Session): |
| """ |
| Test graduation criteria calculation. |
| |
| Scenario: |
| - Create agent with 10 episodes |
| - Set intervention rate to 40% |
| - Set constitutional score to 0.70 |
| - Calculate readiness |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| confidence_score=0.4, |
| _session=db_session |
| ) |
| |
| agent.configuration = { |
| "episode_count": 10, |
| "intervention_rate": 0.40, |
| "constitutional_score": 0.70, |
| } |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| assert agent.configuration["episode_count"] >= 10, "Should have 10 episodes" |
| assert agent.configuration["intervention_rate"] <= 0.50, "Intervention rate should be <= 50%" |
| assert agent.configuration["constitutional_score"] >= 0.70, "Constitutional score should be >= 0.70" |
|
|
| def test_constitutional_compliance_validation(self, db_session: Session): |
| """ |
| Test constitutional compliance validation. |
| |
| Scenario: |
| - Create agent meeting episode/intervention criteria |
| - Set constitutional score below threshold (0.65) |
| - Verify promotion blocked |
| - Set constitutional score above threshold (0.75) |
| - Verify compliance check passes |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| |
| agent_low = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| _session=db_session |
| ) |
| agent_low.configuration = { |
| "episode_count": 10, |
| "intervention_rate": 0.40, |
| "constitutional_score": 0.65, |
| } |
| db_session.add(agent_low) |
| db_session.commit() |
| db_session.refresh(agent_low) |
|
|
| |
| assert agent_low.configuration["constitutional_score"] < 0.70, "Constitutional score below threshold" |
|
|
| |
| agent_high = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| _session=db_session |
| ) |
| agent_high.configuration = { |
| "episode_count": 10, |
| "intervention_rate": 0.40, |
| "constitutional_score": 0.75, |
| } |
| db_session.add(agent_high) |
| db_session.commit() |
| db_session.refresh(agent_high) |
|
|
| |
| assert agent_high.configuration["constitutional_score"] >= 0.70, "Constitutional score above threshold" |
|
|
| def test_end_to_end_graduation_flow(self, db_session: Session): |
| """ |
| Test complete graduation flow across all levels. |
| |
| Scenario: |
| - Create qualified STUDENT agent |
| - Execute full promotion process |
| - Verify agent.status transitions to INTERN |
| - Repeat for INTERN → SUPERVISED |
| - Repeat for SUPERVISED → AUTONOMOUS |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| |
| agent = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| _session=db_session |
| ) |
| agent.configuration = { |
| "episode_count": 10, |
| "intervention_rate": 0.40, |
| "constitutional_score": 0.75, |
| } |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| agent.status = AgentStatus.INTERN.value |
| agent.confidence_score = 0.6 |
| agent.configuration["promoted_at"] = datetime.utcnow().isoformat() |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| assert agent.status == AgentStatus.INTERN.value, "Should promote to INTERN" |
|
|
| |
| agent.configuration["episode_count"] = 25 |
| agent.configuration["intervention_rate"] = 0.15 |
| agent.configuration["constitutional_score"] = 0.85 |
| agent.status = AgentStatus.SUPERVISED.value |
| agent.confidence_score = 0.8 |
| agent.configuration["promoted_at"] = datetime.utcnow().isoformat() |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| assert agent.status == AgentStatus.SUPERVISED.value, "Should promote to SUPERVISED" |
|
|
| |
| agent.configuration["episode_count"] = 50 |
| agent.configuration["intervention_rate"] = 0.0 |
| agent.configuration["constitutional_score"] = 0.95 |
| agent.status = AgentStatus.AUTONOMOUS.value |
| agent.confidence_score = 0.95 |
| agent.configuration["promoted_at"] = datetime.utcnow().isoformat() |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| assert agent.status == AgentStatus.AUTONOMOUS.value, "Should promote to AUTONOMOUS" |
|
|
| def test_readiness_score_calculation(self, db_session: Session): |
| """ |
| Test readiness score calculation formula. |
| |
| Scenario: |
| - Test 40% episodes, 30% interventions, 30% constitutional split |
| - Verify formula: readiness = 0.4*episode_score + 0.3*intervention_score + 0.3*constitutional_score |
| """ |
| |
| episode_score = 1.0 |
| intervention_score = 1.0 |
| constitutional_score = 1.0 |
|
|
| readiness = 0.4 * episode_score + 0.3 * intervention_score + 0.3 * constitutional_score |
| assert readiness == 1.0, "Perfect scores should give readiness = 1.0" |
|
|
| |
| agent = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| _session=db_session |
| ) |
| agent.configuration = { |
| "episode_count": 10, |
| "intervention_rate": 0.50, |
| "constitutional_score": 0.70, |
| } |
| db_session.add(agent) |
| db_session.commit() |
|
|
| |
| assert agent.configuration["episode_count"] == 10, "Episode count at threshold" |
| assert agent.configuration["intervention_rate"] == 0.50, "Intervention rate at threshold" |
| assert agent.configuration["constitutional_score"] == 0.70, "Constitutional score at threshold" |
|
|
| def test_promotion_rejection(self, db_session: Session): |
| """ |
| Test promotion rejection when criteria not met. |
| |
| Scenario: |
| - Create agent with insufficient episodes (5) |
| - Try to promote to INTERN |
| - Verify promotion rejected |
| - Verify agent status unchanged (STUDENT) |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| |
| agent1 = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| _session=db_session |
| ) |
| agent1.configuration = { |
| "episode_count": 5, |
| "intervention_rate": 0.30, |
| "constitutional_score": 0.75, |
| } |
| db_session.add(agent1) |
| db_session.commit() |
| db_session.refresh(agent1) |
|
|
| |
| assert agent1.configuration["episode_count"] < 10, "Episode count below threshold" |
| assert agent1.status == AgentStatus.STUDENT.value, "Status should remain STUDENT" |
|
|
| |
| agent2 = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| _session=db_session |
| ) |
| agent2.configuration = { |
| "episode_count": 10, |
| "intervention_rate": 0.60, |
| "constitutional_score": 0.75, |
| } |
| db_session.add(agent2) |
| db_session.commit() |
| db_session.refresh(agent2) |
|
|
| |
| assert agent2.configuration["intervention_rate"] > 0.50, "Intervention rate above threshold" |
| assert agent2.status == AgentStatus.STUDENT.value, "Status should remain STUDENT" |
|
|
| def test_maturity_update_persistence(self, db_session: Session): |
| """ |
| Test maturity state persistence to database. |
| |
| Scenario: |
| - Promote agent |
| - Verify database updated immediately |
| - Query agent from new session |
| - Verify status persisted |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| confidence_score=0.4, |
| _session=db_session |
| ) |
| agent.configuration = { |
| "episode_count": 10, |
| "intervention_rate": 0.40, |
| "constitutional_score": 0.75, |
| } |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| agent.status = AgentStatus.INTERN.value |
| agent.confidence_score = 0.6 |
| agent.configuration["promoted_at"] = datetime.utcnow().isoformat() |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| persisted_agent = db_session.query(AgentRegistry).filter( |
| AgentRegistry.id == agent.id |
| ).first() |
|
|
| |
| assert persisted_agent is not None, "Agent should be retrievable" |
| assert persisted_agent.status == AgentStatus.INTERN.value, "Status should persist" |
| assert persisted_agent.confidence_score == 0.6, "Confidence score should persist" |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.integration |
| class TestCrossCuttingConcerns: |
| """ |
| Tests covering shared concerns across all critical paths: |
| - Governance enforcement |
| - Data integrity |
| - Audit trail completeness |
| - Error recovery |
| - Concurrency |
| """ |
|
|
| def test_governance_bypass_prevention(self, db_session: Session): |
| """ |
| Test governance enforcement at all maturity levels. |
| |
| Scenario: |
| - Test governance check at all maturity levels |
| - Verify STUDENT cannot perform restricted actions |
| - Verify AUTONOMOUS can perform all actions |
| """ |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| governance_service = AgentGovernanceService(db_session) |
|
|
| |
| student = AgentFactory( |
| status=AgentStatus.STUDENT.value, |
| confidence_score=0.3, |
| _session=db_session |
| ) |
| db_session.add(student) |
|
|
| |
| intern = AgentFactory( |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.6, |
| _session=db_session |
| ) |
| db_session.add(intern) |
|
|
| |
| autonomous = AgentFactory( |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95, |
| _session=db_session |
| ) |
| db_session.add(autonomous) |
| db_session.commit() |
|
|
| |
| |
| student_result = governance_service.can_perform_action( |
| agent_id=student.id, |
| action_type="execute" |
| ) |
| assert student_result["allowed"] is False, "STUDENT should be blocked from execute" |
|
|
| |
| auto_result = governance_service.can_perform_action( |
| agent_id=autonomous.id, |
| action_type="execute" |
| ) |
| assert auto_result["allowed"] is True, "AUTONOMOUS should be allowed for execute" |
|
|
| def test_data_integrity_across_paths(self, db_session: Session): |
| """ |
| Test data consistency across critical paths. |
| |
| Scenario: |
| - Create data in agent execution flow |
| - Access from episode flow (same agent) |
| - Verify data consistency |
| - Verify foreign keys maintained |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory(_session=db_session) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| execution = AgentExecutionFactory( |
| agent_id=agent.id, |
| status="completed", |
| _session=db_session |
| ) |
| db_session.add(execution) |
|
|
| |
| episode = EpisodeFactory( |
| agent_id=agent.id, |
| user_id=user.id, |
| _session=db_session |
| ) |
| db_session.add(episode) |
|
|
| |
| canvas_audit = CanvasAuditFactory( |
| canvas_id=str(uuid.uuid4()), |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="line_chart", |
| action="create", |
| _session=db_session |
| ) |
| db_session.add(canvas_audit) |
| db_session.commit() |
|
|
| |
| assert execution.agent_id == agent.id, "Execution should link to agent" |
| assert episode.agent_id == agent.id, "Episode should link to agent" |
| assert canvas_audit.agent_id == agent.id, "Canvas audit should link to agent" |
|
|
| def test_audit_trail_completeness(self, db_session: Session): |
| """ |
| Test audit trail completeness across all paths. |
| |
| Scenario: |
| - Execute agent request |
| - Create episode |
| - Create canvas |
| - Verify all actions logged in audit trail |
| - Verify timestamps sequential |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory(_session=db_session) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| timestamps = [] |
|
|
| |
| execution = AgentExecutionFactory( |
| agent_id=agent.id, |
| status="completed", |
| _session=db_session |
| ) |
| db_session.add(execution) |
| db_session.commit() |
| timestamps.append(("execution", execution.started_at)) |
|
|
| |
| episode = EpisodeFactory( |
| agent_id=agent.id, |
| user_id=user.id, |
| _session=db_session |
| ) |
| db_session.add(episode) |
| db_session.commit() |
| timestamps.append(("episode", episode.created_at)) |
|
|
| |
| canvas_audit = CanvasAuditFactory( |
| canvas_id=str(uuid.uuid4()), |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="line_chart", |
| action="create", |
| _session=db_session |
| ) |
| db_session.add(canvas_audit) |
| db_session.commit() |
| timestamps.append(("canvas", canvas_audit.created_at)) |
|
|
| |
| assert len(timestamps) == 3, "Should have 3 audit entries" |
|
|
| def test_error_recovery_across_paths(self, db_session: Session): |
| """ |
| Test error recovery and isolation between paths. |
| |
| Scenario: |
| - Trigger error in agent execution |
| - Verify cleanup occurs |
| - Continue to episode creation |
| - Verify no corruption from previous error |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory(_session=db_session) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| failed_execution = AgentExecutionFactory( |
| agent_id=agent.id, |
| status="failed", |
| error_message="Simulated error", |
| _session=db_session |
| ) |
| db_session.add(failed_execution) |
| db_session.commit() |
|
|
| |
| assert failed_execution.status == "failed", "Failed execution should be logged" |
| assert failed_execution.error_message is not None, "Error message should be present" |
|
|
| |
| episode = EpisodeFactory( |
| agent_id=agent.id, |
| user_id=user.id, |
| _session=db_session |
| ) |
| db_session.add(episode) |
| db_session.commit() |
|
|
| |
| assert episode.id is not None, "Episode should be created" |
| assert episode.agent_id == agent.id, "Episode should link to agent" |
|
|
| def test_concurrency_across_paths(self, db_session: Session): |
| """ |
| Test concurrent operations across paths. |
| |
| Scenario: |
| - Execute agent request while creating episode |
| - Verify no race conditions |
| - Verify both operations complete |
| """ |
| user = UserFactory(_session=db_session) |
| db_session.add(user) |
| db_session.commit() |
|
|
| agent = AgentFactory(_session=db_session) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| |
|
|
| |
| execution = AgentExecutionFactory( |
| agent_id=agent.id, |
| status="completed", |
| _session=db_session |
| ) |
| db_session.add(execution) |
|
|
| |
| episode = EpisodeFactory( |
| agent_id=agent.id, |
| user_id=user.id, |
| _session=db_session |
| ) |
| db_session.add(episode) |
|
|
| |
| canvas_audit = CanvasAuditFactory( |
| canvas_id=str(uuid.uuid4()), |
| agent_id=agent.id, |
| user_id=user.id, |
| canvas_type="line_chart", |
| action="create", |
| _session=db_session |
| ) |
| db_session.add(canvas_audit) |
|
|
| |
| db_session.commit() |
|
|
| |
| assert execution.id is not None, "Execution should complete" |
| assert episode.id is not None, "Episode should complete" |
| assert canvas_audit.id is not None, "Canvas should complete" |
|
|
| |
| executions = db_session.query(AgentExecution).filter( |
| AgentExecution.agent_id == agent.id |
| ).all() |
| assert len(executions) == 1, "Should have exactly 1 execution" |
|
|
| episodes = db_session.query(Episode).filter( |
| Episode.agent_id == agent.id |
| ).all() |
| assert len(episodes) == 1, "Should have exactly 1 episode" |
|
|
| canvases = db_session.query(CanvasAudit).filter( |
| CanvasAudit.agent_id == agent.id |
| ).all() |
| assert len(canvases) == 1, "Should have exactly 1 canvas" |
|
|