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 delay4
// so it cannot be coalesced with the 101. A client that never issues a socket5
// read will never see it; the frame is only reachable through nowait's own6
// single non-blocking read.7
//8
// Plain TCP on purpose: the defect was in a `(current-scheduler)` guard, not9
// in TLS, so this needs no certificate and no `openssl` on PATH.10
import crypto from "node:crypto";11
import net from "node:net";12
import path from "node:path";13
import { fileURLToPath } from "node:url";14
import { spawn } from "node:child_process";16
const here = path.dirname(fileURLToPath(import.meta.url));17
const repo = path.resolve(here, "../..");18
const PAYLOAD = "HELLO-FROM-SERVER";19
const FRAME_DELAY_MS = 300;21
function 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]);25
}27
let sentFrame = false;28
let upgradeSeen = false;29
const 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 the42
// payload in the connection buffer during the handshake and the test43
// 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
});58
let resolvePort;59
const listening = new Promise((resolve) => { resolvePort = resolve; });60
server.listen(0, "127.0.0.1", () => resolvePort(server.address().port));61
const port = await listening;63
const dependencyRoots = [64
"sigil-stdlib", "sigil-socket", "sigil-tls", "sigil-crypto",65
].map((name) => path.join(repo, ".sigil", "deps", name, "src"));66
const 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, not72
// the client file's location: spawned from /tmp this test loads whatever73
// (sigil websocket) is installed globally and reports green no matter what is74
// in src/. Verified by planting an unconditional `error` at the top of75
// ws-receive-nowait -- from /tmp the suite still passed.76
const child = spawn("sigil", args, {77
cwd: repo,78
env: { ...process.env, SIGIL_WS_TEST_PORT: String(port) },79
stdio: ["ignore", "pipe", "pipe"],80
});81
let stdout = "";82
let stderr = "";83
child.stdout.on("data", (chunk) => { stdout += chunk; });84
child.stderr.on("data", (chunk) => { stderr += chunk; });86
const timeout = setTimeout(() => child.kill("SIGKILL"), 15000);87
const status = await new Promise((resolve) => child.on("close", resolve));88
clearTimeout(timeout);89
server.close();91
// Positive control on the apparatus: if the server never got far enough to92
// send the frame, the client's silence says nothing about the client. That93
// must read as a setup failure, not as a regression.94
//95
// But "frame not sent" has two causes and they mean opposite things. The96
// frame goes out on a 300ms timer, so a client that dies BEFORE then -- a97
// failed upgrade, a missing dependency, a compile error in connection.sgl --98
// closes the socket first and leaves sentFrame false through no fault of the99
// apparatus. Reporting that as SETUP-FAILED would blame the harness for a100
// genuine client failure, so split the two on whether the upgrade completed.101
if (!sentFrame) {102
const reached = upgradeSeen103
? "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}`);106
}107
if (status !== 0 || !stdout.includes("native-websocket-nowait-pump-ok")) {108
throw new Error(`nowait pump client failed (${status})\nstdout:\n${stdout}\nstderr:\n${stderr}`);109
}110
console.log("native-websocket-nowait-pump-integration-ok");