mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
* fix(electron): reject non-executable paths and contain launch rejections An executable_path that runs but never prints the inspector line made Playwright's electron.launch() leave three waits unawaited. They rejected unhandled and Node ended the CLI or the MCP server (#2269). Check the resolved executable_path is a regular file, and on non-Windows hosts that it passes X_OK, before any launch. Wrap only the launch call in a scoped unhandledRejection listener that absorbs rejections whose stack is in playwright-core/lib and re-throws anything else. Tested on Linux with Node 22.23.1, 24.18.0 and 26.11.1. Refs #2269 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊 * fix(electron): absorb only this launch's orphaned rejections The listener around the launch dropped every rejection with a playwright-core stack, so an unrelated Playwright failure during the launch window would vanish. Hold those rejections instead. When the launch settles, drop only the ones whose message is part of the launch's own error (the orphans of a failed launch). Raise every other one again through the unhandledRejection path, so the process still fails. A launch that succeeds raises all of them. Rejections without a Playwright stack still fail at once. One setImmediate turn before the decision lets rejections queued by the settling tick arrive first. It is a single turn, not a timer. A shared set of absorbed errors stops overlapping launches from raising each other's orphans. The listener is removed on every path. Add child-process tests that drive the built provider through its launch seam, since Vitest's worker has its own unhandledRejection listener. 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊 * test(electron): distinguish launch success from capture ownership * test(electron): assert cleanup ownership failures around overlapping launches * fix(comparison): recheck historical calls-only changes (#2223) * fix(comparison): preserve incomplete function call evidence Keep missing, unknown, partial and unresolved call relationships unknown in function comparisons and reconstruction claims. Aggregate IDA caller and callee coverage without overwriting uncertainty. Validate with 144 focused tests, typecheck/lint, docs checks and test discovery. Real providers were not verified; the full headless IDA session suite requires unavailable Windows native controls. * fix(comparison): recheck historical calls-only changes --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> * fix(android): make DEX descriptor pattern portable * fix(ci): isolate Linux Hopper process fixture inventory * fix(browser): avoid replaying observed launch rejections * style: format comparison and IDA sources * test(electron): allow cold macOS ownership setup * test(electron): allow cold macOS setup in the early-exit path test The test that runs an executable which exits at once builds the Swift ownership helper twice on macOS: in the CLI process, and again in the MCP server process on its first tool call that prepares. A cold Swift cache can spend up to 120 seconds on a build. The test had only the 30 second default. Give the test 180 seconds, ase1579ec31did for electronLaunchRejections.test.ts and65a809a0ddid for webSourceLocationCli.test.ts. cli.run has no deadline of its own, so it needs no timeoutMs. 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊 * test(ci): isolate report fixtures and declare PowerPC Java bounds --------- Co-authored-by: Claude Opus 5.5 <opus@fox.invalid> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: Lucien <164857476+dakjdakd@users.noreply.github.com>
231 lines
8.0 KiB
TypeScript
231 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"],
|
|
message: expect.stringContaining(
|
|
"must be absolute local filesystem paths",
|
|
),
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(response.stdout).not.toContain("ENOENT");
|
|
expect(response.stdout).not.toContain("file:/opt/electron");
|
|
},
|
|
);
|