mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 23:01:38 +00:00
test(watchos): cover localized chat status fallback
This commit is contained in:
66
apps/ios/Tests/Logic/WatchChatStatusLocalizationTests.swift
Normal file
66
apps/ios/Tests/Logic/WatchChatStatusLocalizationTests.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
import Testing
|
||||
|
||||
struct WatchChatStatusLocalizationTests {
|
||||
@Test func `snapshot parser keeps known status codes`() throws {
|
||||
let snapshot = try #require(WatchAppSnapshotMessage.parsePayload(Self.payload(
|
||||
chatStatusCode: OpenClawWatchChatStatusCode.connectIPhone.rawValue,
|
||||
chatStatusText: "Legacy status")))
|
||||
|
||||
#expect(snapshot.chatStatusCode == .connectIPhone)
|
||||
#expect(snapshot.chatStatusText == "Legacy status")
|
||||
}
|
||||
|
||||
@Test func `snapshot parser preserves legacy fallback for unknown status codes`() throws {
|
||||
let snapshot = try #require(WatchAppSnapshotMessage.parsePayload(Self.payload(
|
||||
chatStatusCode: "futureStatus",
|
||||
chatStatusText: "Future status from iPhone")))
|
||||
|
||||
#expect(snapshot.chatStatusCode == nil)
|
||||
#expect(snapshot.chatStatusText == "Future status from iPhone")
|
||||
}
|
||||
|
||||
@Test func `watch renders known status locally before legacy text`() {
|
||||
let rendered = WatchAppSnapshotMessage.localizedChatStatusText(
|
||||
statusCode: .noMessages,
|
||||
legacyText: "English status from iPhone",
|
||||
chatCount: 0,
|
||||
hasAppSnapshot: true)
|
||||
|
||||
#expect(rendered == String(localized: "No chat messages yet"))
|
||||
}
|
||||
|
||||
@Test func `watch renders legacy text when status code is unknown`() throws {
|
||||
let snapshot = try #require(WatchAppSnapshotMessage.parsePayload(Self.payload(
|
||||
chatStatusCode: "futureStatus",
|
||||
chatStatusText: "Future status from iPhone")))
|
||||
let rendered = WatchAppSnapshotMessage.localizedChatStatusText(
|
||||
statusCode: snapshot.chatStatusCode,
|
||||
legacyText: snapshot.chatStatusText,
|
||||
chatCount: 0,
|
||||
hasAppSnapshot: true)
|
||||
|
||||
#expect(rendered == "Future status from iPhone")
|
||||
}
|
||||
|
||||
private static func payload(
|
||||
chatStatusCode: String,
|
||||
chatStatusText: String) -> [String: Any]
|
||||
{
|
||||
[
|
||||
"type": OpenClawWatchPayloadType.appSnapshot.rawValue,
|
||||
"gatewayStatusText": "Connected",
|
||||
"gatewayConnected": true,
|
||||
"agentName": "Main",
|
||||
"sessionKey": "main",
|
||||
"talkStatusText": "Off",
|
||||
"talkEnabled": false,
|
||||
"talkListening": false,
|
||||
"talkSpeaking": false,
|
||||
"pendingApprovalCount": 0,
|
||||
"chatStatusCode": chatStatusCode,
|
||||
"chatStatusText": chatStatusText,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -537,52 +537,7 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable {
|
||||
}
|
||||
|
||||
private static func parseAppSnapshotPayload(_ payload: [String: Any]) -> WatchAppSnapshotMessage? {
|
||||
guard let type = payload["type"] as? String,
|
||||
type == WatchPayloadType.appSnapshot.rawValue
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let gatewayStatusText = (payload["gatewayStatusText"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let agentName = (payload["agentName"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let agentAvatarURL = (payload["agentAvatarUrl"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let agentAvatarText = (payload["agentAvatarText"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let sessionKey = (payload["sessionKey"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let gatewayStableID = WatchGatewayID.exact(payload["gatewayStableID"] as? String)
|
||||
let talkStatusText = (payload["talkStatusText"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let pendingApprovalCount = (payload["pendingApprovalCount"] as? Int)
|
||||
?? (payload["pendingApprovalCount"] as? NSNumber)?.intValue
|
||||
?? 0
|
||||
let sentAtMs = (payload["sentAtMs"] as? NSNumber)?.int64Value
|
||||
let snapshotId = (payload["snapshotId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let chatItems = (payload["chatItems"] as? [Any])?.compactMap(Self.parseChatItem)
|
||||
let chatStatusCode = (payload["chatStatusCode"] as? String)
|
||||
.flatMap(OpenClawWatchChatStatusCode.init(rawValue:))
|
||||
let chatStatusText = (payload["chatStatusText"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return WatchAppSnapshotMessage(
|
||||
gatewayStatusText: gatewayStatusText.isEmpty ? "Unknown" : gatewayStatusText,
|
||||
gatewayConnected: Self.boolValue(payload["gatewayConnected"]),
|
||||
agentName: agentName.isEmpty ? "Main" : agentName,
|
||||
agentAvatarURL: agentAvatarURL?.isEmpty == false ? agentAvatarURL : nil,
|
||||
agentAvatarText: agentAvatarText?.isEmpty == false ? agentAvatarText : nil,
|
||||
sessionKey: sessionKey.isEmpty ? "main" : sessionKey,
|
||||
gatewayStableID: gatewayStableID,
|
||||
talkStatusText: talkStatusText.isEmpty ? "Off" : talkStatusText,
|
||||
talkEnabled: Self.boolValue(payload["talkEnabled"]),
|
||||
talkListening: Self.boolValue(payload["talkListening"]),
|
||||
talkSpeaking: Self.boolValue(payload["talkSpeaking"]),
|
||||
pendingApprovalCount: max(0, pendingApprovalCount),
|
||||
chatItems: chatItems,
|
||||
chatStatusCode: chatStatusCode,
|
||||
chatStatusText: chatStatusText?.isEmpty == false ? chatStatusText : nil,
|
||||
sentAtMs: sentAtMs,
|
||||
snapshotId: snapshotId)
|
||||
WatchAppSnapshotMessage.parsePayload(payload)
|
||||
}
|
||||
|
||||
private static func parseChatCompletionPayload(
|
||||
@@ -603,34 +558,6 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable {
|
||||
sentAtMs: sentAtMs)
|
||||
}
|
||||
|
||||
private static func parseChatItem(_ item: Any) -> WatchChatItem? {
|
||||
guard let dict = item as? [String: Any] else { return nil }
|
||||
guard let id = (dict["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!id.isEmpty
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let trimmedRole = (dict["role"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let text = (dict["text"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let text, !text.isEmpty else { return nil }
|
||||
let timestampMs = (dict["timestampMs"] as? NSNumber)?.int64Value
|
||||
return WatchChatItem(
|
||||
id: id,
|
||||
role: trimmedRole.isEmpty ? "assistant" : trimmedRole,
|
||||
text: text,
|
||||
timestampMs: timestampMs)
|
||||
}
|
||||
|
||||
private static func boolValue(_ value: Any?) -> Bool {
|
||||
if let bool = value as? Bool {
|
||||
return bool
|
||||
}
|
||||
if let number = value as? NSNumber {
|
||||
return number.boolValue
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private static func encodeAppSnapshotRequestPayload(
|
||||
_ request: WatchAppSnapshotRequestMessage) -> [String: Any]
|
||||
{
|
||||
|
||||
@@ -87,6 +87,113 @@ struct WatchAppSnapshotMessage: Codable, Equatable {
|
||||
var chatStatusText: String?
|
||||
var sentAtMs: Int64?
|
||||
var snapshotId: String?
|
||||
|
||||
static func parsePayload(_ payload: [String: Any]) -> Self? {
|
||||
guard let type = payload["type"] as? String,
|
||||
type == WatchPayloadType.appSnapshot.rawValue
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let gatewayStatusText = (payload["gatewayStatusText"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let agentName = (payload["agentName"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let agentAvatarURL = (payload["agentAvatarUrl"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let agentAvatarText = (payload["agentAvatarText"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let sessionKey = (payload["sessionKey"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let gatewayStableID = WatchGatewayID.exact(payload["gatewayStableID"] as? String)
|
||||
let talkStatusText = (payload["talkStatusText"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let pendingApprovalCount = (payload["pendingApprovalCount"] as? Int)
|
||||
?? (payload["pendingApprovalCount"] as? NSNumber)?.intValue
|
||||
?? 0
|
||||
let sentAtMs = (payload["sentAtMs"] as? NSNumber)?.int64Value
|
||||
let snapshotId = (payload["snapshotId"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let chatItems = (payload["chatItems"] as? [Any])?.compactMap(Self.parseChatItem)
|
||||
let chatStatusCode = (payload["chatStatusCode"] as? String)
|
||||
.flatMap(OpenClawWatchChatStatusCode.init(rawValue:))
|
||||
let chatStatusText = (payload["chatStatusText"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return Self(
|
||||
gatewayStatusText: gatewayStatusText.isEmpty ? "Unknown" : gatewayStatusText,
|
||||
gatewayConnected: Self.boolValue(payload["gatewayConnected"]),
|
||||
agentName: agentName.isEmpty ? "Main" : agentName,
|
||||
agentAvatarURL: agentAvatarURL?.isEmpty == false ? agentAvatarURL : nil,
|
||||
agentAvatarText: agentAvatarText?.isEmpty == false ? agentAvatarText : nil,
|
||||
sessionKey: sessionKey.isEmpty ? "main" : sessionKey,
|
||||
gatewayStableID: gatewayStableID,
|
||||
talkStatusText: talkStatusText.isEmpty ? "Off" : talkStatusText,
|
||||
talkEnabled: Self.boolValue(payload["talkEnabled"]),
|
||||
talkListening: Self.boolValue(payload["talkListening"]),
|
||||
talkSpeaking: Self.boolValue(payload["talkSpeaking"]),
|
||||
pendingApprovalCount: max(0, pendingApprovalCount),
|
||||
chatItems: chatItems,
|
||||
chatStatusCode: chatStatusCode,
|
||||
chatStatusText: chatStatusText?.isEmpty == false ? chatStatusText : nil,
|
||||
sentAtMs: sentAtMs,
|
||||
snapshotId: snapshotId)
|
||||
}
|
||||
|
||||
static func localizedChatStatusText(
|
||||
statusCode: OpenClawWatchChatStatusCode?,
|
||||
legacyText: String?,
|
||||
chatCount: Int,
|
||||
hasAppSnapshot: Bool) -> String
|
||||
{
|
||||
if let statusCode {
|
||||
return switch statusCode {
|
||||
case .connectIPhone:
|
||||
String(localized: "Connect iPhone chat to read messages")
|
||||
case .noMessages:
|
||||
String(localized: "No chat messages yet")
|
||||
case .unavailable:
|
||||
String(localized: "Chat unavailable")
|
||||
}
|
||||
}
|
||||
if let legacyText, !legacyText.isEmpty {
|
||||
return legacyText
|
||||
}
|
||||
if chatCount > 0 {
|
||||
return String(
|
||||
AttributedString(localized: "^[\(chatCount) recent message](inflect: true)").characters)
|
||||
}
|
||||
return hasAppSnapshot
|
||||
? String(localized: "No messages synced")
|
||||
: String(localized: "Waiting for iPhone")
|
||||
}
|
||||
|
||||
private static func parseChatItem(_ item: Any) -> WatchChatItem? {
|
||||
guard let dict = item as? [String: Any] else { return nil }
|
||||
guard let id = (dict["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!id.isEmpty
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let trimmedRole = (dict["role"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let text = (dict["text"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let text, !text.isEmpty else { return nil }
|
||||
let timestampMs = (dict["timestampMs"] as? NSNumber)?.int64Value
|
||||
return WatchChatItem(
|
||||
id: id,
|
||||
role: trimmedRole.isEmpty ? "assistant" : trimmedRole,
|
||||
text: text,
|
||||
timestampMs: timestampMs)
|
||||
}
|
||||
|
||||
private static func boolValue(_ value: Any?) -> Bool {
|
||||
if let bool = value as? Bool {
|
||||
return bool
|
||||
}
|
||||
if let number = value as? NSNumber {
|
||||
return number.boolValue
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
typealias WatchChatCompletionMessage = OpenClawWatchChatCompletionMessage
|
||||
|
||||
@@ -538,27 +538,11 @@ private struct WatchControlSurfaceView: View {
|
||||
}
|
||||
|
||||
private var chatStatusText: String {
|
||||
if let statusCode = store.appSnapshot?.chatStatusCode {
|
||||
return switch statusCode {
|
||||
case .connectIPhone:
|
||||
String(localized: "Connect iPhone chat to read messages")
|
||||
case .noMessages:
|
||||
String(localized: "No chat messages yet")
|
||||
case .unavailable:
|
||||
String(localized: "Chat unavailable")
|
||||
}
|
||||
}
|
||||
if let status = store.appSnapshot?.chatStatusText, !status.isEmpty {
|
||||
return status
|
||||
}
|
||||
if self.chatCount > 0 {
|
||||
let count = self.chatCount
|
||||
return String(
|
||||
AttributedString(localized: "^[\(count) recent message](inflect: true)").characters)
|
||||
}
|
||||
return self.store.hasAppSnapshot
|
||||
? String(localized: "No messages synced")
|
||||
: String(localized: "Waiting for iPhone")
|
||||
WatchAppSnapshotMessage.localizedChatStatusText(
|
||||
statusCode: self.store.appSnapshot?.chatStatusCode,
|
||||
legacyText: self.store.appSnapshot?.chatStatusText,
|
||||
chatCount: self.chatCount,
|
||||
hasAppSnapshot: self.store.hasAppSnapshot)
|
||||
}
|
||||
|
||||
private var chatSendStatusText: String? {
|
||||
|
||||
@@ -421,6 +421,8 @@ targets:
|
||||
group: ShareExtension
|
||||
- path: WatchApp/Sources/WatchVoiceTurnTracker.swift
|
||||
group: WatchApp/Sources
|
||||
- path: WatchApp/Sources/WatchInboxMessages.swift
|
||||
group: WatchApp/Sources
|
||||
dependencies:
|
||||
- package: OpenClawKit
|
||||
settings:
|
||||
|
||||
Reference in New Issue
Block a user