spinor-os / tests /test_assignment.py
luguog's picture
Deploy spinor-os as static HF Space receiver
bf3cbf1 verified
Raw
History Blame Contribute Delete
18 kB
"""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