Files
reverse-engineering-anything/tests/boundary/process/clineClientVerifier.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

145 lines
6.6 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it.each(["success", "wrong-server", "cleanup-failure"] as const)(
"owns Cline verifier orchestration through %s",
async (mode) => {
const state = await createTestTempDirectory("rea-cline-verifier-");
const fixture = join(state, "owners.mjs");
const hook = join(state, "hook.mjs");
await writeFile(
fixture,
`import assert from 'node:assert/strict';
import {appendFile, mkdir, stat, writeFile} from 'node:fs/promises';
import {join} from 'node:path';
const event = async (value) => appendFile(process.env.STATE+'/events',value+'\\n');
export async function createVerifierCommands(){
const owners=[];
return {
adopt(owner){owners.push(owner);},
async exec(command,args,options={}){
const env=options.env;
assert(env,'commands must receive isolated environment');
assert.equal((await stat(env.HOME)).isDirectory(),true,'HOME must exist before any command');
assert.equal(env.USERPROFILE,env.HOME);
for(const key of ['CLINE_MCP_SETTINGS_PATH','CLINE_DATA_DIR','CLINE_DIR']) assert.equal(env[key],undefined,key+' must be absent');
if(args.includes('--version')){await event('version');return {stdout:'recorded Cline 1',stderr:''};}
if(args.includes('setup')){
assert(args.includes('cline'));assert.equal(options.cwd,env.HOME);
const path=join(env.HOME,'.cline','data','settings');await mkdir(path,{recursive:true});
await writeFile(join(path,'cline_mcp_settings.json'),JSON.stringify({mcpServers:{rea:{command:'recorded-server',args:['mcp'],env:{RECORDED:'yes'}}}}));
await event('setup');return {stdout:'{}',stderr:''};
}
assert.deepEqual(args,['--data-dir',join(env.HOME,'.cline','data'),'config','mcp']);
await event('client-list');return {stdout:'rea [stdio]',stderr:''};
},
async close(){await event('commands-close-start');for(const owner of owners)await owner.close();await event('commands-close-done');}
};
}
export async function createOwnedMcpStdioTransport(options){
assert.equal(options.command,'recorded-server');assert.deepEqual(options.arguments,['mcp']);
assert.equal(options.hostEnvironment.RECORDED,'yes');
let closing;
return {
async start(){await event('transport-start');},
async send(message){
if(message.method==='notifications/initialized'){await event('initialized');return;}
if(message.method==='initialize'||message.method==='tools/list'){
await event(message.method+'-request');
setTimeout(async()=>{
await event(message.method+'-reply');
this.onmessage?.({jsonrpc:'2.0',id:message.id,result:message.method==='initialize'?{protocolVersion:'2025-11-25',capabilities:{tools:{}},serverInfo:{name:process.env.MODE==='wrong-server'?'other':'rea',version:'1'}}:{tools:[{name:'recorded_tool',inputSchema:{type:'object'}}]}});
},15);
}
},
close(){return closing??= (async()=>{
await event('transport-close-start');await new Promise(resolve=>setTimeout(resolve,15));
if(process.env.MODE==='cleanup-failure'){await event('transport-close-failed');throw Error('recorded transport cleanup failure');}
await event('transport-close-done');this.onclose?.();
})();}
};
}
export function createVerifierRun(){return {run_id:'recorded-run'};}
export async function completeVerifierRun(run){await event('lineage');return {...run,process_lineage:{status:'recorded',descendants:[]}};}
export function execFile(){throw Error('unowned child_process execution is forbidden in this orchestration fixture');}
export function spawn(){throw Error('unowned child_process execution is forbidden in this orchestration fixture');}
`,
);
await writeFile(
hook,
`import {registerHooks} from 'node:module';import{pathToFileURL}from'node:url';
registerHooks({resolve(specifier,context,next){
if(context.parentURL?.endsWith('/scripts/verify-cline-client.mjs')&&['./lib/verify-commands.mjs','./lib/owned-mcp-stdio.mjs','./lib/verifier-run.mjs','node:child_process'].includes(specifier))return{url:pathToFileURL(process.env.STATE+'/owners.mjs').href,shortCircuit:true};
return next(specifier,context);
}});
`,
);
const script = fileURLToPath(
new URL("../../../scripts/verify-cline-client.mjs", import.meta.url),
);
const result = spawnSync(process.execPath, ["--import", hook, script], {
encoding: "utf8",
timeout: 10_000,
env: {
...process.env,
STATE: state,
MODE: mode,
REA_VERIFY_CLINE_COMMAND: "recorded-cline",
CLINE_MCP_SETTINGS_PATH: join(state, "outside-settings.json"),
CLINE_DATA_DIR: join(state, "outside-data"),
CLINE_DIR: join(state, "outside-root"),
TMPDIR: state,
TMP: state,
TEMP: state,
},
});
const events = await readFile(join(state, "events"), "utf8").catch(
() => "",
);
expect(result.error).toBeUndefined();
expect(events, result.stderr + result.stdout).toContain(
"version\nsetup\nclient-list\n",
);
expect(events).toContain("initialize-reply\n");
expect(events).toContain("tools/list-reply\n");
expect(events).toContain("commands-close-start\n");
expect(events.indexOf("tools/list-reply\n")).toBeLessThan(
events.indexOf("transport-close-start\n"),
);
if (mode === "cleanup-failure") {
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("recorded transport cleanup failure");
expect(events).toContain("transport-close-failed\n");
expect(events).not.toContain("lineage\n");
expect(result.stdout).not.toContain('"status": "verified"');
} else {
expect(events).toContain("transport-close-done\n");
expect(events).toContain("commands-close-done\n");
if (mode === "wrong-server") {
expect(result.status).not.toBe(0);
expect(result.stderr).toMatch(/initialize|identify|server|rea/u);
expect(result.stdout).not.toContain('"status": "verified"');
} else {
expect(result.status, result.stderr).toBe(0);
const report = JSON.parse(result.stdout) as {
status: string;
tool_count: number;
};
expect(report.status).toBe("verified");
expect(report.tool_count).toBe(1);
expect(events.indexOf("transport-close-done\n")).toBeLessThan(
events.indexOf("lineage\n"),
);
expect(events.indexOf("commands-close-done\n")).toBeLessThan(
events.indexOf("lineage\n"),
);
}
}
},
);