230 lines
8.0 KiB
TypeScript
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");
|
|
},
|
|
);
|