2026-10-14 13:10:33 +08:00
|
|
|
import json
|
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from test_display import submit
|
|
|
|
|
|
|
|
|
|
from pigway_frame_flow.errors import ErrorCode, FlowError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def lose_response(service, monkeypatch, change_display=True):
|
|
|
|
|
driver = service.registry.get("living-room")
|
|
|
|
|
upload = driver.upload
|
|
|
|
|
|
|
|
|
|
def uncertain(path, name):
|
|
|
|
|
upload(path, name)
|
|
|
|
|
if not change_display:
|
|
|
|
|
driver.client.display(1)
|
|
|
|
|
raise FlowError(
|
|
|
|
|
ErrorCode.WRITE_RESULT_UNKNOWN, "Cloud write outcome is unknown", 502
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(driver, "upload", uncertain)
|
|
|
|
|
operation = submit(service)
|
|
|
|
|
service.process_one()
|
|
|
|
|
assert service.operation(operation)["status"] == "UNKNOWN"
|
|
|
|
|
return operation, driver
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_lost_upload_response_resolves_by_exact_bytes_without_reupload(
|
|
|
|
|
service, monkeypatch
|
|
|
|
|
):
|
|
|
|
|
operation, driver = lose_response(service, monkeypatch)
|
|
|
|
|
service.sync()
|
|
|
|
|
assert service.operation(operation)["status"] == "COMPLETED"
|
|
|
|
|
assert service.operation(operation)["result"]["image_id"] == 2
|
|
|
|
|
assert driver.client.uploads == 1
|
|
|
|
|
assert not service.blocked("living-room")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_same_name_wrong_bytes_does_not_unlock(service, monkeypatch):
|
|
|
|
|
operation, driver = lose_response(service, monkeypatch)
|
|
|
|
|
monkeypatch.setattr(driver, "download", lambda _: b"unrelated")
|
|
|
|
|
service.sync()
|
|
|
|
|
assert service.operation(operation)["status"] == "UNKNOWN"
|
|
|
|
|
assert service.blocked("living-room")
|
|
|
|
|
assert driver.client.uploads == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ambiguous_gallery_does_not_download_or_unlock(service, monkeypatch):
|
|
|
|
|
operation, driver = lose_response(service, monkeypatch)
|
|
|
|
|
driver.client.state["images"].append(dict(driver.client.state["images"][-1], id=3))
|
|
|
|
|
monkeypatch.setattr(driver, "download", lambda _: pytest.fail("Ambiguous download"))
|
|
|
|
|
service.sync()
|
|
|
|
|
assert service.operation(operation)["status"] == "UNKNOWN"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_confirmed_upload_not_displayed_closes_failed_display_after_stable_checks(
|
|
|
|
|
service, monkeypatch
|
|
|
|
|
):
|
|
|
|
|
operation, driver = lose_response(service, monkeypatch, change_display=False)
|
|
|
|
|
service.sync()
|
|
|
|
|
result = service.operation(operation)["result"]
|
|
|
|
|
assert result["upload_confirmed"]
|
|
|
|
|
start = datetime.fromisoformat(result["upload_intent"]["started_at"])
|
|
|
|
|
import pigway_frame_flow.display.upload_reconciliation as module
|
|
|
|
|
|
|
|
|
|
class Later(datetime):
|
|
|
|
|
@classmethod
|
|
|
|
|
def now(cls, tz=None):
|
|
|
|
|
return start + timedelta(seconds=service.settings.sync_timeout + 600)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(module, "datetime", Later)
|
|
|
|
|
service.unknown_checked.clear()
|
|
|
|
|
service.sync()
|
|
|
|
|
assert service.operation(operation)["status"] == "UNKNOWN"
|
|
|
|
|
result = service.operation(operation)["result"]
|
|
|
|
|
result["unchanged_observed_at"] = (
|
|
|
|
|
datetime.now(UTC) - timedelta(seconds=61)
|
|
|
|
|
).isoformat()
|
|
|
|
|
service.db.execute(
|
|
|
|
|
"UPDATE operations SET result=? WHERE id=?", (json.dumps(result), operation)
|
|
|
|
|
)
|
|
|
|
|
service.unknown_checked.clear()
|
|
|
|
|
service.sync()
|
|
|
|
|
assert service.operation(operation)["status"] == "FAILED"
|
|
|
|
|
assert service.operation(operation)["error"]["code"] == "DISPLAY_NOT_CONFIRMED"
|
|
|
|
|
assert not service.blocked("living-room")
|
|
|
|
|
assert driver.client.uploads == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_legacy_unknown_without_intent_remains_locked(service):
|
|
|
|
|
operation = submit(service)
|
|
|
|
|
service._start_write(operation)
|
|
|
|
|
service.update(operation, "UNKNOWN")
|
|
|
|
|
service.sync()
|
|
|
|
|
assert service.blocked("living-room")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_recovered_upload_preserves_temporary_restore(service, monkeypatch):
|
|
|
|
|
driver = service.registry.get("living-room")
|
|
|
|
|
upload = driver.upload
|
|
|
|
|
|
|
|
|
|
def uncertain(path, name):
|
|
|
|
|
upload(path, name)
|
|
|
|
|
raise FlowError(ErrorCode.WRITE_RESULT_UNKNOWN, "Response lost", 502)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(driver, "upload", uncertain)
|
|
|
|
|
operation = submit(service, duration=60, restore=True)
|
|
|
|
|
service.process_one()
|
|
|
|
|
service.sync()
|
|
|
|
|
assert service.operation(operation)["status"] == "COMPLETED"
|
|
|
|
|
assert service.top("living-room")["id"] == operation
|
|
|
|
|
assert service.state("living-room")["managed_image_id"] == 2
|
2026-10-16 10:30:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_manual_close_preserves_unknown_upload_evidence(service, monkeypatch):
|
|
|
|
|
operation, driver = lose_response(service, monkeypatch, change_display=False)
|
|
|
|
|
before = service.operation(operation)
|
|
|
|
|
assert before["result"].get("image_id") is None
|
|
|
|
|
result = service.reconcile(operation, acknowledge=True)
|
|
|
|
|
after = service.operation(operation)
|
|
|
|
|
assert result["resolved"]
|
|
|
|
|
assert after["status"] == "FAILED"
|
|
|
|
|
assert after["result"] == before["result"]
|
|
|
|
|
assert after["error"]["code"] == "DISPLAY_NOT_CONFIRMED"
|
|
|
|
|
assert after["error"]["details"]["previous_error"] == before["error"]
|
|
|
|
|
assert after["error"]["details"]["observed_image_id"] == 1
|
|
|
|
|
assert not service.blocked("living-room")
|
|
|
|
|
assert driver.client.uploads == 1
|
|
|
|
|
assert (
|
|
|
|
|
service.db.one("SELECT * FROM history ORDER BY id DESC LIMIT 1")["result"]
|
|
|
|
|
== "ACKNOWLEDGED"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upload_transport_diagnostics_persist_on_operation(service, monkeypatch):
|
|
|
|
|
driver = service.registry.get("living-room")
|
|
|
|
|
|
|
|
|
|
def fail(*args):
|
|
|
|
|
raise FlowError(
|
|
|
|
|
ErrorCode.WRITE_RESULT_UNKNOWN,
|
|
|
|
|
"Cloud write outcome is unknown",
|
|
|
|
|
502,
|
|
|
|
|
details={"transport_error": "read_timeout"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(driver, "upload", fail)
|
|
|
|
|
operation = submit(service)
|
|
|
|
|
service.process_one()
|
|
|
|
|
op = service.operation(operation)
|
|
|
|
|
assert op["status"] == "UNKNOWN"
|
|
|
|
|
assert op["error"]["details"] == {"transport_error": "read_timeout"}
|
|
|
|
|
assert op["result"]["upload_intent"]
|