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:
Haoran Xu authored and GitHub committed 2026-10-23 02:33:38 +08:00
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",