From 46348d33c5c55ddb02fe1c082237e56df5eab57a Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Wed, 9 Sep 2026 07:11:45 +0100 Subject: [PATCH 1/2] test(core): run every tls-transport scenario when no test name is given The runner required a scenario name and exited with a usage message otherwise, so the suite could not be run as a whole. Bare invocation now runs all scenarios in order; passing a name still selects a single scenario. --- src/test/tls-transport.integration.test.ts | 55 ++++++++++++---------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/test/tls-transport.integration.test.ts b/src/test/tls-transport.integration.test.ts index 4f1100c..bd83d56 100644 --- a/src/test/tls-transport.integration.test.ts +++ b/src/test/tls-transport.integration.test.ts @@ -2,7 +2,8 @@ * TlsTransport integration test — verifies that two MeshStore instances * can communicate over TLS with certificate pinning. * - * Run: node dist/test/tls-transport.integration.test.js + * Run: node dist/test/tls-transport.integration.test.js [test-name] + * With no argument, every scenario runs in order. */ import * as net from "node:net"; @@ -156,38 +157,42 @@ async function testFingerprintIsPeerId(): Promise { // --------------------------------------------------------------------------- const testName = process.argv[2]; -if (testName === undefined) { - console.error("Usage: node tls-transport.integration.test.ts "); - process.exit(1); -} const tests: Record Promise> = { "tls-communication": testTlsPeerCommunication, "tls-fingerprint": testFingerprintIsPeerId, }; -const fn = tests[testName]; -if (!fn) { +const selected = + testName === undefined + ? Object.entries(tests) + : Object.entries(tests).filter(([name]) => name === testName); +if (selected.length === 0) { console.error(`Unknown test: ${testName}`); console.error(`Available: ${Object.keys(tests).join(", ")}`); process.exit(1); } -fn() - .then(async () => { - const maxWait = 2000; - const start = Date.now(); - while ( - (( - process as unknown as { _getActiveHandles?: () => unknown[] } - )._getActiveHandles?.()?.length ?? 0) > 0 && - Date.now() - start < maxWait - ) { - await new Promise((resolve) => setTimeout(resolve, 50)); - } - process.exit(0); - }) - .catch((err: unknown) => { - console.error(`FAIL [${testName}]:`, err); - process.exit(1); - }); +async function run(): Promise { + for (const [name, fn] of selected) { + console.log(`Running ${name}:`); + await fn(); + } + + const maxWait = 2000; + const start = Date.now(); + while ( + (( + process as unknown as { _getActiveHandles?: () => unknown[] } + )._getActiveHandles?.()?.length ?? 0) > 0 && + Date.now() - start < maxWait + ) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + process.exit(0); +} + +run().catch((err: unknown) => { + console.error(`FAIL [${testName ?? "all"}]:`, err); + process.exit(1); +}); From d9c50863fb0111dc4f408be8c769242ffd341f2d Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Wed, 9 Sep 2026 07:11:47 +0100 Subject: [PATCH 2/2] test(core): wire the mesh integration suites into CI and cover the ws queue pnpm test ran only the coordinator socket-error suite and the two identity unit suites; the mesh integration suites (mesh e2e, broadcast-window, identity-restart) ran only ad hoc, so the cross-visibility and broadcast-queue regressions they pin were invisible to CI. Run them all in one serial node --test invocation (--test-concurrency=1) so concurrent meshes cannot contend for CPU on shared runners, and include tls-transport now that it runs its full scenario set without an argument. The transport broadcast queue from the dial-window fix is implemented per transport but only the TLS path had coverage; mesh e2e exercises the TCP path and a new ws broadcast-window test covers the WebSocket path (two WS peers, immediate post-init registration, room-message push), which had no coverage at all. --- package.json | 2 +- .../ws-broadcast-window.integration.test.ts | 110 ++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/test/ws-broadcast-window.integration.test.ts diff --git a/package.json b/package.json index 10f4016..36d3841 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "generate:server-json": "tsx scripts/sync-release-metadata.ts", "publish:mcp-registry": "tsx scripts/publish-mcp-registry.ts", "lint": "eslint .", - "test": "node --test dist/test/coordinator-socket-error.integration.test.js dist/test/identity-store.test.js dist/test/identity-cert.test.js", + "test": "node --test --test-concurrency=1 dist/test/coordinator-socket-error.integration.test.js dist/test/identity-store.test.js dist/test/identity-cert.test.js dist/test/broadcast-window.integration.test.js dist/test/identity-restart.integration.test.js dist/test/mesh-e2e.integration.test.js dist/test/ws-broadcast-window.integration.test.js dist/test/tls-transport.integration.test.js", "test:visibility": "node --test dist/test/visibility.integration.test.js", "test:delivery": "node dist/test/delivery-receipt.runner.js", "test:all": "pnpm test && pnpm test:delivery", diff --git a/src/test/ws-broadcast-window.integration.test.ts b/src/test/ws-broadcast-window.integration.test.ts new file mode 100644 index 0000000..9f2166d --- /dev/null +++ b/src/test/ws-broadcast-window.integration.test.ts @@ -0,0 +1,110 @@ +/** + * Integration test for the WebSocket transport's broadcast queue: state patches broadcast before the WS data connections are established must be queued and flushed on registration, not dropped (#23). + * + * Mirrors broadcast-window.integration.test.ts over TlsTransport; the queue logic is implemented per transport, so each needs its own coverage. + */ + +import * as assert from "node:assert/strict"; +import { MeshStore } from "../core/mesh-store.js"; +import { WebSocketTransport } from "../core/ws-transport.js"; + +const TEST_PORT = 19892; +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +/** A peer wired like a WS-based participant (browser relay worker). */ +function makePeer(): MeshStore { + const store = new MeshStore(TEST_PORT); + store.setTransport(new WebSocketTransport(store.events)); + return store; +} + +/** Poll until the predicate holds, or fail with the message. */ +async function waitFor( + what: string, + check: () => Promise, +): Promise { + for (let i = 0; i < 20; i++) { + if (await check()) return; + await sleep(100); + } + assert.ok(false, `timed out waiting for ${what}`); +} + +async function main(): Promise { + // A is the coordinator and stays up throughout. + const a = makePeer(); + await a.init(); + await a.registerAgent({ + name: "peer-a", + harness: "user", + cwd: "/test/a", + pid: process.pid, + visibility: "visible", + tags: [], + }); + + // B joins and registers IMMEDIATELY after init() — no settle delay. This is exactly the pattern that used to race the dials and lose the upsert. + const b = makePeer(); + await b.init(); + await b.registerAgent({ + name: "peer-b", + harness: "user", + cwd: "/test/b", + pid: process.pid, + visibility: "visible", + tags: [], + }); + const bId = b.peerId; + + await waitFor( + "the coordinator to see the immediately-registered agent", + async () => { + const agents = await a.listAgents(a.peerId); + const seen = agents.find((agent) => agent.id === bId); + return seen?.status === "active"; + }, + ); + + await waitFor("the joining peer to see the coordinator's agent", async () => { + const agents = await b.listAgents(b.peerId); + return agents.some((agent) => agent.id === a.peerId); + }); + + // A room message from the coordinator must push to the joiner over WS. + await a.createRoom({ + name: "ws-window", + type: "public", + owner: a.peerId, + description: "ws broadcast window", + }); + await waitFor("the room to reach the joiner", async () => { + const rooms = await b.listRooms(b.peerId); + return rooms.some((room) => room.id === "ws-window"); + }); + await b.joinRoom("ws-window", b.peerId); + // Wait for the membership to reach the sender before sending: delivery is + // computed from the sender's local room state. + await waitFor("the coordinator to see the joiner in the room", async () => { + const room = await a.getRoom("ws-window"); + return room?.members.includes(bId) === true; + }); + const deliveries: string[] = []; + b.onDelivery = (_id, ev) => { + if (ev.type === "room_message") deliveries.push(ev.message.content); + }; + await a.sendRoomMessage("ws-window", a.peerId, "hello over ws"); + await waitFor("the WS room message push", async () => + deliveries.includes("hello over ws"), + ); + + await b.shutdown(); + await a.shutdown(); + console.log("✓ immediate registration and delivery work over WebSocket"); +} + +main().catch((err: unknown) => { + console.error("Test failed:", err); + process.exitCode = 1; + // The sequence above keeps mesh handles open when it fails partway; exit explicitly so a failure cannot hang the runner. + process.exit(1); +});