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
279 lines
9.1 KiB
Swift
279 lines
9.1 KiB
Swift
import Foundation
|
|
import OpenClawKit
|
|
|
|
extension WebSocketTasking {
|
|
/// Keep unit-test doubles resilient to protocol additions.
|
|
func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void) {
|
|
pongReceiveHandler(nil)
|
|
}
|
|
}
|
|
|
|
enum GatewayWebSocketTestSupport {
|
|
static func connectChallengeData(nonce: String = "test-nonce") -> Data {
|
|
let json = """
|
|
{
|
|
"type": "event",
|
|
"event": "connect.challenge",
|
|
"payload": { "nonce": "\(nonce)" }
|
|
}
|
|
"""
|
|
return Data(json.utf8)
|
|
}
|
|
|
|
static func connectRequestID(from message: URLSessionWebSocketTask.Message) -> String? {
|
|
guard let obj = self.requestFrameObject(from: message) else { return nil }
|
|
guard (obj["type"] as? String) == "req", (obj["method"] as? String) == "connect" else {
|
|
return nil
|
|
}
|
|
return obj["id"] as? String
|
|
}
|
|
|
|
static func connectRequestParams(from message: URLSessionWebSocketTask.Message) -> [String: Any]? {
|
|
guard let obj = self.requestFrameObject(from: message) else { return nil }
|
|
guard (obj["type"] as? String) == "req", (obj["method"] as? String) == "connect" else {
|
|
return nil
|
|
}
|
|
return obj["params"] as? [String: Any]
|
|
}
|
|
|
|
static func connectScopes(from message: URLSessionWebSocketTask.Message) -> [String]? {
|
|
guard let obj = self.requestFrameObject(from: message) else { return nil }
|
|
guard (obj["type"] as? String) == "req", (obj["method"] as? String) == "connect" else {
|
|
return nil
|
|
}
|
|
let params = obj["params"] as? [String: Any]
|
|
return params?["scopes"] as? [String]
|
|
}
|
|
|
|
static func connectOkData(id: String) -> Data {
|
|
let json = """
|
|
{
|
|
"type": "res",
|
|
"id": "\(id)",
|
|
"ok": true,
|
|
"payload": {
|
|
"type": "hello-ok",
|
|
"protocol": 2,
|
|
"server": { "version": "test", "connId": "test" },
|
|
"features": { "methods": [], "events": [] },
|
|
"snapshot": {
|
|
"presence": [ { "ts": 1 } ],
|
|
"health": {},
|
|
"stateVersion": { "presence": 0, "health": 0 },
|
|
"uptimeMs": 0
|
|
},
|
|
"auth": { "role": "operator", "scopes": [] },
|
|
"policy": { "maxPayload": 1, "maxBufferedBytes": 1, "tickIntervalMs": 30000 }
|
|
}
|
|
}
|
|
"""
|
|
return Data(json.utf8)
|
|
}
|
|
|
|
static func connectAuthFailureData(
|
|
id: String,
|
|
detailCode: String,
|
|
message: String = "gateway auth rejected",
|
|
canRetryWithDeviceToken: Bool = false,
|
|
recommendedNextStep: String? = nil) -> Data
|
|
{
|
|
let recommendedNextStepJson = if let recommendedNextStep {
|
|
"""
|
|
,
|
|
"recommendedNextStep": "\(recommendedNextStep)"
|
|
"""
|
|
} else {
|
|
""
|
|
}
|
|
let json = """
|
|
{
|
|
"type": "res",
|
|
"id": "\(id)",
|
|
"ok": false,
|
|
"error": {
|
|
"code": "INVALID_REQUEST",
|
|
"message": "\(message)",
|
|
"details": {
|
|
"code": "\(detailCode)",
|
|
"canRetryWithDeviceToken": \(canRetryWithDeviceToken ? "true" : "false")
|
|
\(recommendedNextStepJson)
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
return Data(json.utf8)
|
|
}
|
|
|
|
static func requestID(from message: URLSessionWebSocketTask.Message) -> String? {
|
|
guard let obj = self.requestFrameObject(from: message) else { return nil }
|
|
guard (obj["type"] as? String) == "req" else {
|
|
return nil
|
|
}
|
|
return obj["id"] as? String
|
|
}
|
|
|
|
private static func requestFrameObject(from message: URLSessionWebSocketTask.Message) -> [String: Any]? {
|
|
let data: Data? = switch message {
|
|
case let .data(d): d
|
|
case let .string(s): s.data(using: .utf8)
|
|
@unknown default: nil
|
|
}
|
|
guard let data else { return nil }
|
|
return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
}
|
|
|
|
static func okResponseData(id: String) -> Data {
|
|
let json = """
|
|
{
|
|
"type": "res",
|
|
"id": "\(id)",
|
|
"ok": true,
|
|
"payload": { "ok": true }
|
|
}
|
|
"""
|
|
return Data(json.utf8)
|
|
}
|
|
}
|
|
|
|
extension NSLock {
|
|
@inline(__always)
|
|
fileprivate func withLock<T>(_ body: () throws -> T) rethrows -> T {
|
|
self.lock(); defer { self.unlock() }
|
|
return try body()
|
|
}
|
|
}
|
|
|
|
final class GatewayTestWebSocketTask: WebSocketTasking, @unchecked Sendable {
|
|
typealias SendHook = @Sendable (GatewayTestWebSocketTask, URLSessionWebSocketTask.Message, Int) async throws -> Void
|
|
typealias ReceiveHook = @Sendable (GatewayTestWebSocketTask, Int) async throws -> URLSessionWebSocketTask.Message
|
|
|
|
private let lock = NSLock()
|
|
private let sendHook: SendHook?
|
|
private let receiveHook: ReceiveHook?
|
|
private var _state: URLSessionTask.State = .suspended
|
|
private var connectRequestID: String?
|
|
private var sendCount = 0
|
|
private var receiveCount = 0
|
|
private var cancelCount = 0
|
|
private var pendingReceiveHandler: (@Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)?
|
|
|
|
init(sendHook: SendHook? = nil, receiveHook: ReceiveHook? = nil) {
|
|
self.sendHook = sendHook
|
|
self.receiveHook = receiveHook
|
|
}
|
|
|
|
var state: URLSessionTask.State {
|
|
get { self.lock.withLock { self._state } }
|
|
set { self.lock.withLock { self._state = newValue } }
|
|
}
|
|
|
|
func snapshotCancelCount() -> Int {
|
|
self.lock.withLock { self.cancelCount }
|
|
}
|
|
|
|
func snapshotConnectRequestID() -> String? {
|
|
self.lock.withLock { self.connectRequestID }
|
|
}
|
|
|
|
func snapshotSendCount() -> Int {
|
|
self.lock.withLock { self.sendCount }
|
|
}
|
|
|
|
func resume() {
|
|
self.state = .running
|
|
}
|
|
|
|
func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
|
_ = (closeCode, reason)
|
|
let handler = self.lock.withLock { () -> (@Sendable (Result<
|
|
URLSessionWebSocketTask.Message,
|
|
Error,
|
|
>) -> Void)? in
|
|
self._state = .canceling
|
|
self.cancelCount += 1
|
|
defer { self.pendingReceiveHandler = nil }
|
|
return self.pendingReceiveHandler
|
|
}
|
|
handler?(Result<URLSessionWebSocketTask.Message, Error>.failure(URLError(.cancelled)))
|
|
}
|
|
|
|
func send(_ message: URLSessionWebSocketTask.Message) async throws {
|
|
let sendIndex = self.lock.withLock { () -> Int in
|
|
let current = self.sendCount
|
|
self.sendCount += 1
|
|
return current
|
|
}
|
|
if sendIndex == 0, let id = GatewayWebSocketTestSupport.connectRequestID(from: message) {
|
|
self.lock.withLock { self.connectRequestID = id }
|
|
}
|
|
try await self.sendHook?(self, message, sendIndex)
|
|
}
|
|
|
|
func receive() async throws -> URLSessionWebSocketTask.Message {
|
|
let receiveIndex = self.lock.withLock { () -> Int in
|
|
let current = self.receiveCount
|
|
self.receiveCount += 1
|
|
return current
|
|
}
|
|
if let receiveHook = self.receiveHook {
|
|
return try await receiveHook(self, receiveIndex)
|
|
}
|
|
if receiveIndex == 0 {
|
|
return .data(GatewayWebSocketTestSupport.connectChallengeData())
|
|
}
|
|
let id = self.snapshotConnectRequestID() ?? "connect"
|
|
return .data(GatewayWebSocketTestSupport.connectOkData(id: id))
|
|
}
|
|
|
|
func receive(
|
|
completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)
|
|
{
|
|
self.lock.withLock { self.pendingReceiveHandler = completionHandler }
|
|
}
|
|
|
|
func emitReceiveSuccess(_ message: URLSessionWebSocketTask.Message) {
|
|
let handler = self.lock.withLock { self.pendingReceiveHandler }
|
|
handler?(Result<URLSessionWebSocketTask.Message, Error>.success(message))
|
|
}
|
|
|
|
func emitReceiveFailure(_ error: Error = URLError(.networkConnectionLost)) {
|
|
let handler = self.lock.withLock { self.pendingReceiveHandler }
|
|
handler?(Result<URLSessionWebSocketTask.Message, Error>.failure(error))
|
|
}
|
|
}
|
|
|
|
final class GatewayTestWebSocketSession: WebSocketSessioning, @unchecked Sendable {
|
|
typealias TaskFactory = @Sendable () -> GatewayTestWebSocketTask
|
|
|
|
private let lock = NSLock()
|
|
private let taskFactory: TaskFactory
|
|
private var tasks: [GatewayTestWebSocketTask] = []
|
|
private var makeCount = 0
|
|
|
|
init(taskFactory: @escaping TaskFactory = { GatewayTestWebSocketTask() }) {
|
|
self.taskFactory = taskFactory
|
|
}
|
|
|
|
func snapshotMakeCount() -> Int {
|
|
self.lock.withLock { self.makeCount }
|
|
}
|
|
|
|
func snapshotCancelCount() -> Int {
|
|
self.lock.withLock { self.tasks.reduce(0) { $0 + $1.snapshotCancelCount() } }
|
|
}
|
|
|
|
func latestTask() -> GatewayTestWebSocketTask? {
|
|
self.lock.withLock { self.tasks.last }
|
|
}
|
|
|
|
func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
|
|
_ = url
|
|
let task = self.taskFactory()
|
|
self.lock.withLock {
|
|
self.makeCount += 1
|
|
self.tasks.append(task)
|
|
}
|
|
return WebSocketTaskBox(task: task)
|
|
}
|
|
}
|