summaryrefslogtreecommitdiff
path: root/packages/cli/src/services/FileCommandLoader.test.ts
diff options
context:
space:
mode:
authorYuki Okita <[email protected]>2025-08-20 10:55:47 +0900
committerGitHub <[email protected]>2025-08-20 01:55:47 +0000
commit21c6480b65528a98ac0e1e3855f3c78c1f9b7cbe (patch)
tree5555ec429209e87e0c21483c9e5fddd53ac01dbc /packages/cli/src/services/FileCommandLoader.test.ts
parent1049d388451120587a8643a401fd71430a8cd5fe (diff)
Refac: Centralize storage file management (#4078)
Co-authored-by: Taylor Mullen <[email protected]>
Diffstat (limited to 'packages/cli/src/services/FileCommandLoader.test.ts')
-rw-r--r--packages/cli/src/services/FileCommandLoader.test.ts89
1 files changed, 46 insertions, 43 deletions
diff --git a/packages/cli/src/services/FileCommandLoader.test.ts b/packages/cli/src/services/FileCommandLoader.test.ts
index 42d93074..9960a632 100644
--- a/packages/cli/src/services/FileCommandLoader.test.ts
+++ b/packages/cli/src/services/FileCommandLoader.test.ts
@@ -5,11 +5,7 @@
*/
import * as path from 'node:path';
-import {
- Config,
- getProjectCommandsDir,
- getUserCommandsDir,
-} from '@google/gemini-cli-core';
+import { Config, Storage } from '@google/gemini-cli-core';
import mock from 'mock-fs';
import { FileCommandLoader } from './FileCommandLoader.js';
import { assert, vi } from 'vitest';
@@ -57,6 +53,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...original,
+ Storage: original.Storage,
isCommandAllowed: vi.fn(),
ShellExecutionService: {
execute: vi.fn(),
@@ -86,7 +83,7 @@ describe('FileCommandLoader', () => {
});
it('loads a single command from a file', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'test.toml': 'prompt = "This is a test prompt"',
@@ -127,7 +124,7 @@ describe('FileCommandLoader', () => {
itif(process.platform !== 'win32')(
'loads commands from a symlinked directory',
async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
const realCommandsDir = '/real/commands';
mock({
[realCommandsDir]: {
@@ -152,7 +149,7 @@ describe('FileCommandLoader', () => {
itif(process.platform !== 'win32')(
'loads commands from a symlinked subdirectory',
async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
const realNamespacedDir = '/real/namespaced-commands';
mock({
[userCommandsDir]: {
@@ -176,7 +173,7 @@ describe('FileCommandLoader', () => {
);
it('loads multiple commands', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'test1.toml': 'prompt = "Prompt 1"',
@@ -191,7 +188,7 @@ describe('FileCommandLoader', () => {
});
it('creates deeply nested namespaces correctly', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
@@ -205,7 +202,7 @@ describe('FileCommandLoader', () => {
const mockConfig = {
getProjectRoot: vi.fn(() => '/path/to/project'),
getExtensions: vi.fn(() => []),
- } as unknown as Config;
+ } as Config;
const loader = new FileCommandLoader(mockConfig);
const commands = await loader.loadCommands(signal);
expect(commands).toHaveLength(1);
@@ -213,7 +210,7 @@ describe('FileCommandLoader', () => {
});
it('creates namespaces from nested directories', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
git: {
@@ -232,8 +229,10 @@ describe('FileCommandLoader', () => {
});
it('returns both user and project commands in order', async () => {
- const userCommandsDir = getUserCommandsDir();
- const projectCommandsDir = getProjectCommandsDir(process.cwd());
+ const userCommandsDir = Storage.getUserCommandsDir();
+ const projectCommandsDir = new Storage(
+ process.cwd(),
+ ).getProjectCommandsDir();
mock({
[userCommandsDir]: {
'test.toml': 'prompt = "User prompt"',
@@ -246,7 +245,7 @@ describe('FileCommandLoader', () => {
const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => []),
- } as unknown as Config;
+ } as Config;
const loader = new FileCommandLoader(mockConfig);
const commands = await loader.loadCommands(signal);
@@ -284,7 +283,7 @@ describe('FileCommandLoader', () => {
});
it('ignores files with TOML syntax errors', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'invalid.toml': 'this is not valid toml',
@@ -300,7 +299,7 @@ describe('FileCommandLoader', () => {
});
it('ignores files that are semantically invalid (missing prompt)', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'no_prompt.toml': 'description = "This file is missing a prompt"',
@@ -316,7 +315,7 @@ describe('FileCommandLoader', () => {
});
it('handles filename edge cases correctly', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'test.v1.toml': 'prompt = "Test prompt"',
@@ -338,7 +337,7 @@ describe('FileCommandLoader', () => {
});
it('uses a default description if not provided', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'test.toml': 'prompt = "Test prompt"',
@@ -353,7 +352,7 @@ describe('FileCommandLoader', () => {
});
it('uses the provided description', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'test.toml': 'prompt = "Test prompt"\ndescription = "My test command"',
@@ -368,7 +367,7 @@ describe('FileCommandLoader', () => {
});
it('should sanitize colons in filenames to prevent namespace conflicts', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'legacy:command.toml': 'prompt = "This is a legacy command"',
@@ -388,7 +387,7 @@ describe('FileCommandLoader', () => {
describe('Processor Instantiation Logic', () => {
it('instantiates only DefaultArgumentProcessor if no {{args}} or !{} are present', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'simple.toml': `prompt = "Just a regular prompt"`,
@@ -403,7 +402,7 @@ describe('FileCommandLoader', () => {
});
it('instantiates only ShellProcessor if {{args}} is present (but not !{})', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'args.toml': `prompt = "Prompt with {{args}}"`,
@@ -418,7 +417,7 @@ describe('FileCommandLoader', () => {
});
it('instantiates ShellProcessor and DefaultArgumentProcessor if !{} is present (but not {{args}})', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'shell.toml': `prompt = "Prompt with !{cmd}"`,
@@ -433,7 +432,7 @@ describe('FileCommandLoader', () => {
});
it('instantiates only ShellProcessor if both {{args}} and !{} are present', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'both.toml': `prompt = "Prompt with {{args}} and !{cmd}"`,
@@ -450,8 +449,10 @@ describe('FileCommandLoader', () => {
describe('Extension Command Loading', () => {
it('loads commands from active extensions', async () => {
- const userCommandsDir = getUserCommandsDir();
- const projectCommandsDir = getProjectCommandsDir(process.cwd());
+ const userCommandsDir = Storage.getUserCommandsDir();
+ const projectCommandsDir = new Storage(
+ process.cwd(),
+ ).getProjectCommandsDir();
const extensionDir = path.join(
process.cwd(),
'.gemini/extensions/test-ext',
@@ -485,7 +486,7 @@ describe('FileCommandLoader', () => {
path: extensionDir,
},
]),
- } as unknown as Config;
+ } as Config;
const loader = new FileCommandLoader(mockConfig);
const commands = await loader.loadCommands(signal);
@@ -499,8 +500,10 @@ describe('FileCommandLoader', () => {
});
it('extension commands have extensionName metadata for conflict resolution', async () => {
- const userCommandsDir = getUserCommandsDir();
- const projectCommandsDir = getProjectCommandsDir(process.cwd());
+ const userCommandsDir = Storage.getUserCommandsDir();
+ const projectCommandsDir = new Storage(
+ process.cwd(),
+ ).getProjectCommandsDir();
const extensionDir = path.join(
process.cwd(),
'.gemini/extensions/test-ext',
@@ -534,7 +537,7 @@ describe('FileCommandLoader', () => {
path: extensionDir,
},
]),
- } as unknown as Config;
+ } as Config;
const loader = new FileCommandLoader(mockConfig);
const commands = await loader.loadCommands(signal);
@@ -641,7 +644,7 @@ describe('FileCommandLoader', () => {
path: extensionDir2,
},
]),
- } as unknown as Config;
+ } as Config;
const loader = new FileCommandLoader(mockConfig);
const commands = await loader.loadCommands(signal);
@@ -677,7 +680,7 @@ describe('FileCommandLoader', () => {
path: extensionDir,
},
]),
- } as unknown as Config;
+ } as Config;
const loader = new FileCommandLoader(mockConfig);
const commands = await loader.loadCommands(signal);
expect(commands).toHaveLength(0);
@@ -709,7 +712,7 @@ describe('FileCommandLoader', () => {
getExtensions: vi.fn(() => [
{ name: 'a', version: '1.0.0', isActive: true, path: extensionDir },
]),
- } as unknown as Config;
+ } as Config;
const loader = new FileCommandLoader(mockConfig);
const commands = await loader.loadCommands(signal);
@@ -742,7 +745,7 @@ describe('FileCommandLoader', () => {
describe('Argument Handling Integration (via ShellProcessor)', () => {
it('correctly processes a command with {{args}}', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'shorthand.toml':
@@ -774,7 +777,7 @@ describe('FileCommandLoader', () => {
describe('Default Argument Processor Integration', () => {
it('correctly processes a command without {{args}}', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'model_led.toml':
@@ -808,7 +811,7 @@ describe('FileCommandLoader', () => {
describe('Shell Processor Integration', () => {
it('instantiates ShellProcessor if {{args}} is present (even without shell trigger)', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'args_only.toml': `prompt = "Hello {{args}}"`,
@@ -821,7 +824,7 @@ describe('FileCommandLoader', () => {
expect(ShellProcessor).toHaveBeenCalledWith('args_only');
});
it('instantiates ShellProcessor if the trigger is present', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'shell.toml': `prompt = "Run this: ${SHELL_INJECTION_TRIGGER}echo hello}"`,
@@ -835,7 +838,7 @@ describe('FileCommandLoader', () => {
});
it('does not instantiate ShellProcessor if no triggers ({{args}} or !{}) are present', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'regular.toml': `prompt = "Just a regular prompt"`,
@@ -849,7 +852,7 @@ describe('FileCommandLoader', () => {
});
it('returns a "submit_prompt" action if shell processing succeeds', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'shell.toml': `prompt = "Run !{echo 'hello'}"`,
@@ -876,7 +879,7 @@ describe('FileCommandLoader', () => {
});
it('returns a "confirm_shell_commands" action if shell processing requires it', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
const rawInvocation = '/shell rm -rf /';
mock({
[userCommandsDir]: {
@@ -910,7 +913,7 @@ describe('FileCommandLoader', () => {
});
it('re-throws other errors from the processor', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'shell.toml': `prompt = "Run !{something}"`,
@@ -935,7 +938,7 @@ describe('FileCommandLoader', () => {
).rejects.toThrow('Something else went wrong');
});
it('assembles the processor pipeline in the correct order (Shell -> Default)', async () => {
- const userCommandsDir = getUserCommandsDir();
+ const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
// This prompt uses !{} but NOT {{args}}, so both processors should be active.