mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
cee2433758
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>
101 lines
3.2 KiB
TypeScript
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()));
|
|
}),
|
|
};
|
|
}
|