Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions packages/cli/src/services/FileCommandLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

import * as glob from 'glob';
import * as path from 'node:path';
import { GEMINI_DIR, Storage, type Config } from '@google/gemini-cli-core';
import {
GEMINI_DIR,
Storage,
type Config,
homedir,
} from '@google/gemini-cli-core';
import mock from 'mock-fs';
import { FileCommandLoader } from './FileCommandLoader.js';
import { assert, vi } from 'vitest';
Expand All @@ -21,7 +26,7 @@ import {
ShellProcessor,
} from './prompt-processors/shellProcessor.js';
import { DefaultArgumentProcessor } from './prompt-processors/argumentProcessor.js';
import type { CommandContext } from '../ui/commands/types.js';
import { CommandKind, type CommandContext } from '../ui/commands/types.js';
import { AtFileProcessor } from './prompt-processors/atFileProcessor.js';

const mockShellProcess = vi.hoisted(() => vi.fn());
Expand Down Expand Up @@ -315,6 +320,34 @@ describe('FileCommandLoader', () => {
}
});

it('does not duplicate commands when project root is the home directory', async () => {
const homeDir = homedir();
const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'test.toml': 'prompt = "User prompt"',
'another.toml': 'prompt = "Another prompt"',
},
});

const mockConfig = {
getProjectRoot: vi.fn(() => homeDir),
getExtensions: vi.fn(() => []),
getFolderTrust: vi.fn(() => false),
isTrustedFolder: vi.fn(() => false),
} as unknown as Config;
const loader = new FileCommandLoader(mockConfig);
const commands = await loader.loadCommands(signal);

// Should load each command only once (as user commands), not twice
expect(commands).toHaveLength(2);
const names = commands.map((c) => c.name);
expect(names).toContain('test');
expect(names).toContain('another');
// Verify they are loaded as user commands, not duplicated as workspace commands
expect(commands.every((c) => c.kind === CommandKind.USER_FILE)).toBe(true);
});

it('ignores files with TOML syntax errors', async () => {
const userCommandsDir = Storage.getUserCommandsDir();
mock({
Expand Down
16 changes: 10 additions & 6 deletions packages/cli/src/services/FileCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,20 @@ export class FileCommandLoader implements ICommandLoader {
const storage = this.config?.storage ?? new Storage(this.projectRoot);

// 1. User commands
const userCommandsDir = Storage.getUserCommandsDir();
dirs.push({
path: Storage.getUserCommandsDir(),
path: userCommandsDir,
kind: CommandKind.USER_FILE,
});

// 2. Project commands
dirs.push({
path: storage.getProjectCommandsDir(),
kind: CommandKind.WORKSPACE_FILE,
});
// 2. Project commands (skip if same directory as user commands, e.g. when
// cwd is the user's home directory, to avoid false conflict warnings)
if (!storage.isWorkspaceHomeDir()) {
dirs.push({
path: storage.getProjectCommandsDir(),
kind: CommandKind.WORKSPACE_FILE,
});
}

// 3. Extension commands (processed last to detect all conflicts)
if (this.config) {
Expand Down
Loading