Files
rea/tests/boundary/filesystem/clientRegistrationStatus.test.ts
Kaoru0822 8b4c359135 feat(setup): register OMP during setup (#1301)
* feat(setup): register OMP during setup

Add OMP (oh-my-pi) as a setup, doctor and uninstall client. Setup writes
a type: stdio entry to the user-level agent mcp.json that OMP reads,
following PI_CONFIG_DIR, an absolute PI_CODING_AGENT_DIR and the
OMP_PROFILE/PI_PROFILE named profile. It removes rea from OMP's
disabledServers denylist, which would otherwise hide the registration,
and doctor reports a listed registration as stale.

Closes #1257

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(setup): honor OMP's enabledServers allowlist in doctor

OMP's user enabledServers allowlist forces an entry with enabled: false
on, unless disabledServers also lists it. Doctor reported such a working
registration as stale. Also format the FAQ client list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(setup): keep OMP's allowlisted entry in update maintenance

rea update skipped an owned OMP entry with enabled: false even when
OMP's enabledServers allowlist runs it. Apply the same forced-enable rule
as doctor. Share the client-location environment reset across the client
tests instead of repeating it in each file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:51:12 -07:00

302 lines
9.5 KiB
TypeScript

import { mkdir, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js";
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
beforeEach(clearClientLocationEnvironment);
afterEach(() => {
vi.unstubAllEnvs();
});
describe("client registration status", () => {
it("discovers Claude Code from its config file without a marker directory", async () => {
const home = await createTestTempDirectory("rea-claude-config-only-");
await writeFile(
join(home, ".claude.json"),
JSON.stringify({
mcpServers: {
rea: {
command: "npx",
args: ["-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"],
},
},
}),
);
const statuses = await readClientRegistrationStatuses(home, "/current/rea");
expect(statuses).toEqual([
expect.objectContaining({
client: "claude_code",
config_path: join(home, ".claude.json"),
state: "aligned",
remediation: null,
}),
]);
});
it("distinguishes aligned, stale, missing, and invalid registrations", async () => {
const home = await createTestTempDirectory("rea-registrations-");
await Promise.all([
mkdir(join(home, ".codex")),
mkdir(join(home, ".cursor")),
mkdir(join(home, ".gemini")),
mkdir(join(home, ".claude")),
]);
await writeFile(
join(home, ".codex/config.toml"),
`[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "${PRODUCT_IDENTITY.registrationPackageSpecifier}", "mcp"]\nstartup_timeout_sec = 30\n`,
);
await writeFile(
join(home, ".cursor/mcp.json"),
JSON.stringify({
mcpServers: {
rea: { command: "/old/rea", args: ["mcp"], env: { SECRET: "x" } },
},
}),
);
await writeFile(join(home, ".gemini/settings.json"), "not-json");
const statuses = await readClientRegistrationStatuses(home, "/current/rea");
expect(statuses).toEqual(
expect.arrayContaining([
expect.objectContaining({
client: "codex",
state: "aligned",
remediation: null,
}),
expect.objectContaining({
client: "cursor",
state: "stale",
command: ["/old/rea", "mcp"],
remediation: expect.any(String),
}),
expect.objectContaining({
client: "gemini_cli",
state: "invalid",
command: [],
remediation: expect.any(String),
}),
expect.objectContaining({
client: "claude_code",
state: "missing",
command: [],
remediation: expect.any(String),
}),
]),
);
expect(JSON.stringify(statuses)).not.toContain("SECRET");
});
it.each([null, [], "invalid"])(
"reports a malformed server table %j as invalid",
async (servers) => {
const home = await createTestTempDirectory("rea-registration-shape-");
await mkdir(join(home, ".cursor"));
await writeFile(
join(home, ".cursor/mcp.json"),
JSON.stringify({ mcpServers: servers }),
);
expect(await readClientRegistrationStatuses(home)).toEqual([
expect.objectContaining({
client: "cursor",
state: "invalid",
command: [],
}),
]);
},
);
it("reports an unversioned npx registration as stale", async () => {
const home = await createTestTempDirectory("rea-registrations-");
await mkdir(join(home, ".codex"));
await writeFile(
join(home, ".codex/config.toml"),
'[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents", "mcp"]\n',
);
const statuses = await readClientRegistrationStatuses(home);
expect(statuses).toEqual([
expect.objectContaining({
client: "codex",
command: ["npx", "-y", "rea-agents", "mcp"],
state: "stale",
}),
]);
});
});
describe("commandcode registration status", () => {
const writeCommandcodeRegistration = async (
home: string,
registration: Record<string, unknown>,
): Promise<void> => {
await mkdir(join(home, ".commandcode"));
await writeFile(
join(home, ".commandcode/mcp.json"),
JSON.stringify({ mcpServers: { rea: registration } }),
);
};
const ownedArgs = (): readonly string[] => [
"-y",
PRODUCT_IDENTITY.registrationPackageSpecifier,
"mcp",
];
it.each([undefined, "sse"])(
"reports transport %j as invalid",
async (transport) => {
const home = await createTestTempDirectory("rea-commandcode-transport-");
await writeCommandcodeRegistration(home, {
...(transport === undefined ? {} : { transport }),
command: "npx",
args: [...ownedArgs()],
});
expect(await readClientRegistrationStatuses(home)).toEqual([
expect.objectContaining({
client: "commandcode",
state: "invalid",
command: [],
}),
]);
},
);
it("reports a disabled registration as stale", async () => {
const home = await createTestTempDirectory("rea-commandcode-disabled-");
await writeCommandcodeRegistration(home, {
transport: "stdio",
enabled: false,
command: "npx",
args: [...ownedArgs()],
});
expect(await readClientRegistrationStatuses(home)).toEqual([
expect.objectContaining({
client: "commandcode",
state: "stale",
}),
]);
});
});
describe("Node-wrapped registration policy", () => {
it.each([1, 30])(
"checks Codex startup timeout %d independently of launcher",
async (timeout) => {
const home = await createTestTempDirectory("rea-node-registration-");
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 = ${String(timeout)}\n`,
);
const statuses = await readClientRegistrationStatuses(home, entry, {
environment: {},
});
expect(statuses).toEqual([
expect.objectContaining({
client: "codex",
state: timeout === 30 ? "aligned" : "stale",
}),
]);
},
);
it.each([1, 30])(
"checks Grok Build startup timeout %d independently of launcher",
async (timeout) => {
const home = await createTestTempDirectory("rea-grok-registration-");
await mkdir(join(home, ".grok"));
const entry = resolve("scripts/rea.mjs");
await writeFile(
join(home, ".grok/config.toml"),
`[mcp_servers.rea]\ncommand = ${JSON.stringify(process.execPath)}\nargs = [${JSON.stringify(entry)}, "mcp"]\nstartup_timeout_sec = ${String(timeout)}\n`,
);
const statuses = await readClientRegistrationStatuses(home, entry, {
environment: {},
});
expect(statuses).toEqual([
expect.objectContaining({
client: "grok_build",
state: timeout === 30 ? "aligned" : "stale",
}),
]);
},
);
it("reports Grok Build stale when rea is listed in disabled_mcp_servers", async () => {
const home = await createTestTempDirectory("rea-grok-disabled-status-");
await mkdir(join(home, ".grok"));
const entry = resolve("scripts/rea.mjs");
await writeFile(
join(home, ".grok/config.toml"),
`disabled_mcp_servers = ["rea"]\n[mcp_servers.rea]\ncommand = ${JSON.stringify(process.execPath)}\nargs = [${JSON.stringify(entry)}, "mcp"]\nstartup_timeout_sec = 30\n`,
);
expect(
await readClientRegistrationStatuses(home, entry, { environment: {} }),
).toEqual([
expect.objectContaining({ client: "grok_build", state: "stale" }),
]);
});
it("checks a direct-launcher Codex startup timeout without Node wrapping", async () => {
const home = await createTestTempDirectory("rea-direct-registration-");
await mkdir(join(home, ".codex"));
await writeFile(
join(home, ".codex/config.toml"),
'[mcp_servers.rea]\ncommand = "/current/rea"\nargs = ["mcp"]\nstartup_timeout_sec = 1\n',
);
const statuses = await readClientRegistrationStatuses(
home,
"/current/rea",
{
environment: {},
},
);
expect(statuses).toEqual([
expect.objectContaining({ client: "codex", state: "stale" }),
]);
});
it.each([{ tools: ["binary_session"] }, { tools: ["*"] }])(
"checks Copilot tool selection $tools independently of launcher",
async ({ tools }) => {
const home = await createTestTempDirectory("rea-node-copilot-");
await mkdir(join(home, ".copilot"));
const entry = resolve("scripts/rea.mjs");
await writeFile(
join(home, ".copilot/mcp-config.json"),
JSON.stringify({
mcpServers: {
rea: {
type: "stdio",
command: process.execPath,
args: [entry, "mcp"],
tools,
},
},
}),
);
const statuses = await readClientRegistrationStatuses(home, entry, {
environment: {},
});
expect(statuses).toEqual([
expect.objectContaining({
client: "copilot_cli",
state: tools.includes("*") ? "aligned" : "stale",
}),
]);
},
);
});