chore(nests): mv nestsClient-browser-interop/ → nestsClient/tests/browser-interop/

Mirrors the existing nestsClient/tests/hang-interop/ layout — all
T16 cross-stack interop test infrastructure (Rust sidecars + bun
browser harness) now lives under nestsClient/tests/.

Updates:
- nestsClient/build.gradle.kts: browserInteropDir path.
- PlaywrightDriver.kt + BrowserInteropTest.kt: kdoc comments.
- All plan docs in nestsClient/plans/ that referenced the old
  path.

Compiles clean post-move. No CI changes (those jobs are
intentionally not wired per 2026-05-07-cross-stack-interop-ci-gating.md;
when re-added they'll reference the new path).
This commit is contained in:
Claude
2026-05-07 14:43:55 +00:00
parent 32065901c8
commit bd7b166fda
21 changed files with 33 additions and 33 deletions
@@ -0,0 +1,136 @@
// Phase 4 (T16) bun static + WebSocket back-channel server.
//
// One process per Kotlin test, bound to a random port; the PlaywrightDriver
// passes the port back to the harness pages as `?wsPort=…`. PCM frames sent
// over the WS as binary messages get appended to `--out-pcm`. A textual
// `done` message flips the server's `done` flag so the test driver can
// poll it via the `/state` endpoint and tear down cleanly.
//
// Argv:
// --port <int> listen port; 0 picks a random one (logged on stdout)
// --root <dir> directory to serve static files from (= dist/)
// --out-pcm <path> file to append received PCM frames to
//
// Stdout (machine-readable, single line then blank line):
// port=<int>
// ready
//
// Errors go to stderr; non-zero exit code on fatal startup failure.
import { type ServerWebSocket } from "bun";
import { mkdirSync, openSync, closeSync, writeSync, existsSync } from "node:fs";
import { dirname, resolve, join } from "node:path";
interface Args {
port: number;
root: string;
outPcm: string;
}
function parseArgs(): Args {
const args = process.argv.slice(2);
let port = 0;
let root = "";
let outPcm = "";
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === "--port") port = Number(args[++i]);
else if (a === "--root") root = args[++i];
else if (a === "--out-pcm") outPcm = args[++i];
else throw new Error(`unknown arg: ${a}`);
}
if (!root) throw new Error("--root is required");
if (!outPcm) throw new Error("--out-pcm is required");
return { port, root, outPcm: resolve(outPcm) };
}
const args = parseArgs();
mkdirSync(dirname(args.outPcm), { recursive: true });
// Truncate any prior file: the harness page reopens each run.
const fd = openSync(args.outPcm, "w");
let done = false;
const wsClients = new Set<ServerWebSocket<unknown>>();
const contentType = (path: string): string => {
if (path.endsWith(".html")) return "text/html; charset=utf-8";
if (path.endsWith(".js")) return "application/javascript; charset=utf-8";
if (path.endsWith(".mjs")) return "application/javascript; charset=utf-8";
if (path.endsWith(".json")) return "application/json; charset=utf-8";
if (path.endsWith(".css")) return "text/css; charset=utf-8";
if (path.endsWith(".wasm")) return "application/wasm";
return "application/octet-stream";
};
const server = Bun.serve({
port: args.port,
hostname: "127.0.0.1",
fetch(req, srv) {
const url = new URL(req.url);
if (url.pathname === "/pcm") {
// WebSocket upgrade for the PCM back-channel.
if (srv.upgrade(req)) return;
return new Response("expected websocket upgrade", { status: 400 });
}
if (url.pathname === "/state") {
return new Response(JSON.stringify({ done }), {
headers: { "content-type": "application/json" },
});
}
// Static file serve out of root/.
let path = url.pathname === "/" ? "/listen.html" : url.pathname;
const filePath = join(args.root, path.replace(/^\/+/, ""));
// Reject path traversal attempts.
if (!filePath.startsWith(resolve(args.root))) {
return new Response("forbidden", { status: 403 });
}
if (!existsSync(filePath)) {
return new Response("not found: " + path, { status: 404 });
}
const file = Bun.file(filePath);
return new Response(file, {
headers: {
"content-type": contentType(filePath),
// No-cache so a `bun build` rebuild between Playwright runs
// is picked up immediately.
"cache-control": "no-store",
},
});
},
websocket: {
message(ws, message) {
wsClients.add(ws);
if (typeof message === "string") {
if (message === "done") {
done = true;
console.log("[server] received `done`");
}
return;
}
// Binary PCM frame — append raw bytes to the out file.
const buf = message instanceof ArrayBuffer ? new Uint8Array(message) : new Uint8Array(message.buffer, message.byteOffset, message.byteLength);
writeSync(fd, buf);
},
open(ws) {
wsClients.add(ws);
},
close(ws) {
wsClients.delete(ws);
},
},
});
// Machine-readable handshake for PlaywrightDriver.
process.stdout.write(`port=${server.port}\n`);
process.stdout.write("ready\n");
// Clean up the fd on Ctrl-C / parent kill so we don't leak it.
const shutdown = () => {
try {
closeSync(fd);
} catch { /* ignore */ }
server.stop(true);
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);