import { execFile } from "node:child_process"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { afterEach, describe, expect, it } from "vitest"; import { startFakeCdpBrowser, type FakeCdpBrowser, } from "./fixtures/fakeCdpBrowser.js"; import { writeElectronBoundaryFixture } from "./fixtures/electronBoundaryApplication.js"; import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../src/contracts/javascriptRuntimeReconciliationExample.js"; const execute = promisify(execFile); const INTEGRATION_TEST_TIMEOUT_MS = 20_000; describe("Electron CLI parity", () => { const browsers: FakeCdpBrowser[] = []; const temporary: string[] = []; afterEach(async () => { await Promise.all( browsers.splice(0).map(async (browser) => browser.close()), ); await Promise.all( temporary .splice(0) .map(async (path) => rm(path, { recursive: true, force: true })), ); }); it( "returns the same root-confined Electron Evidence contracts", async () => { const root = await mkdtemp(join(tmpdir(), "rea-electron-cli-")); temporary.push(root); await writeFile( join(root, "index.html"), "", ); await writeFile(join(root, "app.js"), "export const app = true;"); const browser = await startFakeCdpBrowser({ electronFileUrl: pathToFileURL(join(root, "index.html")).href, }); browsers.push(browser); const environment = { ...process.env, REA_ELECTRON_OBSERVE_ENABLED: "true", REA_ELECTRON_CDP_ENDPOINTS_JSON: JSON.stringify([browser.endpoint]), REA_ELECTRON_FILE_ROOTS_JSON: JSON.stringify([root]), }; const listed = await runCli( ["list-electron-targets", browser.endpoint, "--approved", "--json"], environment, ); expect(listed).toMatchObject({ operation: "list_electron_targets", normalized_result: { targets: { items: [{ target_id: "electron-page" }] }, }, }); const inspected = await runCli( [ "inspect-electron-page", browser.endpoint, "electron-page", "--approved", "--observation-ms", "0", "--json", ], environment, ); expect(inspected).toMatchObject({ operation: "inspect_electron_page", provider: { id: "rea-cdp-electron" }, normalized_result: { target: { file_path: join(root, "index.html") }, }, }); }, INTEGRATION_TEST_TIMEOUT_MS, ); it( "returns the static JavaScript application Evidence contract", async () => { const root = await mkdtemp(join(tmpdir(), "rea-electron-static-cli-")); temporary.push(root); await writeElectronBoundaryFixture(root); const environment = { ...process.env, REA_INVESTIGATION_INPUT_ROOTS_JSON: JSON.stringify([root]), }; const analyzed = await runCli( ["analyze-javascript-application", root, "--approved", "--json"], environment, ); expect(analyzed).toMatchObject({ operation: "analyze_javascript_application", provider: { id: "rea-javascript-application" }, normalized_result: { input_path: root, summary: { browser_windows: 3, context_bridge_apis: 2, ipc: { paired_renderer_transmissions: 4 }, }, }, }); }, INTEGRATION_TEST_TIMEOUT_MS, ); it( "reads a file and returns the derived static/runtime reconciliation contract", async () => { const root = await mkdtemp(join(tmpdir(), "rea-runtime-cli-")); const inputPath = join(root, "reconciliation.json"); temporary.push(root); await writeFile( inputPath, JSON.stringify(JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE), ); const reconciled = await runCli( ["reconcile-javascript-runtime", inputPath, "--json"], process.env, ); expect(reconciled).toMatchObject({ operation: "reconcile_javascript_runtime", provider: { id: "rea-javascript-runtime-reconciliation" }, normalized_result: { schema_version: 1, summary: { runtime_scripts: 1, matched: 1 }, source_map_authority: { used_for_primary_matching: false }, }, }); const missingPath = join(root, "missing.json"); const missing = await runCli( ["reconcile-javascript-runtime", missingPath, "--json"], process.env, ); expect(missing).toMatchObject({ input_path: missingPath, input_reason: "read-failed", maximum_input_bytes: 64 * 1_024 * 1_024, }); }, INTEGRATION_TEST_TIMEOUT_MS, ); }); const runCli = async ( arguments_: readonly string[], environment: NodeJS.ProcessEnv, ): Promise => { try { const { stdout } = await execute( process.execPath, ["scripts/rea.mjs", ...arguments_], { cwd: process.cwd(), env: environment, maxBuffer: 16 * 1_024 * 1_024 }, ); return JSON.parse(stdout); } catch (cause: unknown) { if ( typeof cause === "object" && cause !== null && "stdout" in cause && typeof cause.stdout === "string" ) return JSON.parse(cause.stdout); throw cause; } };