mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
377 lines
13 KiB
TypeScript
377 lines
13 KiB
TypeScript
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
import {
|
|
clientRegistrationEntry,
|
|
parseClientConfiguration,
|
|
effectiveClientServer,
|
|
} from "../../../src/application/ClientConfigurationDocument.js";
|
|
import {
|
|
configureClientConfiguration,
|
|
inspectClientConfiguration,
|
|
clientConfigurationAligned,
|
|
} from "../../../src/application/SetupClientConfiguration.js";
|
|
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
|
|
import { npxRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import {
|
|
effectiveClientConfiguration,
|
|
readClientConfigurationFiles,
|
|
} from "../../../src/application/ClientConfigurationFiles.js";
|
|
import { clientRegistrationEnvironmentDetails } from "../../../src/application/ClientRegistrationEnvironment.js";
|
|
|
|
const command = npxRegistrationCommand(process.platform);
|
|
const language = "x86:LE:32:default";
|
|
const compiler = "borlandcpp";
|
|
|
|
it.each(["missing", "empty", "legacy", "native"] as const)(
|
|
"uses effective OpenCode native activation when the upper layer is %s",
|
|
async (shape) => {
|
|
const home = await createTestTempDirectory(
|
|
"rea-opencode-effective-dialect-",
|
|
);
|
|
const root = join(home, ".config", "opencode");
|
|
await mkdir(root, { recursive: true });
|
|
const lower = join(root, "opencode.json");
|
|
const upper = join(root, "opencode.jsonc");
|
|
const originalLower = JSON.stringify({
|
|
mcp: {
|
|
servers: {
|
|
rea: { type: "local", command, disabled: true },
|
|
other: { type: "local", command: ["other"], disabled: true },
|
|
},
|
|
},
|
|
});
|
|
await writeFile(lower, originalLower);
|
|
if (shape !== "missing")
|
|
await writeFile(
|
|
upper,
|
|
JSON.stringify({
|
|
theme: "caller-theme",
|
|
...(shape === "legacy"
|
|
? {
|
|
mcp: {
|
|
rea: {
|
|
type: "local",
|
|
command,
|
|
enabled: true,
|
|
future_policy: { retain: true },
|
|
},
|
|
},
|
|
}
|
|
: {}),
|
|
...(shape === "native" ? { mcp: { servers: {} } } : {}),
|
|
}),
|
|
);
|
|
const client = {
|
|
name: "opencode",
|
|
format: "opencode",
|
|
configPath: upper,
|
|
configPaths: [lower, upper],
|
|
} as const;
|
|
expect(
|
|
await configureClientConfiguration(client, {}, command),
|
|
).toMatchObject({ status: "configured" });
|
|
const files = await readClientConfigurationFiles(client);
|
|
expect(files.ok).toBe(true);
|
|
if (!files.ok) throw new Error(files.error.detail);
|
|
const effective = effectiveClientConfiguration(files.value);
|
|
expect(effective?.dialect).toBe("opencode_v2");
|
|
expect(
|
|
effective === undefined
|
|
? undefined
|
|
: effectiveClientServer(effective, "rea"),
|
|
).toMatchObject({ disabled: false });
|
|
const written = JSON.parse(await readFile(upper, "utf8"));
|
|
expect(written.mcp.servers.rea).toMatchObject({
|
|
type: "local",
|
|
command,
|
|
disabled: false,
|
|
});
|
|
if (shape === "legacy")
|
|
expect(written.mcp.servers.rea.future_policy).toEqual({ retain: true });
|
|
expect(written.mcp.rea).toBeUndefined();
|
|
expect(await inspectClientConfiguration(client, {}, command)).toEqual({
|
|
status: "already_current",
|
|
});
|
|
expect(await configureClientConfiguration(client, {}, command)).toEqual({
|
|
status: "unchanged",
|
|
});
|
|
expect(await readFile(lower, "utf8")).toBe(originalLower);
|
|
},
|
|
);
|
|
|
|
const layered = async (lower: unknown, upper: unknown) => {
|
|
const home = await createTestTempDirectory("rea-layered-environment-");
|
|
const directory = join(home, ".config/opencode");
|
|
await mkdir(directory, { recursive: true });
|
|
const paths = [
|
|
join(directory, "opencode.json"),
|
|
join(directory, "opencode.jsonc"),
|
|
];
|
|
const sources = [lower, upper].map((environment) =>
|
|
JSON.stringify({
|
|
mcp: {
|
|
rea: {
|
|
...clientRegistrationEntry("opencode", command, {}),
|
|
environment,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
for (const [index, path] of paths.entries())
|
|
await writeFile(path, sources[index]!);
|
|
const client = {
|
|
name: "opencode",
|
|
format: "opencode",
|
|
configPath: paths[1]!,
|
|
configPaths: paths,
|
|
} as const;
|
|
const status = async () =>
|
|
(
|
|
await readClientRegistrationStatuses(home, "rea", {
|
|
environment: { XDG_CONFIG_HOME: join(home, ".config") },
|
|
})
|
|
).find(({ client }) => client === "opencode");
|
|
const unchanged = async () => {
|
|
for (const [index, path] of paths.entries()) {
|
|
expect(await readFile(path, "utf8")).toBe(sources[index]);
|
|
await expect(access(`${path}.rea.backup`)).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
}
|
|
};
|
|
return { client, paths, sources, status, unchanged };
|
|
};
|
|
|
|
it.skipIf(process.platform !== "win32").each([
|
|
{ REA_ANALYSIS_PROVIDER: "ghidra", rea_analysis_provider: "hopper" },
|
|
{ rea_analysis_provider: "hopper", REA_ANALYSIS_PROVIDER: "ghidra" },
|
|
])(
|
|
"keeps explicit overrides above inherited Windows aliases: %j",
|
|
async (lower) => {
|
|
const fixture = await layered(lower, {});
|
|
expect(
|
|
await configureClientConfiguration(
|
|
fixture.client,
|
|
{ REA_ANALYSIS_PROVIDER: "ghidra" },
|
|
command,
|
|
),
|
|
).toMatchObject({ status: "configured" });
|
|
const sources = await readClientConfigurationFiles(fixture.client);
|
|
expect(sources.ok).toBe(true);
|
|
if (!sources.ok) throw new Error(sources.error.detail);
|
|
const environment = clientRegistrationEnvironmentDetails(
|
|
effectiveClientConfiguration(sources.value),
|
|
);
|
|
expect(environment.ok).toBe(true);
|
|
if (!environment.ok) throw environment.error;
|
|
expect(environment.value.timeoutInputs?.analysisProvider).toBe("ghidra");
|
|
expect(environment.value.values).toHaveProperty(
|
|
"REA_ANALYSIS_PROVIDER",
|
|
"ghidra",
|
|
);
|
|
expect(environment.value.values).not.toHaveProperty(
|
|
"rea_analysis_provider",
|
|
);
|
|
expect(await readFile(fixture.paths[0]!, "utf8")).toBe(fixture.sources[0]);
|
|
expect(
|
|
await configureClientConfiguration(
|
|
fixture.client,
|
|
{ REA_ANALYSIS_PROVIDER: "ghidra" },
|
|
command,
|
|
),
|
|
).toEqual({ status: "unchanged" });
|
|
},
|
|
);
|
|
|
|
it("validates language/compiler settings in the effective OpenCode environment", async () => {
|
|
const fixture = await layered(
|
|
{ REA_GHIDRA_LANGUAGE_ID: language },
|
|
{ REA_GHIDRA_COMPILER_SPEC_ID: compiler },
|
|
);
|
|
expect(await inspectClientConfiguration(fixture.client, {}, command)).toEqual(
|
|
{ status: "already_current" },
|
|
);
|
|
expect(await clientConfigurationAligned(fixture.client, {}, command)).toBe(
|
|
true,
|
|
);
|
|
expect(await fixture.status()).toMatchObject({ state: "aligned" });
|
|
expect(
|
|
await configureClientConfiguration(fixture.client, {}, command),
|
|
).toEqual({ status: "unchanged" });
|
|
await fixture.unchanged();
|
|
});
|
|
|
|
it("rejects a companion absent from the complete layered environment", async () => {
|
|
const fixture = await layered(
|
|
{ CALLER_SETTING: "keep" },
|
|
{ REA_GHIDRA_COMPILER_SPEC_ID: compiler },
|
|
);
|
|
expect(
|
|
await inspectClientConfiguration(fixture.client, {}, command),
|
|
).toMatchObject({
|
|
status: "invalid",
|
|
remediation: expect.stringContaining("REA_GHIDRA_COMPILER_SPEC_ID"),
|
|
});
|
|
expect(await clientConfigurationAligned(fixture.client, {}, command)).toBe(
|
|
false,
|
|
);
|
|
expect(await fixture.status()).toMatchObject({
|
|
state: "invalid",
|
|
remediation: expect.stringContaining("REA_GHIDRA_COMPILER_SPEC_ID"),
|
|
});
|
|
expect(
|
|
await configureClientConfiguration(fixture.client, {}, command),
|
|
).toMatchObject({ status: "failed" });
|
|
await fixture.unchanged();
|
|
});
|
|
|
|
it("keeps higher-priority values and does not copy inherited settings into the write layer", async () => {
|
|
const fixture = await layered(
|
|
{
|
|
REA_GHIDRA_LANGUAGE_ID: language,
|
|
HOPPER_LOADER_ARGS_JSON: '["lower"]',
|
|
["__proto__"]: "lower caller value",
|
|
},
|
|
{
|
|
REA_GHIDRA_COMPILER_SPEC_ID: compiler,
|
|
HOPPER_LOADER_ARGS_JSON: '[""]',
|
|
["__proto__"]: "selected caller value",
|
|
},
|
|
);
|
|
expect(
|
|
await configureClientConfiguration(
|
|
fixture.client,
|
|
{ REA_LOG_LEVEL: "debug" },
|
|
command,
|
|
),
|
|
).toMatchObject({ status: "configured" });
|
|
expect(await readFile(fixture.paths[0]!, "utf8")).toBe(fixture.sources[0]);
|
|
const configured = parseClientConfiguration(
|
|
await readFile(fixture.client.configPath, "utf8"),
|
|
"opencode",
|
|
);
|
|
expect(effectiveClientServer(configured, "rea")).toHaveProperty(
|
|
"environment",
|
|
{
|
|
REA_GHIDRA_COMPILER_SPEC_ID: compiler,
|
|
HOPPER_LOADER_ARGS_JSON: '[""]',
|
|
REA_LOG_LEVEL: "debug",
|
|
["__proto__"]: "selected caller value",
|
|
},
|
|
);
|
|
expect(
|
|
await readFile(`${fixture.client.configPath}.rea.backup`, "utf8"),
|
|
).toBe(fixture.sources[1]);
|
|
expect(await fixture.status()).toMatchObject({ state: "aligned" });
|
|
expect(await clientConfigurationAligned(fixture.client, {}, command)).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
await configureClientConfiguration(fixture.client, {}, command),
|
|
).toEqual({ status: "unchanged" });
|
|
});
|
|
|
|
it("merges caller server names as own keys without inventing absent registrations", () => {
|
|
const lower = parseClientConfiguration(
|
|
'{"mcp":{"__proto__":{"type":"local","command":["peer"],"environment":{"LOWER":"retained"}}}}',
|
|
"opencode",
|
|
);
|
|
const upper = parseClientConfiguration(
|
|
'{"mcp":{"__proto__":{"environment":{"UPPER":"selected"}}}}',
|
|
"opencode",
|
|
);
|
|
expect(effectiveClientServer(lower, "constructor")).toBeUndefined();
|
|
const effective = effectiveClientConfiguration([
|
|
{ ...lower, sourcePath: "lower.json" },
|
|
{ ...upper, sourcePath: "upper.json" },
|
|
]);
|
|
expect(effective).toBeDefined();
|
|
if (effective === undefined) throw new Error("missing layered configuration");
|
|
expect(Object.keys(effective.servers)).toEqual(["__proto__"]);
|
|
expect(effectiveClientServer(effective, "__proto__")).toEqual({
|
|
type: "local",
|
|
command: ["peer"],
|
|
environment: { LOWER: "retained", UPPER: "selected" },
|
|
});
|
|
expect(effectiveClientServer(effective, "toString")).toBeUndefined();
|
|
});
|
|
|
|
it.each([
|
|
["invalid JSON", { HOPPER_LOADER_ARGS_JSON: "{" }],
|
|
["invalid value type", { HOPPER_LOADER_ARGS_JSON: 42 }],
|
|
["invalid environment map", []],
|
|
])(
|
|
"does not hide a lower-priority %s behind a valid override",
|
|
async (_name, lower) => {
|
|
const fixture = await layered(lower, { HOPPER_LOADER_ARGS_JSON: "[]" });
|
|
expect(
|
|
await inspectClientConfiguration(fixture.client, {}, command),
|
|
).toMatchObject({
|
|
status: "invalid",
|
|
remediation: expect.stringContaining(fixture.paths[0]!),
|
|
});
|
|
|
|
expect(await fixture.status()).toMatchObject({ state: "invalid" });
|
|
expect(
|
|
await configureClientConfiguration(fixture.client, {}, command),
|
|
).toMatchObject({ status: "failed" });
|
|
await fixture.unchanged();
|
|
},
|
|
);
|
|
|
|
it("uses inherited settings when creating a missing write layer", async () => {
|
|
const fixture = await layered({ REA_GHIDRA_LANGUAGE_ID: language }, {});
|
|
await rm(fixture.client.configPath);
|
|
const overrides = { REA_GHIDRA_COMPILER_SPEC_ID: compiler };
|
|
expect(
|
|
await inspectClientConfiguration(fixture.client, overrides, command),
|
|
).toEqual({ status: "create" });
|
|
expect(
|
|
await configureClientConfiguration(fixture.client, overrides, command),
|
|
).toMatchObject({ status: "configured" });
|
|
const parsed = parseClientConfiguration(
|
|
await readFile(fixture.client.configPath, "utf8"),
|
|
"opencode",
|
|
);
|
|
expect(effectiveClientServer(parsed, "rea")).toHaveProperty(
|
|
"environment",
|
|
overrides,
|
|
);
|
|
expect(await readFile(fixture.paths[0]!, "utf8")).toBe(fixture.sources[0]);
|
|
expect(await fixture.status()).toMatchObject({ state: "aligned" });
|
|
await expect(
|
|
access(`${fixture.client.configPath}.rea.backup`),
|
|
).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("replaces the selected write layer rather than the last loaded layer", async () => {
|
|
const fixture = await layered(
|
|
{ REA_GHIDRA_LANGUAGE_ID: language },
|
|
{ REA_GHIDRA_COMPILER_SPEC_ID: compiler },
|
|
);
|
|
const client = { ...fixture.client, configPath: fixture.paths[0]! };
|
|
const overrides = { REA_LOG_LEVEL: "debug" };
|
|
expect(await inspectClientConfiguration(client, {}, command)).toEqual({
|
|
status: "already_current",
|
|
});
|
|
expect(
|
|
await configureClientConfiguration(client, overrides, command),
|
|
).toMatchObject({ status: "configured" });
|
|
const parsed = parseClientConfiguration(
|
|
await readFile(client.configPath, "utf8"),
|
|
"opencode",
|
|
);
|
|
expect(effectiveClientServer(parsed, "rea")).toHaveProperty("environment", {
|
|
REA_GHIDRA_LANGUAGE_ID: language,
|
|
...overrides,
|
|
});
|
|
expect(await readFile(fixture.paths[1]!, "utf8")).toBe(fixture.sources[1]);
|
|
expect(await readFile(`${client.configPath}.rea.backup`, "utf8")).toBe(
|
|
fixture.sources[0],
|
|
);
|
|
expect(await clientConfigurationAligned(client, {}, command)).toBe(true);
|
|
expect(await fixture.status()).toMatchObject({ state: "aligned" });
|
|
});
|