import { access, mkdir, readFile, writeFile, rm } from "node:fs/promises"; import { dirname, join } from "node:path"; import { expect } from "vitest"; import { parse } from "jsonc-parser"; import { PRODUCT_IDENTITY } from "../../../src/identity.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; for (const policy of [{ excluded: ["rea"] }, { allowed: ["other"] }]) { cliTest( `Gemini refuses a registration blocked by ${Object.keys(policy)[0]}`, async ({ cli }) => { const home = await createTestTempDirectory("rea-gemini-policy-"); const config = join(home, ".gemini/settings.json"); const original = JSON.stringify({ mcp: policy, mcpServers: { rea: { command: "npx", args: ["-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"], }, }, }); await mkdir(dirname(config), { recursive: true }); await writeFile(config, original); const run = (arguments_: readonly string[]) => cli.run({ arguments: arguments_, cwd: home, environment: { HOME: home, USERPROFILE: home }, timeoutMs: 20_000, }); const setup = await run([ "setup", "--client", "gemini_cli", "--yes", "--json", ]); expect(setup.exitCode).toBe(1); expect(setup.json).toMatchObject({ status: "needs_human", plannedActions: [], appliedActions: [], remediation: expect.stringContaining("mcp." + Object.keys(policy)[0]), }); expect(await readFile(config, "utf8")).toBe(original); await expect(access(config + ".rea.backup")).rejects.toMatchObject({ code: "ENOENT", }); const doctor = await run(["doctor", "--client", "gemini_cli", "--json"]); expect(doctor.exitCode).toBe(1); expect(doctor.json).toMatchObject({ healthy: false, identity: { registrations: expect.arrayContaining([ expect.objectContaining({ client: "gemini_cli", state: "stale", remediation: expect.stringContaining("mcp."), }), ]), }, }); }, ); } cliTest( "OpenCode diagnoses merged JSON/JSONC and removes owned entries from both files", async ({ cli }) => { const home = await createTestTempDirectory("rea-opencode-layers-"); const directory = join(home, ".config/opencode"); await mkdir(directory, { recursive: true }); const lower = join(directory, "opencode.json"); const higher = join(directory, "opencode.jsonc"); const originalLower = JSON.stringify({ mcp: { rea: { type: "local", command: [ "npx", "-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp", ], enabled: true, }, other: { type: "local", command: ["other"] }, }, }); const originalHigher = '// keep this comment\n{"mcp":{"rea":{"enabled":false}},"theme":"system"}\n'; await writeFile(lower, originalLower); await writeFile(higher, originalHigher); const run = (arguments_: readonly string[]) => cli.run({ arguments: arguments_, cwd: home, environment: { HOME: home, USERPROFILE: home }, timeoutMs: 20_000, }); const before = await run(["doctor", "--client", "opencode", "--json"]); expect(before.exitCode).toBe(1); expect(before.json).toMatchObject({ identity: { registrations: expect.arrayContaining([ expect.objectContaining({ client: "opencode", state: "stale" }), ]), }, }); const setup = await run([ "setup", "--client", "opencode", "--yes", "--json", ]); expect(setup.exitCode).toBe(0); expect(setup.json).toMatchObject({ status: "ready" }); expect(await readFile(lower, "utf8")).toBe(originalLower); expect(await readFile(higher + ".rea.backup", "utf8")).toBe(originalHigher); expect(await readFile(higher, "utf8")).toContain("// keep this comment"); expect( (await run(["doctor", "--client", "opencode", "--json"])).exitCode, ).toBe(0); const uninstall = await run(["uninstall", "--json"]); expect(uninstall.exitCode).toBe(0); const report = uninstall.json as { items?: readonly { name: string; status: string }[]; }; const opencodeItems = report.items?.filter( ({ name }) => name === "opencode", ); expect(opencodeItems).toEqual([ expect.objectContaining({ status: "removed" }), ]); expect(parse(await readFile(lower, "utf8"))).toEqual({ mcp: { other: { type: "local", command: ["other"] } }, }); expect(parse(await readFile(higher, "utf8"))).toEqual({ mcp: {}, theme: "system", }); }, ); cliTest( "malformed lower-priority OpenCode config blocks setup before any writes", async ({ cli }) => { const home = await createTestTempDirectory("rea-opencode-invalid-layer-"); const directory = join(home, ".config/opencode"); await mkdir(directory, { recursive: true }); const lower = join(directory, "opencode.json"); const higher = join(directory, "opencode.jsonc"); await writeFile(lower, "malformed"); await writeFile(higher, "{}"); const result = await cli.run({ arguments: ["setup", "--client", "opencode", "--yes", "--json"], cwd: home, environment: { HOME: home, USERPROFILE: home }, }); expect(result.exitCode).toBe(1); expect(result.json).toMatchObject({ status: "needs_human", plannedActions: [], appliedActions: [], }); expect(await readFile(lower, "utf8")).toBe("malformed"); expect(await readFile(higher, "utf8")).toBe("{}"); await expect(access(higher + ".rea.backup")).rejects.toMatchObject({ code: "ENOENT", }); }, ); cliTest( "Gemini system policy blocks a new registration without modifying managed files", async ({ cli }) => { const home = await createTestTempDirectory("rea-gemini-managed-"); const policy = join(home, "managed.json"); const original = JSON.stringify({ mcp: { excluded: ["rea"] }, unrelated: true, }); await writeFile(policy, original); const environment = { HOME: home, USERPROFILE: home, GEMINI_CLI_SYSTEM_SETTINGS_PATH: policy, GEMINI_CLI_SYSTEM_DEFAULTS_PATH: join(home, "defaults.json"), }; const setup = await cli.run({ arguments: ["setup", "--client", "gemini_cli", "--yes", "--json"], cwd: home, environment, }); expect(setup.exitCode).toBe(1); expect(setup.json).toMatchObject({ status: "needs_human", plannedActions: [], appliedActions: [], remediation: expect.stringContaining("mcp.excluded"), }); expect(await readFile(policy, "utf8")).toBe(original); await expect( access(join(home, ".gemini/settings.json")), ).rejects.toMatchObject({ code: "ENOENT" }); await expect(access(policy + ".rea.backup")).rejects.toMatchObject({ code: "ENOENT", }); }, ); cliTest( "Gemini intersects system/default/user allowlists and accepts an empty effective allowlist", async ({ cli }) => { const home = await createTestTempDirectory("rea-gemini-allowlists-"); const system = join(home, "system.json"); const defaults = join(home, "defaults.json"); const user = join(home, ".gemini/settings.json"); await mkdir(dirname(user), { recursive: true }); await writeFile( system, JSON.stringify({ mcp: { allowed: ["rea", "other"] } }), ); await writeFile(defaults, JSON.stringify({ mcp: { allowed: ["other"] } })); await writeFile( user, JSON.stringify({ mcp: { allowed: ["rea", "other"] } }), ); const environment = { HOME: home, USERPROFILE: home, GEMINI_CLI_SYSTEM_SETTINGS_PATH: system, GEMINI_CLI_SYSTEM_DEFAULTS_PATH: defaults, }; const run = () => cli.run({ arguments: ["setup", "--client", "gemini_cli", "--yes", "--json"], cwd: home, environment, }); const blocked = await run(); expect(blocked.exitCode).toBe(1); expect(blocked.json).toMatchObject({ status: "needs_human", appliedActions: [], }); await writeFile(defaults, JSON.stringify({ mcp: { allowed: ["rea"] } })); const allowed = await run(); expect(allowed.exitCode).toBe(0); expect(allowed.json).toMatchObject({ status: "ready" }); expect(JSON.parse(await readFile(defaults, "utf8"))).toEqual({ mcp: { allowed: ["rea"] }, }); // The consumer treats an empty consolidated allowlist as unrestricted. await writeFile(defaults, JSON.stringify({ mcp: { allowed: [] } })); const empty = await run(); expect(empty.exitCode).toBe(0); expect(empty.json).toMatchObject({ status: "ready", appliedActions: [] }); }, ); cliTest( "Gemini matches allowlist spellings independently of their policy order", async ({ cli }) => { const home = await createTestTempDirectory("rea-gemini-case-"); const system = join(home, "system.json"); const defaults = join(home, "defaults.json"); await writeFile(system, JSON.stringify({ mcp: { allowed: ["REA"] } })); const environment = { HOME: home, USERPROFILE: home, GEMINI_CLI_SYSTEM_SETTINGS_PATH: system, GEMINI_CLI_SYSTEM_DEFAULTS_PATH: defaults, }; const run = () => cli.run({ arguments: ["setup", "--client", "gemini_cli", "--yes", "--json"], cwd: home, environment, }); // The intersection compares names case-insensitively, so the spelling that // survives must not decide the outcome: the same pair of policies in // either arrangement has to consolidate to the same decision. for (const [systemSpelling, defaultSpelling] of [ ["REA", "rea"], ["rea", "REA"], ]) { await writeFile( system, JSON.stringify({ mcp: { allowed: [systemSpelling] } }), ); await writeFile( defaults, JSON.stringify({ mcp: { allowed: [defaultSpelling] } }), ); const result = await run(); expect(result.exitCode, `${systemSpelling}+${defaultSpelling}`).toBe(0); expect(result.json, `${systemSpelling}+${defaultSpelling}`).toMatchObject( { status: "ready", }, ); } }, ); cliTest( "Gemini compares exclusions and padded allowlist entries through one normalization", async ({ cli }) => { const home = await createTestTempDirectory("rea-gemini-normalized-"); const system = join(home, "system.json"); const defaults = join(home, "defaults.json"); const environment = { HOME: home, USERPROFILE: home, GEMINI_CLI_SYSTEM_SETTINGS_PATH: system, GEMINI_CLI_SYSTEM_DEFAULTS_PATH: defaults, }; const run = () => cli.run({ arguments: ["setup", "--client", "gemini_cli", "--yes", "--json"], cwd: home, environment, }); await writeFile(defaults, JSON.stringify({ mcp: {} })); // An exclusion is a membership test over the same names the intersection // compares, so a differently spelled and padded entry blocks setup rather // than slipping past the raw comparison. await writeFile(system, JSON.stringify({ mcp: { excluded: [" REA "] } })); const blocked = await run(); expect(blocked.exitCode).toBe(1); expect(blocked.json).toMatchObject({ status: "needs_human", plannedActions: [], appliedActions: [], remediation: expect.stringContaining("mcp.excluded"), }); // Padding is presentation, not identity: a padded allowlist entry permits // the same server its exact spelling permits. await writeFile(system, JSON.stringify({ mcp: { allowed: [" REA "] } })); const allowed = await run(); expect(allowed.exitCode).toBe(0); expect(allowed.json).toMatchObject({ status: "ready" }); }, ); cliTest( "OpenCode doctor sees a global registration when the custom directory is absent", async ({ cli }) => { const home = await createTestTempDirectory("rea-opencode-absent-custom-"); const global = join(home, ".config/opencode/opencode.json"); const custom = join(home, "missing-custom"); await mkdir(dirname(global), { recursive: true }); await writeFile( global, JSON.stringify({ mcp: { rea: { type: "local", command: [ "npx", "-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp", ], enabled: true, }, }, }), ); const run = (args: readonly string[]) => cli.run({ arguments: args, cwd: home, environment: { HOME: home, USERPROFILE: home, OPENCODE_CONFIG_DIR: custom, }, }); const doctor = await run(["doctor", "--client", "opencode", "--json"]); expect(doctor.exitCode).toBe(0); expect(doctor.json).toMatchObject({ identity: { registrations: expect.arrayContaining([ expect.objectContaining({ client: "opencode", state: "aligned" }), ]), }, }); const planned = await run([ "setup", "--client", "opencode", "--dry-run", "--json", ]); expect(planned.json).toMatchObject({ clientStates: expect.arrayContaining([ expect.objectContaining({ client: expect.objectContaining({ name: "opencode" }), detected: true, }), ]), }); await expect(access(custom)).rejects.toMatchObject({ code: "ENOENT" }); }, ); cliTest( "OpenCode retains the XDG global layer when a custom directory is selected", async ({ cli }) => { const home = await createTestTempDirectory("rea-opencode-global-custom-"); const global = join(home, ".config/opencode/opencode.json"); const custom = join(home, "custom/opencode.jsonc"); await mkdir(dirname(global), { recursive: true }); await mkdir(dirname(custom), { recursive: true }); const owned = { type: "local", command: [ "npx", "-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp", ], enabled: true, }; const original = JSON.stringify({ mcp: { rea: owned, other: { type: "local", command: ["other"] } }, }); await writeFile(global, original); await writeFile(custom, "{}"); const run = (args: readonly string[]) => cli.run({ arguments: args, cwd: home, environment: { HOME: home, USERPROFILE: home, OPENCODE_CONFIG_DIR: dirname(custom), }, }); const doctor = await run(["doctor", "--client", "opencode", "--json"]); expect(doctor.json).toMatchObject({ identity: { registrations: expect.arrayContaining([ expect.objectContaining({ client: "opencode", state: "aligned" }), ]), }, }); await writeFile(global, "malformed"); expect( (await run(["setup", "--client", "opencode", "--yes", "--json"])).json, ).toMatchObject({ status: "needs_human", plannedActions: [], appliedActions: [], remediation: expect.stringContaining(global), }); expect(await readFile(custom, "utf8")).toBe("{}"); await writeFile(global, original); expect( (await run(["setup", "--client", "opencode", "--yes", "--json"])) .exitCode, ).toBe(0); expect((await run(["uninstall", "--json"])).exitCode).toBe(0); expect(parse(await readFile(global, "utf8"))).toEqual({ mcp: { other: { type: "local", command: ["other"] } }, }); expect(parse(await readFile(custom, "utf8"))).toEqual({ mcp: {} }); }, ); cliTest( "Gemini blocks a conflicting system definition and accepts an identical managed entry", async ({ cli }) => { const home = await createTestTempDirectory("rea-gemini-managed-server-"); const user = join(home, ".gemini/settings.json"); const system = join(home, "system.json"); const defaults = join(home, "defaults.json"); const environment = { HOME: home, USERPROFILE: home, GEMINI_CLI_SYSTEM_SETTINGS_PATH: system, GEMINI_CLI_SYSTEM_DEFAULTS_PATH: defaults, }; const run = (args: readonly string[]) => cli.run({ arguments: args, cwd: home, environment }); const command = ["setup", "--client", "gemini_cli", "--yes", "--json"]; // A user registration overrides a default definition, including its entire env object. await writeFile( defaults, JSON.stringify({ mcpServers: { rea: { command: "managed-default", env: { EXTRA: "default" } }, }, }), ); expect((await run(command)).exitCode).toBe(0); const original = await readFile(user, "utf8"); const managed = JSON.stringify({ mcpServers: { rea: { command: "managed-override", args: [] } }, unrelated: true, }); await writeFile(system, managed); expect((await run(command)).json).toMatchObject({ status: "needs_human", plannedActions: [], appliedActions: [], remediation: expect.stringContaining(system), }); expect( (await run(["doctor", "--client", "gemini_cli", "--json"])).json, ).toMatchObject({ healthy: false, identity: { registrations: expect.arrayContaining([ expect.objectContaining({ client: "gemini_cli", state: "stale", remediation: expect.stringContaining("mcpServers.rea"), }), ]), }, }); expect(await readFile(user, "utf8")).toBe(original); expect(await readFile(system, "utf8")).toBe(managed); const aligned = JSON.stringify({ mcpServers: JSON.parse(original).mcpServers, }); await writeFile(system, aligned); expect((await run(command)).json).toMatchObject({ status: "ready", appliedActions: [], }); expect( (await run(["doctor", "--client", "gemini_cli", "--json"])).exitCode, ).toBe(0); expect(await readFile(system, "utf8")).toBe(aligned); await expect(access(system + ".rea.backup")).rejects.toMatchObject({ code: "ENOENT", }); }, ); cliTest( "Gemini workspace policy follows persisted trust and explicit trust overrides", async ({ cli }) => { const home = await createTestTempDirectory("rea-gemini-workspace-policy-"); const project = join(home, "project"); const workspace = join(project, ".gemini/settings.json"); const trust = join(home, "trust.json"); await mkdir(dirname(workspace), { recursive: true }); const environment: NodeJS.ProcessEnv = { HOME: home, USERPROFILE: home, GEMINI_CLI_SYSTEM_SETTINGS_PATH: join(home, "system.json"), GEMINI_CLI_SYSTEM_DEFAULTS_PATH: join(home, "defaults.json"), GEMINI_CLI_TRUSTED_FOLDERS_PATH: trust, }; const run = (args: readonly string[]) => cli.run({ arguments: args, cwd: project, environment }); const command = ["setup", "--client", "gemini_cli", "--yes", "--json"]; const original = JSON.stringify({ mcp: { excluded: ["rea"] }, unrelated: true, }); await writeFile(workspace, original); await writeFile( trust, JSON.stringify({ [home]: "TRUST_FOLDER", [project]: "DO_NOT_TRUST" }), ); expect((await run(command)).exitCode).toBe(0); await writeFile( trust, JSON.stringify({ [join(project, "child")]: "TRUST_PARENT" }), ); expect((await run(command)).json).toMatchObject({ status: "needs_human", appliedActions: [], remediation: expect.stringContaining(workspace), }); expect( (await run(["doctor", "--client", "gemini_cli", "--json"])).json, ).toMatchObject({ healthy: false, identity: { registrations: expect.arrayContaining([ expect.objectContaining({ client: "gemini_cli", state: "stale", remediation: expect.stringContaining("mcp.excluded"), }), ]), }, }); environment.GEMINI_CLI_TRUST_WORKSPACE = "false"; expect((await run(command)).json).toMatchObject({ status: "ready", appliedActions: [], }); environment.GEMINI_CLI_TRUST_WORKSPACE = "true"; await writeFile(workspace, JSON.stringify({ mcp: { allowed: ["other"] } })); expect((await run(command)).json).toMatchObject({ status: "needs_human", remediation: expect.stringContaining("mcp.allowed"), }); // Restricted mode takes precedence over an explicit trust opt-in. environment.GEMINI_RESTRICTED_MODE = "true"; expect((await run(command)).json).toMatchObject({ status: "ready", appliedActions: [], }); expect(JSON.parse(await readFile(workspace, "utf8"))).toEqual({ mcp: { allowed: ["other"] }, }); await expect(access(workspace + ".rea.backup")).rejects.toMatchObject({ code: "ENOENT", }); }, ); cliTest( "Gemini workspace definitions use shallow precedence and remain read-only", async ({ cli }) => { const home = await createTestTempDirectory("rea-gemini-workspace-server-"); const project = join(home, "project"); const workspace = join(project, ".gemini/settings.json"); const system = join(home, "system.json"); await mkdir(dirname(workspace), { recursive: true }); const environment: NodeJS.ProcessEnv = { HOME: home, USERPROFILE: home, GEMINI_CLI_SYSTEM_SETTINGS_PATH: system, GEMINI_CLI_SYSTEM_DEFAULTS_PATH: join(home, "defaults.json"), GEMINI_CLI_TRUST_WORKSPACE: "true", }; const run = (args: readonly string[]) => cli.run({ arguments: args, cwd: project, environment }); const command = ["setup", "--client", "gemini_cli", "--yes", "--json"]; const original = JSON.stringify({ mcpServers: { rea: { command: "project-server", args: [] } }, }); await writeFile(workspace, original); expect((await run(command)).json).toMatchObject({ status: "needs_human", plannedActions: [], appliedActions: [], remediation: expect.stringContaining(workspace), }); await expect( access(join(home, ".gemini/settings.json")), ).rejects.toMatchObject({ code: "ENOENT" }); // Obtain this setup invocation's actual command while workspace settings are untrusted. environment.GEMINI_CLI_TRUST_WORKSPACE = "false"; expect((await run(command)).exitCode).toBe(0); const user = JSON.parse( await readFile(join(home, ".gemini/settings.json"), "utf8"), ); environment.GEMINI_CLI_TRUST_WORKSPACE = "true"; // System's entire entry wins over the workspace entry. await writeFile(system, JSON.stringify({ mcpServers: user.mcpServers })); expect((await run(command)).exitCode).toBe(0); expect( (await run(["doctor", "--client", "gemini_cli", "--json"])).exitCode, ).toBe(0); expect(await readFile(workspace, "utf8")).toBe(original); await expect(access(workspace + ".rea.backup")).rejects.toMatchObject({ code: "ENOENT", }); }, ); cliTest( "Gemini validates trust configuration and honors a disabled folder trust feature", async ({ cli }) => { const home = await createTestTempDirectory("rea-gemini-trust-invalid-"); const project = join(home, "project"); const workspace = join(project, ".gemini/settings.json"); const user = join(home, ".gemini/settings.json"); const trust = join(home, ".gemini/trustedFolders.json"); await mkdir(dirname(workspace), { recursive: true }); await mkdir(dirname(user), { recursive: true }); await writeFile(workspace, JSON.stringify({ mcp: { excluded: ["rea"] } })); await writeFile(trust, JSON.stringify({ [project]: "INVALID" })); const run = () => cli.run({ arguments: ["setup", "--client", "gemini_cli", "--yes", "--json"], cwd: project, environment: { HOME: home, USERPROFILE: home, GEMINI_CLI_SYSTEM_SETTINGS_PATH: join(home, "system.json"), GEMINI_CLI_SYSTEM_DEFAULTS_PATH: join(home, "defaults.json"), }, }); expect((await run()).json).toMatchObject({ status: "needs_human", plannedActions: [], appliedActions: [], remediation: expect.stringContaining(trust), }); await expect(access(user)).rejects.toMatchObject({ code: "ENOENT" }); await rm(trust); await writeFile( user, JSON.stringify({ security: { folderTrust: { enabled: false } } }), ); expect((await run()).json).toMatchObject({ status: "needs_human", remediation: expect.stringContaining("mcp.excluded"), }); }, );