Files
reverse-engineering-anything/src/application/ClientRegistration.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

287 lines
8.6 KiB
TypeScript

import { expect, it } from "vitest";
import {
parseClientRegistrationPolicy,
parseClientRegistrationLauncher,
replaceClientRegistrationLauncher,
} from "./ClientRegistration.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import {
parseClientConfiguration,
clientConfigurationRecordSchema,
} from "./ClientConfigurationDocument.js";
import {
supportedClientRegistrationProfile,
type SetupClient,
} from "./SupportedClients.js";
import { mcpClientRegistrationProfile } from "./McpRegistrationCatalog.js";
import { admitClientStdioConnection } from "./ClientRegistrationConnection.js";
import type { ClientRegistrationTarget } from "./ClientRegistrationProfile.js";
const zedTarget: ClientRegistrationTarget = {
dialect: "zed",
profile: mcpClientRegistrationProfile("zed"),
};
const gooseTarget: ClientRegistrationTarget = {
dialect: "goose",
profile: mcpClientRegistrationProfile("goose"),
};
const cursorTarget: ClientRegistrationTarget = {
dialect: "json",
profile: mcpClientRegistrationProfile("cursor"),
};
const client: SetupClient = {
name: "opencode",
registrationProfile: supportedClientRegistrationProfile("opencode"),
format: "opencode",
configPath: "/fixture/opencode.json",
};
it("preserves every OpenCode environment key while normalizing its launcher", () => {
const input = JSON.parse(
'{"type":"local","command":["rea","mcp"],"environment":{"__proto__":"operator","constructor":"selected","normal":"value"}}',
);
const result = parseClientRegistrationPolicy(input, client);
if (!result.ok) throw result.error;
expect(result.value.command).toBe("rea");
expect(result.value.args).toEqual(["mcp"]);
expect(result.value.environment).toEqual(input.environment);
const environment = result.value.environment;
if (typeof environment !== "object" || environment === null)
throw new Error("Missing parsed environment");
expect(Object.hasOwn(environment, "__proto__")).toBe(true);
});
it.each([42, null, {}])(
"rejects malformed OpenCode environment values at __proto__: %j",
(value) => {
const environment = Object.fromEntries([["__proto__", value]]);
const result = parseClientRegistrationPolicy(
{ type: "local", command: ["rea", "mcp"], environment },
client,
);
expect(result.ok).toBe(false);
},
);
it("preserves Zed source metadata while replacing its native launcher", () => {
const previous = {
source: "operator-metadata",
command: "old",
args: ["old"],
env: { KEEP: "1" },
custom: { retained: true },
};
const updated = replaceClientRegistrationLauncher(
zedTarget,
["fixture", ""],
previous,
);
if (!updated.ok) throw updated.error;
expect(updated.value).toEqual({
...previous,
command: "fixture",
args: [""],
});
const fresh = replaceClientRegistrationLauncher(
zedTarget,
["fixture"],
undefined,
);
if (!fresh.ok) throw fresh.error;
expect(fresh.value).not.toHaveProperty("source");
});
it("normalizes Goose's single env alias without emitting duplicate native fields", () => {
const env = JSON.parse(
'{"KEEP":"1","__proto__":"selected","constructor":"owned"}',
);
const previous = {
cmd: "old",
args: [],
env,
enabled: false,
custom: { retained: true },
};
const before = structuredClone(previous);
const result = replaceClientRegistrationLauncher(
gooseTarget,
["fixture", ""],
previous,
);
if (!result.ok) throw result.error;
expect(result.value.envs).toEqual(env);
expect(result.value).not.toHaveProperty("env");
const canonical = clientConfigurationRecordSchema.parse(result.value.envs);
expect(Object.hasOwn(canonical, "__proto__")).toBe(true);
expect(result.value).toMatchObject({
enabled: false,
custom: { retained: true },
});
expect(previous).toEqual(before);
});
it.each([{ envs: { KEEP: "1" } }, {}])(
"keeps canonical Goose environment data stable: %j",
(previous) => {
const result = replaceClientRegistrationLauncher(
gooseTarget,
["fixture"],
previous,
);
if (!result.ok) throw result.error;
expect(result.value.envs).toEqual("envs" in previous ? previous.envs : {});
expect(result.value).not.toHaveProperty("env");
},
);
it("keeps a native Goose leading-zero string when normalizing the env alias", () => {
const parsed = parseClientConfiguration(
"extensions:\n rea:\n cmd: old\n env:\n VALUE: 0123\n",
"goose",
);
const result = replaceClientRegistrationLauncher(
gooseTarget,
["fixture"],
parsed.servers.rea,
);
if (!result.ok) throw result.error;
expect(result.value.envs).toEqual({ VALUE: "0123" });
expect(result.value).not.toHaveProperty("env");
});
it("normalizes an explicitly empty Goose env alias to one canonical field", () => {
const result = replaceClientRegistrationLauncher(gooseTarget, ["fixture"], {
env: {},
});
if (!result.ok) throw result.error;
expect(result.value.envs).toEqual({});
expect(result.value).not.toHaveProperty("env");
});
it.each([
{ env: { KEEP: "1" }, envs: { KEEP: "1" } },
{ env: { KEEP: "old" }, envs: { KEEP: "new" } },
])(
"refuses both Goose environment aliases even when their contents agree: %j",
(previous) => {
const result = replaceClientRegistrationLauncher(
gooseTarget,
["fixture"],
previous,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Ambiguous Goose aliases were admitted");
expect(result.error).toBeInstanceOf(ConfigurationError);
expect(result.error.message).toContain("envs or its env alias, not both");
expect(result.error.settings).toEqual([
{
setting: "envs",
constraint: "env and envs declare the same native field",
},
]);
},
);
it.each([null, 42, ["value"], { VALUE: 123 }])(
"refuses a malformed Goose env alias: %j",
(env) => {
const result = replaceClientRegistrationLauncher(gooseTarget, ["fixture"], {
env,
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Malformed Goose alias was admitted");
expect(result.error).toBeInstanceOf(ConfigurationError);
expect(result.error.settings).toEqual([
{
setting: "env",
constraint: "Expected a mapping of string environment values",
},
]);
},
);
it("keeps other clients' env field in its own dialect", () => {
const previous = { env: { KEEP: "1" }, command: "old" };
const result = replaceClientRegistrationLauncher(
cursorTarget,
["fixture"],
previous,
);
if (!result.ok) throw result.error;
expect(result.value.env).toEqual({ KEEP: "1" });
expect(result.value).not.toHaveProperty("envs");
});
it.each([
{
target: {
dialect: "opencode",
profile: mcpClientRegistrationProfile("opencode"),
},
value: {
type: "remote",
url: "https://fixture.invalid",
command: ["/fixture/rea", "--mcp"],
},
},
{
target: { dialect: "json", profile: mcpClientRegistrationProfile("cline") },
value: {
transport: {
type: "streamableHttp",
url: "https://fixture.invalid",
command: "/fixture/rea",
args: ["--mcp"],
},
},
},
] satisfies readonly { target: ClientRegistrationTarget; value: unknown }[])(
"retains launcher syntax independently of a non-stdio native transport: $target.profile.nativeClient",
({ target, value }) => {
expect(parseClientRegistrationLauncher(value, target)).toEqual({
ok: true,
value: { command: "/fixture/rea", args: ["--mcp"] },
});
expect(admitClientStdioConnection(value, target).ok).toBe(false);
},
);
it("retains malformed stored argv syntax for ownership while refusing launch readiness", () => {
const value = {
command: "/fixture/rea",
args: ["--mcp", "recorded\0argument"],
};
expect(parseClientRegistrationLauncher(value, cursorTarget)).toEqual({
ok: true,
value,
});
expect(admitClientStdioConnection(value, cursorTarget)).toMatchObject({
ok: false,
error: { kind: "invalid", cause: { _tag: "ConfigurationError" } },
});
});
it("reads policy from the selected nested Cline launcher", () => {
const selected: SetupClient = {
name: "cline",
registrationProfile: supportedClientRegistrationProfile("cline"),
format: "json",
configPath: "/fixture/cline.json",
};
const parsed = parseClientRegistrationPolicy(
{
transport: { type: "stdio", command: "selected", args: ["mcp"] },
command: "shadowed",
args: ["ignored"],
disabled: true,
},
selected,
);
if (!parsed.ok) throw parsed.error;
expect(parsed.value.command).toBe("selected");
expect(parsed.value.args).toEqual(["mcp"]);
expect(parsed.value.disabled).toBe(true);
});