Files
pigway-frame-flow/tests/test_recovery.py
T

110 lines
3.8 KiB
Python

from datetime import UTC, datetime, timedelta
from test_display import submit
from pigway_frame_flow.display.service import DisplayService
from pigway_frame_flow.errors import ErrorCode, FlowError
def unknown_upload(service, monkeypatch):
def fail(*args):
raise FlowError(ErrorCode.WRITE_RESULT_UNKNOWN, "Response lost", 502)
monkeypatch.setattr(service.registry.get("living-room"), "upload", fail)
operation = submit(service)
service.process_one()
return operation
def test_missing_upload_is_visible_without_unsafe_unlock(client, service, monkeypatch):
operation = unknown_upload(service, monkeypatch)
service.sync()
recovery = client.get("/api/v1/operations/" + operation).json()["recovery"]
assert recovery["reason"] == "gallery_match_missing"
assert recovery["state"] == "checking"
assert not recovery["requires_manual_review"]
assert service.blocked("living-room")
assert service.registry.get("living-room").client.uploads == 0
def test_persistent_block_escalates_once_survives_restart_and_preserves_evidence(
service, monkeypatch
):
operation = unknown_upload(service, monkeypatch)
original = service.operation(operation)
service.db.execute(
"UPDATE operations SET created_at=? WHERE id=?",
(
(datetime.now(UTC) - timedelta(hours=1)).isoformat(),
operation,
),
)
service.sync()
first = service.operation(operation)["recovery"]
assert first["state"] == "manual_review"
assert first["reason"] == "gallery_match_missing"
service.unknown_checked.clear()
service.sync()
restarted = DisplayService(
service.db, service.settings, service.registry, service.assets, service.renderer
)
restarted.sync()
assert (
len(service.db.all("SELECT * FROM events WHERE event_type='operation.blocked'"))
== 1
)
assert (
service.db.one(
"SELECT COUNT(*) AS n FROM history WHERE result='MANUAL_REVIEW_REQUIRED'"
)["n"]
== 1
)
assert service.operation(operation)["recovery"]["checks"] == 3
assert service.operation(operation)["result"] == original["result"]
service.reconcile(operation, acknowledge=True)
assert service.operation(operation)["recovery"]["state"] == "resolved"
assert not service.operation(operation)["recovery"]["requires_manual_review"]
assert not service.blocked("living-room")
def test_missing_legacy_evidence_escalates_without_cloud_write(service):
operation = submit(service)
service._start_write(operation)
service.update(operation, "UNKNOWN")
service.sync()
assert (
service.operation(operation)["recovery"]["reason"] == "missing_upload_evidence"
)
assert service.blocked("living-room")
def test_delete_unknown_requires_manual_review_without_replay(service):
operation = service.submit(
"delete", {"image_id": 1}, "living-room", "admin", "test"
)
service._start_write(operation)
service.update(operation, "UNKNOWN")
service.sync()
assert (
service.operation(operation)["recovery"]["reason"]
== "manual_reconciliation_required"
)
assert service.blocked("living-room")
assert service.registry.get("living-room").images()[0].image_id == 1
def test_malformed_reconciliation_does_not_abort_other_background_work(
service, monkeypatch
):
operation = unknown_upload(service, monkeypatch)
def invalid_metadata(*args):
raise ValueError("vendor metadata with private data")
monkeypatch.setattr(service.upload_reconciliation, "check", invalid_metadata)
service.sync()
op = service.operation(operation)
assert op["recovery"]["reason"] == "reconciliation_error"
assert "private" not in str(op["recovery"])
assert service.blocked("living-room")