@mentra/bluetooth-sdk 3.4.0-dev.996 → 3.4.0-dev.999

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/MIGRATION.md +61 -0
  2. package/README.md +47 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +8 -1
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +48 -4
  6. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +21 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  8. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +2 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/connection/ConnectionModels.kt +5 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/GlassesAudioSetupPolicy.kt +64 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +788 -134
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveOwnerLossPolicy.kt +227 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +19 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingReadinessPolicy.kt +94 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateMachine.kt +411 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateStore.kt +124 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +50 -0
  18. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerOwnerLossTest.kt +146 -0
  19. package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusAudioSetupTest.kt +51 -0
  20. package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +2 -0
  21. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/GlassesAudioSetupPolicyTest.kt +65 -0
  22. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveAudioSetupReadinessTest.kt +231 -0
  23. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveOwnerLossPolicyTest.kt +368 -0
  24. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +20 -0
  25. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingReadinessPolicyTest.kt +136 -0
  26. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateMachineTest.kt +346 -0
  27. package/build/BluetoothSdk.types.d.ts +101 -0
  28. package/build/BluetoothSdk.types.d.ts.map +1 -1
  29. package/build/BluetoothSdk.types.js.map +1 -1
  30. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  31. package/build/_private/BluetoothSdkModule.js +1 -0
  32. package/build/_private/BluetoothSdkModule.js.map +1 -1
  33. package/build/generated/releaseMetadata.js +5 -5
  34. package/build/generated/releaseMetadata.js.map +1 -1
  35. package/build/index.d.ts +1 -1
  36. package/build/index.d.ts.map +1 -1
  37. package/build/index.js +3 -0
  38. package/build/index.js.map +1 -1
  39. package/ios/BluetoothSdkModule.swift +8 -2
  40. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  41. package/ios/Source/Bridge.swift +11 -3
  42. package/ios/Source/DeviceManager.swift +80 -6
  43. package/ios/Source/DeviceStore.swift +27 -0
  44. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  45. package/ios/Source/MentraBluetoothSDK.swift +10 -4
  46. package/ios/Source/errors/BluetoothSdkError.swift +5 -1
  47. package/ios/Source/sgcs/GlassesAudioSetupPolicy.swift +181 -0
  48. package/ios/Source/sgcs/MentraLive.swift +1053 -166
  49. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +260 -1
  50. package/ios/Source/sgcs/MentraLivePairingStateMachine.swift +385 -0
  51. package/ios/Source/sgcs/MentraLivePairingStateStore.swift +121 -0
  52. package/ios/Source/sgcs/SGCManager.swift +7 -0
  53. package/ios/Source/sgcs/Simulated.swift +2 -0
  54. package/ios/Source/status/DeviceStatus.swift +35 -0
  55. package/ios/Source/types/DeviceModels.swift +8 -1
  56. package/ios/Source/utils/AudioSessionMonitor.swift +158 -5
  57. package/ios/Tests/BluetoothAudioRouteTests.swift +82 -0
  58. package/ios/Tests/GlassesAudioSetupPolicyTests.swift +191 -0
  59. package/ios/Tests/MentraLiveAudioSetupTests.swift +215 -0
  60. package/ios/Tests/MentraLiveConnectionOptionsTests.swift +55 -0
  61. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +327 -0
  62. package/ios/Tests/MentraLivePairingStateMachineTests.swift +402 -0
  63. package/ios/Tests/MentraLiveSessionReadinessTests.swift +328 -26
  64. package/package.json +3 -2
  65. package/src/BluetoothSdk.types.ts +97 -0
  66. package/src/_private/BluetoothSdkModule.ts +1 -0
  67. package/src/generated/releaseMetadata.ts +5 -5
  68. package/src/index.ts +10 -0
@@ -1,8 +1,57 @@
1
+ import CoreBluetooth
1
2
  import Foundation
2
3
  @testable import MentraBluetoothSDK
3
4
  import XCTest
4
5
 
