"""Contract and integration tests for the strategy-assignment provider layer.""" from __future__ import annotations import json import urllib.error import urllib.request from datetime import datetime, timedelta, timezone from io import BytesIO from typing import Any from unittest.mock import patch import pytest from spinor_os.assignment import ( AssignmentContext, AssignmentError, AssignmentInput, AssignmentProviderRouter, AssignmentResult, CachedAssignmentProvider, LocalConstrainedAllocationProvider, ProviderHealthStatus, RemoteAdvantageFoundryProvider, ValidationError, build_default_router, ) from spinor_os.llm_assignment import LLMAssignmentProvider from spinor_os.config import MissionClass FIXTURE_PATH = __import__("pathlib").Path(__file__).parent / "fixtures" / "advantage_foundry_assign_response.json" @pytest.fixture def sample_input() -> AssignmentInput: return AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", campaign_id="campaign_async_account", context={ "territory": "new_york", "recent_activity": [], "account_signals": [], "capabilities": [], "recent_missions": [], }, allocation_constraints={ "compliance_required": True, "avoid_repetition": True, "allow_exploration": True, "maximum_active_experiments": 3, }, ) @pytest.fixture def remote_response() -> dict[str, Any]: return json.loads(FIXTURE_PATH.read_text()) class FakeHTTPResponse: """A minimal stand-in for an urllib HTTP response.""" def __init__(self, status: int, body: bytes): self._status = status self._body = body self.fp = BytesIO(body) def getcode(self) -> int: return self._status def read(self) -> bytes: return self._body def __enter__(self): return self def __exit__(self, *args): pass def make_urlopen_mock(response: FakeHTTPResponse): """Return a callable that returns the given response.""" def _urlopen(req, timeout=None): return response return _urlopen def test_assignment_input_validation(): with pytest.raises(ValueError): AssignmentInput(organization_id="", employee_id="rep_047", role="field_representative") def test_remote_provider_maps_to_camel_case(sample_input: AssignmentInput, remote_response: dict): body = json.dumps(remote_response).encode() response = FakeHTTPResponse(200, body) provider = RemoteAdvantageFoundryProvider( url="http://example.test/api/strategies/assign", timeout_ms=5000, max_retries=0, ) captured = {} def capture_urlopen(req, timeout=None): captured["body"] = json.loads(req.data.decode()) return response with patch.object(urllib.request, "urlopen", side_effect=capture_urlopen): result = provider.assign(sample_input) assert result.count == 2 assert result.source == "advantage_foundry" assert result.fallback_status == "none" assert captured["body"]["employeeId"] == "rep_047" assert captured["body"]["role"] == "field_representative" assert "organizationId" in captured["body"] assert captured["body"]["campaignId"] == "campaign_async_account" assert "allocationConstraints" in captured["body"] decision = result.decisions[0] assert decision.assignment_id assert decision.assigned_employee_id == "rep_047" assert decision.assigned_strategy_id assert decision.provenance.provider == "advantage_foundry" assert decision.provenance.audit_receipt assert decision.expires_at is not None def test_remote_provider_rejects_malformed_response(): body = json.dumps({"unexpected": "shape"}).encode() response = FakeHTTPResponse(200, body) provider = RemoteAdvantageFoundryProvider( url="http://example.test/api/strategies/assign", timeout_ms=5000, max_retries=0, ) inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", ) with patch.object(urllib.request, "urlopen", return_value=response): with pytest.raises(Exception): provider.assign(inp) def test_remote_provider_validation_error_4xx(): def raise_400(req, timeout=None): raise urllib.error.HTTPError( "http://example.test", 400, "Bad Request", {"Content-Type": "application/json"}, BytesIO(json.dumps({"error": {"fieldErrors": {"employeeId": ["Required"]}}}).encode()), ) provider = RemoteAdvantageFoundryProvider( url="http://example.test/api/strategies/assign", timeout_ms=5000, max_retries=2, ) inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", ) with patch.object(urllib.request, "urlopen", side_effect=raise_400): with pytest.raises(ValidationError): provider.assign(inp) def test_remote_provider_retries_then_falls_back(): call_count = 0 def flaky_urlopen(req, timeout=None): nonlocal call_count call_count += 1 if call_count < 3: raise urllib.error.HTTPError( "http://example.test", 503, "Service Unavailable", {}, BytesIO(b""), ) body = json.dumps(json.loads(FIXTURE_PATH.read_text())).encode() return FakeHTTPResponse(200, body) provider = RemoteAdvantageFoundryProvider( url="http://example.test/api/strategies/assign", timeout_ms=5000, max_retries=2, ) inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", ) with patch.object(urllib.request, "urlopen", side_effect=flaky_urlopen): result = provider.assign(inp) assert call_count == 3 assert result.count == 2 def test_remote_provider_circuit_breaker_opens(): def always_fail(req, timeout=None): raise urllib.error.HTTPError( "http://example.test", 503, "Service Unavailable", {}, BytesIO(b""), ) provider = RemoteAdvantageFoundryProvider( url="http://example.test/api/strategies/assign", timeout_ms=5000, max_retries=0, circuit_breaker_threshold=2, circuit_breaker_cooldown_seconds=1, ) inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", ) with patch.object(urllib.request, "urlopen", side_effect=always_fail): with pytest.raises(Exception): provider.assign(inp) with pytest.raises(Exception): provider.assign(inp) with pytest.raises(Exception): provider.assign(inp) def test_local_provider_produces_valid_assignment(): inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", ) provider = LocalConstrainedAllocationProvider() result = provider.assign(inp) assert result.count == 1 assert result.source == "local_constrained_allocation" assert result.decisions[0].provenance.provider == "local_constrained_allocation" assert result.decisions[0].compliance_state == "advisory" def test_cached_provider_returns_same_result_without_second_call(): body = json.dumps(json.loads(FIXTURE_PATH.read_text())).encode() response = FakeHTTPResponse(200, body) remote = RemoteAdvantageFoundryProvider( url="http://example.test/api/strategies/assign", timeout_ms=5000, max_retries=0, ) cached = CachedAssignmentProvider(remote, ttl_seconds=60) call_count = 0 def count_urlopen(req, timeout=None): nonlocal call_count call_count += 1 return response inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", ) with patch.object(urllib.request, "urlopen", side_effect=count_urlopen): first = cached.assign(inp) second = cached.assign(inp) assert call_count == 1 assert first.count == second.count == 2 assert first.decisions[0].assignment_id == second.decisions[0].assignment_id def test_router_uses_primary_and_records_fallback(): body = json.dumps(json.loads(FIXTURE_PATH.read_text())).encode() response = FakeHTTPResponse(200, body) remote = RemoteAdvantageFoundryProvider( url="http://example.test/api/strategies/assign", timeout_ms=5000, max_retries=0, ) cached = CachedAssignmentProvider(remote) local = LocalConstrainedAllocationProvider() router = AssignmentProviderRouter(primary=cached, local=local) inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", ) with patch.object(urllib.request, "urlopen", return_value=response): result = router.assign(inp) assert result.source == "advantage_foundry" assert result.fallback_status == "primary" def test_router_falls_back_to_local_when_remote_fails(): def always_fail(req, timeout=None): raise urllib.error.HTTPError( "http://example.test", 503, "Service Unavailable", {}, BytesIO(b""), ) remote = RemoteAdvantageFoundryProvider( url="http://example.test/api/strategies/assign", timeout_ms=5000, max_retries=0, ) local = LocalConstrainedAllocationProvider() router = AssignmentProviderRouter(primary=remote, local=local) inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", ) with patch.object(urllib.request, "urlopen", side_effect=always_fail): result = router.assign(inp) assert result.source == "local_constrained_allocation" assert result.fallback_status == "local_fallback" assert result.error is not None def test_build_default_router_has_providers(): router = build_default_router() assert router.primary is not None assert router.local is not None def test_health_reports_remote_status(): provider = RemoteAdvantageFoundryProvider( url="https://advantage-foundry.netlify.app/api/strategies/assign", ) health = provider.health() assert health.provider == "advantage_foundry" assert health.status in {ProviderHealthStatus.OK, ProviderHealthStatus.DEGRADED, ProviderHealthStatus.UNAVAILABLE} def sample_library() -> list[dict[str, Any]]: return [ { "strategy_id": "STR-workflow-map-email", "name": "Workflow map email", "description": "Send a role-specific workflow diagram instead of a product deck.", "customer_segment": "physician", "territory": "northeast", "modification": "workflow_map_email", "maturity": "provisional_finding", "replication_count": 0, "automation_ready": False, "expected_effect": { "metric": "qualified_response_rate", "direction": "increase", "magnitude": 0.08, "unit": "percentage_point", "timing": "14d", "confidence": 0.75, }, }, { "strategy_id": "STR-personalized-research-email", "name": "Personalized research email", "description": "Tailored pre-call research email for enterprise buyers.", "customer_segment": "enterprise", "territory": "northeast", "modification": "personalized_research_email", "maturity": "proven_finding", "replication_count": 3, "automation_ready": True, "expected_effect": { "metric": "appointment_rate", "direction": "increase", "magnitude": 0.12, "unit": "percentage_point", "timing": "7d", "confidence": 0.9, }, }, ] def test_llm_provider_grounds_decision_in_strategy_library(): provider = LLMAssignmentProvider(provider="openai", model="gpt-4o-mini") raw_response = json.dumps( { "decisions": [ { "allocation_mode": "exploit", "assigned_strategy_id": "STR-personalized-research-email", "assignment_explanation": "High-confidence proven strategy for enterprise segment.", "supporting_evidence": ["replicated 3 times", "automation ready"], "contradictory_evidence": [], "novelty_classification": "proven", "experiment_requirements": ["execute assigned strategy in the field", "capture outcome and baseline evidence"], "expected_value": 0.12, "expected_learning_value": 0.2, "compliance_state": "cleared", "alternatives": [], "trial_number": 1, "confidence_at_assignment": 0.9, } ] } ) inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", context=AssignmentContext(strategy_library=sample_library()), ) with patch.object(provider, "_call_llm", return_value=raw_response): result = provider.assign(inp) assert result.count == 1 assert result.source == provider.name assert result.decisions[0].assigned_strategy_id == "STR-personalized-research-email" assert result.decisions[0].daily_seed == "STR-personalized-research-email" assert result.decisions[0].provenance.validated is True def test_llm_provider_rejects_hallucinated_strategy_id(): provider = LLMAssignmentProvider(provider="openai", model="gpt-4o-mini") raw_response = json.dumps( { "decisions": [ { "allocation_mode": "exploit", "assigned_strategy_id": "FAKE-STRATEGY-ID", "assignment_explanation": "This strategy does not exist.", "novelty_classification": "proven", "experiment_requirements": ["execute"], "expected_value": 0.0, "expected_learning_value": 0.0, "compliance_state": "cleared", "alternatives": [], "trial_number": 1, "confidence_at_assignment": 0.0, } ] } ) inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", context=AssignmentContext(strategy_library=sample_library()), ) with patch.object(provider, "_call_llm", return_value=raw_response): with pytest.raises(Exception): provider.assign(inp) def test_local_provider_uses_real_strategy_library(): inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", context=AssignmentContext(strategy_library=sample_library()), ) provider = LocalConstrainedAllocationProvider() result = provider.assign(inp) assert result.count == 1 assert result.source == "local_constrained_allocation" assert result.decisions[0].assigned_strategy_id in { s["strategy_id"] for s in sample_library() } assert result.fallback_status == "local_primary" def test_llm_provider_grounds_decision_metadata_from_library(): """LLM output should be corrected using the real strategy catalogue metadata.""" provider = LLMAssignmentProvider(provider="openai", model="gpt-4o-mini") # The LLM returns a real strategy id but with mismatched metadata. raw_response = json.dumps( { "decisions": [ { "allocation_mode": "explore", "assigned_strategy_id": "STR-personalized-research-email", "assignment_explanation": "High-confidence proven strategy for enterprise segment.", "supporting_evidence": ["replicated 3 times", "automation ready"], "contradictory_evidence": [], "novelty_classification": "experimental", "experiment_requirements": ["execute assigned strategy in the field", "capture outcome and baseline evidence"], "expected_value": 0.0, "expected_learning_value": 0.0, "compliance_state": "cleared", "alternatives": [], "trial_number": 1, "confidence_at_assignment": 0.9, } ] } ) inp = AssignmentInput( organization_id="org_demo", employee_id="rep_047", role="field_representative", context=AssignmentContext(strategy_library=sample_library()), ) with patch.object(provider, "_call_llm", return_value=raw_response): result = provider.assign(inp) assert result.count == 1 decision = result.decisions[0] assert decision.assigned_strategy_id == "STR-personalized-research-email" assert decision.allocation_mode == "exploit" assert decision.novelty_classification == "proven" assert decision.expected_value == 0.12 assert decision.expected_learning_value == 0.2