Files
reverse-engineering-anything/tests/acceptance/setup/clineClientSetup.test.ts
T
Luqman Bramantyo Rahmadiandmorluto dae1551403 feat(setup): support Cline as a client (#10128)
* feat(setup): support Cline as a client

* test(setup): cover Cline doctor and uninstall ownership

* docs(setup): document Cline support

* test(setup): verify Cline against an installed client

* test(setup): include Cline in the exhaustive client detection list

* docs(setup): note that a hand-written rea entry blocks Cline setup

* fix(setup): isolate Cline verification and await MCP discovery

* fix(verification): own and isolate Cline discovery

* fix(setup): preserve native Cline transport settings

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-11 17:42:53 -05:00

324 lines
9.9 KiB
TypeScript

import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
cliTest,
type CliResult,
type TestCli,
} from "../../support/cli/cliFixture.js";
cliTest(
"registers Cline at its documented path and preserves peers and backups",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-cline-home-");
const config = join(
home,
".cline",
"data",
"settings",
"cline_mcp_settings.json",
);
const original = JSON.stringify({
mcpServers: { peer: { command: "peer" } },
});
await mkdir(join(home, ".cline", "data", "settings"), { recursive: true });
await writeFile(config, original);
const run = () =>
cli.run({
arguments: [
"setup",
"--client",
"cline",
"--skill=false",
"--yes",
"--json",
],
cwd: home,
environment: {
HOME: home,
USERPROFILE: home,
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(home, "missing-ghidra"),
JAVA_HOME: join(home, "missing-java"),
},
});
const first = await run();
expect(first.exitCode, first.stdout).toBe(0);
const configured = await readFile(config, "utf8");
expect(JSON.parse(configured)).toMatchObject({
mcpServers: {
peer: { command: "peer" },
rea: { args: expect.arrayContaining(["mcp"]) },
},
});
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
const second = await run();
expect(second.exitCode, second.stdout).toBe(0);
expect(await readFile(config, "utf8")).toBe(configured);
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
},
);
cliTest(
"honors CLINE_MCP_SETTINGS_PATH and leaves the default file absent",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-cline-override-");
const override = join(home, "custom", "settings.json");
const defaultPath = join(
home,
".cline",
"data",
"settings",
"cline_mcp_settings.json",
);
const result = await cli.run({
arguments: [
"setup",
"--client",
"cline",
"--skill=false",
"--yes",
"--json",
],
cwd: home,
environment: {
HOME: home,
USERPROFILE: home,
CLINE_MCP_SETTINGS_PATH: override,
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(home, "missing-ghidra"),
JAVA_HOME: join(home, "missing-java"),
},
});
expect(result.exitCode, result.stdout).toBe(0);
expect(JSON.parse(await readFile(override, "utf8"))).toMatchObject({
mcpServers: { rea: { args: expect.arrayContaining(["mcp"]) } },
});
await expect(access(defaultPath)).rejects.toMatchObject({
code: "ENOENT",
});
},
);
cliTest(
"resolves the Cline skill client to the shared skills directory",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-cline-skill-");
const result = await cli.run({
arguments: [
"setup",
"--skill-client",
"cline",
"--skill",
"--dry-run",
"--json",
],
cwd: home,
environment: {
HOME: home,
USERPROFILE: home,
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(home, "missing-ghidra"),
JAVA_HOME: join(home, "missing-java"),
},
});
expect(result.exitCode, result.stdout).toBe(0);
const plan = result.json as {
status: string;
plannedActions: { id: string; target: string }[];
};
const skill = plan.plannedActions.filter(
({ id }) => id === "install_skill",
);
expect(skill).toHaveLength(1);
expect(skill[0]!.target).toBe(
join(home, ".agents", "skills", "reverse-engineer-anything"),
);
},
);
const clineEnvironment = (home: string) => ({
HOME: home,
USERPROFILE: home,
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(home, "missing-ghidra"),
JAVA_HOME: join(home, "missing-java"),
});
const setupCline = (cli: TestCli, home: string): Promise<CliResult> =>
cli.run({
arguments: [
"setup",
"--client",
"cline",
"--skill=false",
"--yes",
"--json",
],
cwd: home,
environment: clineEnvironment(home),
});
cliTest(
"reports the Cline registration as aligned and REA-owned",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-cline-doctor-");
expect((await setupCline(cli, home)).exitCode).toBe(0);
const doctor = await cli.run({
arguments: ["doctor", "--client", "cline", "--json"],
cwd: home,
environment: clineEnvironment(home),
});
const report = doctor.json as {
identity: {
registrations: {
client: string;
state: string;
command_identity: string;
}[];
};
};
const registration = report.identity.registrations.find(
({ client }) => client === "cline",
);
expect(registration?.state).toBe("aligned");
expect(registration?.command_identity).toBe("owned");
},
);
cliTest(
"refuses a foreign rea registration in the Cline file",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-cline-foreign-");
const directory = join(home, ".cline", "data", "settings");
const config = join(directory, "cline_mcp_settings.json");
const foreign = JSON.stringify({
mcpServers: { rea: { command: "custom", args: ["keep"] } },
});
await mkdir(directory, { recursive: true });
await writeFile(config, foreign);
const result = await setupCline(cli, home);
expect(result.exitCode).toBe(1);
const plan = result.json as { status: string; remediation: string | null };
expect(plan.status).toBe("needs_human");
expect(plan.remediation).toContain("foreign ownership");
expect(await readFile(config, "utf8")).toBe(foreign);
},
);
cliTest(
"uninstall removes only the REA entry from the Cline file",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-cline-uninstall-");
const directory = join(home, ".cline", "data", "settings");
const config = join(directory, "cline_mcp_settings.json");
await mkdir(directory, { recursive: true });
await writeFile(
config,
JSON.stringify({ mcpServers: { peer: { command: "peer" } } }),
);
expect((await setupCline(cli, home)).exitCode).toBe(0);
const removal = await cli.run({
arguments: ["uninstall", "--json"],
cwd: home,
environment: clineEnvironment(home),
});
expect(removal.exitCode, removal.stdout).toBe(0);
const report = removal.json as {
items: { name: string; status: string }[];
};
expect(report.items.find(({ name }) => name === "cline")?.status).toBe(
"removed",
);
const remaining = JSON.parse(await readFile(config, "utf8"));
expect(remaining.mcpServers).toEqual({ peer: { command: "peer" } });
},
);
cliTest(
"refuses a malformed Cline configuration without rewriting it",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-cline-malformed-");
const directory = join(home, ".cline", "data", "settings");
const config = join(directory, "cline_mcp_settings.json");
const malformed = '{"mcpServers": {';
await mkdir(directory, { recursive: true });
await writeFile(config, malformed);
const result = await setupCline(cli, home);
expect(result.exitCode).toBe(1);
expect(await readFile(config, "utf8")).toBe(malformed);
},
);
cliTest(
"refreshes an owned nested Cline transport without losing its settings",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-cline-nested-");
const config = join(
home,
".cline",
"data",
"settings",
"cline_mcp_settings.json",
);
expect((await setupCline(cli, home)).exitCode).toBe(0);
const configured = JSON.parse(await readFile(config, "utf8"));
const original = configured.mcpServers.rea;
const transport = {
type: "stdio",
command: original.command,
args: original.args,
env: { ...original.env, CLINE_NESTED_VALUE: "caller selected" },
cwd: home,
metadata: { selected: "nested transport" },
};
const nested = {
...configured,
mcpServers: {
...configured.mcpServers,
peer: { command: "recorded-peer" },
rea: {
transport,
env: { SHADOWED_OUTER_VALUE: "retained but not selected" },
metadata: { selected: "outer registration" },
},
},
};
await writeFile(config, JSON.stringify(nested));
const refreshed = await setupCline(cli, home);
expect(refreshed.exitCode, refreshed.stdout).toBe(0);
const after = await readFile(config, "utf8");
expect(JSON.parse(after)).toEqual(nested);
const doctor = await cli.run({
arguments: ["doctor", "--client", "cline", "--json"],
cwd: home,
environment: clineEnvironment(home),
});
expect(doctor.json).toMatchObject({
identity: {
registrations: [
expect.objectContaining({
client: "cline",
state: "aligned",
command_identity: "owned",
}),
],
},
});
expect((await setupCline(cli, home)).exitCode).toBe(0);
expect(await readFile(config, "utf8")).toBe(after);
const removal = await cli.run({
arguments: ["uninstall", "--json"],
cwd: home,
environment: clineEnvironment(home),
});
expect(removal.exitCode, removal.stdout).toBe(0);
expect(JSON.parse(await readFile(config, "utf8"))).toEqual({
...nested,
mcpServers: { peer: { command: "recorded-peer" } },
});
},
);