Files
software-mansion__argent/packages/tool-server/test/chromium-discovery.test.ts
filip131311 f288a461b0 refactor: rename the Electron device platform to "chromium" (CDP) (#332)
## Summary

The Electron support added in #247 is **generic Chromium/CDP under the
hood**. The discovery layer probes the Chromium-default CDP port
(`9222`) and any `ARGENT_*_PORTS` entries; the connection picks the
first non-devtools page target; describe is a plain in-page DOM walker;
input/screenshot/screencast/navigation/JS-debugger are all straight CDP.
Nothing in that path is Electron-specific — the only Electron-specific
piece is the *launcher* (`boot-electron.ts`, which resolves and spawns
an Electron binary).

So any Chromium-family runtime exposing a CDP endpoint — an Electron
app, **or** Chrome/Brave/Edge/WebView2 launched with
`--remote-debugging-port` — is discoverable and drivable through the
same tool surface. This PR renames the platform discriminator (and
everything keyed off it) from `electron` to `chromium` so the naming
matches reality.

## What changed

**Public/contract surface (clean hard rename, no back-compat aliases):**
- Platform discriminator `"electron"` → `"chromium"`; device id
`electron-cdp-<port>` → `chromium-cdp-<port>`.
- `ToolCapability.electron` → `.chromium`; the capability-gate rejection
now reads `Tool 'X' is not supported on chromium app`.
- Env `ARGENT_ELECTRON_PORTS`(`_FILE`) →
`ARGENT_CHROMIUM_PORTS`(`_FILE`); ports cache
`~/.argent/electron-cdp-ports.json` → `chromium-cdp-ports.json`; HTTP
surface `/electron-server/` → `/chromium-server/`.

**Files / dirs / symbols:**
- `src/electron-server/` → `src/chromium-server/`; blueprints
`electron-cdp` / `electron-js-runtime-debugger` → `chromium-*`;
`describe`/`open-url`/`launch-app` `platforms/electron.ts` →
`chromium.ts`; `keyboard/electron-keys` → `chromium-keys`;
`utils/electron-discovery` → `chromium-discovery`.
-
`ElectronServer`/`ElectronCdpApi`/`ElectronJsRuntimeDebugger`/`describeElectron`/`discoverElectronDevices`/`ELECTRON_ID_PREFIX`/…
→ `Chromium*` / `CHROMIUM_*`. Tests renamed to match.

**Stays "electron" (the Electron-specific launcher):**
- `boot-device` keeps the `electronAppPath` / `electronPort` /
`electronArgs` inputs, and `boot-electron.ts` still resolves & spawns an
Electron binary. It just yields a `chromium` device now (an Electron app
*is* a Chromium/CDP runtime).

**Skills:** `argent-device-interact`, `argent-metro-debugger`,
`rules/argent.md`, and the RN-workflow reference were reframed to
describe **Chromium (CDP)** availability — Electron apps *plus* any
Chromium browser over CDP (auto-discovered on `9222` /
`ARGENT_CHROMIUM_PORTS`) — instead of Electron only.

## Test plan
- [x] `tsc --build` clean
- [x] `typecheck:tests` clean
- [x] **1116 vitest cases pass (126 files)**
- [x] prettier clean on all changed/renamed files
- [x] Full-repo sweep: every remaining "electron" mention is intentional
(the launcher path + skill prose explaining Electron as one kind of
Chromium/CDP app); `electronic` in LICENSE/COC untouched.
2026-06-15 11:03:23 +02:00

216 lines
7.1 KiB
TypeScript

import { describe, it, expect, afterEach, beforeAll, afterAll, vi } from "vitest";
import * as fs from "node:fs";
import * as http from "node:http";
import * as os from "node:os";
import * as path from "node:path";
import { AddressInfo } from "node:net";
import {
discoverChromiumDevices,
getCandidateChromiumPorts,
trackChromiumPort,
untrackChromiumPort,
} from "../src/utils/chromium-discovery";
interface FakeCdpServer {
port: number;
close: () => Promise<void>;
}
async function startFakeCdpServer(options?: {
responses?: {
version?: number | object;
list?: number | object;
};
}): Promise<FakeCdpServer> {
const server = http.createServer((req, res) => {
if (req.url === "/json/version") {
const r = options?.responses?.version ?? {
Browser: "Chrome/148.0.7778.97",
webSocketDebuggerUrl: `ws://127.0.0.1:0/devtools/browser/x`,
};
if (typeof r === "number") {
res.statusCode = r;
res.end();
return;
}
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(r));
return;
}
if (req.url === "/json/list") {
const r = options?.responses?.list ?? [
{
id: "abc",
type: "page",
title: "Test Page",
url: "file:///tmp/index.html",
webSocketDebuggerUrl: `ws://127.0.0.1:0/devtools/page/abc`,
},
];
if (typeof r === "number") {
res.statusCode = r;
res.end();
return;
}
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(r));
return;
}
res.statusCode = 404;
res.end();
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = (server.address() as AddressInfo).port;
return {
port,
close: () =>
new Promise<void>((resolve) => {
server.close(() => resolve());
}),
};
}
const portsToCleanup: number[] = [];
const serversToCleanup: FakeCdpServer[] = [];
// Redirect port persistence to a throwaway file so tests never touch the
// real ~/.argent/chromium-cdp-ports.json on a developer machine or CI runner.
const TEST_PORTS_FILE = path.join(os.tmpdir(), `argent-test-chromium-ports-${process.pid}.json`);
beforeAll(() => {
process.env.ARGENT_CHROMIUM_PORTS_FILE = TEST_PORTS_FILE;
});
afterAll(() => {
delete process.env.ARGENT_CHROMIUM_PORTS_FILE;
try {
fs.unlinkSync(TEST_PORTS_FILE);
} catch {
// never created — fine
}
});
afterEach(async () => {
for (const p of portsToCleanup.splice(0)) untrackChromiumPort(p);
for (const s of serversToCleanup.splice(0)) await s.close();
});
describe("getCandidateChromiumPorts", () => {
it("always includes 9222", () => {
const ports = getCandidateChromiumPorts();
expect(ports).toContain(9222);
});
it("includes tracked ports added via trackChromiumPort", () => {
trackChromiumPort(54321);
portsToCleanup.push(54321);
expect(getCandidateChromiumPorts()).toContain(54321);
});
it("removes tracked ports via untrackChromiumPort", () => {
trackChromiumPort(54322);
expect(getCandidateChromiumPorts()).toContain(54322);
untrackChromiumPort(54322);
expect(getCandidateChromiumPorts()).not.toContain(54322);
});
});
describe("discoverChromiumDevices", () => {
it("finds a fake CDP endpoint when its port is tracked", async () => {
const server = await startFakeCdpServer();
serversToCleanup.push(server);
trackChromiumPort(server.port);
portsToCleanup.push(server.port);
const devices = await discoverChromiumDevices({ timeoutMs: 1500 });
const ours = devices.find((d) => d.port === server.port);
expect(ours).toBeDefined();
expect(ours?.platform).toBe("chromium");
expect(ours?.id).toBe(`chromium-cdp-${server.port}`);
expect(ours?.title).toBe("Test Page");
expect(ours?.url).toBe("file:///tmp/index.html");
expect(ours?.browser).toMatch(/Chrome/);
expect(ours?.state).toBe("Running");
});
it("returns no entry for a non-responsive port", async () => {
// Tracked but no server bound
trackChromiumPort(1);
portsToCleanup.push(1);
const devices = await discoverChromiumDevices({ timeoutMs: 300, ports: [1] });
expect(devices).toEqual([]);
});
it("filters out ports with no page targets", async () => {
const server = await startFakeCdpServer({
responses: {
list: [
{ id: "x", type: "service_worker", title: "", url: "", webSocketDebuggerUrl: "ws://x" },
],
},
});
serversToCleanup.push(server);
const devices = await discoverChromiumDevices({ timeoutMs: 1500, ports: [server.port] });
expect(devices).toEqual([]);
});
it("untracks a port after it stops responding", async () => {
const server = await startFakeCdpServer();
trackChromiumPort(server.port);
portsToCleanup.push(server.port);
// First probe succeeds — port stays tracked.
let devices = await discoverChromiumDevices({ timeoutMs: 1500 });
expect(devices.some((d) => d.port === server.port)).toBe(true);
// Close the server, probe again — port should be untracked.
await server.close();
devices = await discoverChromiumDevices({ timeoutMs: 300 });
expect(devices.some((d) => d.port === server.port)).toBe(false);
expect(getCandidateChromiumPorts()).not.toContain(server.port);
});
});
describe("port persistence across tool-server restarts", () => {
// Booted Chromium apps are detached and outlive the tool-server (which
// auto-exits on idle). A fresh module instance — same as a fresh process —
// must rediscover them from the persisted file.
it("a fresh module instance sees ports tracked by a previous one", async () => {
trackChromiumPort(43210);
portsToCleanup.push(43210);
vi.resetModules();
const fresh = await import("../src/utils/chromium-discovery");
expect(fresh.getCandidateChromiumPorts()).toContain(43210);
});
it("a dead persisted port is pruned from the file after a failed probe", async () => {
trackChromiumPort(43211);
portsToCleanup.push(43211);
expect(JSON.parse(fs.readFileSync(TEST_PORTS_FILE, "utf8"))).toContain(43211);
// Nothing listens on 43211 — the probe fails and prunes it everywhere.
await discoverChromiumDevices({ timeoutMs: 300, ports: [43211] });
expect(JSON.parse(fs.readFileSync(TEST_PORTS_FILE, "utf8"))).not.toContain(43211);
expect(getCandidateChromiumPorts()).not.toContain(43211);
});
it("untrackChromiumPort removes the port from the persisted file", () => {
trackChromiumPort(43212);
expect(JSON.parse(fs.readFileSync(TEST_PORTS_FILE, "utf8"))).toContain(43212);
untrackChromiumPort(43212);
expect(JSON.parse(fs.readFileSync(TEST_PORTS_FILE, "utf8"))).not.toContain(43212);
});
it("ignores a corrupt persistence file", () => {
fs.writeFileSync(TEST_PORTS_FILE, "not json{{{");
expect(() => getCandidateChromiumPorts()).not.toThrow();
expect(getCandidateChromiumPorts()).toContain(9222);
// Tracking after corruption rewrites the file cleanly.
trackChromiumPort(43213);
portsToCleanup.push(43213);
expect(JSON.parse(fs.readFileSync(TEST_PORTS_FILE, "utf8"))).toContain(43213);
});
});