agent-relay 3.1.1 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -8
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/dist/__tests__/gateway-control.test.d.ts +2 -0
- package/packages/openclaw/dist/__tests__/gateway-control.test.d.ts.map +1 -0
- package/packages/openclaw/dist/__tests__/gateway-control.test.js +250 -0
- package/packages/openclaw/dist/__tests__/gateway-control.test.js.map +1 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.js +617 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.js.map +1 -1
- package/packages/openclaw/dist/__tests__/spawn-manager.test.js +29 -0
- package/packages/openclaw/dist/__tests__/spawn-manager.test.js.map +1 -1
- package/packages/openclaw/dist/__tests__/ws-client.test.d.ts +2 -0
- package/packages/openclaw/dist/__tests__/ws-client.test.d.ts.map +1 -0
- package/packages/openclaw/dist/__tests__/ws-client.test.js +324 -0
- package/packages/openclaw/dist/__tests__/ws-client.test.js.map +1 -0
- package/packages/openclaw/dist/cli.js +1 -1
- package/packages/openclaw/dist/cli.js.map +1 -1
- package/packages/openclaw/dist/gateway.d.ts +33 -7
- package/packages/openclaw/dist/gateway.d.ts.map +1 -1
- package/packages/openclaw/dist/gateway.js +101 -50
- package/packages/openclaw/dist/gateway.js.map +1 -1
- package/packages/openclaw/dist/types.d.ts +5 -1
- package/packages/openclaw/dist/types.d.ts.map +1 -1
- package/packages/openclaw/package.json +2 -2
- package/packages/openclaw/skill/SKILL.md +35 -13
- package/packages/openclaw/src/__tests__/SPEC-ws-client-testing.md +192 -0
- package/packages/openclaw/src/__tests__/gateway-control.test.ts +288 -0
- package/packages/openclaw/src/__tests__/gateway-threads.test.ts +746 -0
- package/packages/openclaw/src/__tests__/spawn-manager.test.ts +37 -0
- package/packages/openclaw/src/__tests__/ws-client.test.ts +395 -0
- package/packages/openclaw/src/cli.ts +1 -1
- package/packages/openclaw/src/gateway.ts +129 -56
- package/packages/openclaw/src/types.ts +5 -1
- package/packages/policy/package.json +2 -2
- package/packages/sdk/package.json +2 -2
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
|
@@ -149,4 +149,41 @@ describe('SpawnManager', () => {
|
|
|
149
149
|
const released = await manager.release('non-existent-id');
|
|
150
150
|
expect(released).toBe(false);
|
|
151
151
|
});
|
|
152
|
+
|
|
153
|
+
it('should return handle by id via get()', async () => {
|
|
154
|
+
const manager = new SpawnManager({ mode: 'process' });
|
|
155
|
+
|
|
156
|
+
const handle = await manager.spawn({
|
|
157
|
+
name: 'getter-test',
|
|
158
|
+
relayApiKey: 'rk_live_test',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(manager.get(handle.id)).toBeDefined();
|
|
162
|
+
expect(manager.get(handle.id)!.displayName).toBe('getter-test');
|
|
163
|
+
expect(manager.get('nonexistent')).toBeUndefined();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should persist state on spawn', async () => {
|
|
167
|
+
const { writeFile } = await import('node:fs/promises');
|
|
168
|
+
|
|
169
|
+
const manager = new SpawnManager({ mode: 'process' });
|
|
170
|
+
await manager.spawn({
|
|
171
|
+
name: 'persist-test',
|
|
172
|
+
relayApiKey: 'rk_live_test',
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(writeFile).toHaveBeenCalled();
|
|
176
|
+
const writeCall = vi.mocked(writeFile).mock.calls[0];
|
|
177
|
+
expect(writeCall[0]).toContain('spawns.json');
|
|
178
|
+
const written = JSON.parse(writeCall[1] as string) as { spawns: Array<{ displayName: string }> };
|
|
179
|
+
expect(written.spawns).toHaveLength(1);
|
|
180
|
+
expect(written.spawns[0].displayName).toBe('persist-test');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should return empty array from loadPersistedState when no file exists', async () => {
|
|
184
|
+
const manager = new SpawnManager({ mode: 'process' });
|
|
185
|
+
|
|
186
|
+
const state = await manager.loadPersistedState();
|
|
187
|
+
expect(state).toEqual([]);
|
|
188
|
+
});
|
|
152
189
|
});
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { WebSocketServer, type WebSocket as WsType } from 'ws';
|
|
3
|
+
|
|
4
|
+
// Mock spawn/manager and relaycast SDK to prevent side-effects from gateway.ts module load
|
|
5
|
+
vi.mock('../spawn/manager.js', () => ({
|
|
6
|
+
SpawnManager: vi.fn().mockImplementation(() => ({
|
|
7
|
+
size: 0,
|
|
8
|
+
spawn: vi.fn(),
|
|
9
|
+
release: vi.fn(),
|
|
10
|
+
releaseByName: vi.fn(),
|
|
11
|
+
releaseAll: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
list: vi.fn().mockReturnValue([]),
|
|
13
|
+
get: vi.fn(),
|
|
14
|
+
})),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('@relaycast/sdk', () => ({
|
|
18
|
+
RelayCast: vi.fn().mockImplementation(() => ({
|
|
19
|
+
agents: { registerOrGet: vi.fn().mockResolvedValue({ name: 'viewer-test', token: 'tok' }) },
|
|
20
|
+
as: vi.fn().mockReturnValue({
|
|
21
|
+
connect: vi.fn(),
|
|
22
|
+
disconnect: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
subscribe: vi.fn(),
|
|
24
|
+
on: {
|
|
25
|
+
connected: vi.fn().mockReturnValue(() => {}),
|
|
26
|
+
messageCreated: vi.fn().mockReturnValue(() => {}),
|
|
27
|
+
threadReply: vi.fn().mockReturnValue(() => {}),
|
|
28
|
+
dmReceived: vi.fn().mockReturnValue(() => {}),
|
|
29
|
+
groupDmReceived: vi.fn().mockReturnValue(() => {}),
|
|
30
|
+
commandInvoked: vi.fn().mockReturnValue(() => {}),
|
|
31
|
+
reactionAdded: vi.fn().mockReturnValue(() => {}),
|
|
32
|
+
reactionRemoved: vi.fn().mockReturnValue(() => {}),
|
|
33
|
+
reconnecting: vi.fn().mockReturnValue(() => {}),
|
|
34
|
+
disconnected: vi.fn().mockReturnValue(() => {}),
|
|
35
|
+
error: vi.fn().mockReturnValue(() => {}),
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
})),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
vi.mock('node:fs/promises', () => ({
|
|
42
|
+
readFile: vi.fn().mockResolvedValue('{"spawns":[]}'),
|
|
43
|
+
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
44
|
+
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock('node:fs', () => ({
|
|
48
|
+
existsSync: vi.fn().mockReturnValue(false),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
import { OpenClawGatewayClient } from '../gateway.js';
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Mock OpenClaw Gateway WebSocket Server
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
interface MockServerOptions {
|
|
58
|
+
/** Whether to accept or reject auth. Default: true */
|
|
59
|
+
acceptAuth?: boolean;
|
|
60
|
+
/** Delay before sending challenge (ms). 0 = immediate. */
|
|
61
|
+
challengeDelay?: number;
|
|
62
|
+
/** Whether to send a challenge at all. Default: true */
|
|
63
|
+
sendChallenge?: boolean;
|
|
64
|
+
/** Delay before responding to chat.send RPCs (ms). Default: 0 */
|
|
65
|
+
chatDelay?: number;
|
|
66
|
+
/** Whether chat.send succeeds. Default: true */
|
|
67
|
+
chatOk?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class MockOpenClawServer {
|
|
71
|
+
private wss: WebSocketServer;
|
|
72
|
+
private clients: Set<WsType> = new Set();
|
|
73
|
+
port = 0;
|
|
74
|
+
|
|
75
|
+
private acceptAuth: boolean;
|
|
76
|
+
private challengeDelay: number;
|
|
77
|
+
private sendChallenge: boolean;
|
|
78
|
+
private chatDelay: number;
|
|
79
|
+
private chatOk: boolean;
|
|
80
|
+
|
|
81
|
+
constructor(options: MockServerOptions = {}) {
|
|
82
|
+
this.acceptAuth = options.acceptAuth ?? true;
|
|
83
|
+
this.challengeDelay = options.challengeDelay ?? 0;
|
|
84
|
+
this.sendChallenge = options.sendChallenge ?? true;
|
|
85
|
+
this.chatDelay = options.chatDelay ?? 0;
|
|
86
|
+
this.chatOk = options.chatOk ?? true;
|
|
87
|
+
|
|
88
|
+
this.wss = new WebSocketServer({ port: 0 });
|
|
89
|
+
this.port = (this.wss.address() as { port: number }).port;
|
|
90
|
+
|
|
91
|
+
this.wss.on('connection', (ws) => {
|
|
92
|
+
this.clients.add(ws);
|
|
93
|
+
ws.on('close', () => this.clients.delete(ws));
|
|
94
|
+
|
|
95
|
+
if (this.sendChallenge) {
|
|
96
|
+
const challenge = JSON.stringify({
|
|
97
|
+
type: 'event',
|
|
98
|
+
event: 'connect.challenge',
|
|
99
|
+
payload: { nonce: 'test-nonce-123', ts: Date.now() },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (this.challengeDelay > 0) {
|
|
103
|
+
setTimeout(() => ws.send(challenge), this.challengeDelay);
|
|
104
|
+
} else {
|
|
105
|
+
ws.send(challenge);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
ws.on('message', (data) => {
|
|
110
|
+
const msg = JSON.parse(data.toString()) as Record<string, unknown>;
|
|
111
|
+
|
|
112
|
+
// Handle connect request
|
|
113
|
+
if (msg.method === 'connect' && msg.id === 'connect-1') {
|
|
114
|
+
if (this.acceptAuth) {
|
|
115
|
+
ws.send(JSON.stringify({ type: 'res', id: 'connect-1', ok: true }));
|
|
116
|
+
} else {
|
|
117
|
+
ws.send(JSON.stringify({
|
|
118
|
+
type: 'res',
|
|
119
|
+
id: 'connect-1',
|
|
120
|
+
ok: false,
|
|
121
|
+
error: { code: 'auth_failed', message: 'Invalid token' },
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle chat.send RPC
|
|
128
|
+
if (msg.method === 'chat.send') {
|
|
129
|
+
const respond = () => {
|
|
130
|
+
if (this.chatOk) {
|
|
131
|
+
ws.send(JSON.stringify({
|
|
132
|
+
type: 'res',
|
|
133
|
+
id: msg.id,
|
|
134
|
+
ok: true,
|
|
135
|
+
payload: { runId: 'run-1', status: 'accepted' },
|
|
136
|
+
}));
|
|
137
|
+
} else {
|
|
138
|
+
ws.send(JSON.stringify({
|
|
139
|
+
type: 'res',
|
|
140
|
+
id: msg.id,
|
|
141
|
+
ok: false,
|
|
142
|
+
error: { code: 'rate_limited', message: 'Too many requests' },
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (this.chatDelay > 0) {
|
|
148
|
+
setTimeout(respond, this.chatDelay);
|
|
149
|
+
} else {
|
|
150
|
+
respond();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Force-close all connected clients. */
|
|
158
|
+
closeAllClients(code = 1000): void {
|
|
159
|
+
for (const ws of this.clients) {
|
|
160
|
+
ws.close(code);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async close(): Promise<void> {
|
|
165
|
+
this.closeAllClients();
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
this.wss.close(() => resolve());
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Tests
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
describe('OpenClawGatewayClient', () => {
|
|
177
|
+
let server: MockOpenClawServer | null = null;
|
|
178
|
+
|
|
179
|
+
afterEach(async () => {
|
|
180
|
+
if (server) {
|
|
181
|
+
await server.close();
|
|
182
|
+
server = null;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should connect and authenticate (happy path)', async () => {
|
|
187
|
+
server = new MockOpenClawServer();
|
|
188
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
189
|
+
|
|
190
|
+
await client.connect();
|
|
191
|
+
// Should resolve without throwing
|
|
192
|
+
await client.disconnect();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should reject when auth is rejected', async () => {
|
|
196
|
+
server = new MockOpenClawServer({ acceptAuth: false });
|
|
197
|
+
const client = new OpenClawGatewayClient('bad-token', server.port);
|
|
198
|
+
|
|
199
|
+
await expect(client.connect()).rejects.toThrow(/auth failed/i);
|
|
200
|
+
await client.disconnect();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should no-op when already connected', async () => {
|
|
204
|
+
server = new MockOpenClawServer();
|
|
205
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
206
|
+
|
|
207
|
+
await client.connect();
|
|
208
|
+
// Second connect should be a no-op (early return)
|
|
209
|
+
await client.connect();
|
|
210
|
+
await client.disconnect();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should timeout when no challenge is sent', async () => {
|
|
214
|
+
server = new MockOpenClawServer({ sendChallenge: false });
|
|
215
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
216
|
+
|
|
217
|
+
// Monkey-patch the timeout to be short for the test
|
|
218
|
+
(OpenClawGatewayClient as unknown as Record<string, number>).CONNECT_TIMEOUT_MS = 200;
|
|
219
|
+
|
|
220
|
+
await expect(client.connect()).rejects.toThrow(/timed out/i);
|
|
221
|
+
await client.disconnect();
|
|
222
|
+
|
|
223
|
+
// Restore
|
|
224
|
+
(OpenClawGatewayClient as unknown as Record<string, number>).CONNECT_TIMEOUT_MS = 30_000;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should reject connect when WS closes before auth', async () => {
|
|
228
|
+
server = new MockOpenClawServer({ sendChallenge: false });
|
|
229
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
230
|
+
|
|
231
|
+
// Start connecting, then close server connections immediately
|
|
232
|
+
const connectPromise = client.connect();
|
|
233
|
+
// Give the WS time to establish before closing
|
|
234
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
235
|
+
server.closeAllClients(1000);
|
|
236
|
+
|
|
237
|
+
await expect(connectPromise).rejects.toThrow(/closed before authentication|timed out/i);
|
|
238
|
+
await client.disconnect();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should return true on successful sendChatMessage', async () => {
|
|
242
|
+
server = new MockOpenClawServer();
|
|
243
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
244
|
+
await client.connect();
|
|
245
|
+
|
|
246
|
+
const result = await client.sendChatMessage('hello world');
|
|
247
|
+
expect(result).toBe(true);
|
|
248
|
+
|
|
249
|
+
await client.disconnect();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should pass idempotencyKey in sendChatMessage params', async () => {
|
|
253
|
+
let receivedParams: Record<string, unknown> = {};
|
|
254
|
+
server = new MockOpenClawServer();
|
|
255
|
+
|
|
256
|
+
// Intercept the server to capture params
|
|
257
|
+
const origWss = (server as unknown as { wss: WebSocketServer }).wss;
|
|
258
|
+
const origListeners = origWss.listeners('connection');
|
|
259
|
+
origWss.removeAllListeners('connection');
|
|
260
|
+
origWss.on('connection', (ws) => {
|
|
261
|
+
// Re-emit for original handler
|
|
262
|
+
for (const listener of origListeners) {
|
|
263
|
+
(listener as (ws: WsType) => void)(ws);
|
|
264
|
+
}
|
|
265
|
+
ws.on('message', (data) => {
|
|
266
|
+
const msg = JSON.parse(data.toString()) as Record<string, unknown>;
|
|
267
|
+
if (msg.method === 'chat.send') {
|
|
268
|
+
receivedParams = msg.params as Record<string, unknown>;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
274
|
+
await client.connect();
|
|
275
|
+
|
|
276
|
+
await client.sendChatMessage('test', 'idem-key-123');
|
|
277
|
+
expect(receivedParams.idempotencyKey).toBe('idem-key-123');
|
|
278
|
+
|
|
279
|
+
await client.disconnect();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should return false on RPC error', async () => {
|
|
283
|
+
server = new MockOpenClawServer({ chatOk: false });
|
|
284
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
285
|
+
await client.connect();
|
|
286
|
+
|
|
287
|
+
const result = await client.sendChatMessage('hello');
|
|
288
|
+
expect(result).toBe(false);
|
|
289
|
+
|
|
290
|
+
await client.disconnect();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should return false on sendChatMessage when not connected', async () => {
|
|
294
|
+
// No server at all — connect should fail
|
|
295
|
+
const client = new OpenClawGatewayClient('test-token', 1);
|
|
296
|
+
|
|
297
|
+
const result = await client.sendChatMessage('hello');
|
|
298
|
+
expect(result).toBe(false);
|
|
299
|
+
|
|
300
|
+
await client.disconnect();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should reject pending RPCs on disconnect', async () => {
|
|
304
|
+
// Use a server that never responds to chat.send
|
|
305
|
+
server = new MockOpenClawServer();
|
|
306
|
+
// Override: don't respond to chat.send
|
|
307
|
+
const origWss = (server as unknown as { wss: WebSocketServer }).wss;
|
|
308
|
+
origWss.removeAllListeners('connection');
|
|
309
|
+
origWss.on('connection', (ws) => {
|
|
310
|
+
// Send challenge
|
|
311
|
+
ws.send(JSON.stringify({
|
|
312
|
+
type: 'event',
|
|
313
|
+
event: 'connect.challenge',
|
|
314
|
+
payload: { nonce: 'nonce-1', ts: Date.now() },
|
|
315
|
+
}));
|
|
316
|
+
ws.on('message', (data) => {
|
|
317
|
+
const msg = JSON.parse(data.toString()) as Record<string, unknown>;
|
|
318
|
+
if (msg.method === 'connect') {
|
|
319
|
+
ws.send(JSON.stringify({ type: 'res', id: 'connect-1', ok: true }));
|
|
320
|
+
}
|
|
321
|
+
// Deliberately don't respond to chat.send
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
326
|
+
await client.connect();
|
|
327
|
+
|
|
328
|
+
// Send a message that won't get a response
|
|
329
|
+
const chatPromise = client.sendChatMessage('will be rejected');
|
|
330
|
+
|
|
331
|
+
// Disconnect while the RPC is pending
|
|
332
|
+
await client.disconnect();
|
|
333
|
+
|
|
334
|
+
const result = await chatPromise;
|
|
335
|
+
expect(result).toBe(false);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should not reconnect after disconnect()', async () => {
|
|
339
|
+
server = new MockOpenClawServer();
|
|
340
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
341
|
+
await client.connect();
|
|
342
|
+
await client.disconnect();
|
|
343
|
+
|
|
344
|
+
// After disconnect, the stopped flag should prevent reconnection.
|
|
345
|
+
// Verify by checking that sendChatMessage returns false without hanging.
|
|
346
|
+
const result = await client.sendChatMessage('should fail');
|
|
347
|
+
expect(result).toBe(false);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should handle non-JSON messages gracefully', async () => {
|
|
351
|
+
server = new MockOpenClawServer({ sendChallenge: false });
|
|
352
|
+
const origWss = (server as unknown as { wss: WebSocketServer }).wss;
|
|
353
|
+
origWss.removeAllListeners('connection');
|
|
354
|
+
origWss.on('connection', (ws) => {
|
|
355
|
+
// Send garbage first, then a proper challenge
|
|
356
|
+
ws.send('not json at all');
|
|
357
|
+
ws.send(JSON.stringify({
|
|
358
|
+
type: 'event',
|
|
359
|
+
event: 'connect.challenge',
|
|
360
|
+
payload: { nonce: 'nonce-2', ts: Date.now() },
|
|
361
|
+
}));
|
|
362
|
+
ws.on('message', (data) => {
|
|
363
|
+
const msg = JSON.parse(data.toString()) as Record<string, unknown>;
|
|
364
|
+
if (msg.method === 'connect') {
|
|
365
|
+
ws.send(JSON.stringify({ type: 'res', id: 'connect-1', ok: true }));
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
371
|
+
await client.connect();
|
|
372
|
+
await client.disconnect();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should silently ignore unrecognized event messages', async () => {
|
|
376
|
+
server = new MockOpenClawServer();
|
|
377
|
+
const origWss = (server as unknown as { wss: WebSocketServer }).wss;
|
|
378
|
+
const origListeners = origWss.listeners('connection');
|
|
379
|
+
origWss.removeAllListeners('connection');
|
|
380
|
+
origWss.on('connection', (ws) => {
|
|
381
|
+
for (const listener of origListeners) {
|
|
382
|
+
(listener as (ws: WsType) => void)(ws);
|
|
383
|
+
}
|
|
384
|
+
// Send some random event after auth
|
|
385
|
+
setTimeout(() => {
|
|
386
|
+
ws.send(JSON.stringify({ type: 'event', event: 'chat.tick', payload: {} }));
|
|
387
|
+
}, 100);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const client = new OpenClawGatewayClient('test-token', server.port);
|
|
391
|
+
await client.connect();
|
|
392
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
393
|
+
await client.disconnect();
|
|
394
|
+
});
|
|
395
|
+
});
|
|
@@ -144,7 +144,7 @@ async function runStatus(): Promise<void> {
|
|
|
144
144
|
|
|
145
145
|
// Try to check connectivity
|
|
146
146
|
try {
|
|
147
|
-
const res = await fetch(`${config.baseUrl}/
|
|
147
|
+
const res = await fetch(`${config.baseUrl}/health`);
|
|
148
148
|
console.log(
|
|
149
149
|
`API connectivity: ${res.ok ? 'OK' : `Error (${res.status})`}`,
|
|
150
150
|
);
|