agent-comms 9.0.2 → 9.0.4

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 (192) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +6 -0
  4. package/dist/bridges/claude-code/channel.d.ts.map +1 -1
  5. package/dist/bridges/claude-code/channel.js +3 -1
  6. package/dist/bridges/claude-code/channel.js.map +1 -1
  7. package/dist/bridges/codex/tool.d.ts +2 -1
  8. package/dist/bridges/codex/tool.d.ts.map +1 -1
  9. package/dist/bridges/codex/tool.js +4 -2
  10. package/dist/bridges/codex/tool.js.map +1 -1
  11. package/dist/bridges/mcp/server.d.ts.map +1 -1
  12. package/dist/bridges/mcp/server.js +3 -1
  13. package/dist/bridges/mcp/server.js.map +1 -1
  14. package/dist/bridges/user/web/bridge-port-range.d.ts +52 -0
  15. package/dist/bridges/user/web/bridge-port-range.d.ts.map +1 -0
  16. package/dist/bridges/user/web/bridge-port-range.js +90 -0
  17. package/dist/bridges/user/web/bridge-port-range.js.map +1 -0
  18. package/dist/bridges/user/web/dist/bundle.js +1 -1
  19. package/dist/bridges/user/web/dist/sw.js +1 -1
  20. package/dist/bridges/user/web/frontend/mesh-client.d.ts +6 -2
  21. package/dist/bridges/user/web/frontend/mesh-client.d.ts.map +1 -1
  22. package/dist/bridges/user/web/frontend/mesh-client.js +13 -27
  23. package/dist/bridges/user/web/frontend/mesh-client.js.map +1 -1
  24. package/dist/bridges/user/web/server.d.ts +11 -5
  25. package/dist/bridges/user/web/server.d.ts.map +1 -1
  26. package/dist/bridges/user/web/server.js +78 -21
  27. package/dist/bridges/user/web/server.js.map +1 -1
  28. package/dist/bridges/user/web/test/bridge-port-range.integration.test.d.ts +2 -0
  29. package/dist/bridges/user/web/test/bridge-port-range.integration.test.d.ts.map +1 -0
  30. package/dist/bridges/user/web/test/bridge-port-range.integration.test.js +281 -0
  31. package/dist/bridges/user/web/test/bridge-port-range.integration.test.js.map +1 -0
  32. package/dist/bridges/user/web/test/bridge-web-server.helper.d.ts +13 -0
  33. package/dist/bridges/user/web/test/bridge-web-server.helper.d.ts.map +1 -0
  34. package/dist/bridges/user/web/test/bridge-web-server.helper.js +46 -0
  35. package/dist/bridges/user/web/test/bridge-web-server.helper.js.map +1 -0
  36. package/dist/bridges/user/web/test/bridge-web-server.integration.test.js +4 -2
  37. package/dist/bridges/user/web/test/bridge-web-server.integration.test.js.map +1 -1
  38. package/dist/bridges/user/web/test/lan-web-server.integration.test.js +1 -0
  39. package/dist/bridges/user/web/test/lan-web-server.integration.test.js.map +1 -1
  40. package/dist/bridges/user/web/test/orpc-router.integration.test.js +1 -0
  41. package/dist/bridges/user/web/test/orpc-router.integration.test.js.map +1 -1
  42. package/dist/bridges/user/web/test/web-access.unit.test.d.ts +1 -1
  43. package/dist/bridges/user/web/test/web-access.unit.test.js +19 -375
  44. package/dist/bridges/user/web/test/web-access.unit.test.js.map +1 -1
  45. package/dist/bridges/user/web/test/web-console-route.integration.test.js +1 -0
  46. package/dist/bridges/user/web/test/web-console-route.integration.test.js.map +1 -1
  47. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js +4 -2
  48. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js.map +1 -1
  49. package/dist/bridges/user/web/test/web-server.integration.test.js +1 -0
  50. package/dist/bridges/user/web/test/web-server.integration.test.js.map +1 -1
  51. package/dist/core/bridge-mesh.d.ts +2 -0
  52. package/dist/core/bridge-mesh.d.ts.map +1 -1
  53. package/dist/core/bridge-mesh.js.map +1 -1
  54. package/dist/core/bridge.d.ts +11 -0
  55. package/dist/core/bridge.d.ts.map +1 -1
  56. package/dist/core/bridge.js +17 -2
  57. package/dist/core/bridge.js.map +1 -1
  58. package/dist/core/index.d.ts +1 -1
  59. package/dist/core/index.d.ts.map +1 -1
  60. package/dist/core/index.js +1 -1
  61. package/dist/core/index.js.map +1 -1
  62. package/dist/core/test/build-action.unit.test.d.ts +2 -0
  63. package/dist/core/test/build-action.unit.test.d.ts.map +1 -0
  64. package/dist/core/test/build-action.unit.test.js +28 -0
  65. package/dist/core/test/build-action.unit.test.js.map +1 -0
  66. package/dist/core/tool.d.ts +1 -1
  67. package/dist/core/tool.d.ts.map +1 -1
  68. package/dist/test/bridge-stdout.runner.js +5 -0
  69. package/dist/test/bridge-stdout.runner.js.map +1 -1
  70. package/dist/test/ci-required-checks.unit.test.d.ts +2 -0
  71. package/dist/test/ci-required-checks.unit.test.d.ts.map +1 -0
  72. package/dist/test/ci-required-checks.unit.test.js +137 -0
  73. package/dist/test/ci-required-checks.unit.test.js.map +1 -0
  74. package/dist/test/claude-command.unit.test.js +2 -21
  75. package/dist/test/claude-command.unit.test.js.map +1 -1
  76. package/dist/test/conformance/corpus-files.d.ts +11 -0
  77. package/dist/test/conformance/corpus-files.d.ts.map +1 -0
  78. package/dist/test/conformance/corpus-files.js +71 -0
  79. package/dist/test/conformance/corpus-files.js.map +1 -0
  80. package/dist/test/conformance/corpus-types.d.ts +62 -0
  81. package/dist/test/conformance/corpus-types.d.ts.map +1 -0
  82. package/dist/test/conformance/corpus-types.js +8 -0
  83. package/dist/test/conformance/corpus-types.js.map +1 -0
  84. package/dist/test/conformance/corpus.d.ts +34 -0
  85. package/dist/test/conformance/corpus.d.ts.map +1 -0
  86. package/dist/test/conformance/corpus.js +134 -0
  87. package/dist/test/conformance/corpus.js.map +1 -0
  88. package/dist/test/conformance/read-json.d.ts +2 -0
  89. package/dist/test/conformance/read-json.d.ts.map +1 -0
  90. package/dist/test/conformance/read-json.js +7 -0
  91. package/dist/test/conformance/read-json.js.map +1 -0
  92. package/dist/test/conformance/text.d.ts +10 -0
  93. package/dist/test/conformance/text.d.ts.map +1 -0
  94. package/dist/test/conformance/text.js +64 -0
  95. package/dist/test/conformance/text.js.map +1 -0
  96. package/dist/test/conformance/validate.d.ts +14 -0
  97. package/dist/test/conformance/validate.d.ts.map +1 -0
  98. package/dist/test/conformance/validate.js +85 -0
  99. package/dist/test/conformance/validate.js.map +1 -0
  100. package/dist/test/conformance-functions.helper.d.ts +19 -0
  101. package/dist/test/conformance-functions.helper.d.ts.map +1 -0
  102. package/dist/test/conformance-functions.helper.js +583 -0
  103. package/dist/test/conformance-functions.helper.js.map +1 -0
  104. package/dist/test/conformance.unit.test.d.ts +2 -0
  105. package/dist/test/conformance.unit.test.d.ts.map +1 -0
  106. package/dist/test/conformance.unit.test.js +358 -0
  107. package/dist/test/conformance.unit.test.js.map +1 -0
  108. package/dist/test/error-reporting.unit.test.js +2 -90
  109. package/dist/test/error-reporting.unit.test.js.map +1 -1
  110. package/dist/test/hub-helpers.d.ts +6 -0
  111. package/dist/test/hub-helpers.d.ts.map +1 -1
  112. package/dist/test/hub-helpers.js +17 -0
  113. package/dist/test/hub-helpers.js.map +1 -1
  114. package/dist/test/mcp-stdout-channel.integration.test.js +2 -1
  115. package/dist/test/mcp-stdout-channel.integration.test.js.map +1 -1
  116. package/dist/test/version-report-actions.unit.test.d.ts +1 -1
  117. package/dist/test/version-report-actions.unit.test.js +2 -55
  118. package/dist/test/version-report-actions.unit.test.js.map +1 -1
  119. package/package.json +4 -2
  120. package/src/bridges/claude-code/channel.ts +3 -1
  121. package/src/bridges/codex/tool.ts +5 -2
  122. package/src/bridges/mcp/server.ts +3 -1
  123. package/src/bridges/user/web/bridge-port-range.ts +130 -0
  124. package/src/bridges/user/web/dist/bundle.js +1 -1
  125. package/src/bridges/user/web/dist/sw.js +1 -1
  126. package/src/bridges/user/web/server.ts +104 -29
  127. package/src/bridges/user/web/test/bridge-web-server.helper.ts +54 -0
  128. package/src/core/bridge-mesh.ts +2 -0
  129. package/src/core/bridge.ts +30 -1
  130. package/src/core/index.ts +1 -0
  131. package/src/core/tool.ts +1 -1
  132. package/dist/bridges/user/web/port-discovery.d.ts +0 -14
  133. package/dist/bridges/user/web/port-discovery.d.ts.map +0 -1
  134. package/dist/bridges/user/web/port-discovery.js +0 -40
  135. package/dist/bridges/user/web/port-discovery.js.map +0 -1
  136. package/dist/bridges/user/web/test/port-discovery.unit.test.d.ts +0 -5
  137. package/dist/bridges/user/web/test/port-discovery.unit.test.d.ts.map +0 -1
  138. package/dist/bridges/user/web/test/port-discovery.unit.test.js +0 -180
  139. package/dist/bridges/user/web/test/port-discovery.unit.test.js.map +0 -1
  140. package/dist/core/test/bridge.unit.test.d.ts +0 -5
  141. package/dist/core/test/bridge.unit.test.d.ts.map +0 -1
  142. package/dist/core/test/bridge.unit.test.js +0 -119
  143. package/dist/core/test/bridge.unit.test.js.map +0 -1
  144. package/dist/test/agent-table.unit.test.d.ts +0 -5
  145. package/dist/test/agent-table.unit.test.d.ts.map +0 -1
  146. package/dist/test/agent-table.unit.test.js +0 -53
  147. package/dist/test/agent-table.unit.test.js.map +0 -1
  148. package/dist/test/bind-retry.unit.test.d.ts +0 -2
  149. package/dist/test/bind-retry.unit.test.d.ts.map +0 -1
  150. package/dist/test/bind-retry.unit.test.js +0 -42
  151. package/dist/test/bind-retry.unit.test.js.map +0 -1
  152. package/dist/test/default-command.unit.test.d.ts +0 -2
  153. package/dist/test/default-command.unit.test.d.ts.map +0 -1
  154. package/dist/test/default-command.unit.test.js +0 -26
  155. package/dist/test/default-command.unit.test.js.map +0 -1
  156. package/dist/test/delegation-policy.unit.test.d.ts +0 -5
  157. package/dist/test/delegation-policy.unit.test.d.ts.map +0 -1
  158. package/dist/test/delegation-policy.unit.test.js +0 -52
  159. package/dist/test/delegation-policy.unit.test.js.map +0 -1
  160. package/dist/test/display-name.unit.test.d.ts +0 -5
  161. package/dist/test/display-name.unit.test.d.ts.map +0 -1
  162. package/dist/test/display-name.unit.test.js +0 -65
  163. package/dist/test/display-name.unit.test.js.map +0 -1
  164. package/dist/test/machine-list-groups.unit.test.d.ts +0 -5
  165. package/dist/test/machine-list-groups.unit.test.d.ts.map +0 -1
  166. package/dist/test/machine-list-groups.unit.test.js +0 -61
  167. package/dist/test/machine-list-groups.unit.test.js.map +0 -1
  168. package/dist/test/relay-selection.unit.test.d.ts +0 -5
  169. package/dist/test/relay-selection.unit.test.d.ts.map +0 -1
  170. package/dist/test/relay-selection.unit.test.js +0 -103
  171. package/dist/test/relay-selection.unit.test.js.map +0 -1
  172. package/dist/test/request-timeouts.unit.test.d.ts +0 -2
  173. package/dist/test/request-timeouts.unit.test.d.ts.map +0 -1
  174. package/dist/test/request-timeouts.unit.test.js +0 -28
  175. package/dist/test/request-timeouts.unit.test.js.map +0 -1
  176. package/dist/test/room-discovery.unit.test.d.ts +0 -2
  177. package/dist/test/room-discovery.unit.test.d.ts.map +0 -1
  178. package/dist/test/room-discovery.unit.test.js +0 -29
  179. package/dist/test/room-discovery.unit.test.js.map +0 -1
  180. package/dist/test/room-lookup.unit.test.d.ts +0 -2
  181. package/dist/test/room-lookup.unit.test.d.ts.map +0 -1
  182. package/dist/test/room-lookup.unit.test.js +0 -59
  183. package/dist/test/room-lookup.unit.test.js.map +0 -1
  184. package/dist/test/room-path.unit.test.d.ts +0 -2
  185. package/dist/test/room-path.unit.test.d.ts.map +0 -1
  186. package/dist/test/room-path.unit.test.js +0 -54
  187. package/dist/test/room-path.unit.test.js.map +0 -1
  188. package/dist/test/send-outcome.unit.test.d.ts +0 -2
  189. package/dist/test/send-outcome.unit.test.d.ts.map +0 -1
  190. package/dist/test/send-outcome.unit.test.js +0 -52
  191. package/dist/test/send-outcome.unit.test.js.map +0 -1
  192. package/src/bridges/user/web/port-discovery.ts +0 -49
