mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 17:59:08 +00:00
* fix(cli)!: own MCP client registration boundaries * fix(cli): bind registration to native connection facts * test(cli): isolate registration failure and fixture controls * test(cli): distinguish native syntax and launcher alignment * fix(cli): preserve registration selection and cancellation * fix(cli): honor Goose and Zed native settings paths * fix(cli): honor registration output controls * fix(cli): reject disabled Junie global locations
195 lines
8.6 KiB
TypeScript
195 lines
8.6 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { createTestProcessRegistry } from "../../support/process/processFixture.js";
|
|
|
|
const PROGRAM = `
|
|
import { homedir } from 'node:os';
|
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
import { access, mkdir, readFile, realpath, writeFile } from 'node:fs/promises';
|
|
const home = process.env.HOME;
|
|
if (!home || await realpath(homedir()) !== await realpath(home))
|
|
throw Error('Fixture HOME must be selected before importing registration owners.');
|
|
const insideHome = (path) => {
|
|
const selected = relative(home, resolve(path));
|
|
if (selected.startsWith('..') || isAbsolute(selected)) throw Error('Fixture path escapes HOME.');
|
|
return path;
|
|
};
|
|
for (const key of ['USERPROFILE','XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_CACHE_HOME','XDG_STATE_HOME','APPDATA','LOCALAPPDATA','GEMINI_CLI_SYSTEM_SETTINGS_PATH','GEMINI_CLI_SYSTEM_DEFAULTS_PATH','GEMINI_CLI_TRUSTED_FOLDERS_PATH'])
|
|
insideHome(process.env[key] ?? '');
|
|
process.chdir(home);
|
|
const { readClientRegistrationStatuses } = await import(${JSON.stringify(new URL("../../../src/application/ClientRegistrationStatus.ts", import.meta.url).href)});
|
|
const { currentClientRegistrationCommand } = await import(${JSON.stringify(new URL("../../../src/application/ClientRegistrationIdentity.ts", import.meta.url).href)});
|
|
const { CURRENT_ENTRYPOINT_PATH } = await import(${JSON.stringify(new URL("../../../src/process/CurrentEntrypointPath.ts", import.meta.url).href)});
|
|
const { supportedClients } = await import(${JSON.stringify(new URL("../../../src/application/SupportedClients.ts", import.meta.url).href)});
|
|
const { registerMcpClient } = await import(${JSON.stringify(new URL("../../../src/application/McpClientRegistration.ts", import.meta.url).href)});
|
|
const { mcpClientRegistrationProfile } = await import(${JSON.stringify(new URL("../../../src/application/McpRegistrationCatalog.ts", import.meta.url).href)});
|
|
const { ArtifactResourceScope } = await import(${JSON.stringify(new URL("../../../src/artifacts/ArtifactResourceScope.ts", import.meta.url).href)});
|
|
const { ConfigurationError } = await import(${JSON.stringify(new URL("../../../src/domain/configurationErrors.ts", import.meta.url).href)});
|
|
const [command, ...args] = currentClientRegistrationCommand();
|
|
if (!command || !CURRENT_ENTRYPOINT_PATH) throw Error('Missing owned fixture entrypoint.');
|
|
const gemini = supportedClients(home, process.platform, process.env).find(client => client.name === 'gemini_cli');
|
|
if (!gemini) throw Error('Missing supported Gemini profile.');
|
|
const configPath = insideHome(gemini.configPath);
|
|
await mkdir(join(configPath, '..'), { recursive: true });
|
|
const row = { command, args, env: { REA_MCP_INPUT_SCHEMA_PROFILE: 'compact' } };
|
|
const cases = [
|
|
['ignored transport', { transport: { operator: true } }],
|
|
['HTTP selected', { httpUrl: 'https://fixture.invalid/http' }],
|
|
['malformed truthy HTTP selector', { httpUrl: { operator: true } }],
|
|
['SSE selected', { url: 'https://fixture.invalid/sse' }],
|
|
['empty URL selectors', { httpUrl: '', url: '', transport: { operator: true } }],
|
|
];
|
|
const statuses = [];
|
|
for (const [label, fields] of cases) {
|
|
const original = JSON.stringify({ mcpServers: { rea: { ...row, ...fields } } });
|
|
await writeFile(configPath, original);
|
|
const status = (await readClientRegistrationStatuses(home, CURRENT_ENTRYPOINT_PATH, {
|
|
environment: process.env, selectedClientIds: ['gemini_cli'],
|
|
})).find(status => status.client === 'gemini_cli');
|
|
if (!status) throw Error('Missing Gemini status.');
|
|
statuses.push({ label, state: status.state, identity: status.command_identity,
|
|
sourceUnchanged: await readFile(configPath, 'utf8') === original });
|
|
}
|
|
const directory = insideHome(join(home, 'layered'));
|
|
await mkdir(directory);
|
|
const target = insideHome(join(directory, 'target.json'));
|
|
const higher = insideHome(join(directory, 'readonly.json'));
|
|
const source = JSON.stringify({ mcpServers: { rea: { ...row, httpUrl: 'https://fixture.invalid/selected' } } });
|
|
await writeFile(higher, source);
|
|
const client = { id: 'gemini-cli', displayName: 'Gemini CLI', format: 'json',
|
|
registrationProfile: mcpClientRegistrationProfile('gemini-cli'),
|
|
scope: 'global', configPath: target, configPaths: [target, higher] };
|
|
const exists = async path => {
|
|
try { await access(path); return true; }
|
|
catch (cause) { if (cause?.code === 'ENOENT') return false; throw cause; }
|
|
};
|
|
const resources = new ArtifactResourceScope();
|
|
try {
|
|
const conflict = await registerMcpClient(client, { command, args }, undefined, resources);
|
|
const refused = { ok: conflict.ok, typed: !conflict.ok && conflict.error instanceof ConfigurationError,
|
|
targetExists: await exists(target), backupExists: await exists(target + '.rea.backup'),
|
|
higherUnchanged: await readFile(higher, 'utf8') === source };
|
|
const harmless = JSON.stringify({ mcpServers: { rea: { ...row, operator: { preserve: true } } } });
|
|
await writeFile(higher, harmless);
|
|
const control = await registerMcpClient(client, { command, args }, undefined, resources);
|
|
console.log(JSON.stringify({ statuses, refused, control: { ok: control.ok,
|
|
status: control.ok ? control.value.status : control.error.message,
|
|
targetExists: await exists(target), higherUnchanged: await readFile(higher, 'utf8') === harmless } }));
|
|
} finally { await resources.close(); }
|
|
`;
|
|
|
|
const observed = z.object({
|
|
statuses: z.array(
|
|
z.object({
|
|
label: z.string(),
|
|
state: z.string(),
|
|
identity: z.string(),
|
|
sourceUnchanged: z.boolean(),
|
|
}),
|
|
),
|
|
refused: z.object({
|
|
ok: z.boolean(),
|
|
typed: z.boolean(),
|
|
targetExists: z.boolean(),
|
|
backupExists: z.boolean(),
|
|
higherUnchanged: z.boolean(),
|
|
}),
|
|
control: z.object({
|
|
ok: z.boolean(),
|
|
status: z.string(),
|
|
targetExists: z.boolean(),
|
|
higherUnchanged: z.boolean(),
|
|
}),
|
|
});
|
|
|
|
it("keeps owned Gemini launcher facts separate from native connection admission through status and publication", async () => {
|
|
const home = await createTestTempDirectory("rea-native-connection-");
|
|
const entrypoint = join(home, "native-connection.mjs");
|
|
await writeFile(entrypoint, PROGRAM);
|
|
const processes = createTestProcessRegistry();
|
|
try {
|
|
const child = await processes.run(
|
|
process.execPath,
|
|
["--import", "tsx", entrypoint],
|
|
{
|
|
timeoutMs: 20_000,
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
SystemRoot: process.env.SystemRoot,
|
|
WINDIR: process.env.WINDIR,
|
|
TMPDIR: process.env.TMPDIR,
|
|
TMP: process.env.TMP,
|
|
TEMP: process.env.TEMP,
|
|
HOME: home,
|
|
USERPROFILE: home,
|
|
XDG_CONFIG_HOME: join(home, "config"),
|
|
XDG_DATA_HOME: join(home, "data"),
|
|
XDG_CACHE_HOME: join(home, "cache"),
|
|
XDG_STATE_HOME: join(home, "state"),
|
|
APPDATA: join(home, "appdata"),
|
|
LOCALAPPDATA: join(home, "localappdata"),
|
|
GEMINI_CLI_SYSTEM_SETTINGS_PATH: join(home, "system-settings.json"),
|
|
GEMINI_CLI_SYSTEM_DEFAULTS_PATH: join(home, "system-defaults.json"),
|
|
GEMINI_CLI_TRUSTED_FOLDERS_PATH: join(home, "trusted-folders.json"),
|
|
GEMINI_CLI_TRUST_WORKSPACE: "true",
|
|
},
|
|
},
|
|
);
|
|
expect(child.exitCode, child.stderr).toBe(0);
|
|
const result = observed.parse(JSON.parse(child.stdout));
|
|
expect(result).toEqual({
|
|
statuses: [
|
|
{
|
|
label: "ignored transport",
|
|
state: "aligned",
|
|
identity: "owned",
|
|
sourceUnchanged: true,
|
|
},
|
|
{
|
|
label: "HTTP selected",
|
|
state: "unknown",
|
|
identity: "owned",
|
|
sourceUnchanged: true,
|
|
},
|
|
{
|
|
label: "malformed truthy HTTP selector",
|
|
state: "unknown",
|
|
identity: "owned",
|
|
sourceUnchanged: true,
|
|
},
|
|
{
|
|
label: "SSE selected",
|
|
state: "unknown",
|
|
identity: "owned",
|
|
sourceUnchanged: true,
|
|
},
|
|
{
|
|
label: "empty URL selectors",
|
|
state: "aligned",
|
|
identity: "owned",
|
|
sourceUnchanged: true,
|
|
},
|
|
],
|
|
refused: {
|
|
ok: false,
|
|
typed: true,
|
|
targetExists: false,
|
|
backupExists: false,
|
|
higherUnchanged: true,
|
|
},
|
|
control: {
|
|
ok: true,
|
|
status: "configured",
|
|
targetExists: true,
|
|
higherUnchanged: true,
|
|
},
|
|
});
|
|
} finally {
|
|
await processes.shutdown();
|
|
}
|
|
});
|