mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
fa5af59931
## What & why
Swept the repo for hand-rolled regex/string parsers that reimplement a
solved problem, and replaced the genuinely fragile ones with
battle-tested packages (or deps already in the tree). Each swap is
paired with a regression test for an input the old code got wrong.
| Site | Was | Now |
|------|-----|-----|
| `adb.ts` `resolveAvdPath`, `linux-preflight.ts` `readMb` | per-key
anchored regex over AVD `.ini` / `config.ini` | **`ini`** |
| `workspace-reader.ts` `extractEnvKeys` | split-on-first-`=` line loop
| **`dotenv`** (`parse`) |
| `uninstall.ts` `readBundledSkillName` | `^name:(.+)$` capture +
outer-quote strip | **`yaml`** (already a dep) |
| `mcp-configs.ts` `escapeRegExp` | hand-rolled char-class escape |
**`escape-string-regexp`** |
| `export.ts` xctrace version, `session-context.ts` RN minor |
`match(/(\d+)\./)` / `split(".")[1]` | **`semver`** `coerce` (already a
dep) |
| `source-resolver.ts` `parseDebugStack` | `at fn (file:l:c)` regex
(required both line+col) | **`stacktrace-parser`** |
### Bugs these fix (covered by new tests)
- **ini**: a quoted `path = "/Users/My Name/.../X.avd"` kept its quotes,
failed the `startsWith("/")` guard → AVD unresolved. Inline comments /
trailing junk after a sizing value made the anchored regex return `null`
→ sizing warning silently lost.
- **dotenv**: `export FOO=bar` produced the key `"export FOO"` (dropped
by the identifier filter); a multi-line quoted value leaked its
continuation lines (`FAKE=...`) as phantom keys.
- **yaml**: a `name:` value with a trailing `# comment` was taken
verbatim (comment included); odd quoting only had one outer pair
stripped.
- **stacktrace-parser**: a `:line`-only frame (no column, as Hermes/JSC
can emit) became an unusable `unknown` frame.
## Deliberately left alone (not "flaky", or no safe swap)
To avoid churn and regressions, I did **not** touch:
- **`uiautomator-parser.ts`** hand-rolled XML parser — a deliberate,
documented choice with a dedicated hardening suite, including a
*leniency* test for malformed dumps (stray `</node>`). A spec XML parser
would reject inputs this is built to recover from.
- **`ios-profiler/pipeline/xml-parser.ts`** (xctrace XML) — the most
tangled parser, but it has no real `.trace` fixtures and only thin
entity-decode tests. A rewrite can't be proven equivalent without
device-generated traces, so swapping it blind risks silently breaking
iOS CPU/hang/leak reports. Best done separately with a captured fixture.
- **`http.ts` `extractHostname`** — DNS-rebinding guard; `URL.hostname`
keeps the IPv6 brackets the loopback check compares without, so the
"fix" isn't simpler and the path is security-sensitive.
- **`adb.ts` `shellQuote`** — the POSIX single-quote escape is the
standard correct form.
- **`parseAdbDevices`, `android-screen.ts` `wm size`, `detect-app.ts`
dumpsys** — tool-specific CLI-output scraping with no clean package
equivalent (and documented edge-case handling).
- **`workspace-reader.ts` `extractMetroPort`** — a best-effort heuristic
over JS config; a correct replacement needs an AST parse, out of scope
here.
- **`config-parse.ts` Yarn/npm age-gate parsing** — a precise,
unit-tested domain parser; `ms` would introduce a bare-number ambiguity.
## Verification
- `tsc --build` clean across the workspace graph.
- Tests: tool-server **1205**, installer **264**, update-core **31** —
all pass (incl. the new regression tests).
- `prettier --check` clean on all changed files.
- Production esbuild bundle (`bundle-tools.cjs`) emits a valid
`tool-server.cjs`/`mcp-server.mjs` with
`ini`/`dotenv`/`stacktrace-parser` inlined (`node --check` OK). *(The
script's final step errored only on the auth-gated
`trace-processor.wasm` download, unrelated to bundling.)*
New runtime deps: `ini`, `dotenv`, `stacktrace-parser` (tool-server),
`escape-string-regexp` (installer) — all small, pure-JS. Lockfile change
is purely additive.
576 lines
18 KiB
TypeScript
576 lines
18 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 { networkInspectorBlueprint } from "../../src/blueprints/network-inspector";
|
|
import { networkLogsTool } from "../../src/tools/network/network-logs";
|
|
import { networkRequestTool } from "../../src/tools/network/network-request";
|
|
|
|
/**
|
|
* Integration test using a mock Metro HTTP + CDP WebSocket server.
|
|
* Verifies the network inspector blueprint depends on JsRuntimeDebugger
|
|
* (sharing the same CDP connection) and that network tools work end-to-end.
|
|
*/
|
|
|
|
let mockServer: http.Server;
|
|
let wss: WebSocketServer;
|
|
let mockPort: number;
|
|
let registry: Registry;
|
|
|
|
/**
|
|
* Track how many WebSocket connections the mock server receives.
|
|
* The key assertion: only ONE CDP connection should be opened even when
|
|
* both JsRuntimeDebugger and NetworkInspector are resolved.
|
|
*/
|
|
let wsConnectionCount = 0;
|
|
|
|
/**
|
|
* Simulated network log state stored in the "JS runtime" (mock).
|
|
* When Runtime.evaluate runs the interceptor or log-read scripts,
|
|
* we return responses matching what the real runtime would produce.
|
|
*/
|
|
let interceptorInstalled = false;
|
|
const networkLog: Array<{
|
|
id: number;
|
|
requestId: string;
|
|
state: string;
|
|
request: { url: string; method: string; headers: Record<string, string> };
|
|
response?: {
|
|
url: string;
|
|
status: number;
|
|
statusText: string;
|
|
headers: Record<string, string>;
|
|
mimeType: string;
|
|
};
|
|
resourceType: string;
|
|
encodedDataLength?: number;
|
|
timestamp: number;
|
|
durationMs?: number;
|
|
responseBody?: string;
|
|
}> = [];
|
|
|
|
function handleCDPMessage(ws: WebSocket, raw: string) {
|
|
const msg = JSON.parse(raw);
|
|
const { id, method, params } = msg;
|
|
|
|
switch (method) {
|
|
case "FuseboxClient.setClientMetadata":
|
|
case "ReactNativeApplication.enable":
|
|
case "Runtime.runIfWaitingForDebugger":
|
|
ws.send(JSON.stringify({ id, result: {} }));
|
|
break;
|
|
|
|
case "Runtime.enable":
|
|
ws.send(JSON.stringify({ id, result: {} }));
|
|
break;
|
|
|
|
case "Debugger.enable":
|
|
ws.send(JSON.stringify({ 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,
|
|
sourceMapURL: "index.bundle.map",
|
|
},
|
|
})
|
|
);
|
|
break;
|
|
|
|
case "Debugger.setPauseOnExceptions":
|
|
case "Debugger.setAsyncCallStackDepth":
|
|
ws.send(JSON.stringify({ id, result: {} }));
|
|
break;
|
|
|
|
case "Runtime.addBinding":
|
|
ws.send(JSON.stringify({ id, result: {} }));
|
|
break;
|
|
|
|
case "Runtime.evaluate": {
|
|
const expr = params.expression as string;
|
|
|
|
// The interceptor script contains "globalThis.fetch =" (monkey-patching),
|
|
// while the log-read scripts never do.
|
|
if (expr.includes("globalThis.fetch =")) {
|
|
// This is the network interceptor installation script
|
|
interceptorInstalled = true;
|
|
ws.send(
|
|
JSON.stringify({
|
|
id,
|
|
result: {
|
|
result: {
|
|
type: "string",
|
|
value: JSON.stringify({ installed: true }),
|
|
},
|
|
},
|
|
})
|
|
);
|
|
} else if (expr.includes("__argent_network_log") || expr.includes("__argent_network_by_id")) {
|
|
handleLogReadScript(ws, id, expr);
|
|
} else {
|
|
// Generic evaluate
|
|
ws.send(
|
|
JSON.stringify({
|
|
id,
|
|
result: {
|
|
result: { type: "string", value: "eval-result" },
|
|
},
|
|
})
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
ws.send(
|
|
JSON.stringify({
|
|
id,
|
|
error: {
|
|
code: -32601,
|
|
message: `Method not found: ${method}`,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
function handleLogReadScript(ws: WebSocket, id: number, expr: string) {
|
|
if (expr.includes("__argent_network_by_id")) {
|
|
// Detail read script — extract the requestId. The script embeds it as a
|
|
// JSON string literal (`byId["..."]`), so parse that literal back.
|
|
const match = expr.match(/byId\[("(?:[^"\\]|\\.)*")\]/);
|
|
const requestId = match ? (JSON.parse(match[1]) as string) : null;
|
|
const entry = networkLog.find((e) => e.requestId === requestId);
|
|
|
|
if (!interceptorInstalled) {
|
|
ws.send(
|
|
JSON.stringify({
|
|
id,
|
|
result: {
|
|
result: {
|
|
type: "string",
|
|
value: JSON.stringify({
|
|
error: "Network interceptor not installed",
|
|
}),
|
|
},
|
|
},
|
|
})
|
|
);
|
|
} else if (!entry) {
|
|
ws.send(
|
|
JSON.stringify({
|
|
id,
|
|
result: {
|
|
result: {
|
|
type: "string",
|
|
value: JSON.stringify({ error: "Request not found" }),
|
|
},
|
|
},
|
|
})
|
|
);
|
|
} else {
|
|
ws.send(
|
|
JSON.stringify({
|
|
id,
|
|
result: {
|
|
result: {
|
|
type: "string",
|
|
value: JSON.stringify(entry),
|
|
},
|
|
},
|
|
})
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// List read script — extract start and limit from the script
|
|
const startMatch = expr.match(/var start = (\d+)/);
|
|
const limitMatch = expr.match(/var limit = (\d+)/);
|
|
const start = startMatch ? parseInt(startMatch[1], 10) : 0;
|
|
const limit = limitMatch ? parseInt(limitMatch[1], 10) : 50;
|
|
|
|
// Filter out Metro server requests (like the real interceptor does)
|
|
const filtered = networkLog.filter(
|
|
(e) =>
|
|
!e.request.url.includes(`localhost:${mockPort}`) &&
|
|
!e.request.url.includes(`127.0.0.1:${mockPort}`)
|
|
);
|
|
|
|
const slice = filtered.slice(start, limit > 0 ? start + limit : start);
|
|
const entries = slice.map((e) => ({
|
|
id: e.id,
|
|
requestId: e.requestId,
|
|
state: e.state,
|
|
request: { url: e.request.url, method: e.request.method },
|
|
response: e.response
|
|
? {
|
|
status: e.response.status,
|
|
statusText: e.response.statusText,
|
|
mimeType: e.response.mimeType,
|
|
}
|
|
: undefined,
|
|
resourceType: e.resourceType,
|
|
encodedDataLength: e.encodedDataLength,
|
|
timestamp: e.timestamp,
|
|
durationMs: e.durationMs,
|
|
}));
|
|
|
|
ws.send(
|
|
JSON.stringify({
|
|
id,
|
|
result: {
|
|
result: {
|
|
type: "string",
|
|
value: JSON.stringify({
|
|
entries,
|
|
total: filtered.length,
|
|
interceptorInstalled,
|
|
}),
|
|
},
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
// Seed mock network log with test data
|
|
networkLog.push(
|
|
{
|
|
id: 0,
|
|
requestId: "rn-net-1",
|
|
state: "finished",
|
|
request: {
|
|
url: "https://api.example.com/users",
|
|
method: "GET",
|
|
headers: { "content-type": "application/json" },
|
|
},
|
|
response: {
|
|
url: "https://api.example.com/users",
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"authorization": "Bearer secret-token",
|
|
},
|
|
mimeType: "application/json",
|
|
},
|
|
resourceType: "Fetch",
|
|
encodedDataLength: 1234,
|
|
timestamp: Date.now() / 1000,
|
|
durationMs: 150,
|
|
responseBody: JSON.stringify([
|
|
{ id: 1, name: "Alice" },
|
|
{ id: 2, name: "Bob" },
|
|
]),
|
|
},
|
|
{
|
|
id: 1,
|
|
requestId: "rn-net-2",
|
|
state: "failed",
|
|
request: {
|
|
url: "https://api.example.com/broken",
|
|
method: "POST",
|
|
headers: {},
|
|
},
|
|
resourceType: "Fetch",
|
|
timestamp: Date.now() / 1000,
|
|
durationMs: 50,
|
|
}
|
|
);
|
|
|
|
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([
|
|
{
|
|
id: "page-1",
|
|
title: "React Native (mock)",
|
|
description: "[C++ connection]",
|
|
webSocketDebuggerUrl: `ws://localhost:${mockPort}/inspector/debug?device=0&page=1`,
|
|
deviceName: "MockDevice",
|
|
reactNative: {
|
|
capabilities: { prefersFuseboxFrontend: true },
|
|
},
|
|
},
|
|
])
|
|
);
|
|
return;
|
|
}
|
|
res.statusCode = 404;
|
|
res.end("Not found");
|
|
});
|
|
|
|
wss = new WebSocketServer({ server: mockServer });
|
|
wss.on("connection", (ws) => {
|
|
wsConnectionCount++;
|
|
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.registerBlueprint(networkInspectorBlueprint);
|
|
registry.registerTool(networkLogsTool);
|
|
registry.registerTool(networkRequestTool);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await registry.dispose();
|
|
await new Promise<void>((resolve) => {
|
|
wss.close(() => {
|
|
mockServer.close(() => resolve());
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("NetworkInspector integration (mock server)", () => {
|
|
it("shares the same CDP connection with JsRuntimeDebugger (only 1 WS connection)", async () => {
|
|
// Reset counter before this test group starts.
|
|
// The beforeAll already sets up the server but doesn't create any services yet.
|
|
wsConnectionCount = 0;
|
|
|
|
// Resolve the network inspector, which should trigger JsRuntimeDebugger first
|
|
await registry.resolveService(`NetworkInspector:${mockPort}:mock-device`);
|
|
|
|
// Only ONE WebSocket connection should have been opened
|
|
expect(wsConnectionCount).toBe(1);
|
|
|
|
// Verify both services are running
|
|
expect(registry.getServiceState(`JsRuntimeDebugger:${mockPort}:mock-device`)).toBe("RUNNING");
|
|
expect(registry.getServiceState(`NetworkInspector:${mockPort}:mock-device`)).toBe("RUNNING");
|
|
});
|
|
|
|
it("view-network-logs returns paginated network entries", async () => {
|
|
const result = (await registry.invokeTool("view-network-logs", {
|
|
port: mockPort,
|
|
device_id: "mock-device",
|
|
})) as string;
|
|
|
|
expect(result).toContain("NETWORK LOGS");
|
|
expect(result).toContain("rn-net-1");
|
|
expect(result).toContain("GET");
|
|
expect(result).toContain("/users");
|
|
expect(result).toContain("200 OK");
|
|
expect(result).toContain("rn-net-2");
|
|
});
|
|
|
|
it("view-network-logs returns 'no traffic' for empty log", async () => {
|
|
// Temporarily clear the log
|
|
const savedLog = networkLog.splice(0);
|
|
try {
|
|
const result = (await registry.invokeTool("view-network-logs", {
|
|
port: mockPort,
|
|
device_id: "mock-device",
|
|
})) as string;
|
|
|
|
expect(result).toContain("No network traffic captured");
|
|
} finally {
|
|
// Restore the log
|
|
networkLog.push(...savedLog);
|
|
}
|
|
});
|
|
|
|
it("view-network-request-details returns full details for a known requestId", async () => {
|
|
const result = (await registry.invokeTool("view-network-request-details", {
|
|
port: mockPort,
|
|
device_id: "mock-device",
|
|
requestId: "rn-net-1",
|
|
})) as Record<string, unknown>;
|
|
|
|
expect(result.requestId).toBe("rn-net-1");
|
|
expect(result.state).toBe("finished");
|
|
|
|
const req = result.request as Record<string, unknown>;
|
|
expect(req.url).toBe("https://api.example.com/users");
|
|
expect(req.method).toBe("GET");
|
|
|
|
const resp = result.response as Record<string, unknown>;
|
|
expect(resp.status).toBe(200);
|
|
expect(resp.statusText).toBe("OK");
|
|
|
|
// Verify sensitive headers are redacted
|
|
const respHeaders = resp.headers as Record<string, string>;
|
|
expect(respHeaders.authorization).toBe("[REDACTED]");
|
|
expect(respHeaders["content-type"]).toBe("application/json");
|
|
});
|
|
|
|
it("view-network-request-details returns response body when includeBody is true", async () => {
|
|
const result = (await registry.invokeTool("view-network-request-details", {
|
|
port: mockPort,
|
|
device_id: "mock-device",
|
|
requestId: "rn-net-1",
|
|
includeBody: true,
|
|
})) as Record<string, unknown>;
|
|
|
|
const resp = result.response as Record<string, unknown>;
|
|
expect(resp.body).toBeDefined();
|
|
expect(resp.body).toContain("Alice");
|
|
expect(resp.body).toContain("Bob");
|
|
});
|
|
|
|
it("view-network-request-details returns error for unknown requestId", async () => {
|
|
const result = (await registry.invokeTool("view-network-request-details", {
|
|
port: mockPort,
|
|
device_id: "mock-device",
|
|
requestId: "rn-net-999",
|
|
})) as string;
|
|
|
|
expect(result).toContain("Request not found");
|
|
});
|
|
|
|
it("still only 1 WS connection when JsRuntimeDebugger is resolved before NetworkInspector", async () => {
|
|
// Dispose everything first so we can start fresh
|
|
await registry.disposeService(`JsRuntimeDebugger:${mockPort}:mock-device`).catch(() => {});
|
|
wsConnectionCount = 0;
|
|
|
|
// Explicitly resolve JsRuntimeDebugger first
|
|
await registry.resolveService(`JsRuntimeDebugger:${mockPort}:mock-device`);
|
|
expect(wsConnectionCount).toBe(1);
|
|
|
|
// Now resolve NetworkInspector — should NOT open a second connection
|
|
await registry.resolveService(`NetworkInspector:${mockPort}:mock-device`);
|
|
expect(wsConnectionCount).toBe(1);
|
|
});
|
|
|
|
it("view-network-logs filters out requests to the Metro server port", async () => {
|
|
// Add a request that targets the Metro server itself
|
|
const metroEntry = {
|
|
id: networkLog.length,
|
|
requestId: "rn-net-metro",
|
|
state: "finished" as const,
|
|
request: {
|
|
url: `http://localhost:${mockPort}/symbolicate`,
|
|
method: "POST",
|
|
headers: {},
|
|
},
|
|
response: {
|
|
url: `http://localhost:${mockPort}/symbolicate`,
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: {},
|
|
mimeType: "application/json",
|
|
},
|
|
resourceType: "Fetch",
|
|
encodedDataLength: 100,
|
|
timestamp: Date.now() / 1000,
|
|
durationMs: 10,
|
|
};
|
|
networkLog.push(metroEntry);
|
|
|
|
try {
|
|
const result = (await registry.invokeTool("view-network-logs", {
|
|
port: mockPort,
|
|
device_id: "mock-device",
|
|
})) as string;
|
|
|
|
// The Metro request should be filtered out
|
|
expect(result).not.toContain("rn-net-metro");
|
|
expect(result).not.toContain("/symbolicate");
|
|
// But non-Metro requests should still be present
|
|
expect(result).toContain("rn-net-1");
|
|
} finally {
|
|
// Remove the test entry
|
|
const idx = networkLog.findIndex((e) => e.requestId === "rn-net-metro");
|
|
if (idx >= 0) networkLog.splice(idx, 1);
|
|
}
|
|
});
|
|
|
|
it("view-network-logs returns page index out-of-range error", async () => {
|
|
const result = (await registry.invokeTool("view-network-logs", {
|
|
port: mockPort,
|
|
device_id: "mock-device",
|
|
pageIndex: 999,
|
|
})) as string;
|
|
|
|
expect(result).toContain("Page index out of range");
|
|
});
|
|
|
|
it("view-network-request-details omits body when includeBody is false", async () => {
|
|
const result = (await registry.invokeTool("view-network-request-details", {
|
|
port: mockPort,
|
|
device_id: "mock-device",
|
|
requestId: "rn-net-1",
|
|
includeBody: false,
|
|
})) as Record<string, unknown>;
|
|
|
|
const resp = result.response as Record<string, unknown>;
|
|
expect(resp).toBeDefined();
|
|
expect(resp.status).toBe(200);
|
|
// Body should NOT be included
|
|
expect(resp.body).toBeUndefined();
|
|
});
|
|
|
|
it("view-network-request-details truncates large response bodies", async () => {
|
|
// Add a request with a very large body (> 1000 chars)
|
|
const largeBody = "x".repeat(2000);
|
|
const largeEntry = {
|
|
id: networkLog.length,
|
|
requestId: "rn-net-large",
|
|
state: "finished" as const,
|
|
request: {
|
|
url: "https://api.example.com/large",
|
|
method: "GET",
|
|
headers: {},
|
|
},
|
|
response: {
|
|
url: "https://api.example.com/large",
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: {},
|
|
mimeType: "text/plain",
|
|
},
|
|
resourceType: "Fetch",
|
|
encodedDataLength: 2000,
|
|
timestamp: Date.now() / 1000,
|
|
durationMs: 100,
|
|
responseBody: largeBody,
|
|
};
|
|
networkLog.push(largeEntry);
|
|
|
|
try {
|
|
const result = (await registry.invokeTool("view-network-request-details", {
|
|
port: mockPort,
|
|
device_id: "mock-device",
|
|
requestId: "rn-net-large",
|
|
includeBody: true,
|
|
})) as Record<string, unknown>;
|
|
|
|
const resp = result.response as Record<string, unknown>;
|
|
expect(resp.body).toBeDefined();
|
|
const body = resp.body as string;
|
|
expect(body).toContain("TRUNCATED");
|
|
expect(body).toContain("2000 chars");
|
|
// Body should be shorter than original
|
|
expect(body.length).toBeLessThan(2000);
|
|
} finally {
|
|
const idx = networkLog.findIndex((e) => e.requestId === "rn-net-large");
|
|
if (idx >= 0) networkLog.splice(idx, 1);
|
|
}
|
|
});
|
|
|
|
it("NetworkInspector cascades teardown when JsRuntimeDebugger is disposed", async () => {
|
|
// Dispose JsRuntimeDebugger — NetworkInspector should also be torn down
|
|
await registry.disposeService(`JsRuntimeDebugger:${mockPort}:mock-device`);
|
|
|
|
expect(registry.getServiceState(`JsRuntimeDebugger:${mockPort}:mock-device`)).toBe("IDLE");
|
|
expect(registry.getServiceState(`NetworkInspector:${mockPort}:mock-device`)).toBe("IDLE");
|
|
});
|
|
});
|