Files
software-mansion__argent/packages/tool-server/test/debugger/metro-cdp-harness.ts
Ignacy Łątka cee2433758 test(tool-server): bind the ten remaining test servers to loopback (#1142)
Fixes #853

**Cause:** `listen(0)` with no host binds every interface, so ten
ephemeral-port test servers were LAN-reachable for as long as a suite
ran.

**Fix:** pass `"127.0.0.1"` at all ten sites, matching the twenty-two
siblings that already do. Adds a guard test that scans `src/` and
`test/` for any `.listen(0)` missing the host, so a new one fails CI.

No sites left behind: `grep -rn "\.listen("` over every package turns up
only these ten, the twenty-two already-loopback sites, the unix-socket
listens in `native-devtools.ts` / `ax-service.ts` (no host applies), and
`src/index.ts` which already passes `HOST`.

**Docs:** none needed - test-only, no tool, CLI, config key, flow file,
or user-facing capability changes.

<details>
<summary>Verification</summary>

Guard test discriminates - with the ten fixes stashed it names every one
of them:

```
+ [
+   "test/http-upload.test.ts:167",
+   "test/debugger/debugger-status-not-connected.test.ts:76",
+   "test/debugger/log-registry-not-connected.test.ts:73",
+   "test/debugger/metro-cdp-harness.ts:90",
+   "test/metro/integration.test.ts:154",
+   "test/metro/legacy-runtime-guards.test.ts:68",
+   "test/metro/multi-device.test.ts:90",
+   "test/metro/teardown-log-history.test.ts:80",
+   "test/metro/vega-target-routing.test.ts:98",
+   "test/network/network-integration.test.ts:327",
+ ]
```

`http-upload.test.ts` needed two extra lines: `listen(port)` binds
synchronously, but `listen(port, host)` defers behind a DNS lookup, so
`server.address()` came back `null`. It now awaits `listening`. The
client `http.request` had no `host` (defaulting to `localhost`), and its
`error` handler is a no-op, so a failed dial would have made the
assertion pass vacuously - pinned to `127.0.0.1`. Probed that the
request still reaches the server and a partial file is really created
before cleanup:

```
PARTIALS ["argent-upload-d567a1aa-75a0-44e3-9e06-6d42544be1ee.tar.gz"]
ERR Error: socket hang up
```

Ran from the worktree root:

- `npx tsc --build` - clean
- `npm run typecheck:tests -w @argent/tool-server` - clean
- `npm test -w @argent/tool-server` - 369 files, 4833 passed, 1 skipped
- `npm run knip` - clean
- `npx prettier --write` on the changed files - no diff

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 21:26:51 +02:00

101 lines
3.2 KiB
TypeScript

import { WebSocketServer } from "ws";
import * as http from "node:http";
import { AddressInfo } from "node:net";
/** A TCP port that (was just proven to be bindable and) now has no listener. */
export async function freePort(): Promise<number> {
return new Promise<number>((resolve) => {
const srv = http.createServer();
srv.listen(0, "127.0.0.1", () => {
const port = (srv.address() as AddressInfo).port;
srv.close(() => resolve(port));
});
});
}
export interface MockMetroCdp {
port: number;
close: () => Promise<void>;
}
/**
* Mock Metro HTTP + CDP WebSocket target, the minimum wire behavior the
* JsRuntimeDebugger connect pipeline needs (same protocol surface as
* test/metro/integration.test.ts): /status, /json/list with one Fusebox
* target, and a CDP socket that ACKs every method (Debugger.enable also emits
* a scriptParsed; Runtime.evaluate answers a string so the addBinding probe
* sees the binding as installed).
*/
export async function startMockMetroCdp(opts?: {
/** Advertise this logicalDeviceId on the single target (default: none). */
logicalDeviceId?: string;
}): Promise<MockMetroCdp> {
let port = 0;
const server = http.createServer((req, res) => {
if (req.url === "/status") {
res.setHeader("X-React-Native-Project-Root", "/mock/project");
res.end("packager-status:running");
return;
}
if (req.url === "/json/list") {
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify([
{
id: "page-1",
title: "React Native (mock)",
description: "[C++ connection]",
webSocketDebuggerUrl: `ws://localhost:${port}/inspector/debug?device=0&page=1`,
deviceName: "MockDevice",
reactNative: {
capabilities: { prefersFuseboxFrontend: true },
...(opts?.logicalDeviceId ? { logicalDeviceId: opts.logicalDeviceId } : {}),
},
},
])
);
return;
}
res.statusCode = 404;
res.end();
});
const wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.method === "Debugger.enable") {
ws.send(JSON.stringify({ id: msg.id, result: { debuggerId: "mock-debugger" } }));
ws.send(
JSON.stringify({
method: "Debugger.scriptParsed",
params: {
scriptId: "1",
url: "http://localhost/index.bundle?platform=ios&dev=true",
startLine: 0,
endLine: 50000,
},
})
);
return;
}
if (msg.method === "Runtime.evaluate") {
ws.send(
JSON.stringify({ id: msg.id, result: { result: { type: "string", value: "mock" } } })
);
return;
}
ws.send(JSON.stringify({ id: msg.id, result: {} }));
});
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
port = (server.address() as AddressInfo).port;
return {
port,
close: () =>
new Promise((resolve) => {
for (const c of wss.clients) c.close();
wss.close(() => server.close(() => resolve()));
}),
};
}