@febro28/aya-bridge 0.1.2 → 0.1.3
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 +2 -2
- package/src/bridge.js +68 -15
- package/test/run.js +694 -544
package/test/run.js
CHANGED
|
@@ -1,544 +1,694 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const assert = require("node:assert/strict");
|
|
4
|
-
const fs = require("node:fs/promises");
|
|
5
|
-
const os = require("node:os");
|
|
6
|
-
const path = require("node:path");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
status,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
console.error(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
yield Buffer.from(
|
|
41
|
-
|
|
42
|
-
);
|
|
43
|
-
yield Buffer.from("
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
assert.equal(events.
|
|
53
|
-
assert.equal(events[0].
|
|
54
|
-
assert.equal(events[
|
|
55
|
-
assert.equal(events[1].
|
|
56
|
-
assert.equal(events[1].
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
config.
|
|
66
|
-
config.openclaw.
|
|
67
|
-
config.openclaw.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
options.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
await bridge.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
assert.equal(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
assert.equal(
|
|
142
|
-
|
|
143
|
-
assert.equal(
|
|
144
|
-
assert.equal(
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
await bridge.
|
|
198
|
-
await bridge.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
await
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
markers.push("
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
await bridge.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("node:assert/strict");
|
|
4
|
+
const fs = require("node:fs/promises");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const { setTimeout: sleep } = require("node:timers/promises");
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
BridgeDaemon,
|
|
11
|
+
createLogger,
|
|
12
|
+
defaultConfig,
|
|
13
|
+
parseSSE
|
|
14
|
+
} = require("../src/bridge");
|
|
15
|
+
|
|
16
|
+
function jsonResponse(status, body) {
|
|
17
|
+
return {
|
|
18
|
+
ok: status >= 200 && status < 300,
|
|
19
|
+
status,
|
|
20
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
21
|
+
async text() {
|
|
22
|
+
return JSON.stringify(body);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function runTest(name, fn) {
|
|
28
|
+
try {
|
|
29
|
+
await fn();
|
|
30
|
+
console.log(`ok - ${name}`);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.error(`not ok - ${name}`);
|
|
33
|
+
console.error(err && err.stack ? err.stack : err);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function testParseSSE() {
|
|
39
|
+
async function* body() {
|
|
40
|
+
yield Buffer.from("event: stream.hello\n");
|
|
41
|
+
yield Buffer.from("data: {\"type\":\"stream.hello\",\"resume_status\":\"fresh\"}\n\n");
|
|
42
|
+
yield Buffer.from("id: dly_1\nevent: room.turn_ready\n");
|
|
43
|
+
yield Buffer.from("data: {\"delivery_id\":\"dly_1\",\"room_id\":\"room_1\"}\n\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const events = [];
|
|
47
|
+
for await (const event of parseSSE(body())) {
|
|
48
|
+
events.push(event);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
assert.equal(events.length, 2);
|
|
52
|
+
assert.equal(events[0].event, "stream.hello");
|
|
53
|
+
assert.equal(events[0].data.resume_status, "fresh");
|
|
54
|
+
assert.equal(events[1].id, "dly_1");
|
|
55
|
+
assert.equal(events[1].event, "room.turn_ready");
|
|
56
|
+
assert.equal(events[1].data.room_id, "room_1");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function testHandleTurnReady() {
|
|
60
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-"));
|
|
61
|
+
const markers = [];
|
|
62
|
+
let wakePayload = null;
|
|
63
|
+
const config = defaultConfig(tmpDir);
|
|
64
|
+
config.aya.api_base_url = "https://api.example.test";
|
|
65
|
+
config.openclaw.hook_url = "http://127.0.0.1:18789/hooks/agent";
|
|
66
|
+
config.openclaw.hook_token = "oc_hook_test";
|
|
67
|
+
config.openclaw.agent_id = "main";
|
|
68
|
+
|
|
69
|
+
const fetchImpl = async (input, options = {}) => {
|
|
70
|
+
const url = String(input);
|
|
71
|
+
if (url === "https://api.example.test/v1/rooms/room_1/access-token" && options.method === "POST") {
|
|
72
|
+
markers.push("token");
|
|
73
|
+
return jsonResponse(201, {
|
|
74
|
+
room_id: "room_1",
|
|
75
|
+
agent_id: "agt_test",
|
|
76
|
+
token: "rat_token_1",
|
|
77
|
+
scope: "room:automation",
|
|
78
|
+
expires_at: new Date(Date.now() + 300000).toISOString()
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (url === "https://api.example.test/v1/agent/stream/ack" && options.method === "POST") {
|
|
82
|
+
markers.push("ack");
|
|
83
|
+
return jsonResponse(200, { status: "acked" });
|
|
84
|
+
}
|
|
85
|
+
if (url === "http://127.0.0.1:18789/hooks/agent" && options.method === "POST") {
|
|
86
|
+
markers.push("wake");
|
|
87
|
+
wakePayload = JSON.parse(String(options.body || "{}"));
|
|
88
|
+
return jsonResponse(200, { ok: true });
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const bridge = new BridgeDaemon({
|
|
94
|
+
config,
|
|
95
|
+
fetchImpl,
|
|
96
|
+
logger: createLogger("error"),
|
|
97
|
+
session: {
|
|
98
|
+
api_key: "aya_api_test",
|
|
99
|
+
session_token: "as_test",
|
|
100
|
+
agent_id: "agt_test"
|
|
101
|
+
},
|
|
102
|
+
state: {
|
|
103
|
+
last_acknowledged_delivery_id: "",
|
|
104
|
+
last_connected_at: null,
|
|
105
|
+
last_stream_status: "idle"
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
await bridge.ensureLayout();
|
|
109
|
+
|
|
110
|
+
await bridge.handleTurnReady({
|
|
111
|
+
type: "room.turn_ready",
|
|
112
|
+
delivery_id: "dly_1",
|
|
113
|
+
room_id: "room_1",
|
|
114
|
+
next_turn: 0,
|
|
115
|
+
next_actor_id: "agt_test"
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await bridge.handleTurnReady({
|
|
119
|
+
type: "room.turn_ready",
|
|
120
|
+
delivery_id: "dly_1",
|
|
121
|
+
room_id: "room_1",
|
|
122
|
+
next_turn: 0,
|
|
123
|
+
next_actor_id: "agt_test"
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const tokenPath = path.join(bridge.paths.tokenDir, "room_1.json");
|
|
127
|
+
const token = JSON.parse(await fs.readFile(tokenPath, "utf8"));
|
|
128
|
+
assert.equal(token.token, "rat_token_1");
|
|
129
|
+
|
|
130
|
+
const wakeFiles = await fs.readdir(bridge.paths.wakeQueueDir);
|
|
131
|
+
assert.equal(wakeFiles.length, 0);
|
|
132
|
+
assert.deepEqual(markers, ["token", "ack", "wake", "ack"]);
|
|
133
|
+
assert.equal(wakePayload.agentId, "main");
|
|
134
|
+
assert.equal(wakePayload.name, "areyouai");
|
|
135
|
+
assert.ok(String(wakePayload.message || "").startsWith("[AYA_WAKE_V1]\n"));
|
|
136
|
+
const contract = JSON.parse(String(wakePayload.message).split("\n").slice(1).join("\n"));
|
|
137
|
+
assert.equal(contract.contract, "aya.wake.v1");
|
|
138
|
+
assert.equal(contract.delivery_id, "dly_1");
|
|
139
|
+
assert.equal(contract.room_id, "room_1");
|
|
140
|
+
assert.equal(contract.next_turn, 0);
|
|
141
|
+
assert.equal(contract.next_actor_id, "agt_test");
|
|
142
|
+
const contractTokenPath = String(contract.token_path || "");
|
|
143
|
+
assert.equal(path.basename(contractTokenPath), "room_1.json");
|
|
144
|
+
assert.equal(path.basename(path.dirname(contractTokenPath)), "tokens");
|
|
145
|
+
|
|
146
|
+
const state = JSON.parse(await fs.readFile(bridge.paths.statePath, "utf8"));
|
|
147
|
+
assert.equal(state.last_acknowledged_delivery_id, "dly_1");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function testWakeQueueRetry() {
|
|
151
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-retry-"));
|
|
152
|
+
let wakeAttempts = 0;
|
|
153
|
+
const markers = [];
|
|
154
|
+
const config = defaultConfig(tmpDir);
|
|
155
|
+
config.openclaw.hook_url = "http://127.0.0.1:18789/hooks/agent";
|
|
156
|
+
config.openclaw.hook_token = "oc_hook_test";
|
|
157
|
+
|
|
158
|
+
const fetchImpl = async (input, options = {}) => {
|
|
159
|
+
const url = String(input);
|
|
160
|
+
if (url === "http://127.0.0.1:18789/hooks/agent" && options.method === "POST") {
|
|
161
|
+
wakeAttempts += 1;
|
|
162
|
+
markers.push(`wake-${wakeAttempts}`);
|
|
163
|
+
if (wakeAttempts === 1) {
|
|
164
|
+
return jsonResponse(500, { error: "temporary" });
|
|
165
|
+
}
|
|
166
|
+
return jsonResponse(200, { ok: true });
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const bridge = new BridgeDaemon({
|
|
172
|
+
config,
|
|
173
|
+
fetchImpl,
|
|
174
|
+
logger: createLogger("error"),
|
|
175
|
+
session: {},
|
|
176
|
+
state: {
|
|
177
|
+
last_acknowledged_delivery_id: "dly_existing",
|
|
178
|
+
last_connected_at: null,
|
|
179
|
+
last_stream_status: "idle"
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
await bridge.ensureLayout();
|
|
183
|
+
await bridge.writeRoomToken("room_retry", {
|
|
184
|
+
room_id: "room_retry",
|
|
185
|
+
agent_id: "agt_test",
|
|
186
|
+
token: "rat_retry",
|
|
187
|
+
expires_at: new Date(Date.now() + 300000).toISOString(),
|
|
188
|
+
scope: "room:automation"
|
|
189
|
+
});
|
|
190
|
+
await bridge.enqueueWakeJob({
|
|
191
|
+
delivery_id: "dly_retry",
|
|
192
|
+
type: "room.turn_ready",
|
|
193
|
+
room_id: "room_retry",
|
|
194
|
+
received_at: new Date().toISOString()
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await bridge.drainWakeQueue();
|
|
198
|
+
let wakeFiles = await fs.readdir(bridge.paths.wakeQueueDir);
|
|
199
|
+
assert.equal(wakeFiles.length, 1);
|
|
200
|
+
|
|
201
|
+
await bridge.drainWakeQueue();
|
|
202
|
+
wakeFiles = await fs.readdir(bridge.paths.wakeQueueDir);
|
|
203
|
+
assert.equal(wakeFiles.length, 0);
|
|
204
|
+
assert.deepEqual(markers, ["wake-1", "wake-2"]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function testWakeRefreshesNearExpiryRoomToken() {
|
|
208
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-refresh-"));
|
|
209
|
+
const markers = [];
|
|
210
|
+
let wakePayload = null;
|
|
211
|
+
const config = defaultConfig(tmpDir);
|
|
212
|
+
config.aya.api_base_url = "https://api.example.test";
|
|
213
|
+
config.openclaw.hook_url = "http://127.0.0.1:18789/hooks/agent";
|
|
214
|
+
config.openclaw.hook_token = "oc_hook_test";
|
|
215
|
+
config.aya.token_refresh_threshold_seconds = 60;
|
|
216
|
+
|
|
217
|
+
const expiringSoon = new Date(Date.now() + 15_000).toISOString();
|
|
218
|
+
const refreshedExpiry = new Date(Date.now() + 300_000).toISOString();
|
|
219
|
+
|
|
220
|
+
const fetchImpl = async (input, options = {}) => {
|
|
221
|
+
const url = String(input);
|
|
222
|
+
if (url === "https://api.example.test/v1/rooms/room_refresh/access-token" && options.method === "POST") {
|
|
223
|
+
markers.push("refresh");
|
|
224
|
+
return jsonResponse(201, {
|
|
225
|
+
room_id: "room_refresh",
|
|
226
|
+
agent_id: "agt_test",
|
|
227
|
+
token: "rat_refreshed",
|
|
228
|
+
scope: "room:automation",
|
|
229
|
+
expires_at: refreshedExpiry
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (url === "http://127.0.0.1:18789/hooks/agent" && options.method === "POST") {
|
|
233
|
+
markers.push("wake");
|
|
234
|
+
wakePayload = JSON.parse(String(options.body || "{}"));
|
|
235
|
+
return jsonResponse(200, { ok: true });
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const bridge = new BridgeDaemon({
|
|
241
|
+
config,
|
|
242
|
+
fetchImpl,
|
|
243
|
+
logger: createLogger("error"),
|
|
244
|
+
session: {
|
|
245
|
+
api_key: "aya_api_test",
|
|
246
|
+
session_token: "as_test",
|
|
247
|
+
agent_id: "agt_test"
|
|
248
|
+
},
|
|
249
|
+
state: {
|
|
250
|
+
last_acknowledged_delivery_id: "",
|
|
251
|
+
last_connected_at: null,
|
|
252
|
+
last_stream_status: "idle"
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
await bridge.ensureLayout();
|
|
256
|
+
await bridge.writeRoomToken("room_refresh", {
|
|
257
|
+
room_id: "room_refresh",
|
|
258
|
+
agent_id: "agt_test",
|
|
259
|
+
token: "rat_old",
|
|
260
|
+
expires_at: expiringSoon,
|
|
261
|
+
scope: "room:automation"
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await bridge.wakeOpenClaw({
|
|
265
|
+
delivery_id: "dly_refresh",
|
|
266
|
+
type: "room.turn_ready",
|
|
267
|
+
room_id: "room_refresh",
|
|
268
|
+
next_turn: 2,
|
|
269
|
+
next_actor_id: "agt_test",
|
|
270
|
+
token_expires_at: expiringSoon
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
assert.deepEqual(markers, ["refresh", "wake"]);
|
|
274
|
+
assert.ok(wakePayload, "wake payload should be present");
|
|
275
|
+
const contract = JSON.parse(String(wakePayload.message).split("\n").slice(1).join("\n"));
|
|
276
|
+
assert.equal(contract.token_expires_at, refreshedExpiry);
|
|
277
|
+
|
|
278
|
+
const tokenPath = path.join(bridge.paths.tokenDir, "room_refresh.json");
|
|
279
|
+
const stored = JSON.parse(await fs.readFile(tokenPath, "utf8"));
|
|
280
|
+
assert.equal(stored.token, "rat_refreshed");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function testServeDowngradesExpectedDisconnectLogs() {
|
|
284
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-disconnect-"));
|
|
285
|
+
const logs = { info: [], warn: [] };
|
|
286
|
+
const config = defaultConfig(tmpDir);
|
|
287
|
+
config.aya.api_base_url = "https://api.example.test";
|
|
288
|
+
config.aya.reconnect.base_delay_ms = 1;
|
|
289
|
+
config.aya.reconnect.max_delay_ms = 1;
|
|
290
|
+
config.aya.reconnect.jitter_ms = 0;
|
|
291
|
+
|
|
292
|
+
const fetchImpl = async (input, options = {}) => {
|
|
293
|
+
const url = String(input);
|
|
294
|
+
if (url === "https://api.example.test/v1/agent/stream" && options.method === "GET") {
|
|
295
|
+
async function* body() {
|
|
296
|
+
yield Buffer.from("event: stream.hello\n");
|
|
297
|
+
yield Buffer.from("data: {\"type\":\"stream.hello\",\"resume_status\":\"fresh\"}\n\n");
|
|
298
|
+
throw new Error("terminated");
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
ok: true,
|
|
302
|
+
status: 200,
|
|
303
|
+
headers: new Map([["content-type", "text/event-stream"]]),
|
|
304
|
+
body: body(),
|
|
305
|
+
async text() {
|
|
306
|
+
return "";
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const bridge = new BridgeDaemon({
|
|
314
|
+
config,
|
|
315
|
+
fetchImpl,
|
|
316
|
+
logger: {
|
|
317
|
+
debug: (...args) => logs.info.push(args.join(" ")),
|
|
318
|
+
info: (...args) => logs.info.push(args.join(" ")),
|
|
319
|
+
warn: (...args) => logs.warn.push(args.join(" ")),
|
|
320
|
+
error: (...args) => logs.warn.push(args.join(" "))
|
|
321
|
+
},
|
|
322
|
+
session: {
|
|
323
|
+
api_key: "aya_api_test",
|
|
324
|
+
session_token: "as_test",
|
|
325
|
+
agent_id: "agt_test"
|
|
326
|
+
},
|
|
327
|
+
state: {
|
|
328
|
+
last_acknowledged_delivery_id: "",
|
|
329
|
+
last_connected_at: null,
|
|
330
|
+
last_stream_status: "idle"
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
await bridge.ensureLayout();
|
|
334
|
+
|
|
335
|
+
const controller = new AbortController();
|
|
336
|
+
const servePromise = bridge.serve(controller.signal);
|
|
337
|
+
await sleep(20);
|
|
338
|
+
controller.abort();
|
|
339
|
+
await servePromise;
|
|
340
|
+
|
|
341
|
+
assert.ok(logs.info.some((line) => line.includes("stream disconnected error=terminated")));
|
|
342
|
+
assert.ok(!logs.warn.some((line) => line.includes("stream disconnected error=terminated")));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function testWakeEmitsTypingPulse() {
|
|
346
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-typing-"));
|
|
347
|
+
const markers = [];
|
|
348
|
+
let hookPayload = null;
|
|
349
|
+
const config = defaultConfig(tmpDir);
|
|
350
|
+
config.aya.api_base_url = "https://api.example.test";
|
|
351
|
+
config.openclaw.hook_url = "http://127.0.0.1:18789/hooks/agent";
|
|
352
|
+
config.openclaw.hook_token = "oc_hook_test";
|
|
353
|
+
|
|
354
|
+
const fetchImpl = async (input, options = {}) => {
|
|
355
|
+
const url = String(input);
|
|
356
|
+
if (url === "https://api.example.test/v1/rooms/room_typing/access-token" && options.method === "POST") {
|
|
357
|
+
markers.push("token");
|
|
358
|
+
return jsonResponse(201, {
|
|
359
|
+
room_id: "room_typing",
|
|
360
|
+
agent_id: "agt_test",
|
|
361
|
+
token: "rat_typing",
|
|
362
|
+
scope: "room:automation",
|
|
363
|
+
expires_at: new Date(Date.now() + 300000).toISOString()
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (url === "https://api.example.test/v1/rooms/room_typing/typing" && options.method === "POST") {
|
|
367
|
+
const payload = JSON.parse(String(options.body || "{}"));
|
|
368
|
+
markers.push(payload.state === "start" ? "typing-start" : "typing-stop");
|
|
369
|
+
assert.equal(String(options.headers?.Authorization || ""), "Bearer rat_typing");
|
|
370
|
+
if (payload.state === "start") {
|
|
371
|
+
assert.equal(payload.ttl_ms, 30000);
|
|
372
|
+
}
|
|
373
|
+
return jsonResponse(200, { ok: true });
|
|
374
|
+
}
|
|
375
|
+
if (url === "http://127.0.0.1:18789/hooks/agent" && options.method === "POST") {
|
|
376
|
+
markers.push("wake");
|
|
377
|
+
hookPayload = JSON.parse(String(options.body || "{}"));
|
|
378
|
+
return jsonResponse(200, { ok: true });
|
|
379
|
+
}
|
|
380
|
+
throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const bridge = new BridgeDaemon({
|
|
384
|
+
config,
|
|
385
|
+
fetchImpl,
|
|
386
|
+
logger: createLogger("error"),
|
|
387
|
+
session: {
|
|
388
|
+
api_key: "aya_api_test",
|
|
389
|
+
session_token: "as_test",
|
|
390
|
+
agent_id: "agt_test"
|
|
391
|
+
},
|
|
392
|
+
state: {
|
|
393
|
+
last_acknowledged_delivery_id: "",
|
|
394
|
+
last_connected_at: null,
|
|
395
|
+
last_stream_status: "idle"
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
await bridge.ensureLayout();
|
|
399
|
+
|
|
400
|
+
await bridge.wakeOpenClaw({
|
|
401
|
+
delivery_id: "dly_typing",
|
|
402
|
+
type: "room.turn_ready",
|
|
403
|
+
room_id: "room_typing",
|
|
404
|
+
next_turn: 4,
|
|
405
|
+
next_actor_id: "agt_test"
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
assert.deepEqual(markers, ["token", "typing-start", "wake", "typing-stop"]);
|
|
409
|
+
assert.ok(hookPayload, "wake payload should be present");
|
|
410
|
+
const contract = JSON.parse(String(hookPayload.message).split("\n").slice(1).join("\n"));
|
|
411
|
+
assert.equal(contract.room_id, "room_typing");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function testEnqueueWakeJobDedupesEquivalentTurnReady() {
|
|
415
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-dedupe-"));
|
|
416
|
+
const bridge = new BridgeDaemon({
|
|
417
|
+
config: defaultConfig(tmpDir),
|
|
418
|
+
logger: createLogger("error"),
|
|
419
|
+
session: {},
|
|
420
|
+
state: {}
|
|
421
|
+
});
|
|
422
|
+
await bridge.ensureLayout();
|
|
423
|
+
|
|
424
|
+
await bridge.enqueueWakeJob({
|
|
425
|
+
delivery_id: "dly_one",
|
|
426
|
+
type: "room.turn_ready",
|
|
427
|
+
room_id: "room_dedupe",
|
|
428
|
+
next_turn: 5,
|
|
429
|
+
next_actor_id: "agt_x",
|
|
430
|
+
received_at: new Date().toISOString()
|
|
431
|
+
});
|
|
432
|
+
await bridge.enqueueWakeJob({
|
|
433
|
+
delivery_id: "dly_two",
|
|
434
|
+
type: "room.turn_ready",
|
|
435
|
+
room_id: "room_dedupe",
|
|
436
|
+
next_turn: 5,
|
|
437
|
+
next_actor_id: "agt_x",
|
|
438
|
+
received_at: new Date().toISOString()
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const files = await fs.readdir(bridge.paths.wakeQueueDir);
|
|
442
|
+
assert.equal(files.length, 1);
|
|
443
|
+
assert.ok(files[0].includes("dly_one"), `expected first delivery file, got ${files[0]}`);
|
|
444
|
+
|
|
445
|
+
bridge.rememberCompletedWakeKey("room_dedupe|5|agt_x");
|
|
446
|
+
await bridge.saveState();
|
|
447
|
+
const skipped = await bridge.enqueueWakeJob({
|
|
448
|
+
delivery_id: "dly_three",
|
|
449
|
+
type: "room.turn_ready",
|
|
450
|
+
room_id: "room_dedupe",
|
|
451
|
+
next_turn: 5,
|
|
452
|
+
next_actor_id: "agt_x",
|
|
453
|
+
received_at: new Date().toISOString()
|
|
454
|
+
});
|
|
455
|
+
assert.equal(skipped, "");
|
|
456
|
+
const filesAfter = await fs.readdir(bridge.paths.wakeQueueDir);
|
|
457
|
+
assert.equal(filesAfter.length, 1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function testProcessRecoveryClearsTerminalWakeJobs() {
|
|
461
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-recovery-"));
|
|
462
|
+
const markers = [];
|
|
463
|
+
const config = defaultConfig(tmpDir);
|
|
464
|
+
config.aya.api_base_url = "https://api.example.test";
|
|
465
|
+
|
|
466
|
+
const fetchImpl = async (input, options = {}) => {
|
|
467
|
+
const url = String(input);
|
|
468
|
+
if (url === "https://api.example.test/v1/agent/actionable-rooms") {
|
|
469
|
+
markers.push("recovery");
|
|
470
|
+
return jsonResponse(200, {
|
|
471
|
+
actionable: [],
|
|
472
|
+
terminal: [
|
|
473
|
+
{
|
|
474
|
+
room_id: "room_terminal",
|
|
475
|
+
room_state: "CLOSED"
|
|
476
|
+
}
|
|
477
|
+
]
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const bridge = new BridgeDaemon({
|
|
484
|
+
config,
|
|
485
|
+
fetchImpl,
|
|
486
|
+
logger: createLogger("error"),
|
|
487
|
+
session: {
|
|
488
|
+
api_key: "aya_api_test",
|
|
489
|
+
session_token: "as_test",
|
|
490
|
+
agent_id: "agt_test"
|
|
491
|
+
},
|
|
492
|
+
state: {
|
|
493
|
+
last_acknowledged_delivery_id: "dly_old",
|
|
494
|
+
last_connected_at: null,
|
|
495
|
+
last_stream_status: "connected"
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
await bridge.ensureLayout();
|
|
499
|
+
await bridge.writeRoomToken("room_terminal", {
|
|
500
|
+
room_id: "room_terminal",
|
|
501
|
+
agent_id: "agt_test",
|
|
502
|
+
token: "rat_old",
|
|
503
|
+
expires_at: new Date(Date.now() + 300000).toISOString(),
|
|
504
|
+
scope: "room:automation"
|
|
505
|
+
});
|
|
506
|
+
bridge.rememberCompletedWakeKey("room_terminal|9|agt_test");
|
|
507
|
+
await bridge.saveState();
|
|
508
|
+
await bridge.enqueueWakeJob({
|
|
509
|
+
delivery_id: "dly_terminal",
|
|
510
|
+
type: "room.turn_ready",
|
|
511
|
+
room_id: "room_terminal",
|
|
512
|
+
next_turn: 9,
|
|
513
|
+
next_actor_id: "agt_test",
|
|
514
|
+
received_at: new Date().toISOString()
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
await bridge.processRecovery();
|
|
518
|
+
|
|
519
|
+
assert.deepEqual(markers, ["recovery"]);
|
|
520
|
+
assert.equal(bridge.state.last_acknowledged_delivery_id, "");
|
|
521
|
+
const tokenPath = path.join(bridge.paths.tokenDir, "room_terminal.json");
|
|
522
|
+
const tokenExists = await fs.access(tokenPath).then(() => true).catch(() => false);
|
|
523
|
+
assert.equal(tokenExists, false);
|
|
524
|
+
const wakeFiles = await fs.readdir(bridge.paths.wakeQueueDir);
|
|
525
|
+
assert.equal(wakeFiles.length, 0);
|
|
526
|
+
const state = JSON.parse(await fs.readFile(bridge.paths.statePath, "utf8"));
|
|
527
|
+
assert.ok(!state.completed_wake_keys.includes("room_terminal|9|agt_test"));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function testProcessRecoveryRetainsCursorOnFailure() {
|
|
531
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-recovery-fail-"));
|
|
532
|
+
const markers = [];
|
|
533
|
+
let streamURL = null;
|
|
534
|
+
const config = defaultConfig(tmpDir);
|
|
535
|
+
config.aya.api_base_url = "https://api.example.test";
|
|
536
|
+
const fetchImpl = async (input, options = {}) => {
|
|
537
|
+
const url = String(input);
|
|
538
|
+
if (url === "https://api.example.test/v1/agent/actionable-rooms") {
|
|
539
|
+
markers.push("recovery-fail");
|
|
540
|
+
return jsonResponse(500, { error: "temporary" });
|
|
541
|
+
}
|
|
542
|
+
if (url.startsWith("https://api.example.test/v1/agent/stream") && options.method === "GET") {
|
|
543
|
+
streamURL = url;
|
|
544
|
+
return {
|
|
545
|
+
ok: true,
|
|
546
|
+
status: 200,
|
|
547
|
+
headers: new Map([[
|
|
548
|
+
"content-type",
|
|
549
|
+
"text/event-stream"
|
|
550
|
+
]]),
|
|
551
|
+
body: {
|
|
552
|
+
async getReader() {
|
|
553
|
+
return {
|
|
554
|
+
async read() {
|
|
555
|
+
return { done: true, value: undefined };
|
|
556
|
+
},
|
|
557
|
+
releaseLock() {}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
async text() {
|
|
562
|
+
return "";
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const bridge = new BridgeDaemon({
|
|
570
|
+
config,
|
|
571
|
+
fetchImpl,
|
|
572
|
+
logger: createLogger("error"),
|
|
573
|
+
session: {
|
|
574
|
+
api_key: "aya_api_test",
|
|
575
|
+
session_token: "as_test",
|
|
576
|
+
agent_id: "agt_test"
|
|
577
|
+
},
|
|
578
|
+
state: {
|
|
579
|
+
last_acknowledged_delivery_id: "dly_old",
|
|
580
|
+
last_connected_at: null,
|
|
581
|
+
last_stream_status: "connected"
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
await bridge.ensureLayout();
|
|
585
|
+
|
|
586
|
+
await assert.rejects(() => bridge.processRecovery(), /500/);
|
|
587
|
+
assert.deepEqual(markers, ["recovery-fail"]);
|
|
588
|
+
assert.equal(bridge.state.last_acknowledged_delivery_id, "dly_old");
|
|
589
|
+
|
|
590
|
+
await bridge.openStream(new AbortController().signal);
|
|
591
|
+
assert.ok(streamURL, "stream URL should be captured");
|
|
592
|
+
assert.ok(streamURL.includes("last_delivery_id=dly_old"), streamURL);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function testHandleTerminalClearsPendingWakeJobs() {
|
|
596
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-terminal-"));
|
|
597
|
+
const markers = [];
|
|
598
|
+
const fetchImpl = async (input, options = {}) => {
|
|
599
|
+
const url = String(input);
|
|
600
|
+
if (url === "https://api.example.test/v1/agent/stream/ack" && options.method === "POST") {
|
|
601
|
+
markers.push("ack");
|
|
602
|
+
return jsonResponse(200, { status: "acked" });
|
|
603
|
+
}
|
|
604
|
+
throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
|
|
605
|
+
};
|
|
606
|
+
const config = defaultConfig(tmpDir);
|
|
607
|
+
config.aya.api_base_url = "https://api.example.test";
|
|
608
|
+
const bridge = new BridgeDaemon({
|
|
609
|
+
config,
|
|
610
|
+
fetchImpl,
|
|
611
|
+
logger: createLogger("error"),
|
|
612
|
+
session: {
|
|
613
|
+
api_key: "aya_api_test",
|
|
614
|
+
session_token: "as_test",
|
|
615
|
+
agent_id: "agt_test"
|
|
616
|
+
},
|
|
617
|
+
state: {
|
|
618
|
+
last_acknowledged_delivery_id: "",
|
|
619
|
+
last_connected_at: null,
|
|
620
|
+
last_stream_status: "idle",
|
|
621
|
+
completed_wake_keys: []
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
await bridge.ensureLayout();
|
|
625
|
+
await bridge.writeRoomToken("room_terminal", {
|
|
626
|
+
room_id: "room_terminal",
|
|
627
|
+
agent_id: "agt_test",
|
|
628
|
+
token: "rat_old",
|
|
629
|
+
expires_at: new Date(Date.now() + 300000).toISOString(),
|
|
630
|
+
scope: "room:automation"
|
|
631
|
+
});
|
|
632
|
+
await bridge.enqueueWakeJob({
|
|
633
|
+
delivery_id: "dly_pending",
|
|
634
|
+
type: "room.turn_ready",
|
|
635
|
+
room_id: "room_terminal",
|
|
636
|
+
next_turn: 9,
|
|
637
|
+
next_actor_id: "agt_test",
|
|
638
|
+
received_at: new Date().toISOString()
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
await bridge.handleTerminal({
|
|
642
|
+
type: "room.closed",
|
|
643
|
+
delivery_id: "dly_closed",
|
|
644
|
+
room_id: "room_terminal"
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const tokenExists = await fs.access(path.join(bridge.paths.tokenDir, "room_terminal.json")).then(() => true).catch(() => false);
|
|
648
|
+
assert.equal(tokenExists, false);
|
|
649
|
+
const wakeFiles = await fs.readdir(bridge.paths.wakeQueueDir);
|
|
650
|
+
assert.equal(wakeFiles.length, 0);
|
|
651
|
+
assert.deepEqual(markers, ["ack"]);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function main() {
|
|
655
|
+
const mode = String(process.argv[2] || "all").trim();
|
|
656
|
+
if (mode === "parse" || mode === "all") {
|
|
657
|
+
await runTest("parseSSE yields event payloads", testParseSSE);
|
|
658
|
+
}
|
|
659
|
+
if (mode === "turn" || mode === "all") {
|
|
660
|
+
await runTest("handleTurnReady dedupes duplicate turn_ready deliveries", testHandleTurnReady);
|
|
661
|
+
}
|
|
662
|
+
if (mode === "retry" || mode === "all") {
|
|
663
|
+
await runTest("wake queue retries pending jobs", testWakeQueueRetry);
|
|
664
|
+
}
|
|
665
|
+
if (mode === "refresh" || mode === "all") {
|
|
666
|
+
await runTest("wake refreshes near-expiry room token", testWakeRefreshesNearExpiryRoomToken);
|
|
667
|
+
}
|
|
668
|
+
if (mode === "disconnect" || mode === "all") {
|
|
669
|
+
await runTest("serve downgrades expected disconnect logs", testServeDowngradesExpectedDisconnectLogs);
|
|
670
|
+
}
|
|
671
|
+
if (mode === "typing" || mode === "all") {
|
|
672
|
+
await runTest("wake emits typing pulse around hook execution", testWakeEmitsTypingPulse);
|
|
673
|
+
}
|
|
674
|
+
if (mode === "dedupe" || mode === "all") {
|
|
675
|
+
await runTest("enqueueWakeJob dedupes equivalent room.turn_ready jobs", testEnqueueWakeJobDedupesEquivalentTurnReady);
|
|
676
|
+
}
|
|
677
|
+
if (mode === "recovery" || mode === "all") {
|
|
678
|
+
await runTest("processRecovery clears terminal room wake jobs", testProcessRecoveryClearsTerminalWakeJobs);
|
|
679
|
+
}
|
|
680
|
+
if (mode === "recovery-failure" || mode === "all") {
|
|
681
|
+
await runTest("processRecovery retains cursor on failure", testProcessRecoveryRetainsCursorOnFailure);
|
|
682
|
+
}
|
|
683
|
+
if (mode === "terminal" || mode === "all") {
|
|
684
|
+
await runTest("handleTerminal clears pending wake jobs", testHandleTerminalClearsPendingWakeJobs);
|
|
685
|
+
}
|
|
686
|
+
if (process.exitCode) {
|
|
687
|
+
process.exit(process.exitCode);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
main().catch((err) => {
|
|
692
|
+
console.error(err && err.stack ? err.stack : err);
|
|
693
|
+
process.exit(1);
|
|
694
|
+
});
|