fix(doctor): read each client's own activation key (#6331)

* fix(doctor): read each client's own activation key

Doctor treated both `disabled: true` and `enabled: false` as a disabled
registration for every client except Grok. The installed clients each
read only one of them: Codex and Kimi Code honor `enabled` and still
load a server marked `disabled`, while Devin honors `disabled` and
still loads a server marked `enabled: false`. Doctor reported those
loaded registrations as stale, so `rea doctor --client codex` failed
with config_drift and setup offered to "repair" a working entry.

Skip the key each of these clients ignores. Other clients keep the
existing rule.

* fix(tests): resolve the Devin config root per platform in activation cases

The client activation cases wrote Devin's mcp_config.json under the
XDG root on every platform, but devinDirectory resolves APPDATA (or
~/AppData/Roaming) on Windows. Follow the client's per-platform root
so the cases exercise the file doctor actually reads there.
This commit is contained in:
Christopher Pruijsen authored and GitHub committed 2026-10-23 02:20:55 +08:00
1 parent 26bc77ce56
commit 4b4babf1e7
2 files changed
+83 -2

No files matched your search

+9 -1
View File
@@ -356,13 +356,21 @@ export const readClientRegistrationInspections = async (
);
};
// Codex and Kimi Code load a server marked `disabled: true` and honor only
// `enabled`; Devin loads one marked `enabled: false` and honors only `disabled`.
const ignoresDisabledKey = new Set(["codex", "kimi"]);
const ignoresEnabledKey = new Set(["devin"]);
const registrationActivation = (
registration: z.output<typeof registrationSchema>,
client: SetupClient,
parsed: ClientConfigurationDocument,
): "enabled" | "disabled" =>
(client.format !== "grok" && registration.disabled === true) ||
(client.format !== "grok" &&
!ignoresDisabledKey.has(client.name) &&
registration.disabled === true) ||
(registration.enabled === false &&
!ignoresEnabledKey.has(client.name) &&
!clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey)) ||
((parsed.dialect === "zcode" || parsed.dialect === "zcode_fallback") &&
registration.enable === false) ||
@@ -1,5 +1,5 @@
import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
import { join, relative, resolve } from "node:path";
import { dirname, join, relative, resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -312,6 +312,79 @@ describe("commandcode registration status", () => {
});
});
describe("client activation keys", () => {
it.each([
["disabled = true", "aligned"],
["enabled = false", "stale"],
] as const)(
"reads only the activation key Codex honors (%s)",
async (activation, state) => {
const home = await createTestTempDirectory("rea-codex-activation-");
await mkdir(join(home, ".codex"));
const entry = resolve("scripts/rea.mjs");
await writeFile(
join(home, ".codex/config.toml"),
`[mcp_servers.rea]\ncommand = ${JSON.stringify(process.execPath)}\nargs = [${JSON.stringify(entry)}, "mcp"]\nstartup_timeout_sec = 30\ntool_timeout_sec = 450\n${activation}\n`,
);
expect(
await readClientRegistrationStatuses(home, entry, { environment: {} }),
).toEqual([expect.objectContaining({ client: "codex", state })]);
},
);
// Each JSON client loads rea under the key it ignores and skips it under the key it honors.
it.each([
["kimi", { disabled: true }, "aligned"],
["kimi", { enabled: false }, "stale"],
["devin", { enabled: false }, "aligned"],
["devin", { disabled: true }, "stale"],
] as const)(
"reads only the activation key %s honors (%j)",
async (client, activation, state) => {
const home = await createTestTempDirectory("rea-json-activation-");
const entry = resolve("scripts/rea.mjs");
const [config, fields] =
client === "kimi"
? [
join(home, ".kimi-code/mcp.json"),
{
env: { REA_MCP_INPUT_SCHEMA_PROFILE: "compact" },
startupTimeoutMs: 30_000,
toolTimeoutMs: 450_000,
},
]
: [
join(
process.platform === "win32"
? join(home, "AppData", "Roaming", "devin")
: join(home, ".config", "devin"),
"mcp_config.json",
),
{},
];
await mkdir(dirname(config), { recursive: true });
await writeFile(
config,
JSON.stringify({
mcpServers: {
rea: {
command: process.execPath,
args: [entry, "mcp"],
...fields,
...activation,
},
},
}),
);
expect(
await readClientRegistrationStatuses(home, entry, {
environment: { XDG_CONFIG_HOME: join(home, ".config") },
}),
).toEqual([expect.objectContaining({ client, state })]);
},
);
});
describe("Node-wrapped registration policy", () => {
it.each([1, 30])(
"accepts Codex caller-selected startup timeout %d independently of launcher",