mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-03 04:00:18 +00:00
The six decision-model dataclasses (Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain) accepted `auto_generate_id` only as a plain `__post_init__` parameter. Since it was neither a field nor an InitVar, the generated `__init__` never forwarded it, so the parameter was always its `True` default and the `auto_generate_id=False` validation branch was unreachable dead code. Declare `auto_generate_id: InitVar[bool] = True` on each dataclass so the generated `__init__` forwards it to `__post_init__`, restoring the required-id contract. Serialization is unaffected because InitVar is not a real field. Add regression tests covering the auto-generate path, the required-id error path, and the explicit-id path. Fixes #1152 Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
543 lines
19 KiB
Python
543 lines
19 KiB
Python
"""
|
|
Tests for Decision Tracking Data Models
|
|
|
|
This module tests the decision tracking data models including
|
|
validation, serialization, and deserialization.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
import pytest
|
|
|
|
from semantica.context.decision_models import (
|
|
ApprovalChain,
|
|
Decision,
|
|
DecisionContext,
|
|
Policy,
|
|
PolicyException,
|
|
Precedent,
|
|
deserialize_decision,
|
|
deserialize_policy,
|
|
serialize_decision,
|
|
serialize_policy,
|
|
validate_decision,
|
|
validate_policy,
|
|
)
|
|
|
|
|
|
class TestDecision:
|
|
"""Test Decision data model."""
|
|
|
|
def test_decision_creation(self):
|
|
"""Test basic decision creation."""
|
|
decision = Decision(
|
|
decision_id="test_001",
|
|
category="credit_approval",
|
|
scenario="Credit limit increase request",
|
|
reasoning="Customer has excellent payment history",
|
|
outcome="approved",
|
|
confidence=0.85,
|
|
timestamp=datetime.now(),
|
|
decision_maker="ai_agent_001"
|
|
)
|
|
|
|
assert decision.decision_id == "test_001"
|
|
assert decision.category == "credit_approval"
|
|
assert decision.scenario == "Credit limit increase request"
|
|
assert decision.reasoning == "Customer has excellent payment history"
|
|
assert decision.outcome == "approved"
|
|
assert decision.confidence == 0.85
|
|
assert decision.decision_maker == "ai_agent_001"
|
|
|
|
def test_decision_auto_id(self):
|
|
"""Test automatic ID generation."""
|
|
decision = Decision(
|
|
decision_id="",
|
|
category="test",
|
|
scenario="test scenario",
|
|
reasoning="test reasoning",
|
|
outcome="test outcome",
|
|
confidence=0.5,
|
|
timestamp=datetime.now(),
|
|
decision_maker="test_agent"
|
|
)
|
|
|
|
assert decision.decision_id != ""
|
|
assert len(decision.decision_id) > 0
|
|
|
|
def test_decision_confidence_validation(self):
|
|
"""Test confidence validation."""
|
|
with pytest.raises(ValueError):
|
|
Decision(
|
|
decision_id="test",
|
|
category="test",
|
|
scenario="test",
|
|
reasoning="test",
|
|
outcome="test",
|
|
confidence=1.5, # Invalid confidence > 1
|
|
timestamp=datetime.now(),
|
|
decision_maker="test"
|
|
)
|
|
|
|
with pytest.raises(ValueError):
|
|
Decision(
|
|
decision_id="test",
|
|
category="test",
|
|
scenario="test",
|
|
reasoning="test",
|
|
outcome="test",
|
|
confidence=-0.1, # Invalid confidence < 0
|
|
timestamp=datetime.now(),
|
|
decision_maker="test"
|
|
)
|
|
|
|
def test_decision_to_dict(self):
|
|
"""Test decision serialization to dictionary."""
|
|
decision = Decision(
|
|
decision_id="test_001",
|
|
category="test",
|
|
scenario="test scenario",
|
|
reasoning="test reasoning",
|
|
outcome="test outcome",
|
|
confidence=0.8,
|
|
timestamp=datetime.now(),
|
|
decision_maker="test_agent",
|
|
metadata={"key": "value"}
|
|
)
|
|
|
|
decision_dict = decision.to_dict()
|
|
|
|
assert decision_dict["decision_id"] == "test_001"
|
|
assert decision_dict["category"] == "test"
|
|
assert decision_dict["scenario"] == "test scenario"
|
|
assert decision_dict["reasoning"] == "test reasoning"
|
|
assert decision_dict["outcome"] == "test outcome"
|
|
assert decision_dict["confidence"] == 0.8
|
|
assert decision_dict["decision_maker"] == "test_agent"
|
|
assert decision_dict["metadata"]["key"] == "value"
|
|
assert isinstance(decision_dict["timestamp"], str)
|
|
|
|
def test_decision_from_dict(self):
|
|
"""Test decision deserialization from dictionary."""
|
|
decision_data = {
|
|
"decision_id": "test_001",
|
|
"category": "test",
|
|
"scenario": "test scenario",
|
|
"reasoning": "test reasoning",
|
|
"outcome": "test outcome",
|
|
"confidence": 0.8,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"decision_maker": "test_agent",
|
|
"metadata": {"key": "value"}
|
|
}
|
|
|
|
decision = Decision.from_dict(decision_data)
|
|
|
|
assert decision.decision_id == "test_001"
|
|
assert decision.category == "test"
|
|
assert decision.scenario == "test scenario"
|
|
assert decision.reasoning == "test reasoning"
|
|
assert decision.outcome == "test outcome"
|
|
assert decision.confidence == 0.8
|
|
assert decision.decision_maker == "test_agent"
|
|
assert decision.metadata["key"] == "value"
|
|
assert isinstance(decision.timestamp, datetime)
|
|
|
|
|
|
class TestDecisionContext:
|
|
"""Test DecisionContext data model."""
|
|
|
|
def test_decision_context_creation(self):
|
|
"""Test basic decision context creation."""
|
|
context = DecisionContext(
|
|
context_id="ctx_001",
|
|
decision_id="decision_001",
|
|
entity_snapshots={"entity1": {"name": "test", "type": "person"}},
|
|
risk_factors=["high_risk", "new_customer"],
|
|
cross_system_inputs={"salesforce": {"tier": "premium"}},
|
|
metadata={"version": "1.0"}
|
|
)
|
|
|
|
assert context.context_id == "ctx_001"
|
|
assert context.decision_id == "decision_001"
|
|
assert len(context.entity_snapshots) == 1
|
|
assert context.entity_snapshots["entity1"]["name"] == "test"
|
|
assert context.risk_factors == ["high_risk", "new_customer"]
|
|
assert context.cross_system_inputs["salesforce"]["tier"] == "premium"
|
|
assert context.metadata["version"] == "1.0"
|
|
|
|
def test_decision_context_auto_id(self):
|
|
"""Test automatic ID generation."""
|
|
context = DecisionContext(
|
|
context_id="",
|
|
decision_id="decision_001",
|
|
entity_snapshots={},
|
|
risk_factors=[]
|
|
)
|
|
|
|
assert context.context_id != ""
|
|
assert len(context.context_id) > 0
|
|
|
|
|
|
class TestPolicy:
|
|
"""Test Policy data model."""
|
|
|
|
def test_policy_creation(self):
|
|
"""Test basic policy creation."""
|
|
now = datetime.now()
|
|
policy = Policy(
|
|
policy_id="policy_001",
|
|
name="Credit Approval Policy",
|
|
description="Standard credit approval rules",
|
|
rules={"min_score": 650, "max_debt_ratio": 0.4},
|
|
category="credit_approval",
|
|
version="1.0",
|
|
created_at=now,
|
|
updated_at=now,
|
|
metadata={"department": "risk"}
|
|
)
|
|
|
|
assert policy.policy_id == "policy_001"
|
|
assert policy.name == "Credit Approval Policy"
|
|
assert policy.description == "Standard credit approval rules"
|
|
assert policy.rules["min_score"] == 650
|
|
assert policy.category == "credit_approval"
|
|
assert policy.version == "1.0"
|
|
assert policy.metadata["department"] == "risk"
|
|
|
|
def test_policy_auto_id(self):
|
|
"""Test automatic ID generation."""
|
|
policy = Policy(
|
|
policy_id="",
|
|
name="Test Policy",
|
|
description="Test description",
|
|
rules={"test": "rule"},
|
|
category="test",
|
|
version="1.0",
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now()
|
|
)
|
|
|
|
assert policy.policy_id != ""
|
|
assert len(policy.policy_id) > 0
|
|
|
|
|
|
class TestPolicyException:
|
|
"""Test PolicyException data model."""
|
|
|
|
def test_policy_exception_creation(self):
|
|
"""Test basic policy exception creation."""
|
|
policy_exception = PolicyException(
|
|
exception_id="exc_001",
|
|
decision_id="decision_001",
|
|
policy_id="policy_001",
|
|
reason="Customer is VIP with special arrangements",
|
|
approver="manager_001",
|
|
approval_timestamp=datetime.now(),
|
|
justification="Long-term customer with excellent history",
|
|
metadata={"override_type": "vip_exception"}
|
|
)
|
|
|
|
assert policy_exception.exception_id == "exc_001"
|
|
assert policy_exception.decision_id == "decision_001"
|
|
assert policy_exception.policy_id == "policy_001"
|
|
assert policy_exception.reason == "Customer is VIP with special arrangements"
|
|
assert policy_exception.approver == "manager_001"
|
|
assert policy_exception.justification == "Long-term customer with excellent history"
|
|
assert policy_exception.metadata["override_type"] == "vip_exception"
|
|
|
|
def test_policy_exception_auto_id(self):
|
|
"""Test automatic ID generation."""
|
|
policy_exception = PolicyException(
|
|
exception_id="",
|
|
decision_id="decision_001",
|
|
policy_id="policy_001",
|
|
reason="test reason",
|
|
approver="test_approver",
|
|
approval_timestamp=datetime.now(),
|
|
justification="test justification"
|
|
)
|
|
|
|
assert policy_exception.exception_id != ""
|
|
assert len(policy_exception.exception_id) > 0
|
|
|
|
|
|
class TestPrecedent:
|
|
"""Test Precedent data model."""
|
|
|
|
def test_precedent_creation(self):
|
|
"""Test basic precedent creation."""
|
|
precedent = Precedent(
|
|
precedent_id="prec_001",
|
|
source_decision_id="decision_001",
|
|
similarity_score=0.85,
|
|
relationship_type="similar_scenario",
|
|
metadata={"matched_features": ["category", "amount"]}
|
|
)
|
|
|
|
assert precedent.precedent_id == "prec_001"
|
|
assert precedent.source_decision_id == "decision_001"
|
|
assert precedent.similarity_score == 0.85
|
|
assert precedent.relationship_type == "similar_scenario"
|
|
assert precedent.metadata["matched_features"] == ["category", "amount"]
|
|
|
|
def test_precedent_validation(self):
|
|
"""Test precedent validation."""
|
|
# Test invalid similarity score
|
|
with pytest.raises(ValueError):
|
|
Precedent(
|
|
precedent_id="test",
|
|
source_decision_id="test",
|
|
similarity_score=1.5, # Invalid > 1
|
|
relationship_type="similar_scenario"
|
|
)
|
|
|
|
# Test invalid relationship type
|
|
with pytest.raises(ValueError):
|
|
Precedent(
|
|
precedent_id="test",
|
|
source_decision_id="test",
|
|
similarity_score=0.8,
|
|
relationship_type="invalid_type"
|
|
)
|
|
|
|
|
|
class TestApprovalChain:
|
|
"""Test ApprovalChain data model."""
|
|
|
|
def test_approval_chain_creation(self):
|
|
"""Test basic approval chain creation."""
|
|
approval = ApprovalChain(
|
|
approval_id="app_001",
|
|
decision_id="decision_001",
|
|
approver="manager_001",
|
|
approval_method="slack_dm",
|
|
approval_context="Approved via Slack DM with justification",
|
|
timestamp=datetime.now(),
|
|
metadata={"message_id": "msg_12345"}
|
|
)
|
|
|
|
assert approval.approval_id == "app_001"
|
|
assert approval.decision_id == "decision_001"
|
|
assert approval.approver == "manager_001"
|
|
assert approval.approval_method == "slack_dm"
|
|
assert approval.approval_context == "Approved via Slack DM with justification"
|
|
assert approval.metadata["message_id"] == "msg_12345"
|
|
|
|
def test_approval_chain_validation(self):
|
|
"""Test approval chain validation."""
|
|
# Test invalid approval method
|
|
with pytest.raises(ValueError):
|
|
ApprovalChain(
|
|
approval_id="test",
|
|
decision_id="test",
|
|
approver="test",
|
|
approval_method="invalid_method", # Invalid method
|
|
approval_context="test context",
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
|
|
class TestValidationFunctions:
|
|
"""Test validation helper functions."""
|
|
|
|
def test_validate_decision(self):
|
|
"""Test decision validation."""
|
|
# Valid decision
|
|
valid_decision = Decision(
|
|
decision_id="test",
|
|
category="test",
|
|
scenario="test",
|
|
reasoning="test",
|
|
outcome="test",
|
|
confidence=0.8,
|
|
timestamp=datetime.now(),
|
|
decision_maker="test"
|
|
)
|
|
assert validate_decision(valid_decision) is True
|
|
|
|
# Invalid decision (missing required field)
|
|
invalid_decision = Decision(
|
|
decision_id="test",
|
|
category="", # Empty category
|
|
scenario="test",
|
|
reasoning="test",
|
|
outcome="test",
|
|
confidence=0.8,
|
|
timestamp=datetime.now(),
|
|
decision_maker="test"
|
|
)
|
|
assert validate_decision(invalid_decision) is False
|
|
|
|
def test_validate_policy(self):
|
|
"""Test policy validation."""
|
|
# Valid policy
|
|
valid_policy = Policy(
|
|
policy_id="test",
|
|
name="Test Policy",
|
|
description="Test description",
|
|
rules={"test": "rule"},
|
|
category="test",
|
|
version="1.0",
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now()
|
|
)
|
|
assert validate_policy(valid_policy) is True
|
|
|
|
# Invalid policy (missing required field)
|
|
invalid_policy = Policy(
|
|
policy_id="test",
|
|
name="Test Policy",
|
|
description="Test description",
|
|
rules={"test": "rule"},
|
|
category="", # Empty category
|
|
version="1.0",
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now()
|
|
)
|
|
assert validate_policy(invalid_policy) is False
|
|
|
|
|
|
class TestSerializationFunctions:
|
|
"""Test serialization helper functions."""
|
|
|
|
def test_serialize_deserialize_decision(self):
|
|
"""Test decision serialization and deserialization."""
|
|
original_decision = Decision(
|
|
decision_id="test_001",
|
|
category="test",
|
|
scenario="test scenario",
|
|
reasoning="test reasoning",
|
|
outcome="test outcome",
|
|
confidence=0.8,
|
|
timestamp=datetime.now(),
|
|
decision_maker="test_agent",
|
|
metadata={"key": "value"}
|
|
)
|
|
|
|
# Serialize
|
|
serialized = serialize_decision(original_decision)
|
|
assert isinstance(serialized, str)
|
|
|
|
# Deserialize
|
|
deserialized = deserialize_decision(serialized)
|
|
|
|
assert deserialized.decision_id == original_decision.decision_id
|
|
assert deserialized.category == original_decision.category
|
|
assert deserialized.scenario == original_decision.scenario
|
|
assert deserialized.reasoning == original_decision.reasoning
|
|
assert deserialized.outcome == original_decision.outcome
|
|
assert deserialized.confidence == original_decision.confidence
|
|
assert deserialized.decision_maker == original_decision.decision_maker
|
|
assert deserialized.metadata == original_decision.metadata
|
|
|
|
def test_serialize_deserialize_policy(self):
|
|
"""Test policy serialization and deserialization."""
|
|
original_policy = Policy(
|
|
policy_id="policy_001",
|
|
name="Test Policy",
|
|
description="Test description",
|
|
rules={"test": "rule"},
|
|
category="test",
|
|
version="1.0",
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
metadata={"department": "risk"}
|
|
)
|
|
|
|
# Serialize
|
|
serialized = serialize_policy(original_policy)
|
|
assert isinstance(serialized, str)
|
|
|
|
# Deserialize
|
|
deserialized = deserialize_policy(serialized)
|
|
|
|
assert deserialized.policy_id == original_policy.policy_id
|
|
assert deserialized.name == original_policy.name
|
|
assert deserialized.description == original_policy.description
|
|
assert deserialized.rules == original_policy.rules
|
|
assert deserialized.category == original_policy.category
|
|
assert deserialized.version == original_policy.version
|
|
assert deserialized.metadata == original_policy.metadata
|
|
|
|
|
|
class TestAutoGenerateIdContract:
|
|
"""Test the auto_generate_id InitVar contract across all decision models.
|
|
|
|
Regression coverage for the InitVar fix: previously ``auto_generate_id``
|
|
was a plain ``__post_init__`` parameter that dataclass-generated ``__init__``
|
|
never forwarded, so the ``auto_generate_id=False`` branch was dead code and
|
|
the "id is required" contract could never fire.
|
|
"""
|
|
|
|
def _base_kwargs(self, cls):
|
|
now = datetime.now()
|
|
return {
|
|
Decision: dict(
|
|
decision_id="", category="c", scenario="s", reasoning="r",
|
|
outcome="o", confidence=0.5, timestamp=now, decision_maker="m",
|
|
),
|
|
DecisionContext: dict(
|
|
context_id="", decision_id="d", entity_snapshots={}, risk_factors=[],
|
|
),
|
|
Policy: dict(
|
|
policy_id="", name="n", description="d", rules={}, category="c",
|
|
version="1", created_at=now, updated_at=now,
|
|
),
|
|
PolicyException: dict(
|
|
exception_id="", decision_id="d", policy_id="p", reason="r",
|
|
approver="a", approval_timestamp=now, justification="j",
|
|
),
|
|
Precedent: dict(
|
|
precedent_id="", source_decision_id="d", similarity_score=0.5,
|
|
relationship_type="same_policy",
|
|
),
|
|
ApprovalChain: dict(
|
|
approval_id="", decision_id="d", approver="a",
|
|
approval_method="email", approval_context="x", timestamp=now,
|
|
),
|
|
}[cls]
|
|
|
|
@pytest.mark.parametrize(
|
|
"cls",
|
|
[Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain],
|
|
)
|
|
def test_auto_generate_id_is_not_a_field(self, cls):
|
|
"""auto_generate_id must stay an InitVar, never a real dataclass field."""
|
|
import dataclasses
|
|
|
|
names = [f.name for f in dataclasses.fields(cls)]
|
|
assert "auto_generate_id" not in names
|
|
|
|
@pytest.mark.parametrize(
|
|
"cls",
|
|
[Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain],
|
|
)
|
|
def test_default_auto_generates_id(self, cls):
|
|
"""With defaults, an empty id is auto-populated and stays a non-field."""
|
|
obj = cls(**self._base_kwargs(cls))
|
|
id_field = [f.name for f in __import__("dataclasses").fields(cls)][0]
|
|
assert getattr(obj, id_field)
|
|
assert "auto_generate_id" not in vars(obj)
|
|
|
|
@pytest.mark.parametrize(
|
|
"cls",
|
|
[Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain],
|
|
)
|
|
def test_required_id_when_auto_generate_disabled(self, cls):
|
|
"""auto_generate_id=False with an empty id must raise ValueError."""
|
|
with pytest.raises(ValueError):
|
|
cls(auto_generate_id=False, **self._base_kwargs(cls))
|
|
|
|
def test_explicit_id_honored_with_auto_generate_disabled(self):
|
|
"""A provided id is preserved when auto_generate_id=False."""
|
|
kwargs = self._base_kwargs(Decision)
|
|
kwargs["decision_id"] = "fixed-id"
|
|
decision = Decision(auto_generate_id=False, **kwargs)
|
|
assert decision.decision_id == "fixed-id"
|
|
assert "auto_generate_id" not in decision.to_dict()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__])
|