agent-comms 1.27.3 → 1.28.1

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.
Files changed (75) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +1 -1
  4. package/dist/core/comms-store.d.ts +4 -37
  5. package/dist/core/comms-store.d.ts.map +1 -1
  6. package/dist/core/comms-store.js +3 -3
  7. package/dist/core/handshake.d.ts +101 -0
  8. package/dist/core/handshake.d.ts.map +1 -0
  9. package/dist/core/handshake.js +276 -0
  10. package/dist/core/handshake.js.map +1 -0
  11. package/dist/core/mesh-store.d.ts +12 -3
  12. package/dist/core/mesh-store.d.ts.map +1 -1
  13. package/dist/core/mesh-store.js +54 -35
  14. package/dist/core/mesh-store.js.map +1 -1
  15. package/dist/core/store.d.ts +1 -27
  16. package/dist/core/store.d.ts.map +1 -1
  17. package/dist/core/store.js +0 -62
  18. package/dist/core/store.js.map +1 -1
  19. package/dist/core/tls-transport.d.ts +12 -9
  20. package/dist/core/tls-transport.d.ts.map +1 -1
  21. package/dist/core/tls-transport.js +96 -58
  22. package/dist/core/tls-transport.js.map +1 -1
  23. package/dist/core/tool.d.ts +34 -5
  24. package/dist/core/tool.d.ts.map +1 -1
  25. package/dist/core/tool.js +41 -0
  26. package/dist/core/tool.js.map +1 -1
  27. package/dist/core/wire-protocol.d.ts +21 -2
  28. package/dist/core/wire-protocol.d.ts.map +1 -1
  29. package/dist/core/wire-protocol.js +23 -2
  30. package/dist/core/wire-protocol.js.map +1 -1
  31. package/dist/test/approval.integration.test.js +320 -285
  32. package/dist/test/approval.integration.test.js.map +1 -1
  33. package/dist/test/become-coordinator-actual-port.integration.test.js +0 -19
  34. package/dist/test/become-coordinator-actual-port.integration.test.js.map +1 -1
  35. package/dist/test/coordinator-socket-error.integration.test.js +2 -0
  36. package/dist/test/coordinator-socket-error.integration.test.js.map +1 -1
  37. package/dist/test/delivery-receipt.helper.js +15 -0
  38. package/dist/test/delivery-receipt.helper.js.map +1 -1
  39. package/dist/test/downtime-replay.test.js +5 -2
  40. package/dist/test/downtime-replay.test.js.map +1 -1
  41. package/dist/test/federation.integration.test.js +2 -0
  42. package/dist/test/federation.integration.test.js.map +1 -1
  43. package/dist/test/handshake.test.d.ts +5 -0
  44. package/dist/test/handshake.test.d.ts.map +1 -0
  45. package/dist/test/handshake.test.js +196 -0
  46. package/dist/test/handshake.test.js.map +1 -0
  47. package/dist/test/listener-policy.integration.test.js +214 -158
  48. package/dist/test/listener-policy.integration.test.js.map +1 -1
  49. package/dist/test/mesh-e2e.integration.test.js +2 -0
  50. package/dist/test/mesh-e2e.integration.test.js.map +1 -1
  51. package/dist/test/mesh-smoke.integration.test.d.ts +2 -4
  52. package/dist/test/mesh-smoke.integration.test.d.ts.map +1 -1
  53. package/dist/test/mesh-smoke.integration.test.js +28 -5
  54. package/dist/test/mesh-smoke.integration.test.js.map +1 -1
  55. package/dist/test/state-sync-convergence.test.js +5 -2
  56. package/dist/test/state-sync-convergence.test.js.map +1 -1
  57. package/dist/test/test-transport.d.ts +8 -0
  58. package/dist/test/test-transport.d.ts.map +1 -0
  59. package/dist/test/test-transport.js +29 -0
  60. package/dist/test/test-transport.js.map +1 -0
  61. package/dist/test/visibility.integration.test.js +6 -0
  62. package/dist/test/visibility.integration.test.js.map +1 -1
  63. package/package.json +4 -2
  64. package/dist/core/tcp-transport.d.ts +0 -85
  65. package/dist/core/tcp-transport.d.ts.map +0 -1
  66. package/dist/core/tcp-transport.js +0 -659
  67. package/dist/core/tcp-transport.js.map +0 -1
  68. package/dist/core/ws-transport.d.ts +0 -79
  69. package/dist/core/ws-transport.d.ts.map +0 -1
  70. package/dist/core/ws-transport.js +0 -621
  71. package/dist/core/ws-transport.js.map +0 -1
  72. package/dist/test/ws-broadcast-window.integration.test.d.ts +0 -7
  73. package/dist/test/ws-broadcast-window.integration.test.d.ts.map +0 -1
  74. package/dist/test/ws-broadcast-window.integration.test.js +0 -94
  75. package/dist/test/ws-broadcast-window.integration.test.js.map +0 -1
