From 446c8c131abd4a47a9123f8eee816ee342aba2d5 Mon Sep 17 00:00:00 2001 From: xingzhou Date: Mon, 24 Aug 2026 14:12:34 +0800 Subject: [PATCH] fix(ios): device status leaves battery monitoring enabled (#128174) * fix(ios): restore battery monitoring after device status * fix(ios): preserve native battery monitoring ownership * test(ios): make battery ownership regression deterministic * fix(ios): avoid Swift Testing key-path inference failure --- .../Sources/Device/DeviceStatusService.swift | 7 ++-- apps/ios/Tests/NodeAppModelInvokeTests.swift | 41 ++++++++++++++++++- .../WatchApp/Sources/WatchDirectNode.swift | 2 + 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/apps/ios/Sources/Device/DeviceStatusService.swift b/apps/ios/Sources/Device/DeviceStatusService.swift index bd5b45dfaa12..58bc44c0eb19 100644 --- a/apps/ios/Sources/Device/DeviceStatusService.swift +++ b/apps/ios/Sources/Device/DeviceStatusService.swift @@ -11,7 +11,7 @@ final class DeviceStatusService: DeviceStatusServicing { } func status() async throws -> OpenClawDeviceStatusPayload { - let battery = self.batteryStatus() + let battery = Self.batteryStatus(device: UIDevice.current) let thermal = self.thermalStatus() let storage = self.storageStatus() let network = await self.networkStatus.currentStatus() @@ -40,9 +40,10 @@ final class DeviceStatusService: DeviceStatusServicing { locale: locale) } - private func batteryStatus() -> OpenClawBatteryStatusPayload { - let device = UIDevice.current + static func batteryStatus(device: UIDevice) -> OpenClawBatteryStatusPayload { + let wasMonitoring = device.isBatteryMonitoringEnabled device.isBatteryMonitoringEnabled = true + defer { device.isBatteryMonitoringEnabled = wasMonitoring } let level = device.batteryLevel >= 0 ? Double(device.batteryLevel) : nil let state: OpenClawBatteryState = switch device.batteryState { case .charging: .charging diff --git a/apps/ios/Tests/NodeAppModelInvokeTests.swift b/apps/ios/Tests/NodeAppModelInvokeTests.swift index d9c49e3d04a8..4ca54c4721b7 100644 --- a/apps/ios/Tests/NodeAppModelInvokeTests.swift +++ b/apps/ios/Tests/NodeAppModelInvokeTests.swift @@ -974,7 +974,47 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi } } +@MainActor +private final class BatteryMonitoringDevice: UIDevice { + private var monitoringEnabled: Bool + private(set) var monitoringStatesDuringBatteryReads: [Bool] = [] + + init(monitoringEnabled: Bool) { + self.monitoringEnabled = monitoringEnabled + super.init() + } + + override var isBatteryMonitoringEnabled: Bool { + get { self.monitoringEnabled } + set { self.monitoringEnabled = newValue } + } + + override var batteryLevel: Float { + self.monitoringStatesDuringBatteryReads.append(self.monitoringEnabled) + return 0.5 + } + + override var batteryState: UIDevice.BatteryState { + self.monitoringStatesDuringBatteryReads.append(self.monitoringEnabled) + return .charging + } +} + @Suite(.serialized) struct NodeAppModelInvokeTests { + @Test @MainActor func `device status battery snapshot preserves monitoring ownership`() { + for initial in [false, true] { + let device = BatteryMonitoringDevice(monitoringEnabled: initial) + + let payload = DeviceStatusService.batteryStatus(device: device) + + #expect(payload.level == 0.5) + #expect(payload.state == .charging) + #expect(!device.monitoringStatesDuringBatteryReads.isEmpty) + #expect(device.monitoringStatesDuringBatteryReads.allSatisfy { $0 }) + #expect(device.isBatteryMonitoringEnabled == initial) + } + } + @Test @MainActor func `health summary routes a fixed period to the health service`() async throws { let service = MockHealthSummaryService() let appModel = NodeAppModel(healthSummaryService: service) @@ -7542,5 +7582,4 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi try await appModel.sendVoiceTranscript(text: "hello", sessionKey: "main") } } - } diff --git a/apps/ios/WatchApp/Sources/WatchDirectNode.swift b/apps/ios/WatchApp/Sources/WatchDirectNode.swift index 3a99c33a7a2c..df479a129572 100644 --- a/apps/ios/WatchApp/Sources/WatchDirectNode.swift +++ b/apps/ios/WatchApp/Sources/WatchDirectNode.swift @@ -688,7 +688,9 @@ final class WatchDirectNode { private func deviceStatus() -> OpenClawDeviceStatusPayload { let device = WKInterfaceDevice.current() + let wasMonitoring = device.isBatteryMonitoringEnabled device.isBatteryMonitoringEnabled = true + defer { device.isBatteryMonitoringEnabled = wasMonitoring } let batteryState: OpenClawBatteryState = switch device.batteryState { case .charging: .charging case .full: .full