system-prompts-and-models-o.../dealix/tests/unit/test_dealix_contracts.py
2026-05-01 14:03:52 +03:00

199 lines
6.4 KiB
Python

"""Tests for Dealix contracts — DecisionOutput, classifications, validators."""
from __future__ import annotations
import pytest
from dealix.classifications import (
NEVER_AUTO_EXECUTE,
ApprovalClass,
ReversibilityClass,
SensitivityClass,
classify,
)
from dealix.contracts.audit_log import AuditAction, AuditEntry
from dealix.contracts.decision import (
DecisionOutput,
Evidence,
NextAction,
)
from dealix.contracts.event_envelope import EventEnvelope
# ── Classifications ────────────────────────────────────────────
def test_classify_known_action():
a, r, s = classify("proposal_send")
assert a == ApprovalClass.A2
assert r == ReversibilityClass.R2
assert s == SensitivityClass.S2
def test_classify_unknown_defaults_conservative():
a, r, s = classify("some_brand_new_action_xyz")
# Conservative default
assert a == ApprovalClass.A2
assert r == ReversibilityClass.R2
assert s == SensitivityClass.S2
def test_never_auto_execute_includes_expected_actions():
for critical in (
"pricing_offer_commit",
"contract_change",
"nda_send",
"payment_terms_change",
"regulator_communication",
"sensitive_data_export",
):
assert critical in NEVER_AUTO_EXECUTE
def test_approval_class_properties():
assert not ApprovalClass.A0.requires_approval
assert ApprovalClass.A1.requires_approval
assert ApprovalClass.A3.minimum_approvers == 2
def test_reversibility_class_r3_blocks():
assert ReversibilityClass.R3.blocks_auto_execution
assert not ReversibilityClass.R0.blocks_auto_execution
def test_sensitivity_s3_is_pdpl():
assert SensitivityClass.S3.is_pdpl_scope
assert not SensitivityClass.S1.is_pdpl_scope
# ── DecisionOutput validation ──────────────────────────────────
def test_decision_low_stakes_works_without_evidence():
d = DecisionOutput(
entity_id="lead_1",
objective="qualify_lead",
agent_name="icp_matcher",
recommendation={"tier": "A"},
confidence=0.9,
rationale="test",
approval_class=ApprovalClass.A0,
reversibility_class=ReversibilityClass.R0,
sensitivity_class=SensitivityClass.S1,
)
assert not d.is_high_stakes
assert not d.requires_human_approval
def test_decision_high_stakes_requires_evidence():
with pytest.raises(ValueError, match="Evidence"):
DecisionOutput(
entity_id="lead_1",
objective="recommend_proposal",
agent_name="proposal",
recommendation={"send": True},
confidence=0.9,
rationale="test",
approval_class=ApprovalClass.A2,
reversibility_class=ReversibilityClass.R2,
sensitivity_class=SensitivityClass.S2,
evidence=[], # missing!
)
def test_decision_high_stakes_with_evidence_ok():
d = DecisionOutput(
entity_id="lead_1",
objective="recommend_proposal",
agent_name="proposal",
recommendation={"send": True},
confidence=0.9,
rationale="test",
approval_class=ApprovalClass.A2,
reversibility_class=ReversibilityClass.R2,
sensitivity_class=SensitivityClass.S2,
evidence=[Evidence(source="test", excerpt="evidence here")],
)
assert d.is_high_stakes
assert d.requires_human_approval
def test_decision_r3_is_high_stakes_even_with_a0():
# R3 alone is enough to be high-stakes
with pytest.raises(ValueError, match="Evidence"):
DecisionOutput(
entity_id="deal_1",
objective="irreversible_commit",
agent_name="test",
recommendation={"commit": True},
confidence=0.9,
rationale="test",
approval_class=ApprovalClass.A0,
reversibility_class=ReversibilityClass.R3,
sensitivity_class=SensitivityClass.S1,
evidence=[],
)
def test_decision_has_auto_generated_ids():
d = DecisionOutput(
entity_id="x",
objective="test",
agent_name="test",
recommendation={},
confidence=0.5,
rationale="r",
approval_class=ApprovalClass.A0,
reversibility_class=ReversibilityClass.R0,
sensitivity_class=SensitivityClass.S0,
)
assert d.decision_id.startswith("dec_")
assert d.schema_version == "1.0"
assert d.created_at # ISO string
# ── NextAction ─────────────────────────────────────────────────
def test_next_action_fields():
na = NextAction(
action_type="proposal_send",
description="send proposal",
approval_class=ApprovalClass.A2,
reversibility_class=ReversibilityClass.R2,
sensitivity_class=SensitivityClass.S2,
payload={"proposal_id": "p1"},
)
assert na.action_type == "proposal_send"
assert na.payload["proposal_id"] == "p1"
# ── EventEnvelope ──────────────────────────────────────────────
def test_event_envelope_cloudevents_required():
ev = EventEnvelope(
source="dealix/test",
type="dealix.test.emitted",
)
assert ev.specversion == "1.0"
assert ev.id.startswith("evt_")
assert ev.time # ISO string
def test_event_envelope_carries_classification():
ev = EventEnvelope(
source="dealix/phase8",
type="dealix.lead.intaken",
approval_class=ApprovalClass.A0,
reversibility_class=ReversibilityClass.R0,
sensitivity_class=SensitivityClass.S2,
)
assert ev.sensitivity_class == SensitivityClass.S2
# ── AuditEntry ─────────────────────────────────────────────────
def test_audit_entry_minimal():
entry = AuditEntry(action=AuditAction.DECISION_EMITTED)
assert entry.audit_id.startswith("aud_")
assert entry.outcome == "ok"
assert entry.schema_version == "1.0"
def test_audit_action_enum_values():
assert AuditAction.POLICY_ESCALATED.value == "policy.escalated"
assert AuditAction.TOOL_CONTRADICTED.value == "tool.contradicted"
assert AuditAction.WORKFLOW_COMPENSATED.value == "workflow.compensated"