Introduce GEMINI_CLI_HOME for strict test isolation (#15907)

This commit is contained in:
N. Taylor Mullen
2026-01-06 20:09:39 -08:00
committed by GitHub
parent a26463b056
commit 7956eb239e
54 changed files with 455 additions and 148 deletions

View File

@@ -7,53 +7,79 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Command } from './types.js';
describe('CommandRegistry', () => {
const mockListExtensionsCommandInstance: Command = {
const {
mockExtensionsCommand,
mockListExtensionsCommand,
mockExtensionsCommandInstance,
mockListExtensionsCommandInstance,
} = vi.hoisted(() => {
const listInstance: Command = {
name: 'extensions list',
description: 'Lists all installed extensions.',
execute: vi.fn(),
};
const mockListExtensionsCommand = vi.fn(
() => mockListExtensionsCommandInstance,
);
const mockExtensionsCommandInstance: Command = {
const extInstance: Command = {
name: 'extensions',
description: 'Manage extensions.',
execute: vi.fn(),
subCommands: [mockListExtensionsCommandInstance],
subCommands: [listInstance],
};
const mockExtensionsCommand = vi.fn(() => mockExtensionsCommandInstance);
return {
mockListExtensionsCommandInstance: listInstance,
mockExtensionsCommandInstance: extInstance,
mockExtensionsCommand: vi.fn(() => extInstance),
mockListExtensionsCommand: vi.fn(() => listInstance),
};
});
vi.mock('./extensions.js', () => ({
ExtensionsCommand: mockExtensionsCommand,
ListExtensionsCommand: mockListExtensionsCommand,
}));
vi.mock('./init.js', () => ({
InitCommand: vi.fn(() => ({
name: 'init',
description: 'Initializes the server.',
execute: vi.fn(),
})),
}));
vi.mock('./restore.js', () => ({
RestoreCommand: vi.fn(() => ({
name: 'restore',
description: 'Restores the server.',
execute: vi.fn(),
})),
}));
import { commandRegistry } from './command-registry.js';
describe('CommandRegistry', () => {
beforeEach(async () => {
vi.resetModules();
vi.doMock('./extensions.js', () => ({
ExtensionsCommand: mockExtensionsCommand,
ListExtensionsCommand: mockListExtensionsCommand,
}));
vi.clearAllMocks();
commandRegistry.initialize();
});
it('should register ExtensionsCommand on initialization', async () => {
const { commandRegistry } = await import('./command-registry.js');
expect(mockExtensionsCommand).toHaveBeenCalled();
const command = commandRegistry.get('extensions');
expect(command).toBe(mockExtensionsCommandInstance);
});
it('should register sub commands on initialization', async () => {
const { commandRegistry } = await import('./command-registry.js');
const command = commandRegistry.get('extensions list');
expect(command).toBe(mockListExtensionsCommandInstance);
});
it('get() should return undefined for a non-existent command', async () => {
const { commandRegistry } = await import('./command-registry.js');
const command = commandRegistry.get('non-existent');
expect(command).toBeUndefined();
});
it('register() should register a new command', async () => {
const { commandRegistry } = await import('./command-registry.js');
const mockCommand: Command = {
name: 'test-command',
description: '',
@@ -65,7 +91,6 @@ describe('CommandRegistry', () => {
});
it('register() should register a nested command', async () => {
const { commandRegistry } = await import('./command-registry.js');
const mockSubSubCommand: Command = {
name: 'test-command-sub-sub',
description: '',
@@ -95,8 +120,8 @@ describe('CommandRegistry', () => {
});
it('register() should not enter an infinite loop with a cyclic command', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const { commandRegistry } = await import('./command-registry.js');
const { debugLogger } = await import('@google/gemini-cli-core');
const warnSpy = vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
const mockCommand: Command = {
name: 'cyclic-command',
description: '',
@@ -112,7 +137,6 @@ describe('CommandRegistry', () => {
expect(warnSpy).toHaveBeenCalledWith(
'Command cyclic-command already registered. Skipping.',
);
// If the test finishes, it means we didn't get into an infinite loop.
warnSpy.mockRestore();
});
});

View File

@@ -10,10 +10,15 @@ import { InitCommand } from './init.js';
import { RestoreCommand } from './restore.js';
import type { Command } from './types.js';
class CommandRegistry {
export class CommandRegistry {
private readonly commands = new Map<string, Command>();
constructor() {
this.initialize();
}
initialize() {
this.commands.clear();
this.register(new ExtensionsCommand());
this.register(new RestoreCommand());
this.register(new InitCommand());

View File

@@ -6,7 +6,6 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { homedir } from 'node:os';
import * as dotenv from 'dotenv';
import type { TelemetryTarget } from '@google/gemini-cli-core';
@@ -23,6 +22,7 @@ import {
type ExtensionLoader,
startupProfiler,
PREVIEW_GEMINI_MODEL,
homedir,
} from '@google/gemini-cli-core';
import { logger } from '../utils/logger.js';

View File

@@ -11,10 +11,10 @@ import {
type MCPServerConfig,
type ExtensionInstallMetadata,
type GeminiCLIExtension,
homedir,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { logger } from '../utils/logger.js';
export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
@@ -39,7 +39,7 @@ interface ExtensionConfig {
export function loadExtensions(workspaceDir: string): GeminiCLIExtension[] {
const allExtensions = [
...loadExtensionsFromDir(workspaceDir),
...loadExtensionsFromDir(os.homedir()),
...loadExtensionsFromDir(homedir()),
];
const uniqueExtensions: GeminiCLIExtension[] = [];

View File

@@ -27,13 +27,21 @@ vi.mock('node:os', async (importOriginal) => {
};
});
vi.mock('@google/gemini-cli-core', () => ({
GEMINI_DIR: '.gemini',
debugLogger: {
error: vi.fn(),
},
getErrorMessage: (error: unknown) => String(error),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
const path = await import('node:path');
const os = await import('node:os');
return {
...actual,
GEMINI_DIR: '.gemini',
debugLogger: {
error: vi.fn(),
},
getErrorMessage: (error: unknown) => String(error),
homedir: () => path.join(os.tmpdir(), `gemini-home-${mocks.suffix}`),
};
});
describe('loadSettings', () => {
const mockHomeDir = path.join(os.tmpdir(), `gemini-home-${mocks.suffix}`);

View File

@@ -6,7 +6,6 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { homedir } from 'node:os';
import type { MCPServerConfig } from '@google/gemini-cli-core';
import {
@@ -14,6 +13,7 @@ import {
GEMINI_DIR,
getErrorMessage,
type TelemetrySettings,
homedir,
} from '@google/gemini-cli-core';
import stripJsonComments from 'strip-json-comments';

View File

@@ -9,7 +9,7 @@ import { gzipSync, gunzipSync } from 'node:zlib';
import * as tar from 'tar';
import * as fse from 'fs-extra';
import { promises as fsPromises, createReadStream } from 'node:fs';
import { tmpdir } from 'node:os';
import { tmpdir } from '@google/gemini-cli-core';
import { join } from 'node:path';
import type { Task as SDKTask } from '@a2a-js/sdk';
import type { TaskStore } from '@a2a-js/sdk/server';