Exit with an error message if parsing settings.json fails. (#747)

This commit is contained in:
Jacob Richman
2025-06-06 09:56:45 -07:00
committed by GitHub
parent b4a6b16227
commit 89aca349cf
6 changed files with 246 additions and 32 deletions

View File

@@ -96,6 +96,7 @@ describe('Settings Loading and Merging', () => {
expect(settings.user.settings).toEqual({});
expect(settings.workspace.settings).toEqual({});
expect(settings.merged).toEqual({});
expect(settings.errors.length).toBe(0);
});
it('should load user settings if only user file exists', () => {
@@ -300,27 +301,61 @@ describe('Settings Loading and Merging', () => {
});
it('should handle JSON parsing errors gracefully', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
(mockFsExistsSync as Mock).mockReturnValue(true); // Both files "exist"
const invalidJsonContent = 'invalid json';
const userReadError = new SyntaxError(
"Expected ',' or '}' after property value in JSON at position 10",
);
const workspaceReadError = new SyntaxError(
'Unexpected token i in JSON at position 0',
);
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
// Make it return invalid json for the paths it will try to read
if (p === USER_SETTINGS_PATH || p === MOCK_WORKSPACE_SETTINGS_PATH)
return 'invalid json';
return '';
if (p === USER_SETTINGS_PATH) {
// Simulate JSON.parse throwing for user settings
vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
throw userReadError;
});
return invalidJsonContent; // Content that would cause JSON.parse to throw
}
if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
// Simulate JSON.parse throwing for workspace settings
vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
throw workspaceReadError;
});
return invalidJsonContent;
}
return '{}'; // Default for other reads
},
);
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const settings = loadSettings(MOCK_WORKSPACE_DIR);
// Check that settings are empty due to parsing errors
expect(settings.user.settings).toEqual({});
expect(settings.workspace.settings).toEqual({});
expect(settings.merged).toEqual({});
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
consoleErrorSpy.mockRestore();
// Check that error objects are populated in settings.errors
expect(settings.errors).toBeDefined();
// Assuming both user and workspace files cause errors and are added in order
expect(settings.errors.length).toEqual(2);
const userError = settings.errors.find(
(e) => e.path === USER_SETTINGS_PATH,
);
expect(userError).toBeDefined();
expect(userError?.message).toBe(userReadError.message);
const workspaceError = settings.errors.find(
(e) => e.path === MOCK_WORKSPACE_SETTINGS_PATH,
);
expect(workspaceError).toBeDefined();
expect(workspaceError?.message).toBe(workspaceReadError.message);
// Restore JSON.parse mock if it was spied on specifically for this test
vi.restoreAllMocks(); // Or more targeted restore if needed
});
it('should resolve environment variables in user settings', () => {
@@ -585,13 +620,14 @@ describe('Settings Loading and Merging', () => {
'MY_AGENTS.md',
);
expect(loadedSettings.merged.contextFileName).toBe('MY_AGENTS.md');
expect(loadedSettings.merged.theme).toBe('matrix');
expect(loadedSettings.merged.theme).toBe('matrix'); // User setting should still be there
expect(fs.writeFileSync).toHaveBeenCalledWith(
MOCK_WORKSPACE_SETTINGS_PATH,
JSON.stringify({ contextFileName: 'MY_AGENTS.md' }, null, 2),
'utf-8',
);
// Workspace theme overrides user theme
loadedSettings.setValue(SettingScope.Workspace, 'theme', 'ocean');
expect(loadedSettings.workspace.settings.theme).toBe('ocean');

View File

@@ -7,7 +7,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { homedir } from 'os';
import { MCPServerConfig } from '@gemini-code/core';
import { MCPServerConfig, getErrorMessage } from '@gemini-code/core';
import stripJsonComments from 'strip-json-comments';
import { DefaultLight } from '../ui/themes/default-light.js';
import { DefaultDark } from '../ui/themes/default.js';
@@ -47,19 +47,30 @@ export interface Settings {
// Add other settings here.
}
export interface SettingsError {
message: string;
path: string;
}
export interface SettingsFile {
settings: Settings;
path: string;
}
export class LoadedSettings {
constructor(user: SettingsFile, workspace: SettingsFile) {
constructor(
user: SettingsFile,
workspace: SettingsFile,
errors: SettingsError[],
) {
this.user = user;
this.workspace = workspace;
this.errors = errors;
this._merged = this.computeMergedSettings();
}
readonly user: SettingsFile;
readonly workspace: SettingsFile;
readonly errors: SettingsError[];
private _merged: Settings;
@@ -147,6 +158,7 @@ function resolveEnvVarsInObject<T>(obj: T): T {
export function loadSettings(workspaceDir: string): LoadedSettings {
let userSettings: Settings = {};
let workspaceSettings: Settings = {};
const settingsErrors: SettingsError[] = [];
// Load user settings
try {
@@ -163,8 +175,11 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
userSettings.theme = DefaultDark.name;
}
}
} catch (error) {
console.error('Error reading user settings file:', error);
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: USER_SETTINGS_PATH,
});
}
const workspaceSettingsPath = path.join(
@@ -190,13 +205,23 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
workspaceSettings.theme = DefaultDark.name;
}
}
} catch (error) {
console.error('Error reading workspace settings file:', error);
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: workspaceSettingsPath,
});
}
return new LoadedSettings(
{ path: USER_SETTINGS_PATH, settings: userSettings },
{ path: workspaceSettingsPath, settings: workspaceSettings },
{
path: USER_SETTINGS_PATH,
settings: userSettings,
},
{
path: workspaceSettingsPath,
settings: workspaceSettings,
},
settingsErrors,
);
}