AtlatestRepositorysigil-websocket

sigil-websocket / tree / test / integrationtest-native-websocket-nowait-pump.mjs

1// Regression harness for the nowait-only pump with no scheduler.
2//
3// Completes a real WebSocket upgrade, then sends ONE text frame after a delay
4// so it cannot be coalesced with the 101. A client that never issues a socket
5// read will never see it; the frame is only reachable through nowait's own
6// single non-blocking read.
7//
8// Plain TCP on purpose: the defect was in a `(current-scheduler)` guard, not
9// in TLS, so this needs no certificate and no `openssl` on PATH.
10import crypto from "node:crypto";
11import net from "node:net";
12import path from "node:path";
13import { fileURLToPath } from "node:url";
14import { spawn } from "node:child_process";
16const here = path.dirname(fileURLToPath(import.meta.url));
17const repo = path.resolve(here, "../..");
18const PAYLOAD = "HELLO-FROM-SERVER";
19const FRAME_DELAY_MS = 300;
21function textFrame(text) {
22 const payload = Buffer.from(text, "utf8");
23 if (payload.length >= 126) throw new Error("fixture payload is unexpectedly large");
24 return Buffer.concat([Buffer.from([0x81, payload.length]), payload]);
27let sentFrame = false;
28let upgradeSeen = false;
29const server = net.createServer((socket) => {
30 let buffer = Buffer.alloc(0);
31 socket.on("data", (chunk) => {
32 buffer = Buffer.concat([buffer, chunk]);
33 const end = buffer.indexOf("\r\n\r\n");
34 if (end < 0) return;
35 const request = buffer.subarray(0, end + 4).toString("ascii");
36 const key = request.match(/Sec-WebSocket-Key:\s*([^\r\n]+)/i)?.[1];
37 if (!key) throw new Error("client handshake omitted Sec-WebSocket-Key");
38 const accept = crypto.createHash("sha1")
39 .update(`${key.trim()}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
40 .digest("base64");
41 // The 101 goes out ALONE. Coalescing the frame with it would put the
42 // payload in the connection buffer during the handshake and the test
43 // would pass without any socket read ever happening.
44 socket.write(Buffer.from(
45 `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`,
46 "ascii",
47 ));
48 buffer = Buffer.alloc(0);
49 upgradeSeen = true;
50 setTimeout(() => {
51 socket.write(textFrame(PAYLOAD));
52 sentFrame = true;
53 }, FRAME_DELAY_MS);
54 });
55 socket.on("error", () => {});
56});
58let resolvePort;
59const listening = new Promise((resolve) => { resolvePort = resolve; });
60server.listen(0, "127.0.0.1", () => resolvePort(server.address().port));
61const port = await listening;
63const dependencyRoots = [
64 "sigil-stdlib", "sigil-socket", "sigil-tls", "sigil-crypto",
65].map((name) => path.join(repo, ".sigil", "deps", name, "src"));
66const args = [
67 "-L", path.join(repo, "build/dev/lib"),
68 ...dependencyRoots.flatMap((root) => ["-L", root]),
69 path.join(here, "nowait-pump-client.sgl"),
70];
71// cwd MUST be the repo. Module resolution follows the working directory, not
72// the client file's location: spawned from /tmp this test loads whatever
73// (sigil websocket) is installed globally and reports green no matter what is
74// in src/. Verified by planting an unconditional `error` at the top of
75// ws-receive-nowait -- from /tmp the suite still passed.
76const child = spawn("sigil", args, {
77 cwd: repo,
78 env: { ...process.env, SIGIL_WS_TEST_PORT: String(port) },
79 stdio: ["ignore", "pipe", "pipe"],
80});
81let stdout = "";
82let stderr = "";
83child.stdout.on("data", (chunk) => { stdout += chunk; });
84child.stderr.on("data", (chunk) => { stderr += chunk; });
86const timeout = setTimeout(() => child.kill("SIGKILL"), 15000);
87const status = await new Promise((resolve) => child.on("close", resolve));
88clearTimeout(timeout);
89server.close();
91// Positive control on the apparatus: if the server never got far enough to
92// send the frame, the client's silence says nothing about the client. That
93// must read as a setup failure, not as a regression.
94//
95// But "frame not sent" has two causes and they mean opposite things. The
96// frame goes out on a 300ms timer, so a client that dies BEFORE then -- a
97// failed upgrade, a missing dependency, a compile error in connection.sgl --
98// closes the socket first and leaves sentFrame false through no fault of the
99// apparatus. Reporting that as SETUP-FAILED would blame the harness for a
100// genuine client failure, so split the two on whether the upgrade completed.
101if (!sentFrame) {
102 const reached = upgradeSeen
103 ? "the client connected and completed its upgrade, then exited before the frame timer ran -- this is a CLIENT failure, not a setup failure"
104 : "the server never saw a completed upgrade request, so the apparatus never got far enough to test anything (SETUP-FAILED)";
105 throw new Error(`no frame was sent: ${reached}\nstatus: ${status}\nstdout:\n${stdout}\nstderr:\n${stderr}`);
107if (status !== 0 || !stdout.includes("native-websocket-nowait-pump-ok")) {
108 throw new Error(`nowait pump client failed (${status})\nstdout:\n${stdout}\nstderr:\n${stderr}`);
110console.log("native-websocket-nowait-pump-integration-ok");