mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 21:32:56 -07:00
refactor(editor): use async functions to avoid blocking event loop
Replace synchronous execSync calls with async alternatives in editor detection functions to prevent blocking the Node.js event loop. Changes: - Add commandExistsAsync using promisified exec - Add checkHasEditorTypeAsync, isEditorAvailableAsync, detectFirstAvailableEditorAsync, and resolveEditorAsync - Update confirmation.ts and coreToolScheduler.ts to use resolveEditorAsync - Mark synchronous resolveEditor as deprecated - Add comprehensive tests for all async functions The synchronous versions are kept for UI components that require synchronous execution (useEditorSettings, editorSettingsManager).
This commit is contained in:
@@ -15,18 +15,23 @@ import {
|
||||
} from 'vitest';
|
||||
import {
|
||||
checkHasEditorType,
|
||||
checkHasEditorTypeAsync,
|
||||
getDiffCommand,
|
||||
openDiff,
|
||||
allowEditorTypeInSandbox,
|
||||
isEditorAvailable,
|
||||
isEditorAvailableAsync,
|
||||
detectFirstAvailableEditor,
|
||||
detectFirstAvailableEditorAsync,
|
||||
resolveEditor,
|
||||
resolveEditorAsync,
|
||||
type EditorType,
|
||||
} from './editor.js';
|
||||
import { execSync, spawn, spawnSync } from 'node:child_process';
|
||||
import { exec, execSync, spawn, spawnSync } from 'node:child_process';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
vi.mock('child_process', () => ({
|
||||
exec: vi.fn(),
|
||||
execSync: vi.fn(),
|
||||
spawn: vi.fn(),
|
||||
spawnSync: vi.fn(() => ({ error: null, status: 0 })),
|
||||
@@ -670,4 +675,160 @@ describe('editor utils', () => {
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// Helper to create a mock exec that simulates async behavior
|
||||
const mockExecAsync = (implementation: (cmd: string) => boolean): void => {
|
||||
(exec as unknown as Mock).mockImplementation(
|
||||
(
|
||||
cmd: string,
|
||||
callback: (error: Error | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (implementation(cmd)) {
|
||||
callback(null, '/usr/bin/cmd', '');
|
||||
} else {
|
||||
callback(new Error('Command not found'), '', '');
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
describe('checkHasEditorTypeAsync', () => {
|
||||
it('should return true if vim command exists', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
mockExecAsync((cmd) => cmd.includes('vim'));
|
||||
expect(await checkHasEditorTypeAsync('vim')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if vim command does not exist', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
mockExecAsync(() => false);
|
||||
expect(await checkHasEditorTypeAsync('vim')).toBe(false);
|
||||
});
|
||||
|
||||
it('should check zed and zeditor commands in order', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
mockExecAsync((cmd) => cmd.includes('zeditor'));
|
||||
expect(await checkHasEditorTypeAsync('zed')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEditorAvailableAsync', () => {
|
||||
it('should return false for undefined editor', async () => {
|
||||
expect(await isEditorAvailableAsync(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty string editor', async () => {
|
||||
expect(await isEditorAvailableAsync('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for invalid editor type', async () => {
|
||||
expect(await isEditorAvailableAsync('invalid-editor')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for vscode when installed and not in sandbox mode', async () => {
|
||||
mockExecAsync((cmd) => cmd.includes('code'));
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
expect(await isEditorAvailableAsync('vscode')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for vscode when not installed', async () => {
|
||||
mockExecAsync(() => false);
|
||||
expect(await isEditorAvailableAsync('vscode')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for vscode in sandbox mode', async () => {
|
||||
mockExecAsync((cmd) => cmd.includes('code'));
|
||||
vi.stubEnv('SANDBOX', 'sandbox');
|
||||
expect(await isEditorAvailableAsync('vscode')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for vim in sandbox mode', async () => {
|
||||
mockExecAsync((cmd) => cmd.includes('vim'));
|
||||
vi.stubEnv('SANDBOX', 'sandbox');
|
||||
expect(await isEditorAvailableAsync('vim')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectFirstAvailableEditorAsync', () => {
|
||||
it('should return undefined when no editors are installed', async () => {
|
||||
mockExecAsync(() => false);
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
expect(await detectFirstAvailableEditorAsync()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prioritize terminal editors over GUI editors', async () => {
|
||||
mockExecAsync(
|
||||
(cmd) =>
|
||||
(cmd.includes('vim') && !cmd.includes('nvim')) ||
|
||||
cmd.includes('code'),
|
||||
);
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
expect(await detectFirstAvailableEditorAsync()).toBe('vim');
|
||||
});
|
||||
|
||||
it('should return vim in sandbox mode', async () => {
|
||||
mockExecAsync((cmd) => cmd.includes('vim') && !cmd.includes('nvim'));
|
||||
vi.stubEnv('SANDBOX', 'sandbox');
|
||||
expect(await detectFirstAvailableEditorAsync()).toBe('vim');
|
||||
});
|
||||
|
||||
it('should skip GUI editors in sandbox mode', async () => {
|
||||
mockExecAsync((cmd) => cmd.includes('code'));
|
||||
vi.stubEnv('SANDBOX', 'sandbox');
|
||||
expect(await detectFirstAvailableEditorAsync()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveEditorAsync', () => {
|
||||
it('should return the preferred editor when available', async () => {
|
||||
mockExecAsync((cmd) => cmd.includes('vim'));
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
const result = await resolveEditorAsync('vim');
|
||||
expect(result.editor).toBe('vim');
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return error when preferred editor is not installed', async () => {
|
||||
mockExecAsync(() => false);
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
const result = await resolveEditorAsync('vim');
|
||||
expect(result.editor).toBeUndefined();
|
||||
expect(result.error).toContain('Vim');
|
||||
expect(result.error).toContain('not installed');
|
||||
});
|
||||
|
||||
it('should return error when preferred GUI editor cannot be used in sandbox mode', async () => {
|
||||
mockExecAsync((cmd) => cmd.includes('code'));
|
||||
vi.stubEnv('SANDBOX', 'sandbox');
|
||||
const result = await resolveEditorAsync('vscode');
|
||||
expect(result.editor).toBeUndefined();
|
||||
expect(result.error).toContain('VS Code');
|
||||
expect(result.error).toContain('sandbox mode');
|
||||
});
|
||||
|
||||
it('should auto-detect editor when no preference is set', async () => {
|
||||
mockExecAsync((cmd) => cmd.includes('vim') && !cmd.includes('nvim'));
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
const result = await resolveEditorAsync(undefined);
|
||||
expect(result.editor).toBe('vim');
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return error when no preference is set and no editors are available', async () => {
|
||||
mockExecAsync(() => false);
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
const result = await resolveEditorAsync(undefined);
|
||||
expect(result.editor).toBeUndefined();
|
||||
expect(result.error).toContain('No external editor');
|
||||
expect(result.error).toContain('/editor');
|
||||
});
|
||||
|
||||
it('should work with terminal editors in sandbox mode when no preference is set', async () => {
|
||||
mockExecAsync((cmd) => cmd.includes('vim') && !cmd.includes('nvim'));
|
||||
vi.stubEnv('SANDBOX', 'sandbox');
|
||||
const result = await resolveEditorAsync(undefined);
|
||||
expect(result.editor).toBe('vim');
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user