126 lines
4.9 KiB
Python
126 lines
4.9 KiB
Python
from datetime import datetime, timezone
|
|
from inspect import getsource
|
|
from collections import Counter
|
|
|
|
import pytest
|
|
|
|
from scripts.apply_blif_flow_v2_data_repair import (
|
|
EXPECTED_REPAIR_COUNT, FROZEN_REPAIRS, RESOLUTION_CODES,
|
|
apply_transaction, assert_test_database, phase1_high_rows,
|
|
validate_frozen_repair_set, validate_target_states,
|
|
)
|
|
|
|
|
|
def planned_rows():
|
|
return [
|
|
{"task_id": task_id, "action_code": action, "classification": classification,
|
|
"opportunity_id": opportunity_id, "safety_tier": "HIGH", "auto_repair_safe": True}
|
|
for task_id, (action, classification, opportunity_id) in FROZEN_REPAIRS.items()
|
|
]
|
|
|
|
|
|
def target_rows(*, applied=False):
|
|
now = datetime.now(timezone.utc)
|
|
return [
|
|
{"id": task_id, "action_code": action, "opportunity_id": opportunity_id,
|
|
"status": "done" if applied else "pending",
|
|
"resolution_code": RESOLUTION_CODES[classification] if applied else None,
|
|
"resolved_at": now if applied else None, "resolved_by_event_id": None,
|
|
"superseded_by_task_id": None}
|
|
for task_id, (action, classification, opportunity_id) in FROZEN_REPAIRS.items()
|
|
]
|
|
|
|
|
|
def test_default_cli_is_dry_run():
|
|
source = getsource(__import__("scripts.apply_blif_flow_v2_data_repair", fromlist=["main"]).main)
|
|
assert 'add_argument("--apply", action="store_true"' in source
|
|
assert "run(apply=args.apply)" in source
|
|
|
|
|
|
@pytest.mark.parametrize("identity", [
|
|
("clientflow", "clientflow_codex_test", "off"),
|
|
("other_test", "clientflow_codex_test", "off"),
|
|
("clientflow_codex_test", "wrong_user", "off"),
|
|
])
|
|
def test_wrong_database_or_user_hard_fails(identity):
|
|
with pytest.raises(RuntimeError, match="refusing repair"):
|
|
assert_test_database(identity)
|
|
|
|
|
|
def test_exact_repair_set_is_required():
|
|
rows = planned_rows()
|
|
validate_frozen_repair_set(rows)
|
|
assert len(rows) == EXPECTED_REPAIR_COUNT
|
|
with pytest.raises(RuntimeError, match="expected 12"):
|
|
validate_frozen_repair_set(rows[:-1])
|
|
|
|
|
|
def test_changed_frozen_identity_aborts():
|
|
rows = planned_rows()
|
|
rows[0] = {**rows[0], "classification": "SUPERSEDED"}
|
|
with pytest.raises(RuntimeError, match="cohort drift"):
|
|
validate_frozen_repair_set(rows)
|
|
|
|
|
|
def test_ambiguous_and_valid_current_are_never_selected():
|
|
result = {"tasks": {"tasks": planned_rows() + [
|
|
{"task_id": "ambiguous", "classification": "AMBIGUOUS", "safety_tier": "LOW", "auto_repair_safe": False},
|
|
{"task_id": "valid", "classification": "VALID_CURRENT", "safety_tier": "HIGH", "auto_repair_safe": False},
|
|
]}}
|
|
selected = phase1_high_rows(result)
|
|
assert {row["classification"] for row in selected}.isdisjoint({"AMBIGUOUS", "VALID_CURRENT"})
|
|
|
|
|
|
def test_resolution_codes_and_deterministic_event_fields_are_written():
|
|
source = getsource(apply_transaction)
|
|
assert "resolved_by_event_id=CAST(:resolved_by_event_id AS UUID)" in source
|
|
assert RESOLUTION_CODES == {
|
|
"SATISFIED_BY_EVENT": "satisfied_by_event", "SUPERSEDED": "superseded",
|
|
"DUPLICATE": "duplicate_obligation", "PREMATURE": "premature_downstream",
|
|
}
|
|
|
|
|
|
def test_duplicate_repair_never_deletes_evidence_or_tasks():
|
|
source = getsource(apply_transaction).upper()
|
|
assert "DELETE" not in source
|
|
assert "UPDATE TASKS" in source
|
|
assert "COMMERCIAL_DOCUMENTS" not in source
|
|
assert "OPERATION_LINKS" not in source
|
|
|
|
|
|
def test_apply_is_one_atomic_transaction_with_rollback():
|
|
source = getsource(apply_transaction)
|
|
assert "transaction = conn.begin()" in source
|
|
assert "transaction.commit()" in source
|
|
assert "transaction.rollback()" in source
|
|
|
|
|
|
def test_second_apply_is_idempotent():
|
|
assert validate_target_states(target_rows(applied=False)) == "pending"
|
|
assert validate_target_states(target_rows(applied=True)) == "already_applied"
|
|
|
|
|
|
def test_partial_apply_state_aborts():
|
|
rows = target_rows(applied=True)
|
|
rows[0].update(status="pending", resolution_code=None, resolved_at=None)
|
|
with pytest.raises(RuntimeError, match="partial repair state"):
|
|
validate_target_states(rows)
|
|
|
|
|
|
def test_premature_resolution_does_not_mutate_projection_or_opportunity():
|
|
source = getsource(apply_transaction).upper()
|
|
assert "UPDATE OPPORTUNITIES" not in source
|
|
assert "UPDATE OPPORTUNITY_FLOW_STATE_V2" not in source
|
|
|
|
|
|
def test_frozen_set_contains_named_duplicate_and_instalbeira_repairs():
|
|
assert FROZEN_REPAIRS["a15b2545-591f-4d73-b8a2-3268efd01f98"][1] == "DUPLICATE"
|
|
assert FROZEN_REPAIRS["b8965dd9-f0fc-42cf-a824-f8804add9e18"][1] == "DUPLICATE"
|
|
instal = [row for row in planned_rows() if row["opportunity_id"] == "5c33db95-fab8-477a-bddd-0b9cc8f91302"]
|
|
assert Counter(row["classification"] for row in instal) == Counter({"PREMATURE": 2, "SATISFIED_BY_EVENT": 1})
|
|
|
|
|
|
def test_zero_unsafe_false_negative_categories_in_frozen_set():
|
|
assert {classification for _, classification, _ in FROZEN_REPAIRS.values()} == {
|
|
"SATISFIED_BY_EVENT", "SUPERSEDED", "DUPLICATE", "PREMATURE"}
|