Skip to content

Commit 2a02ab6

Browse files
authored
refactor(apps): reuse Watch approval transport models (#105024)
* refactor(apps): reuse watch approval transport models * fix(ios): satisfy exact identifier lint
1 parent 0086763 commit 2a02ab6

5 files changed

Lines changed: 74 additions & 293 deletions

File tree

apps/ios/Sources/Services/ExactOpaqueIdentifier.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ struct ExactOpaqueIdentifierKey: Hashable, Sendable {
1616
func hash(into hasher: inout Hasher) {
1717
hasher.combine(self.bytes)
1818
}
19+
20+
var notificationComponent: String {
21+
let hexDigits = Array("0123456789ABCDEF".utf8)
22+
// Keep dots encoded so gateway and approval components cannot collapse into
23+
// the same notification identifier when their separator moves.
24+
let encoded = self.bytes.flatMap { byte -> [UInt8] in
25+
switch byte {
26+
case 0x30...0x39, 0x41...0x5A, 0x61...0x7A, 0x2D, 0x5F, 0x7E:
27+
[byte]
28+
default:
29+
[0x25, hexDigits[Int(byte >> 4)], hexDigits[Int(byte & 0x0F)]]
30+
}
31+
}
32+
return String(encoded.map { Character(UnicodeScalar($0)) })
33+
}
1934
}
2035

2136
enum ExactOpaqueIdentifier {

apps/ios/Tests/WatchApprovalTransportSourceGuardTests.swift

Lines changed: 33 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
import Foundation
22
import Testing
3-
4-
private struct TestSnapshotCorrelation: Hashable {
5-
let requestID: [UInt8]
6-
let gatewayID: [UInt8]
7-
8-
init(requestID: String, gatewayID: String) {
9-
self.requestID = Array(requestID.utf8)
10-
self.gatewayID = Array(gatewayID.utf8)
11-
}
12-
}
3+
@testable import OpenClaw
134

145
struct WatchApprovalTransportSourceGuardTests {
156
@Test func `watch approval loading and screenshot proof are visible`() throws {
@@ -122,17 +113,6 @@ struct WatchApprovalTransportSourceGuardTests {
122113
"WatchGatewayID.key(snapshot.gatewayStableID) == WatchGatewayID.key(token.gatewayStableID)"))
123114
}
124115

125-
@Test func `lost requested snapshot ignores unrelated accepted snapshots`() {
126-
let requested = TestSnapshotCorrelation(requestID: "request-a", gatewayID: "gateway-a")
127-
let unrelatedRequest = TestSnapshotCorrelation(requestID: "request-b", gatewayID: "gateway-a")
128-
let unrelatedOwner = TestSnapshotCorrelation(requestID: "request-a", gatewayID: "gateway-b")
129-
var accepted: Set<TestSnapshotCorrelation> = [unrelatedRequest, unrelatedOwner]
130-
131-
#expect(accepted.remove(requested) == nil)
132-
accepted.insert(requested)
133-
#expect(accepted.remove(requested) == requested)
134-
}
135-
136116
@Test func `watch applies retry reset only to its exact active attempt`() throws {
137117
let storeSource = try Self.readWatchSource("WatchInboxStore.swift")
138118
let promptConsume = try Self.extract(
@@ -176,10 +156,11 @@ struct WatchApprovalTransportSourceGuardTests {
176156
#expect(!parser.contains("?? []"))
177157
}
178158

179-
@Test func `watch approval ids remain exact opaque values`() throws {
159+
@Test func `watch reuses exact compound identifier policy`() throws {
180160
let receiverSource = try Self.readWatchSource("WatchConnectivityReceiver.swift")
181161
let storeSource = try Self.readWatchSource("WatchInboxStore.swift")
182162
let messagesSource = try Self.readWatchSource("WatchInboxMessages.swift")
163+
let viewSource = try Self.readWatchSource("WatchInboxView.swift")
183164
let parser = try Self.extract(
184165
receiverSource,
185166
from: "private static func parseExecApprovalItem(",
@@ -196,20 +177,13 @@ struct WatchApprovalTransportSourceGuardTests {
196177
storeSource,
197178
from: "private func restorePersistedState()",
198179
to: "private func persistState()")
199-
// The identity validators live in WatchInboxMessages.swift since the
200-
// message/model types were split out of WatchInboxStore.swift.
201-
let approvalValidator = try Self.extract(
202-
messagesSource,
203-
from: "enum WatchApprovalID {",
204-
to: "enum WatchGatewayID {")
205-
let gatewayValidator = try Self.extract(
206-
messagesSource,
207-
from: "enum WatchGatewayID {",
208-
to: "struct WatchExecApprovalIdentityKey:")
209-
210180
let prefixed = "\u{001C}approval"
211-
#expect(prefixed != "approval")
212-
#expect(Array(prefixed.utf8) != Array("approval".utf8))
181+
#expect(ExecApprovalIdentifier.exact(prefixed) == prefixed)
182+
#expect(ExecApprovalIdentifier.exact(" approval ") == " approval ")
183+
#expect(ExecApprovalIdentifier.exact(".") == nil)
184+
#expect(GatewayStableIdentifier.exact(" gateway ") == " gateway ")
185+
#expect(messagesSource.contains("typealias WatchApprovalID = ExecApprovalIdentifier"))
186+
#expect(messagesSource.contains("typealias WatchGatewayID = GatewayStableIdentifier"))
213187
#expect(parser.contains("WatchApprovalID.exact(payload[\"id\"] as? String)"))
214188
#expect(!parser.contains("id = (payload[\"id\"] as? String)?.trimmingCharacters"))
215189
#expect(ownerKey.contains("WatchApprovalID.key(approvalId)"))
@@ -218,26 +192,10 @@ struct WatchApprovalTransportSourceGuardTests {
218192
#expect(snapshotConsume.contains("let hasCanonicalRequestCorrelation ="))
219193
#expect(snapshotConsume.contains("guard hasCanonicalRequestCorrelation else { return true }"))
220194
#expect(restore.contains("WatchApprovalID.exact(record.approvalID) != nil"))
221-
#expect(approvalValidator.contains("!value.isEmpty"))
222-
#expect(approvalValidator.contains("value != \".\","))
223-
#expect(approvalValidator.contains("value != \"..\""))
224-
#expect(!approvalValidator.contains("trimmingCharacters"))
225-
#expect(!approvalValidator.contains("isECMAScriptTrimScalar"))
226-
#expect(gatewayValidator.contains("guard let value, !value.isEmpty"))
227-
#expect(!gatewayValidator.contains("WatchApprovalID.exact"))
228-
#expect(!gatewayValidator.contains("value != \".\""))
229-
#expect(!gatewayValidator.contains("trimmingCharacters"))
230-
#expect(Array(" approval ".utf8) != Array("approval".utf8))
231-
}
232-
233-
@Test func `watch canonical-equivalent approval IDs remain independently targetable`() throws {
234-
let storeSource = try Self.readWatchSource("WatchInboxStore.swift")
235-
let messagesSource = try Self.readWatchSource("WatchInboxMessages.swift")
236-
let viewSource = try Self.readWatchSource("WatchInboxView.swift")
237195
let composedID = "approval-\u{00E9}"
238196
let decomposedID = "approval-e\u{0301}"
239-
let composedKey = Data(composedID.utf8)
240-
let decomposedKey = Data(decomposedID.utf8)
197+
let composedKey = ExactOpaqueIdentifierKey(composedID)
198+
let decomposedKey = ExactOpaqueIdentifierKey(decomposedID)
241199
#expect(composedID == decomposedID)
242200
#expect(composedKey != decomposedKey)
243201

@@ -246,16 +204,19 @@ struct WatchApprovalTransportSourceGuardTests {
246204
let remainingID = try #require(pending[decomposedKey])
247205
#expect(Array(remainingID.utf8) == Array(decomposedID.utf8))
248206

249-
// Byte-exact identity types live in WatchInboxMessages.swift after the split.
250-
#expect(messagesSource.contains("self.bytes = Array(rawValue.utf8)"))
207+
#expect(messagesSource.contains("typealias WatchOpaqueUTF8Key = ExactOpaqueIdentifierKey"))
251208
#expect(messagesSource.contains("var id: WatchExecApprovalIdentityKey"))
252209
#expect(messagesSource.contains("var approvalID: WatchApprovalID.Key"))
253210
#expect(messagesSource.contains("var gatewayID: WatchGatewayID.Key"))
254211
#expect(storeSource.contains("WatchApprovalID.key(tombstone.approvalId) == key.approvalID"))
212+
#expect(storeSource.contains("WatchGatewayID.key(tombstone.gatewayStableID) == key.gatewayID"))
255213
#expect(storeSource.contains("approvalKey.notificationComponent"))
214+
#expect(storeSource.contains("gatewayKey.notificationComponent"))
256215
#expect(!storeSource.contains("record.id == approval.id"))
257216
#expect(!messagesSource.contains("record.id == approval.id"))
258217
#expect(!storeSource.contains("tombstone.approvalId == key.approvalId"))
218+
#expect(receiverSource.contains("WatchGatewayID.exact(payload[\"gatewayStableID\"] as? String)"))
219+
#expect(!receiverSource.contains("gatewayStableID?.trimmingCharacters"))
259220
#expect(viewSource.contains("record.approvalID"))
260221
#expect(viewSource.contains("$0.id == self.record.id"))
261222
}
@@ -316,32 +277,10 @@ struct WatchApprovalTransportSourceGuardTests {
316277
#expect(detailScroll.contains("self.content"))
317278
}
318279

319-
@Test func `watch compounds exact owner and approval identity`() throws {
320-
let storeSource = try Self.readWatchSource("WatchInboxStore.swift")
321-
let messagesSource = try Self.readWatchSource("WatchInboxMessages.swift")
322-
let receiverSource = try Self.readWatchSource("WatchConnectivityReceiver.swift")
323-
let sameApprovalID = Data("approval-same".utf8)
324-
let composedOwner = Data("gateway-\u{00E9}".utf8)
325-
let decomposedOwner = Data("gateway-e\u{0301}".utf8)
326-
#expect(composedOwner != decomposedOwner)
327-
#expect(Set([[composedOwner, sameApprovalID], [decomposedOwner, sameApprovalID]]).count == 2)
328-
329-
// The compound identity key type lives in WatchInboxMessages.swift after the split.
330-
#expect(messagesSource.contains("struct WatchExecApprovalIdentityKey: Hashable"))
331-
#expect(storeSource.contains("selectedExecApprovalGatewayStableID"))
332-
#expect(storeSource.contains("gatewayKey.notificationComponent"))
333-
#expect(storeSource.contains("WatchGatewayID.key(tombstone.gatewayStableID) == key.gatewayID"))
334-
#expect(storeSource.contains("\"watch.execApproval.\\(record.approvalID)\""))
335-
#expect(receiverSource.contains("WatchGatewayID.exact(payload[\"gatewayStableID\"] as? String)"))
336-
#expect(!receiverSource.contains("gatewayStableID?.trimmingCharacters"))
337-
#expect(!storeSource.contains("isECMAScriptTrimScalar"))
338-
#expect(!messagesSource.contains("isECMAScriptTrimScalar"))
339-
#expect(Array("\u{0085}gateway".utf8) != Array("gateway".utf8))
340-
}
341-
342280
@Test func `watch notification identity frames dotted components`() throws {
343281
let storeSource = try Self.readWatchSource("WatchInboxStore.swift")
344282
let messagesSource = try Self.readWatchSource("WatchInboxMessages.swift")
283+
let identifierSource = try Self.readIOSServiceSource("ExactOpaqueIdentifier.swift")
345284
let promptConsume = try Self.extract(
346285
storeSource,
347286
from: "func consume(\n execApprovalPrompt",
@@ -350,12 +289,14 @@ struct WatchApprovalTransportSourceGuardTests {
350289
let rawRight = "a" + "." + "b.c"
351290
#expect(rawLeft == rawRight)
352291

353-
let framedLeft = Self.notificationComponent("a.b") + "." + Self.notificationComponent("c")
354-
let framedRight = Self.notificationComponent("a") + "." + Self.notificationComponent("b.c")
292+
let framedLeft = ExactOpaqueIdentifierKey("a.b").notificationComponent + "."
293+
+ ExactOpaqueIdentifierKey("c").notificationComponent
294+
let framedRight = ExactOpaqueIdentifierKey("a").notificationComponent + "."
295+
+ ExactOpaqueIdentifierKey("b.c").notificationComponent
355296
#expect(framedLeft != framedRight)
356-
// The notification-component percent encoder lives in WatchInboxMessages.swift.
357-
#expect(messagesSource.contains("0x2D, 0x5F, 0x7E"))
358-
#expect(!messagesSource.contains("0x2D, 0x2E, 0x5F, 0x7E"))
297+
#expect(messagesSource.contains("typealias WatchOpaqueUTF8Key = ExactOpaqueIdentifierKey"))
298+
#expect(identifierSource.contains("0x2D, 0x5F, 0x7E"))
299+
#expect(!identifierSource.contains("0x2D, 0x2E, 0x5F, 0x7E"))
359300
#expect(!storeSource.contains("0x2D, 0x2E, 0x5F, 0x7E"))
360301
#expect(storeSource.contains("gatewayKey.notificationComponent).\\(approvalKey.notificationComponent"))
361302
#expect(storeSource.contains("legacyExecApprovalNotificationIdentifier"))
@@ -412,22 +353,6 @@ struct WatchApprovalTransportSourceGuardTests {
412353
"Self.gatewayIDsMatch(approval.gatewayStableID, snapshotGatewayID)"))
413354
}
414355

415-
private static func notificationComponent(_ rawValue: String) -> String {
416-
let hexDigits = Array("0123456789ABCDEF".utf8)
417-
var encoded: [UInt8] = []
418-
for byte in rawValue.utf8 {
419-
switch byte {
420-
case 0x30...0x39, 0x41...0x5A, 0x61...0x7A, 0x2D, 0x5F, 0x7E:
421-
encoded.append(byte)
422-
default:
423-
encoded.append(0x25)
424-
encoded.append(hexDigits[Int(byte >> 4)])
425-
encoded.append(hexDigits[Int(byte & 0x0F)])
426-
}
427-
}
428-
return String(decoding: encoded, as: UTF8.self)
429-
}
430-
431356
private static func readWatchSource(_ filename: String) throws -> String {
432357
let url = URL(fileURLWithPath: #filePath)
433358
.deletingLastPathComponent()
@@ -437,6 +362,15 @@ struct WatchApprovalTransportSourceGuardTests {
437362
return try String(contentsOf: url, encoding: .utf8)
438363
}
439364

365+
private static func readIOSServiceSource(_ filename: String) throws -> String {
366+
let url = URL(fileURLWithPath: #filePath)
367+
.deletingLastPathComponent()
368+
.deletingLastPathComponent()
369+
.appendingPathComponent("Sources/Services")
370+
.appendingPathComponent(filename)
371+
return try String(contentsOf: url, encoding: .utf8)
372+
}
373+
440374
private static func extract(_ source: String, from start: String, to end: String) throws -> String {
441375
let startRange = try #require(source.range(of: start))
442376
let tail = source[startRange.lowerBound...]

0 commit comments

Comments
 (0)