| """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") |
|
|
| |
| 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 |
|
|