mirror of
https://github.com/mcloh/Enterprise-MCP-Service-Bus.git
synced 2026-11-06 06:24:12 +00:00
M3 implementada e backlog atualizado
This commit is contained in:
129
RI/tests/unit/test_orchestrator_decisioning.py
Normal file
129
RI/tests/unit/test_orchestrator_decisioning.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user