fix(k1): restore canonical local connection lifecycle
This commit is contained in:
@@ -200,14 +200,18 @@ def service_with_fake_runtime(
|
||||
return service, runtime
|
||||
|
||||
|
||||
def _wifi_status_read(ipv4: str | None) -> dict[str, Any]:
|
||||
def _wifi_status_read(
|
||||
ipv4: str | None,
|
||||
*,
|
||||
device_id: str = "test-ble-transport",
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
||||
"observed_at_utc": "2026-07-20T12:00:00Z",
|
||||
"adapter": "CoreBluetooth",
|
||||
"bleak_version": "test",
|
||||
"device_macos_uuid": "test-ble-transport",
|
||||
"device_macos_uuid": device_id,
|
||||
"device_name": "XGR-K1",
|
||||
"service_uuid": "00007f00-0000-1000-8000-00805f9b34fb",
|
||||
"status_characteristic_uuid": "00007f02-0000-1000-8000-00805f9b34fb",
|
||||
@@ -2867,6 +2871,7 @@ def test_quick_connect_activates_the_device_ap_then_associates_the_host(
|
||||
"ready_observed": True,
|
||||
"write_performed": True,
|
||||
"write_mode": "with_response",
|
||||
"observations": [{"status": {"mode": "WIFI_AP"}}],
|
||||
}
|
||||
finally:
|
||||
ble_session_open = False
|
||||
@@ -3090,6 +3095,7 @@ def test_quick_connect_does_not_start_host_wifi_without_ap_ready_flag(
|
||||
"ready_observed": False,
|
||||
"write_performed": True,
|
||||
"write_mode": "with_response",
|
||||
"observations": [],
|
||||
}
|
||||
|
||||
def forbidden_association(*_: object, **__: object) -> dict[str, Any]:
|
||||
@@ -3121,6 +3127,9 @@ def test_quick_connect_does_not_start_host_wifi_without_ap_ready_flag(
|
||||
assert len(quick_sessions) == 1
|
||||
assert (quick_sessions[0] / "ap-activation.redacted.json").exists()
|
||||
assert not (quick_sessions[0] / "manifest.redacted.json").exists()
|
||||
reconciliation = service.state()["network_write_reconciliation"]
|
||||
assert reconciliation["transport_ref"] == "k1-a"
|
||||
assert reconciliation["status"] == "device-state-unknown-after-write"
|
||||
|
||||
|
||||
def test_failed_connection_change_revokes_the_previous_route(
|
||||
@@ -3154,6 +3163,7 @@ def test_failed_connection_change_revokes_the_previous_route(
|
||||
"ready_observed": True,
|
||||
"write_performed": True,
|
||||
"write_mode": "with_response",
|
||||
"observations": [{"status": {"mode": "WIFI_AP"}}],
|
||||
}
|
||||
|
||||
def failed_association(*_: object, **__: object) -> dict[str, Any]:
|
||||
@@ -3195,6 +3205,75 @@ def test_failed_connection_change_revokes_the_previous_route(
|
||||
assert failure_evidence["scan_elapsed_ms"] == 15014
|
||||
|
||||
|
||||
def test_quick_connect_helper_build_failure_after_ap_write_preserves_side_effect_facts(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
_set_scanned_devices(service, [{"device_id": "k1-a", "name": "XGR-TEST-A"}])
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"ensure_wifi_profile_from_credential_source",
|
||||
lambda *_args, **_kwargs: {
|
||||
"schema_version": 1,
|
||||
"adapter": "macOS Keychain",
|
||||
"available": True,
|
||||
"profile_enrolled": False,
|
||||
"credential_source": "exact-firmware-profile",
|
||||
},
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def ready_activation(*_: object, **__: object) -> AsyncIterator[dict[str, Any]]:
|
||||
yield {
|
||||
"profile_id": "xgrids-k1-fw3-quick-connect-ap-v1",
|
||||
"started_at_utc": "2026-08-06T10:00:00Z",
|
||||
"completed_at_utc": "2026-08-06T10:00:01Z",
|
||||
"outcome": "ap_ready_observed",
|
||||
"ready_observed": True,
|
||||
"write_performed": True,
|
||||
"write_mode": "with_response",
|
||||
"observations": [{"status": {"mode": "WIFI_AP"}}],
|
||||
}
|
||||
|
||||
def failed_post_write_build(*_: object, **__: object) -> dict[str, Any]:
|
||||
raise facade_module.HostWifiProfileError(
|
||||
"host-wifi-helper-build-failed",
|
||||
helper_stage="compile",
|
||||
helper_elapsed_ms=21,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(facade_module, "device_ap_activation_session", ready_activation)
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"associate_with_wifi_profile_once",
|
||||
failed_post_write_build,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
facade_module.HostWifiProfileError,
|
||||
match="host-wifi-helper-build-failed",
|
||||
):
|
||||
asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
state = service.state()
|
||||
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
||||
assert operation["stage_code"] == "host-wifi-association-failed"
|
||||
assert operation["error"]["code"] == "host-wifi-helper-build-failed"
|
||||
assert operation["error"]["side_effect_status"] == "confirmed"
|
||||
assert operation["error"]["safe_to_retry"] is False
|
||||
assert operation["error"]["helper_stage"] == "compile"
|
||||
assert state["network_write_reconciliation"] is None
|
||||
|
||||
|
||||
def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
@@ -3236,7 +3315,8 @@ def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host(
|
||||
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
||||
assert operation["status"] == "failed"
|
||||
assert operation["error"]["safe_to_retry"] is False
|
||||
assert operation["error"]["side_effect_status"] == "unknown"
|
||||
assert operation["error"]["side_effect_status"] == "confirmed"
|
||||
assert state["network_write_reconciliation"] is None
|
||||
failure_log = next(
|
||||
record
|
||||
for record in caplog.records
|
||||
@@ -3246,12 +3326,212 @@ def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host(
|
||||
assert failure_log.connection_mode == "bridge"
|
||||
assert failure_log.error_code == "RuntimeError"
|
||||
assert failure_log.safe_to_retry is False
|
||||
assert failure_log.side_effect_status == "unknown"
|
||||
assert failure_log.side_effect_status == "confirmed"
|
||||
assert failure_log.network_change_attempted is True
|
||||
assert failure_log.device_write_attempted is True
|
||||
assert failure_log.device_write_confirmed is True
|
||||
assert PRIMARY_TEST_CREDENTIAL not in caplog.text
|
||||
assert "lab-network" not in caplog.text
|
||||
|
||||
|
||||
def test_ambiguous_ble_write_blocks_new_network_mutation_until_reconciled(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
_set_scanned_devices(service, [{"device_id": "k1-a"}])
|
||||
calls = 0
|
||||
|
||||
async def ambiguous_write(*_: object, **__: object) -> dict[str, Any]:
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
exc = RuntimeError("synthetic transport failure")
|
||||
exc.operation_stage = "gatt-write" # type: ignore[attr-defined]
|
||||
exc.device_write_attempted = True # type: ignore[attr-defined]
|
||||
exc.device_write_confirmed = False # type: ignore[attr-defined]
|
||||
exc.att_error_code = 4 # type: ignore[attr-defined]
|
||||
exc.att_error_name = "INVALID_PDU" # type: ignore[attr-defined]
|
||||
raise exc
|
||||
|
||||
monkeypatch.setattr(facade_module, "provision_wifi_once", ambiguous_write)
|
||||
|
||||
with pytest.raises(RuntimeError, match="synthetic transport failure"):
|
||||
asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
ssid="lab-network",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
compatibility_attestation=ATTESTATION,
|
||||
idempotency_key="ambiguous-write-1",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
state = service.state()
|
||||
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
||||
assert operation["error"] == {
|
||||
"category": "device",
|
||||
"code": "RuntimeError",
|
||||
"retryable": False,
|
||||
"safe_to_retry": False,
|
||||
"side_effect_status": "unknown",
|
||||
"operation_stage": "gatt-write",
|
||||
"device_write_attempted": True,
|
||||
"device_write_confirmed": False,
|
||||
"ble_att_error_code": 4,
|
||||
"ble_att_error_name": "INVALID_PDU",
|
||||
}
|
||||
assert state["network_write_reconciliation"] == {
|
||||
"status": "device-state-unknown-after-write",
|
||||
"operation_id": operation["operation_id"],
|
||||
"transport_ref": "k1-a",
|
||||
"connection_mode": "bridge",
|
||||
"operation_stage": "gatt-write",
|
||||
"reason_code": "RuntimeError",
|
||||
"device_write_confirmed": False,
|
||||
"required_action": "explicit-read-only-ble-status-observation",
|
||||
"scope": "process-runtime",
|
||||
"observed_at": state["network_write_reconciliation"]["observed_at"],
|
||||
}
|
||||
|
||||
with pytest.raises(
|
||||
facade_module.NetworkWriteReconciliationRequired,
|
||||
match="новая запись заблокирована",
|
||||
):
|
||||
asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
ssid="another-network",
|
||||
password=SecretStr(SECONDARY_TEST_CREDENTIAL),
|
||||
compatibility_attestation=ATTESTATION,
|
||||
idempotency_key="ambiguous-write-2",
|
||||
)
|
||||
)
|
||||
)
|
||||
assert calls == 1
|
||||
|
||||
|
||||
def test_unchanged_status_does_not_confirm_without_response_write(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
_set_scanned_devices(service, [{"device_id": "k1-a"}])
|
||||
unchanged_status = {"ipv4": None, "mode": "WIFI_CLIENT"}
|
||||
|
||||
async def unchanged_write(*_: object, **__: object) -> dict[str, Any]:
|
||||
return {
|
||||
"started_at_utc": "2026-08-06T10:00:00Z",
|
||||
"completed_at_utc": "2026-08-06T10:00:01Z",
|
||||
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
||||
"outcome": "no_status_change_before_timeout",
|
||||
"write_mode": "without_response",
|
||||
"baseline_status": unchanged_status,
|
||||
"observations": [{"status": unchanged_status}],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(facade_module, "provision_wifi_once", unchanged_write)
|
||||
|
||||
with (
|
||||
caplog.at_level(logging.ERROR, logger=facade_module.__name__),
|
||||
pytest.raises(RuntimeError, match="не сообщило адрес"),
|
||||
):
|
||||
asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
ssid="lab-network",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
record = next(
|
||||
item
|
||||
for item in caplog.records
|
||||
if getattr(item, "event_code", None) == "k1_network_provision_failed"
|
||||
)
|
||||
assert record.device_write_attempted is True
|
||||
assert record.device_write_confirmed is False
|
||||
assert record.side_effect_status == "unknown"
|
||||
assert service.state()["network_write_reconciliation"] is None
|
||||
|
||||
|
||||
def test_read_only_ble_status_clears_network_write_reconciliation_fence(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
_set_scanned_k1(service)
|
||||
service._network_write_reconciliation = { # noqa: SLF001
|
||||
"status": "device-state-unknown-after-write",
|
||||
"operation_id": "ambiguous-operation",
|
||||
"transport_ref": "test-ble-transport",
|
||||
"connection_mode": "bridge",
|
||||
"operation_stage": "gatt-write",
|
||||
"reason_code": "BleakGATTProtocolError",
|
||||
"device_write_confirmed": False,
|
||||
"required_action": "explicit-read-only-ble-status-observation",
|
||||
"scope": "process-runtime",
|
||||
"observed_at": "2026-08-06T10:00:00Z",
|
||||
}
|
||||
|
||||
async def read_current_status(*_: object, **__: object) -> dict[str, Any]:
|
||||
return _wifi_status_read(None)
|
||||
|
||||
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_current_status)
|
||||
|
||||
with pytest.raises(RuntimeError, match="не сообщил актуальный DHCP-адрес"):
|
||||
service.verify_connection(
|
||||
ConnectionVerifyRequest(
|
||||
device_id="test-ble-transport",
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
)
|
||||
|
||||
assert service.state()["network_write_reconciliation"] is None
|
||||
|
||||
|
||||
def test_read_only_ble_status_for_another_transport_keeps_reconciliation_fence(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
_set_scanned_k1(service, device_id="k1-b")
|
||||
fence = {
|
||||
"status": "device-state-unknown-after-write",
|
||||
"operation_id": "ambiguous-operation",
|
||||
"transport_ref": "k1-a",
|
||||
"connection_mode": "bridge",
|
||||
"operation_stage": "gatt-write",
|
||||
"reason_code": "BleakGATTProtocolError",
|
||||
"device_write_confirmed": False,
|
||||
"required_action": "explicit-read-only-ble-status-observation",
|
||||
"scope": "process-runtime",
|
||||
"observed_at": "2026-08-06T10:00:00Z",
|
||||
}
|
||||
service._network_write_reconciliation = dict(fence) # noqa: SLF001
|
||||
|
||||
async def read_other_status(*_: object, **__: object) -> dict[str, Any]:
|
||||
return _wifi_status_read(None, device_id="k1-b")
|
||||
|
||||
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_other_status)
|
||||
|
||||
with pytest.raises(RuntimeError, match="не сообщил актуальный DHCP-адрес"):
|
||||
service.verify_connection(
|
||||
ConnectionVerifyRequest(
|
||||
device_id="k1-b",
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
)
|
||||
|
||||
assert service.state()["network_write_reconciliation"] == fence
|
||||
|
||||
|
||||
def test_provisioning_cannot_switch_device_during_active_acquisition(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
|
||||
Reference in New Issue
Block a user