@@ -1 +1 @@
1
- (function(){var e=`agent-comms-v1`,t=[{"revision":"194357f4b022a304e068ddaeeb4cf799","url":"mesh-worker.js"},{"revision":"f1ee3afb8c9e8f0d7315fd93d6457316","url":"index.html"},{"revision":"f1e3596f5e09029800e9862a7cb9335a","url":"bundle.js"}].map(e=>e.url);self.addEventListener(`install`,n=>{n.waitUntil(self.caches.open(e).then(async e=>e.addAll(t)).then(async()=>self.skipWaiting()))}),self.addEventListener(`activate`,t=>{t.waitUntil(self.caches.keys().then(async t=>Promise.all(t.filter(t=>t!==e).map(async e=>self.caches.delete(e)))))});var n=200,r=300;function i(e,t){return e.method===`GET`&&t.status>=n&&t.status<r}self.addEventListener(`fetch`,t=>{if(new URL(t.request.url).pathname.startsWith(`/api/`)){t.respondWith(fetch(t.request).then(n=>{if(i(t.request,n)){let r=n.clone();self.caches.open(e).then(async e=>e.put(t.request,r))}return n}).catch(async()=>self.caches.open(e).then(async e=>e.match(t.request)).then(e=>e??new Response(`Offline`,{status:503}))));return}t.respondWith(self.caches.open(e).then(async e=>e.match(t.request)).then(async n=>n||fetch(t.request).then(n=>{if(i(t.request,n)){let r=n.clone();self.caches.open(e).then(async e=>e.put(t.request,r))}return n})))})})();
1
+ (function(){var e=`agent-comms-v1`,t=[{"revision":"194357f4b022a304e068ddaeeb4cf799","url":"mesh-worker.js"},{"revision":"f1ee3afb8c9e8f0d7315fd93d6457316","url":"index.html"},{"revision":"89590c500b7ccdfdb97fec446e4a685b","url":"bundle.js"}].map(e=>e.url);self.addEventListener(`install`,n=>{n.waitUntil(self.caches.open(e).then(async e=>e.addAll(t)).then(async()=>self.skipWaiting()))}),self.addEventListener(`activate`,t=>{t.waitUntil(self.caches.keys().then(async t=>Promise.all(t.filter(t=>t!==e).map(async e=>self.caches.delete(e)))))});var n=200,r=300;function i(e,t){return e.method===`GET`&&t.status>=n&&t.status<r}self.addEventListener(`fetch`,t=>{if(new URL(t.request.url).pathname.startsWith(`/api/`)){t.respondWith(fetch(t.request).then(n=>{if(i(t.request,n)){let r=n.clone();self.caches.open(e).then(async e=>e.put(t.request,r))}return n}).catch(async()=>self.caches.open(e).then(async e=>e.match(t.request)).then(e=>e??new Response(`Offline`,{status:503}))));return}t.respondWith(self.caches.open(e).then(async e=>e.match(t.request)).then(async n=>n||fetch(t.request).then(n=>{if(i(t.request,n)){let r=n.clone();self.caches.open(e).then(async e=>e.put(t.request,r))}return n})))})})();
@@ -8,6 +8,7 @@
8
8
  * When AGENT_COMMS_WEB_CONSOLE_DIST points at a built wire-mesh web-console (see web-console-static.ts), GET /web-console/* also serves that as an alternate, generic reference-client UI alongside the frontend above.
9
9
  */
