Files
rea/tests/acceptance/applications/electronInvalidPaths.test.ts

230 lines
8.0 KiB
TypeScript

import { chmod, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest(
"reports every unresolved Electron path through CLI and MCP without launching",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-invalid-paths-");
const application = join(root, "main.js");
await writeFile(application, "module.exports = {};\n");
const missing = {
executable_path: join(root, "missing-electron"),
application_path: join(root, "missing-application"),
application_root: join(root, "missing-root"),
};
const requestPath = join(root, "input.json");
await writeFile(requestPath, JSON.stringify(missing));
const response = await cli.run({
arguments: ["capture-electron-scenario", requestPath, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode).toBe(1);
const expectedIssues = Object.entries(missing).map(([field, path]) => ({
path: [field],
reason: "invalid_value",
message: expect.stringContaining(path),
}));
expect(response.json).toMatchObject({
code: "invalid_request",
details: { issues: expectedIssues },
});
expect(response.stdout).toContain("ENOENT");
const { call } = await connectLocalToolsMcp();
for (const [field, path] of Object.entries(missing)) {
const result = await call("capture_electron_scenario", {
executable_path: process.execPath,
application_path: application,
application_root: root,
[field]: path,
});
expect(result.isError).toBe(true);
const content = result.content.find((item) => item.type === "text");
if (content?.type !== "text") throw new Error("Missing error response");
expect(JSON.parse(content.text)).toMatchObject({
error: {
code: "invalid_request",
details: {
issues: [
{
path: [field],
reason: "invalid_value",
message: expect.stringContaining(path),
},
],
},
},
});
expect(content.text).toContain("ENOENT");
}
const session = await call("binary_session", {});
expect(session.isError, JSON.stringify(session)).not.toBe(true);
},
);
const isWindows = process.platform === "win32";
const errorBody = (result: {
readonly content: readonly { readonly type: string }[];
}) => {
const content = result.content.find((item) => item.type === "text");
if (content?.type !== "text" || !("text" in content))
throw new Error("Missing error response");
return JSON.parse(String(content.text));
};
const createElectronApplication = async (root: string) => {
const application = join(root, "app");
await mkdir(application);
await writeFile(
join(application, "package.json"),
JSON.stringify({ name: "fixture", main: "main.js" }),
);
await writeFile(join(application, "main.js"), "");
return application;
};
cliTest.skipIf(isWindows)(
"rejects a non-executable Electron executable_path before launch through CLI and MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-not-executable-");
const application = await createElectronApplication(root);
const executable = join(root, "not-an-executable.txt");
await writeFile(executable, "plain text");
await chmod(executable, 0o644);
const request = {
executable_path: executable,
application_path: application,
actions: [],
};
const requestPath = join(root, "input.json");
await writeFile(requestPath, JSON.stringify(request));
const response = await cli.run({
arguments: ["capture-electron-scenario", requestPath, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode).toBe(1);
expect(response.json).toMatchObject({
code: "invalid_request",
details: {
issues: [{ path: ["executable_path"], reason: "invalid_value" }],
},
});
const { call } = await connectLocalToolsMcp();
const result = await call("capture_electron_scenario", request);
expect(result.isError).toBe(true);
expect(errorBody(result)).toMatchObject({
error: {
code: "invalid_request",
details: {
issues: [{ path: ["executable_path"], reason: "invalid_value" }],
},
},
});
const session = await call("binary_session", {});
expect(session.isError, JSON.stringify(session)).not.toBe(true);
},
);
cliTest.skipIf(isWindows)(
"returns a typed failure when an executable exits before Electron starts and keeps the MCP server and its Evidence",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-early-exit-");
const application = await createElectronApplication(root);
const executable = join(root, "exits-immediately");
await writeFile(executable, "#!/bin/sh\nexit 0\n");
await chmod(executable, 0o755);
const request = {
executable_path: executable,
application_path: application,
actions: [],
};
const requestPath = join(root, "input.json");
await writeFile(requestPath, JSON.stringify(request));
const response = await cli.run({
arguments: ["capture-electron-scenario", requestPath, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode).toBe(1);
expect(response.json).toMatchObject({
code: "execution_failure",
details: { operation: "capture_electron_scenario" },
});
expect(response.stdout).toContain("Process failed to launch");
const { call } = await connectLocalToolsMcp();
const analysis = await call("analyze_javascript_application", {
input_path: application,
});
expect(analysis.isError, JSON.stringify(analysis)).not.toBe(true);
const retained = async () => {
const bundle = await call("get_evidence_bundle", {});
expect(bundle.isError, JSON.stringify(bundle)).not.toBe(true);
const structured = bundle.structuredContent as {
result: { records: { evidence_id: string }[] };
};
return structured.result.records.map((record) => record.evidence_id);
};
const before = await retained();
expect(before).toHaveLength(1);
const result = await call("capture_electron_scenario", request);
expect(result.isError).toBe(true);
expect(errorBody(result)).toMatchObject({
error: {
code: "execution_failure",
details: { operation: "capture_electron_scenario" },
},
});
const session = await call("binary_session", {});
expect(session.isError, JSON.stringify(session)).not.toBe(true);
expect(await retained()).toEqual(before);
},
180_000,
);
cliTest(
"rejects a file URL path field without resolving an invented local path",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-file-url-");
const application = join(root, "main.js");
await writeFile(application, "module.exports = {};\n");
const response = await cli.run({
arguments: [
"capture-electron-scenario",
JSON.stringify({
executable_path: "file:///opt/electron",
application_path: application,
}),
"--format",
"json",
],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode).toBe(1);
expect(response.json).toMatchObject({
code: "invalid_request",
details: {
issues: [
{
path: ["executable_path"],
reason: "invalid_format",
message: expect.stringContaining("absolute local filesystem path"),
},
],
},
});
expect(response.stdout).not.toContain("ENOENT");
expect(response.stdout).not.toContain("file:/opt/electron");
},
);