mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Refac: Centralize storage file management (#4078)
Co-authored-by: Taylor Mullen <ntaylormullen@google.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user