diff --git a/src/application/SetupRegistrationEnvironment.ts b/src/application/SetupRegistrationEnvironment.ts index f531c6ffb0..fff31b3223 100644 --- a/src/application/SetupRegistrationEnvironment.ts +++ b/src/application/SetupRegistrationEnvironment.ts @@ -7,6 +7,7 @@ const SELECTED_REGISTRATION_SETTINGS = [ "GHIDRA_MAXMEM", "JAVA_HOME", "HOPPER_LAUNCHER_PATH", + "HOPPER_LOADER_ARGS_JSON", "REA_ILSPY_CMD_PATH", "REA_ADB_PATH", "REA_APKTOOL_COMMAND", diff --git a/tests/acceptance/setup/registrationRuntimeEnvironmentCli.test.ts b/tests/acceptance/setup/registrationRuntimeEnvironmentCli.test.ts index 6d9bdfd20c..edbeb257a3 100644 --- a/tests/acceptance/setup/registrationRuntimeEnvironmentCli.test.ts +++ b/tests/acceptance/setup/registrationRuntimeEnvironmentCli.test.ts @@ -26,6 +26,7 @@ cliTest( const config = join(home, ".qwen/settings.json"); const selected = { HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"), + HOPPER_LOADER_ARGS_JSON: '["", "--loader", "Mach-O"]', GHIDRA_INSTALL_DIR: join(home, "offline-ghidra"), GHIDRA_HEADLESS_MAXMEM: "512M", GHIDRA_MAXMEM: "3G", @@ -111,6 +112,8 @@ cliTest( environment.REA_MCP_TOOL_FAMILIES = selected.REA_MCP_TOOL_FAMILIES; selected.REA_OBJDUMP_COMMAND = join(home, "replacement-objdump"); environment.REA_OBJDUMP_COMMAND = selected.REA_OBJDUMP_COMMAND; + selected.HOPPER_LOADER_ARGS_JSON = "[]"; + environment.HOPPER_LOADER_ARGS_JSON = selected.HOPPER_LOADER_ARGS_JSON; // Empty headless heap explicitly selects the lower-priority caller value. selected.GHIDRA_HEADLESS_MAXMEM = ""; selected.GHIDRA_MAXMEM = "768M"; diff --git a/tests/boundary/setup/setupLoaderEnvironment.test.ts b/tests/boundary/setup/setupLoaderEnvironment.test.ts new file mode 100644 index 0000000000..4eb80caad9 --- /dev/null +++ b/tests/boundary/setup/setupLoaderEnvironment.test.ts @@ -0,0 +1,74 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; +import { createCli } from "../../../src/cli.js"; +import { parseConfig } from "../../../src/config/parseConfig.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it("setup carries explicit Hopper loader selections into Cursor without persisting target or unrelated settings", async () => { + const home = await createTestTempDirectory("rea-setup-loader-environment-"); + const path = join(home, ".cursor/mcp.json"); + await mkdir(join(home, ".cursor")); + await writeFile(path, '{"caller":"retain"}\n'); + const environment = { + HOME: home, + USERPROFILE: home, + HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"), + GHIDRA_INSTALL_DIR: join(home, "missing-ghidra"), + JAVA_HOME: join(home, "missing-java"), + HOPPER_LOADER_ARGS_JSON: '["", "--loader", "Mach-O"]', + HOPPER_TARGET_PATH: join(home, "per-target"), + HOPPER_TARGET_KIND: "executable", + CALLER_UNRELATED_SETTING: "do not persist", + }; + const setup = async () => { + let stdout = ""; + await createCli(environment).serve( + ["setup", "--client", "cursor", "--skill=false", "--yes", "--json"], + { + env: environment, + stdout: (chunk) => { + stdout += chunk; + }, + exit: () => undefined, + }, + ); + return JSON.parse(stdout) as unknown; + }; + const loaderFromSavedRegistration = async () => { + const document = JSON.parse(await readFile(path, "utf8")); + expect(document.caller).toBe("retain"); + const saved = document.mcpServers.rea.env; + expect(saved).not.toHaveProperty("CALLER_UNRELATED_SETTING"); + expect(saved).not.toHaveProperty("HOPPER_TARGET_PATH"); + expect(saved).not.toHaveProperty("HOPPER_TARGET_KIND"); + const config = parseConfig(saved); + if (!config.ok) throw config.error; + return config.value.hopperLoaderArgs; + }; + + expect(await setup()).toMatchObject({ status: "ready" }); + expect(await loaderFromSavedRegistration()).toEqual([ + "", + "--loader", + "Mach-O", + ]); + const created = await readFile(path, "utf8"); + expect(JSON.parse(created).mcpServers.rea.env.HOPPER_LOADER_ARGS_JSON).toBe( + environment.HOPPER_LOADER_ARGS_JSON, + ); + environment.HOPPER_LOADER_ARGS_JSON = "[]"; + expect(await setup()).toMatchObject({ status: "ready" }); + expect(await loaderFromSavedRegistration()).toEqual([]); + const updated = await readFile(path, "utf8"); + expect(JSON.parse(updated).mcpServers.rea.env.HOPPER_LOADER_ARGS_JSON).toBe( + "[]", + ); + environment.HOPPER_LOADER_ARGS_JSON = "{"; + expect(await setup()).toMatchObject({ + status: "needs_human", + plannedActions: [], + appliedActions: [], + }); + expect(await readFile(path, "utf8")).toBe(updated); +});