Files
gemini-cli/packages/core/src/config/storage.test.ts
2026-02-06 16:10:17 +00:00

216 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest';
vi.unmock('./storage.js');
vi.unmock('./projectRegistry.js');
vi.unmock('./storageMigration.js');
import * as os from 'node:os';
import * as path from 'node:path';
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
return {
...actual,
mkdirSync: vi.fn(),
};
});
import { Storage } from './storage.js';
import { GEMINI_DIR, homedir } from '../utils/paths.js';
import { ProjectRegistry } from './projectRegistry.js';
import { StorageMigration } from './storageMigration.js';
const PROJECT_SLUG = 'project-slug';
vi.mock('./projectRegistry.js');
vi.mock('./storageMigration.js');
describe('Storage initialize', () => {
const projectRoot = '/tmp/project';
let storage: Storage;
beforeEach(() => {
ProjectRegistry.prototype.initialize = vi.fn().mockResolvedValue(undefined);
ProjectRegistry.prototype.getShortId = vi
.fn()
.mockReturnValue(PROJECT_SLUG);
storage = new Storage(projectRoot);
vi.clearAllMocks();
// Mock StorageMigration.migrateDirectory
vi.mocked(StorageMigration.migrateDirectory).mockResolvedValue(undefined);
});
it('sets up the registry and performs migration if `getProjectTempDir` is called', async () => {
await storage.initialize();
expect(storage.getProjectTempDir()).toBe(
path.join(os.homedir(), GEMINI_DIR, 'tmp', PROJECT_SLUG),
);
// Verify registry initialization
expect(ProjectRegistry).toHaveBeenCalled();
expect(vi.mocked(ProjectRegistry).prototype.initialize).toHaveBeenCalled();
expect(
vi.mocked(ProjectRegistry).prototype.getShortId,
).toHaveBeenCalledWith(projectRoot);
// Verify migration calls
const shortId = 'project-slug';
// We can't easily get the hash here without repeating logic, but we can verify it's called twice
expect(StorageMigration.migrateDirectory).toHaveBeenCalledTimes(2);
// Verify identifier is set by checking a path
expect(storage.getProjectTempDir()).toContain(shortId);
});
});
vi.mock('../utils/paths.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../utils/paths.js')>();
return {
...actual,
homedir: vi.fn(actual.homedir),
};
});
describe('Storage getGlobalSettingsPath', () => {
it('returns path to ~/.gemini/settings.json', () => {
const expected = path.join(os.homedir(), GEMINI_DIR, 'settings.json');
expect(Storage.getGlobalSettingsPath()).toBe(expected);
});
});
describe('Storage - Security', () => {
it('falls back to tmp for gemini but returns empty for agents if the home directory cannot be determined', () => {
vi.mocked(homedir).mockReturnValue('');
// .gemini falls back for backward compatibility
expect(Storage.getGlobalGeminiDir()).toBe(
path.join(os.tmpdir(), GEMINI_DIR),
);
// .agents returns empty to avoid insecure fallback WITHOUT throwing error
expect(Storage.getGlobalAgentsDir()).toBe('');
vi.mocked(homedir).mockReturnValue(os.homedir());
});
});
describe('Storage additional helpers', () => {
const projectRoot = '/tmp/project';
const storage = new Storage(projectRoot);
it('getWorkspaceSettingsPath returns project/.gemini/settings.json', () => {
const expected = path.join(projectRoot, GEMINI_DIR, 'settings.json');
expect(storage.getWorkspaceSettingsPath()).toBe(expected);
});
it('getUserCommandsDir returns ~/.gemini/commands', () => {
const expected = path.join(os.homedir(), GEMINI_DIR, 'commands');
expect(Storage.getUserCommandsDir()).toBe(expected);
});
it('getProjectCommandsDir returns project/.gemini/commands', () => {
const expected = path.join(projectRoot, GEMINI_DIR, 'commands');
expect(storage.getProjectCommandsDir()).toBe(expected);
});
it('getUserSkillsDir returns ~/.gemini/skills', () => {
const expected = path.join(os.homedir(), GEMINI_DIR, 'skills');
expect(Storage.getUserSkillsDir()).toBe(expected);
});
it('getProjectSkillsDir returns project/.gemini/skills', () => {
const expected = path.join(projectRoot, GEMINI_DIR, 'skills');
expect(storage.getProjectSkillsDir()).toBe(expected);
});
it('getUserAgentsDir returns ~/.gemini/agents', () => {
const expected = path.join(os.homedir(), GEMINI_DIR, 'agents');
expect(Storage.getUserAgentsDir()).toBe(expected);
});
it('getProjectAgentsDir returns project/.gemini/agents', () => {
const expected = path.join(projectRoot, GEMINI_DIR, 'agents');
expect(storage.getProjectAgentsDir()).toBe(expected);
});
it('getMcpOAuthTokensPath returns ~/.gemini/mcp-oauth-tokens.json', () => {
const expected = path.join(
os.homedir(),
GEMINI_DIR,
'mcp-oauth-tokens.json',
);
expect(Storage.getMcpOAuthTokensPath()).toBe(expected);
});
it('getGlobalBinDir returns ~/.gemini/tmp/bin', () => {
const expected = path.join(os.homedir(), GEMINI_DIR, 'tmp', 'bin');
expect(Storage.getGlobalBinDir()).toBe(expected);
});
it('getProjectTempPlansDir returns ~/.gemini/tmp/<identifier>/plans', async () => {
await storage.initialize();
const tempDir = storage.getProjectTempDir();
const expected = path.join(tempDir, 'plans');
expect(storage.getProjectTempPlansDir()).toBe(expected);
});
});
describe('Storage - System Paths', () => {
const originalEnv = process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
afterEach(() => {
if (originalEnv !== undefined) {
process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] = originalEnv;
} else {
delete process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
}
});
it('getSystemSettingsPath returns correct path based on platform (default)', () => {
delete process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
const platform = os.platform();
const result = Storage.getSystemSettingsPath();
if (platform === 'darwin') {
expect(result).toBe(
'/Library/Application Support/GeminiCli/settings.json',
);
} else if (platform === 'win32') {
expect(result).toBe('C:\\ProgramData\\gemini-cli\\settings.json');
} else {
expect(result).toBe('/etc/gemini-cli/settings.json');
}
});
it('getSystemSettingsPath follows GEMINI_CLI_SYSTEM_SETTINGS_PATH if set', () => {
const customPath = '/custom/path/settings.json';
process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] = customPath;
expect(Storage.getSystemSettingsPath()).toBe(customPath);
});
it('getSystemPoliciesDir returns correct path based on platform and ignores env var', () => {
process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] =
'/custom/path/settings.json';
const platform = os.platform();
const result = Storage.getSystemPoliciesDir();
expect(result).not.toContain('/custom/path');
if (platform === 'darwin') {
expect(result).toBe('/Library/Application Support/GeminiCli/policies');
} else if (platform === 'win32') {
expect(result).toBe('C:\\ProgramData\\gemini-cli\\policies');
} else {
expect(result).toBe('/etc/gemini-cli/policies');
}
});
});