diff options
| author | Abhi <[email protected]> | 2025-07-22 00:34:55 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-07-22 04:34:55 +0000 |
| commit | 9daead63ddc4a0bddad05ec9f4bb7c0726da44f4 (patch) | |
| tree | a756014f436f4cc356ca334a45494386027e7b4e /packages/cli/src/services/FileCommandLoader.test.ts | |
| parent | 5f813ef51076177aadccc0046f2182310d6b0a1a (diff) | |
(feat): Initial Version of Custom Commands (#4572)
Diffstat (limited to 'packages/cli/src/services/FileCommandLoader.test.ts')
| -rw-r--r-- | packages/cli/src/services/FileCommandLoader.test.ts | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/packages/cli/src/services/FileCommandLoader.test.ts b/packages/cli/src/services/FileCommandLoader.test.ts new file mode 100644 index 00000000..518c9230 --- /dev/null +++ b/packages/cli/src/services/FileCommandLoader.test.ts @@ -0,0 +1,235 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FileCommandLoader } from './FileCommandLoader.js'; +import { + Config, + getProjectCommandsDir, + getUserCommandsDir, +} from '@google/gemini-cli-core'; +import mock from 'mock-fs'; +import { assert } from 'vitest'; +import { createMockCommandContext } from '../test-utils/mockCommandContext.js'; + +const mockContext = createMockCommandContext(); + +describe('FileCommandLoader', () => { + const signal: AbortSignal = new AbortController().signal; + + afterEach(() => { + mock.restore(); + }); + + it('loads a single command from a file', async () => { + const userCommandsDir = getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'test.toml': 'prompt = "This is a test prompt"', + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + + expect(commands).toHaveLength(1); + const command = commands[0]; + expect(command).toBeDefined(); + expect(command.name).toBe('test'); + + const result = await command.action?.(mockContext, ''); + if (result?.type === 'submit_prompt') { + expect(result.content).toBe('This is a test prompt'); + } else { + assert.fail('Incorrect action type'); + } + }); + + it('loads multiple commands', async () => { + const userCommandsDir = getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'test1.toml': 'prompt = "Prompt 1"', + 'test2.toml': 'prompt = "Prompt 2"', + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + + expect(commands).toHaveLength(2); + }); + + it('creates deeply nested namespaces correctly', async () => { + const userCommandsDir = getUserCommandsDir(); + + mock({ + [userCommandsDir]: { + gcp: { + pipelines: { + 'run.toml': 'prompt = "run pipeline"', + }, + }, + }, + }); + const loader = new FileCommandLoader({ + getProjectRoot: () => '/path/to/project', + } as Config); + const commands = await loader.loadCommands(signal); + expect(commands).toHaveLength(1); + expect(commands[0]!.name).toBe('gcp:pipelines:run'); + }); + + it('creates namespaces from nested directories', async () => { + const userCommandsDir = getUserCommandsDir(); + mock({ + [userCommandsDir]: { + git: { + 'commit.toml': 'prompt = "git commit prompt"', + }, + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + + expect(commands).toHaveLength(1); + const command = commands[0]; + expect(command).toBeDefined(); + expect(command.name).toBe('git:commit'); + }); + + it('overrides user commands with project commands', async () => { + const userCommandsDir = getUserCommandsDir(); + const projectCommandsDir = getProjectCommandsDir(process.cwd()); + mock({ + [userCommandsDir]: { + 'test.toml': 'prompt = "User prompt"', + }, + [projectCommandsDir]: { + 'test.toml': 'prompt = "Project prompt"', + }, + }); + + const loader = new FileCommandLoader({ + getProjectRoot: () => process.cwd(), + } as Config); + const commands = await loader.loadCommands(signal); + + expect(commands).toHaveLength(1); + const command = commands[0]; + expect(command).toBeDefined(); + + const result = await command.action?.(mockContext, ''); + if (result?.type === 'submit_prompt') { + expect(result.content).toBe('Project prompt'); + } else { + assert.fail('Incorrect action type'); + } + }); + + it('ignores files with TOML syntax errors', async () => { + const userCommandsDir = getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'invalid.toml': 'this is not valid toml', + 'good.toml': 'prompt = "This one is fine"', + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + + expect(commands).toHaveLength(1); + expect(commands[0].name).toBe('good'); + }); + + it('ignores files that are semantically invalid (missing prompt)', async () => { + const userCommandsDir = getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'no_prompt.toml': 'description = "This file is missing a prompt"', + 'good.toml': 'prompt = "This one is fine"', + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + + expect(commands).toHaveLength(1); + expect(commands[0].name).toBe('good'); + }); + + it('handles filename edge cases correctly', async () => { + const userCommandsDir = getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'test.v1.toml': 'prompt = "Test prompt"', + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + const command = commands[0]; + expect(command).toBeDefined(); + expect(command.name).toBe('test.v1'); + }); + + it('handles file system errors gracefully', async () => { + mock({}); // Mock an empty file system + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + expect(commands).toHaveLength(0); + }); + + it('uses a default description if not provided', async () => { + const userCommandsDir = getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'test.toml': 'prompt = "Test prompt"', + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + const command = commands[0]; + expect(command).toBeDefined(); + expect(command.description).toBe('Custom command from test.toml'); + }); + + it('uses the provided description', async () => { + const userCommandsDir = getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'test.toml': 'prompt = "Test prompt"\ndescription = "My test command"', + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + const command = commands[0]; + expect(command).toBeDefined(); + expect(command.description).toBe('My test command'); + }); + + it('should sanitize colons in filenames to prevent namespace conflicts', async () => { + const userCommandsDir = getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'legacy:command.toml': 'prompt = "This is a legacy command"', + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + + expect(commands).toHaveLength(1); + const command = commands[0]; + expect(command).toBeDefined(); + + // Verify that the ':' in the filename was replaced with an '_' + expect(command.name).toBe('legacy_command'); + }); +}); |
