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
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
203 lines
8.2 KiB
Swift
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")
|
|
}
|
|
}
|