mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
Adds electron/gateway-ws-probe.cjs: a small helper that opens a gateway WebSocket URL and classifies the handshake (open/frame → ok; error or close before open → fail; open-then-early-close → credential rejected; never-opens → timeout). The WebSocket implementation is injected so it can be unit-tested without a real socket. Wires gateway-ws-probe.test.cjs into test:desktop:platforms, covering every handshake outcome plus constructor-throw and missing-impl.
122 lines
4.3 KiB
JavaScript
122 lines
4.3 KiB
JavaScript
/**
|
|
* Tests for electron/gateway-ws-probe.cjs.
|
|
*
|
|
* Run with: node --test electron/gateway-ws-probe.test.cjs
|
|
* (Wired into npm test:desktop:platforms in package.json.)
|
|
*
|
|
* The probe drives a real WebSocket handshake for the "Test remote" button.
|
|
* Here we inject a fake socket so we can deterministically replay each handshake
|
|
* outcome (open, frame, error, early close, never-opens) without a network.
|
|
*/
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const { probeGatewayWebSocket } = require('./gateway-ws-probe.cjs')
|
|
|
|
// Minimal WebSocket double: records listeners synchronously (the probe attaches
|
|
// them in its executor) and exposes emit() so the test can replay events.
|
|
function makeFakeWs() {
|
|
const instances = []
|
|
class FakeWs {
|
|
constructor(url) {
|
|
this.url = url
|
|
this.listeners = {}
|
|
this.closed = false
|
|
instances.push(this)
|
|
}
|
|
addEventListener(type, fn) {
|
|
;(this.listeners[type] ||= []).push(fn)
|
|
}
|
|
close() {
|
|
this.closed = true
|
|
}
|
|
emit(type, event) {
|
|
for (const fn of this.listeners[type] || []) fn(event)
|
|
}
|
|
}
|
|
return { FakeWs, instances }
|
|
}
|
|
|
|
const FAST = { connectTimeoutMs: 1_000, readyGraceMs: 10 }
|
|
|
|
test('probe resolves ok when the socket opens and stays open', async () => {
|
|
const { FakeWs, instances } = makeFakeWs()
|
|
const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
|
|
instances[0].emit('open')
|
|
const result = await promise
|
|
assert.deepEqual(result, { ok: true })
|
|
assert.equal(instances[0].closed, true)
|
|
})
|
|
|
|
test('probe resolves ok immediately when a frame arrives', async () => {
|
|
const { FakeWs, instances } = makeFakeWs()
|
|
const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', {
|
|
WebSocketImpl: FakeWs,
|
|
connectTimeoutMs: 1_000,
|
|
readyGraceMs: 10_000 // long grace: success must come from the frame, not the timer
|
|
})
|
|
instances[0].emit('open')
|
|
instances[0].emit('message', { data: '{"jsonrpc":"2.0"}' })
|
|
const result = await promise
|
|
assert.deepEqual(result, { ok: true })
|
|
})
|
|
|
|
test('probe fails when the socket errors before opening', async () => {
|
|
const { FakeWs, instances } = makeFakeWs()
|
|
const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
|
|
instances[0].emit('error', { message: 'ECONNREFUSED' })
|
|
const result = await promise
|
|
assert.equal(result.ok, false)
|
|
assert.match(result.reason, /ECONNREFUSED/)
|
|
})
|
|
|
|
test('probe fails when the gateway closes before opening', async () => {
|
|
const { FakeWs, instances } = makeFakeWs()
|
|
const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
|
|
instances[0].emit('close', { code: 1006 })
|
|
const result = await promise
|
|
assert.equal(result.ok, false)
|
|
assert.match(result.reason, /before it opened/)
|
|
assert.match(result.reason, /1006/)
|
|
})
|
|
|
|
test('probe fails when the gateway accepts then immediately closes (auth rejected)', async () => {
|
|
const { FakeWs, instances } = makeFakeWs()
|
|
const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
|
|
instances[0].emit('open')
|
|
instances[0].emit('close', { code: 4403, reason: 'forbidden' })
|
|
const result = await promise
|
|
assert.equal(result.ok, false)
|
|
assert.match(result.reason, /credential rejected/)
|
|
assert.match(result.reason, /4403/)
|
|
assert.match(result.reason, /forbidden/)
|
|
})
|
|
|
|
test('probe times out when the socket never opens', async () => {
|
|
const { FakeWs } = makeFakeWs()
|
|
const result = await probeGatewayWebSocket('ws://host/api/ws?token=t', {
|
|
WebSocketImpl: FakeWs,
|
|
connectTimeoutMs: 20,
|
|
readyGraceMs: 10
|
|
})
|
|
assert.equal(result.ok, false)
|
|
assert.match(result.reason, /Timed out/)
|
|
})
|
|
|
|
test('probe fails gracefully when the constructor throws', async () => {
|
|
class ThrowingWs {
|
|
constructor() {
|
|
throw new Error('bad url')
|
|
}
|
|
}
|
|
const result = await probeGatewayWebSocket('ws://host/api/ws', { WebSocketImpl: ThrowingWs, ...FAST })
|
|
assert.equal(result.ok, false)
|
|
assert.match(result.reason, /bad url/)
|
|
})
|
|
|
|
test('probe reports unavailable when no WebSocket implementation is provided', async () => {
|
|
const result = await probeGatewayWebSocket('ws://host/api/ws', { WebSocketImpl: undefined })
|
|
assert.equal(result.ok, false)
|
|
assert.match(result.reason, /not available/)
|
|
})
|