@mentra/bluetooth-sdk 3.4.0-dev.977 → 3.4.0-dev.979

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.
@@ -0,0 +1,348 @@
1
+ import Foundation
2
+ @testable import MentraBluetoothSDK
3
+ import XCTest
4
+
5
+ final class NimoResourceSessionTests: XCTestCase {
6
+ private let uuid = "00000000-0000-4000-8000-000000000001"
7
+ private final class Clock { var value: UInt64 = 100; func next() -> UInt64 { value += 1; return value } }
8
+ private func fixture() -> (NimoResourceSession, NSObject, NSObject, Clock) {
9
+ let clock = Clock(), owner = NSObject(), peer = NSObject()
10
+ let model = NimoResourceSession(captureNonce: uuid, pair: NimoResourcePair(name: "NIMO test", peripheralUuid: uuid), now: clock.next)
11
+ XCTAssertTrue(model.admit(owner, instanceId: uuid, name: "NIMO test"))
12
+ return (model, owner, peer, clock)
13
+ }
14
+ @MainActor
15
+ func testActualNimoReleaseRetiresPendingTimeAckWithoutRadioOrFalseHandshakeFailure() async {
16
+ for cancel in [false, true] {
17
+ for timeoutBeforeEntry in [false, true] {
18
+ let result = await Nimo().packageExercisePendingTimeAck(cancel: cancel, timeoutBeforeEntry: timeoutBeforeEntry)
19
+ XCTAssertTrue(result.errors.isEmpty)
20
+ XCTAssertEqual(result.pendingAcks, 0)
21
+ XCTAssertTrue(result.idle)
22
+ XCTAssertFalse(result.ready)
23
+ XCTAssertTrue(result.sameWriteEvidence)
24
+ XCTAssertTrue(result.ackStillAbsent)
25
+ XCTAssertTrue(result.noRadio)
26
+ XCTAssertFalse(result.physicalClosure)
27
+ }
28
+ }
29
+ }
30
+ @MainActor
31
+ func testActualNimoActiveTimeAckTimeoutRemainsFailure() async {
32
+ let result = await Nimo().packageExercisePendingTimeAck(cancel: true, timeoutBeforeEntry: false, activeTimeout: true)
33
+ XCTAssertTrue(result.errors.contains("handshake-failed"))
34
+ XCTAssertFalse(result.ready)
35
+ XCTAssertTrue(result.noRadio)
36
+ }
37
+ func testReleaseFenceDoesNotTurnHandshakeAbandonmentIntoCommandOrWriterProof() {
38
+ let (model, owner, peer, _) = fixture()
39
+ XCTAssertTrue(model.beginTransport(owner, peripheral: peer, uuid: uuid))
40
+ model.transportConnected(owner, peer); model.observeTws(owner, connected: true)
41
+ let command = model.beginTimeCommand(owner)!
42
+ model.timeWriteStarted(owner, command: command)
43
+ let started = model.timeWriteStartedElapsedMs
44
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: true, mic: true, canvas: true))
45
+ XCTAssertFalse(model.recordHandshakeFailure(owner))
46
+ model.timeWriteCompleted(owner, command: command); model.timeAck(owner, command: command, status: 0)
47
+ XCTAssertEqual(model.timeWriteStartedElapsedMs, started)
48
+ XCTAssertNil(model.timeWriteCompletedElapsedMs)
49
+ XCTAssertNil(model.timeAckElapsedMs)
50
+ XCTAssertFalse(model.ready)
51
+ XCTAssertFalse(model.commandsSettled)
52
+ var producers = NimoResourceProducers(); producers.writerQueued = 1
53
+ XCTAssertFalse(model.settled(producers: producers))
54
+ XCTAssertTrue(model.errors.isEmpty)
55
+ }
56
+ func testForeignManagerCannotReplaceCaptureUntilOriginalClosure() {
57
+ XCTAssertTrue(NimoResourceManagerState.allowsReplacement(fixtureEnabled: false, originalClosed: false))
58
+ XCTAssertFalse(NimoResourceManagerState.allowsReplacement(fixtureEnabled: true, originalClosed: false))
59
+ XCTAssertTrue(NimoResourceManagerState.allowsReplacement(fixtureEnabled: true, originalClosed: true))
60
+ }
61
+ func testFailedOriginalConnectionCanReleaseWithoutInventingDisconnect() {
62
+ let (model, owner, peer, clock) = fixture()
63
+ XCTAssertTrue(model.beginTransport(owner, peripheral: peer, uuid: uuid))
64
+ model.connectionFailed(owner, NSObject(), receivedElapsedMs: clock.next())
65
+ model.connectionFailed(owner, peer, receivedElapsedMs: model.startedElapsedMs! - 1)
66
+ XCTAssertNil(model.connectionFailedElapsedMs)
67
+ model.connectionFailed(owner, peer, receivedElapsedMs: clock.next())
68
+ XCTAssertNotNil(model.connectionFailedElapsedMs)
69
+ XCTAssertFalse(model.ready)
70
+ XCTAssertFalse(model.settled(producers: NimoResourceProducers()))
71
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: true, mic: false, canvas: false))
72
+ XCTAssertTrue(model.settled(producers: NimoResourceProducers()))
73
+ XCTAssertNil(model.disconnectCallbackElapsedMs)
74
+ XCTAssertFalse(model.noTransport)
75
+ XCTAssertNotNil(model.original)
76
+ var producers = NimoResourceProducers(); producers.postedCallbacks = 1
77
+ XCTAssertFalse(model.settled(producers: producers))
78
+ model.transportConnected(owner, peer) // A contradictory late callback cannot preserve closure.
79
+ XCTAssertFalse(model.settled(producers: NimoResourceProducers()))
80
+ XCTAssertTrue(model.errors.contains("connect-after-failed-attempt"))
81
+ let (connected, other, otherPeer, otherClock) = fixture()
82
+ XCTAssertTrue(connected.beginTransport(other, peripheral: otherPeer, uuid: uuid))
83
+ connected.transportConnected(other, otherPeer)
84
+ connected.connectionFailed(other, otherPeer, receivedElapsedMs: otherClock.next())
85
+ XCTAssertNil(connected.connectionFailedElapsedMs)
86
+ XCTAssertTrue(connected.beginRelease(nonce: uuid, cancel: true, mic: false, canvas: false))
87
+ XCTAssertFalse(connected.settled(producers: NimoResourceProducers()))
88
+ let (fenced, fencedOwner, fencedPeer, fencedClock) = fixture()
89
+ XCTAssertTrue(fenced.beginTransport(fencedOwner, peripheral: fencedPeer, uuid: uuid))
90
+ XCTAssertTrue(fenced.beginRelease(nonce: uuid, cancel: true, mic: false, canvas: false))
91
+ fenced.transportConnected(fencedOwner, fencedPeer)
92
+ XCTAssertTrue(fenced.connected)
93
+ fenced.connectionFailed(fencedOwner, fencedPeer, receivedElapsedMs: fencedClock.next())
94
+ XCTAssertNil(fenced.connectionFailedElapsedMs)
95
+ XCTAssertFalse(fenced.settled(producers: NimoResourceProducers()))
96
+ }
97
+ func testOrdinaryManagerCleanupPreservesVerifiedOriginalClosureButNotActiveCapture() {
98
+ let (model, owner, _, _) = fixture()
99
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: true, mic: false, canvas: false))
100
+ model.proveNoTransport()
101
+ let accepted = model.settled(producers: NimoResourceProducers())
102
+ XCTAssertTrue(accepted)
103
+ XCTAssertFalse(model.shouldRunOrdinaryDisconnect(owner, originalClosed: accepted))
104
+ XCTAssertFalse(model.shouldRunOrdinaryDisconnect(owner, originalClosed: accepted))
105
+ XCTAssertTrue(model.errors.isEmpty)
106
+ XCTAssertTrue(model.settled(producers: NimoResourceProducers())) // Fresh status, not old bytes.
107
+ XCTAssertTrue(NimoResourceManagerState.allowsReplacement(fixtureEnabled: true, originalClosed: model.settled(producers: NimoResourceProducers())))
108
+ let (active, other, _, _) = fixture()
109
+ XCTAssertTrue(active.shouldRunOrdinaryDisconnect(other, originalClosed: false))
110
+ XCTAssertTrue(active.errors.contains("ordinary-disconnect-during-capture"))
111
+ XCTAssertFalse(active.settled(producers: NimoResourceProducers()))
112
+ let (failed, third, peer, clock) = fixture()
113
+ XCTAssertTrue(failed.beginTransport(third, peripheral: peer, uuid: uuid))
114
+ failed.connectionFailed(third, peer, receivedElapsedMs: clock.next())
115
+ XCTAssertTrue(failed.beginRelease(nonce: uuid, cancel: true, mic: false, canvas: false))
116
+ XCTAssertTrue(failed.settled(producers: NimoResourceProducers()))
117
+ failed.transportConnected(third, peer) // A late contradictory callback still invalidates closure.
118
+ XCTAssertTrue(failed.shouldRunOrdinaryDisconnect(third, originalClosed: true))
119
+ XCTAssertFalse(failed.settled(producers: NimoResourceProducers()))
120
+ }
121
+ func testManagerEntryRosterScopesOriginalNimoWithoutIgnoringItsCallbacks() {
122
+ let entries = NimoResourceManagerEntries(), foreign = NimoResourceManagerEntries()
123
+ let (model, owner, _, _) = fixture()
124
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: true, mic: false, canvas: false)); model.proveNoTransport()
125
+ let nimo = entries.begin("NIMO")
126
+ var producers = NimoResourceProducers(); producers.postedCallbacks = entries.originalNimo
127
+ XCTAssertFalse(model.settled(producers: producers))
128
+ let g2 = entries.begin("G2")
129
+ XCTAssertEqual(entries.all, 2); XCTAssertEqual(entries.originalNimo, 1)
130
+ foreign.end(nimo); XCTAssertEqual(entries.originalNimo, 1)
131
+ entries.end(nimo); entries.end(nimo) // Exact one-shot ticket, never an early/duplicate decrement.
132
+ producers.postedCallbacks = entries.originalNimo
133
+ XCTAssertEqual(entries.all, 1); XCTAssertTrue(model.settled(producers: producers))
134
+ XCTAssertFalse(model.shouldRunOrdinaryDisconnect(owner, originalClosed: true))
135
+ entries.end(g2); XCTAssertEqual(entries.all, 0)
136
+ producers.postedCallbacks = 1 // A late actual original callback still holds closure.
137
+ XCTAssertFalse(model.settled(producers: producers))
138
+ }
139
+ func testOriginalClosureCannotClearReplacementManagerSharedState() {
140
+ let (model, owner, _, _) = fixture()
141
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: false, mic: false, canvas: false))
142
+ model.proveNoTransport()
143
+ XCTAssertTrue(model.settled(producers: NimoResourceProducers()))
144
+ let g2 = NSObject()
145
+ var connected = true
146
+ NimoResourceManagerState.publish(owner: owner, current: g2) { connected = false }
147
+ XCTAssertTrue(connected)
148
+ NimoResourceManagerState.publish(owner: owner, current: nil) { connected = false }
149
+ XCTAssertTrue(connected)
150
+ NimoResourceManagerState.publish(owner: owner, current: owner) { connected = false }
151
+ XCTAssertFalse(connected)
152
+ }
153
+ func testQueuedOldNotificationsKeepReceiveTimeAndCannotSatisfyNewCommands() {
154
+ let (model, owner, peer, clock) = fixture()
155
+ XCTAssertTrue(model.beginTransport(owner, peripheral: peer, uuid: uuid))
156
+ model.transportConnected(owner, peer)
157
+ model.observeTws(owner, connected: true)
158
+ let command = model.beginTimeCommand(owner)!
159
+ let staleAck = clock.value
160
+ model.timeWriteStarted(owner, command: command)
161
+ model.timeAck(owner, command: command, status: 0, receivedElapsedMs: staleAck)
162
+ XCTAssertNil(model.timeAckElapsedMs)
163
+ let actualAck = clock.next()
164
+ model.timeAck(owner, command: command, status: 0, receivedElapsedMs: actualAck)
165
+ XCTAssertFalse(model.ready) // Real ACK may precede didWrite callback.
166
+ model.timeWriteCompleted(owner, command: command)
167
+ XCTAssertTrue(model.ready)
168
+ XCTAssertEqual(model.timeAckElapsedMs, actualAck)
169
+ let staleStop = clock.value
170
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: false, mic: true, canvas: true))
171
+ model.micStopStarted(owner)
172
+ model.micStopMarker(owner, receivedElapsedMs: staleStop)
173
+ model.exitAcknowledged(owner, status: 0, receivedElapsedMs: staleStop)
174
+ XCTAssertNil(model.micStopMarkerElapsedMs)
175
+ XCTAssertNil(model.exitAckElapsedMs)
176
+ let actualStop = clock.next()
177
+ model.micStopMarker(owner, receivedElapsedMs: actualStop)
178
+ model.micStopWritten(owner)
179
+ XCTAssertEqual(model.micStopMarkerElapsedMs, actualStop)
180
+ }
181
+ func testArmAdmissionPinsOneObjectAndRejectsReplacementWrongNameAndWrongPeripheral() {
182
+ let (model, owner, peer, _) = fixture()
183
+ let admitted = model.admittedElapsedMs
184
+ XCTAssertFalse(model.admit(NSObject(), instanceId: uuid, name: "NIMO test"))
185
+ XCTAssertFalse(model.admit(owner, instanceId: uuid, name: "other"))
186
+ XCTAssertEqual(model.admittedElapsedMs, admitted)
187
+ XCTAssertFalse(model.beginTransport(owner, peripheral: peer, uuid: UUID().uuidString))
188
+ XCTAssertTrue(model.beginTransport(owner, peripheral: peer, uuid: uuid))
189
+ XCTAssertFalse(model.beginTransport(owner, peripheral: peer, uuid: uuid))
190
+ XCTAssertFalse(model.beginTransport(owner, peripheral: NSObject(), uuid: uuid))
191
+ }
192
+ func testReadinessRequiresOriginalConnectedTwsWrittenCommandAndStatusZeroAck() {
193
+ let (model, owner, peer, _) = fixture()
194
+ XCTAssertTrue(model.beginTransport(owner, peripheral: peer, uuid: uuid))
195
+ XCTAssertNil(model.beginTimeCommand(owner))
196
+ model.transportConnected(owner, peer)
197
+ model.observeTws(NSObject(), connected: true)
198
+ XCTAssertNil(model.beginTimeCommand(owner))
199
+ model.observeTws(owner, connected: true)
200
+ let command = model.beginTimeCommand(owner)!
201
+ model.timeAck(owner, command: command, status: 0) // Before actual write start.
202
+ XCTAssertNil(model.timeAckElapsedMs)
203
+ model.timeWriteStarted(owner, command: command)
204
+ model.timeAck(owner, command: command, status: 0) // Early ACK buffers until actual callback.
205
+ XCTAssertFalse(model.ready)
206
+ model.timeWriteCompleted(owner, command: "wrong")
207
+ XCTAssertFalse(model.ready)
208
+ model.timeWriteCompleted(owner, command: command)
209
+ XCTAssertTrue(model.ready)
210
+ let handshake = model.handshakeTwsElapsedMs
211
+ model.observeTws(owner, connected: true)
212
+ XCTAssertEqual(model.handshakeTwsElapsedMs, handshake)
213
+ XCTAssertGreaterThan(model.twsObservedElapsedMs!, model.timeAckElapsedMs!)
214
+ model.observeTws(owner, connected: false)
215
+ XCTAssertFalse(model.ready)
216
+ }
217
+ func testNegativeAckAndReplacementCallbacksNeverEstablishReadiness() {
218
+ let (model, owner, peer, _) = fixture()
219
+ XCTAssertTrue(model.beginTransport(owner, peripheral: peer, uuid: uuid))
220
+ model.transportConnected(owner, NSObject())
221
+ XCTAssertFalse(model.connected)
222
+ model.transportConnected(owner, peer); model.observeTws(owner, connected: true)
223
+ let command = model.beginTimeCommand(owner)!
224
+ model.timeWriteStarted(owner, command: command); model.timeWriteCompleted(owner, command: command)
225
+ model.timeAck(owner, command: command, status: 2)
226
+ XCTAssertFalse(model.ready)
227
+ }
228
+ func testReleaseRequiresRealOriginalCallbackEveryObligationAndActualProducerSettlement() {
229
+ let (model, owner, peer, _) = fixture()
230
+ XCTAssertTrue(model.beginTransport(owner, peripheral: peer, uuid: uuid)); model.transportConnected(owner, peer)
231
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: false, mic: true, canvas: true))
232
+ let start = model.releaseStartedElapsedMs
233
+ XCTAssertFalse(model.beginRelease(nonce: UUID().uuidString, cancel: false, mic: false, canvas: false))
234
+ XCTAssertEqual(model.releaseStartedElapsedMs, start)
235
+ XCTAssertFalse(model.allowsEntry(owner)); XCTAssertNil(model.beginTimeCommand(owner))
236
+ model.proveNoTransport(); XCTAssertFalse(model.noTransport)
237
+ model.micStopMarker(owner) // Before actual stop write: ignored.
238
+ XCTAssertNil(model.micStopMarkerElapsedMs)
239
+ model.micStopStarted(owner); model.micStopMarker(owner); model.micStopWritten(owner)
240
+ model.exitAcknowledged(owner, status: 7)
241
+ XCTAssertFalse(model.commandsSettled)
242
+ model.exitAcknowledged(owner, status: 0)
243
+ XCTAssertTrue(model.commandsSettled)
244
+ model.disconnectRequested(owner)
245
+ XCTAssertFalse(model.settled(producers: NimoResourceProducers())) // cancel return/nil app ref is not closure.
246
+ model.disconnected(owner, NSObject(), error: nil)
247
+ XCTAssertNil(model.disconnectCallbackElapsedMs)
248
+ model.disconnected(owner, peer, error: nil)
249
+ var producers = NimoResourceProducers(); producers.encoderDelivery = 1
250
+ XCTAssertFalse(model.settled(producers: producers))
251
+ producers.encoderDelivery = 0; producers.writerActive = 1
252
+ XCTAssertFalse(model.settled(producers: producers))
253
+ producers.writerActive = 0; producers.reconnectTask = 1
254
+ XCTAssertFalse(model.settled(producers: producers))
255
+ XCTAssertTrue(model.settled(producers: NimoResourceProducers()))
256
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: false, mic: false, canvas: false))
257
+ XCTAssertEqual(model.releaseStartedElapsedMs, start)
258
+ XCTAssertTrue(model.micStopRequired); XCTAssertTrue(model.displayExitRequired)
259
+ }
260
+ func testGenZeroCancellationFencesAdmissionAndProducesExplicitNoTransport() {
261
+ let (model, owner, _, _) = fixture()
262
+ XCTAssertEqual(model.sessionDictionary()?["sessionGeneration"] as? UInt64, 0)
263
+ XCTAssertTrue(model.sessionDictionary()?["startedElapsedMs"] is NSNull)
264
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: true, mic: false, canvas: false))
265
+ model.proveNoTransport()
266
+ XCTAssertTrue(model.settled(producers: NimoResourceProducers()))
267
+ XCTAssertFalse(model.admit(owner, instanceId: uuid, name: "NIMO test"))
268
+ XCTAssertFalse(model.admit(NSObject(), instanceId: uuid, name: "NIMO test"))
269
+ }
270
+ func testDisconnectErrorOrUnrequestedDisconnectNeverProvesIdle() {
271
+ for requested in [true, false] {
272
+ let (model, owner, peer, _) = fixture()
273
+ XCTAssertTrue(model.beginTransport(owner, peripheral: peer, uuid: uuid))
274
+ model.transportConnected(owner, peer)
275
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: false, mic: false, canvas: false))
276
+ if requested { model.disconnectRequested(owner) }
277
+ model.disconnected(owner, peer, error: "radio error")
278
+ XCTAssertFalse(model.settled(producers: NimoResourceProducers()))
279
+ }
280
+ }
281
+ func testReleaseDoesNotAdoptLateGenerationOrRewriteReadinessAndClockAnchors() {
282
+ let (model, owner, peer, _) = fixture()
283
+ XCTAssertTrue(model.beginTransport(owner, peripheral: peer, uuid: uuid)); model.transportConnected(owner, peer)
284
+ model.observeTws(owner, connected: true); let command = model.beginTimeCommand(owner)!
285
+ model.timeWriteStarted(owner, command: command); model.timeWriteCompleted(owner, command: command); model.timeAck(owner, command: command, status: 0)
286
+ let observed = model.twsObservedElapsedMs, ack = model.timeAckElapsedMs
287
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: true, mic: false, canvas: false))
288
+ model.observeTws(owner, connected: false); model.timeAck(owner, command: command, status: 7)
289
+ XCTAssertEqual(model.twsObservedElapsedMs, observed); XCTAssertEqual(model.timeAckElapsedMs, ack)
290
+ XCTAssertFalse(model.beginTransport(owner, peripheral: peer, uuid: uuid))
291
+ XCTAssertFalse(model.ready)
292
+ }
293
+
294
+ func testStrictTypedRequestRejectsPacketPathsUnknownFieldsAndMalformedOriginals() throws {
295
+ let pair: [String: Any] = ["name": "NIMO test", "peripheralUuid": uuid]
296
+ let process: [String: Any] = ["pid": 123, "instanceId": uuid]
297
+ let original: [String: Any] = ["instanceId": uuid, "peripheralUuid": uuid, "sessionGeneration": 1]
298
+ let valid: [String: Any] = ["version": 1, "operation": "arm", "requestNonce": uuid, "captureNonce": uuid,
299
+ "planSha256": String(repeating: "a", count: 64), "grantSha256": String(repeating: "b", count: 64),
300
+ "expectedProcess": process, "expectedPair": pair, "original": NSNull()]
301
+ func encoded(_ value: [String: Any]) throws -> Data { try JSONSerialization.data(withJSONObject: value, options: [.sortedKeys]) }
302
+ XCTAssertEqual(try NimoResourceRequest.decode(encoded(valid)).operation, .arm)
303
+ for replacement: [String: Any] in [
304
+ ["packet": "52010000"], ["path": "/tmp/other.json"], ["operation": "connect"],
305
+ ["requestNonce": "AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA"], ["version": 2], ["grantSha256": "unknown"],
306
+ ["expectedPair": pair.merging(["bluetoothAddress": "00:11:22:33:44:55"]) { _, new in new }],
307
+ ["expectedProcess": process.merging(["pid": true]) { _, new in new }],
308
+ ["expectedPair": pair.merging(["name": String(repeating: "x", count: 129)]) { _, new in new }],
309
+ ["original": original.merging(["sessionGeneration": 2]) { _, new in new }],
310
+ ["original": original.merging(["command": "reset"]) { _, new in new }],
311
+ ["original": original.merging(["instanceId": "replacement"]) { _, new in new }],
312
+ ] {
313
+ XCTAssertThrowsError(try NimoResourceRequest.decode(encoded(valid.merging(replacement) { _, new in new })))
314
+ }
315
+ XCTAssertThrowsError(try NimoResourceRequest.decode(Data(repeating: 32, count: 16_385)))
316
+ }
317
+
318
+
319
+ func testAdmissionPersistenceFailureRetainsOnlyCancelledOriginalAndNoTransportAuthority() {
320
+ let clock = Clock(), owner = NSObject(), peer = NSObject()
321
+ let model = NimoResourceSession(captureNonce: uuid, pair: NimoResourcePair(name: "NIMO test", peripheralUuid: uuid), now: clock.next)
322
+ XCTAssertFalse(model.admit(owner, instanceId: uuid, name: "NIMO test", persist: { _ in
323
+ XCTAssertNil(model.admittedElapsedMs)
324
+ return false
325
+ }))
326
+ XCTAssertTrue(model.isOwner(owner)); XCTAssertTrue(model.cancelled)
327
+ XCTAssertNil(model.admittedElapsedMs)
328
+ XCTAssertFalse(model.allowsEntry(owner))
329
+ XCTAssertFalse(model.beginTransport(owner, peripheral: peer, uuid: uuid))
330
+ XCTAssertFalse(model.admit(NSObject(), instanceId: uuid, name: "NIMO test"))
331
+ XCTAssertTrue(model.beginRelease(nonce: uuid, cancel: true, mic: false, canvas: false))
332
+ model.proveNoTransport()
333
+ XCTAssertFalse(model.settled(producers: NimoResourceProducers())) // Failed durable authority is not fabricated idle.
334
+ }
335
+
336
+ func testTransportPersistencePrecedesEffectAuthorityAndFailureCannotBeRetried() {
337
+ let (model, owner, peer, _) = fixture()
338
+ XCTAssertFalse(model.beginTransport(owner, peripheral: peer, uuid: uuid, persist: { proposed in
339
+ XCTAssertNil(model.original); XCTAssertNil(model.startedElapsedMs)
340
+ XCTAssertGreaterThan(proposed, model.admittedElapsedMs!)
341
+ return false
342
+ }))
343
+ XCTAssertNil(model.original); XCTAssertNil(model.startedElapsedMs)
344
+ XCTAssertTrue(model.cancelled)
345
+ XCTAssertFalse(model.beginTransport(owner, peripheral: peer, uuid: uuid))
346
+ }
347
+
348
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "3.4.0-dev.977",
3
+ "version": "3.4.0-dev.979",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -114,7 +114,7 @@
114
114
  "registry": "https://registry.npmjs.org/"
