diff --git a/src/k1link/device_plugins/xgrids_k1/facade.py b/src/k1link/device_plugins/xgrids_k1/facade.py index cabcca2..be49053 100644 --- a/src/k1link/device_plugins/xgrids_k1/facade.py +++ b/src/k1link/device_plugins/xgrids_k1/facade.py @@ -17994,7 +17994,15 @@ class XgridsK1CompatibilityService: raise RuntimeError( "retained PREPARED STOP owner changed before topology commit" ) - self._retire_application_control_for_network_change() + # Verify owns a read-only replacement of the local control + # generation. A terminal device fault may leave that dead + # generation in ``failed`` even though its worker/socket are + # already gone. Retire only that local owner; the physical + # ledger remains unresolved until fresh DeviceInfo/Status + # evidence below classifies it. + self._retire_application_control_for_network_change( + allow_terminal_failure=True, + ) projected_intent_id = self._apply_read_only_device_topology( transport_ref=actual_transport_ref, connection_mode=requested_mode, diff --git a/tests/test_xgrids_acquisition_lifecycle.py b/tests/test_xgrids_acquisition_lifecycle.py index d4b1c42..3f0b59a 100644 --- a/tests/test_xgrids_acquisition_lifecycle.py +++ b/tests/test_xgrids_acquisition_lifecycle.py @@ -21481,6 +21481,22 @@ def test_public_physical_recovery_refreshes_stale_dhcp_then_classifies_without_w ) restarted, runtime = service_with_fake_runtime(tmp_path) + retirement_permissions: list[bool] = [] + + class TerminalDeviceFaultControl(FakeInteractiveControlSession): + def retire_for_network_change( + self, + *, + allow_terminal_failure: bool = False, + **kwargs: object, + ) -> dict[str, object]: + retirement_permissions.append(allow_terminal_failure) + if not allow_terminal_failure: + raise ApplicationAcceptanceError( + "terminal device fault requires explicit local retirement" + ) + return super().retire_for_network_change(**kwargs) + association_probe = FakeHostWifiAssociationProbe("a" * 64) restarted._host_wifi_association_probe = association_probe # type: ignore[assignment] # noqa: SLF001 capture = _durable_status_capture(device_id="test-ble-transport") @@ -21550,6 +21566,10 @@ def test_public_physical_recovery_refreshes_stale_dhcp_then_classifies_without_w assert configured_recovery["required_connection_mode"] == "bridge" assert configured_recovery["requires_live_gatt_validation"] is True + restarted._application_control_session = TerminalDeviceFaultControl( # type: ignore[assignment] # noqa: SLF001 + initial_state="failed" + ) + verified = asyncio.run(restarted.verify_connection(ConnectionVerifyRequest())) after = restarted._physical_command_ledger.snapshot().record # noqa: SLF001 @@ -21576,6 +21596,7 @@ def test_public_physical_recovery_refreshes_stale_dhcp_then_classifies_without_w facade_module.COMMAND_BOUND_ASSOCIATION_TIMEOUT_SECONDS, ] assert command_edges == [] + assert retirement_permissions == [True] assert runtime.start_calls == [] assert runtime.stop_calls == 0 assert verified["k1_ip"] == "192.168.68.51"