fix: harden Flow v2 authoritative cutover semantics

This commit is contained in:
plx
2026-08-16 01:38:39 +00:00
parent ac385342f5
commit 213ba6b41b
7 changed files with 211 additions and 30 deletions

View File

@@ -27,6 +27,19 @@ def test_business_baselines_and_terminal():
assert decide_authoritative_operation(projection("COMPLETED"), now=NOW).queue == "not_current"
def test_terminal_state_ignores_leftover_pending_obligation():
got = decide_authoritative_operation(projection("COMPLETED"),
obligations=[task("REVIEW_RECONSTRUCTED_PROCESS")], now=NOW)
assert (got.effective_action, got.queue, got.reason_code) == (None, "not_current", "TERMINAL_BUSINESS_STATE")
def test_specific_reconstructed_review_overrides_generic_business_review():
got = decide_authoritative_operation(projection("REVIEW_REQUIRED", "REVIEW_REQUIRED"),
obligations=[task("REVIEW_RECONSTRUCTED_PROCESS")], now=NOW)
assert (got.business_next_action, got.effective_action, got.queue) == (
"REVIEW_REQUIRED", "REVIEW_RECONSTRUCTED_PROCESS", "review")
def test_missing_projection_fails_closed_without_v1():
got = decide_authoritative_operation(None, now=NOW)
assert (got.effective_action, got.queue, got.reason_code) == ("REVIEW_REQUIRED", "review", "MISSING_V2_PROJECTION")
@@ -46,7 +59,8 @@ def test_only_active_non_superseded_tasks_override_and_precedence():
obligations = [task("CALL_CUSTOMER"), task("SEND_INFO"), task("SUPPORT"), task("REVIEW_MANUALLY")]
assert decide_authoritative_operation(projection(), obligations=obligations, now=NOW).effective_action == "REVIEW_MANUALLY"
assert decide_authoritative_operation(projection(), obligations=[task("SUPPORT")], now=NOW).effective_action == "SUPPORT"
assert decide_authoritative_operation(projection(), obligations=[task("SEND_INFO")], now=NOW).effective_action == "SEND_INFO"
assert decide_authoritative_operation(projection("INQUIRY", "SEND_INFO"), obligations=[task("SEND_INFO")], now=NOW).effective_action == "SEND_INFO"
assert decide_authoritative_operation(projection(), obligations=[task("SEND_INFO")], now=NOW).effective_action is None
assert decide_authoritative_operation(projection(), obligations=[task("CALL_CUSTOMER")], now=NOW).effective_action == "CALL_CUSTOMER"

View File

@@ -15,6 +15,7 @@ class Result:
def __init__(self, row): self.row = row
def mappings(self): return self
def one(self): return self.row
def all(self): return self.row if isinstance(self.row, list) else [self.row]
class Connection:
@@ -26,6 +27,8 @@ class Connection:
return Result({"database": self.database, "user": "audit", "transaction_read_only": self.readonly})
if "count(*) FROM opportunities" in sql:
return Result({"opportunities": 328, "projections": 328})
if "GROUP BY business_state" in sql:
return Result([])
return Result({})
def close(self): pass
@@ -37,8 +40,8 @@ class Engine:
def named_decisions(fail=False):
result = {}
for name, (oid, action, queue) in sim.NAMED.items():
result[oid] = {"effective_action": action,
for name, (oid, business_action, action, queue) in sim.NAMED.items():
result[oid] = {"business_next_action": business_action, "effective_action": action,
"operational_queue": "not_current" if queue == "suppressed" else queue,
"suppress_current_card": queue == "suppressed"}
if fail:
@@ -136,3 +139,29 @@ def test_named_case_failure_is_nonzero():
report = sim.build_report(identity={}, configured_mode="compare", v1={"work_items": []},
v2={"work_items": []}, named_decisions=named_decisions(True), missing_projections=0)
assert sim.exit_code(report) != 0
def test_action_changes_correlate_by_material_process_not_action_key():
v1_item = {"work_item_key": "opportunity:o:action:OLD", "opportunity_id": "o",
"action_code": "OLD", "operational_queue": "do_now"}
v2_item = {"work_item_key": "opportunity:o:action:NEW", "opportunity_id": "o",
"canonical_opportunity_id": "o", "action_code": "NEW", "operational_queue": "do_now"}
report = sim.build_report(identity={}, configured_mode="compare",
v1={"work_items": [v1_item]}, v2={"work_items": [v2_item]},
named_decisions=named_decisions(), missing_projections=0)
assert len(report["changed_actions"]) == 1
assert report["removed_cards"] == [] and report["added_cards"] == []
def test_authoritative_material_collapse_selects_one_card_and_retains_evidence():
from app.operations_service import _collapse_authoritative_material_processes
rows = [
{"process_key": "conversation:chatwoot:1863", "work_item_key": "x:SEND_INFO",
"action_code": "SEND_INFO", "operational_queue": "do_now", "source_refs": [{"source": "task", "id": "1"}]},
{"process_key": "conversation:chatwoot:1863", "work_item_key": "x:REVIEW_MANUALLY",
"action_code": "REVIEW_MANUALLY", "operational_queue": "review", "source_refs": [{"source": "communication", "id": "2"}]},
]
result = _collapse_authoritative_material_processes(rows)
assert len(result) == 1
assert result[0]["action_code"] == "REVIEW_MANUALLY"
assert {ref["id"] for ref in result[0]["source_refs"]} == {"1", "2"}