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()));