110 lines
3.8 KiB
Python
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")
|