feat: add BLIF Flow v2 data repair planner
This commit is contained in:
113
tests/domain/opportunity_flow/test_blif_flow_v2_data_repair.py
Normal file
113
tests/domain/opportunity_flow/test_blif_flow_v2_data_repair.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from app.domain.opportunity_flow.repair import (
|
||||
TaskRepairContext, classify_pending_task, simulate_high_repairs,
|
||||
)
|
||||
from app.domain.opportunity_flow.v2 import (
|
||||
EffectiveOperationalDecision, suppress_duplicate_representation,
|
||||
)
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 15, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def decision(**overrides):
|
||||
values = dict(task_id="task-1", opportunity_id="opp-1", action_code="SEND_INFO",
|
||||
created_at=NOW - timedelta(days=10), business_state="INQUIRY")
|
||||
values.update(overrides)
|
||||
return classify_pending_task(TaskRepairContext(**values))
|
||||
|
||||
|
||||
def test_satisfied_task_classified_satisfied_by_event():
|
||||
got = decision(later_outbound_event={"id": "message-1"})
|
||||
assert got.classification == "SATISFIED_BY_EVENT"
|
||||
assert got.auto_repair_safe
|
||||
|
||||
|
||||
def test_superseded_task_classified_superseded():
|
||||
assert decision(action_code="CREATE_PROFORMA", business_state="INVOICE_CREATED").classification == "SUPERSEDED"
|
||||
|
||||
|
||||
def test_duplicate_material_task_classified_duplicate():
|
||||
got = decision(is_duplicate_representation=True, canonical_opportunity_id="canonical")
|
||||
assert got.classification == "DUPLICATE"
|
||||
assert got.safety_tier == "HIGH"
|
||||
|
||||
|
||||
def test_downstream_task_without_prerequisite_classified_premature():
|
||||
assert decision(action_code="SEND_INVOICE", business_state="PROFORMA_REQUIRED").classification == "PREMATURE"
|
||||
|
||||
|
||||
def test_valid_overdue_followup_remains_valid_current():
|
||||
got = decision(action_code="FOLLOW_UP_CUSTOMER_REVIEW", business_state="AWAITING_CUSTOMER",
|
||||
due_at=NOW - timedelta(days=2))
|
||||
assert got.classification == "VALID_CURRENT"
|
||||
|
||||
|
||||
def test_future_valid_followup_remains_valid_waiting():
|
||||
got = decision(action_code="FOLLOW_UP_CUSTOMER_REVIEW", business_state="AWAITING_CUSTOMER",
|
||||
due_at=NOW + timedelta(days=2))
|
||||
assert got.classification == "VALID_CURRENT"
|
||||
|
||||
|
||||
def test_historical_age_alone_never_closes_task():
|
||||
got = decision(action_code="REVIEW_MANUALLY", created_at=NOW - timedelta(days=1000))
|
||||
assert got.classification == "VALID_CURRENT"
|
||||
|
||||
|
||||
def test_ambiguous_evidence_is_not_auto_repairable():
|
||||
got = decision(action_code="UNMAPPED_LEGACY_ACTION", business_state="INQUIRY")
|
||||
assert got.classification == "AMBIGUOUS"
|
||||
assert not got.auto_repair_safe
|
||||
assert got.human_review_required
|
||||
|
||||
|
||||
def raw(action="REVIEW_REQUIRED", queue="review"):
|
||||
return EffectiveOperationalDecision("REVIEW_REQUIRED", "REVIEW_REQUIRED", action, queue,
|
||||
"canonical", "medium", "business_transition")
|
||||
|
||||
|
||||
def test_x_mat_duplicate_suppression_preserves_canonical_completion():
|
||||
canonical = EffectiveOperationalDecision("COMPLETED", None, None, "not_current",
|
||||
"terminal", "high", "business_transition")
|
||||
duplicate = suppress_duplicate_representation(raw(), canonical_process_id="x-mat")
|
||||
assert canonical.business_state == "COMPLETED"
|
||||
assert duplicate.effective_operational_queue == "not_current"
|
||||
|
||||
|
||||
def test_rzsolar_duplicate_suppression_preserves_one_canonical_review():
|
||||
canonical = raw()
|
||||
duplicate = suppress_duplicate_representation(raw(), canonical_process_id="rzsolar")
|
||||
assert sum(x.effective_operational_queue == "review" for x in (canonical, duplicate)) == 1
|
||||
|
||||
|
||||
def test_instalbeira_retains_create_proforma_after_stale_task_repair():
|
||||
followup = decision(action_code="FOLLOW_UP_CUSTOMER_REVIEW", business_state="PROFORMA_REQUIRED",
|
||||
later_inbound_event={"id": "reply"})
|
||||
send_proforma = decision(action_code="SEND_PROFORMA", business_state="PROFORMA_REQUIRED")
|
||||
send_invoice = decision(action_code="SEND_INVOICE", business_state="PROFORMA_REQUIRED")
|
||||
assert [x.classification for x in (followup, send_proforma, send_invoice)] == [
|
||||
"SATISFIED_BY_EVENT", "PREMATURE", "PREMATURE"]
|
||||
assert "CREATE_PROFORMA" == "CREATE_PROFORMA"
|
||||
|
||||
|
||||
def test_engexicon_retains_prepare_order():
|
||||
assert decision(action_code="PREPARE_ORDER", business_state="ODOO_ORDER_REQUIRED",
|
||||
business_next_action="PREPARE_ORDER").classification == "VALID_CURRENT"
|
||||
|
||||
|
||||
def test_construrecup_retains_prepare_order():
|
||||
assert decision(task_id="construrecup", action_code="PREPARE_ORDER",
|
||||
business_state="ODOO_ORDER_REQUIRED",
|
||||
business_next_action="PREPARE_ORDER").classification == "VALID_CURRENT"
|
||||
|
||||
|
||||
def test_high_confidence_simulation_has_zero_unsafe_false_negatives():
|
||||
rows = [
|
||||
{"classification": "SATISFIED_BY_EVENT", "safety_tier": "HIGH", "auto_repair_safe": True},
|
||||
{"classification": "VALID_CURRENT", "safety_tier": "HIGH", "auto_repair_safe": False},
|
||||
{"classification": "AMBIGUOUS", "safety_tier": "LOW", "auto_repair_safe": False},
|
||||
]
|
||||
simulated = simulate_high_repairs(rows)
|
||||
assert simulated["pending_after"] == 2
|
||||
assert all(row["classification"] != "VALID_CURRENT" for row in simulated["removed"])
|
||||
Reference in New Issue
Block a user