mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 20:06:14 +00:00
fix(setup): identify invalid launch environment overrides (#7899)
* fix(setup): identify invalid launch environment overrides * test(setup): cover merged environment recovery
This commit is contained in:
1 parent
473cfa36b8
commit
16cfe30dab
3 files changed
+226
-7
No files matched your search
@@ -22,11 +22,34 @@ export interface ClientConfigurationSource extends ClientConfigurationDocument {
|
||||
const invalidEnvironment = (
|
||||
path: string,
|
||||
error: ConfigurationError,
|
||||
): ClientConfigurationFileError => ({
|
||||
kind: "malformed",
|
||||
path,
|
||||
detail: `Configuration ${path} has an invalid REA server environment: ${error.settings.map(({ setting, constraint }) => `${setting}: ${constraint}`).join("; ")}. Repair it before rerunning setup.`,
|
||||
});
|
||||
overrides: Readonly<Record<string, string>> = {},
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): ClientConfigurationFileError => {
|
||||
const constraints = error.settings
|
||||
.map(({ setting, constraint }) => `${setting}: ${constraint}`)
|
||||
.join("; ");
|
||||
const overrideNames = new Set(
|
||||
Object.keys(overrides).map((name) =>
|
||||
platform === "win32" ? name.toUpperCase() : name,
|
||||
),
|
||||
);
|
||||
const overrideSettings = error.settings
|
||||
.map(({ setting }) => setting)
|
||||
.filter((setting) =>
|
||||
overrideNames.has(platform === "win32" ? setting.toUpperCase() : setting),
|
||||
);
|
||||
const location =
|
||||
overrideSettings.length > 0
|
||||
? `The proposed registration environment includes launch overrides (${overrideSettings.join(", ")}) that do not satisfy the effective REA configuration constraints`
|
||||
: overrideNames.size > 0
|
||||
? `The effective REA server environment for ${path} is invalid; check the client configuration and proposed launch overrides`
|
||||
: `Configuration ${path} has an invalid REA server environment`;
|
||||
return {
|
||||
kind: "malformed",
|
||||
path,
|
||||
detail: `${location}: ${constraints}${constraints.endsWith(".") ? "" : "."} ${overrideNames.size === 0 ? "Repair it" : "Check the launch environment and any existing registration settings"} before rerunning setup.`,
|
||||
};
|
||||
};
|
||||
|
||||
/** A named source failure that setup can explain without losing the bad path. */
|
||||
export interface ClientConfigurationFileError {
|
||||
@@ -110,7 +133,14 @@ export const readClientConfigurationFiles = async (
|
||||
platform,
|
||||
);
|
||||
if (!environment.ok)
|
||||
return err(invalidEnvironment(path, environment.error));
|
||||
return err(
|
||||
invalidEnvironment(
|
||||
path,
|
||||
environment.error,
|
||||
path === client.configPath ? writeOverrides : undefined,
|
||||
platform,
|
||||
),
|
||||
);
|
||||
documents.push({ ...document.value, sourcePath: path });
|
||||
}
|
||||
}
|
||||
@@ -121,7 +151,14 @@ export const readClientConfigurationFiles = async (
|
||||
platform,
|
||||
);
|
||||
if (!environment.ok)
|
||||
return err(invalidEnvironment(client.configPath, environment.error));
|
||||
return err(
|
||||
invalidEnvironment(
|
||||
client.configPath,
|
||||
environment.error,
|
||||
writeOverrides,
|
||||
platform,
|
||||
),
|
||||
);
|
||||
return ok(documents);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"identifies rejected launch overrides without blaming or changing a client file",
|
||||
async ({ cli }) => {
|
||||
const home = await createTestTempDirectory(
|
||||
"rea-setup-override-diagnostic-",
|
||||
);
|
||||
const config = join(home, ".claude.json");
|
||||
const environment = {
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
XDG_CONFIG_HOME: join(home, "config"),
|
||||
XDG_DATA_HOME: join(home, "data"),
|
||||
XDG_CACHE_HOME: join(home, "cache"),
|
||||
XDG_STATE_HOME: join(home, "state"),
|
||||
};
|
||||
const run = (overrides: Record<string, string>) =>
|
||||
cli.run({
|
||||
arguments: [
|
||||
"setup",
|
||||
"--client",
|
||||
"claude_code",
|
||||
"--yes",
|
||||
"--skill=false",
|
||||
"--json",
|
||||
],
|
||||
cwd: home,
|
||||
environment: { ...environment, ...overrides },
|
||||
});
|
||||
|
||||
const absent = await run({ REA_ASAR_MAX_HEADER_BYTES: "0" });
|
||||
expect(absent.exitCode).toBe(1);
|
||||
expect(absent.json).toMatchObject({
|
||||
status: "needs_human",
|
||||
plannedActions: [],
|
||||
appliedActions: [],
|
||||
remediation: expect.stringContaining("launch overrides"),
|
||||
});
|
||||
expect(absent.stdout).toContain("REA_ASAR_MAX_HEADER_BYTES");
|
||||
expect(absent.stdout).not.toContain(
|
||||
`Configuration ${config} has an invalid`,
|
||||
);
|
||||
await expect(access(config)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
|
||||
const original = JSON.stringify({
|
||||
mcpServers: {
|
||||
rea: { command: "rea", args: ["mcp"], env: { CALLER_KEY: "keep" } },
|
||||
},
|
||||
});
|
||||
await writeFile(config, original);
|
||||
const existing = await run({ REA_BRIDGE_MAX_REQUEST_BYTES: "1" });
|
||||
expect(existing.exitCode).toBe(1);
|
||||
expect(existing.json).toMatchObject({
|
||||
status: "needs_human",
|
||||
remediation: expect.stringContaining("launch overrides"),
|
||||
});
|
||||
expect(existing.stdout).toContain("REA_BRIDGE_MAX_REQUEST_BYTES");
|
||||
expect(await readFile(config, "utf8")).toBe(original);
|
||||
await expect(access(`${config}.rea.backup`)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
|
||||
const invalidFile = JSON.stringify({
|
||||
mcpServers: {
|
||||
rea: {
|
||||
command: "rea",
|
||||
args: ["mcp"],
|
||||
env: { HOPPER_LOADER_ARGS_JSON: "not-json" },
|
||||
},
|
||||
},
|
||||
});
|
||||
await writeFile(config, invalidFile);
|
||||
const stored = await run({});
|
||||
expect(stored.exitCode).toBe(1);
|
||||
expect(stored.json).toMatchObject({
|
||||
status: "needs_human",
|
||||
remediation: expect.stringContaining(config),
|
||||
});
|
||||
expect(stored.stdout).toContain("HOPPER_LOADER_ARGS_JSON");
|
||||
expect(stored.stdout).not.toContain("not-json");
|
||||
expect(await readFile(config, "utf8")).toBe(invalidFile);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,94 @@
|
||||
import { join } from "node:path";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { expect, it } from "vitest";
|
||||
import { readClientConfigurationFiles } from "../../../src/application/ClientConfigurationFiles.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it("attributes case-folded Windows overrides without disclosing values", async () => {
|
||||
const root = await createTestTempDirectory("rea-config-override-origin-");
|
||||
const configPath = join(root, "client.json");
|
||||
const client = { name: "cursor", format: "json", configPath } as const;
|
||||
const result = await readClientConfigurationFiles(
|
||||
client,
|
||||
undefined,
|
||||
"win32",
|
||||
{
|
||||
rea_asar_max_header_bytes: "0",
|
||||
},
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
detail: expect.stringContaining(
|
||||
"launch overrides (REA_ASAR_MAX_HEADER_BYTES)",
|
||||
),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a malformed stored environment attributed to its file", async () => {
|
||||
const root = await createTestTempDirectory("rea-config-file-origin-");
|
||||
const configPath = join(root, "client.json");
|
||||
await writeFile(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
rea: {
|
||||
command: "rea",
|
||||
args: ["mcp"],
|
||||
env: { HOPPER_LOADER_ARGS_JSON: "not-json" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = await readClientConfigurationFiles({
|
||||
name: "cursor",
|
||||
format: "json",
|
||||
configPath,
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { detail: expect.stringContaining(`Configuration ${configPath}`) },
|
||||
});
|
||||
if (!result.ok) expect(result.error.detail).not.toContain("not-json");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
stored: { REA_GHIDRA_LANGUAGE_ID: "x86:LE:64:default" },
|
||||
overrides: { REA_GHIDRA_PROJECT_PATH: "/tmp/selected-project.gpr" },
|
||||
reported: "REA_GHIDRA_LANGUAGE_ID",
|
||||
},
|
||||
{
|
||||
stored: { HOPPER_LOADER_ARGS_JSON: "invalid-json" },
|
||||
overrides: { REA_ASAR_MAX_HEADER_BYTES: "1024" },
|
||||
reported: "HOPPER_LOADER_ARGS_JSON",
|
||||
},
|
||||
])(
|
||||
"recommends checking both sources when the reported setting is not an override: $reported",
|
||||
async ({ stored, overrides, reported }) => {
|
||||
const root = await createTestTempDirectory("rea-config-merged-origin-");
|
||||
const configPath = join(root, "client.json");
|
||||
await writeFile(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
mcpServers: { rea: { command: "rea", args: ["mcp"], env: stored } },
|
||||
}),
|
||||
);
|
||||
const result = await readClientConfigurationFiles(
|
||||
{ name: "cursor", format: "json", configPath },
|
||||
undefined,
|
||||
process.platform,
|
||||
overrides,
|
||||
);
|
||||
if (result.ok) throw new Error("Expected rejected environment");
|
||||
expect(result.error.detail).toContain(reported);
|
||||
expect(result.error.detail).toContain(
|
||||
"check the client configuration and proposed launch overrides",
|
||||
);
|
||||
expect(result.error.detail).toContain(
|
||||
"Check the launch environment and any existing registration settings",
|
||||
);
|
||||
expect(result.error.detail).not.toContain("invalid-json");
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user