test(web): remove two timing races that hung CI for a minute each

Both failures showed up as "Test timed out in 30000ms" followed by "Hook
timed out in 30000ms" on loaded PR runners, in files nobody had touched.

relay host-client: the scripted client dialed its WebSocket before awaiting
its WebCrypto setup and only then attached the open/message listeners. On a
loaded runner the loopback socket opened while key generation was still
queued on the threadpool, the open event found no listener, no hello was
sent, and the client waited forever. The socket is now dialed after the key
material is ready, in the same tick as the listeners. The fake relay also
terminates leftover sockets on stop so a failure is reported once, not
twice.

walkthrough routes: each test slept 20 ms and assumed the request had
reached the route by then. The tests now wait until the service has been
asked to generate one more time than before the request, and afterEach
closes idle keep-alive connections so server.close() cannot hang on a
response a failed test never received.

Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
This commit is contained in:
Bohdan Triapitsyn
2026-09-05 12:40:07 +03:00
parent a12b9be443
commit 7a3244bab2
2 changed files with 35 additions and 6 deletions
@@ -98,6 +98,9 @@ const startFakeRelay = () => {
wsUrl: `ws://127.0.0.1:${port}`,
state,
stop: () => new Promise((r) => {
// A socket a failed test left open would hold server.close() until
// the hook timeout; drop them so a failure is reported once.
for (const client of wss.clients) client.terminate();
wss.close();
server.close(() => r());
}),
@@ -162,7 +165,6 @@ const runScriptedClient = async ({ relayUrl, serverId, hostEncPubJwk }) => {
url.searchParams.set('role', 'client');
url.searchParams.set('serverId', serverId);
url.searchParams.set('connectionId', connectionId);
const ws = new WebSocket(url.toString());
const hostPub = await globalThis.crypto.subtle.importKey(
'jwk',
@@ -182,6 +184,12 @@ const runScriptedClient = async ({ relayUrl, serverId, hostEncPubJwk }) => {
resolveDone = resolve;
});
// Dialed only now, with the key material ready and the listeners attached
// in the same tick. Dialing before the WebCrypto awaits above let a loopback
// socket open while the key generation was still queued on the threadpool,
// and an `open` event with no listener means no hello, no ready, and a
// client that waits forever. Loaded CI runners hit exactly that.
const ws = new WebSocket(url.toString());
ws.on('open', async () => {
ws.send(JSON.stringify({
t: 'hello',