diff --git a/packages/cli/src/services/FileCommandLoader.test.ts b/packages/cli/src/services/FileCommandLoader.test.ts index a3c3018f83..562562c9e4 100644 --- a/packages/cli/src/services/FileCommandLoader.test.ts +++ b/packages/cli/src/services/FileCommandLoader.test.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as glob from 'glob'; import * as path from 'node:path'; import type { Config } from '@google/gemini-cli-core'; import { GEMINI_DIR, Storage } from '@google/gemini-cli-core'; @@ -70,11 +71,18 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { }; }); +vi.mock('glob', () => ({ + glob: vi.fn(), +})); + describe('FileCommandLoader', () => { const signal: AbortSignal = new AbortController().signal; - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks(); + const { glob: actualGlob } = + await vi.importActual('glob'); + vi.mocked(glob.glob).mockImplementation(actualGlob); mockShellProcess.mockImplementation( (prompt: PromptPipelineContent, context: CommandContext) => { const userArgsRaw = context?.invocation?.args || ''; @@ -1288,4 +1296,45 @@ describe('FileCommandLoader', () => { expect(commands).toHaveLength(0); }); }); + + describe('Aborted signal', () => { + it('does not log errors if the signal is aborted', async () => { + const controller = new AbortController(); + const abortSignal = controller.signal; + + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const mockConfig = { + getProjectRoot: vi.fn(() => '/path/to/project'), + getExtensions: vi.fn(() => []), + getFolderTrust: vi.fn(() => false), + isTrustedFolder: vi.fn(() => false), + } as unknown as Config; + + // Set up mock-fs so that the loader attempts to read a directory. + const userCommandsDir = Storage.getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'test1.toml': 'prompt = "Prompt 1"', + }, + }); + + const loader = new FileCommandLoader(mockConfig); + + // Mock glob to throw an AbortError + const abortError = new DOMException('Aborted', 'AbortError'); + vi.mocked(glob.glob).mockImplementation(async () => { + controller.abort(); // Ensure the signal is aborted when the service checks + throw abortError; + }); + + await loader.loadCommands(abortSignal); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); + }); }); diff --git a/packages/cli/src/services/FileCommandLoader.ts b/packages/cli/src/services/FileCommandLoader.ts index cf91af7d89..a8da797e36 100644 --- a/packages/cli/src/services/FileCommandLoader.ts +++ b/packages/cli/src/services/FileCommandLoader.ts @@ -85,6 +85,10 @@ export class FileCommandLoader implements ICommandLoader { * @returns A promise that resolves to an array of all loaded SlashCommands. */ async loadCommands(signal: AbortSignal): Promise { + if (this.folderTrustEnabled && !this.isTrustedFolder) { + return []; + } + const allCommands: SlashCommand[] = []; const globOptions = { nodir: true, @@ -102,10 +106,6 @@ export class FileCommandLoader implements ICommandLoader { cwd: dirInfo.path, }); - if (this.folderTrustEnabled && !this.isTrustedFolder) { - return []; - } - const commandPromises = files.map((file) => this.parseAndAdaptFile( path.join(dirInfo.path, file), @@ -122,7 +122,10 @@ export class FileCommandLoader implements ICommandLoader { // Add all commands without deduplication allCommands.push(...commands); } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + if ( + !signal.aborted && + (error as { code?: string })?.code !== 'ENOENT' + ) { console.error( `[FileCommandLoader] Error loading commands from ${dirInfo.path}:`, error,