Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/cli/src/config/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3003,6 +3003,44 @@ describe('Settings Loading and Merging', () => {
expect(snap1).toBe(snap2);
});

it('getSnapshot() should preserve readOnly metadata for each scope', () => {
const readonlySettings = new LoadedSettings(
{
path: getSystemSettingsPath(),
settings: {},
originalSettings: {},
readOnly: true,
},
{
path: getSystemDefaultsPath(),
settings: {},
originalSettings: {},
readOnly: true,
},
{
path: USER_SETTINGS_PATH,
settings: {},
originalSettings: {},
readOnly: false,
},
{
path: MOCK_WORKSPACE_SETTINGS_PATH,
settings: {},
originalSettings: {},
readOnly: true,
},
true,
[],
);

const snapshot = readonlySettings.getSnapshot();

expect(snapshot.system.readOnly).toBe(true);
expect(snapshot.systemDefaults.readOnly).toBe(true);
expect(snapshot.user.readOnly).toBe(false);
expect(snapshot.workspace.readOnly).toBe(true);
});

it('setValue() should create a new snapshot reference and emit event', () => {
const oldSnapshot = loadedSettings.getSnapshot();
const oldUserRef = oldSnapshot.user.settings;
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,7 @@ export class LoadedSettings {

private computeSnapshot(): LoadedSettingsSnapshot {
const cloneSettingsFile = (file: SettingsFile): SettingsFile => ({
path: file.path,
rawJson: file.rawJson,
...file,
settings: structuredClone(file.settings),
originalSettings: structuredClone(file.originalSettings),
});
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/test-utils/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface MockSettingsFile {
settings: any;
originalSettings: any;
path: string;
readOnly?: boolean;
}

interface CreateMockSettingsOptions {
Expand Down
141 changes: 140 additions & 1 deletion packages/cli/src/ui/components/SettingsDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import { waitFor } from '../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SettingsDialog } from './SettingsDialog.js';
import { SettingScope } from '../../config/settings.js';
import { createMockSettings } from '../../test-utils/settings.js';
import {
createMockSettings,
type MockSettingsFile,
} from '../../test-utils/settings.js';
import { makeFakeConfig } from '@google/gemini-cli-core';
import { act } from 'react';
import { TEST_ONLY } from '../../utils/settingsUtils.js';
Expand Down Expand Up @@ -250,6 +253,17 @@ const renderDialog = async (
},
);

const createSettingsFile = (
path: string,
settings: Record<string, unknown> = {},
readOnly?: boolean,
): MockSettingsFile => ({
settings,
originalSettings: settings,
path,
readOnly,
});

describe('SettingsDialog', () => {
beforeEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -588,6 +602,131 @@ describe('SettingsDialog', () => {

unmount();
});

it('should not offer read-only system settings as an editable target', async () => {
const settings = createMockSettings({
system: createSettingsFile('', {}, true),
});
const onSelect = vi.fn();

const { lastFrame, unmount } = await renderDialog(settings, onSelect);

await waitFor(() => {
expect(lastFrame()).toContain('Apply To');
});

const output = lastFrame();
expect(output).toContain('User Settings');
expect(output).toContain('Workspace Settings');
expect(output).not.toContain('System Settings');

unmount();
});

it('should not offer a read-only home-directory workspace as an editable target', async () => {
const settings = createMockSettings({
user: createSettingsFile('/mock/home/.gemini/settings.json'),
system: createSettingsFile('/mock/system/settings.json', {}, true),
systemDefaults: createSettingsFile(
'/mock/system-defaults/settings.json',
{},
true,
),
workspace: createSettingsFile('', {}, true),
});
const setValueSpy = vi.spyOn(settings, 'setValue');
const onSelect = vi.fn();

const { lastFrame, stdin, unmount, waitUntilReady } = await renderDialog(
settings,
onSelect,
);

await waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});

const output = lastFrame();
expect(output).not.toContain('Workspace Settings');
expect(output).not.toContain('System Settings');

await act(async () => {
stdin.write(TerminalKeys.ENTER as string);
});
await waitUntilReady();

expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'general.vimMode',
true,
);

unmount();
});

it('should fall back to the first writable scope when the selected scope is read-only', async () => {
const settings = createMockSettings({
user: createSettingsFile('', {}, true),
system: createSettingsFile('', {}, true),
workspace: createSettingsFile('/mock/workspace/.gemini/settings.json'),
});
const setValueSpy = vi.spyOn(settings, 'setValue');
const onSelect = vi.fn();

const { lastFrame, stdin, unmount, waitUntilReady } = await renderDialog(
settings,
onSelect,
);

await waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});

await act(async () => {
stdin.write(TerminalKeys.ENTER as string);
});
await waitUntilReady();

expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.Workspace,
'general.vimMode',
true,
);

unmount();
});

it('should not save when all editable scopes are read-only', async () => {
const settings = createMockSettings({
user: createSettingsFile('', {}, true),
system: createSettingsFile('', {}, true),
workspace: createSettingsFile(
'/mock/workspace/.gemini/settings.json',
{},
true,
),
});
const setValueSpy = vi.spyOn(settings, 'setValue');
const onSelect = vi.fn();

const { lastFrame, stdin, unmount, waitUntilReady } = await renderDialog(
settings,
onSelect,
);

await waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});

await act(async () => {
stdin.write(TerminalKeys.ENTER as string);
});
await waitUntilReady();

expect(setValueSpy).not.toHaveBeenCalled();

unmount();
});
});

describe('Restart Prompt', () => {
Expand Down
Loading
Loading