| """ |
| Agent Execution Memory Leak Tests |
| |
| This module contains memory leak detection tests for agent execution operations. |
| These tests detect Python-level memory leaks during repeated agent execution using |
| Bloomberg's memray profiler. |
| |
| Test Categories: |
| - Single-agent execution leaks: Memory growth during 100 serial executions |
| - Concurrent agent execution leaks: Thread-safe memory accumulation detection |
| - Agent registry operations: Memory leaks during agent registration/updates |
| |
| Invariants Tested: |
| - INV-01: Agent execution should not grow memory (>10MB over 100 executions) |
| - INV-02: Agent execution should not accumulate allocations (>1000 per execution) |
| - INV-03: Concurrent execution should not leak thread-local memory |
| - INV-04: Agent registry operations should not leak (registration, updates) |
| |
| Performance Targets: |
| - Single-agent execution: <10MB memory growth (100 iterations) |
| - Concurrent execution: <20MB memory growth (10 threads × 10 iterations) |
| - Agent registry: <5MB memory growth (100 operations) |
| |
| Requirements: |
| - Python 3.11+ (memray requirement) |
| - memray>=1.12.0 (install with: pip install memray) |
| |
| Usage: |
| # Run all agent execution leak tests |
| pytest backend/tests/memory_leaks/test_agent_execution_leaks.py -v |
| |
| # Run specific test |
| pytest backend/tests/memory_leaks/test_agent_execution_leaks.py::test_agent_execution_no_leak -v |
| |
| # Run with memory leak marker |
| pytest backend/tests/memory_leaks/ -v -m memory_leak |
| |
| Phase: 243-01 (Memory & Performance Bug Discovery) |
| See: .planning/phases/243-memory-and-performance-bug-discovery/243-01-PLAN.md |
| """ |
|
|
| from typing import Optional |
| from unittest.mock import Mock, patch |
|
|
| import pytest |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.memory_leak |
| @pytest.mark.slow |
| def test_agent_execution_no_leak(memray_session, check_memory_growth): |
| """ |
| Test that agent execution does not leak memory over 100 iterations. |
| |
| INVARIANT: Agent execution should not grow memory (>10MB over 100 executions) |
| |
| STRATEGY: |
| - Execute mock agent 100 times in loop (amplification for leak detection) |
| - Track memory using memray_session fixture |
| - Assert <10MB memory growth (threshold from Phase 243-01 requirements) |
| |
| RADII: |
| - 100 iterations sufficient to amplify small leaks (1KB/iter → 100KB) |
| - Detects cumulative leaks from unclosed connections, cache misses |
| - Based on industry standard for memory leak testing (Google, Meta) |
| |
| Test Metadata: |
| - Iterations: 100 |
| - Threshold: 10MB |
| - Amplification: 100x (small leaks become detectable) |
| |
| Examples: |
| >>> # Run test (requires memray) |
| >>> pytest test_agent_execution_leaks.py::test_agent_execution_no_leak -v |
| |
| Phase: 243-01 |
| TQ-01 through TQ-05 compliant (invariant-first, documented, clear assertions) |
| |
| Args: |
| memray_session: memray.Tracker fixture for memory profiling |
| check_memory_growth: Helper fixture for asserting memory thresholds |
| |
| Raises: |
| AssertionError: If memory growth exceeds 10MB threshold |
| """ |
| from core.agent_governance_service import AgentGovernanceService |
|
|
| |
| db_session = Mock() |
| governance = AgentGovernanceService(db_session) |
|
|
| |
| for i in range(100): |
| try: |
| |
| with patch.object(governance, 'register_or_update_agent') as mock_exec: |
| mock_exec.return_value = Mock( |
| id=f"agent_{i}", |
| name=f"Test Agent {i}", |
| status="STUDENT" |
| ) |
|
|
| |
| governance.register_or_update_agent( |
| name=f"TestAgent{i}", |
| category="test", |
| module_path="test.module", |
| class_name="TestAgent" |
| ) |
| except Exception as e: |
| |
| pytest.logger.warning(f"Iteration {i} failed: {e}") |
|
|
| |
| check_memory_growth( |
| memray_session, |
| threshold_mb=10.0, |
| context_msg="Agent execution (100 iterations)" |
| ) |
|
|
|
|
| @pytest.mark.memory_leak |
| @pytest.mark.slow |
| def test_agent_execution_allocation_count(memray_session, assert_allocation_count): |
| """ |
| Test that agent execution does not accumulate excessive allocations. |
| |
| INVARIANT: Agent execution should not accumulate allocations (>1000 per execution) |
| |
| STRATEGY: |
| - Execute mock agent 100 times in loop |
| - Track allocation count using memray.Stats |
| - Assert <1000 allocations per execution |
| |
| RADII: |
| - 100 iterations provide statistical significance |
| - Detects allocation hotspots (object creation, string formatting) |
| - Based on industry benchmarks (Chrome, Firefox leak detection) |
| |
| Test Metadata: |
| - Iterations: 100 |
| - Max allocations: 1000 |
| - Per-execution limit: 10 allocations/iteration |
| |
| Phase: 243-01 |
| TQ-01 through TQ-05 compliant |
| |
| Args: |
| memray_session: memray.Tracker fixture for memory profiling |
| assert_allocation_count: Helper fixture for allocation thresholds |
| |
| Raises: |
| AssertionError: If allocation count exceeds 1000 threshold |
| """ |
| from core.agent_governance_service import AgentGovernanceService |
|
|
| |
| db_session = Mock() |
| governance = AgentGovernanceService(db_session) |
|
|
| |
| for i in range(100): |
| try: |
| with patch.object(governance, 'register_or_update_agent') as mock_exec: |
| mock_exec.return_value = Mock( |
| id=f"agent_{i}", |
| name=f"Test Agent {i}", |
| status="STUDENT" |
| ) |
|
|
| governance.register_or_update_agent( |
| name=f"TestAgent{i}", |
| category="test", |
| module_path="test.module", |
| class_name="TestAgent" |
| ) |
| except Exception as e: |
| pytest.logger.warning(f"Iteration {i} failed: {e}") |
|
|
| |
| assert_allocation_count( |
| memray_session, |
| max_allocations=1000, |
| context_msg="Agent execution allocation count" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.memory_leak |
| @pytest.mark.slow |
| def test_agent_concurrent_execution_leaks(memray_session, check_memory_growth): |
| """ |
| Test that concurrent agent execution does not leak thread-local memory. |
| |
| INVARIANT: Concurrent execution should not leak thread-local memory |
| |
| STRATEGY: |
| - Test concurrent agent execution (10 parallel threads, 10 iterations each) |
| - Detect thread-unsafe memory accumulation (thread-local storage, GIL contention) |
| - Assert memory growth proportional to concurrency (<20MB for 10×10) |
| |
| RADII: |
| - 10 threads × 10 iterations = 100 total executions |
| - Detects threading leaks (unclosed thread-local connections) |
| - Based on industry standards (Python threading best practices) |
| |
| Test Metadata: |
| - Threads: 10 |
| - Iterations per thread: 10 |
| - Total executions: 100 |
| - Threshold: 20MB (2× single-agent threshold due to concurrency) |
| |
| Examples: |
| >>> # Run test (requires memray) |
| >>> pytest test_agent_execution_leaks.py::test_agent_concurrent_execution_leaks -v |
| |
| Phase: 243-01 |
| TQ-01 through TQ-05 compliant |
| |
| Args: |
| memray_session: memray.Tracker fixture for memory profiling |
| check_memory_growth: Helper fixture for asserting memory thresholds |
| |
| Raises: |
| AssertionError: If memory growth exceeds 20MB threshold |
| """ |
| import concurrent.futures |
| from core.agent_governance_service import AgentGovernanceService |
|
|
| |
| db_session = Mock() |
|
|
| def execute_agent_thread(thread_id: int, iteration: int) -> None: |
| """Execute agent in thread (testing thread-local memory).""" |
| governance = AgentGovernanceService(db_session) |
|
|
| try: |
| with patch.object(governance, 'register_or_update_agent') as mock_exec: |
| mock_exec.return_value = Mock( |
| id=f"agent_{thread_id}_{iteration}", |
| name=f"Test Agent {thread_id}-{iteration}", |
| status="STUDENT" |
| ) |
|
|
| governance.register_or_update_agent( |
| name=f"TestAgent{thread_id}_{iteration}", |
| category="test", |
| module_path="test.module", |
| class_name="TestAgent" |
| ) |
| except Exception as e: |
| pytest.logger.warning(f"Thread {thread_id} iteration {iteration} failed: {e}") |
|
|
| |
| with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: |
| futures = [] |
| for thread_id in range(10): |
| for iteration in range(10): |
| future = executor.submit(execute_agent_thread, thread_id, iteration) |
| futures.append(future) |
|
|
| |
| concurrent.futures.wait(futures) |
|
|
| |
| check_memory_growth( |
| memray_session, |
| threshold_mb=20.0, |
| context_msg="Concurrent agent execution (10 threads × 10 iterations)" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.memory_leak |
| @pytest.mark.slow |
| def test_agent_registry_no_leak(memray_session, check_memory_growth): |
| """ |
| Test that agent registry operations do not leak memory. |
| |
| INVARIANT: Agent registry operations should not leak memory |
| |
| STRATEGY: |
| - Perform 100 agent registration operations |
| - Perform 100 agent update operations |
| - Assert <5MB memory growth (registry operations are lightweight) |
| |
| RADII: |
| - 100 operations provide statistical significance |
| - Detects cache leaks, query result accumulation |
| - Registry operations should be memory-efficient (no caching bloat) |
| |
| Test Metadata: |
| - Registrations: 100 |
| - Updates: 100 |
| - Total operations: 200 |
| - Threshold: 5MB (lightweight operations) |
| |
| Phase: 243-01 |
| TQ-01 through TQ-05 compliant |
| |
| Args: |
| memray_session: memray.Tracker fixture for memory profiling |
| check_memory_growth: Helper fixture for asserting memory thresholds |
| |
| Raises: |
| AssertionError: If memory growth exceeds 5MB threshold |
| """ |
| from core.agent_governance_service import AgentGovernanceService |
|
|
| |
| db_session = Mock() |
| governance = AgentGovernanceService(db_session) |
|
|
| |
| for i in range(100): |
| try: |
| with patch.object(db_session, 'query') as mock_query: |
| mock_query.return_value.filter.return_value.first.return_value = None |
|
|
| with patch.object(db_session, 'add'): |
| with patch.object(db_session, 'commit'): |
| governance.register_or_update_agent( |
| name=f"TestAgent{i}", |
| category="test", |
| module_path=f"test.module{i}", |
| class_name="TestAgent" |
| ) |
| except Exception as e: |
| pytest.logger.warning(f"Registration {i} failed: {e}") |
|
|
| |
| for i in range(100): |
| try: |
| |
| existing_agent = Mock( |
| id=f"agent_{i}", |
| name=f"Test Agent {i}", |
| status="STUDENT" |
| ) |
|
|
| with patch.object(db_session, 'query') as mock_query: |
| mock_query.return_value.filter.return_value.first.return_value = existing_agent |
|
|
| with patch.object(db_session, 'commit'): |
| governance.register_or_update_agent( |
| name=f"UpdatedAgent{i}", |
| category="test", |
| module_path=f"test.module{i}", |
| class_name="TestAgent" |
| ) |
| except Exception as e: |
| pytest.logger.warning(f"Update {i} failed: {e}") |
|
|
| |
| check_memory_growth( |
| memray_session, |
| threshold_mb=5.0, |
| context_msg="Agent registry operations (100 registrations + 100 updates)" |
| ) |
|
|