From e4ffa5fd92bb3f1439e397f751f535aa923c9967 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 12 Jul 2026 15:58:00 +0200 Subject: [PATCH] fix(watch): decode semantic approval outcomes --- .../Logic/WatchChatStatusLocalizationTests.swift | 4 +++- .../WatchApprovalTransportSourceGuardTests.swift | 12 ++++++++++++ .../WatchApp/Sources/WatchConnectivityReceiver.swift | 2 ++ apps/ios/WatchApp/Sources/WatchInboxMessages.swift | 5 +++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/ios/Tests/Logic/WatchChatStatusLocalizationTests.swift b/apps/ios/Tests/Logic/WatchChatStatusLocalizationTests.swift index 6e7309f36626..bd85c6aa43d3 100644 --- a/apps/ios/Tests/Logic/WatchChatStatusLocalizationTests.swift +++ b/apps/ios/Tests/Logic/WatchChatStatusLocalizationTests.swift @@ -129,8 +129,9 @@ struct WatchChatStatusLocalizationTests { } @Test func `semantic approval outcomes win before legacy decision fallbacks`() { + let parsedOutcome = WatchExecApprovalResolvedMessage.parseTransportOutcome("allowedAlways") let allowedAlways = WatchExecApprovalOutcome.resolved( - outcome: .allowedAlways, + outcome: parsedOutcome, legacyText: "Approval denied.", decision: .deny, source: "another-reviewer") @@ -148,6 +149,7 @@ struct WatchChatStatusLocalizationTests { #expect(allowedAlways.code == .allowedAlways) #expect(deniedElsewhere.code == .denied) #expect(unknownElsewhere.code == .resolvedElsewhere) + #expect(WatchExecApprovalResolvedMessage.parseTransportOutcome("future") == nil) } @Test func `gateway presentation localizes key and keeps backend override verbatim`() { diff --git a/apps/ios/Tests/WatchApprovalTransportSourceGuardTests.swift b/apps/ios/Tests/WatchApprovalTransportSourceGuardTests.swift index a73d4b996b07..5089ac98c8fa 100644 --- a/apps/ios/Tests/WatchApprovalTransportSourceGuardTests.swift +++ b/apps/ios/Tests/WatchApprovalTransportSourceGuardTests.swift @@ -157,6 +157,18 @@ struct WatchApprovalTransportSourceGuardTests { #expect(!parser.contains("?? []")) } + @Test func `watch preserves semantic approval outcomes through receiver parsing`() throws { + let receiverSource = try Self.readWatchSource("WatchConnectivityReceiver.swift") + let parser = try Self.extract( + receiverSource, + from: "private static func parseExecApprovalResolvedPayload(", + to: "private static func parseExecApprovalExpiredPayload(") + + #expect(parser.contains( + "WatchExecApprovalResolvedMessage.parseTransportOutcome(payload[\"outcome\"])")) + #expect(parser.contains("outcome: outcome")) + } + @Test func `watch reuses exact compound identifier policy`() throws { let receiverSource = try Self.readWatchSource("WatchConnectivityReceiver.swift") let storeSource = try Self.readWatchSource("WatchInboxStore.swift") diff --git a/apps/ios/WatchApp/Sources/WatchConnectivityReceiver.swift b/apps/ios/WatchApp/Sources/WatchConnectivityReceiver.swift index f3458595ec1a..04069b0e3bfe 100644 --- a/apps/ios/WatchApp/Sources/WatchConnectivityReceiver.swift +++ b/apps/ios/WatchApp/Sources/WatchConnectivityReceiver.swift @@ -470,6 +470,7 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable { } guard let approvalId = WatchApprovalID.exact(payload["approvalId"] as? String) else { return nil } let decision = Self.parseExecApprovalDecision(payload["decision"]) + let outcome = WatchExecApprovalResolvedMessage.parseTransportOutcome(payload["outcome"]) let gatewayStableID = WatchGatewayID.exact(payload["gatewayStableID"] as? String) let resolvedAtMs = (payload["resolvedAtMs"] as? NSNumber)?.int64Value let source = (payload["source"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) @@ -479,6 +480,7 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable { approvalId: approvalId, gatewayStableID: gatewayStableID, decision: decision, + outcome: outcome, resolvedAtMs: resolvedAtMs, source: source, outcomeText: outcomeText?.isEmpty == false ? outcomeText : nil) diff --git a/apps/ios/WatchApp/Sources/WatchInboxMessages.swift b/apps/ios/WatchApp/Sources/WatchInboxMessages.swift index ffe30383aa5b..b2d5f6e61e67 100644 --- a/apps/ios/WatchApp/Sources/WatchInboxMessages.swift +++ b/apps/ios/WatchApp/Sources/WatchInboxMessages.swift @@ -33,6 +33,11 @@ struct WatchExecApprovalResolvedMessage: Codable, Equatable { var resolvedAtMs: Int64? var source: String? var outcomeText: String? + + static func parseTransportOutcome(_ value: Any?) -> WatchExecApprovalTransportOutcome? { + guard let rawValue = value as? String else { return nil } + return WatchExecApprovalTransportOutcome(rawValue: rawValue) + } } struct WatchExecApprovalExpiredMessage: Codable, Equatable {