"""Durable operations: failures, cancellation and recovery must preserve newer work.""" import asyncio import copy import time import pytest from mcp_api import control as module from mcp_api.auth import principal, MCPError from mcp_api.control import Control, revision, request_context from mcp_api.schemas import WriteOptions from mcp_api.store import Store @pytest.fixture def harness(tmp_path, monkeypatch): store = Store(tmp_path / "control.sqlite3") store.open() state = {"scope": "firewall", "enabled": False, "rules": [], "settings": {}} def snapshot(*args): return copy.deepcopy(state) async def restore(value): state.clear() state.update(copy.deepcopy(value)) monkeypatch.setattr(module, "snapshot", snapshot) monkeypatch.setattr(module, "restore", restore) monkeypatch.setattr(module, "command_lock", asyncio.Lock()) marker = principal.set({"id": "tester", "name": "test", "role": "admin"}) yield Control(store, {}), state, snapshot, restore principal.reset(marker) store.close() def test_failed_apply_compensates_and_retry_keeps_failure(harness): control, state, snapshot, restore = harness async def scenario(): before = snapshot() async def broken(): state["enabled"] = True raise RuntimeError("engine refused") req = {"actor": "tester", "id": "once", "fingerprint": "same"} marker = request_context.set(req) try: result = await control.mutate("test", before, {**before, "enabled": True}, WriteOptions(request_id="once", if_revision=revision(before)), broken) finally: request_context.reset(marker) assert not result["ok"] assert result["change"]["status"] == "rolled_back" assert state == before assert control.replay(req)["error"] == result["error"] asyncio.run(scenario()) def test_cancelling_running_job_compensates(harness): control, state, snapshot, restore = harness async def scenario(): before = snapshot() started = asyncio.Event() async def slow(): state["enabled"] = True started.set() await asyncio.Event().wait() result = await control.mutate("test", before, {**before, "enabled": True}, WriteOptions(request_id="cancel", if_revision=revision(before), background=True), slow) await started.wait() task = control.tasks[result["data"]["job_id"]] task.cancel() await task saved = control.change(result["data"]["job_id"]) assert saved["status"] == "rolled_back" assert saved["result"]["error"]["code"] == "CANCELLED" assert state == before asyncio.run(scenario()) @pytest.mark.parametrize("intervening", [False, True]) def test_restart_recovery_refuses_unknown_configuration(harness, intervening): control, state, snapshot, restore = harness before = snapshot() candidate = {**before, "enabled": True} state.update(candidate) if intervening: state["rules"] = [{"name": "newer operator rule"}] control.store.put("change", "c", {"id": "c", "scope": "firewall", "status": "applying", "rollback_available": True, "before": before, "candidate": candidate, "before_revision": revision(before)}) asyncio.run(control.recover()) assert control.change("c")["status"] == ("recovery_required" if intervening else "rolled_back") assert state["enabled"] is intervening def test_expired_lease_never_overwrites_newer_state(harness): control, state, snapshot, restore = harness async def scenario(): before = snapshot() candidate = {**before, "enabled": True} result = await control.mutate("test", before, candidate, WriteOptions(request_id="lease", if_revision=revision(before), confirm_within_seconds=5), lambda: restore(candidate)) change = control.change(result["change"]["id"]) change["confirm_by"] = time.time() - 1 control.store.put("change", change["id"], change) state["rules"] = [{"name": "newer rule"}] await control.expire() assert control.change(change["id"])["status"] == "recovery_required" assert state["rules"] == [{"name": "newer rule"}] asyncio.run(scenario())