mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
71344c40b9
## Problem Opening debugger sessions with **different** `device_id`s could all resolve to the **same** physical device. `selectTarget` filters Metro's `/json/list` CDP targets by `reactNative.logicalDeviceId === device_id`, but when the provided `device_id` matched **no** target it kept *all* candidates and fell through to the first/priority target: ```ts const filtered = candidates.filter((t) => t.reactNative?.logicalDeviceId === options.deviceId); if (filtered.length) candidates = filtered; // unmatched id → silently ignored ``` This silently no-ops surprisingly often: Metro's `logicalDeviceId` is an internal hash, **not** the iOS simulator UDID. Verified live against a real device: - simulator UDID: `6DBF83B4-F341-4F8D-B48D-CD8FF312CCFB` - Metro `logicalDeviceId`: `a1956f6bd1e3a3ac3dbb505e24d79687a5ea60b1` With one device the fallback is harmless (it picks the only device). With **several** devices on one Metro, every unmatched `device_id` collapses onto whichever device Metro lists first — so two debugger sessions opened with different `device_id`s drive the same device. ## Fix In `selectTarget`, only fall back to the priority target when there is **at most one distinct device** to fall back to. When multiple distinct devices are connected and the `device_id` matches none, throw an error that lists the valid `logicalDeviceId`s so the caller can re-target instead of being silently misrouted. The single-device convenience path (pass any id — including a UDID — and get the one connected device) is preserved. ## Verification - `selectTarget` unit tests: distinct ids → distinct devices; ambiguous unmatched id throws and surfaces the valid ids; single-device and no-`logicalDeviceId` fallbacks still work. - Full-stack multi-device test driving `Registry → JsRuntimeDebugger → debugger-connect` against a two-device mock Metro: distinct `device_id`s route to distinct devices; unmatched id is rejected. - Full `tool-server` suite: **1203 passed**. - Confirmed the real contract on a live iOS sim: `debugger-connect` with the UDID still connects (single-device fallback) and returns `logicalDeviceId a1956f…`. ## Notes The three RN-only debugger tools (`debugger-component-tree`, `debugger-inspect-element`, `debugger-reload-metro`) build their Metro service ref via a raw template literal instead of `debuggerServiceRef()`. For the Metro path the resulting URN is byte-identical, so it's not part of this bug; left untouched to keep the fix scoped.
123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
import { WebSocketServer, WebSocket } from "ws";
|
|
import * as http from "node:http";
|
|
import { Registry } from "@argent/registry";
|
|
import { jsRuntimeDebuggerBlueprint } from "../../src/blueprints/js-runtime-debugger";
|
|
import { debuggerConnectTool } from "../../src/tools/debugger/debugger-connect";
|
|
|
|
/**
|
|
* End-to-end regression for the "multiple debuggers all resolve to the same
|
|
* device despite being passed different device_id" bug.
|
|
*
|
|
* Stands up a mock Metro whose /json/list reports TWO distinct devices (each
|
|
* with its own logicalDeviceId + deviceName) on a single port, then drives the
|
|
* real Registry → JsRuntimeDebugger blueprint → debugger-connect path.
|
|
*/
|
|
|
|
let mockServer: http.Server;
|
|
let wss: WebSocketServer;
|
|
let mockPort: number;
|
|
let registry: Registry;
|
|
|
|
const DEVICES = [
|
|
{ logicalDeviceId: "logical-aaa", deviceName: "iPhone 16 Pro Max" },
|
|
{ logicalDeviceId: "logical-bbb", deviceName: "Pixel 9" },
|
|
];
|
|
|
|
function handleCDPMessage(ws: WebSocket, raw: string) {
|
|
const { id, method } = JSON.parse(raw) as { id: number; method: string };
|
|
switch (method) {
|
|
case "Debugger.enable":
|
|
ws.send(JSON.stringify({ id, result: { debuggerId: "mock" } }));
|
|
ws.send(
|
|
JSON.stringify({
|
|
method: "Debugger.scriptParsed",
|
|
params: {
|
|
scriptId: "1",
|
|
url: "http://localhost/index.bundle?platform=ios&dev=true",
|
|
startLine: 0,
|
|
endLine: 1,
|
|
sourceMapURL: "index.bundle.map",
|
|
},
|
|
})
|
|
);
|
|
break;
|
|
default:
|
|
ws.send(JSON.stringify({ id, result: {} }));
|
|
}
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
await new Promise<void>((resolve) => {
|
|
mockServer = 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(
|
|
DEVICES.map((d, i) => ({
|
|
id: `page-${i}`,
|
|
title: `app (${d.deviceName})`,
|
|
description: "[C++ connection]",
|
|
webSocketDebuggerUrl: `ws://localhost:${mockPort}/inspector/debug?device=${d.logicalDeviceId}&page=1`,
|
|
deviceName: d.deviceName,
|
|
reactNative: {
|
|
logicalDeviceId: d.logicalDeviceId,
|
|
capabilities: { prefersFuseboxFrontend: true },
|
|
},
|
|
}))
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
res.statusCode = 404;
|
|
res.end("Not found");
|
|
});
|
|
|
|
wss = new WebSocketServer({ server: mockServer });
|
|
wss.on("connection", (ws) => ws.on("message", (raw) => handleCDPMessage(ws, raw.toString())));
|
|
|
|
mockServer.listen(0, () => {
|
|
mockPort = (mockServer.address() as { port: number }).port;
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
registry = new Registry();
|
|
registry.registerBlueprint(jsRuntimeDebuggerBlueprint);
|
|
registry.registerTool(debuggerConnectTool);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await registry.dispose();
|
|
await new Promise<void>((resolve) => wss.close(() => mockServer.close(() => resolve())));
|
|
});
|
|
|
|
describe("multi-device debugger routing (mock Metro, two devices)", () => {
|
|
it("routes each device_id to its own device", async () => {
|
|
const a = (await registry.invokeTool("debugger-connect", {
|
|
port: mockPort,
|
|
device_id: "logical-aaa",
|
|
})) as Record<string, unknown>;
|
|
const b = (await registry.invokeTool("debugger-connect", {
|
|
port: mockPort,
|
|
device_id: "logical-bbb",
|
|
})) as Record<string, unknown>;
|
|
|
|
expect(a.deviceName).toBe("iPhone 16 Pro Max");
|
|
expect(b.deviceName).toBe("Pixel 9");
|
|
// The two connections must NOT collapse onto the same device.
|
|
expect(a.logicalDeviceId).not.toBe(b.logicalDeviceId);
|
|
});
|
|
|
|
it("rejects an unmatched device_id instead of silently picking the first device", async () => {
|
|
await expect(
|
|
registry.invokeTool("debugger-connect", { port: mockPort, device_id: "not-a-real-device" })
|
|
).rejects.toThrow(/No debugger target matches device_id "not-a-real-device"/);
|
|
});
|
|
});
|