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")