@@ -11,6 +11,7 @@ import { test, describe } from "node:test";
11
11
  import { MeshStore } from "../core/mesh-store.js";
12
12
  import { CommsTool } from "../core/tool.js";
13
13
  import { buildAction } from "../core/bridge.js";
14
+ import { waitFor, wireTestTransport } from "./test-transport.js";
14
15
  /** Find a free port on localhost by binding to port 0. */
15
16
  function findFreePort() {
16
17
  return new Promise((resolve, reject) => {
@@ -40,322 +41,356 @@ describe("connection approval", () => {
40
41
  const portB = portA + 100;
41
42
  // Set up coordinator (store A)
42
43
  const storeA = new MeshStore(portA);
43
- const receivedRequests = [];
44
- storeA.onDelivery = (_id, event) => {
45
- if (event.type === "connection_request") {
46
- receivedRequests.push(event);
47
- }
48
- };
49
- await storeA.init();
50
- await storeA.registerAgent({
51
- name: "coordinator",
52
- harness: "test",
53
- cwd: "/test/a",
54
- pid: process.pid,
55
- visibility: "visible",
56
- tags: [],
57
- });
58
- await sleep(100);
59
- // Set up connecting peer (store B) that uses connectToRemote
44
+ wireTestTransport(storeA);
60
45
  const storeB = new MeshStore(portB);
61
- await storeB.startDataServerOnly();
62
- await storeB.registerAgent({
63
- name: "connector",
64
- harness: "test",
65
- cwd: "/test/b",
66
- pid: process.pid,
67
- visibility: "visible",
68
- tags: [],
69
- });
70
- // Initiate connection request
71
- storeB.connectToRemote("127.0.0.1", portA);
72
- // Wait for the coordinator to receive the request
73
- await sleep(300);
74
- // Coordinator should have received a connection_request event
75
- assert.equal(receivedRequests.length, 1, "Coordinator should receive exactly one connection request");
76
- const request = receivedRequests[0];
77
- assert.equal(request?.type, "connection_request");
78
- assert.ok(request?.connectionId, "Request should have a connectionId");
79
- assert.equal(request?.peerId, storeB.peerId, "Request should contain connector's peer ID");
80
- assert.equal(request?.name, "connector", "Request should contain connector's name");
81
- await storeB.shutdown();
82
- await storeA.shutdown();
46
+ wireTestTransport(storeB);
47
+ try {
48
+ const receivedRequests = [];
49
+ storeA.onDelivery = (_id, event) => {
50
+ if (event.type === "connection_request") {
51
+ receivedRequests.push(event);
52
+ }
53
+ };
54
+ await storeA.init();
55
+ await storeA.registerAgent({
56
+ name: "coordinator",
57
+ harness: "test",
58
+ cwd: "/test/a",
59
+ pid: process.pid,
60
+ visibility: "visible",
61
+ tags: [],
62
+ });
63
+ await sleep(100);
64
+ // Set up connecting peer (store B) that uses connectToRemote
65
+ await storeB.startDataServerOnly();
66
+ await storeB.registerAgent({
67
+ name: "connector",
68
+ harness: "test",
69
+ cwd: "/test/b",
70
+ pid: process.pid,
71
+ visibility: "visible",
72
+ tags: [],
73
+ });
74
+ // Initiate connection request
75
+ storeB.connectToRemote("127.0.0.1", portA);
76
+ // Wait for the coordinator to receive the request
77
+ await waitFor(() => receivedRequests.length === 1, "coordinator receives the connection request");
78
+ // Coordinator should have received a connection_request event
79
+ assert.equal(receivedRequests.length, 1, "Coordinator should receive exactly one connection request");
80
+ const request = receivedRequests[0];
81
+ assert.equal(request?.type, "connection_request");
82
+ assert.ok(request?.connectionId, "Request should have a connectionId");
83
+ assert.equal(request?.peerId, storeB.peerId, "Request should contain connector's peer ID");
84
+ assert.equal(request?.name, "connector", "Request should contain connector's name");
85
+ }
86
+ finally {
87
+ await storeB.shutdown();
88
+ await storeA.shutdown();
89
+ }
83
90
  });
84
91
  void test("accept establishes the peer connection", async () => {
85
92
  const portA = await uniquePort();
86
93
  const portB = portA + 100;
87
94
  const storeA = new MeshStore(portA);
88
- const receivedRequests = [];
89
- storeA.onDelivery = (_id, event) => {
90
- if (event.type === "connection_request") {
91
- receivedRequests.push(event);
92
- }
93
- };
94
- await storeA.init();
95
- await storeA.registerAgent({
96
- name: "coordinator",
97
- harness: "test",
98
- cwd: "/test/a",
99
- pid: process.pid,
100
- visibility: "visible",
101
- tags: [],
102
- });
103
- await sleep(100);
95
+ wireTestTransport(storeA);
104
96
  const storeB = new MeshStore(portB);
105
- await storeB.startDataServerOnly();
106
- await storeB.registerAgent({
107
- name: "connector",
108
- harness: "test",
109
- cwd: "/test/b",
110
- pid: process.pid,
111
- visibility: "visible",
112
- tags: [],
113
- });
114
- storeB.connectToRemote("127.0.0.1", portA);
115
- await sleep(300);
116
- assert.equal(receivedRequests.length, 1, "Coordinator should receive a connection request");
117
- const request = receivedRequests[0];
118
- assert.ok(request?.connectionId);
119
- // Accept the connection
120
- await storeA.acceptConnection(request.connectionId);
121
- // Wait for state sync (peer_list → connectToPeer → state_sync → handlePeerConnected)
122
- await sleep(500);
123
- // Verify both stores see each other's agents
124
- const agentsA = await storeA.listAgents(storeA.peerId);
125
- const agentsB = await storeB.listAgents(storeB.peerId);
126
- assert.ok(agentsA.some((a) => a.id === storeB.peerId), "Coordinator should see the connector agent");
127
- assert.ok(agentsB.some((a) => a.id === storeA.peerId), "Connector should see the coordinator agent");
128
- await storeB.shutdown();
129
- await storeA.shutdown();
97
+ wireTestTransport(storeB);
98
+ try {
99
+ const receivedRequests = [];
100
+ storeA.onDelivery = (_id, event) => {
101
+ if (event.type === "connection_request") {
102
+ receivedRequests.push(event);
103
+ }
104
+ };
105
+ await storeA.init();
106
+ await storeA.registerAgent({
107
+ name: "coordinator",
108
+ harness: "test",
109
+ cwd: "/test/a",
110
+ pid: process.pid,
111
+ visibility: "visible",
112
+ tags: [],
113
+ });
114
+ await sleep(100);
115
+ await storeB.startDataServerOnly();
116
+ await storeB.registerAgent({
117
+ name: "connector",
118
+ harness: "test",
119
+ cwd: "/test/b",
120
+ pid: process.pid,
121
+ visibility: "visible",
122
+ tags: [],
123
+ });
124
+ storeB.connectToRemote("127.0.0.1", portA);
125
+ await waitFor(() => receivedRequests.length === 1, "coordinator receives the connection request");
126
+ const request = receivedRequests[0];
127
+ assert.ok(request?.connectionId);
128
+ // Accept the connection
129
+ await storeA.acceptConnection(request.connectionId);
130
+ // State sync (peer_list -> connectToPeer -> state_sync -> handlePeerConnected) is a real TLS round trip, not instantaneous -- poll rather than assume any fixed delay is enough under a loaded CI runner.
131
+ await waitFor(() => storeA.serialise().agents[storeB.peerId] !== undefined, "coordinator sees the connector agent");
132
+ await waitFor(() => storeB.serialise().agents[storeA.peerId] !== undefined, "connector sees the coordinator agent");
133
+ const agentsA = await storeA.listAgents(storeA.peerId);
134
+ const agentsB = await storeB.listAgents(storeB.peerId);
135
+ assert.ok(agentsA.some((a) => a.id === storeB.peerId), "Coordinator should see the connector agent");
136
+ assert.ok(agentsB.some((a) => a.id === storeA.peerId), "Connector should see the coordinator agent");
137
+ }
138
+ finally {
139
+ await storeB.shutdown();
140
+ await storeA.shutdown();
141
+ }
130
142
  });
131
143
  void test("reject closes with reason", async () => {
132
144
  const portA = await uniquePort();
133
145
  const portB = portA + 100;
134
146
  const storeA = new MeshStore(portA);
135
- const receivedRequests = [];
136
- storeA.onDelivery = (_id, event) => {
137
- if (event.type === "connection_request") {
138
- receivedRequests.push(event);
139
- }
140
- };
141
- await storeA.init();
142
- await storeA.registerAgent({
143
- name: "coordinator",
144
- harness: "test",
145
- cwd: "/test/a",
146
- pid: process.pid,
147
- visibility: "visible",
148
- tags: [],
149
- });
150
- await sleep(100);
147
+ wireTestTransport(storeA);
151
148
  const storeB = new MeshStore(portB);
152
- await storeB.startDataServerOnly();
153
- await storeB.registerAgent({
154
- name: "connector",
155
- harness: "test",
156
- cwd: "/test/b",
157
- pid: process.pid,
158
- visibility: "visible",
159
- tags: [],
160
- });
161
- storeB.connectToRemote("127.0.0.1", portA);
162
- await sleep(300);
163
- assert.equal(receivedRequests.length, 1, "Coordinator should receive a connection request");
164
- const request = receivedRequests[0];
165
- assert.ok(request?.connectionId);
166
- // Reject the connection
167
- await storeA.rejectConnection(request.connectionId, "unauthorised");
168
- // Give the rejection time to propagate
169
- await sleep(200);
170
- // Verify the coordinator no longer sees the connector agent
171
- const agentsA = await storeA.listAgents(storeA.peerId);
172
- assert.ok(!agentsA.some((a) => a.id === storeB.peerId), "Rejected peer should not appear in agent list");
173
- await storeB.shutdown();
174
- await storeA.shutdown();
149
+ wireTestTransport(storeB);
150
+ try {
151
+ const receivedRequests = [];
152
+ storeA.onDelivery = (_id, event) => {
153
+ if (event.type === "connection_request") {
154
+ receivedRequests.push(event);
155
+ }
156
+ };
157
+ await storeA.init();
158
+ await storeA.registerAgent({
159
+ name: "coordinator",
160
+ harness: "test",
161
+ cwd: "/test/a",
162
+ pid: process.pid,
163
+ visibility: "visible",
164
+ tags: [],
165
+ });
166
+ await sleep(100);
167
+ await storeB.startDataServerOnly();
168
+ await storeB.registerAgent({
169
+ name: "connector",
170
+ harness: "test",
171
+ cwd: "/test/b",
172
+ pid: process.pid,
173
+ visibility: "visible",
174
+ tags: [],
175
+ });
176
+ storeB.connectToRemote("127.0.0.1", portA);
177
+ await waitFor(() => receivedRequests.length === 1, "coordinator receives the connection request");
178
+ assert.equal(receivedRequests.length, 1, "Coordinator should receive a connection request");
179
+ const request = receivedRequests[0];
180
+ assert.ok(request?.connectionId);
181
+ // Reject the connection
182
+ await storeA.rejectConnection(request.connectionId, "unauthorised");
183
+ // Give the rejection time to propagate -- there is no positive event to poll for here (the assertion below proves an absence), so a fixed wait is the right shape, unlike the accept-flow tests above.
184
+ await sleep(200);
185
+ // Verify the coordinator no longer sees the connector agent
186
+ const agentsA = await storeA.listAgents(storeA.peerId);
187
+ assert.ok(!agentsA.some((a) => a.id === storeB.peerId), "Rejected peer should not appear in agent list");
188
+ }
189
+ finally {
190
+ await storeB.shutdown();
191
+ await storeA.shutdown();
192
+ }
175
193
  });
176
194
  void test("mesh_pending lists pending connections", async () => {
177
195
  const portA = await uniquePort();
178
196
  const portB = portA + 100;
179
197
  const storeA = new MeshStore(portA);
180
- storeA.onDelivery = () => { };
181
- await storeA.init();
182
- await storeA.registerAgent({
183
- name: "coordinator",
184
- harness: "test",
185
- cwd: "/test/a",
186
- pid: process.pid,
187
- visibility: "visible",
188
- tags: [],
189
- });
190
- await sleep(100);
198
+ wireTestTransport(storeA);
191
199
  const storeB = new MeshStore(portB);
192
- await storeB.startDataServerOnly();
193
- await storeB.registerAgent({
194
- name: "connector",
195
- harness: "test",
196
- cwd: "/test/b",
197
- pid: process.pid,
198
- visibility: "visible",
199
- tags: [],
200
- });
201
- // No pending connections initially
202
- let pending = storeA.listPendingConnections();
203
- assert.equal(pending.length, 0, "Should have no pending connections initially");
204
- // Initiate connection request
205
- storeB.connectToRemote("127.0.0.1", portA);
206
- await sleep(300);
207
- // Should now have one pending connection
208
- pending = storeA.listPendingConnections();
209
- assert.equal(pending.length, 1, "Should have one pending connection");
210
- assert.equal(pending[0]?.name, "connector", "Pending connection should show connector name");
211
- assert.equal(pending[0]?.peerId, storeB.peerId, "Pending connection should show connector peer ID");
212
- // Accept to clean up
213
- const connectionId = pending[0]?.connectionId;
214
- assert.ok(connectionId);
215
- await storeA.acceptConnection(connectionId);
216
- // Wait for state sync
217
- await sleep(300);
218
- // No more pending connections after acceptance
219
- pending = storeA.listPendingConnections();
220
- assert.equal(pending.length, 0, "Should have no pending connections after accept");
221
- await storeB.shutdown();
222
- await storeA.shutdown();
200
+ wireTestTransport(storeB);
201
+ try {
202
+ storeA.onDelivery = () => { };
203
+ await storeA.init();
204
+ await storeA.registerAgent({
205
+ name: "coordinator",
206
+ harness: "test",
207
+ cwd: "/test/a",
208
+ pid: process.pid,
209
+ visibility: "visible",
210
+ tags: [],
211
+ });
212
+ await sleep(100);
213
+ await storeB.startDataServerOnly();
214
+ await storeB.registerAgent({
215
+ name: "connector",
216
+ harness: "test",
217
+ cwd: "/test/b",
218
+ pid: process.pid,
219
+ visibility: "visible",
220
+ tags: [],
221
+ });
222
+ // No pending connections initially
223
+ let pending = storeA.listPendingConnections();
224
+ assert.equal(pending.length, 0, "Should have no pending connections initially");
225
+ // Initiate connection request
226
+ storeB.connectToRemote("127.0.0.1", portA);
227
+ await waitFor(() => storeA.listPendingConnections().length === 1, "coordinator sees the pending connection");
228
+ // Should now have one pending connection
229
+ pending = storeA.listPendingConnections();
230
+ assert.equal(pending.length, 1, "Should have one pending connection");
231
+ assert.equal(pending[0]?.name, "connector", "Pending connection should show connector name");
232
+ assert.equal(pending[0]?.peerId, storeB.peerId, "Pending connection should show connector peer ID");
233
+ // Accept to clean up
234
+ const connectionId = pending[0]?.connectionId;
235
+ assert.ok(connectionId);
236
+ await storeA.acceptConnection(connectionId);
237
+ // No more pending connections after acceptance
238
+ await waitFor(() => storeA.listPendingConnections().length === 0, "pending connection is cleared after accept");
239
+ }
240
+ finally {
241
+ await storeB.shutdown();
242
+ await storeA.shutdown();
243
+ }
223
244
  });
224
245
  void test("tool handles mesh_connect/mesh_accept/mesh_reject/mesh_pending actions", async () => {
225
246
  const portA = await uniquePort();
226
247
  const portB = portA + 100;
227
248
  const storeA = new MeshStore(portA);
228
- storeA.onDelivery = () => { };
229
- await storeA.init();
230
- await storeA.registerAgent({
231
- name: "coordinator",
232
- harness: "test",
233
- cwd: "/test/a",
234
- pid: process.pid,
235
- visibility: "visible",
236
- tags: [],
237
- });
238
- const toolA = new CommsTool(storeA);
239
- await sleep(100);
249
+ wireTestTransport(storeA);
240
250
  const storeB = new MeshStore(portB);
241
- await storeB.startDataServerOnly();
242
- await storeB.registerAgent({
243
- name: "connector",
244
- harness: "test",
245
- cwd: "/test/b",
246
- pid: process.pid,
247
- visibility: "visible",
248
- tags: [],
249
- });
250
- const toolB = new CommsTool(storeB);
251
- // Use buildAction to construct the mesh_connect action
252
- const connectAction = buildAction({
253
- action: "mesh_connect",
254
- host: "127.0.0.1",
255
- port: portA,
256
- });
257
- // Give coordinator time to settle
258
- await sleep(200);
259
- // Verify coordinator is listening
260
- const listeners = storeA.listListeners();
261
- assert.ok(listeners.length > 0, `Coordinator should have listeners, got ${listeners.length}`);
262
- assert.equal(listeners[0]?.port, portA, `Coordinator should be on port ${portA}`);
263
- // B initiates the connection via the tool
264
- const connectResult = await toolB.handle({
265
- agentId: storeB.peerId,
266
- harness: "test",
267
- cwd: "/test/b",
268
- pid: process.pid,
269
- }, connectAction);
270
- assert.ok(!connectResult.isError, `mesh_connect should succeed: ${connectResult.content}`);
271
- await sleep(300);
272
- // A checks pending connections via the tool
273
- const pendingAction = buildAction({ action: "mesh_pending" });
274
- const pendingResult = await toolA.handle({
275
- agentId: storeA.peerId,
276
- harness: "test",
277
- cwd: "/test/a",
278
- pid: process.pid,
279
- }, pendingAction);
280
- assert.ok(!pendingResult.isError, `mesh_pending should succeed: ${pendingResult.content}`);
281
- assert.ok(pendingResult.content.includes("connector"), "Pending list should show connector name");
282
- // Extract connectionId from the pending connections list
283
- const pendingConns = storeA.listPendingConnections();
284
- assert.equal(pendingConns.length, 1, "Should have one pending connection");
285
- const connectionId = pendingConns[0]?.connectionId;
286
- assert.ok(connectionId);
287
- // A accepts the connection via the tool
288
- const acceptAction = buildAction({
289
- action: "mesh_accept",
290
- connectionId,
291
- });
292
- const acceptResult = await toolA.handle({
293
- agentId: storeA.peerId,
294
- harness: "test",
295
- cwd: "/test/a",
296
- pid: process.pid,
297
- }, acceptAction);
298
- assert.ok(!acceptResult.isError, `mesh_accept should succeed: ${acceptResult.content}`);
299
- await sleep(200);
300
- // Verify both see each other
301
- const agentsA = await storeA.listAgents(storeA.peerId);
302
- const agentsB = await storeB.listAgents(storeB.peerId);
303
- assert.ok(agentsA.some((a) => a.id === storeB.peerId), "A should see B after accept");
304
- assert.ok(agentsB.some((a) => a.id === storeA.peerId), "B should see A after accept");
305
- await storeB.shutdown();
306
- await storeA.shutdown();
251
+ wireTestTransport(storeB);
252
+ try {
253
+ storeA.onDelivery = () => { };
254
+ await storeA.init();
255
+ await storeA.registerAgent({
256
+ name: "coordinator",
257
+ harness: "test",
258
+ cwd: "/test/a",
259
+ pid: process.pid,
260
+ visibility: "visible",
261
+ tags: [],
262
+ });
263
+ const toolA = new CommsTool(storeA);
264
+ await sleep(100);
265
+ await storeB.startDataServerOnly();
266
+ await storeB.registerAgent({
267
+ name: "connector",
268
+ harness: "test",
269
+ cwd: "/test/b",
270
+ pid: process.pid,
271
+ visibility: "visible",
272
+ tags: [],
273
+ });
274
+ const toolB = new CommsTool(storeB);
275
+ // Use buildAction to construct the mesh_connect action
276
+ const connectAction = buildAction({
277
+ action: "mesh_connect",
278
+ host: "127.0.0.1",
279
+ port: portA,
280
+ });
281
+ // Give coordinator time to settle
282
+ await sleep(200);
283
+ // Verify coordinator is listening
284
+ const listeners = storeA.listListeners();
285
+ assert.ok(listeners.length > 0, `Coordinator should have listeners, got ${listeners.length}`);
286
+ assert.equal(listeners[0]?.port, portA, `Coordinator should be on port ${portA}`);
287
+ // B initiates the connection via the tool
288
+ const connectResult = await toolB.handle({
289
+ agentId: storeB.peerId,
290
+ harness: "test",
291
+ cwd: "/test/b",
292
+ pid: process.pid,
293
+ }, connectAction);
294
+ assert.ok(!connectResult.isError, `mesh_connect should succeed: ${connectResult.content}`);
295
+ await waitFor(() => storeA.listPendingConnections().length === 1, "coordinator sees the pending connection");
296
+ // A checks pending connections via the tool
297
+ const pendingAction = buildAction({ action: "mesh_pending" });
298
+ const pendingResult = await toolA.handle({
299
+ agentId: storeA.peerId,
300
+ harness: "test",
301
+ cwd: "/test/a",
302
+ pid: process.pid,
303
+ }, pendingAction);
304
+ assert.ok(!pendingResult.isError, `mesh_pending should succeed: ${pendingResult.content}`);
305
+ assert.ok(pendingResult.content.includes("connector"), "Pending list should show connector name");
306
+ // Extract connectionId from the pending connections list
307
+ const pendingConns = storeA.listPendingConnections();
308
+ assert.equal(pendingConns.length, 1, "Should have one pending connection");
309
+ const connectionId = pendingConns[0]?.connectionId;
310
+ assert.ok(connectionId);
311
+ // A accepts the connection via the tool
312
+ const acceptAction = buildAction({
313
+ action: "mesh_accept",
314
+ connectionId,
315
+ });
316
+ const acceptResult = await toolA.handle({
317
+ agentId: storeA.peerId,
318
+ harness: "test",
319
+ cwd: "/test/a",
320
+ pid: process.pid,
321
+ }, acceptAction);
322
+ assert.ok(!acceptResult.isError, `mesh_accept should succeed: ${acceptResult.content}`);
323
+ // State sync is a real TLS round trip, not instantaneous -- poll rather than assume any fixed delay is enough under a loaded CI runner.
324
+ await waitFor(() => storeA.serialise().agents[storeB.peerId] !== undefined, "A sees B after accept");
325
+ await waitFor(() => storeB.serialise().agents[storeA.peerId] !== undefined, "B sees A after accept");
326
+ // Verify both see each other
327
+ const agentsA = await storeA.listAgents(storeA.peerId);
328
+ const agentsB = await storeB.listAgents(storeB.peerId);
329
+ assert.ok(agentsA.some((a) => a.id === storeB.peerId), "A should see B after accept");
330
+ assert.ok(agentsB.some((a) => a.id === storeA.peerId), "B should see A after accept");
331
+ }
332
+ finally {
333
+ await storeB.shutdown();
334
+ await storeA.shutdown();
335
+ }
307
336
  });
308
337
  void test("tool mesh_reject returns error message", async () => {
309
338
  const portA = await uniquePort();
310
339
  const portB = portA + 100;
311
340
  const storeA = new MeshStore(portA);
312
- storeA.onDelivery = () => { };
313
- await storeA.init();
314
- await storeA.registerAgent({
315
- name: "coordinator",
316
- harness: "test",
317
- cwd: "/test/a",
318
- pid: process.pid,
319
- visibility: "visible",
320
- tags: [],
321
- });
322
- const toolA = new CommsTool(storeA);
323
- await sleep(100);
341
+ wireTestTransport(storeA);
324
342
  const storeB = new MeshStore(portB);
325
- await storeB.startDataServerOnly();
326
- await storeB.registerAgent({
327
- name: "connector",
328
- harness: "test",
329
- cwd: "/test/b",
330
- pid: process.pid,
331
- visibility: "visible",
332
- tags: [],
333
- });
334
- // B connects
335
- storeB.connectToRemote("127.0.0.1", portA);
336
- await sleep(300);
337
- const pendingConns = storeA.listPendingConnections();
338
- const connectionId = pendingConns[0]?.connectionId;
339
- assert.ok(connectionId);
340
- // A rejects via the tool
341
- const rejectAction = buildAction({
342
- action: "mesh_reject",
343
- connectionId,
344
- reason: "not allowed",
345
- });
346
- const rejectResult = await toolA.handle({
347
- agentId: storeA.peerId,
348
- harness: "test",
349
- cwd: "/test/a",
350
- pid: process.pid,
351
- }, rejectAction);
352
- assert.ok(!rejectResult.isError, `mesh_reject should succeed: ${rejectResult.content}`);
353
- assert.ok(rejectResult.content.includes("not allowed"), "Result should include the reason");
354
- // Verify rejection took effect
355
- const pending = storeA.listPendingConnections();
356
- assert.equal(pending.length, 0, "No pending connections after reject");
357
- await storeB.shutdown();
358
- await storeA.shutdown();
343
+ wireTestTransport(storeB);
344
+ try {
345
+ storeA.onDelivery = () => { };
346
+ await storeA.init();
347
+ await storeA.registerAgent({
348
+ name: "coordinator",
349
+ harness: "test",
350
+ cwd: "/test/a",
351
+ pid: process.pid,
352
+ visibility: "visible",
353
+ tags: [],
354
+ });
355
+ const toolA = new CommsTool(storeA);
356
+ await sleep(100);
357
+ await storeB.startDataServerOnly();
358
+ await storeB.registerAgent({
359
+ name: "connector",
360
+ harness: "test",
361
+ cwd: "/test/b",
362
+ pid: process.pid,
363
+ visibility: "visible",
364
+ tags: [],
365
+ });
366
+ // B connects
367
+ storeB.connectToRemote("127.0.0.1", portA);
368
+ await waitFor(() => storeA.listPendingConnections().length === 1, "coordinator sees the pending connection");
369
+ const pendingConns = storeA.listPendingConnections();
370
+ const connectionId = pendingConns[0]?.connectionId;
371
+ assert.ok(connectionId);
372
+ // A rejects via the tool
373
+ const rejectAction = buildAction({
374
+ action: "mesh_reject",
375
+ connectionId,
376
+ reason: "not allowed",
377
+ });
378
+ const rejectResult = await toolA.handle({
379
+ agentId: storeA.peerId,
380
+ harness: "test",
381
+ cwd: "/test/a",
382
+ pid: process.pid,
383
+ }, rejectAction);
384
+ assert.ok(!rejectResult.isError, `mesh_reject should succeed: ${rejectResult.content}`);
385
+ assert.ok(rejectResult.content.includes("not allowed"), "Result should include the reason");
386
+ // Verify rejection took effect
387
+ const pending = storeA.listPendingConnections();
388
+ assert.equal(pending.length, 0, "No pending connections after reject");
389
+ }
390
+ finally {
391
+ await storeB.shutdown();
392
+ await storeA.shutdown();
393
+ }
359
394
  });
360
395
  });
361
396
  //# sourceMappingURL=approval.integration.test.js.map