220 lines
7.7 KiB
TypeScript
220 lines
7.7 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";
|
|
|
|
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" });
|
|
});
|