From 13ebba77c07779a471224e926418d67184e2fabf Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Mon, 9 Nov 2026 21:41:43 -0500 Subject: [PATCH] test(windows-smoke): check the local server by connecting when sockets can't be listed The `windows` runner session can't enumerate sockets: Get-NetTCPConnection and netstat come back empty even for the DevTools port the script is connected to, so "local server listening" failed on every run (main too, run 3145) and masked real regressions. When the socket list is empty, test what cinny #43 is about directly: the server must accept a connection on 127.0.0.1:44548 and refuse it on every non-loopback IPv4 address of the machine. The list-based check is kept for runners where it works. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- scripts/windows-smoke.mjs | 48 +++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/scripts/windows-smoke.mjs b/scripts/windows-smoke.mjs index 2f08e1a..2f5d0b4 100644 --- a/scripts/windows-smoke.mjs +++ b/scripts/windows-smoke.mjs @@ -13,6 +13,8 @@ import { writeFileSync, mkdirSync } from 'node:fs'; import { execSync } from 'node:child_process'; import { createServer } from 'node:http'; +import { connect } from 'node:net'; +import { networkInterfaces } from 'node:os'; import { chromium } from 'playwright-core'; const OUT = process.argv[2] || 'smoke-out'; @@ -38,6 +40,19 @@ const until = async (fn, ms, step = 500) => { } }; +// TCP connect probe: 'open', 'refused', 'timeout' or the socket error code. +const probe = (host, port, ms = 2000) => + new Promise((resolve) => { + const socket = connect({ host, port }); + const done = (result) => { + socket.destroy(); + resolve(result); + }; + socket.setTimeout(ms, () => done('timeout')); + socket.once('connect', () => done('open')); + socket.once('error', (e) => done(e.code === 'ECONNREFUSED' ? 'refused' : e.code || 'error')); + }); + // 1. Connect to the running app's WebView2. const browser = await until(() => chromium.connectOverCDP('http://127.0.0.1:9222'), 60_000, 1000); if (!browser) { @@ -61,15 +76,40 @@ record('boots to the login screen', booted ? 'pass' : 'fail'); await page.screenshot({ path: `${OUT}/01-login.png` }); // 3. The local server's address (cinny #43 binds 127.0.0.1 explicitly). +let listen = ''; try { - const listen = execSync( + listen = execSync( 'powershell -NoProfile -Command "(Get-NetTCPConnection -LocalPort 44548 -State Listen).LocalAddress -join \',\'"', + { stdio: ['ignore', 'pipe', 'ignore'] }, ) .toString() .trim(); - record('local server listening', listen ? 'pass' : 'fail', listen); -} catch (e) { - record('local server listening', 'fail', String(e).slice(0, 120)); +} catch { + listen = ''; +} +if (listen) { + record('local server listening', 'pass', listen); +} else { + // Some runner sessions can't list sockets at all (Get-NetTCPConnection and + // netstat both come back empty, even for the DevTools port this script is + // connected to). Check what #43 is about directly instead: the server + // answers on loopback and is not reachable on the machine's LAN addresses. + const loopback = await probe('127.0.0.1', 44548); + const lan = Object.values(networkInterfaces()) + .flat() + .filter((a) => a && a.family === 'IPv4' && !a.internal) + .map((a) => a.address); + const exposed = []; + for (const ip of lan) { + if ((await probe(ip, 44548)) === 'open') exposed.push(ip); + } + record( + 'local server listening', + loopback === 'open' && exposed.length === 0 ? 'pass' : 'fail', + `socket list unavailable; 127.0.0.1: ${loopback}; LAN ${lan.join(', ') || 'none'}: ${ + exposed.length ? `OPEN on ${exposed.join(', ')}` : 'not reachable' + }`, + ); } const cfg = await page.evaluate(() => fetch('/config.json').then((r) => r.json())); -- 2.47.3