|
|
@@ -317,6 +317,277 @@ describe('Installer targets — partial-state idempotency', () => {
|
|
|
expect(paths.some((p) => p.endsWith('/AGENTS.md'))).toBe(true);
|
|
|
});
|
|
|
|
|
|
+ it('gemini: install writes settings.json (mcpServers.codegraph) and GEMINI.md with marker block', () => {
|
|
|
+ const gemini = getTarget('gemini')!;
|
|
|
+ const result = gemini.install('global', { autoAllow: true });
|
|
|
+ const settings = path.join(tmpHome, '.gemini', 'settings.json');
|
|
|
+ const geminiMd = path.join(tmpHome, '.gemini', 'GEMINI.md');
|
|
|
+ expect(result.files.some((f) => f.path === settings)).toBe(true);
|
|
|
+ expect(result.files.some((f) => f.path === geminiMd)).toBe(true);
|
|
|
+
|
|
|
+ const cfg = JSON.parse(fs.readFileSync(settings, 'utf-8'));
|
|
|
+ expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] });
|
|
|
+
|
|
|
+ const md = fs.readFileSync(geminiMd, 'utf-8');
|
|
|
+ expect(md).toContain('<!-- CODEGRAPH_START -->');
|
|
|
+ expect(md).toContain('<!-- CODEGRAPH_END -->');
|
|
|
+ expect(md).toContain('codegraph_callers');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('gemini: install preserves pre-existing settings (security.auth survives)', () => {
|
|
|
+ const gemini = getTarget('gemini')!;
|
|
|
+ const settings = path.join(tmpHome, '.gemini', 'settings.json');
|
|
|
+ fs.mkdirSync(path.dirname(settings), { recursive: true });
|
|
|
+ fs.writeFileSync(settings, JSON.stringify({
|
|
|
+ security: { auth: { selectedType: 'oauth-personal' } },
|
|
|
+ }, null, 2) + '\n');
|
|
|
+
|
|
|
+ gemini.install('global', { autoAllow: true });
|
|
|
+
|
|
|
+ const after = JSON.parse(fs.readFileSync(settings, 'utf-8'));
|
|
|
+ expect(after.security?.auth?.selectedType).toBe('oauth-personal');
|
|
|
+ expect(after.mcpServers?.codegraph).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('gemini: uninstall strips codegraph but leaves pre-existing settings (security.auth) intact', () => {
|
|
|
+ const gemini = getTarget('gemini')!;
|
|
|
+ const settings = path.join(tmpHome, '.gemini', 'settings.json');
|
|
|
+ fs.mkdirSync(path.dirname(settings), { recursive: true });
|
|
|
+ fs.writeFileSync(settings, JSON.stringify({
|
|
|
+ security: { auth: { selectedType: 'oauth-personal' } },
|
|
|
+ }, null, 2) + '\n');
|
|
|
+
|
|
|
+ gemini.install('global', { autoAllow: true });
|
|
|
+ gemini.uninstall('global');
|
|
|
+
|
|
|
+ const after = JSON.parse(fs.readFileSync(settings, 'utf-8'));
|
|
|
+ expect(after.security?.auth?.selectedType).toBe('oauth-personal');
|
|
|
+ expect(after.mcpServers).toBeUndefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('gemini: local install writes ./.gemini/settings.json and ./GEMINI.md (project root)', () => {
|
|
|
+ const gemini = getTarget('gemini')!;
|
|
|
+ const result = gemini.install('local', { autoAllow: true });
|
|
|
+ const paths = result.files.map((f) => f.path.replace(/\\/g, '/'));
|
|
|
+ expect(paths.some((p) => p.endsWith('/.gemini/settings.json'))).toBe(true);
|
|
|
+ // Local GEMINI.md sits at the project root, NOT under .gemini/.
|
|
|
+ expect(paths.some((p) => p.endsWith('/GEMINI.md') && !p.endsWith('/.gemini/GEMINI.md'))).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('gemini: GEMINI.md uninstall preserves user content outside the codegraph markers', () => {
|
|
|
+ const gemini = getTarget('gemini')!;
|
|
|
+ const geminiMd = path.join(tmpHome, '.gemini', 'GEMINI.md');
|
|
|
+ fs.mkdirSync(path.dirname(geminiMd), { recursive: true });
|
|
|
+ fs.writeFileSync(geminiMd, '# My personal Gemini context\n\nAlways respond concisely.\n');
|
|
|
+
|
|
|
+ gemini.install('global', { autoAllow: true });
|
|
|
+ gemini.uninstall('global');
|
|
|
+
|
|
|
+ const body = fs.readFileSync(geminiMd, 'utf-8');
|
|
|
+ expect(body).toContain('# My personal Gemini context');
|
|
|
+ expect(body).toContain('Always respond concisely.');
|
|
|
+ expect(body).not.toContain('CODEGRAPH_START');
|
|
|
+ expect(body).not.toContain('codegraph_callers');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: install writes to LEGACY ~/.gemini/antigravity/mcp_config.json when no migration marker', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ antigravity.install('global', { autoAllow: true });
|
|
|
+
|
|
|
+ const legacyFile = path.join(tmpHome, '.gemini', 'antigravity', 'mcp_config.json');
|
|
|
+ expect(fs.existsSync(legacyFile)).toBe(true);
|
|
|
+ const cfg = JSON.parse(fs.readFileSync(legacyFile, 'utf-8'));
|
|
|
+ expect(cfg.mcpServers.codegraph).toBeDefined();
|
|
|
+ // Crucially: does NOT touch the Gemini CLI's settings.json.
|
|
|
+ expect(fs.existsSync(path.join(tmpHome, '.gemini', 'settings.json'))).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: install writes to UNIFIED ~/.gemini/config/mcp_config.json when .migrated marker present', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ // Plant the migration marker — same signal Antigravity itself drops
|
|
|
+ // when it migrates a user's config.
|
|
|
+ const unifiedDir = path.join(tmpHome, '.gemini', 'config');
|
|
|
+ fs.mkdirSync(unifiedDir, { recursive: true });
|
|
|
+ fs.writeFileSync(path.join(unifiedDir, '.migrated'), '');
|
|
|
+
|
|
|
+ antigravity.install('global', { autoAllow: true });
|
|
|
+
|
|
|
+ const unifiedFile = path.join(unifiedDir, 'mcp_config.json');
|
|
|
+ expect(fs.existsSync(unifiedFile)).toBe(true);
|
|
|
+ const cfg = JSON.parse(fs.readFileSync(unifiedFile, 'utf-8'));
|
|
|
+ expect(cfg.mcpServers.codegraph).toBeDefined();
|
|
|
+ // Legacy path is NOT touched when the marker tells us migration happened.
|
|
|
+ expect(fs.existsSync(path.join(tmpHome, '.gemini', 'antigravity', 'mcp_config.json'))).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: install writes to UNIFIED path when ~/.gemini/config/mcp_config.json already exists (even without marker)', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ // Antigravity creates this file on first launch post-migration — its
|
|
|
+ // presence is the second signal we accept, in case the .migrated
|
|
|
+ // marker semantics change across Antigravity versions.
|
|
|
+ const unifiedFile = path.join(tmpHome, '.gemini', 'config', 'mcp_config.json');
|
|
|
+ fs.mkdirSync(path.dirname(unifiedFile), { recursive: true });
|
|
|
+ fs.writeFileSync(unifiedFile, JSON.stringify({ mcpServers: {} }, null, 2) + '\n');
|
|
|
+
|
|
|
+ antigravity.install('global', { autoAllow: true });
|
|
|
+
|
|
|
+ const cfg = JSON.parse(fs.readFileSync(unifiedFile, 'utf-8'));
|
|
|
+ expect(cfg.mcpServers.codegraph).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: entry has NO `type` field (Antigravity rejects entries with it)', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ // Marker → unified path; doesn't matter which path, just inspect the entry shape.
|
|
|
+ fs.mkdirSync(path.join(tmpHome, '.gemini', 'config'), { recursive: true });
|
|
|
+ fs.writeFileSync(path.join(tmpHome, '.gemini', 'config', '.migrated'), '');
|
|
|
+
|
|
|
+ antigravity.install('global', { autoAllow: true });
|
|
|
+
|
|
|
+ const cfg = JSON.parse(fs.readFileSync(
|
|
|
+ path.join(tmpHome, '.gemini', 'config', 'mcp_config.json'), 'utf-8'
|
|
|
+ ));
|
|
|
+ expect(cfg.mcpServers.codegraph.type).toBeUndefined();
|
|
|
+ expect(cfg.mcpServers.codegraph.command).toBeDefined();
|
|
|
+ expect(cfg.mcpServers.codegraph.args).toEqual(['serve', '--mcp']);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: install migrates a legacy codegraph entry to the unified path when marker appears', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ // Simulate: user installed on the legacy path, then Antigravity
|
|
|
+ // migrated their config (dropped the `.migrated` marker + created
|
|
|
+ // the unified file). Re-running codegraph install should land
|
|
|
+ // codegraph in the new file AND strip the stale legacy entry.
|
|
|
+ const legacyFile = path.join(tmpHome, '.gemini', 'antigravity', 'mcp_config.json');
|
|
|
+ fs.mkdirSync(path.dirname(legacyFile), { recursive: true });
|
|
|
+ fs.writeFileSync(legacyFile, JSON.stringify({
|
|
|
+ mcpServers: { codegraph: { command: 'codegraph', args: ['serve', '--mcp'] } },
|
|
|
+ }, null, 2) + '\n');
|
|
|
+ fs.mkdirSync(path.join(tmpHome, '.gemini', 'config'), { recursive: true });
|
|
|
+ fs.writeFileSync(path.join(tmpHome, '.gemini', 'config', '.migrated'), '');
|
|
|
+
|
|
|
+ antigravity.install('global', { autoAllow: true });
|
|
|
+
|
|
|
+ const unified = JSON.parse(fs.readFileSync(
|
|
|
+ path.join(tmpHome, '.gemini', 'config', 'mcp_config.json'), 'utf-8'
|
|
|
+ ));
|
|
|
+ expect(unified.mcpServers.codegraph).toBeDefined();
|
|
|
+ // Legacy file's codegraph entry got stripped.
|
|
|
+ const legacy = JSON.parse(fs.readFileSync(legacyFile, 'utf-8'));
|
|
|
+ expect(legacy.mcpServers).toBeUndefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: install preserves a sibling MCP server in mcp_config.json (legacy path)', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ const mcpFile = path.join(tmpHome, '.gemini', 'antigravity', 'mcp_config.json');
|
|
|
+ fs.mkdirSync(path.dirname(mcpFile), { recursive: true });
|
|
|
+ fs.writeFileSync(mcpFile, JSON.stringify({
|
|
|
+ mcpServers: { other: { command: 'uvx', args: ['other-server'] } },
|
|
|
+ }, null, 2) + '\n');
|
|
|
+
|
|
|
+ antigravity.install('global', { autoAllow: true });
|
|
|
+
|
|
|
+ const after = JSON.parse(fs.readFileSync(mcpFile, 'utf-8'));
|
|
|
+ expect(after.mcpServers.other).toBeDefined();
|
|
|
+ expect(after.mcpServers.codegraph).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: install preserves Antigravity-managed fields on sibling servers (e.g. disabled flag)', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ // Antigravity adds `"disabled": true` to entries the user disables via
|
|
|
+ // the IDE. Install must not clobber that on sibling entries.
|
|
|
+ fs.mkdirSync(path.join(tmpHome, '.gemini', 'config'), { recursive: true });
|
|
|
+ fs.writeFileSync(path.join(tmpHome, '.gemini', 'config', '.migrated'), '');
|
|
|
+ const unified = path.join(tmpHome, '.gemini', 'config', 'mcp_config.json');
|
|
|
+ fs.writeFileSync(unified, JSON.stringify({
|
|
|
+ mcpServers: {
|
|
|
+ 'code-review-graph': {
|
|
|
+ command: 'uvx', args: ['code-review-graph', 'serve'], disabled: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }, null, 2) + '\n');
|
|
|
+
|
|
|
+ antigravity.install('global', { autoAllow: true });
|
|
|
+
|
|
|
+ const after = JSON.parse(fs.readFileSync(unified, 'utf-8'));
|
|
|
+ expect(after.mcpServers['code-review-graph'].disabled).toBe(true);
|
|
|
+ expect(after.mcpServers.codegraph).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: uninstall removes only codegraph, sibling MCP server survives', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ const mcpFile = path.join(tmpHome, '.gemini', 'antigravity', 'mcp_config.json');
|
|
|
+ fs.mkdirSync(path.dirname(mcpFile), { recursive: true });
|
|
|
+ fs.writeFileSync(mcpFile, JSON.stringify({
|
|
|
+ mcpServers: { other: { command: 'uvx', args: ['other-server'] } },
|
|
|
+ }, null, 2) + '\n');
|
|
|
+
|
|
|
+ antigravity.install('global', { autoAllow: true });
|
|
|
+ antigravity.uninstall('global');
|
|
|
+
|
|
|
+ const after = JSON.parse(fs.readFileSync(mcpFile, 'utf-8'));
|
|
|
+ expect(after.mcpServers.other).toBeDefined();
|
|
|
+ expect(after.mcpServers.codegraph).toBeUndefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: uninstall sweeps BOTH legacy and unified paths (handles migration half-state)', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ // User had codegraph in BOTH files (e.g. legacy install + post-migration
|
|
|
+ // re-install before our migration cleanup landed). Uninstall must clean
|
|
|
+ // both so a "fresh slate" really is fresh.
|
|
|
+ const legacy = path.join(tmpHome, '.gemini', 'antigravity', 'mcp_config.json');
|
|
|
+ const unified = path.join(tmpHome, '.gemini', 'config', 'mcp_config.json');
|
|
|
+ fs.mkdirSync(path.dirname(legacy), { recursive: true });
|
|
|
+ fs.mkdirSync(path.dirname(unified), { recursive: true });
|
|
|
+ fs.writeFileSync(legacy, JSON.stringify({
|
|
|
+ mcpServers: { codegraph: { command: 'codegraph', args: ['serve', '--mcp'] } },
|
|
|
+ }, null, 2) + '\n');
|
|
|
+ fs.writeFileSync(unified, JSON.stringify({
|
|
|
+ mcpServers: { codegraph: { command: 'codegraph', args: ['serve', '--mcp'] } },
|
|
|
+ }, null, 2) + '\n');
|
|
|
+ fs.writeFileSync(path.join(path.dirname(unified), '.migrated'), '');
|
|
|
+
|
|
|
+ antigravity.uninstall('global');
|
|
|
+
|
|
|
+ const legacyAfter = JSON.parse(fs.readFileSync(legacy, 'utf-8'));
|
|
|
+ const unifiedAfter = JSON.parse(fs.readFileSync(unified, 'utf-8'));
|
|
|
+ expect(legacyAfter.mcpServers).toBeUndefined();
|
|
|
+ expect(unifiedAfter.mcpServers).toBeUndefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: rejects --location=local with a clear note (global-only IDE)', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ expect(antigravity.supportsLocation('local')).toBe(false);
|
|
|
+ const result = antigravity.install('local', { autoAllow: true });
|
|
|
+ expect(result.files).toEqual([]);
|
|
|
+ expect(result.notes?.join(' ')).toMatch(/no project-local config/);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('antigravity: does not write GEMINI.md (only gemini target owns instructions)', () => {
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ antigravity.install('global', { autoAllow: true });
|
|
|
+ const geminiMd = path.join(tmpHome, '.gemini', 'GEMINI.md');
|
|
|
+ expect(fs.existsSync(geminiMd)).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('gemini + antigravity: both installed coexist (separate MCP files, shared GEMINI.md)', () => {
|
|
|
+ const gemini = getTarget('gemini')!;
|
|
|
+ const antigravity = getTarget('antigravity')!;
|
|
|
+ gemini.install('global', { autoAllow: true });
|
|
|
+ antigravity.install('global', { autoAllow: true });
|
|
|
+
|
|
|
+ const cliCfg = JSON.parse(fs.readFileSync(path.join(tmpHome, '.gemini', 'settings.json'), 'utf-8'));
|
|
|
+ // Antigravity lands on the LEGACY path here since no .migrated marker
|
|
|
+ // was planted — same end-to-end check either way.
|
|
|
+ const ideCfg = JSON.parse(fs.readFileSync(path.join(tmpHome, '.gemini', 'antigravity', 'mcp_config.json'), 'utf-8'));
|
|
|
+ expect(cliCfg.mcpServers.codegraph).toBeDefined();
|
|
|
+ expect(ideCfg.mcpServers.codegraph).toBeDefined();
|
|
|
+
|
|
|
+ // Uninstall one — the other's MCP entry must survive.
|
|
|
+ antigravity.uninstall('global');
|
|
|
+ const cliAfter = JSON.parse(fs.readFileSync(path.join(tmpHome, '.gemini', 'settings.json'), 'utf-8'));
|
|
|
+ expect(cliAfter.mcpServers.codegraph).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
it('hermes: install adds codegraph MCP server and cli toolset, preserving existing yaml', () => {
|
|
|
const hermes = getTarget('hermes')!;
|
|
|
const config = path.join(tmpHome, '.hermes', 'config.yaml');
|
|
|
@@ -617,6 +888,8 @@ describe('Installer targets — registry', () => {
|
|
|
expect(getTarget('codex')?.id).toBe('codex');
|
|
|
expect(getTarget('opencode')?.id).toBe('opencode');
|
|
|
expect(getTarget('hermes')?.id).toBe('hermes');
|
|
|
+ expect(getTarget('gemini')?.id).toBe('gemini');
|
|
|
+ expect(getTarget('antigravity')?.id).toBe('antigravity');
|
|
|
expect(getTarget('not-a-real-target')).toBeUndefined();
|
|
|
});
|
|
|
|