Files
Enterprise-MCP-Service-Bus/RI/tests/unit/test_orchestrator_decisioning.py
2026-09-14 10:11:29 -03:00

130 lines
4.4 KiB
Python

"""Reference NBADecisionModel (EP-11-T03, README.md §6.13)."""
from __future__ import annotations
from datetime import datetime
from emcp_bus.common.models import RiskTier
from emcp_bus.orchestrator.decision_context import DecisionContext, RuntimeContext
from emcp_bus.orchestrator.decisioning import RuleBasedNBADecisionModel
from emcp_bus.orchestrator.nba_model import NBADecision, NBADecisionModel
from emcp_bus.profile_intelligence.models import ProfileView
from emcp_bus.registry.models import (
ApprovalPolicy,
BackendRef,
CapabilityManifest,
LifecycleInfo,
LifecycleStatus,
)
def _manifest(name: str, risk_tier: RiskTier = RiskTier.R1) -> CapabilityManifest:
return CapabilityManifest(
name=name,
version="1.0.0",
domain=name.split(".", 1)[0],
owner="sales-team",
risk_tier=risk_tier,
side_effects=False,
idempotent=True,
entitlements=["Sales.Read"],
backend=BackendRef(type="mcp", service="sales-domain"),
approval=ApprovalPolicy(),
lifecycle=LifecycleInfo(status=LifecycleStatus.ACTIVE),
)
def _context(offerings: list[CapabilityManifest]) -> DecisionContext:
profile = ProfileView(
subject_ref="subject:abc123",
profile_version="v1",
reason_codes=["LOW_RECENCY"],
expires_at=datetime(2026, 1, 1),
)
return DecisionContext(
client_id="sales-read-agent",
entitlement_version="ent-1",
risk_ceiling=RiskTier.R2,
allowed_tools=frozenset(m.name for m in offerings),
profile=profile,
filtered_offerings=offerings,
runtime_context=RuntimeContext(channel="app"),
)
def test_decide_returns_none_when_no_offerings_survive() -> None:
model = RuleBasedNBADecisionModel()
assert model.decide(_context([])) is None
def test_decide_always_includes_reason_codes_policy_context_and_expiry() -> None:
"""EP-11-T03 acceptance criterion: every NBA/NBO carries reasonCodes,
policyContext and expiresAt (README.md §6.13)."""
model = RuleBasedNBADecisionModel()
context = _context([_manifest("sales.customer.get")])
nba = model.decide(context)
assert nba is not None
assert "ELIGIBLE" in nba.reason_codes
assert "LOW_RECENCY" in nba.reason_codes
assert nba.policy_context["entitlementVersion"] == "ent-1"
assert nba.expires_at is not None
def test_decide_prefers_the_lowest_risk_tier_offering() -> None:
model = RuleBasedNBADecisionModel()
context = _context(
[_manifest("sales.quote.create", RiskTier.R2), _manifest("sales.customer.get", RiskTier.R1)]
)
nba = model.decide(context)
assert nba is not None
assert nba.action == "sales.customer.get"
def test_decide_never_selects_an_offering_outside_filtered_offerings() -> None:
"""The structural guarantee EP-11-T03's second acceptance criterion
relies on: the model only ever ranks what DecisionContext already
carries -- there is no code path here that looks anywhere else."""
model = RuleBasedNBADecisionModel()
context = _context([_manifest("sales.customer.get")])
nba = model.decide(context)
assert nba is not None
assert nba.action in {o.name for o in context.filtered_offerings}
def test_a_swapped_decision_model_satisfies_the_same_protocol() -> None:
"""EP-11-T03's pluggability criterion: any NBADecisionModel
implementation works with the exact same DecisionContext/NBADecision
contract -- no EP-03/EP-04/EP-05 change required to swap one in."""
class AlwaysFirstOfferingModel:
def decide(self, context: DecisionContext) -> NBADecision | None:
if not context.filtered_offerings:
return None
offering = context.filtered_offerings[0]
return NBADecision(
decision_id="dec-fixed",
action=offering.name,
capability_version=offering.version,
subject_ref=context.profile.subject_ref,
channel=context.runtime_context.channel,
agent="test-agent",
reason_codes=["ELIGIBLE"],
policy_context={"entitlementVersion": context.entitlement_version},
expires_at=datetime(2026, 1, 1),
)
model: NBADecisionModel = AlwaysFirstOfferingModel()
context = _context([_manifest("sales.customer.get")])
nba = model.decide(context)
assert nba is not None
assert nba.action == "sales.customer.get"