@febro28/aya-bridge 0.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/test/run.js ADDED
@@ -0,0 +1,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
+
8
+ const {
9
+ BridgeDaemon,
10
+ createLogger,
11
+ defaultConfig,
12
+ parseSSE,
13
+ } = require("../src/bridge");
14
+
15
+ function jsonResponse(status, body) {
16
+ return {
17
+ ok: status >= 200 && status < 300,
18
+ status,
19
+ headers: new Map([["content-type", "application/json"]]),
20
+ async text() {
21
+ return JSON.stringify(body);
22
+ },
23
+ };
24
+ }
25
+
26
+ async function runTest(name, fn) {
27
+ try {
28
+ await fn();
29
+ console.log(`ok - ${name}`);
30
+ } catch (err) {
31
+ console.error(`not ok - ${name}`);
32
+ console.error(err && err.stack ? err.stack : err);
33
+ process.exitCode = 1;
34
+ }
35
+ }
36
+
37
+ async function testParseSSE() {
38
+ async function* body() {
39
+ yield Buffer.from("event: stream.hello\n");
40
+ yield Buffer.from(
41
+ 'data: {"type":"stream.hello","resume_status":"fresh"}\n\n',
42
+ );
43
+ yield Buffer.from("id: dly_1\nevent: room.turn_ready\n");
44
+ yield Buffer.from('data: {"delivery_id":"dly_1","room_id":"room_1"}\n\n');
45
+ }
46
+
47
+ const events = [];
48
+ for await (const event of parseSSE(body())) {
49
+ events.push(event);
50
+ }
51
+
52
+ assert.equal(events.length, 2);
53
+ assert.equal(events[0].event, "stream.hello");
54
+ assert.equal(events[0].data.resume_status, "fresh");
55
+ assert.equal(events[1].id, "dly_1");
56
+ assert.equal(events[1].event, "room.turn_ready");
57
+ assert.equal(events[1].data.room_id, "room_1");
58
+ }
59
+
60
+ async function testHandleTurnReady() {
61
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-"));
62
+ const markers = [];
63
+ let wakePayload = null;
64
+ const config = defaultConfig(tmpDir);
65
+ config.aya.api_base_url = "https://api.example.test";
66
+ config.openclaw.hook_url = "http://127.0.0.1:18789/hooks/agent";
67
+ config.openclaw.hook_token = "oc_hook_test";
68
+ config.openclaw.agent_id = "main";
69
+
70
+ const fetchImpl = async (input, options = {}) => {
71
+ const url = String(input);
72
+ if (
73
+ url === "https://api.example.test/v1/rooms/room_1/access-token" &&
74
+ options.method === "POST"
75
+ ) {
76
+ markers.push("token");
77
+ return jsonResponse(201, {
78
+ room_id: "room_1",
79
+ agent_id: "agt_test",
80
+ token: "rat_token_1",
81
+ scope: "room:automation",
82
+ expires_at: new Date(Date.now() + 300000).toISOString(),
83
+ });
84
+ }
85
+ if (
86
+ url === "https://api.example.test/v1/agent/stream/ack" &&
87
+ options.method === "POST"
88
+ ) {
89
+ markers.push("ack");
90
+ return jsonResponse(200, { status: "acked" });
91
+ }
92
+ if (
93
+ url === "http://127.0.0.1:18789/hooks/agent" &&
94
+ options.method === "POST"
95
+ ) {
96
+ markers.push("wake");
97
+ wakePayload = JSON.parse(String(options.body || "{}"));
98
+ return jsonResponse(200, { ok: true });
99
+ }
100
+ throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
101
+ };
102
+
103
+ const bridge = new BridgeDaemon({
104
+ config,
105
+ fetchImpl,
106
+ logger: createLogger("error"),
107
+ session: {
108
+ api_key: "aya_api_test",
109
+ session_token: "as_test",
110
+ agent_id: "agt_test",
111
+ },
112
+ state: {
113
+ last_acknowledged_delivery_id: "",
114
+ last_connected_at: null,
115
+ last_stream_status: "idle",
116
+ },
117
+ });
118
+ await bridge.ensureLayout();
119
+
120
+ await bridge.handleTurnReady({
121
+ type: "room.turn_ready",
122
+ delivery_id: "dly_1",
123
+ room_id: "room_1",
124
+ next_turn: 0,
125
+ next_actor_id: "agt_test",
126
+ });
127
+
128
+ await bridge.handleTurnReady({
129
+ type: "room.turn_ready",
130
+ delivery_id: "dly_1",
131
+ room_id: "room_1",
132
+ next_turn: 0,
133
+ next_actor_id: "agt_test",
134
+ });
135
+
136
+ const tokenPath = path.join(bridge.paths.tokenDir, "room_1.json");
137
+ const token = JSON.parse(await fs.readFile(tokenPath, "utf8"));
138
+ assert.equal(token.token, "rat_token_1");
139
+
140
+ const wakeFiles = await fs.readdir(bridge.paths.wakeQueueDir);
141
+ assert.equal(wakeFiles.length, 0);
142
+ assert.deepEqual(markers, ["token", "ack", "wake", "ack"]);
143
+ assert.equal(wakePayload.agentId, "main");
144
+ assert.equal(wakePayload.name, "areyouai");
145
+ assert.ok(String(wakePayload.message || "").startsWith("[AYA_WAKE_V1]\n"));
146
+ const contract = JSON.parse(
147
+ String(wakePayload.message).split("\n").slice(1).join("\n"),
148
+ );
149
+ assert.equal(contract.contract, "aya.wake.v1");
150
+ assert.equal(contract.delivery_id, "dly_1");
151
+ assert.equal(contract.room_id, "room_1");
152
+ assert.equal(contract.next_turn, 0);
153
+ assert.equal(contract.next_actor_id, "agt_test");
154
+ const contractTokenPath = String(contract.token_path || "");
155
+ assert.equal(path.basename(contractTokenPath), "room_1.json");
156
+ assert.equal(path.basename(path.dirname(contractTokenPath)), "tokens");
157
+
158
+ const state = JSON.parse(await fs.readFile(bridge.paths.statePath, "utf8"));
159
+ assert.equal(state.last_acknowledged_delivery_id, "dly_1");
160
+ }
161
+
162
+ async function testWakeQueueRetry() {
163
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-retry-"));
164
+ let wakeAttempts = 0;
165
+ const markers = [];
166
+ const config = defaultConfig(tmpDir);
167
+ config.openclaw.hook_url = "http://127.0.0.1:18789/hooks/agent";
168
+ config.openclaw.hook_token = "oc_hook_test";
169
+
170
+ const fetchImpl = async (input, options = {}) => {
171
+ const url = String(input);
172
+ if (
173
+ url === "http://127.0.0.1:18789/hooks/agent" &&
174
+ options.method === "POST"
175
+ ) {
176
+ wakeAttempts += 1;
177
+ markers.push(`wake-${wakeAttempts}`);
178
+ if (wakeAttempts === 1) {
179
+ return jsonResponse(500, { error: "temporary" });
180
+ }
181
+ return jsonResponse(200, { ok: true });
182
+ }
183
+ throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
184
+ };
185
+
186
+ const bridge = new BridgeDaemon({
187
+ config,
188
+ fetchImpl,
189
+ logger: createLogger("error"),
190
+ session: {},
191
+ state: {
192
+ last_acknowledged_delivery_id: "dly_existing",
193
+ last_connected_at: null,
194
+ last_stream_status: "idle",
195
+ },
196
+ });
197
+ await bridge.ensureLayout();
198
+ await bridge.writeRoomToken("room_retry", {
199
+ room_id: "room_retry",
200
+ agent_id: "agt_test",
201
+ token: "rat_retry",
202
+ expires_at: new Date(Date.now() + 300000).toISOString(),
203
+ scope: "room:automation",
204
+ });
205
+ await bridge.enqueueWakeJob({
206
+ delivery_id: "dly_retry",
207
+ type: "room.turn_ready",
208
+ room_id: "room_retry",
209
+ received_at: new Date().toISOString(),
210
+ });
211
+
212
+ await bridge.drainWakeQueue();
213
+ let wakeFiles = await fs.readdir(bridge.paths.wakeQueueDir);
214
+ assert.equal(wakeFiles.length, 1);
215
+
216
+ await bridge.drainWakeQueue();
217
+ wakeFiles = await fs.readdir(bridge.paths.wakeQueueDir);
218
+ assert.equal(wakeFiles.length, 0);
219
+ assert.deepEqual(markers, ["wake-1", "wake-2"]);
220
+ }
221
+
222
+ async function testWakeRefreshesNearExpiryRoomToken() {
223
+ const tmpDir = await fs.mkdtemp(
224
+ path.join(os.tmpdir(), "aya-bridge-refresh-"),
225
+ );
226
+ const markers = [];
227
+ let wakePayload = null;
228
+ const config = defaultConfig(tmpDir);
229
+ config.aya.api_base_url = "https://api.example.test";
230
+ config.openclaw.hook_url = "http://127.0.0.1:18789/hooks/agent";
231
+ config.openclaw.hook_token = "oc_hook_test";
232
+ config.aya.token_refresh_threshold_seconds = 60;
233
+
234
+ const expiringSoon = new Date(Date.now() + 15_000).toISOString();
235
+ const refreshedExpiry = new Date(Date.now() + 300_000).toISOString();
236
+
237
+ const fetchImpl = async (input, options = {}) => {
238
+ const url = String(input);
239
+ if (
240
+ url === "https://api.example.test/v1/rooms/room_refresh/access-token" &&
241
+ options.method === "POST"
242
+ ) {
243
+ markers.push("refresh");
244
+ return jsonResponse(201, {
245
+ room_id: "room_refresh",
246
+ agent_id: "agt_test",
247
+ token: "rat_refreshed",
248
+ scope: "room:automation",
249
+ expires_at: refreshedExpiry,
250
+ });
251
+ }
252
+ if (
253
+ url === "http://127.0.0.1:18789/hooks/agent" &&
254
+ options.method === "POST"
255
+ ) {
256
+ markers.push("wake");
257
+ wakePayload = JSON.parse(String(options.body || "{}"));
258
+ return jsonResponse(200, { ok: true });
259
+ }
260
+ throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
261
+ };
262
+
263
+ const bridge = new BridgeDaemon({
264
+ config,
265
+ fetchImpl,
266
+ logger: createLogger("error"),
267
+ session: {
268
+ api_key: "aya_api_test",
269
+ session_token: "as_test",
270
+ agent_id: "agt_test",
271
+ },
272
+ state: {
273
+ last_acknowledged_delivery_id: "",
274
+ last_connected_at: null,
275
+ last_stream_status: "idle",
276
+ },
277
+ });
278
+ await bridge.ensureLayout();
279
+ await bridge.writeRoomToken("room_refresh", {
280
+ room_id: "room_refresh",
281
+ agent_id: "agt_test",
282
+ token: "rat_old",
283
+ expires_at: expiringSoon,
284
+ scope: "room:automation",
285
+ });
286
+
287
+ await bridge.wakeOpenClaw({
288
+ delivery_id: "dly_refresh",
289
+ type: "room.turn_ready",
290
+ room_id: "room_refresh",
291
+ next_turn: 2,
292
+ next_actor_id: "agt_test",
293
+ token_expires_at: expiringSoon,
294
+ });
295
+
296
+ assert.deepEqual(markers, ["refresh", "wake"]);
297
+ assert.ok(wakePayload, "wake payload should be present");
298
+ const contract = JSON.parse(
299
+ String(wakePayload.message).split("\n").slice(1).join("\n"),
300
+ );
301
+ assert.equal(contract.token_expires_at, refreshedExpiry);
302
+
303
+ const tokenPath = path.join(bridge.paths.tokenDir, "room_refresh.json");
304
+ const stored = JSON.parse(await fs.readFile(tokenPath, "utf8"));
305
+ assert.equal(stored.token, "rat_refreshed");
306
+ }
307
+
308
+ async function testEnqueueWakeJobDedupesEquivalentTurnReady() {
309
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "aya-bridge-dedupe-"));
310
+ const bridge = new BridgeDaemon({
311
+ config: defaultConfig(tmpDir),
312
+ logger: createLogger("error"),
313
+ session: {},
314
+ state: {},
315
+ });
316
+ await bridge.ensureLayout();
317
+
318
+ await bridge.enqueueWakeJob({
319
+ delivery_id: "dly_one",
320
+ type: "room.turn_ready",
321
+ room_id: "room_dedupe",
322
+ next_turn: 5,
323
+ next_actor_id: "agt_x",
324
+ received_at: new Date().toISOString(),
325
+ });
326
+ await bridge.enqueueWakeJob({
327
+ delivery_id: "dly_two",
328
+ type: "room.turn_ready",
329
+ room_id: "room_dedupe",
330
+ next_turn: 5,
331
+ next_actor_id: "agt_x",
332
+ received_at: new Date().toISOString(),
333
+ });
334
+
335
+ const files = await fs.readdir(bridge.paths.wakeQueueDir);
336
+ assert.equal(files.length, 1);
337
+ assert.ok(
338
+ files[0].includes("dly_one"),
339
+ `expected first delivery file, got ${files[0]}`,
340
+ );
341
+
342
+ bridge.rememberCompletedWakeKey("room_dedupe|5|agt_x");
343
+ await bridge.saveState();
344
+ const skipped = await bridge.enqueueWakeJob({
345
+ delivery_id: "dly_three",
346
+ type: "room.turn_ready",
347
+ room_id: "room_dedupe",
348
+ next_turn: 5,
349
+ next_actor_id: "agt_x",
350
+ received_at: new Date().toISOString(),
351
+ });
352
+ assert.equal(skipped, "");
353
+ const filesAfter = await fs.readdir(bridge.paths.wakeQueueDir);
354
+ assert.equal(filesAfter.length, 1);
355
+ }
356
+
357
+ async function testProcessRecoveryClearsTerminalWakeJobs() {
358
+ const tmpDir = await fs.mkdtemp(
359
+ path.join(os.tmpdir(), "aya-bridge-recovery-"),
360
+ );
361
+ const markers = [];
362
+ const config = defaultConfig(tmpDir);
363
+ config.aya.api_base_url = "https://api.example.test";
364
+
365
+ const fetchImpl = async (input, options = {}) => {
366
+ const url = String(input);
367
+ if (url === "https://api.example.test/v1/agent/actionable-rooms") {
368
+ markers.push("recovery");
369
+ return jsonResponse(200, {
370
+ actionable: [],
371
+ terminal: [
372
+ {
373
+ room_id: "room_terminal",
374
+ room_state: "CLOSED",
375
+ },
376
+ ],
377
+ });
378
+ }
379
+ throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
380
+ };
381
+
382
+ const bridge = new BridgeDaemon({
383
+ config,
384
+ fetchImpl,
385
+ logger: createLogger("error"),
386
+ session: {
387
+ api_key: "aya_api_test",
388
+ session_token: "as_test",
389
+ agent_id: "agt_test",
390
+ },
391
+ state: {
392
+ last_acknowledged_delivery_id: "dly_old",
393
+ last_connected_at: null,
394
+ last_stream_status: "connected",
395
+ },
396
+ });
397
+ await bridge.ensureLayout();
398
+ await bridge.writeRoomToken("room_terminal", {
399
+ room_id: "room_terminal",
400
+ agent_id: "agt_test",
401
+ token: "rat_old",
402
+ expires_at: new Date(Date.now() + 300000).toISOString(),
403
+ scope: "room:automation",
404
+ });
405
+ bridge.rememberCompletedWakeKey("room_terminal|9|agt_test");
406
+ await bridge.saveState();
407
+ await bridge.enqueueWakeJob({
408
+ delivery_id: "dly_terminal",
409
+ type: "room.turn_ready",
410
+ room_id: "room_terminal",
411
+ next_turn: 9,
412
+ next_actor_id: "agt_test",
413
+ received_at: new Date().toISOString(),
414
+ });
415
+
416
+ await bridge.processRecovery();
417
+
418
+ assert.deepEqual(markers, ["recovery"]);
419
+ const tokenPath = path.join(bridge.paths.tokenDir, "room_terminal.json");
420
+ const tokenExists = await fs
421
+ .access(tokenPath)
422
+ .then(() => true)
423
+ .catch(() => false);
424
+ assert.equal(tokenExists, false);
425
+ const wakeFiles = await fs.readdir(bridge.paths.wakeQueueDir);
426
+ assert.equal(wakeFiles.length, 0);
427
+ const state = JSON.parse(await fs.readFile(bridge.paths.statePath, "utf8"));
428
+ assert.ok(!state.completed_wake_keys.includes("room_terminal|9|agt_test"));
429
+ }
430
+
431
+ async function testHandleTerminalClearsPendingWakeJobs() {
432
+ const tmpDir = await fs.mkdtemp(
433
+ path.join(os.tmpdir(), "aya-bridge-terminal-"),
434
+ );
435
+ const markers = [];
436
+ const fetchImpl = async (input, options = {}) => {
437
+ const url = String(input);
438
+ if (
439
+ url === "https://api.example.test/v1/agent/stream/ack" &&
440
+ options.method === "POST"
441
+ ) {
442
+ markers.push("ack");
443
+ return jsonResponse(200, { status: "acked" });
444
+ }
445
+ throw new Error(`unexpected fetch ${options.method || "GET"} ${url}`);
446
+ };
447
+ const config = defaultConfig(tmpDir);
448
+ config.aya.api_base_url = "https://api.example.test";
449
+ const bridge = new BridgeDaemon({
450
+ config,
451
+ fetchImpl,
452
+ logger: createLogger("error"),
453
+ session: {
454
+ api_key: "aya_api_test",
455
+ session_token: "as_test",
456
+ agent_id: "agt_test",
457
+ },
458
+ state: {
459
+ last_acknowledged_delivery_id: "",
460
+ last_connected_at: null,
461
+ last_stream_status: "idle",
462
+ completed_wake_keys: [],
463
+ },
464
+ });
465
+ await bridge.ensureLayout();
466
+ await bridge.writeRoomToken("room_terminal", {
467
+ room_id: "room_terminal",
468
+ agent_id: "agt_test",
469
+ token: "rat_old",
470
+ expires_at: new Date(Date.now() + 300000).toISOString(),
471
+ scope: "room:automation",
472
+ });
473
+ await bridge.enqueueWakeJob({
474
+ delivery_id: "dly_pending",
475
+ type: "room.turn_ready",
476
+ room_id: "room_terminal",
477
+ next_turn: 9,
478
+ next_actor_id: "agt_test",
479
+ received_at: new Date().toISOString(),
480
+ });
481
+
482
+ await bridge.handleTerminal({
483
+ type: "room.closed",
484
+ delivery_id: "dly_closed",
485
+ room_id: "room_terminal",
486
+ });
487
+
488
+ const tokenExists = await fs
489
+ .access(path.join(bridge.paths.tokenDir, "room_terminal.json"))
490
+ .then(() => true)
491
+ .catch(() => false);
492
+ assert.equal(tokenExists, false);
493
+ const wakeFiles = await fs.readdir(bridge.paths.wakeQueueDir);
494
+ assert.equal(wakeFiles.length, 0);
495
+ assert.deepEqual(markers, ["ack"]);
496
+ }
497
+
498
+ async function main() {
499
+ const mode = String(process.argv[2] || "all").trim();
500
+ if (mode === "parse" || mode === "all") {
501
+ await runTest("parseSSE yields event payloads", testParseSSE);
502
+ }
503
+ if (mode === "turn" || mode === "all") {
504
+ await runTest(
505
+ "handleTurnReady dedupes duplicate turn_ready deliveries",
506
+ testHandleTurnReady,
507
+ );
508
+ }
509
+ if (mode === "retry" || mode === "all") {
510
+ await runTest("wake queue retries pending jobs", testWakeQueueRetry);
511
+ }
512
+ if (mode === "refresh" || mode === "all") {
513
+ await runTest(
514
+ "wake refreshes near-expiry room token",
515
+ testWakeRefreshesNearExpiryRoomToken,
516
+ );
517
+ }
518
+ if (mode === "dedupe" || mode === "all") {
519
+ await runTest(
520
+ "enqueueWakeJob dedupes equivalent room.turn_ready jobs",
521
+ testEnqueueWakeJobDedupesEquivalentTurnReady,
522
+ );
523
+ }
524
+ if (mode === "recovery" || mode === "all") {
525
+ await runTest(
526
+ "processRecovery clears terminal room wake jobs",
527
+ testProcessRecoveryClearsTerminalWakeJobs,
528
+ );
529
+ }
530
+ if (mode === "terminal" || mode === "all") {
531
+ await runTest(
532
+ "handleTerminal clears pending wake jobs",
533
+ testHandleTerminalClearsPendingWakeJobs,
534
+ );
535
+ }
536
+ if (process.exitCode) {
537
+ process.exit(process.exitCode);
538
+ }
539
+ }
540
+
541
+ main().catch((err) => {
542
+ console.error(err && err.stack ? err.stack : err);
543
+ process.exit(1);
544
+ });