feat: add guarded production Flow v2 authoritative simulation
This commit is contained in:
138
tests/test_blif_flow_v2_authoritative_simulation.py
Normal file
138
tests/test_blif_flow_v2_authoritative_simulation.py
Normal file
@@ -0,0 +1,138 @@
|
||||
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
|
||||
|
||||
|
||||
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})
|
||||
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, action, queue) in sim.NAMED.items():
|
||||
result[oid] = {"effective_action": action,
|
||||
"operational_queue": "not_current" if queue == "suppressed" else queue,
|
||||
"suppress_current_card": queue == "suppressed"}
|
||||
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
|
||||
Reference in New Issue
Block a user