| """ |
| ⚠️ PROTECTED PROPERTY-BASED TEST ⚠️ |
| |
| This file tests CRITICAL SYSTEM INVARIANTS for the Atom platform. |
| |
| DO NOT MODIFY THIS FILE unless: |
| 1. You are fixing a TEST BUG (not an implementation bug) |
| 2. You are ADDING new invariants |
| 3. You have EXPLICIT APPROVAL from engineering lead |
| |
| These tests must remain IMPLEMENTATION-AGNOSTIC. |
| Test only observable behaviors and public API contracts. |
| |
| Protection: tests/.protection_markers/PROPERTY_TEST_GUARDIAN.md |
| """ |
|
|
| import asyncio |
| import uuid |
|
|
| import pytest |
| from hypothesis import given, settings, HealthCheck |
| from hypothesis import strategies as st |
| from sqlalchemy.orm import Session |
|
|
| from core.agent_context_resolver import AgentContextResolver |
| from core.models import AgentRegistry, AgentStatus, User, UserRole, ChatSession |
|
|
|
|
| class TestAgentContextResolverContracts: |
| """Test AgentContextResolver maintains its interface contracts.""" |
|
|
| @given( |
| requested_agent_id=st.sampled_from([None, "invalid_agent_id", ""]) |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_resolve_agent_always_returns_agent_or_none( |
| self, db_session: Session, requested_agent_id: str |
| ): |
| """ |
| CONTRACT: resolve_agent_for_request MUST NEVER crash. |
| |
| Should always return (agent or None, context_dict), never raise exception. |
| """ |
| |
| resolver = AgentContextResolver(db_session) |
|
|
| |
| import uuid |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| import asyncio |
| try: |
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=requested_agent_id, |
| action_type="chat" |
| )) |
|
|
| |
| assert context is not None, "Context must never be None" |
| assert isinstance(context, dict), "Context must be dict" |
|
|
| |
| if agent is not None: |
| assert isinstance(agent, AgentRegistry), "Agent must be AgentRegistry instance" |
|
|
| except Exception as e: |
| pytest.fail(f"resolve_agent_for_request crashed: {e}") |
|
|
| @given( |
| use_explicit_agent=st.booleans(), |
| use_session_agent=st.booleans() |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_resolve_agent_fallback_chain( |
| self, db_session: Session, use_explicit_agent: bool, use_session_agent: bool |
| ): |
| """ |
| CONTRACT: resolve_agent_for_request MUST follow fallback chain. |
| |
| Priority order: |
| 1. Explicit agent_id |
| 2. Session agent |
| 3. System default |
| """ |
| |
| resolver = AgentContextResolver(db_session) |
|
|
| |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| explicit_agent = AgentRegistry( |
| name="ExplicitAgent", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.5, |
| |
| ) |
| db_session.add(explicit_agent) |
| db_session.commit() |
| db_session.refresh(explicit_agent) |
|
|
| |
| session = ChatSession( |
| user_id=user.id, |
| title="Test Session" |
| ) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
|
|
| session_agent = AgentRegistry( |
| name="SessionAgent", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.5, |
| |
| ) |
| db_session.add(session_agent) |
| db_session.commit() |
| db_session.refresh(session_agent) |
|
|
| |
| resolver.set_session_agent(session.id, session_agent.id) |
|
|
| |
| if use_explicit_agent: |
| expected_agent_id = explicit_agent.id |
| expected_path = ["explicit_agent_id"] |
| elif use_session_agent: |
| expected_agent_id = session_agent.id |
| expected_path = ["session_agent"] |
| else: |
| |
| expected_agent_id = None |
| expected_path = ["system_default"] |
|
|
| |
| requested_id = explicit_agent.id if use_explicit_agent else None |
| session_id = session.id if use_session_agent else None |
|
|
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=session_id, |
| requested_agent_id=requested_id, |
| action_type="chat" |
| )) |
|
|
| |
| assert agent is not None, "Should always resolve to an agent" |
| assert isinstance(agent, AgentRegistry), "Agent must be AgentRegistry" |
|
|
| |
| if use_explicit_agent: |
| assert "explicit_agent_id" in context["resolution_path"], \ |
| "Should use explicit agent when provided" |
| elif use_session_agent: |
| assert "session_agent" in context["resolution_path"], \ |
| "Should use session agent when no explicit agent" |
|
|
| |
| assert "user_id" in context, "Context must have 'user_id'" |
| assert "resolution_path" in context, "Context must have 'resolution_path'" |
| assert "resolved_at" in context, "Context must have 'resolved_at'" |
|
|
| @given( |
| agent_status=st.sampled_from([ |
| AgentStatus.STUDENT.value, |
| AgentStatus.INTERN.value, |
| AgentStatus.SUPERVISED.value, |
| AgentStatus.AUTONOMOUS.value, |
| ]), |
| action_type=st.text(min_size=1, max_size=50).filter(lambda x: x.strip()) |
| ) |
| @settings(max_examples=150, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_validate_agent_for_action_returns_dict( |
| self, db_session: Session, agent_status: str, action_type: str |
| ): |
| """ |
| CONTRACT: validate_agent_for_action MUST return governance decision dict. |
| |
| Returns same structure as AgentGovernanceService.can_perform_action. |
| """ |
| |
| resolver = AgentContextResolver(db_session) |
| agent = AgentRegistry( |
| name="TestAgent", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=agent_status, |
| confidence_score=0.5, |
| |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| decision = resolver.validate_agent_for_action(agent, action_type) |
|
|
| |
| assert isinstance(decision, dict), "validate_agent_for_action must return dict" |
|
|
| |
| required_fields = ["allowed", "reason", "requires_human_approval"] |
| for field in required_fields: |
| assert field in decision, f"Missing required field: {field}" |
|
|
| assert isinstance(decision["allowed"], bool), "'allowed' must be bool" |
| assert isinstance(decision["reason"], str), "'reason' must be str" |
|
|
| @given( |
| valid_session=st.booleans(), |
| valid_agent=st.booleans() |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_set_session_agent_returns_bool( |
| self, db_session: Session, valid_session: bool, valid_agent: bool |
| ): |
| """ |
| CONTRACT: set_session_agent MUST return bool. |
| |
| Returns True on success, False on failure. |
| """ |
| |
| resolver = AgentContextResolver(db_session) |
|
|
| |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
|
|
| session_id = None |
| if valid_session: |
| session = ChatSession( |
| user_id=user.id, |
| title="Test Session" |
| ) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
| session_id = session.id |
| else: |
| session_id = str(uuid.uuid4()) |
|
|
| agent_id = None |
| if valid_agent: |
| agent = AgentRegistry( |
| name="TestAgent", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.5, |
|
|
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
| agent_id = agent.id |
| else: |
| agent_id = str(uuid.uuid4()) |
|
|
| |
| result = resolver.set_session_agent(session_id, agent_id) |
|
|
| |
| assert isinstance(result, bool), "set_session_agent must return bool" |
|
|
| |
| if valid_session and valid_agent: |
| assert result is True, "Should return True for valid session and agent" |
| else: |
| assert result is False, "Should return False for invalid session or agent" |
|
|
|
|
| class TestContextValidationInvariants: |
| """Property-based tests for context validation invariants.""" |
|
|
| @given( |
| user_role=st.sampled_from([ |
| UserRole.MEMBER.value, |
| UserRole.ADMIN.value, |
| ]), |
| action_type=st.sampled_from([ |
| "chat", "stream", "present", "submit", |
| "browser_navigate", "device_camera" |
| ]) |
| ) |
| @settings(max_examples=100, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_context_always_contains_user_id( |
| self, db_session: Session, user_role: str, action_type: str |
| ): |
| """INVARIANT: Context must always contain user_id field.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=user_role, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=None, |
| action_type=action_type |
| )) |
|
|
| |
| assert "user_id" in context, "Context must contain user_id" |
| assert context["user_id"] == user.id, "user_id must match" |
| assert isinstance(context["user_id"], str), "user_id must be string" |
|
|
| @given( |
| agent_maturity=st.sampled_from([ |
| AgentStatus.STUDENT.value, |
| AgentStatus.INTERN.value, |
| AgentStatus.SUPERVISED.value, |
| AgentStatus.AUTONOMOUS.value, |
| ]), |
| confidence_score=st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False) |
| ) |
| @settings(max_examples=100, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_context_includes_agent_status( |
| self, db_session: Session, agent_maturity: str, confidence_score: float |
| ): |
| """INVARIANT: Context must include agent maturity information.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| agent = AgentRegistry( |
| name=f"Agent_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=agent_maturity, |
| confidence_score=confidence_score, |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| resolved_agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=agent.id, |
| action_type="chat" |
| )) |
|
|
| |
| assert resolved_agent is not None, "Agent should be resolved" |
| assert "requested_agent_id" in context, "Context must contain requested_agent_id" |
| assert context["requested_agent_id"] == agent.id, "requested_agent_id must match" |
| |
| assert resolved_agent.status == agent_maturity, "Agent status must match" |
|
|
| @given( |
| resolution_path_length=st.integers(min_value=1, max_value=5) |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_context_resolution_path_non_empty( |
| self, db_session: Session, resolution_path_length: int |
| ): |
| """INVARIANT: Resolution path must never be empty.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=None, |
| action_type="chat" |
| )) |
|
|
| |
| assert "resolution_path" in context, "Context must contain resolution_path" |
| assert isinstance(context["resolution_path"], list), "resolution_path must be list" |
| assert len(context["resolution_path"]) > 0, "resolution_path must not be empty" |
|
|
| @given( |
| timestamp_delta_ms=st.integers(min_value=-1000, max_value=1000) |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_context_contains_timestamp( |
| self, db_session: Session, timestamp_delta_ms: int |
| ): |
| """INVARIANT: Context must contain resolution timestamp.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| from datetime import datetime |
| before = datetime.utcnow() |
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=None, |
| action_type="chat" |
| )) |
| after = datetime.utcnow() |
|
|
| |
| assert "resolved_at" in context, "Context must contain resolved_at" |
| assert isinstance(context["resolved_at"], str), "resolved_at must be ISO format string" |
| |
| resolved_at = datetime.fromisoformat(context["resolved_at"]) |
| assert before <= resolved_at <= after, "resolved_at must be within time window" |
|
|
|
|
| class TestAgentResolutionEdgeCases: |
| """Property-based tests for agent resolution edge cases.""" |
|
|
| @given( |
| agent_count=st.integers(min_value=0, max_value=10) |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_resolve_with_multiple_agents( |
| self, db_session: Session, agent_count: int |
| ): |
| """INVARIANT: Should resolve correctly even with multiple agents.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| agent_ids = [] |
| for i in range(agent_count): |
| agent = AgentRegistry( |
| name=f"Agent_{i}_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.5 + (i * 0.1), |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
| agent_ids.append(agent.id) |
|
|
| |
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=None, |
| action_type="chat" |
| )) |
|
|
| |
| assert agent is not None, "Should resolve to an agent" |
| assert isinstance(agent, AgentRegistry), "Must be AgentRegistry instance" |
|
|
| @given( |
| invalid_id=st.one_of( |
| st.text(min_size=1, max_size=50).filter(lambda x: x != ""), |
| st.integers(min_value=1, max_value=1000).map(str) |
| ) |
| ) |
| @settings(max_examples=100, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_resolve_with_invalid_agent_id( |
| self, db_session: Session, invalid_id: str |
| ): |
| """INVARIANT: Should gracefully handle invalid agent IDs.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| try: |
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=invalid_id, |
| action_type="chat" |
| )) |
|
|
| |
| assert context is not None, "Context must never be None" |
| assert isinstance(context, dict), "Context must be dict" |
|
|
| except Exception as e: |
| pytest.fail(f"resolve_agent_for_request crashed with invalid ID: {e}") |
|
|
| @given( |
| confidence_score=st.floats( |
| min_value=-0.1, |
| max_value=1.1, |
| allow_nan=False, |
| allow_infinity=False |
| ) |
| ) |
| @settings(max_examples=100, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_resolve_agent_with_edge_case_confidence( |
| self, db_session: Session, confidence_score: float |
| ): |
| """INVARIANT: Should handle edge case confidence scores.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| agent = AgentRegistry( |
| name=f"Agent_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=max(0.0, min(1.0, confidence_score)), |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| try: |
| resolved_agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=agent.id, |
| action_type="chat" |
| )) |
|
|
| |
| assert resolved_agent is not None, "Should resolve agent" |
| assert context is not None, "Context must not be None" |
|
|
| except Exception as e: |
| pytest.fail(f"Failed with edge case confidence {confidence_score}: {e}") |
|
|
|
|
| class TestSessionManagementInvariants: |
| """Property-based tests for session management invariants.""" |
|
|
| @given( |
| session_count=st.integers(min_value=1, max_value=5) |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_set_agent_for_multiple_sessions( |
| self, db_session: Session, session_count: int |
| ): |
| """INVARIANT: Should handle multiple sessions independently.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| agent = AgentRegistry( |
| name=f"Agent_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.7, |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| session_ids = [] |
| for i in range(session_count): |
| session = ChatSession( |
| user_id=user.id, |
| title=f"Session {i}" |
| ) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
| session_ids.append(session.id) |
|
|
| |
| results = [] |
| for session_id in session_ids: |
| result = resolver.set_session_agent(session_id, agent.id) |
| results.append(result) |
|
|
| |
| assert all(results), "All session agent assignments should succeed" |
| assert len(results) == session_count, f"Should have {session_count} results" |
|
|
| @given( |
| agent_reassignments=st.integers(min_value=1, max_value=10) |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_reassign_session_agent( |
| self, db_session: Session, agent_reassignments: int |
| ): |
| """INVARIANT: Should handle reassigning session agents.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| session = ChatSession( |
| user_id=user.id, |
| title="Test Session" |
| ) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
|
|
| |
| agent_ids = [] |
| for i in range(agent_reassignments): |
| agent = AgentRegistry( |
| name=f"Agent_{i}_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.5 + (i * 0.05), |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
| agent_ids.append(agent.id) |
|
|
| |
| results = [] |
| for agent_id in agent_ids: |
| result = resolver.set_session_agent(session.id, agent_id) |
| results.append(result) |
|
|
| |
| assert all(results), "All reassignments should succeed" |
| assert len(results) == agent_reassignments, f"Should have {agent_reassignments} results" |
|
|
| @given( |
| has_session=st.booleans(), |
| has_agent=st.booleans() |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_clear_session_agent( |
| self, db_session: Session, has_session: bool, has_agent: bool |
| ): |
| """INVARIANT: Clearing session agent should be idempotent.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| session_id = None |
| if has_session: |
| session = ChatSession( |
| user_id=user.id, |
| title="Test Session" |
| ) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
| session_id = session.id |
|
|
| agent_id = None |
| if has_agent: |
| agent = AgentRegistry( |
| name=f"Agent_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.7, |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
| agent_id = agent.id |
|
|
| |
| if session_id and agent_id: |
| resolver.set_session_agent(session_id, agent_id) |
|
|
| |
| |
| result = resolver.set_session_agent(session_id, None) |
|
|
| |
| assert isinstance(result, bool), "Must return bool" |
|
|
| @given( |
| session_count=st.integers(min_value=10, max_value=50) |
| ) |
| @settings(max_examples=20, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_session_agent_isolation( |
| self, db_session: Session, session_count: int |
| ): |
| """INVARIANT: Sessions should have independent agent assignments.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| sessions = [] |
| agents = [] |
| for i in range(session_count): |
| session = ChatSession( |
| user_id=user.id, |
| title=f"Session {i}" |
| ) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
| sessions.append(session) |
|
|
| agent = AgentRegistry( |
| name=f"Agent_{i}_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.5 + (i * 0.01), |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
| agents.append(agent) |
|
|
| |
| assignment_results = [] |
| for session, agent in zip(sessions, agents): |
| result = resolver.set_session_agent(session.id, agent.id) |
| assignment_results.append(result) |
|
|
| |
| assert all(assignment_results), "All session-agent assignments should succeed" |
| assert len(assignment_results) == session_count |
|
|
|
|
| class TestErrorHandlingInvariants: |
| """Property-based tests for error handling invariants.""" |
|
|
| @given( |
| action_type=st.sampled_from([ |
| "chat", "stream", "present", "submit", |
| "browser_navigate", "device_camera", |
| "invalid_action", "", " " |
| ]) |
| ) |
| @settings(max_examples=100, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_validate_with_various_actions( |
| self, db_session: Session, action_type: str |
| ): |
| """INVARIANT: Validation should handle various action types.""" |
| |
| resolver = AgentContextResolver(db_session) |
| agent = AgentRegistry( |
| name=f"Agent_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.7, |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| try: |
| decision = resolver.validate_agent_for_action(agent, action_type) |
|
|
| |
| assert isinstance(decision, dict), "Must return dict" |
| assert "allowed" in decision, "Must have 'allowed' field" |
| assert "reason" in decision, "Must have 'reason' field" |
| assert isinstance(decision["allowed"], bool), "'allowed' must be bool" |
|
|
| except Exception as e: |
| pytest.fail(f"validate_agent_for_action crashed for action '{action_type}': {e}") |
|
|
| @given( |
| agent_status=st.sampled_from([ |
| AgentStatus.STUDENT.value, |
| AgentStatus.INTERN.value, |
| AgentStatus.SUPERVISED.value, |
| AgentStatus.AUTONOMOUS.value, |
| ]) |
| ) |
| @settings(max_examples=100, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_validate_with_all_maturity_levels( |
| self, db_session: Session, agent_status: str |
| ): |
| """INVARIANT: Validation should work for all maturity levels.""" |
| |
| resolver = AgentContextResolver(db_session) |
| agent = AgentRegistry( |
| name=f"Agent_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=agent_status, |
| confidence_score=0.7, |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| try: |
| decision = resolver.validate_agent_for_action(agent, "chat") |
|
|
| |
| assert isinstance(decision, dict), "Must return dict" |
| assert "allowed" in decision, "Must have 'allowed' field" |
| assert "reason" in decision, "Must have 'reason' field" |
|
|
| except Exception as e: |
| pytest.fail(f"validate_agent_for_action crashed for status {agent_status}: {e}") |
|
|
| @given( |
| invalid_agent=st.one_of( |
| st.none(), |
| st.text(min_size=1, max_size=50) |
| ) |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_validate_with_invalid_agent( |
| self, db_session: Session, invalid_agent |
| ): |
| """INVARIANT: Validation should handle invalid agents gracefully.""" |
| |
| resolver = AgentContextResolver(db_session) |
|
|
| |
| try: |
| |
| if isinstance(invalid_agent, AgentRegistry): |
| decision = resolver.validate_agent_for_action(invalid_agent, "chat") |
| assert isinstance(decision, dict), "Must return dict" |
| else: |
| |
| decision = resolver.validate_agent_for_action(invalid_agent, "chat") |
| |
| assert isinstance(decision, dict), "Must return dict or raise" |
|
|
| except Exception as e: |
| |
| assert True |
|
|
| @given( |
| user_exists=st.booleans(), |
| session_exists=st.booleans(), |
| agent_exists=st.booleans() |
| ) |
| @settings(max_examples=50, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_resolve_with_combinations_of_invalid_inputs( |
| self, db_session: Session, user_exists: bool, session_exists: bool, agent_exists: bool |
| ): |
| """INVARIANT: Should handle combinations of valid/invalid inputs.""" |
| |
| resolver = AgentContextResolver(db_session) |
|
|
| user_id = None |
| if user_exists: |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
| user_id = user.id |
| else: |
| user_id = str(uuid.uuid4()) |
|
|
| session_id = None |
| if session_exists: |
| if user_exists: |
| session = ChatSession( |
| user_id=user_id, |
| title="Test Session" |
| ) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
| session_id = session.id |
|
|
| agent_id = None |
| if agent_exists: |
| agent = AgentRegistry( |
| name=f"Agent_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.7, |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
| agent_id = agent.id |
|
|
| |
| try: |
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user_id, |
| session_id=session_id, |
| requested_agent_id=agent_id, |
| action_type="chat" |
| )) |
|
|
| |
| assert context is not None, "Context must never be None" |
| assert isinstance(context, dict), "Context must be dict" |
| assert "user_id" in context, "Context must have user_id" |
| assert "resolution_path" in context, "Context must have resolution_path" |
|
|
| except Exception as e: |
| |
| assert True |
|
|
|
|
| class TestPerformanceInvariants: |
| """Property-based tests for performance invariants.""" |
|
|
| @given( |
| agent_count=st.integers(min_value=1, max_value=20) |
| ) |
| @settings(max_examples=30, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_resolve_performance_scale( |
| self, db_session: Session, agent_count: int |
| ): |
| """INVARIANT: Resolution should scale reasonably with agent count.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| for i in range(agent_count): |
| agent = AgentRegistry( |
| name=f"Agent_{i}_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.5 + (i * 0.02), |
| ) |
| db_session.add(agent) |
| db_session.commit() |
|
|
| |
| import time |
| start = time.time() |
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=None, |
| action_type="chat" |
| )) |
| elapsed = time.time() - start |
|
|
| |
| |
| assert elapsed < 1.0, f"Resolution took {elapsed:.3f}s, too slow for {agent_count} agents" |
| assert agent is not None, "Should resolve agent" |
| assert context is not None, "Should have context" |
|
|
| @given( |
| consecutive_resolves=st.integers(min_value=1, max_value=10) |
| ) |
| @settings(max_examples=20, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_consecutive_resolution_performance( |
| self, db_session: Session, consecutive_resolves: int |
| ): |
| """INVARIANT: Consecutive resolutions should be fast.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| import time |
| start = time.time() |
| for _ in range(consecutive_resolves): |
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=None, |
| action_type="chat" |
| )) |
| elapsed = time.time() - start |
|
|
| |
| avg_time = elapsed / consecutive_resolves |
| assert avg_time < 0.1, f"Average resolution time {avg_time:.3f}s too slow" |
|
|
| @given( |
| lookup_count=st.integers(min_value=1, max_value=20) |
| ) |
| @settings(max_examples=20, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_validation_performance( |
| self, db_session: Session, lookup_count: int |
| ): |
| """INVARIANT: Validation should be fast for repeated lookups.""" |
| |
| resolver = AgentContextResolver(db_session) |
| agent = AgentRegistry( |
| name=f"Agent_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.7, |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
|
|
| |
| import time |
| start = time.time() |
| for _ in range(lookup_count): |
| decision = resolver.validate_agent_for_action(agent, "chat") |
| elapsed = time.time() - start |
|
|
| |
| avg_time = elapsed / lookup_count |
| assert avg_time < 0.05, f"Average validation time {avg_time:.3f}s too slow" |
|
|
|
|
| class TestConcurrentAccessInvariants: |
| """Property-based tests for concurrent access invariants.""" |
|
|
| @given( |
| thread_count=st.integers(min_value=2, max_value=5) |
| ) |
| @settings(max_examples=20, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_concurrent_resolution( |
| self, db_session: Session, thread_count: int |
| ): |
| """INVARIANT: Should handle concurrent resolution requests.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| |
| agents = [] |
| for i in range(thread_count): |
| agent = AgentRegistry( |
| name=f"Agent_{i}_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.5 + (i * 0.1), |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
| agents.append(agent) |
|
|
| |
| import concurrent.futures |
|
|
| def resolve_agent(agent_id): |
| agent, context = asyncio.run(resolver.resolve_agent_for_request( |
| user_id=user.id, |
| session_id=None, |
| requested_agent_id=agent_id, |
| action_type="chat" |
| )) |
| return agent is not None and context is not None |
|
|
| with concurrent.futures.ThreadPoolExecutor(max_workers=thread_count) as executor: |
| futures = [executor.submit(resolve_agent, agent.id) for agent in agents] |
| results = [f.result() for f in concurrent.futures.as_completed(futures)] |
|
|
| |
| assert all(results), "All concurrent resolutions should succeed" |
| assert len(results) == thread_count, f"Should have {thread_count} results" |
|
|
| @given( |
| update_count=st.integers(min_value=2, max_value=10) |
| ) |
| @settings(max_examples=20, suppress_health_check=[HealthCheck.function_scoped_fixture]) |
| def test_concurrent_session_updates( |
| self, db_session: Session, update_count: int |
| ): |
| """INVARIANT: Should handle concurrent session agent updates.""" |
| |
| resolver = AgentContextResolver(db_session) |
| user = User( |
| email=f"test_{uuid.uuid4()}@example.com", |
| role=UserRole.MEMBER.value, |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| session = ChatSession( |
| user_id=user.id, |
| title="Test Session" |
| ) |
| db_session.add(session) |
| db_session.commit() |
| db_session.refresh(session) |
|
|
| |
| agents = [] |
| for i in range(update_count): |
| agent = AgentRegistry( |
| name=f"Agent_{i}_{uuid.uuid4()}", |
| category="test", |
| module_path="test.module", |
| class_name="TestClass", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.5 + (i * 0.05), |
| ) |
| db_session.add(agent) |
| db_session.commit() |
| db_session.refresh(agent) |
| agents.append(agent) |
|
|
| |
| results = [] |
| for agent in agents: |
| result = resolver.set_session_agent(session.id, agent.id) |
| results.append(result) |
|
|
| |
| |
| assert all(results), "All updates should succeed" |
| assert len(results) == update_count, f"Should have {update_count} results" |
|
|
| |
| |
| |
| assert any(results), "At least one update should succeed" |
|
|