fix(macos): fail fast during node lifecycle cleanup (#105282)

This commit is contained in:
Peter Steinberger
2026-07-12 11:41:21 +01:00
committed by GitHub
parent 4249cdb8fe
commit 7303f42917
2 changed files with 81 additions and 9 deletions

View File

@@ -1145,6 +1145,71 @@ struct GatewayNodeSessionTests {
await gateway.disconnect()
}
@Test
func `replacement invoke fails promptly while disconnect lifecycle is blocked`() async throws {
let session = FakeGatewayWebSocketSession()
let gateway = GatewayNodeSession()
let invalidationGate = AsyncGate()
let invocations = DisconnectProbe()
let options = GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: ["system.which"],
permissions: [:],
clientId: "openclaw-macos",
clientMode: "node",
clientDisplayName: "macOS Test",
includeDeviceIdentity: false)
try await gateway.connect(
url: #require(URL(string: "ws://first.example.invalid")),
token: nil,
bootstrapToken: nil,
password: nil,
connectOptions: options,
sessionBox: WebSocketSessionBox(session: session),
onConnected: {},
onDisconnected: { _ in },
onInvoke: { request in
await invocations.record(request.id)
return BridgeInvokeResponse(id: request.id, ok: true, payloadJSON: nil, error: nil)
},
onRouteInvalidated: { await invalidationGate.wait() })
let firstTask = try #require(session.latestTask())
try await waitUntil("receive loop armed before disconnect") {
firstTask.hasPendingReceiveHandler()
}
firstTask.emitReceiveFailure()
try await waitUntil("disconnect lifecycle blocked") {
await invalidationGate.hasStarted()
}
try await waitUntil("replacement transport connected") {
session.snapshotMakeCount() == 2
}
let replacementTask = try #require(session.latestTask())
try await waitUntil("replacement socket receiving") {
replacementTask.hasPendingReceiveHandler()
}
replacementTask.emitInvokeRequest(id: "during-lifecycle", command: "system.which")
try await waitUntil("lifecycle unavailable result") {
replacementTask.sentRequestCount(method: "node.invoke.result") == 1
}
let result = try #require(replacementTask.sentRequests(method: "node.invoke.result").first)
let params = try #require(result["params"] as? [String: Any])
let error = try #require(params["error"] as? [String: Any])
#expect(params["id"] as? String == "during-lifecycle")
#expect(params["ok"] as? Bool == false)
#expect(error["code"] as? String == OpenClawNodeErrorCode.unavailable.rawValue)
#expect(error["message"] as? String == "UNAVAILABLE: node lifecycle transition in progress")
#expect(await invocations.values() == [])
await invalidationGate.release()
await gateway.disconnect()
}
@Test
func `disconnect cleanup finishes after an in flight connected callback`() async throws {
let session = FakeGatewayWebSocketSession()