mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0
This commit is contained in:
@@ -4,9 +4,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { getProjectTempDir } from '@qwen-code/qwen-code-core';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { Storage } from '@qwen-code/qwen-code-core';
|
||||
|
||||
const cleanupFunctions: Array<(() => void) | (() => Promise<void>)> = [];
|
||||
|
||||
@@ -26,7 +26,8 @@ export async function runExitCleanup() {
|
||||
}
|
||||
|
||||
export async function cleanupCheckpoints() {
|
||||
const tempDir = getProjectTempDir(process.cwd());
|
||||
const storage = new Storage(process.cwd());
|
||||
const tempDir = storage.getProjectTempDir();
|
||||
const checkpointsDir = join(tempDir, 'checkpoints');
|
||||
try {
|
||||
await fs.rm(checkpointsDir, { recursive: true, force: true });
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { SettingScope, LoadedSettings } from '../config/settings.js';
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
import { SettingScope } from '../config/settings.js';
|
||||
import { settingExistsInScope } from './settingsUtils.js';
|
||||
|
||||
/**
|
||||
|
||||
12
packages/cli/src/utils/errors.ts
Normal file
12
packages/cli/src/utils/errors.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
export enum AppEvent {
|
||||
OpenDebugConsole = 'open-debug-console',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { vi, describe, expect, it, afterEach, beforeEach } from 'vitest';
|
||||
import * as child_process from 'child_process';
|
||||
import * as child_process from 'node:child_process';
|
||||
import {
|
||||
isGitHubRepository,
|
||||
getGitRepoRoot,
|
||||
@@ -120,7 +120,7 @@ describe('getLatestRelease', async () => {
|
||||
|
||||
it('throws an error if the fetch fails', async () => {
|
||||
global.fetch = vi.fn(() => Promise.reject('nope'));
|
||||
expect(getLatestGitHubRelease()).rejects.toThrowError(
|
||||
await expect(getLatestGitHubRelease()).rejects.toThrowError(
|
||||
/Unable to determine the latest/,
|
||||
);
|
||||
});
|
||||
@@ -132,7 +132,7 @@ describe('getLatestRelease', async () => {
|
||||
json: () => Promise.resolve({ foo: 'bar' }),
|
||||
} as Response),
|
||||
);
|
||||
expect(getLatestGitHubRelease()).rejects.toThrowError(
|
||||
await expect(getLatestGitHubRelease()).rejects.toThrowError(
|
||||
/Unable to determine the latest/,
|
||||
);
|
||||
});
|
||||
@@ -144,6 +144,6 @@ describe('getLatestRelease', async () => {
|
||||
json: () => Promise.resolve({ tag_name: 'v1.2.3' }),
|
||||
} as Response),
|
||||
);
|
||||
expect(getLatestGitHubRelease()).resolves.toBe('v1.2.3');
|
||||
await expect(getLatestGitHubRelease()).resolves.toBe('v1.2.3');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { ProxyAgent } from 'undici';
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
|
||||
import type { Mock } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { getInstallationInfo, PackageManager } from './installationInfo.js';
|
||||
import { updateEventEmitter } from './updateEventEmitter.js';
|
||||
import { UpdateObject } from '../ui/utils/updateCheck.js';
|
||||
import { LoadedSettings } from '../config/settings.js';
|
||||
import type { UpdateObject } from '../ui/utils/updateCheck.js';
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
import EventEmitter from 'node:events';
|
||||
import { handleAutoUpdate } from './handleAutoUpdate.js';
|
||||
|
||||
@@ -63,7 +64,9 @@ describe('handleAutoUpdate', () => {
|
||||
|
||||
mockSettings = {
|
||||
merged: {
|
||||
disableAutoUpdate: false,
|
||||
general: {
|
||||
disableAutoUpdate: false,
|
||||
},
|
||||
},
|
||||
} as LoadedSettings;
|
||||
|
||||
@@ -92,7 +95,7 @@ describe('handleAutoUpdate', () => {
|
||||
});
|
||||
|
||||
it('should do nothing if update nag is disabled', () => {
|
||||
mockSettings.merged.disableUpdateNag = true;
|
||||
mockSettings.merged.general!.disableUpdateNag = true;
|
||||
handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);
|
||||
expect(mockGetInstallationInfo).not.toHaveBeenCalled();
|
||||
expect(mockUpdateEventEmitter.emit).not.toHaveBeenCalled();
|
||||
@@ -100,7 +103,7 @@ describe('handleAutoUpdate', () => {
|
||||
});
|
||||
|
||||
it('should emit "update-received" but not update if auto-updates are disabled', () => {
|
||||
mockSettings.merged.disableAutoUpdate = true;
|
||||
mockSettings.merged.general!.disableAutoUpdate = true;
|
||||
mockGetInstallationInfo.mockReturnValue({
|
||||
updateCommand: 'npm i -g @qwen-code/qwen-code@latest',
|
||||
updateMessage: 'Please update manually.',
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { UpdateObject } from '../ui/utils/updateCheck.js';
|
||||
import { LoadedSettings } from '../config/settings.js';
|
||||
import type { UpdateObject } from '../ui/utils/updateCheck.js';
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
import { getInstallationInfo } from './installationInfo.js';
|
||||
import { updateEventEmitter } from './updateEventEmitter.js';
|
||||
import { HistoryItem, MessageType } from '../ui/types.js';
|
||||
import type { HistoryItem } from '../ui/types.js';
|
||||
import { MessageType } from '../ui/types.js';
|
||||
import { spawnWrapper } from './spawnWrapper.js';
|
||||
import { spawn } from 'child_process';
|
||||
import type { spawn } from 'node:child_process';
|
||||
|
||||
export function handleAutoUpdate(
|
||||
info: UpdateObject | null,
|
||||
@@ -22,13 +23,13 @@ export function handleAutoUpdate(
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.merged.disableUpdateNag) {
|
||||
if (settings.merged.general?.disableUpdateNag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installationInfo = getInstallationInfo(
|
||||
projectRoot,
|
||||
settings.merged.disableAutoUpdate ?? false,
|
||||
settings.merged.general?.disableAutoUpdate ?? false,
|
||||
);
|
||||
|
||||
let combinedMessage = info.message;
|
||||
@@ -40,7 +41,10 @@ export function handleAutoUpdate(
|
||||
message: combinedMessage,
|
||||
});
|
||||
|
||||
if (!installationInfo.updateCommand || settings.merged.disableAutoUpdate) {
|
||||
if (
|
||||
!installationInfo.updateCommand ||
|
||||
settings.merged.general?.disableAutoUpdate
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const isNightly = info.update.latest.includes('nightly');
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { getInstallationInfo, PackageManager } from './installationInfo.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as childProcess from 'node:child_process';
|
||||
import { isGitRepository } from '@qwen-code/qwen-code-core';
|
||||
|
||||
vi.mock('@qwen-code/qwen-code-core', () => ({
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
*/
|
||||
|
||||
import { isGitRepository } from '@qwen-code/qwen-code-core';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as childProcess from 'node:child_process';
|
||||
|
||||
export enum PackageManager {
|
||||
NPM = 'npm',
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
readPackageUp,
|
||||
type PackageJson as BasePackageJson,
|
||||
} from 'read-package-up';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
export type PackageJson = BasePackageJson & {
|
||||
config?: {
|
||||
|
||||
112
packages/cli/src/utils/readStdin.test.ts
Normal file
112
packages/cli/src/utils/readStdin.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest';
|
||||
import { readStdin } from './readStdin.js';
|
||||
|
||||
// Mock process.stdin
|
||||
const mockStdin = {
|
||||
setEncoding: vi.fn(),
|
||||
read: vi.fn(),
|
||||
on: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
};
|
||||
|
||||
describe('readStdin', () => {
|
||||
let originalStdin: typeof process.stdin;
|
||||
let onReadableHandler: () => void;
|
||||
let onEndHandler: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
originalStdin = process.stdin;
|
||||
|
||||
// Replace process.stdin with our mock
|
||||
Object.defineProperty(process, 'stdin', {
|
||||
value: mockStdin,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
// Capture event handlers
|
||||
mockStdin.on.mockImplementation((event: string, handler: () => void) => {
|
||||
if (event === 'readable') onReadableHandler = handler;
|
||||
if (event === 'end') onEndHandler = handler;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
Object.defineProperty(process, 'stdin', {
|
||||
value: originalStdin,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should read and accumulate data from stdin', async () => {
|
||||
mockStdin.read
|
||||
.mockReturnValueOnce('I love ')
|
||||
.mockReturnValueOnce('Gemini!')
|
||||
.mockReturnValueOnce(null);
|
||||
|
||||
const promise = readStdin();
|
||||
|
||||
// Trigger readable event
|
||||
onReadableHandler();
|
||||
|
||||
// Trigger end to resolve
|
||||
onEndHandler();
|
||||
|
||||
await expect(promise).resolves.toBe('I love Gemini!');
|
||||
});
|
||||
|
||||
it('should handle empty stdin input', async () => {
|
||||
mockStdin.read.mockReturnValue(null);
|
||||
|
||||
const promise = readStdin();
|
||||
|
||||
// Trigger end immediately
|
||||
onEndHandler();
|
||||
|
||||
await expect(promise).resolves.toBe('');
|
||||
});
|
||||
|
||||
// Emulate terminals where stdin is not TTY (eg: git bash)
|
||||
it('should timeout and resolve with empty string when no input is available', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const promise = readStdin();
|
||||
|
||||
// Fast-forward past the timeout (to run test faster)
|
||||
vi.advanceTimersByTime(500);
|
||||
|
||||
await expect(promise).resolves.toBe('');
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should clear timeout once when data is received and resolve with data', async () => {
|
||||
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
|
||||
mockStdin.read
|
||||
.mockReturnValueOnce('chunk1')
|
||||
.mockReturnValueOnce('chunk2')
|
||||
.mockReturnValueOnce(null);
|
||||
|
||||
const promise = readStdin();
|
||||
|
||||
// Trigger readable event
|
||||
onReadableHandler();
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalledOnce();
|
||||
|
||||
// Trigger end to resolve
|
||||
onEndHandler();
|
||||
|
||||
await expect(promise).resolves.toBe('chunk1chunk2');
|
||||
});
|
||||
});
|
||||
@@ -11,9 +11,22 @@ export async function readStdin(): Promise<string> {
|
||||
let totalSize = 0;
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
const pipedInputShouldBeAvailableInMs = 500;
|
||||
let pipedInputTimerId: null | NodeJS.Timeout = setTimeout(() => {
|
||||
// stop reading if input is not available yet, this is needed
|
||||
// in terminals where stdin is never TTY and nothing's piped
|
||||
// which causes the program to get stuck expecting data from stdin
|
||||
onEnd();
|
||||
}, pipedInputShouldBeAvailableInMs);
|
||||
|
||||
const onReadable = () => {
|
||||
let chunk;
|
||||
while ((chunk = process.stdin.read()) !== null) {
|
||||
if (pipedInputTimerId) {
|
||||
clearTimeout(pipedInputTimerId);
|
||||
pipedInputTimerId = null;
|
||||
}
|
||||
|
||||
if (totalSize + chunk.length > MAX_STDIN_SIZE) {
|
||||
const remainingSize = MAX_STDIN_SIZE - totalSize;
|
||||
data += chunk.slice(0, remainingSize);
|
||||
@@ -39,6 +52,10 @@ export async function readStdin(): Promise<string> {
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (pipedInputTimerId) {
|
||||
clearTimeout(pipedInputTimerId);
|
||||
pipedInputTimerId = null;
|
||||
}
|
||||
process.stdin.removeListener('readable', onReadable);
|
||||
process.stdin.removeListener('end', onEnd);
|
||||
process.stdin.removeListener('error', onError);
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
|
||||
export function resolvePath(p: string): string {
|
||||
if (!p) {
|
||||
|
||||
@@ -9,13 +9,15 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { quote, parse } from 'shell-quote';
|
||||
import {
|
||||
USER_SETTINGS_DIR,
|
||||
SETTINGS_DIRECTORY_NAME,
|
||||
} from '../config/settings.js';
|
||||
import { promisify } from 'util';
|
||||
import { Config, SandboxConfig } from '@qwen-code/qwen-code-core';
|
||||
import { promisify } from 'node:util';
|
||||
import type { Config, SandboxConfig } from '@qwen-code/qwen-code-core';
|
||||
import { FatalSandboxError } from '@qwen-code/qwen-code-core';
|
||||
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
@@ -24,6 +26,7 @@ function getContainerPath(hostPath: string): string {
|
||||
if (os.platform() !== 'win32') {
|
||||
return hostPath;
|
||||
}
|
||||
|
||||
const withForwardSlashes = hostPath.replace(/\\/g, '/');
|
||||
const match = withForwardSlashes.match(/^([A-Z]):\/(.*)/i);
|
||||
if (match) {
|
||||
@@ -114,7 +117,7 @@ function ports(): string[] {
|
||||
.map((p) => p.trim());
|
||||
}
|
||||
|
||||
function entrypoint(workdir: string): string[] {
|
||||
function entrypoint(workdir: string, cliArgs: string[]): string[] {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
const containerWorkdir = getContainerPath(workdir);
|
||||
const shellCmds = [];
|
||||
@@ -166,7 +169,7 @@ function entrypoint(workdir: string): string[] {
|
||||
),
|
||||
);
|
||||
|
||||
const cliArgs = process.argv.slice(2).map((arg) => quote([arg]));
|
||||
const quotedCliArgs = cliArgs.slice(2).map((arg) => quote([arg]));
|
||||
const cliCmd =
|
||||
process.env['NODE_ENV'] === 'development'
|
||||
? process.env['DEBUG']
|
||||
@@ -176,8 +179,7 @@ function entrypoint(workdir: string): string[] {
|
||||
? `node --inspect-brk=0.0.0.0:${process.env['DEBUG_PORT'] || '9229'} $(which qwen)`
|
||||
: 'qwen';
|
||||
|
||||
const args = [...shellCmds, cliCmd, ...cliArgs];
|
||||
|
||||
const args = [...shellCmds, cliCmd, ...quotedCliArgs];
|
||||
return ['bash', '-c', args.join(' ')];
|
||||
}
|
||||
|
||||
@@ -185,6 +187,7 @@ export async function start_sandbox(
|
||||
config: SandboxConfig,
|
||||
nodeArgs: string[] = [],
|
||||
cliConfig?: Config,
|
||||
cliArgs: string[] = [],
|
||||
) {
|
||||
const patcher = new ConsolePatcher({
|
||||
debugMode: cliConfig?.getDebugMode() || !!process.env['DEBUG'],
|
||||
@@ -196,12 +199,15 @@ export async function start_sandbox(
|
||||
if (config.command === 'sandbox-exec') {
|
||||
// disallow BUILD_SANDBOX
|
||||
if (process.env['BUILD_SANDBOX']) {
|
||||
console.error('ERROR: cannot BUILD_SANDBOX when using macOS Seatbelt');
|
||||
process.exit(1);
|
||||
throw new FatalSandboxError(
|
||||
'Cannot BUILD_SANDBOX when using macOS Seatbelt',
|
||||
);
|
||||
}
|
||||
|
||||
const profile = (process.env['SEATBELT_PROFILE'] ??= 'permissive-open');
|
||||
let profileFile = new URL(`sandbox-macos-${profile}.sb`, import.meta.url)
|
||||
.pathname;
|
||||
let profileFile = fileURLToPath(
|
||||
new URL(`sandbox-macos-${profile}.sb`, import.meta.url),
|
||||
);
|
||||
// if profile name is not recognized, then look for file under project settings directory
|
||||
if (!BUILTIN_SEATBELT_PROFILES.includes(profile)) {
|
||||
profileFile = path.join(
|
||||
@@ -210,10 +216,9 @@ export async function start_sandbox(
|
||||
);
|
||||
}
|
||||
if (!fs.existsSync(profileFile)) {
|
||||
console.error(
|
||||
`ERROR: missing macos seatbelt profile file '${profileFile}'`,
|
||||
throw new FatalSandboxError(
|
||||
`Missing macos seatbelt profile file '${profileFile}'`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// Log on STDERR so it doesn't clutter the output on STDOUT
|
||||
console.error(`using macos seatbelt (profile: ${profile}) ...`);
|
||||
@@ -263,6 +268,8 @@ export async function start_sandbox(
|
||||
args.push('-D', `INCLUDE_DIR_${i}=${dirPath}`);
|
||||
}
|
||||
|
||||
const finalArgv = cliArgs;
|
||||
|
||||
args.push(
|
||||
'-f',
|
||||
profileFile,
|
||||
@@ -271,7 +278,7 @@ export async function start_sandbox(
|
||||
[
|
||||
`SANDBOX=sandbox-exec`,
|
||||
`NODE_OPTIONS="${nodeOptions}"`,
|
||||
...process.argv.map((arg) => quote([arg])),
|
||||
...finalArgv.map((arg) => quote([arg])),
|
||||
].join(' '),
|
||||
);
|
||||
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
|
||||
@@ -319,13 +326,12 @@ export async function start_sandbox(
|
||||
console.error(data.toString());
|
||||
});
|
||||
proxyProcess.on('close', (code, signal) => {
|
||||
console.error(
|
||||
`ERROR: proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
|
||||
);
|
||||
if (sandboxProcess?.pid) {
|
||||
process.kill(-sandboxProcess.pid, 'SIGTERM');
|
||||
}
|
||||
process.exit(1);
|
||||
throw new FatalSandboxError(
|
||||
`Proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
|
||||
);
|
||||
});
|
||||
console.log('waiting for proxy to start ...');
|
||||
await execAsync(
|
||||
@@ -360,11 +366,10 @@ export async function start_sandbox(
|
||||
// note this can only be done with binary linked from gemini-cli repo
|
||||
if (process.env['BUILD_SANDBOX']) {
|
||||
if (!gcPath.includes('gemini-cli/packages/')) {
|
||||
console.error(
|
||||
'ERROR: cannot build sandbox using installed gemini binary; ' +
|
||||
throw new FatalSandboxError(
|
||||
'Cannot build sandbox using installed gemini binary; ' +
|
||||
'run `npm link ./packages/cli` under gemini-cli repo to switch to linked binary.',
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.error('building sandbox ...');
|
||||
const gcRoot = gcPath.split('/packages/')[0];
|
||||
@@ -397,10 +402,9 @@ export async function start_sandbox(
|
||||
image === LOCAL_DEV_SANDBOX_IMAGE_NAME
|
||||
? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.'
|
||||
: 'Please check the image name, your network connection, or notify gemini-cli-dev@google.com if the issue persists.';
|
||||
console.error(
|
||||
`ERROR: Sandbox image '${image}' is missing or could not be pulled. ${remedy}`,
|
||||
throw new FatalSandboxError(
|
||||
`Sandbox image '${image}' is missing or could not be pulled. ${remedy}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// use interactive mode and auto-remove container on exit
|
||||
@@ -478,17 +482,15 @@ export async function start_sandbox(
|
||||
mount = `${from}:${to}:${opts}`;
|
||||
// check that from path is absolute
|
||||
if (!path.isAbsolute(from)) {
|
||||
console.error(
|
||||
`ERROR: path '${from}' listed in SANDBOX_MOUNTS must be absolute`,
|
||||
throw new FatalSandboxError(
|
||||
`Path '${from}' listed in SANDBOX_MOUNTS must be absolute`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// check that from path exists on host
|
||||
if (!fs.existsSync(from)) {
|
||||
console.error(
|
||||
`ERROR: missing mount path '${from}' listed in SANDBOX_MOUNTS`,
|
||||
throw new FatalSandboxError(
|
||||
`Missing mount path '${from}' listed in SANDBOX_MOUNTS`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.error(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`);
|
||||
args.push('--volume', mount);
|
||||
@@ -674,10 +676,9 @@ export async function start_sandbox(
|
||||
console.error(`SANDBOX_ENV: ${env}`);
|
||||
args.push('--env', env);
|
||||
} else {
|
||||
console.error(
|
||||
'ERROR: SANDBOX_ENV must be a comma-separated list of key=value pairs',
|
||||
throw new FatalSandboxError(
|
||||
'SANDBOX_ENV must be a comma-separated list of key=value pairs',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -707,7 +708,7 @@ export async function start_sandbox(
|
||||
// Determine if the current user's UID/GID should be passed to the sandbox.
|
||||
// See shouldUseCurrentUserInSandbox for more details.
|
||||
let userFlag = '';
|
||||
const finalEntrypoint = entrypoint(workdir);
|
||||
const finalEntrypoint = entrypoint(workdir, cliArgs);
|
||||
|
||||
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') {
|
||||
args.push('--user', 'root');
|
||||
@@ -785,13 +786,12 @@ export async function start_sandbox(
|
||||
console.error(data.toString().trim());
|
||||
});
|
||||
proxyProcess.on('close', (code, signal) => {
|
||||
console.error(
|
||||
`ERROR: proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
|
||||
);
|
||||
if (sandboxProcess?.pid) {
|
||||
process.kill(-sandboxProcess.pid, 'SIGTERM');
|
||||
}
|
||||
process.exit(1);
|
||||
throw new FatalSandboxError(
|
||||
`Proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
|
||||
);
|
||||
});
|
||||
console.log('waiting for proxy to start ...');
|
||||
await execAsync(
|
||||
|
||||
@@ -42,12 +42,7 @@ describe('SettingsUtils', () => {
|
||||
const categories = getSettingsByCategory();
|
||||
|
||||
expect(categories).toHaveProperty('General');
|
||||
expect(categories).toHaveProperty('Accessibility');
|
||||
expect(categories).toHaveProperty('Checkpointing');
|
||||
expect(categories).toHaveProperty('File Filtering');
|
||||
expect(categories).toHaveProperty('UI');
|
||||
expect(categories).toHaveProperty('Mode');
|
||||
expect(categories).toHaveProperty('Updates');
|
||||
});
|
||||
|
||||
it('should include key property in grouped settings', () => {
|
||||
@@ -63,7 +58,7 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('getSettingDefinition', () => {
|
||||
it('should return definition for valid setting', () => {
|
||||
const definition = getSettingDefinition('showMemoryUsage');
|
||||
const definition = getSettingDefinition('ui.showMemoryUsage');
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition?.label).toBe('Show Memory Usage');
|
||||
});
|
||||
@@ -76,13 +71,13 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('requiresRestart', () => {
|
||||
it('should return true for settings that require restart', () => {
|
||||
expect(requiresRestart('autoConfigureMaxOldSpaceSize')).toBe(true);
|
||||
expect(requiresRestart('checkpointing.enabled')).toBe(true);
|
||||
expect(requiresRestart('advanced.autoConfigureMemory')).toBe(true);
|
||||
expect(requiresRestart('general.checkpointing.enabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for settings that do not require restart', () => {
|
||||
expect(requiresRestart('showMemoryUsage')).toBe(false);
|
||||
expect(requiresRestart('hideTips')).toBe(false);
|
||||
expect(requiresRestart('ui.showMemoryUsage')).toBe(false);
|
||||
expect(requiresRestart('ui.hideTips')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for invalid settings', () => {
|
||||
@@ -92,10 +87,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('getDefaultValue', () => {
|
||||
it('should return correct default values', () => {
|
||||
expect(getDefaultValue('showMemoryUsage')).toBe(false);
|
||||
expect(getDefaultValue('fileFiltering.enableRecursiveFileSearch')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(getDefaultValue('ui.showMemoryUsage')).toBe(false);
|
||||
expect(
|
||||
getDefaultValue('context.fileFiltering.enableRecursiveFileSearch'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return undefined for invalid settings', () => {
|
||||
@@ -106,19 +101,19 @@ describe('SettingsUtils', () => {
|
||||
describe('getRestartRequiredSettings', () => {
|
||||
it('should return all settings that require restart', () => {
|
||||
const restartSettings = getRestartRequiredSettings();
|
||||
expect(restartSettings).toContain('autoConfigureMaxOldSpaceSize');
|
||||
expect(restartSettings).toContain('checkpointing.enabled');
|
||||
expect(restartSettings).not.toContain('showMemoryUsage');
|
||||
expect(restartSettings).toContain('advanced.autoConfigureMemory');
|
||||
expect(restartSettings).toContain('general.checkpointing.enabled');
|
||||
expect(restartSettings).not.toContain('ui.showMemoryUsage');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEffectiveValue', () => {
|
||||
it('should return value from settings when set', () => {
|
||||
const settings = { showMemoryUsage: true };
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const settings = { ui: { showMemoryUsage: true } };
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -127,10 +122,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should return value from merged settings when not set in current scope', () => {
|
||||
const settings = {};
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -142,7 +137,7 @@ describe('SettingsUtils', () => {
|
||||
const mergedSettings = {};
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -151,14 +146,14 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should handle nested settings correctly', () => {
|
||||
const settings = {
|
||||
accessibility: { disableLoadingPhrases: true },
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
};
|
||||
const mergedSettings = {
|
||||
accessibility: { disableLoadingPhrases: false },
|
||||
ui: { accessibility: { disableLoadingPhrases: false } },
|
||||
};
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -181,9 +176,9 @@ describe('SettingsUtils', () => {
|
||||
describe('getAllSettingKeys', () => {
|
||||
it('should return all setting keys', () => {
|
||||
const keys = getAllSettingKeys();
|
||||
expect(keys).toContain('showMemoryUsage');
|
||||
expect(keys).toContain('accessibility.disableLoadingPhrases');
|
||||
expect(keys).toContain('checkpointing.enabled');
|
||||
expect(keys).toContain('ui.showMemoryUsage');
|
||||
expect(keys).toContain('ui.accessibility.disableLoadingPhrases');
|
||||
expect(keys).toContain('general.checkpointing.enabled');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,10 +204,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('isValidSettingKey', () => {
|
||||
it('should return true for valid setting keys', () => {
|
||||
expect(isValidSettingKey('showMemoryUsage')).toBe(true);
|
||||
expect(isValidSettingKey('accessibility.disableLoadingPhrases')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isValidSettingKey('ui.showMemoryUsage')).toBe(true);
|
||||
expect(
|
||||
isValidSettingKey('ui.accessibility.disableLoadingPhrases'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid setting keys', () => {
|
||||
@@ -223,10 +218,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('getSettingCategory', () => {
|
||||
it('should return correct category for valid settings', () => {
|
||||
expect(getSettingCategory('showMemoryUsage')).toBe('UI');
|
||||
expect(getSettingCategory('accessibility.disableLoadingPhrases')).toBe(
|
||||
'Accessibility',
|
||||
);
|
||||
expect(getSettingCategory('ui.showMemoryUsage')).toBe('UI');
|
||||
expect(
|
||||
getSettingCategory('ui.accessibility.disableLoadingPhrases'),
|
||||
).toBe('UI');
|
||||
});
|
||||
|
||||
it('should return undefined for invalid settings', () => {
|
||||
@@ -236,18 +231,20 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('shouldShowInDialog', () => {
|
||||
it('should return true for settings marked to show in dialog', () => {
|
||||
expect(shouldShowInDialog('showMemoryUsage')).toBe(true);
|
||||
expect(shouldShowInDialog('vimMode')).toBe(true);
|
||||
expect(shouldShowInDialog('hideWindowTitle')).toBe(true);
|
||||
expect(shouldShowInDialog('usageStatisticsEnabled')).toBe(false);
|
||||
expect(shouldShowInDialog('ui.showMemoryUsage')).toBe(true);
|
||||
expect(shouldShowInDialog('general.vimMode')).toBe(true);
|
||||
expect(shouldShowInDialog('ui.hideWindowTitle')).toBe(true);
|
||||
expect(shouldShowInDialog('privacy.usageStatisticsEnabled')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for settings marked to hide from dialog', () => {
|
||||
expect(shouldShowInDialog('selectedAuthType')).toBe(false);
|
||||
expect(shouldShowInDialog('coreTools')).toBe(false);
|
||||
expect(shouldShowInDialog('customThemes')).toBe(false);
|
||||
expect(shouldShowInDialog('theme')).toBe(false); // Changed to false
|
||||
expect(shouldShowInDialog('preferredEditor')).toBe(false); // Changed to false
|
||||
expect(shouldShowInDialog('security.auth.selectedType')).toBe(false);
|
||||
expect(shouldShowInDialog('tools.core')).toBe(false);
|
||||
expect(shouldShowInDialog('ui.customThemes')).toBe(false);
|
||||
expect(shouldShowInDialog('ui.theme')).toBe(false); // Changed to false
|
||||
expect(shouldShowInDialog('general.preferredEditor')).toBe(false); // Changed to false
|
||||
});
|
||||
|
||||
it('should return true for invalid settings (default behavior)', () => {
|
||||
@@ -263,10 +260,10 @@ describe('SettingsUtils', () => {
|
||||
expect(categories['UI']).toBeDefined();
|
||||
const uiSettings = categories['UI'];
|
||||
const uiKeys = uiSettings.map((s) => s.key);
|
||||
expect(uiKeys).toContain('showMemoryUsage');
|
||||
expect(uiKeys).toContain('hideWindowTitle');
|
||||
expect(uiKeys).not.toContain('customThemes'); // This is marked false
|
||||
expect(uiKeys).not.toContain('theme'); // This is now marked false
|
||||
expect(uiKeys).toContain('ui.showMemoryUsage');
|
||||
expect(uiKeys).toContain('ui.hideWindowTitle');
|
||||
expect(uiKeys).not.toContain('ui.customThemes'); // This is marked false
|
||||
expect(uiKeys).not.toContain('ui.theme'); // This is now marked false
|
||||
});
|
||||
|
||||
it('should not include Advanced category settings', () => {
|
||||
@@ -282,15 +279,15 @@ describe('SettingsUtils', () => {
|
||||
const allSettings = Object.values(categories).flat();
|
||||
const allKeys = allSettings.map((s) => s.key);
|
||||
|
||||
expect(allKeys).toContain('vimMode');
|
||||
expect(allKeys).toContain('ideMode');
|
||||
expect(allKeys).toContain('disableAutoUpdate');
|
||||
expect(allKeys).toContain('showMemoryUsage');
|
||||
expect(allKeys).not.toContain('usageStatisticsEnabled');
|
||||
expect(allKeys).not.toContain('selectedAuthType');
|
||||
expect(allKeys).not.toContain('coreTools');
|
||||
expect(allKeys).not.toContain('theme'); // Now hidden
|
||||
expect(allKeys).not.toContain('preferredEditor'); // Now hidden
|
||||
expect(allKeys).toContain('general.vimMode');
|
||||
expect(allKeys).toContain('ide.enabled');
|
||||
expect(allKeys).toContain('general.disableAutoUpdate');
|
||||
expect(allKeys).toContain('ui.showMemoryUsage');
|
||||
expect(allKeys).not.toContain('privacy.usageStatisticsEnabled');
|
||||
expect(allKeys).not.toContain('security.auth.selectedType');
|
||||
expect(allKeys).not.toContain('tools.core');
|
||||
expect(allKeys).not.toContain('ui.theme'); // Now hidden
|
||||
expect(allKeys).not.toContain('general.preferredEditor'); // Now hidden
|
||||
});
|
||||
});
|
||||
|
||||
@@ -299,12 +296,12 @@ describe('SettingsUtils', () => {
|
||||
const booleanSettings = getDialogSettingsByType('boolean');
|
||||
|
||||
const keys = booleanSettings.map((s) => s.key);
|
||||
expect(keys).toContain('showMemoryUsage');
|
||||
expect(keys).toContain('vimMode');
|
||||
expect(keys).toContain('hideWindowTitle');
|
||||
expect(keys).not.toContain('usageStatisticsEnabled');
|
||||
expect(keys).not.toContain('selectedAuthType'); // Advanced setting
|
||||
expect(keys).not.toContain('useExternalAuth'); // Advanced setting
|
||||
expect(keys).toContain('ui.showMemoryUsage');
|
||||
expect(keys).toContain('general.vimMode');
|
||||
expect(keys).toContain('ui.hideWindowTitle');
|
||||
expect(keys).not.toContain('privacy.usageStatisticsEnabled');
|
||||
expect(keys).not.toContain('security.auth.selectedType'); // Advanced setting
|
||||
expect(keys).not.toContain('security.auth.useExternal'); // Advanced setting
|
||||
});
|
||||
|
||||
it('should return only string dialog settings', () => {
|
||||
@@ -312,9 +309,9 @@ describe('SettingsUtils', () => {
|
||||
|
||||
const keys = stringSettings.map((s) => s.key);
|
||||
// Note: theme and preferredEditor are now hidden from dialog
|
||||
expect(keys).not.toContain('theme'); // Now marked false
|
||||
expect(keys).not.toContain('preferredEditor'); // Now marked false
|
||||
expect(keys).not.toContain('selectedAuthType'); // Advanced setting
|
||||
expect(keys).not.toContain('ui.theme'); // Now marked false
|
||||
expect(keys).not.toContain('general.preferredEditor'); // Now marked false
|
||||
expect(keys).not.toContain('security.auth.selectedType'); // Advanced setting
|
||||
|
||||
// Most string settings are now hidden, so let's just check they exclude advanced ones
|
||||
expect(keys.every((key) => !key.startsWith('tool'))).toBe(true); // No tool-related settings
|
||||
@@ -326,24 +323,28 @@ describe('SettingsUtils', () => {
|
||||
const dialogKeys = getDialogSettingKeys();
|
||||
|
||||
// Should include settings marked for dialog
|
||||
expect(dialogKeys).toContain('showMemoryUsage');
|
||||
expect(dialogKeys).toContain('vimMode');
|
||||
expect(dialogKeys).toContain('hideWindowTitle');
|
||||
expect(dialogKeys).not.toContain('usageStatisticsEnabled');
|
||||
expect(dialogKeys).toContain('ideMode');
|
||||
expect(dialogKeys).toContain('disableAutoUpdate');
|
||||
expect(dialogKeys).toContain('ui.showMemoryUsage');
|
||||
expect(dialogKeys).toContain('general.vimMode');
|
||||
expect(dialogKeys).toContain('ui.hideWindowTitle');
|
||||
expect(dialogKeys).not.toContain('privacy.usageStatisticsEnabled');
|
||||
expect(dialogKeys).toContain('ide.enabled');
|
||||
expect(dialogKeys).toContain('general.disableAutoUpdate');
|
||||
|
||||
// Should include nested settings marked for dialog
|
||||
expect(dialogKeys).toContain('fileFiltering.respectGitIgnore');
|
||||
expect(dialogKeys).toContain('fileFiltering.respectGeminiIgnore');
|
||||
expect(dialogKeys).toContain('fileFiltering.enableRecursiveFileSearch');
|
||||
expect(dialogKeys).toContain('context.fileFiltering.respectGitIgnore');
|
||||
expect(dialogKeys).toContain(
|
||||
'context.fileFiltering.respectGeminiIgnore',
|
||||
);
|
||||
expect(dialogKeys).toContain(
|
||||
'context.fileFiltering.enableRecursiveFileSearch',
|
||||
);
|
||||
|
||||
// Should NOT include settings marked as hidden
|
||||
expect(dialogKeys).not.toContain('theme'); // Hidden
|
||||
expect(dialogKeys).not.toContain('customThemes'); // Hidden
|
||||
expect(dialogKeys).not.toContain('preferredEditor'); // Hidden
|
||||
expect(dialogKeys).not.toContain('selectedAuthType'); // Advanced
|
||||
expect(dialogKeys).not.toContain('coreTools'); // Advanced
|
||||
expect(dialogKeys).not.toContain('ui.theme'); // Hidden
|
||||
expect(dialogKeys).not.toContain('ui.customThemes'); // Hidden
|
||||
expect(dialogKeys).not.toContain('general.preferredEditor'); // Hidden
|
||||
expect(dialogKeys).not.toContain('security.auth.selectedType'); // Advanced
|
||||
expect(dialogKeys).not.toContain('tools.core'); // Advanced
|
||||
expect(dialogKeys).not.toContain('mcpServers'); // Advanced
|
||||
expect(dialogKeys).not.toContain('telemetry'); // Advanced
|
||||
});
|
||||
@@ -358,7 +359,7 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should handle nested settings display correctly', () => {
|
||||
// Test the specific issue with fileFiltering.respectGitIgnore
|
||||
const key = 'fileFiltering.respectGitIgnore';
|
||||
const key = 'context.fileFiltering.respectGitIgnore';
|
||||
const initialSettings = {};
|
||||
const pendingSettings = {};
|
||||
|
||||
@@ -411,11 +412,11 @@ describe('SettingsUtils', () => {
|
||||
describe('Business Logic Utilities', () => {
|
||||
describe('getSettingValue', () => {
|
||||
it('should return value from settings when set', () => {
|
||||
const settings = { showMemoryUsage: true };
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const settings = { ui: { showMemoryUsage: true } };
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
|
||||
const value = getSettingValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -424,10 +425,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should return value from merged settings when not set in current scope', () => {
|
||||
const settings = {};
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
|
||||
const value = getSettingValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -449,51 +450,68 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('isSettingModified', () => {
|
||||
it('should return true when value differs from default', () => {
|
||||
expect(isSettingModified('showMemoryUsage', true)).toBe(true);
|
||||
expect(isSettingModified('ui.showMemoryUsage', true)).toBe(true);
|
||||
expect(
|
||||
isSettingModified('fileFiltering.enableRecursiveFileSearch', false),
|
||||
isSettingModified(
|
||||
'context.fileFiltering.enableRecursiveFileSearch',
|
||||
false,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when value matches default', () => {
|
||||
expect(isSettingModified('showMemoryUsage', false)).toBe(false);
|
||||
expect(isSettingModified('ui.showMemoryUsage', false)).toBe(false);
|
||||
expect(
|
||||
isSettingModified('fileFiltering.enableRecursiveFileSearch', true),
|
||||
isSettingModified(
|
||||
'context.fileFiltering.enableRecursiveFileSearch',
|
||||
true,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('settingExistsInScope', () => {
|
||||
it('should return true for top-level settings that exist', () => {
|
||||
const settings = { showMemoryUsage: true };
|
||||
expect(settingExistsInScope('showMemoryUsage', settings)).toBe(true);
|
||||
const settings = { ui: { showMemoryUsage: true } };
|
||||
expect(settingExistsInScope('ui.showMemoryUsage', settings)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for top-level settings that do not exist', () => {
|
||||
const settings = {};
|
||||
expect(settingExistsInScope('showMemoryUsage', settings)).toBe(false);
|
||||
expect(settingExistsInScope('ui.showMemoryUsage', settings)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true for nested settings that exist', () => {
|
||||
const settings = {
|
||||
accessibility: { disableLoadingPhrases: true },
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
};
|
||||
expect(
|
||||
settingExistsInScope('accessibility.disableLoadingPhrases', settings),
|
||||
settingExistsInScope(
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for nested settings that do not exist', () => {
|
||||
const settings = {};
|
||||
expect(
|
||||
settingExistsInScope('accessibility.disableLoadingPhrases', settings),
|
||||
settingExistsInScope(
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when parent exists but child does not', () => {
|
||||
const settings = { accessibility: {} };
|
||||
const settings = { ui: { accessibility: {} } };
|
||||
expect(
|
||||
settingExistsInScope('accessibility.disableLoadingPhrases', settings),
|
||||
settingExistsInScope(
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -502,41 +520,41 @@ describe('SettingsUtils', () => {
|
||||
it('should set top-level setting value', () => {
|
||||
const pendingSettings = {};
|
||||
const result = setPendingSettingValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
true,
|
||||
pendingSettings,
|
||||
);
|
||||
|
||||
expect(result.showMemoryUsage).toBe(true);
|
||||
expect(result.ui?.showMemoryUsage).toBe(true);
|
||||
});
|
||||
|
||||
it('should set nested setting value', () => {
|
||||
const pendingSettings = {};
|
||||
const result = setPendingSettingValue(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
true,
|
||||
pendingSettings,
|
||||
);
|
||||
|
||||
expect(result.accessibility?.disableLoadingPhrases).toBe(true);
|
||||
expect(result.ui?.accessibility?.disableLoadingPhrases).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve existing nested settings', () => {
|
||||
const pendingSettings = {
|
||||
accessibility: { disableLoadingPhrases: false },
|
||||
ui: { accessibility: { disableLoadingPhrases: false } },
|
||||
};
|
||||
const result = setPendingSettingValue(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
true,
|
||||
pendingSettings,
|
||||
);
|
||||
|
||||
expect(result.accessibility?.disableLoadingPhrases).toBe(true);
|
||||
expect(result.ui?.accessibility?.disableLoadingPhrases).toBe(true);
|
||||
});
|
||||
|
||||
it('should not mutate original settings', () => {
|
||||
const pendingSettings = {};
|
||||
setPendingSettingValue('showMemoryUsage', true, pendingSettings);
|
||||
setPendingSettingValue('ui.showMemoryUsage', true, pendingSettings);
|
||||
|
||||
expect(pendingSettings).toEqual({});
|
||||
});
|
||||
@@ -545,16 +563,16 @@ describe('SettingsUtils', () => {
|
||||
describe('hasRestartRequiredSettings', () => {
|
||||
it('should return true when modified settings require restart', () => {
|
||||
const modifiedSettings = new Set<string>([
|
||||
'autoConfigureMaxOldSpaceSize',
|
||||
'showMemoryUsage',
|
||||
'advanced.autoConfigureMemory',
|
||||
'ui.showMemoryUsage',
|
||||
]);
|
||||
expect(hasRestartRequiredSettings(modifiedSettings)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when no modified settings require restart', () => {
|
||||
const modifiedSettings = new Set<string>([
|
||||
'showMemoryUsage',
|
||||
'hideTips',
|
||||
'ui.showMemoryUsage',
|
||||
'ui.hideTips',
|
||||
]);
|
||||
expect(hasRestartRequiredSettings(modifiedSettings)).toBe(false);
|
||||
});
|
||||
@@ -568,15 +586,15 @@ describe('SettingsUtils', () => {
|
||||
describe('getRestartRequiredFromModified', () => {
|
||||
it('should return only settings that require restart', () => {
|
||||
const modifiedSettings = new Set<string>([
|
||||
'autoConfigureMaxOldSpaceSize',
|
||||
'showMemoryUsage',
|
||||
'checkpointing.enabled',
|
||||
'advanced.autoConfigureMemory',
|
||||
'ui.showMemoryUsage',
|
||||
'general.checkpointing.enabled',
|
||||
]);
|
||||
const result = getRestartRequiredFromModified(modifiedSettings);
|
||||
|
||||
expect(result).toContain('autoConfigureMaxOldSpaceSize');
|
||||
expect(result).toContain('checkpointing.enabled');
|
||||
expect(result).not.toContain('showMemoryUsage');
|
||||
expect(result).toContain('advanced.autoConfigureMemory');
|
||||
expect(result).toContain('general.checkpointing.enabled');
|
||||
expect(result).not.toContain('ui.showMemoryUsage');
|
||||
});
|
||||
|
||||
it('should return empty array when no settings require restart', () => {
|
||||
@@ -592,12 +610,12 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('getDisplayValue', () => {
|
||||
it('should show value without * when setting matches default', () => {
|
||||
const settings = { showMemoryUsage: false }; // false matches default, so no *
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const settings = { ui: { showMemoryUsage: false } }; // false matches default, so no *
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
@@ -607,11 +625,11 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should show default value when setting is not in scope', () => {
|
||||
const settings = {}; // no setting in scope
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
@@ -620,12 +638,12 @@ describe('SettingsUtils', () => {
|
||||
});
|
||||
|
||||
it('should show value with * when changed from default', () => {
|
||||
const settings = { showMemoryUsage: true }; // true is different from default (false)
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const settings = { ui: { showMemoryUsage: true } }; // true is different from default (false)
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
@@ -635,11 +653,11 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should show default value without * when setting does not exist in scope', () => {
|
||||
const settings = {}; // setting doesn't exist in scope, show default
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
@@ -649,12 +667,12 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should show value with * when user changes from default', () => {
|
||||
const settings = {}; // setting doesn't exist in scope originally
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const modifiedSettings = new Set<string>(['showMemoryUsage']);
|
||||
const pendingSettings = { showMemoryUsage: true }; // user changed to true
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
const modifiedSettings = new Set<string>(['ui.showMemoryUsage']);
|
||||
const pendingSettings = { ui: { showMemoryUsage: true } }; // user changed to true
|
||||
|
||||
const result = getDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
@@ -668,14 +686,14 @@ describe('SettingsUtils', () => {
|
||||
it('should return true when setting does not exist in scope', () => {
|
||||
const settings = {}; // setting doesn't exist
|
||||
|
||||
const result = isDefaultValue('showMemoryUsage', settings);
|
||||
const result = isDefaultValue('ui.showMemoryUsage', settings);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when setting exists in scope', () => {
|
||||
const settings = { showMemoryUsage: true }; // setting exists
|
||||
const settings = { ui: { showMemoryUsage: true } }; // setting exists
|
||||
|
||||
const result = isDefaultValue('showMemoryUsage', settings);
|
||||
const result = isDefaultValue('ui.showMemoryUsage', settings);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
@@ -683,17 +701,19 @@ describe('SettingsUtils', () => {
|
||||
const settings = {}; // nested setting doesn't exist
|
||||
|
||||
const result = isDefaultValue(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when nested setting exists in scope', () => {
|
||||
const settings = { accessibility: { disableLoadingPhrases: true } }; // nested setting exists
|
||||
const settings = {
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
}; // nested setting exists
|
||||
|
||||
const result = isDefaultValue(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
@@ -702,11 +722,11 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('isValueInherited', () => {
|
||||
it('should return false for top-level settings that exist in scope', () => {
|
||||
const settings = { showMemoryUsage: true };
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const settings = { ui: { showMemoryUsage: true } };
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
|
||||
const result = isValueInherited(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -715,10 +735,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should return true for top-level settings that do not exist in scope', () => {
|
||||
const settings = {};
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
|
||||
const result = isValueInherited(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -727,14 +747,14 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should return false for nested settings that exist in scope', () => {
|
||||
const settings = {
|
||||
accessibility: { disableLoadingPhrases: true },
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
};
|
||||
const mergedSettings = {
|
||||
accessibility: { disableLoadingPhrases: true },
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
};
|
||||
|
||||
const result = isValueInherited(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -744,11 +764,11 @@ describe('SettingsUtils', () => {
|
||||
it('should return true for nested settings that do not exist in scope', () => {
|
||||
const settings = {};
|
||||
const mergedSettings = {
|
||||
accessibility: { disableLoadingPhrases: true },
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
};
|
||||
|
||||
const result = isValueInherited(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -758,11 +778,11 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('getEffectiveDisplayValue', () => {
|
||||
it('should return value from settings when available', () => {
|
||||
const settings = { showMemoryUsage: true };
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const settings = { ui: { showMemoryUsage: true } };
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
|
||||
const result = getEffectiveDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -771,10 +791,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should return value from merged settings when not in scope', () => {
|
||||
const settings = {};
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
|
||||
const result = getEffectiveDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -786,7 +806,7 @@ describe('SettingsUtils', () => {
|
||||
const mergedSettings = {};
|
||||
|
||||
const result = getEffectiveDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Settings, SettingScope, LoadedSettings } from '../config/settings.js';
|
||||
import {
|
||||
SETTINGS_SCHEMA,
|
||||
import type {
|
||||
Settings,
|
||||
SettingScope,
|
||||
LoadedSettings,
|
||||
} from '../config/settings.js';
|
||||
import type {
|
||||
SettingDefinition,
|
||||
SettingsSchema,
|
||||
} from '../config/settingsSchema.js';
|
||||
import { SETTINGS_SCHEMA } from '../config/settingsSchema.js';
|
||||
|
||||
// The schema is now nested, but many parts of the UI and logic work better
|
||||
// with a flattened structure and dot-notation keys. This section flattens the
|
||||
@@ -395,22 +399,7 @@ export function saveModifiedSettings(
|
||||
const isDefaultValue = value === getDefaultValue(settingKey);
|
||||
|
||||
if (existsInOriginalFile || !isDefaultValue) {
|
||||
// This is tricky because setValue only works on top-level keys.
|
||||
// We need to set the whole parent object.
|
||||
const [parentKey] = path;
|
||||
if (parentKey) {
|
||||
const newParentValue = setPendingSettingValueAny(
|
||||
settingKey,
|
||||
value,
|
||||
loadedSettings.forScope(scope).settings,
|
||||
)[parentKey as keyof Settings];
|
||||
|
||||
loadedSettings.setValue(
|
||||
scope,
|
||||
parentKey as keyof Settings,
|
||||
newParentValue,
|
||||
);
|
||||
}
|
||||
loadedSettings.setValue(scope, settingKey, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export const spawnWrapper = spawn;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getStartupWarnings } from './startupWarnings.js';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { getErrorMessage } from '@qwen-code/qwen-code-core';
|
||||
|
||||
vi.mock('fs/promises');
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import os from 'os';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import { join as pathJoin } from 'node:path';
|
||||
import { getErrorMessage } from '@qwen-code/qwen-code-core';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
/**
|
||||
* A shared event emitter for application-wide communication
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { getUserStartupWarnings } from './userStartupWarnings.js';
|
||||
import * as os from 'os';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import * as os from 'node:os';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
// Mock os.homedir to control the home directory in tests
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import * as os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'node:fs/promises';
|
||||
import * as os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
type WarningCheck = {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user