From b064acf57c0a3c2ae368e3f89f3dbb0f533bc042 Mon Sep 17 00:00:00 2001 From: ruv Date: Mon, 12 Oct 2026 12:20:40 -0400 Subject: [PATCH] test(harness): cover Node 20 WebSocket capability boundary --- harness/ruview/test/iphone-close.test.mjs | 21 ++++++++++++++++--- .../ruview/test/sdk-device-options.test.mjs | 14 ++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/harness/ruview/test/iphone-close.test.mjs b/harness/ruview/test/iphone-close.test.mjs index 4c1577c6a..520775012 100644 --- a/harness/ruview/test/iphone-close.test.mjs +++ b/harness/ruview/test/iphone-close.test.mjs @@ -4,6 +4,9 @@ import http from 'node:http'; import { createHash } from 'node:crypto'; import { readIphoneLidar } from '../src/devices/lidar.js'; +const hasNativeWebSocket = typeof globalThis.WebSocket === 'function'; +const needsWebSocket = { skip: !hasNativeWebSocket && 'requires the native WebSocket client (Node 22+)' }; + // A real owned loopback WebSocket producer: upgrade, optional depth frame, // close handshake. No injected client event callbacks or hardware. async function relay(t, { frame = true, code = 1000 } = {}) { @@ -28,7 +31,19 @@ async function relay(t, { frame = true, code = 1000 } = {}) { return `ws://127.0.0.1:${server.address().port}/ws/lidar`; } -test('public iPhone reader completes when a successful relay stream closes', async t => { +test('public iPhone reader reports an unavailable WebSocket without connecting', { skip: hasNativeWebSocket && 'native WebSocket is available' }, async t => { + const server = http.createServer(); + let connections = 0; + server.on('connection', socket => { connections++; socket.destroy(); }); + await new Promise(r => server.listen(0, '127.0.0.1', r)); + t.after(() => new Promise(r => server.close(r))); + const result = await readIphoneLidar({ url: `ws://127.0.0.1:${server.address().port}/ws/lidar`, seconds: 2 }); + assert.deepEqual(result, { ok: false, reason: 'websocket_unavailable', remedy: 'Use Node 22+ (built-in WebSocket client).' }); + await new Promise(r => setImmediate(r)); + assert.equal(connections, 0, 'unavailable transport must not open a connection'); +}); + +test('public iPhone reader completes when a successful relay stream closes', needsWebSocket, async t => { const url = await relay(t); const started = performance.now(); const result = await readIphoneLidar({ url, seconds: 2 }); @@ -38,7 +53,7 @@ test('public iPhone reader completes when a successful relay stream closes', asy assert.ok(performance.now() - started < 1000, 'closed relay must finish before the capture timeout'); }); -test('normal empty relay close completes as no_frames', async t => { +test('normal empty relay close completes as no_frames', needsWebSocket, async t => { const url = await relay(t, { frame: false }); const started = performance.now(); const result = await readIphoneLidar({ url, seconds: 2 }); @@ -47,7 +62,7 @@ test('normal empty relay close completes as no_frames', async t => { assert.ok(performance.now() - started < 1000, 'empty closed relay must finish before timeout'); }); -test('abnormal empty relay close and packet limit controls retain semantics', async t => { +test('abnormal empty relay close and packet limit controls retain semantics', needsWebSocket, async t => { const denied = await readIphoneLidar({ url: await relay(t, { frame: false, code: 1008 }), seconds: 2 }); assert.equal(denied.reason, 'relay_closed'); assert.match(denied.detail, /1008/); diff --git a/harness/ruview/test/sdk-device-options.test.mjs b/harness/ruview/test/sdk-device-options.test.mjs index 9ea461c08..787180cb7 100644 --- a/harness/ruview/test/sdk-device-options.test.mjs +++ b/harness/ruview/test/sdk-device-options.test.mjs @@ -10,6 +10,7 @@ const str = (f, s) => { const b = Buffer.from(s); return Buffer.concat([encodeVa const key = (f, n) => { const b = Buffer.alloc(4); b.writeUInt32LE(n); return Buffer.concat([encodeVarint((f << 3) | 5), b]); }; const float = (f, v) => { const b = Buffer.alloc(4); b.writeFloatLE(v); return Buffer.concat([encodeVarint((f << 3) | 5), b]); }; const pause = ms => new Promise(r => setTimeout(r, ms)); +const hasNativeWebSocket = typeof globalThis.WebSocket === 'function'; async function ownServer(t, server) { const sockets = new Set(); @@ -57,7 +58,18 @@ test('public SDK accepts spectrum options and returns real UDP amplitude rows', assert.deepEqual(result.spectrum[0].frames, [Array(8).fill(5)]); }); -test('public SDK accepts iPhone max_frames and terminates at the packet cap', async t => { +test('public SDK reports unavailable iPhone transport without connecting', { skip: hasNativeWebSocket && 'native WebSocket is available' }, async t => { + const server = http.createServer(); + let connections = 0; + server.on('connection', socket => { connections++; socket.destroy(); }); + const port = await ownServer(t, server); + const result = await createRuView().devices.lidar({ source: 'iphone', url: `ws://127.0.0.1:${port}/ws/lidar`, seconds: 2, max_frames: 1 }); + assert.deepEqual(result, { ok: false, reason: 'websocket_unavailable', remedy: 'Use Node 22+ (built-in WebSocket client).' }); + await new Promise(r => setImmediate(r)); + assert.equal(connections, 0, 'unavailable transport must not open a connection'); +}); + +test('public SDK accepts iPhone max_frames and terminates at the packet cap', { skip: !hasNativeWebSocket && 'requires the native WebSocket client (Node 22+)' }, async t => { const server = http.createServer(); server.on('upgrade', (req, sock) => { const accept = createHash('sha1').update(req.headers['sec-websocket-key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');