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"; const command = npxRegistrationCommand(process.platform); const language = "x86:LE:32:default"; const compiler = "borlandcpp"; 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("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"]' }, { REA_GHIDRA_COMPILER_SPEC_ID: compiler, HOPPER_LOADER_ARGS_JSON: '[""]' }, ); 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", }, ); 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.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" }); });