115
115
  },
116
116
  "dependencies": {
117
- "@mentra/cloud-protocol": "3.4.0-dev.977"
117
+ "@mentra/cloud-protocol": "3.4.0-dev.979"
118
118
  },
119
119
  "devDependencies": {
120
120
  "@types/node": "^25.9.3",
@@ -93,6 +93,9 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
93
93
 
94
94
  // Connection Commands
95
95
  requestStatus(): Promise<void>
96
+ /** Mac-only explicit fixture launch. Strict nimo-macos-resource-v1 Codable
97
+ * request/receipt JSON; ordinary launches refuse. No packet/path commands. */
98
+ nimoResourceSession?(requestJSON: string): Promise<string>
96
99
  connectDefault(options?: ConnectOptions): Promise<void>
97
100
  connectDefaultWithOptions(options: Required<ConnectOptions>): Promise<void>
98
101
  setDefaultDevice(device: Device | null): Promise<void>
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
12
12
  export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.4.0",
15
- "releaseIdentity": "3.4.0-dev.977",
16
- "releaseSetId": "mentra-3.4.0-dev.977",
17
- "sourceCommit": "349c05248d43d17d40a1e3009a26cdecc36109c9",
18
- "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.977.json",
19
- "otaManifestSha256": "4471fbb20c442e136ea3b8c672706254db44f93b17752354d3cd477915c5069f"
15
+ "releaseIdentity": "3.4.0-dev.979",
16
+ "releaseSetId": "mentra-3.4.0-dev.979",
17
+ "sourceCommit": "cec2dff0b77fc7963d63602bba43556c690daf9d",
18
+ "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.979.json",
19
+ "otaManifestSha256": "667fffda403d22a07c60a0029ef602c556d13ddfa4bf8161fda5f56f54f50f13"
20
20
  })