5
6
  final class MentraLivePairingAdvertisementTests: XCTestCase {
7
+ func testKnownConnectUsesThePeripheralFoundDuringPairing() {
8
+ XCTAssertEqual(
9
+ MentraLiveKnownConnectRoute.decide(
10
+ bluetoothPoweredOn: false,
11
+ hasCachedPeripheral: true,
12
+ hasSavedIdentifier: true,
13
+ systemConnectedMatch: true
14
+ ),
15
+ .waitForPower
16
+ )
17
+ XCTAssertEqual(
18
+ MentraLiveKnownConnectRoute.decide(
19
+ bluetoothPoweredOn: true,
20
+ hasCachedPeripheral: true,
21
+ hasSavedIdentifier: true,
22
+ systemConnectedMatch: false
23
+ ),
24
+ .cachedPeripheral
25
+ )
26
+ XCTAssertEqual(
27
+ MentraLiveKnownConnectRoute.decide(
28
+ bluetoothPoweredOn: true,
29
+ hasCachedPeripheral: false,
30
+ hasSavedIdentifier: true,
31
+ systemConnectedMatch: false
32
+ ),
33
+ .savedIdentifier
34
+ )
35
+ XCTAssertEqual(
36
+ MentraLiveKnownConnectRoute.decide(
37
+ bluetoothPoweredOn: true,
38
+ hasCachedPeripheral: false,
39
+ hasSavedIdentifier: false,
40
+ systemConnectedMatch: true
41
+ ),
42
+ .alreadyConnected
43
+ )
44
+ XCTAssertEqual(
45
+ MentraLiveKnownConnectRoute.decide(
46
+ bluetoothPoweredOn: true,
47
+ hasCachedPeripheral: false,
48
+ hasSavedIdentifier: false,
49
+ systemConnectedMatch: false
50
+ ),
51
+ .scan
52
+ )
53
+ }
54
+
6
55
  func testConnectedPairingRecoveryRequiresExplicitPendingTarget() {
7
56
  XCTAssertFalse(
8
57
  MentraLivePendingPairingTarget.matches(
@@ -139,6 +188,188 @@ final class MentraLivePairingAdvertisementTests: XCTestCase {
139
188
  }
140
189
  }
141
190
 
191
+ func testCapabilityFlagDoesNotConfirmOwnership() {
192
+ XCTAssertFalse(MentraLiveConnectionAttemptPolicy.pairingInfoConfirmsOwner(ownerCommitted: false))
193
+ XCTAssertTrue(MentraLiveConnectionAttemptPolicy.pairingInfoConfirmsOwner(ownerCommitted: true))
194
+ }
195
+
196
+ func testStaleIosBondPromptsOnceUntilTheCooldownPasses() {
197
+ typealias Policy = MentraLiveConnectionAttemptPolicy
198
+ XCTAssertTrue(
199
+ Policy.shouldPromptToForgetStaleIosBond(peerRemovedPairingInformation: true, promptedAt: 0, now: 100)
200
+ )
201
+ XCTAssertFalse(
202
+ Policy.shouldPromptToForgetStaleIosBond(peerRemovedPairingInformation: true, promptedAt: 90, now: 100)
203
+ )
204
+ XCTAssertTrue(
205
+ Policy.shouldPromptToForgetStaleIosBond(
206
+ peerRemovedPairingInformation: true,
207
+ promptedAt: 100,
208
+ now: 100 + Policy.staleIosBondPromptCooldownSeconds
209
+ )
210
+ )
211
+ XCTAssertFalse(
212
+ Policy.shouldPromptToForgetStaleIosBond(peerRemovedPairingInformation: false, promptedAt: 0, now: 100)
213
+ )
214
+ }
215
+
216
+ func testOnlySecureFirmwareOutsideExplicitPairingRejectsAnOwner() {
217
+ typealias Policy = MentraLiveConnectionAttemptPolicy
218
+ XCTAssertTrue(Policy.isOwnerRejection(isAuthFailure: true, explicitPairing: false, secureFirmware: true))
219
+ XCTAssertFalse(Policy.isOwnerRejection(isAuthFailure: true, explicitPairing: true, secureFirmware: true))
220
+ XCTAssertFalse(Policy.isOwnerRejection(isAuthFailure: true, explicitPairing: false, secureFirmware: false))
221
+ XCTAssertFalse(Policy.isOwnerRejection(isAuthFailure: false, explicitPairing: false, secureFirmware: true))
222
+ }
223
+
224
+ func testASingleRejectionDoesNotStandDown() {
225
+ var streak = MentraLiveOwnerRejectionStreak()
226
+ XCTAssertFalse(streak.record(nowMs: 0))
227
+ }
228
+
229
+ func testQuickRejectionsWaitForTheStreakToSpanAboutAMinute() {
230
+ var streak = MentraLiveOwnerRejectionStreak()
231
+ XCTAssertFalse(streak.record(nowMs: 0))
232
+ XCTAssertFalse(streak.record(nowMs: 3_000))
233
+ // Third attempt, but a glasses reboot also rejects for a few seconds.
234
+ XCTAssertFalse(streak.record(nowMs: 8_000))
235
+ XCTAssertFalse(streak.record(nowMs: 24_000))
236
+ XCTAssertTrue(streak.record(nowMs: MentraLiveOwnerRejectionStreak.minSpanMs))
237
+ }
238
+
239
+ func testASuccessfulSessionEndsTheStreak() {
240
+ var streak = MentraLiveOwnerRejectionStreak()
241
+ _ = streak.record(nowMs: 0)
242
+ _ = streak.record(nowMs: 10_000)
243
+ streak.reset()
244
+ XCTAssertFalse(streak.record(nowMs: 50_000))
245
+ XCTAssertFalse(streak.record(nowMs: 60_000))
246
+ }
247
+
248
+ func testRejectionsFarApartAreNotOneStreak() {
249
+ var streak = MentraLiveOwnerRejectionStreak()
250
+ _ = streak.record(nowMs: 0)
251
+ _ = streak.record(nowMs: 10_000)
252
+ _ = streak.record(nowMs: 20_000)
253
+ let later = 20_000 + MentraLiveOwnerRejectionStreak.streakGapMs + 1
254
+ XCTAssertFalse(streak.record(nowMs: later))
255
+ XCTAssertFalse(streak.record(nowMs: later + 10_000))
256
+ XCTAssertFalse(streak.record(nowMs: later + 20_000))
257
+ }
258
+
259
+ func testOneAttemptReportingTwiceCountsOnce() {
260
+ var streak = MentraLiveOwnerRejectionStreak()
261
+ let span = MentraLiveOwnerRejectionStreak.minSpanMs
262
+ // didFailToConnect then didDisconnect for each of two attempts.
263
+ XCTAssertFalse(streak.record(nowMs: 0))
264
+ XCTAssertFalse(streak.record(nowMs: 400))
265
+ XCTAssertFalse(streak.record(nowMs: span))
266
+ XCTAssertFalse(streak.record(nowMs: span + 400))
267
+ // A genuine third attempt completes the streak.
268
+ XCTAssertTrue(streak.record(nowMs: span + 10_000))
269
+ }
270
+
271
+ func testAutomaticReconnectStandsDownWhileTheSavedGlassesAdvertisePairingMode() {
272
+ typealias Policy = MentraLiveConnectionAttemptPolicy
273
+ XCTAssertTrue(Policy.shouldStandDownInsteadOfReconnect(
274
+ pairingMode: true, savedGlasses: true, explicitPairing: false
275
+ ))
276
+ XCTAssertFalse(Policy.shouldStandDownInsteadOfReconnect(
277
+ pairingMode: true, savedGlasses: true, explicitPairing: true
278
+ ))
279
+ XCTAssertFalse(Policy.shouldStandDownInsteadOfReconnect(
280
+ pairingMode: false, savedGlasses: true, explicitPairing: false
281
+ ))
282
+ XCTAssertFalse(Policy.shouldStandDownInsteadOfReconnect(
283
+ pairingMode: true, savedGlasses: false, explicitPairing: false
284
+ ))
285
+ }
286
+
287
+ func testPauseIsTheWindowPlusTheCloseMarginWithNo300sClamp() {
288
+ typealias Policy = MentraLiveConnectionAttemptPolicy
289
+ XCTAssertEqual(Policy.yieldMs(windowMs: 120_000), 150_000)
290
+ XCTAssertEqual(Policy.yieldMs(windowMs: 900_000), 930_000)
291
+ XCTAssertEqual(Policy.yieldMs(windowMs: 5_000), 35_000)
292
+ // A missing window falls back to the spec default, not to a floor.
293
+ XCTAssertEqual(Policy.yieldMs(windowMs: 0), 150_000)
294
+ XCTAssertEqual(Policy.yieldMs(windowMs: -1), 150_000)
295
+ }
296
+
297
+ func testARestartedAppKeepsOnlyALiveDeadlineWithinThePersistedPause() {
298
+ typealias Policy = MentraLiveConnectionAttemptPolicy
299
+ let now = 1_000_000
300
+ let pause = 150_000
301
+ XCTAssertEqual(Policy.persistedYieldRemainingMs(untilWallMs: 0, nowWallMs: now, pauseMs: pause), 0)
302
+ XCTAssertEqual(Policy.persistedYieldRemainingMs(untilWallMs: now + 50_000, nowWallMs: now, pauseMs: pause), 50_000)
303
+ XCTAssertEqual(Policy.persistedYieldRemainingMs(untilWallMs: now - 1, nowWallMs: now, pauseMs: pause), 0)
304
+ XCTAssertEqual(Policy.persistedYieldRemainingMs(untilWallMs: now + pause, nowWallMs: now, pauseMs: pause), pause)
305
+ // Longer than the pause persisted with it: a clock jump, not a deadline.
306
+ XCTAssertEqual(Policy.persistedYieldRemainingMs(untilWallMs: now + pause + 1, nowWallMs: now, pauseMs: pause), 0)
307
+ // A long window is not clamped to the old 300 s bound.
308
+ XCTAssertEqual(
309
+ Policy.persistedYieldRemainingMs(untilWallMs: now + 600_000, nowWallMs: now, pauseMs: 930_000),
310
+ 600_000
311
+ )
312
+ XCTAssertEqual(Policy.persistedYieldRemainingMs(untilWallMs: now + 10, nowWallMs: now, pauseMs: 0), 0)
313
+ }
314
+
315
+ func testPairSelectGoesOnlyWithAnExplicitAttemptToSecureGlasses() {
316
+ typealias Policy = MentraLiveConnectionAttemptPolicy
317
+ XCTAssertTrue(Policy.shouldSendPairSelect(explicitPairing: true, securePairingTarget: true))
318
+ XCTAssertFalse(Policy.shouldSendPairSelect(explicitPairing: false, securePairingTarget: true))
319
+ XCTAssertFalse(Policy.shouldSendPairSelect(explicitPairing: true, securePairingTarget: false))
320
+ XCTAssertFalse(Policy.shouldSendPairSelect(explicitPairing: false, securePairingTarget: false))
321
+ }
322
+
323
+ func testParkedSecureReadinessAlwaysHasABondWaitDeadline() {
324
+ typealias Policy = MentraLiveConnectionAttemptPolicy
325
+ // glasses_ready parks exactly when the attempt is explicit and the target secure; the
326
+ // wait must then always run so the pair cannot hang.
327
+ XCTAssertTrue(Policy.shouldRunSecurePairingWait(explicitPairing: true, securePairingTarget: true))
328
+ XCTAssertFalse(Policy.shouldRunSecurePairingWait(explicitPairing: false, securePairingTarget: true))
329
+ XCTAssertFalse(Policy.shouldRunSecurePairingWait(explicitPairing: true, securePairingTarget: false))
330
+ }
331
+
332
+ func testTheStaleBondCodeMatchesTheAppContract() {
333
+ XCTAssertEqual(MentraLiveConnectionAttemptPolicy.iosForgetPairingCode, "ios_forget_pairing")
334
+ }
335
+
336
+ func testAuthFailureClassifierCoversError14AndInsufficientSecurityOnly() {
337
+ XCTAssertTrue(MentraLive.isPairingAuthFailure(
338
+ NSError(domain: CBErrorDomain, code: CBError.peerRemovedPairingInformation.rawValue)
339
+ ))
340
+ XCTAssertTrue(MentraLive.isPairingAuthFailure(
341
+ NSError(domain: CBATTErrorDomain, code: CBATTError.insufficientAuthentication.rawValue)
342
+ ))
343
+ XCTAssertTrue(MentraLive.isPairingAuthFailure(
344
+ NSError(domain: CBATTErrorDomain, code: CBATTError.insufficientEncryption.rawValue)
345
+ ))
346
+ // A plain remote disconnect (the glasses' 0x14 refusal) and timeouts are not.
347
+ XCTAssertFalse(MentraLive.isPairingAuthFailure(
348
+ NSError(domain: CBErrorDomain, code: CBError.peripheralDisconnected.rawValue)
349
+ ))
350
+ XCTAssertFalse(MentraLive.isPairingAuthFailure(
351
+ NSError(domain: CBErrorDomain, code: CBError.connectionTimeout.rawValue)
352
+ ))
353
+ XCTAssertFalse(MentraLive.isPairingAuthFailure(nil))
354
+ }
355
+
356
+ func testReadsTheOwnerChangedFlagAndWindowIdFromTheV4Trailer() {
357
+ var payload = securePayload(pairingFlag: 0)
358
+ payload[2 + 7] = 0x01 | 0x04
359
+ payload[2 + 12] = 9
360
+ let result = MentraLivePairingAdvertisement.parse(coreBluetoothManufacturerData: payload)
361
+ XCTAssertEqual(result?.ownerChanged, true)
362
+ XCTAssertEqual(result?.pairingGeneration, 9)
363
+ XCTAssertEqual(result?.pairingMode, false)
364
+ }
365
+
366
+ func testOwnerChangedIsFalseWithoutTheFlag() {
367
+ let result = MentraLivePairingAdvertisement.parse(
368
+ coreBluetoothManufacturerData: securePayload(pairingFlag: 1)
369
+ )
370
+ XCTAssertEqual(result?.ownerChanged, false)
371
+ }
372
+
142
373
  private func legacyPayload() -> Data {
143
374
  var data = Data(repeating: 0, count: 29)
144
375
  data[0] = 0x22
@@ -157,4 +388,100 @@ final class MentraLivePairingAdvertisementTests: XCTestCase {
157
388
  data[2 + 11] = 0x50
158
389
  return data
159
390
  }
391
+
392
+ func testAPacketWithoutTheTrailerRightAfterOneWithItIsPartialNotLegacy() {
393
+ let t0 = 1_000_000
394
+ XCTAssertTrue(
395
+ MentraLiveDiscoveryPolicy.isPartialAdvertisement(secureCapable: false, lastSecureTrailerAtMs: t0, nowMs: t0 + 500)
396
+ )
397
+ XCTAssertTrue(
398
+ MentraLiveDiscoveryPolicy.isPartialAdvertisement(
399
+ secureCapable: false, lastSecureTrailerAtMs: t0, nowMs: t0 + MentraLiveDiscoveryPolicy.secureTrailerMemoryMs
400
+ )
401
+ )
402
+ XCTAssertFalse(
403
+ MentraLiveDiscoveryPolicy.isPartialAdvertisement(secureCapable: false, lastSecureTrailerAtMs: nil, nowMs: t0),
404
+ "legacy firmware never carries the trailer and is still listed as pairable"
405
+ )
406
+ XCTAssertFalse(
407
+ MentraLiveDiscoveryPolicy.isPartialAdvertisement(secureCapable: true, lastSecureTrailerAtMs: t0, nowMs: t0 + 500)
408
+ )
409
+ XCTAssertFalse(
410
+ MentraLiveDiscoveryPolicy.isPartialAdvertisement(secureCapable: true, lastSecureTrailerAtMs: nil, nowMs: t0)
411
+ )
412
+ }
413
+
414
+ func testTheSecureTrailerMemoryIsTenSeconds() {
415
+ XCTAssertEqual(MentraLiveDiscoveryPolicy.secureTrailerMemoryMs, 10_000)
416
+ }
417
+
418
+ func testARolledBackUnitListsAsLegacyOnceTheShortMemoryExpires() {
419
+ var memory = MentraLiveSecureTrailerMemory()
420
+ let unit = UUID()
421
+ let t0 = 5_000_000
422
+ memory.recordSecureTrailer(unit, nowMs: t0)
423
+ XCTAssertTrue(memory.isPartialAdvertisement(unit, secureCapable: false, nowMs: t0 + 2_000))
424
+ // Rolled back to legacy firmware: no trailer any more. After the memory it is legacy.
425
+ XCTAssertFalse(memory.isPartialAdvertisement(unit, secureCapable: false, nowMs: t0 + 10_001))
426
+ XCTAssertFalse(memory.hasRecentSecureTrailer(unit, nowMs: t0 + 10_001))
427
+ }
428
+
429
+ func testASameNamedLegacyUnitIsNotHidden() {
430
+ var memory = MentraLiveSecureTrailerMemory()
431
+ let secureUnit = UUID()
432
+ let legacyUnitWithTheSameName = UUID()
433
+ let t0 = 5_000_000
434
+ memory.recordSecureTrailer(secureUnit, nowMs: t0)
435
+ XCTAssertTrue(memory.isPartialAdvertisement(secureUnit, secureCapable: false, nowMs: t0 + 100))
436
+ XCTAssertFalse(memory.isPartialAdvertisement(legacyUnitWithTheSameName, secureCapable: false, nowMs: t0 + 100))
437
+ }
438
+
439
+ func testForgetClearsTheSecureTrailerMemory() {
440
+ var memory = MentraLiveSecureTrailerMemory()
441
+ let unit = UUID()
442
+ memory.recordSecureTrailer(unit, nowMs: 1_000)
443
+ memory.clear()
444
+ XCTAssertFalse(memory.isPartialAdvertisement(unit, secureCapable: false, nowMs: 1_100))
445
+ }
446
+
447
+ func testSecurePairingTargetFollowsOnlyTheSelectedRow() {
448
+ typealias Policy = MentraLiveConnectionAttemptPolicy
449
+ XCTAssertTrue(Policy.isSecurePairingTarget(selectedCapability: true, securePairingInfoThisSession: false))
450
+ XCTAssertFalse(
451
+ Policy.isSecurePairingTarget(selectedCapability: false, securePairingInfoThisSession: true),
452
+ "a row listed as legacy stays legacy"
453
+ )
454
+ XCTAssertFalse(Policy.isSecurePairingTarget(selectedCapability: nil, securePairingInfoThisSession: false))
455
+ XCTAssertTrue(
456
+ Policy.isSecurePairingTarget(selectedCapability: nil, securePairingInfoThisSession: true),
457
+ "an unknown row follows this session's secure pairing_info"
458
+ )
459
+ }
460
+
461
+ func testGlassesReadyReadinessForEachTargetCapability() {
462
+ typealias Policy = MentraLiveConnectionAttemptPolicy
463
+ func readiness(
464
+ explicit: Bool = true, row: Bool?, securePairingInfo: Bool = false, confirmed: Bool = false
465
+ ) -> Policy.Readiness {
466
+ Policy.readinessAtGlassesReady(
467
+ explicitPairing: explicit,
468
+ selectedCapability: row,
469
+ securePairingInfoThisSession: securePairingInfo,
470
+ securePairingConfirmed: confirmed
471
+ )
472
+ }
473
+ // Automatic reconnect never waits.
474
+ XCTAssertEqual(readiness(explicit: false, row: nil), .ready)
475
+ XCTAssertEqual(readiness(explicit: false, row: true), .ready)
476
+ // Known rows.
477
+ XCTAssertEqual(readiness(row: false), .ready)
478
+ XCTAssertEqual(readiness(row: true), .awaitOwnerCommit)
479
+ XCTAssertEqual(readiness(row: true, confirmed: true), .ready)
480
+ // Unknown row: wait (bounded) for pairing_info unless one already decided it.
481
+ XCTAssertEqual(readiness(row: nil), .awaitPairingInfo)
482
+ XCTAssertEqual(readiness(row: nil, securePairingInfo: true), .awaitOwnerCommit)
483
+ XCTAssertEqual(readiness(row: nil, securePairingInfo: true, confirmed: true), .ready)
484
+ XCTAssertEqual(Policy.unknownCapabilityPairingInfoWaitMs, 5_000)
485
+ XCTAssertEqual(Policy.secureFirmwareRefreshMs, 15_000)
486
+ }
160
487
  }