10
10
 
11
+ import { once } from "node:events";
11
12
  import * as http from "node:http";
12
13
  import { fileURLToPath } from "node:url";
13
14
  import { WebSocketServer } from "ws";
@@ -56,6 +57,7 @@ import {
56
57
  type AccessDecision,
57
58
  type AccessPolicy,
58
59
  } from "./web-access.js";
60
+ import { bridgePortRange } from "./bridge-port-range.js";
59
61
  import { startWebBeacon } from "./web-beacon.js";
60
62
  import {
61
63
  createFrontendAssetLoader,
@@ -112,7 +114,7 @@ export interface WebServerHandle {
112
114
  accessToken: string | undefined;
113
115
  }
114
116
 
115
- /** Resolve the listening port from a running web server handle, or undefined if the OS hasn't assigned one yet. */
117
+ /** The port a web server handle holds. A handle is returned only once its server is listening, so this is undefined only after the server has been closed. */
116
118
  export function getWebPort(handle: WebServerHandle): number | undefined {
117
119
  const addr = handle.server.address();
118
120
  return typeof addr === "object" && addr ? addr.port : undefined;
@@ -135,14 +137,15 @@ export function getWebUrlStatus(
135
137
  /**
136
138
  * Starts the web UI for a bridge on top of the bridge's own store, so the process stays one mesh peer and one agent (the agent id is the store's peer id, before or after the agent registers). Every bridge entry point that owns a store uses this, and only the standalone dashboard command builds a controller, and so a store, of its own.
137
139
  *
138
- * The server listens on an OS-assigned free port (port 0) to avoid TOCTOU races when multiple bridges start web servers concurrently: the OS atomically allocates a unique free port for each. The PWA discovery path (probe from 19877) is only used when the page is served from a non-local host (e.g. GitHub Pages); when served locally, the browser connects via location.host directly, so the port number doesn't need to be predictable.
140
+ * The server binds the first free port of the bridge port range (see bridge-port-range.ts), which is where the hosted dashboard looks for a bridge, and takes an operating-system port only when the whole range is taken. Each port is bound for real rather than tested first, so bridges starting at the same moment cannot be handed the same port. `options.bridgePortBase` is the first port of the range to bind from; it defaults to the range the dashboard probes, and tests pass a free range of their own.
139
141
  */
140
142
  export async function tryStartBridgeWebServer(
141
143
  store: MeshStore,
142
144
  harness: string,
145
+ options?: Readonly<{ bridgePortBase?: number | undefined }>,
143
146
  ): Promise<WebServerHandle | undefined> {
144
147
  return createWebServer({
145
- port: 0,
148
+ bridgePortBase: options?.bridgePortBase,
146
149
  existingController: ChatController.fromExisting(store, {
147
150
  agentId: store.peerId,
148
151
  harness,
@@ -152,8 +155,72 @@ export async function tryStartBridgeWebServer(
152
155
  });
153
156
  }
154
157
 
158
+ /** Whether a listen failure means the port is already taken. */
159
+ function isAddressInUse(error: unknown): boolean {
160
+ return (
161
+ typeof error === "object" &&
162
+ error !== null &&
163
+ "code" in error &&
164
+ error.code === "EADDRINUSE"
165
+ );
166
+ }
167
+
168
+ /** Binds `server` to `port` on `host`; true once listening, false when the port is taken, and rejects on any other failure. */
169
+ async function tryListen(
170
+ server: http.Server,
171
+ port: number,
172
+ host: string,
173
+ ): Promise<boolean> {
174
+ // once() rejects when the server emits "error" before "listening", and removes both of its listeners either way.
175
+ const listening = once(server, "listening");
176
+ server.listen(port, host);
177
+ try {
178
+ await listening;
179
+ return true;
180
+ } catch (error) {
181
+ if (isAddressInUse(error)) return false;
182
+ throw error;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Binds `server` to the first of `ports` that is free, in order, and returns the port it holds.
188
+ *
189
+ * Each port is bound for real, so a port another process takes between two attempts is simply skipped, where a test-then-bind would hand two processes the same port. Only "already in use" moves on to the next port: any other bind error rejects with that error. When `fallBackToAnyPort` is set and every port is taken the operating system picks one; when nothing could be bound the error names every port tried.
190
+ */
191
+ async function listen(
192
+ server: http.Server,
193
+ host: string,
194
+ ports: readonly number[],
195
+ fallBackToAnyPort: boolean,
196
+ ): Promise<number> {
197
+ for (const port of ports) {
198
+ if (await tryListen(server, port, host)) return boundPortOf(server);
199
+ }
200
+ if (fallBackToAnyPort && (await tryListen(server, 0, host))) {
201
+ return boundPortOf(server);
202
+ }
203
+ const tried = fallBackToAnyPort
204
+ ? [...ports.map(String), "an operating-system port"]
205
+ : ports.map(String);
206
+ throw new Error(
207
+ `Agent Comms web UI: every port tried is already in use on ${host}: ${tried.join(", ")}`,
208
+ );
209
+ }
210
+
211
+ /** The port a listening server holds. */
212
+ function boundPortOf(server: http.Server): number {
213
+ const addr = server.address();
214
+ if (typeof addr !== "object" || addr === null) {
215
+ throw new Error("Agent Comms web UI: the listening server has no address");
216
+ }
217
+ return addr.port;
218
+ }
219
+
155
220
  /**
156
- * Create and start the web server on a dynamic port.
221
+ * Create the web server and resolve once it is listening.
222
+ *
223
+ * Without an explicit `port` it binds the first free port of the bridge port range, falling back to an operating-system port when the whole range is taken. An explicit `port` (0 for an operating-system port) is bound as given, and a port that is taken rejects instead of moving on. When binding fails nothing is left behind: the server is bound before the publisher, the listeners and the websocket server are created, and a controller this call created is shut down.
157
224
  *
158
225
  * Accepts an optional ChatController for reuse — bridges that already
159
226
  * have a MeshStore and agent identity pass theirs in so the web UI
@@ -162,7 +229,9 @@ export async function tryStartBridgeWebServer(
162
229
  * Dashboard controller is created.
163
230
  */
164
231
  export async function createWebServer(options?: {
165
- port?: number;
232
+ port?: number | undefined;
233
+ /** The first port of the bridge port range tried when no explicit `port` is given. Defaults to the range the hosted dashboard probes; tests pass a free range so they neither wait on nor disturb a bridge running on the machine. */
234
+ bridgePortBase?: number | undefined;
166
235
  /** The IP address to bind. Defaults to AGENT_COMMS_WEB_HOST, then loopback. A non-loopback address makes the server reachable from the LAN and turns on the access token (see web-access.ts). */
167
236
  host?: string | undefined;
168
237
  /** The addresses whose clients count as on this machine besides loopback. Defaults to every interface address of this machine; tests replace it to make a client on the machine's own address stand in for a remote device (see AccessPolicy.localClientAddresses). */
@@ -177,7 +246,8 @@ export async function createWebServer(options?: {
177
246
  hubUrl?: string | undefined;
178
247
  }): Promise<WebServerHandle> {
179
248
  const {
180
- port = 0,
249
+ port,
250
+ bridgePortBase,
181
251
  host: requestedHost,
182
252
  localClientAddresses = ownInterfaceAddresses,
183
253
  beaconPort,
@@ -204,11 +274,11 @@ export async function createWebServer(options?: {
204
274
  ? generateAccessToken()
205
275
  : undefined;
206
276
 
207
- // The port is only known once the server listens (port 0 asks the OS), so the policy's port is filled in by the listen callback, before any request can arrive.
277
+ // The port is only known once the server listens (port 0 asks the OS), so the policy's port is filled in once the server is bound, before any request can arrive.
208
278
  const accessPolicy: AccessPolicy = {
209
279
  token: accessToken,
210
280
  bindHost: host,
211
- port,
281
+ port: port ?? 0,
212
282
  localClientAddresses,
213
283
  };
214
284
 
@@ -217,6 +287,21 @@ export async function createWebServer(options?: {
217
287
  handleRequest(req, res, controller, webConsoleDist);
218
288
  });
219
289
 
290
+ let boundPort: number;
291
+ try {
292
+ boundPort = await listen(
293
+ server,
294
+ host,
295
+ port === undefined ? bridgePortRange(bridgePortBase) : [port],
296
+ port === undefined,
297
+ );
298
+ } catch (error) {
299
+ // A controller this call created is this call's to release; one passed in belongs to its caller.
300
+ if (!existingController) await controller.shutdown();
301
+ throw error;
302
+ }
303
+ accessPolicy.port = boundPort;
304
+
220
305
  const publisher = new MeshEventPublisher();
221
306
  ensurePatchListener(controller.meshStore, publisher);
222
307
  controller.on("message", (event: DeliveryEvent) => {
@@ -263,22 +348,16 @@ export async function createWebServer(options?: {
263
348
  });
264
349
  });
265
350
 
266
- server.listen(port, host, () => {
267
- const addr = server.address();
268
- accessPolicy.port = typeof addr === "object" && addr ? addr.port : port;
269
- // stderr, not stdout: a stdio MCP bridge owns stdout for JSON-RPC, so any other line there is protocol noise to a strictly framed client.
270
- console.error(
271
- `Agent Comms web UI: ${localWebUrl(host, accessPolicy.port)}`,
351
+ // stderr, not stdout: a stdio MCP bridge owns stdout for JSON-RPC, so any other line there is protocol noise to a strictly framed client.
352
+ console.error(`Agent Comms web UI: ${localWebUrl(host, boundPort)}`);
353
+ if (accessToken !== undefined) {
354
+ announceLanAccess(
355
+ server,
356
+ { ...accessPolicy, token: accessToken },
357
+ controller.meshStore.peerId,
358
+ beaconPort,
272
359
  );
273
- if (accessToken !== undefined) {
274
- announceLanAccess(
275
- server,
276
- { ...accessPolicy, token: accessToken },
277
- controller.meshStore.peerId,
278
- beaconPort,
279
- );
280
- }
281
- });
360
+ }
282
361
 
283
362
  return { server, controller, wss, publisher, host, accessToken };
284
363
  }
@@ -363,13 +442,9 @@ function announceLanAccess(
363
442
  // Standalone mode — `npx agent-comms chat`
364
443
  // ---------------------------------------------------------------------------
365
444
 
366
- export async function runWeb(userName: string, port = 0): Promise<void> {
445
+ export async function runWeb(userName: string, port?: number): Promise<void> {
367
446
  const handle = await createWebServer({ port });
368
- handle.server.on("listening", () => {
369
- console.log(
370
- `Connected as ${userName} (user) [${handle.controller.agentId}]`,
371
- );
372
- });
447
+ console.log(`Connected as ${userName} (user) [${handle.controller.agentId}]`);
373
448
 
374
449
  // Graceful shutdown
375
450
  const cleanup = async (): Promise<void> => {
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Starting and stopping a bridge's web server on its own store, as a bridge starts it, for tests of the port range and of what probes it.
3
+ */
4
+ import { MeshStore } from "../../../../core/mesh-store.js";
5
+ import {
6
+ freeLocalPort,
7
+ type TeardownStack,
8
+ } from "../../../../test/hub-helpers.js";
9
+ import { wireTestTransport } from "../../../../test/test-transport.js";
10
+ import { tryStartBridgeWebServer, type WebServerHandle } from "../server.js";
11
+
12
+ /** Closes the web socket server and the HTTP server of a handle, whether or not it is still listening. */
13
+ export async function stopWebServer(handle: WebServerHandle): Promise<void> {
14
+ await new Promise<void>((resolve) => {
15
+ handle.wss.close(() => {
16
+ resolve();
17
+ });
18
+ });
19
+ if (!handle.server.listening) return;
20
+ await new Promise<void>((resolve) => {
21
+ handle.server.close(() => {
22
+ resolve();
23
+ });
24
+ handle.server.closeAllConnections();
25
+ });
26
+ }
27
+
28
+ /** A mesh store with an isolated coordinator and first-contact port, shut down by `cleanups`. */
29
+ export async function startTestStore(
30
+ cleanups: Readonly<TeardownStack>,
31
+ ): Promise<MeshStore> {
32
+ const store = new MeshStore({
33
+ coordinatorPort: await freeLocalPort(),
34
+ firstContactPort: await freeLocalPort(),
35
+ });
36
+ await wireTestTransport(store);
37
+ await store.init();
38
+ cleanups.push(async () => store.shutdown());
39
+ return store;
40
+ }
41
+
42
+ /** A bridge's web server on a store of its own, bound from `bridgePortBase` and stopped by `cleanups`. */
43
+ export async function startTestBridge(
44
+ cleanups: Readonly<TeardownStack>,
45
+ bridgePortBase: number,
46
+ ): Promise<WebServerHandle> {
47
+ const store = await startTestStore(cleanups);
48
+ const handle = await tryStartBridgeWebServer(store, "test", {
49
+ bridgePortBase,
50
+ });
51
+ if (handle === undefined) throw new Error("expected the web server to start");
52
+ cleanups.push(async () => stopWebServer(handle));
53
+ return handle;
54
+ }
@@ -58,6 +58,8 @@ export interface BridgeRunOptions {
58
58
  mesh?: BridgeMeshOptions | undefined;
59
59
  /** The identity slot. Defaults to this process's working directory under the bridge's own harness. */
60
60
  slot?: IdentitySlot | undefined;
61
+ /** The first port of the bridge port range the bridge's web server binds from; see bridge-port-range.ts. Defaults to the range the hosted dashboard probes, so only a test that must not bind it sets this. */
62
+ webPortBase?: number | undefined;
61
63
  }
62
64
 
63
65
  /** The synchronous half of bridge construction: everything loadOrCreateIdentity's own synchronous key material makes possible. Use this directly only when the calling entry point cannot await inline (see this file's own header comment); every other caller should use createBridgeMesh below. */
@@ -162,9 +162,38 @@ class BuildActionError extends Error {
162
162
  }
163
163
  }
164
164
 
165
+ /** Thrown by buildAction for parameters MCP_TOOL_PARAMS rejects, so a malformed tool call reaches its caller as an error naming what is wrong and is never run as some other action. `issues` is every problem found, each with the path of the parameter at fault. */
166
+ export class InvalidToolParamsError extends Error {
167
+ constructor(
168
+ readonly issues: readonly {
169
+ readonly path: readonly (string | number)[];
170
+ readonly message: string;
171
+ }[],
172
+ ) {
173
+ super(
174
+ `Invalid parameters: ${issues
175
+ .map(
176
+ (issue) =>
177
+ `${issue.path.length === 0 ? "(root)" : issue.path.join(".")}: ${issue.message}`,
178
+ )
179
+ .join("; ")}`,
180
+ );
181
+ this.name = "InvalidToolParamsError";
182
+ }
183
+ }
184
+
165
185
  export function buildAction(params: Record<string, unknown>): CommsAction {
166
186
  const parsed = MCP_TOOL_PARAMS.safeParse(params);
167
- if (!parsed.success) return { action: "whoami" };
187
+ if (!parsed.success) {
188
+ throw new InvalidToolParamsError(
189
+ parsed.error.issues.map((issue) => ({
190
+ path: issue.path.map((step) =>
191
+ typeof step === "number" ? step : String(step),
192
+ ),
193
+ message: issue.message,
194
+ })),
195
+ );
196
+ }
168
197
  const p = parsed.data;
169
198
 
170
199
  switch (p.action) {
package/src/core/index.ts CHANGED
@@ -36,6 +36,7 @@ export type { IdentitySlot } from "./identity-store.js";
36
36
  export type { CommsContext, CommsResult } from "./tool.js";
37
37
  export {
38
38
  buildAction,
39
+ InvalidToolParamsError,
39
40
  formatDeliveryEvent,
40
41
  extractStreamingBehavior,
41
42
  isActionableEvent,
package/src/core/tool.ts CHANGED
@@ -89,7 +89,7 @@ export interface CommsContext {
89
89
  pid: number;
90
90
  }
91
91
 
92
- /** The calling bridge's own web UI address, as reported by the web_url action -- mirrors comms-url's own three-way distinction between a server that was never started, one whose OS-assigned port hasn't come back yet, and one that's actually reachable. */
92
+ /** The calling bridge's own web UI address, as reported by the web_url action; mirrors comms-url's own three-way distinction between a server that was never started, one with no port because it is not listening, and one that's actually reachable. */
93
93
  export type WebUrlStatus =
94
94
  | { kind: "not_running" }
95
95
  | { kind: "pending" }
@@ -1,14 +0,0 @@
1
- /**
2
- * Port discovery — walk up from a base port to find a free one.
3
- *
4
- * Extracted from server.ts so the logic can be unit-tested without
5
- * loading the full server module (which reads built assets at import time).
6
- */
7
- /** Maximum number of ports to try when searching for a free one. */
8
- export declare const DEFAULT_MAX_ATTEMPTS = 10;
9
- /**
10
- * Try binding sequentially from `base`.
11
- * Returns the first port that succeeds, or undefined if all are taken.
12
- */
13
- export declare function findFreePort(base: number, maxAttempts?: number): Promise<number | undefined>;
14
- //# sourceMappingURL=port-discovery.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"port-discovery.d.ts","sourceRoot":"","sources":["../../../../src/bridges/user/web/port-discovery.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,oEAAoE;AACpE,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,MAA6B,GACzC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA2B7B"}
@@ -1,40 +0,0 @@
1
- /**
2
- * Port discovery — walk up from a base port to find a free one.
3
- *
4
- * Extracted from server.ts so the logic can be unit-tested without
5
- * loading the full server module (which reads built assets at import time).
6
- */
7
- import * as http from "node:http";
8
- const WEB_HOST = "127.0.0.1";
9
- /** Maximum number of ports to try when searching for a free one. */
10
- export const DEFAULT_MAX_ATTEMPTS = 10;
11
- /**
12
- * Try binding sequentially from `base`.
13
- * Returns the first port that succeeds, or undefined if all are taken.
14
- */
15
- export async function findFreePort(base, maxAttempts = DEFAULT_MAX_ATTEMPTS) {
16
- return new Promise((resolve) => {
17
- let attempts = 0;
18
- function tryPort(port) {
19
- if (attempts >= maxAttempts) {
20
- resolve(undefined);
21
- return;
22
- }
23
- attempts++;
24
- const probe = http.createServer();
25
- probe.on("error", () => {
26
- probe.close();
27
- tryPort(port + 1);
28
- });
29
- probe.listen(port, WEB_HOST, () => {
30
- const addr = probe.address();
31
- const actualPort = typeof addr === "object" && addr ? addr.port : port;
32
- probe.close(() => {
33
- resolve(actualPort);
34
- });
35
- });
36
- }
37
- tryPort(base);
38
- });
39
- }
40
- //# sourceMappingURL=port-discovery.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"port-discovery.js","sourceRoot":"","sources":["../../../../src/bridges/user/web/port-discovery.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,MAAM,QAAQ,GAAG,WAAW,CAAC;AAE7B,oEAAoE;AACpE,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEvC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAY,EACZ,cAAsB,oBAAoB;IAE1C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,QAAQ,GAAG,CAAC,CAAC;QAEjB,SAAS,OAAO,CAAC,IAAY;YAC3B,IAAI,QAAQ,IAAI,WAAW,EAAE,CAAC;gBAC5B,OAAO,CAAC,SAAS,CAAC,CAAC;gBACnB,OAAO;YACT,CAAC;YACD,QAAQ,EAAE,CAAC;YAEX,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YAClC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBACrB,KAAK,CAAC,KAAK,EAAE,CAAC;gBACd,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;YACpB,CAAC,CAAC,CAAC;YACH,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE;gBAChC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;gBAC7B,MAAM,UAAU,GAAG,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;gBACvE,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE;oBACf,OAAO,CAAC,UAAU,CAAC,CAAC;gBACtB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -1,5 +0,0 @@
1
- /**
2
- * Unit tests for port-discovery.ts — sequential port probing.
3
- */
4
- export {};
5
- //# sourceMappingURL=port-discovery.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"port-discovery.unit.test.d.ts","sourceRoot":"","sources":["../../../../../src/bridges/user/web/test/port-discovery.unit.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -1,180 +0,0 @@
1
- /**
2
- * Unit tests for port-discovery.ts — sequential port probing.
3
- */
4
- import { describe, it, expect } from "vitest";
5
- import * as http from "node:http";
6
- import { findFreePort } from "../port-discovery.js";
7
- /** How many times to retry a single bind attempt against a transient EADDRINUSE before giving up for real. */
8
- const BLOCK_PORT_MAX_ATTEMPTS = 10;
9
- /** Delay between retries, long enough for a port an unrelated process grabbed as its own ephemeral source port to be released again. Raised from 50ms/5 attempts (a ~250ms total window) after that budget still wasn't always enough on a loaded CI runner -- 10 attempts at 100ms gives a ~1s window before genuinely giving up. */
10
- const BLOCK_PORT_RETRY_DELAY_MS = 100;
11
- function isErrnoException(error) {
12
- return error instanceof Error && "code" in error;
13
- }
14
- async function delay(ms) {
15
- return new Promise((resolve) => {
16
- setTimeout(resolve, ms);
17
- });
18
- }
19
- /**
20
- * Create a server that blocks a specific port. Returns a cleanup function.
21
- *
22
- * These tests deliberately use fixed port numbers (kept below PORT_RANGE_SAFE_CEILING, see below) so findFreePort's own sequential-scan behaviour is exercised deterministically. That keeps them clear of the OS's own ephemeral/dynamic port allocation range, but retrying briefly on EADDRINUSE is kept as a second line of defence for any other transient bind contention (e.g. another process on the same fixed port left over from a prior run) without weakening what the test actually asserts.
23
- */
24
- async function blockPort(port) {
25
- for (let attempt = 1;; attempt++) {
26
- try {
27
- return await new Promise((resolve, reject) => {
28
- const server = http.createServer();
29
- server.on("error", reject);
30
- server.listen(port, "127.0.0.1", () => {
31
- resolve(async () => {
32
- return new Promise((res) => {
33
- server.close(() => res());
34
- });
35
- });
36
- });
37
- });
38
- }
39
- catch (error) {
40
- const isAddrInUse = isErrnoException(error) && error.code === "EADDRINUSE";
41
- if (!isAddrInUse || attempt >= BLOCK_PORT_MAX_ATTEMPTS) {
42
- throw error;
43
- }
44
- await delay(BLOCK_PORT_RETRY_DELAY_MS);
45
- }
46
- }
47
- }
48
- /** Ceiling every fixture port below must stay under: the lower bound of the ephemeral/dynamic port range both Linux (default 32768-60999, via net.ipv4.ip_local_port_range) and macOS (default 49152-65535, via net.inet.ip.portrange.first/last) draw from when the kernel auto-assigns a source port to an outbound connection. A fixture port inside either range can be transiently claimed by a completely unrelated process's outbound socket at the exact moment a test tries to bind it as a listening server, producing a genuine EADDRINUSE race no amount of retrying fully closes out. Staying below it removes the collision domain outright rather than tolerating it. */
49
- const PORT_RANGE_SAFE_CEILING = 32768;
50
- /** Base port for the "base port is free" scenario. */
51
- const TEST_PORT_AVAILABLE_BASE = 20000;
52
- /** Base port for the "single port taken" scenario. */
53
- const TEST_PORT_SINGLE_TAKEN_BASE = 20100;
54
- /** The port findFreePort should land on once TEST_PORT_SINGLE_TAKEN_BASE is blocked. */
55
- const TEST_PORT_SINGLE_TAKEN_RESULT = 20101;
56
- /** Base port for the "multiple consecutive ports taken" scenario. */
57
- const TEST_PORT_MULTIPLE_TAKEN_BASE = 20200;
58
- /** Second port blocked in the "multiple consecutive ports taken" scenario. */
59
- const TEST_PORT_MULTIPLE_TAKEN_SECOND = 20201;
60
- /** Third port blocked in the "multiple consecutive ports taken" scenario. */
61
- const TEST_PORT_MULTIPLE_TAKEN_THIRD = 20202;
62
- /** The port findFreePort should land on once all three ports above are blocked. */
63
- const TEST_PORT_MULTIPLE_TAKEN_RESULT = 20203;
64
- /** Base port for the "every port in range is taken" scenario. */
65
- const TEST_PORT_ALL_TAKEN_BASE = 20300;
66
- /** How many consecutive ports get blocked, and the maxAttempts passed to findFreePort, in the "every port in range is taken" scenario -- both are the same number by design so the scan exhausts exactly the blocked range. */
67
- const TEST_ALL_TAKEN_PORT_COUNT = 3;
68
- /** Base port for the "maxAttempts allows walking past taken ports" scenario. */
69
- const TEST_PORT_MAX_ATTEMPTS_BASE = 20400;
70
- /** Second port blocked in the "maxAttempts allows walking past taken ports" scenario. */
71
- const TEST_PORT_MAX_ATTEMPTS_SECOND = 20401;
72
- /** maxAttempts passed to findFreePort in the "maxAttempts allows walking past taken ports" scenario -- deliberately larger than the number of blocked ports so the scan walks past them. */
73
- const TEST_MAX_ATTEMPTS_LIMIT = 5;
74
- /** The port findFreePort should land on once both ports above are blocked. */
75
- const TEST_PORT_MAX_ATTEMPTS_RESULT = 20402;
76
- /** Port a test transiently occupies to prove blockPort's own EADDRINUSE retry, not luck, is what recovers. */
77
- const TEST_PORT_TRANSIENT_CONTENTION = 20500;
78
- /** Every literal fixture port declared above, gathered so the ephemeral-range invariant can be checked once for all of them rather than per-constant. */
79
- const ALL_FIXTURE_PORTS = [
80
- TEST_PORT_AVAILABLE_BASE,
81
- TEST_PORT_SINGLE_TAKEN_BASE,
82
- TEST_PORT_SINGLE_TAKEN_RESULT,
83
- TEST_PORT_MULTIPLE_TAKEN_BASE,
84
- TEST_PORT_MULTIPLE_TAKEN_SECOND,
85
- TEST_PORT_MULTIPLE_TAKEN_THIRD,
86
- TEST_PORT_MULTIPLE_TAKEN_RESULT,
87
- TEST_PORT_ALL_TAKEN_BASE,
88
- TEST_PORT_MAX_ATTEMPTS_BASE,
89
- TEST_PORT_MAX_ATTEMPTS_SECOND,
90
- TEST_PORT_MAX_ATTEMPTS_RESULT,
91
- TEST_PORT_TRANSIENT_CONTENTION,
92
- ];
93
- describe("findFreePort fixture ports", () => {
94
- it("all stay below the OS ephemeral port range", () => {
95
- for (const port of ALL_FIXTURE_PORTS) {
96
- expect(port).toBeLessThan(PORT_RANGE_SAFE_CEILING);
97
- }
98
- });
99
- });
100
- describe("blockPort", () => {
101
- it("recovers from a transient EADDRINUSE instead of failing on the first attempt", async () => {
102
- const contender = http.createServer();
103
- await new Promise((resolve, reject) => {
104
- contender.on("error", reject);
105
- contender.listen(TEST_PORT_TRANSIENT_CONTENTION, "127.0.0.1", () => {
106
- resolve();
107
- });
108
- });
109
- // Release the port partway through blockPort's own retry window, well before it gives up, so the recovery below is provably the retry loop's own doing rather than the contender happening to already be gone before blockPort's first attempt.
110
- setTimeout(() => {
111
- contender.close();
112
- }, BLOCK_PORT_RETRY_DELAY_MS * 2);
113
- const unblock = await blockPort(TEST_PORT_TRANSIENT_CONTENTION);
114
- await unblock();
115
- });
116
- });
117
- describe("findFreePort", () => {
118
- it("returns the base port when it is available", async () => {
119
- const port = await findFreePort(TEST_PORT_AVAILABLE_BASE);
120
- expect(typeof port).toBe("number");
121
- expect(port).toBe(TEST_PORT_AVAILABLE_BASE);
122
- });
123
- it("walks to the next port when the base is taken", async () => {
124
- const unblock = await blockPort(TEST_PORT_SINGLE_TAKEN_BASE);
125
- try {
126
- const port = await findFreePort(TEST_PORT_SINGLE_TAKEN_BASE);
127
- expect(typeof port).toBe("number");
128
- expect(port).toBe(TEST_PORT_SINGLE_TAKEN_RESULT);
129
- }
130
- finally {
131
- await unblock();
132
- }
133
- });
134
- it("skips multiple taken ports", async () => {
135
- const unblock1 = await blockPort(TEST_PORT_MULTIPLE_TAKEN_BASE);
136
- const unblock2 = await blockPort(TEST_PORT_MULTIPLE_TAKEN_SECOND);
137
- const unblock3 = await blockPort(TEST_PORT_MULTIPLE_TAKEN_THIRD);
138
- try {
139
- const port = await findFreePort(TEST_PORT_MULTIPLE_TAKEN_BASE);
140
- expect(typeof port).toBe("number");
141
- expect(port).toBe(TEST_PORT_MULTIPLE_TAKEN_RESULT);
142
- }
143
- finally {
144
- await unblock1();
145
- await unblock2();
146
- await unblock3();
147
- }
148
- });
149
- it("returns undefined when all ports in range are taken", async () => {
150
- // Block TEST_ALL_TAKEN_PORT_COUNT ports with maxAttempts=TEST_ALL_TAKEN_PORT_COUNT
151
- const blockers = [];
152
- for (let i = 0; i < TEST_ALL_TAKEN_PORT_COUNT; i++) {
153
- blockers.push(await blockPort(TEST_PORT_ALL_TAKEN_BASE + i));
154
- }
155
- try {
156
- const port = await findFreePort(TEST_PORT_ALL_TAKEN_BASE, TEST_ALL_TAKEN_PORT_COUNT);
157
- expect(port).toBe(undefined);
158
- }
159
- finally {
160
- for (const unblock of blockers) {
161
- await unblock();
162
- }
163
- }
164
- });
165
- it("respects the maxAttempts parameter", async () => {
166
- // Block 2 ports but allow TEST_MAX_ATTEMPTS_LIMIT attempts — should walk past them
167
- const unblock1 = await blockPort(TEST_PORT_MAX_ATTEMPTS_BASE);
168
- const unblock2 = await blockPort(TEST_PORT_MAX_ATTEMPTS_SECOND);
169
- try {
170
- const port = await findFreePort(TEST_PORT_MAX_ATTEMPTS_BASE, TEST_MAX_ATTEMPTS_LIMIT);
171
- expect(typeof port).toBe("number");
172
- expect(port).toBe(TEST_PORT_MAX_ATTEMPTS_RESULT);
173
- }
174
- finally {
175
- await unblock1();
176
- await unblock2();
177
- }
178
- });
179
- });
180
- //# sourceMappingURL=port-discovery.unit.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"port-discovery.unit.test.js","sourceRoot":"","sources":["../../../../../src/bridges/user/web/test/port-discovery.unit.test.ts"],"names":[],"mappings":"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"}
@@ -1,5 +0,0 @@
1
- /**
2
- * Unit tests for bridge.ts — formatDeliveryEvent.
3
- */
4
- export {};
5
- //# sourceMappingURL=bridge.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bridge.unit.test.d.ts","sourceRoot":"","sources":["../../../src/core/test/bridge.unit.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}