fix(opencode): override inherited disabled flags (#3469)
Set disabled to false in generated OpenCode V2 registrations so a lower-priority disabled entry cannot keep approved setup stale. Cover layered JSON/JSONC refreshes, idempotent setup and doctor readiness, exact backups, preserved lower settings, malformed-config refusal, and the exclusion of disabled registrations from automatic maintenance. Update the native V2 registration expectations. Fixes #3318
This commit is contained in:
1 parent
740f107ff9
commit
c592ea6bac
4 files changed
+182
-3
No files matched your search
@@ -1674,10 +1674,11 @@ export const clientRegistrationEntry = (
|
||||
...(Object.keys(environment).length === 0 ? {} : { env: environment }),
|
||||
};
|
||||
case "opencode_v2":
|
||||
// V2 connects servers unless `disabled` is true.
|
||||
// Override lower-priority disabled flags when refreshing a V2 registration.
|
||||
return {
|
||||
type: "local",
|
||||
command: [...command],
|
||||
disabled: false,
|
||||
...(Object.keys(environment).length === 0
|
||||
? {}
|
||||
: { environment: { ...environment } }),
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { parse } from "jsonc-parser";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const setupArguments = [
|
||||
"setup",
|
||||
"--client",
|
||||
"opencode",
|
||||
"--skill=false",
|
||||
"--yes",
|
||||
"--json",
|
||||
];
|
||||
|
||||
const layeredOpenCode = async (
|
||||
cli: TestCli,
|
||||
disabled: boolean,
|
||||
jsonc: boolean,
|
||||
) => {
|
||||
const home = await createTestTempDirectory("rea-opencode-v2-activation-");
|
||||
const configurationHome = join(home, "config");
|
||||
const directory = join(configurationHome, "opencode");
|
||||
await mkdir(directory, { recursive: true });
|
||||
const lower = join(directory, "config.json");
|
||||
const upper = join(directory, jsonc ? "opencode.jsonc" : "opencode.json");
|
||||
const other = { type: "local", command: ["other-server"], disabled: true };
|
||||
const originalLower = JSON.stringify({
|
||||
mcp: {
|
||||
servers: {
|
||||
rea: {
|
||||
type: "local",
|
||||
command: ["rea", "mcp"],
|
||||
disabled,
|
||||
environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "off" },
|
||||
},
|
||||
other,
|
||||
},
|
||||
},
|
||||
});
|
||||
const originalUpper = `${jsonc ? "// Keep the user configuration comment.\n" : ""}${JSON.stringify(
|
||||
{
|
||||
mcp: { servers: {} },
|
||||
theme: "system",
|
||||
},
|
||||
)}\n`;
|
||||
await writeFile(lower, originalLower);
|
||||
await writeFile(upper, originalUpper);
|
||||
const run = (arguments_: readonly string[]) =>
|
||||
cli.run({
|
||||
arguments: arguments_,
|
||||
cwd: home,
|
||||
environment: {
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
XDG_CONFIG_HOME: configurationHome,
|
||||
APPDATA: join(home, "appdata"),
|
||||
LOCALAPPDATA: join(home, "localappdata"),
|
||||
HOPPER_LAUNCHER_PATH: join(home, "absent-hopper"),
|
||||
},
|
||||
});
|
||||
return { lower, upper, originalLower, originalUpper, other, run };
|
||||
};
|
||||
|
||||
for (const disabled of [true, false]) {
|
||||
for (const jsonc of [false, true]) {
|
||||
cliTest(
|
||||
`OpenCode V2 setup activates ${jsonc ? "JSONC" : "JSON"} over a lower disabled=${disabled} registration`,
|
||||
async ({ cli }) => {
|
||||
const fixture = await layeredOpenCode(cli, disabled, jsonc);
|
||||
const setup = await fixture.run(setupArguments);
|
||||
expect(setup.exitCode, setup.stdout).toBe(0);
|
||||
expect(setup.json).toMatchObject({ status: "ready" });
|
||||
const configured = await readFile(fixture.upper, "utf8");
|
||||
expect(parse(configured)).toMatchObject({
|
||||
mcp: {
|
||||
servers: {
|
||||
rea: {
|
||||
type: "local",
|
||||
},
|
||||
},
|
||||
},
|
||||
theme: "system",
|
||||
});
|
||||
if (disabled)
|
||||
expect(parse(configured).mcp.servers.rea.disabled).toBe(false);
|
||||
if (jsonc)
|
||||
expect(configured).toContain(
|
||||
"// Keep the user configuration comment.",
|
||||
);
|
||||
expect(await readFile(fixture.lower, "utf8")).toBe(
|
||||
fixture.originalLower,
|
||||
);
|
||||
expect(await readFile(`${fixture.upper}.rea.backup`, "utf8")).toBe(
|
||||
fixture.originalUpper,
|
||||
);
|
||||
|
||||
const repeated = await fixture.run(setupArguments);
|
||||
expect(repeated.exitCode, repeated.stdout).toBe(0);
|
||||
expect(repeated.json).toMatchObject({
|
||||
status: "ready",
|
||||
plannedActions: [],
|
||||
appliedActions: [],
|
||||
});
|
||||
const doctor = await fixture.run([
|
||||
"doctor",
|
||||
"--client",
|
||||
"opencode",
|
||||
"--json",
|
||||
]);
|
||||
expect(doctor.exitCode, doctor.stdout).toBe(0);
|
||||
expect(doctor.json).toMatchObject({
|
||||
healthy: true,
|
||||
identity: {
|
||||
registrations: expect.arrayContaining([
|
||||
expect.objectContaining({ client: "opencode", state: "aligned" }),
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(await readFile(fixture.upper, "utf8")).toBe(configured);
|
||||
expect(await readFile(fixture.lower, "utf8")).toBe(
|
||||
fixture.originalLower,
|
||||
);
|
||||
expect(
|
||||
parse(await readFile(fixture.lower, "utf8")).mcp.servers.other,
|
||||
).toEqual(fixture.other);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cliTest(
|
||||
"OpenCode V2 activation does not bypass a malformed lower configuration",
|
||||
async ({ cli }) => {
|
||||
const fixture = await layeredOpenCode(cli, true, true);
|
||||
const malformed = "{ not json";
|
||||
await writeFile(fixture.lower, malformed);
|
||||
const setup = await fixture.run(setupArguments);
|
||||
expect(setup.exitCode, setup.stdout).toBe(1);
|
||||
expect(setup.json).toMatchObject({
|
||||
status: "needs_human",
|
||||
plannedActions: [],
|
||||
appliedActions: [],
|
||||
remediation: expect.stringContaining(fixture.lower),
|
||||
});
|
||||
expect(await readFile(fixture.lower, "utf8")).toBe(malformed);
|
||||
expect(await readFile(fixture.upper, "utf8")).toBe(fixture.originalUpper);
|
||||
await expect(access(`${fixture.upper}.rea.backup`)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -458,7 +458,7 @@ describe("OpenCode V2 native server table", () => {
|
||||
timeout: { catalog: 30000 },
|
||||
servers: {
|
||||
other: { type: "local", command: ["node", "other.js"] },
|
||||
rea: { type: "local", command: [...command] },
|
||||
rea: { type: "local", command: [...command], disabled: false },
|
||||
},
|
||||
});
|
||||
expect(await readClientRegistrationStatuses(home)).toEqual(
|
||||
@@ -501,7 +501,9 @@ describe("OpenCode V2 native server table", () => {
|
||||
expect(
|
||||
parseOpenCode(await readFile(client.configPath, "utf8")).mcp,
|
||||
).toEqual({
|
||||
servers: { rea: { type: "local", command: [...command] } },
|
||||
servers: {
|
||||
rea: { type: "local", command: [...command], disabled: false },
|
||||
},
|
||||
});
|
||||
expect((await systemUninstallHost(home).removeClient(client)).status).toBe(
|
||||
"removed",
|
||||
|
||||
@@ -130,6 +130,28 @@ describe("OMP integration maintenance", () => {
|
||||
});
|
||||
|
||||
describe("maintenance activation and layered configuration", () => {
|
||||
it("leaves an inherited disabled OpenCode V2 registration outside maintenance", async () => {
|
||||
const upper = await writeClient(
|
||||
"opencode",
|
||||
JSON.stringify({ mcp: { servers: {} }, theme: "user-selected" }),
|
||||
);
|
||||
const lower = join(home, ".config", "opencode", "config.json");
|
||||
const originalLower = JSON.stringify({
|
||||
mcp: {
|
||||
servers: {
|
||||
rea: { type: "local", command: [entryPoint, "mcp"], disabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
await writeFile(lower, originalLower);
|
||||
const originalUpper = await readFile(upper, "utf8");
|
||||
await expect(
|
||||
existingMaintenanceScope(home, entryPoint, process.env),
|
||||
).resolves.toMatchObject({ clients: [] });
|
||||
expect(await readFile(lower, "utf8")).toBe(originalLower);
|
||||
expect(await readFile(upper, "utf8")).toBe(originalUpper);
|
||||
});
|
||||
|
||||
it("leaves a natively disabled ZCode registration outside maintenance", async () => {
|
||||
const configPath = await writeClient(
|
||||
"zcode",
|
||||
|
||||
Reference in new issue
Block a user