Files
adolf/apps/ios/Tests/IOSGatewayChatTransportTests.swift
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

203 lines
8.2 KiB
Swift

import Foundation
import OpenClawChatUI
import OpenClawKit
import OpenClawProtocol
import Testing
@testable import OpenClaw
@Suite struct IOSGatewayChatTransportTests {
private func object(from json: String) throws -> [String: Any] {
let data = try #require(json.data(using: .utf8))
let value = try JSONSerialization.jsonObject(with: data)
return try #require(value as? [String: Any])
}
@Test func agentWaitTreatsSuccessAsCompletion() {
#expect(IOSGatewayChatTransport.isAgentWaitCompletionStatus("success"))
#expect(IOSGatewayChatTransport.isAgentWaitCompletionStatus(" ok "))
#expect(IOSGatewayChatTransport.isAgentWaitCompletionStatus("completed"))
#expect(IOSGatewayChatTransport.isAgentWaitCompletionStatus("succeeded"))
#expect(!IOSGatewayChatTransport.isAgentWaitCompletionStatus("timeout"))
#expect(!IOSGatewayChatTransport.isAgentWaitCompletionStatus("failed"))
}
@Test func agentWaitTimeoutAddsGatewayMargin() {
#expect(IOSGatewayChatTransport.agentWaitRequestTimeoutSeconds(timeoutMs: 1) == 6)
#expect(IOSGatewayChatTransport.agentWaitRequestTimeoutSeconds(timeoutMs: 1000) == 6)
#expect(IOSGatewayChatTransport.agentWaitRequestTimeoutSeconds(timeoutMs: 30000) == 35)
}
@Test func compactionLeavesTerminalTimeoutToGateway() {
#expect(IOSGatewayChatTransport.compactionRequestTimeoutSeconds == 0)
}
@Test func agentWaitCompletionDecodesFallbackRunId() throws {
let data = Data(#"{"status":"completed"}"#.utf8)
let completion = try IOSGatewayChatTransport.decodeAgentWaitCompletion(data, fallbackRunId: "run-local")
#expect(completion.runId == "run-local")
#expect(completion.status == "completed")
#expect(completion.completed)
}
@Test func listSessionsParamsIncludeGlobalSessionsButNotUnknown() throws {
let params = try self.object(from: IOSGatewayChatTransport.makeListSessionsParamsJSON(limit: 12))
#expect(params["includeGlobal"] as? Bool == true)
#expect(params["includeUnknown"] as? Bool == false)
#expect(params["limit"] as? Int == 12)
}
@Test func commandsListParamsRequestTextScopeWithArgs() throws {
let params = try self.object(from: IOSGatewayChatTransport.makeCommandsListParamsJSON())
#expect(params["scope"] as? String == "text")
#expect(params["includeArgs"] as? Bool == true)
#expect(params["agentId"] == nil)
}
@Test func commandsListParamsIncludeAgentForAgentScopedSession() throws {
let params = try self.object(
from: IOSGatewayChatTransport.makeCommandsListParamsJSON(sessionKey: "agent:reviewer:ios-main"))
#expect(params["scope"] as? String == "text")
#expect(params["includeArgs"] as? Bool == true)
#expect(params["agentId"] as? String == "reviewer")
}
@Test func chatSendParamsOmitEmptyAttachmentsAndKeepSessionFields() throws {
let params = try self.object(
from: IOSGatewayChatTransport.makeChatSendParamsJSON(
sessionKey: "agent:main",
message: "hello",
thinking: "low",
idempotencyKey: "send-1",
attachments: []))
#expect(params["sessionKey"] as? String == "agent:main")
#expect(params["message"] as? String == "hello")
#expect(params["thinking"] as? String == "low")
#expect(params["idempotencyKey"] as? String == "send-1")
#expect(params["timeoutMs"] as? Int == IOSGatewayChatTransport.defaultChatSendTimeoutMs)
#expect(params["attachments"] == nil)
}
@Test func requestsFailFastWhenGatewayNotConnected() async {
let gateway = GatewayNodeSession()
let transport = IOSGatewayChatTransport(gateway: gateway)
do {
_ = try await transport.requestHistory(sessionKey: "node-test")
Issue.record("Expected requestHistory to throw when gateway not connected")
} catch {}
do {
_ = try await transport.sendMessage(
sessionKey: "node-test",
message: "hello",
thinking: "low",
idempotencyKey: "idempotency",
attachments: [])
Issue.record("Expected sendMessage to throw when gateway not connected")
} catch {}
do {
_ = try await transport.requestHealth(timeoutMs: 250)
Issue.record("Expected requestHealth to throw when gateway not connected")
} catch {}
do {
try await transport.resetSession(sessionKey: "node-test")
Issue.record("Expected resetSession to throw when gateway not connected")
} catch {}
do {
try await transport.setActiveSessionKey("node-test")
Issue.record("Expected setActiveSessionKey to throw when gateway not connected")
} catch {}
}
@Test func mapsSessionMessageEventToSessionMessage() {
let payload = AnyCodable([
"sessionKey": AnyCodable("agent:main:main"),
"agentId": AnyCodable("main"),
"messageId": AnyCodable("msg-1"),
"messageSeq": AnyCodable(7),
"message": AnyCodable([
"role": AnyCodable("assistant"),
"content": AnyCodable([
AnyCodable([
"type": AnyCodable("text"),
"text": AnyCodable("agent reply"),
]),
]),
"timestamp": AnyCodable(1234.5),
]),
])
let frame = EventFrame(
type: "event",
event: "session.message",
payload: payload,
seq: 1,
stateversion: nil)
let mapped = IOSGatewayChatTransport.mapEventFrame(frame)
switch mapped {
case let .sessionMessage(message):
#expect(message.sessionKey == "agent:main:main")
#expect(message.agentId == "main")
#expect(message.messageId == "msg-1")
#expect(message.messageSeq == 7)
#expect(message.message?.role == "assistant")
#expect(message.message?.content.first?.text == "agent reply")
default:
Issue.record("expected .sessionMessage from session.message event, got \(String(describing: mapped))")
}
}
@Test func mapsChatEventToChat() {
let payload = AnyCodable([
"runId": AnyCodable("run-1"),
"sessionKey": AnyCodable("main"),
"state": AnyCodable("final"),
])
let frame = EventFrame(type: "event", event: "chat", payload: payload, seq: 1, stateversion: nil)
let mapped = IOSGatewayChatTransport.mapEventFrame(frame)
switch mapped {
case let .chat(chat):
#expect(chat.runId == "run-1")
#expect(chat.sessionKey == "main")
#expect(chat.state == "final")
default:
Issue.record("expected .chat from chat event, got \(String(describing: mapped))")
}
}
@Test func mapsUnknownEventToNil() {
let frame = EventFrame(
type: "event",
event: "unknown",
payload: AnyCodable(["a": AnyCodable(1)]),
seq: 1,
stateversion: nil)
let mapped = IOSGatewayChatTransport.mapEventFrame(frame)
#expect(mapped == nil)
}
}
@Suite struct LocalFixtureChatTransportTests {
@Test func sentUserTurnCarriesGatewayIdempotencyMetadata() async throws {
let transport = LocalFixtureChatTransport(fixture: .appleReviewDemo)
_ = try await transport.sendMessage(
sessionKey: "main",
message: "hello",
thinking: "auto",
idempotencyKey: "fixture-run",
attachments: [])
let history = try await transport.requestHistory(sessionKey: "main")
let decoded = try #require(history.messages).compactMap { payload -> OpenClawChatMessage? in
guard let data = try? JSONEncoder().encode(payload) else { return nil }
return try? JSONDecoder().decode(OpenClawChatMessage.self, from: data)
}
#expect(decoded.last(where: { $0.role == "user" })?.idempotencyKey == "fixture-run:user")
}
}