from argparse import Namespace import importlib.util from pathlib import Path import pytest PATH = Path("scripts/simulate_blif_flow_v2_authoritative.py") SPEC = importlib.util.spec_from_file_location("authoritative_simulation", PATH) sim = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(sim) 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: def __init__(self, database="clientflow", readonly="on"): self.database, self.readonly, self.sql = database, readonly, [] def exec_driver_sql(self, sql): self.sql.append(sql.strip()) if "current_database" in sql: 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 class Engine: def __init__(self, connection): self.connection = connection def connect(self): return self.connection def named_decisions(fail=False): result = {} 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", "canonical_opportunity_id": sim.DUPLICATE_CANONICAL.get(oid, oid)} if fail: result[sim.NAMED["Instalbeira"][0]]["effective_action"] = "SEND_INVOICE" return result def runtime(connection, *, fail_named=False, modes=None): empty = {"work_items": [], "waiting_items": [], "backlog_items": [], "not_current_items": []} def operations(**kwargs): assert kwargs["connection"] is connection if modes is not None: modes.append(kwargs["flow_mode"]) return empty def decisions(ids, **kwargs): assert kwargs == {"flow_mode": "authoritative", "connection": connection} return named_decisions(fail_named) return lambda: (Engine(connection), operations, decisions) def test_help_performs_zero_db_work(): called = False def loader(): nonlocal called called = True raise AssertionError("DB/application runtime must not load") with pytest.raises(SystemExit) as exc: sim.main(["--help"], runtime_loader=loader) assert exc.value.code == 0 and called is False def test_production_requires_explicit_flag(monkeypatch): monkeypatch.setenv("BLIF_FLOW_V2_MODE", "compare") with pytest.raises(RuntimeError, match="requires --production"): sim.run(Namespace(production_readonly_simulation=False), runtime_loader=runtime(Connection())) @pytest.mark.parametrize("database,readonly,mode,message", [ ("test_db", "on", "compare", "current_database"), ("clientflow", "off", "compare", "transaction_read_only"), ("clientflow", "on", None, "explicit BLIF_FLOW"), ("clientflow", "on", "off", "BLIF_FLOW_V2_MODE=compare"), ("clientflow", "on", "shadow", "BLIF_FLOW_V2_MODE=compare"), ("clientflow", "on", "authoritative", "BLIF_FLOW_V2_MODE=compare"), ]) def test_production_guards(monkeypatch, database, readonly, mode, message): if mode is None: monkeypatch.delenv("BLIF_FLOW_V2_MODE", raising=False) else: monkeypatch.setenv("BLIF_FLOW_V2_MODE", mode) with pytest.raises(RuntimeError, match=message): sim.run(Namespace(production_readonly_simulation=True), runtime_loader=runtime(Connection(database, readonly))) def test_safe_simulation_uses_explicit_modes_without_changing_config_and_has_no_db_writes(monkeypatch, tmp_path): monkeypatch.setenv("BLIF_FLOW_V2_MODE", "compare") monkeypatch.setattr(sim, "JSON_PATH", tmp_path / "report.json") monkeypatch.setattr(sim, "TEXT_PATH", tmp_path / "report.txt") connection = Connection() modes = [] report = sim.run(Namespace(production_readonly_simulation=True), runtime_loader=runtime(connection, modes=modes)) assert report["configured_mode"] == "compare" assert sim.exit_code(report) == 0 assert [sql for sql in connection.sql if sql.split(None, 1)[0].upper() in {"INSERT", "UPDATE", "DELETE", "MERGE"}] == [] assert connection.sql[0] == "BEGIN READ ONLY" and connection.sql[-1] == "ROLLBACK" assert modes == ["off", "authoritative"] def safe_report(): return sim.build_report(identity={}, configured_mode="compare", v1={"work_items": []}, v2={"work_items": []}, named_decisions=named_decisions(), missing_projections=0) def test_unsafe_false_negative_is_nonzero(): report = safe_report() report["unsafe_false_negatives"] = 1 assert sim.exit_code(report) != 0 def test_missing_projection_is_nonzero(): report = safe_report() report["missing_projections"] = 1 assert sim.exit_code(report) != 0 def test_duplicate_current_card_is_nonzero(): report = safe_report() report["duplicate_current_cards"] = 1 assert sim.exit_code(report) != 0 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"}