Files
gemini-cli/packages/cli/src/config/settings_validation_warning.test.ts

164 lines
4.3 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/// <reference types="vitest/globals" />
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import * as fs from 'node:fs';
const mockCoreEvents = vi.hoisted(() => ({
emitFeedback: vi.fn(),
emitConsoleLog: vi.fn(),
emitOutput: vi.fn(),
emitModelChanged: vi.fn(),
drainBacklogs: vi.fn(),
}));
const mockIsWorkspaceTrusted = vi.hoisted(() =>
vi.fn().mockReturnValue({ isTrusted: true, source: 'file' }),
);
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: mockCoreEvents,
homedir: () => '/mock/home/user',
Storage: class extends actual.Storage {
static override getGlobalSettingsPath = () =>
'/mock/home/user/.gemini/settings.json';
override getWorkspaceSettingsPath = () =>
'/mock/workspace/.gemini/settings.json';
static override getGlobalGeminiDir = () => '/mock/home/user/.gemini';
},
};
});
vi.mock('./trustedFolders.js', () => ({
isWorkspaceTrusted: mockIsWorkspaceTrusted,
loadTrustedFolders: vi.fn().mockReturnValue({
isPathTrusted: vi.fn().mockReturnValue(true),
user: { config: {} },
errors: [],
}),
isFolderTrustEnabled: vi.fn().mockReturnValue(false),
TrustLevel: {
TRUST_FOLDER: 'TRUST_FOLDER',
TRUST_PARENT: 'TRUST_PARENT',
DO_NOT_TRUST: 'DO_NOT_TRUST',
},
}));
vi.mock('os', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:os')>();
return {
...actual,
homedir: () => '/mock/home/user',
platform: () => 'linux',
totalmem: () => 16 * 1024 * 1024 * 1024,
};
});
vi.mock('fs', async (importOriginal) => {
const actualFs = await importOriginal<typeof fs>();
return {
...actualFs,
existsSync: vi.fn(),
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
mkdirSync: vi.fn(),
renameSync: vi.fn(),
realpathSync: (p: string) => p,
};
});
// Import loadSettings after all mocks are defined
import {
loadSettings,
USER_SETTINGS_PATH,
type LoadedSettings,
resetSettingsCacheForTesting,
} from './settings.js';
const MOCK_WORKSPACE_DIR = '/mock/workspace';
describe('Settings Validation Warning', () => {
beforeEach(() => {
vi.clearAllMocks();
resetSettingsCacheForTesting();
(fs.readFileSync as Mock).mockReturnValue('{}');
(fs.existsSync as Mock).mockReturnValue(false);
});
it('should emit a warning and NOT throw when settings are invalid', () => {
(fs.existsSync as Mock).mockImplementation(
(p: string) => p === USER_SETTINGS_PATH,
);
const invalidSettingsContent = {
ui: {
customThemes: {
terafox: {
name: 'terafox',
type: 'custom',
DiffModified: '#ffffff', // Invalid key
},
},
},
};
(fs.readFileSync as Mock).mockImplementation((p: string) => {
if (p === USER_SETTINGS_PATH)
return JSON.stringify(invalidSettingsContent);
return '{}';
});
// Should NOT throw
let settings: LoadedSettings | undefined;
expect(() => {
settings = loadSettings(MOCK_WORKSPACE_DIR);
}).not.toThrow();
// Should have recorded a warning in the settings object
expect(
settings?.errors.some((e) =>
e.message.includes("Unrecognized key(s) in object: 'DiffModified'"),
),
).toBe(true);
});
it('should throw a fatal error when settings file is not a valid JSON object', () => {
(fs.existsSync as Mock).mockImplementation(
(p: string) => p === USER_SETTINGS_PATH,
);
(fs.readFileSync as Mock).mockImplementation((p: string) => {
if (p === USER_SETTINGS_PATH) return '[]';
return '{}';
});
expect(() => {
loadSettings(MOCK_WORKSPACE_DIR);
}).toThrow();
});
it('should throw a fatal error when settings file contains invalid JSON', () => {
(fs.existsSync as Mock).mockImplementation(
(p: string) => p === USER_SETTINGS_PATH,
);
(fs.readFileSync as Mock).mockImplementation((p: string) => {
if (p === USER_SETTINGS_PATH) return '{ "invalid": "json", }'; // Trailing comma is invalid in standard JSON
return '{}';
});
expect(() => {
loadSettings(MOCK_WORKSPACE_DIR);
}).toThrow();
});
});