refactor(debug): replace ConsolePatcher with debugLogger and update error reporting

- Replace ConsolePatcher with centralized debugLogger utility
- Refactor errorReporting to use debugLogger instead of file-based reporting
- Remove user-facing console message components:
  - Delete ConsolePatcher.ts, useConsoleMessages.ts/hook
  - Delete ConsoleSummaryDisplay.tsx, DetailedMessagesDisplay.tsx
- Update all tests in packages/core and packages/cli:
  - Mock debugLogger where needed
  - Remove assertions for console output on non-critical errors
  - Keep debugLogger assertions for fatal/network errors
  - Use HOME directory mocking for hermetic file system tests

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-02-02 17:37:54 +08:00
parent 135df54f27
commit 89e3c2cd7a
64 changed files with 1240 additions and 2416 deletions

View file

@ -176,9 +176,6 @@ describe('Session', () => {
});
it('swallows errors and does not throw', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
getAvailableCommandsSpy.mockRejectedValueOnce(
new Error('Command discovery failed'),
);
@ -187,8 +184,6 @@ describe('Session', () => {
session.sendAvailableCommandsUpdate(),
).resolves.toBeUndefined();
expect(mockClient.sessionUpdate).not.toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
});

View file

@ -4,19 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
type MockInstance,
} from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { disableCommand, handleDisable } from './disable.js';
import yargs from 'yargs';
import { SettingScope } from '../../config/settings.js';
const mockDisableExtension = vi.hoisted(() => vi.fn());
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('./utils.js', () => ({
getExtensionManager: vi.fn().mockResolvedValue({
@ -28,6 +23,12 @@ vi.mock('../../utils/errors.js', () => ({
getErrorMessage: vi.fn((error: Error) => error.message),
}));
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: mockWriteStderrLine,
clearScreen: vi.fn(),
}));
describe('extensions disable command', () => {
it('should fail if no name is provided', () => {
const validationParser = yargs([])
@ -59,20 +60,15 @@ describe('extensions disable command', () => {
});
describe('handleDisable', () => {
let consoleLogSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let processExitSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
vi.clearAllMocks();
});
it('should disable an extension with user scope', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
await handleDisable({
name: 'test-extension',
scope: 'user',
@ -82,12 +78,18 @@ describe('handleDisable', () => {
'test-extension',
SettingScope.User,
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "test-extension" successfully disabled for scope "user".',
);
processExitSpy.mockRestore();
});
it('should disable an extension with workspace scope', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
await handleDisable({
name: 'test-extension',
scope: 'workspace',
@ -97,12 +99,18 @@ describe('handleDisable', () => {
'test-extension',
SettingScope.Workspace,
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "test-extension" successfully disabled for scope "workspace".',
);
processExitSpy.mockRestore();
});
it('should default to user scope when no scope is provided', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
await handleDisable({
name: 'test-extension',
});
@ -111,9 +119,15 @@ describe('handleDisable', () => {
'test-extension',
SettingScope.User,
);
processExitSpy.mockRestore();
});
it('should handle errors and exit with code 1', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockDisableExtension.mockImplementationOnce(() => {
throw new Error('Disable failed');
});
@ -123,7 +137,9 @@ describe('handleDisable', () => {
scope: 'user',
});
expect(consoleErrorSpy).toHaveBeenCalledWith('Disable failed');
expect(mockWriteStderrLine).toHaveBeenCalledWith('Disable failed');
expect(processExitSpy).toHaveBeenCalledWith(1);
processExitSpy.mockRestore();
});
});

View file

@ -4,19 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
type MockInstance,
} from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { enableCommand, handleEnable } from './enable.js';
import yargs from 'yargs';
import { SettingScope } from '../../config/settings.js';
const mockEnableExtension = vi.hoisted(() => vi.fn());
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
vi.mock('./utils.js', () => ({
getExtensionManager: vi.fn().mockResolvedValue({
@ -39,6 +33,12 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
};
});
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: vi.fn(),
clearScreen: vi.fn(),
}));
describe('extensions enable command', () => {
it('should fail if no name is provided', () => {
const validationParser = yargs([])
@ -70,10 +70,7 @@ describe('extensions enable command', () => {
});
describe('handleEnable', () => {
let consoleLogSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
});
@ -87,7 +84,7 @@ describe('handleEnable', () => {
'test-extension',
SettingScope.User,
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "test-extension" successfully enabled for scope "user".',
);
});
@ -102,7 +99,7 @@ describe('handleEnable', () => {
'test-extension',
SettingScope.Workspace,
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "test-extension" successfully enabled for scope "workspace".',
);
});
@ -116,7 +113,7 @@ describe('handleEnable', () => {
'test-extension',
SettingScope.User,
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "test-extension" successfully enabled in all scopes.',
);
});

View file

@ -4,15 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type MockInstance,
} from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { handleInstall, installCommand } from './install.js';
import yargs from 'yargs';
@ -23,6 +15,8 @@ const mockRequestConsentNonInteractive = vi.hoisted(() => vi.fn());
const mockRequestConsentOrFail = vi.hoisted(() => vi.fn());
const mockIsWorkspaceTrusted = vi.hoisted(() => vi.fn());
const mockLoadSettings = vi.hoisted(() => vi.fn());
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('@qwen-code/qwen-code-core', () => ({
ExtensionManager: vi.fn().mockImplementation(() => ({
@ -50,6 +44,12 @@ vi.mock('../../utils/errors.js', () => ({
getErrorMessage: vi.fn((error: Error) => error.message),
}));
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: mockWriteStderrLine,
clearScreen: vi.fn(),
}));
describe('extensions install command', () => {
it('should fail if no source is provided', () => {
const validationParser = yargs([])
@ -63,16 +63,7 @@ describe('extensions install command', () => {
});
describe('handleInstall', () => {
let consoleLogSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let processSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log');
consoleErrorSpy = vi.spyOn(console, 'error');
processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockRefreshCache.mockResolvedValue(undefined);
mockLoadSettings.mockReturnValue({ merged: {} });
mockIsWorkspaceTrusted.mockReturnValue(true);
@ -83,6 +74,10 @@ describe('handleInstall', () => {
});
it('should install an extension from a http source', async () => {
const processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockParseInstallSource.mockResolvedValue({
type: 'http',
url: 'http://google.com',
@ -93,12 +88,18 @@ describe('handleInstall', () => {
source: 'http://google.com',
});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "http-extension" installed successfully and enabled.',
);
processSpy.mockRestore();
});
it('should install an extension from a https source', async () => {
const processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockParseInstallSource.mockResolvedValue({
type: 'https',
url: 'https://google.com',
@ -109,12 +110,18 @@ describe('handleInstall', () => {
source: 'https://google.com',
});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "https-extension" installed successfully and enabled.',
);
processSpy.mockRestore();
});
it('should install an extension from a git source', async () => {
const processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockParseInstallSource.mockResolvedValue({
type: 'git',
url: 'git@some-url',
@ -125,12 +132,18 @@ describe('handleInstall', () => {
source: 'git@some-url',
});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "git-extension" installed successfully and enabled.',
);
processSpy.mockRestore();
});
it('throws an error from an unknown source', async () => {
const processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockParseInstallSource.mockRejectedValue(
new Error('Install source not found.'),
);
@ -138,11 +151,19 @@ describe('handleInstall', () => {
source: 'test://google.com',
});
expect(consoleErrorSpy).toHaveBeenCalledWith('Install source not found.');
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'Install source not found.',
);
expect(processSpy).toHaveBeenCalledWith(1);
processSpy.mockRestore();
});
it('should install an extension from a sso source', async () => {
const processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockParseInstallSource.mockResolvedValue({
type: 'sso',
url: 'sso://google.com',
@ -153,12 +174,18 @@ describe('handleInstall', () => {
source: 'sso://google.com',
});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "sso-extension" installed successfully and enabled.',
);
processSpy.mockRestore();
});
it('should install an extension from a local path', async () => {
const processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockParseInstallSource.mockResolvedValue({
type: 'local',
path: '/some/path',
@ -169,12 +196,18 @@ describe('handleInstall', () => {
source: '/some/path',
});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "local-extension" installed successfully and enabled.',
);
processSpy.mockRestore();
});
it('should throw an error if install extension fails', async () => {
const processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockParseInstallSource.mockResolvedValue({
type: 'git',
url: 'git@some-url',
@ -185,7 +218,11 @@ describe('handleInstall', () => {
await handleInstall({ source: 'git@some-url' });
expect(consoleErrorSpy).toHaveBeenCalledWith('Install extension failed');
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'Install extension failed',
);
expect(processSpy).toHaveBeenCalledWith(1);
processSpy.mockRestore();
});
});

View file

@ -4,18 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
type MockInstance,
} from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { linkCommand, handleLink } from './link.js';
import yargs from 'yargs';
const mockInstallExtension = vi.hoisted(() => vi.fn());
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('./utils.js', () => ({
getExtensionManager: vi.fn().mockResolvedValue({
@ -32,6 +27,12 @@ vi.mock('../../utils/errors.js', () => ({
getErrorMessage: vi.fn((error: Error) => error.message),
}));
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: mockWriteStderrLine,
clearScreen: vi.fn(),
}));
describe('extensions link command', () => {
it('should fail if no path is provided', () => {
const validationParser = yargs([])
@ -50,20 +51,15 @@ describe('extensions link command', () => {
});
describe('handleLink', () => {
let consoleLogSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let processExitSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
vi.clearAllMocks();
});
it('should link an extension from a local path', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockInstallExtension.mockResolvedValueOnce({ name: 'linked-extension' });
await handleLink({
@ -77,19 +73,27 @@ describe('handleLink', () => {
},
expect.any(Function),
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "linked-extension" linked successfully and enabled.',
);
processExitSpy.mockRestore();
});
it('should handle errors and exit with code 1', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockInstallExtension.mockRejectedValueOnce(new Error('Link failed'));
await handleLink({
path: '/some/local/path',
});
expect(consoleErrorSpy).toHaveBeenCalledWith('Link failed');
expect(mockWriteStderrLine).toHaveBeenCalledWith('Link failed');
expect(processExitSpy).toHaveBeenCalledWith(1);
processExitSpy.mockRestore();
});
});

View file

@ -4,19 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
type MockInstance,
} from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { listCommand, handleList } from './list.js';
import yargs from 'yargs';
const mockGetLoadedExtensions = vi.hoisted(() => vi.fn());
const mockToOutputString = vi.hoisted(() => vi.fn());
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('./utils.js', () => ({
getExtensionManager: vi.fn().mockResolvedValue({
@ -30,6 +25,12 @@ vi.mock('../../utils/errors.js', () => ({
getErrorMessage: vi.fn((error: Error) => error.message),
}));
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: mockWriteStderrLine,
clearScreen: vi.fn(),
}));
describe('extensions list command', () => {
it('should parse the list command', () => {
const parser = yargs([]).command(listCommand).fail(false).locale('en');
@ -38,28 +39,31 @@ describe('extensions list command', () => {
});
describe('handleList', () => {
let consoleLogSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let processExitSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
vi.clearAllMocks();
});
it('should display message when no extensions are installed', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockGetLoadedExtensions.mockReturnValueOnce([]);
await handleList();
expect(consoleLogSpy).toHaveBeenCalledWith('No extensions installed.');
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'No extensions installed.',
);
processExitSpy.mockRestore();
});
it('should list installed extensions', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
const mockExtensions = [
{ name: 'extension-1', version: '1.0.0' },
{ name: 'extension-2', version: '2.0.0' },
@ -73,19 +77,27 @@ describe('handleList', () => {
expect(mockGetLoadedExtensions).toHaveBeenCalled();
expect(mockToOutputString).toHaveBeenCalledTimes(2);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'extension-1 (1.0.0)\n\nextension-2 (2.0.0)',
);
processExitSpy.mockRestore();
});
it('should handle errors and exit with code 1', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockGetLoadedExtensions.mockImplementationOnce(() => {
throw new Error('List failed');
});
await handleList();
expect(consoleErrorSpy).toHaveBeenCalledWith('List failed');
expect(mockWriteStderrLine).toHaveBeenCalledWith('List failed');
expect(processExitSpy).toHaveBeenCalledWith(1);
processExitSpy.mockRestore();
});
});

View file

@ -4,14 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
type MockInstance,
} from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { settingsCommand } from './settings.js';
import yargs from 'yargs';
@ -19,6 +12,13 @@ const mockGetLoadedExtensions = vi.hoisted(() => vi.fn());
const mockGetScopedEnvContents = vi.hoisted(() => vi.fn());
const mockUpdateSetting = vi.hoisted(() => vi.fn());
const mockPromptForSetting = vi.hoisted(() => vi.fn());
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: vi.fn(),
clearScreen: vi.fn(),
}));
vi.mock('./utils.js', () => ({
getExtensionManager: vi.fn().mockResolvedValue({
@ -89,10 +89,7 @@ describe('extensions settings command', () => {
});
describe('settings set handler', () => {
let consoleLogSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
});
@ -123,7 +120,7 @@ describe('settings set handler', () => {
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings set my-extension API_KEY');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "my-extension" not found.',
);
expect(mockUpdateSetting).not.toHaveBeenCalled();
@ -173,10 +170,7 @@ describe('settings set handler', () => {
});
describe('settings list handler', () => {
let consoleLogSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
});
@ -207,7 +201,7 @@ describe('settings list handler', () => {
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "my-extension" not found.',
);
});
@ -224,7 +218,7 @@ describe('settings list handler', () => {
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "my-extension" has no settings to configure.',
);
});
@ -257,10 +251,16 @@ describe('settings list handler', () => {
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(consoleLogSpy).toHaveBeenCalledWith('Settings for "my-extension":');
expect(consoleLogSpy).toHaveBeenCalledWith('\n- API Key (API_KEY)');
expect(consoleLogSpy).toHaveBeenCalledWith(' Description: Your API key');
expect(consoleLogSpy).toHaveBeenCalledWith(' Value: my-api-key (user)');
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Settings for "my-extension":',
);
expect(mockWriteStdoutLine).toHaveBeenCalledWith('\n- API Key (API_KEY)');
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
' Description: Your API key',
);
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
' Value: my-api-key (user)',
);
});
it('should show workspace scope for workspace-scoped settings', async () => {
@ -286,7 +286,7 @@ describe('settings list handler', () => {
await parser.parseAsync('settings list my-extension');
// Workspace should override user, and show (workspace) scope
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
' Value: workspace-value (workspace)',
);
});
@ -313,7 +313,7 @@ describe('settings list handler', () => {
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(consoleLogSpy).toHaveBeenCalledWith(' Value: [not set]');
expect(mockWriteStdoutLine).toHaveBeenCalledWith(' Value: [not set]');
});
it('should show [value stored in keychain] for sensitive settings', async () => {
@ -338,7 +338,7 @@ describe('settings list handler', () => {
const parser = yargs([]).command(settingsCommand);
await parser.parseAsync('settings list my-extension');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
' Value: [value stored in keychain] (user)',
);
});

View file

@ -4,14 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
type MockInstance,
} from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { updateCommand, handleUpdate } from './update.js';
import yargs from 'yargs';
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
@ -21,6 +14,8 @@ const mockUpdateExtension = vi.hoisted(() => vi.fn());
const mockCheckForAllExtensionUpdates = vi.hoisted(() => vi.fn());
const mockUpdateAllUpdatableExtensions = vi.hoisted(() => vi.fn());
const mockCheckForExtensionUpdate = vi.hoisted(() => vi.fn());
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('./utils.js', () => ({
getExtensionManager: vi.fn().mockResolvedValue({
@ -47,6 +42,12 @@ vi.mock('../../ui/state/extensions.js', () => ({
},
}));
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: mockWriteStderrLine,
clearScreen: vi.fn(),
}));
describe('extensions update command', () => {
it('should fail if neither name nor --all is provided', () => {
const validationParser = yargs([])
@ -80,12 +81,7 @@ describe('extensions update command', () => {
});
describe('handleUpdate', () => {
let consoleLogSpy: MockInstance;
let consoleErrorSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
vi.clearAllMocks();
});
@ -95,7 +91,7 @@ describe('handleUpdate', () => {
await handleUpdate({ name: 'non-existent-extension' });
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "non-existent-extension" not found.',
);
});
@ -107,7 +103,7 @@ describe('handleUpdate', () => {
await handleUpdate({ name: 'test-extension' });
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Unable to install extension "test-extension" due to missing install metadata',
);
});
@ -124,7 +120,7 @@ describe('handleUpdate', () => {
await handleUpdate({ name: 'test-extension' });
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "test-extension" is already up to date.',
);
});
@ -151,7 +147,7 @@ describe('handleUpdate', () => {
ExtensionUpdateState.UPDATE_AVAILABLE,
expect.any(Function),
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "test-extension" successfully updated: 1.0.0 → 2.0.0.',
);
});
@ -173,7 +169,7 @@ describe('handleUpdate', () => {
await handleUpdate({ name: 'test-extension' });
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "test-extension" is already up to date.',
);
});
@ -190,7 +186,7 @@ describe('handleUpdate', () => {
await handleUpdate({ name: 'test-extension' });
expect(consoleErrorSpy).toHaveBeenCalledWith('Update check failed');
expect(mockWriteStderrLine).toHaveBeenCalledWith('Update check failed');
});
});
@ -201,7 +197,9 @@ describe('handleUpdate', () => {
await handleUpdate({ all: true });
expect(consoleLogSpy).toHaveBeenCalledWith('No extensions to update.');
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'No extensions to update.',
);
});
it('should update all extensions with updates available', async () => {
@ -221,7 +219,7 @@ describe('handleUpdate', () => {
await handleUpdate({ all: true });
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "extension-1" successfully updated: 1.0.0 → 2.0.0.\n' +
'Extension "extension-2" successfully updated: 1.0.0 → 1.5.0.',
);
@ -244,7 +242,7 @@ describe('handleUpdate', () => {
await handleUpdate({ all: true });
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Extension "extension-1" successfully updated: 1.0.0 → 2.0.0.',
);
});
@ -256,7 +254,7 @@ describe('handleUpdate', () => {
await handleUpdate({ all: true });
expect(consoleErrorSpy).toHaveBeenCalledWith('Update all failed');
expect(mockWriteStderrLine).toHaveBeenCalledWith('Update all failed');
});
});
});

View file

@ -9,6 +9,15 @@ import { addCommand } from './add.js';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: mockWriteStderrLine,
clearScreen: vi.fn(),
}));
vi.mock('fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs/promises')>();
return {
@ -41,15 +50,13 @@ const mockedLoadSettings = loadSettings as vi.Mock;
describe('mcp add command', () => {
let parser: yargs.Argv;
let mockSetValue: vi.Mock;
let mockConsoleError: vi.Mock;
beforeEach(() => {
vi.resetAllMocks();
const yargsInstance = yargs([]).command(addCommand);
parser = yargsInstance;
mockSetValue = vi.fn();
mockConsoleError = vi.fn();
vi.spyOn(console, 'error').mockImplementation(mockConsoleError);
mockWriteStderrLine.mockClear();
mockedLoadSettings.mockReturnValue({
forScope: () => ({ settings: {} }),
setValue: mockSetValue,
@ -218,7 +225,7 @@ describe('mcp add command', () => {
parser.parseAsync(`add ${serverName} ${command}`),
).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'Error: Please use --scope user to edit settings in the home directory.',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
@ -236,7 +243,7 @@ describe('mcp add command', () => {
parser.parseAsync(`add --scope project ${serverName} ${command}`),
).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'Error: Please use --scope user to edit settings in the home directory.',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
@ -250,7 +257,7 @@ describe('mcp add command', () => {
'mcpServers',
expect.any(Object),
);
expect(mockConsoleError).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
});

View file

@ -4,13 +4,22 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { listMcpServers } from './list.js';
import { loadSettings } from '../../config/settings.js';
import { isWorkspaceTrusted } from '../../config/trustedFolders.js';
import { createTransport, ExtensionManager } from '@qwen-code/qwen-code-core';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: mockWriteStderrLine,
clearScreen: vi.fn(),
}));
vi.mock('../../config/settings.js', () => ({
loadSettings: vi.fn(),
}));
@ -46,7 +55,6 @@ interface MockTransport {
}
describe('mcp list command', () => {
let consoleSpy: vi.SpyInstance;
let mockClient: MockClient;
let mockTransport: MockTransport;
let mockExtensionManager: {
@ -56,8 +64,7 @@ describe('mcp list command', () => {
beforeEach(() => {
vi.resetAllMocks();
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
mockWriteStdoutLine.mockClear();
mockTransport = { close: vi.fn() };
mockClient = {
@ -77,16 +84,14 @@ describe('mcp list command', () => {
mockedIsWorkspaceTrusted.mockReturnValue(true);
});
afterEach(() => {
consoleSpy.mockRestore();
});
it('should display message when no servers configured', async () => {
mockedLoadSettings.mockReturnValue({ merged: { mcpServers: {} } });
await listMcpServers();
expect(consoleSpy).toHaveBeenCalledWith('No MCP servers configured.');
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'No MCP servers configured.',
);
});
it('should display different server types with connected status', async () => {
@ -105,18 +110,20 @@ describe('mcp list command', () => {
await listMcpServers();
expect(consoleSpy).toHaveBeenCalledWith('Configured MCP servers:\n');
expect(consoleSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Configured MCP servers:\n',
);
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
expect.stringContaining(
'stdio-server: /path/to/server arg1 (stdio) - Connected',
),
);
expect(consoleSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
expect.stringContaining(
'sse-server: https://example.com/sse (sse) - Connected',
),
);
expect(consoleSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
expect.stringContaining(
'http-server: https://example.com/http (http) - Connected',
),
@ -136,7 +143,7 @@ describe('mcp list command', () => {
await listMcpServers();
expect(consoleSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
expect.stringContaining(
'test-server: /test/server (stdio) - Disconnected',
),
@ -165,12 +172,12 @@ describe('mcp list command', () => {
await listMcpServers();
expect(consoleSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
expect.stringContaining(
'config-server: /config/server (stdio) - Connected',
),
);
expect(consoleSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
expect.stringContaining(
'extension-server: /ext/server (stdio) - Connected',
),

View file

@ -9,6 +9,15 @@ import yargs from 'yargs';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { removeCommand } from './remove.js';
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: mockWriteStderrLine,
clearScreen: vi.fn(),
}));
vi.mock('fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs/promises')>();
return {
@ -49,6 +58,7 @@ describe('mcp remove command', () => {
forScope: () => ({ settings: mockSettings }),
setValue: mockSetValue,
});
mockWriteStdoutLine.mockClear();
});
it('should remove a server from project settings', async () => {
@ -62,11 +72,10 @@ describe('mcp remove command', () => {
});
it('should show a message if server not found', async () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
await parser.parseAsync('remove non-existent-server');
expect(mockSetValue).not.toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Server "non-existent-server" not found in project settings.',
);
});

View file

@ -20,6 +20,15 @@ import type { Settings } from './settings.js';
import * as ServerConfig from '@qwen-code/qwen-code-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
vi.mock('../utils/stdioHelpers.js', () => ({
writeStderrLine: mockWriteStderrLine,
writeStdoutLine: mockWriteStdoutLine,
clearScreen: vi.fn(),
}));
const createNativeLspServiceInstance = () => ({
discoverAndPrepare: vi.fn(),
start: vi.fn(),
@ -158,23 +167,19 @@ describe('parseArguments', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockWriteStderrLine.mockClear();
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
expect.stringContaining(
'Cannot use both --prompt (-p) and --prompt-interactive (-i) together',
),
);
mockExit.mockRestore();
mockConsoleError.mockRestore();
});
it('should throw an error when using short flags -p and -i together', async () => {
@ -190,23 +195,19 @@ describe('parseArguments', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockWriteStderrLine.mockClear();
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
expect.stringContaining(
'Cannot use both --prompt (-p) and --prompt-interactive (-i) together',
),
);
mockExit.mockRestore();
mockConsoleError.mockRestore();
});
it('should allow --prompt without --prompt-interactive', async () => {
@ -389,23 +390,19 @@ describe('parseArguments', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockWriteStderrLine.mockClear();
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
expect.stringContaining(
'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.',
),
);
mockExit.mockRestore();
mockConsoleError.mockRestore();
});
it('should throw an error when using short flags -y and --approval-mode together', async () => {
@ -414,23 +411,19 @@ describe('parseArguments', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockWriteStderrLine.mockClear();
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
expect.stringContaining(
'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.',
),
);
mockExit.mockRestore();
mockConsoleError.mockRestore();
});
it('should throw an error when include-partial-messages is used without stream-json output', async () => {
@ -439,23 +432,19 @@ describe('parseArguments', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockWriteStderrLine.mockClear();
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
expect.stringContaining(
'--include-partial-messages requires --output-format stream-json',
),
);
mockExit.mockRestore();
mockConsoleError.mockRestore();
});
it('should parse stream-json formats and include-partial-messages flag', async () => {
@ -496,21 +485,17 @@ describe('parseArguments', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockWriteStderrLine.mockClear();
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
expect.stringContaining('Invalid values:'),
);
mockExit.mockRestore();
mockConsoleError.mockRestore();
});
it('should support comma-separated values for --allowed-tools', async () => {
@ -900,21 +885,17 @@ describe('loadCliConfig telemetry', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockWriteStderrLine.mockClear();
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
expect.stringContaining('Invalid values:'),
);
mockExit.mockRestore();
mockConsoleError.mockRestore();
});
});
@ -2138,17 +2119,14 @@ describe('Output format', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockWriteStderrLine.mockClear();
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
expect.stringContaining('Invalid values:'),
);
mockExit.mockRestore();
mockConsoleError.mockRestore();
});
});
@ -2172,23 +2150,19 @@ describe('parseArguments with positional prompt', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockWriteStderrLine.mockClear();
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
expect.stringContaining(
'Cannot use both a positional prompt and the --prompt (-p) flag together',
),
);
mockExit.mockRestore();
mockConsoleError.mockRestore();
});
it('should correctly parse a positional prompt to query field', async () => {

View file

@ -45,7 +45,6 @@ export enum Command {
PASTE_CLIPBOARD_IMAGE = 'pasteClipboardImage',
// App level bindings
SHOW_ERROR_DETAILS = 'showErrorDetails',
TOGGLE_TOOL_DESCRIPTIONS = 'toggleToolDescriptions',
TOGGLE_IDE_CONTEXT_DETAIL = 'toggleIDEContextDetail',
QUIT = 'quit',
@ -156,7 +155,6 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.PASTE_CLIPBOARD_IMAGE]: [{ key: 'v', ctrl: true }],
// App level bindings
[Command.SHOW_ERROR_DETAILS]: [{ key: 'o', ctrl: true }],
[Command.TOGGLE_TOOL_DESCRIPTIONS]: [{ key: 't', ctrl: true }],
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }],
[Command.QUIT]: [{ key: 'c', ctrl: true }],

View file

@ -24,6 +24,8 @@ import { appEvents, AppEvent } from './utils/events.js';
import type { Config } from '@qwen-code/qwen-code-core';
import { OutputFormat } from '@qwen-code/qwen-code-core';
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
// Custom error to identify mock process.exit calls
class MockProcessExitError extends Error {
constructor(readonly code?: string | number | null | undefined) {
@ -79,6 +81,12 @@ vi.mock('./utils/sandbox.js', () => ({
start_sandbox: vi.fn(() => Promise.resolve()), // Mock as an async function that resolves
}));
vi.mock('./utils/stdioHelpers.js', () => ({
writeStderrLine: mockWriteStderrLine,
writeStdoutLine: vi.fn(),
clearScreen: vi.fn(),
}));
vi.mock('./utils/relaunch.js', () => ({
relaunchAppInChildProcess: vi.fn(),
}));
@ -501,34 +509,28 @@ describe('gemini.tsx main function kitty protocol', () => {
});
describe('validateDnsResolutionOrder', () => {
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
consoleWarnSpy.mockRestore();
mockWriteStderrLine.mockClear();
});
it('should return "ipv4first" when the input is "ipv4first"', () => {
expect(validateDnsResolutionOrder('ipv4first')).toBe('ipv4first');
expect(consoleWarnSpy).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
it('should return "verbatim" when the input is "verbatim"', () => {
expect(validateDnsResolutionOrder('verbatim')).toBe('verbatim');
expect(consoleWarnSpy).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
it('should return the default "ipv4first" when the input is undefined', () => {
expect(validateDnsResolutionOrder(undefined)).toBe('ipv4first');
expect(consoleWarnSpy).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
it('should return the default "ipv4first" and log a warning for an invalid string', () => {
expect(validateDnsResolutionOrder('invalid-value')).toBe('ipv4first');
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'Invalid value for dnsResolutionOrder in settings: "invalid-value". Using default "ipv4first".',
);
});

View file

@ -37,7 +37,6 @@ import { SettingsContext } from './ui/contexts/SettingsContext.js';
import { VimModeProvider } from './ui/contexts/VimModeContext.js';
import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js';
import { themeManager } from './ui/themes/theme-manager.js';
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
import { detectAndEnableKittyProtocol } from './ui/utils/kittyProtocolDetector.js';
import { checkForUpdates } from './ui/utils/updateCheck.js';
import {
@ -359,15 +358,6 @@ export async function main() {
// process.exit(0);
// }
// Setup unified ConsolePatcher based on interactive mode
const isInteractive = config.isInteractive();
const consolePatcher = new ConsolePatcher({
stderr: isInteractive,
debugMode: isDebugMode,
});
consolePatcher.patch();
registerCleanup(consolePatcher.cleanup);
const wasRaw = process.stdin.isRaw;
let kittyProtocolDetectionComplete: Promise<boolean> | undefined;
if (config.isInteractive() && !wasRaw && process.stdin.isTTY) {

View file

@ -457,9 +457,6 @@ describe('ControlDispatcher', () => {
it('should handle response for non-existent request in debug mode', () => {
const context = createMockContext(true);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const dispatcherWithDebug = new ControlDispatcher(context);
const response: CLIControlResponse = {
@ -471,15 +468,10 @@ describe('ControlDispatcher', () => {
},
};
dispatcherWithDebug.handleControlResponse(response);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'[ControlDispatcher] No pending outgoing request for: non-existent',
),
);
consoleSpy.mockRestore();
// Should not throw in debug mode
expect(() =>
dispatcherWithDebug.handleControlResponse(response),
).not.toThrow();
});
});
@ -599,11 +591,8 @@ describe('ControlDispatcher', () => {
expect(() => dispatcher.handleCancel('non-existent')).not.toThrow();
});
it('should log cancellation in debug mode', () => {
it('should cancel request in debug mode without throwing', () => {
const context = createMockContext(true);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const dispatcherWithDebug = new ControlDispatcher(context);
const requestId = 'cancel-req-debug';
@ -626,15 +615,7 @@ describe('ControlDispatcher', () => {
timeoutId,
);
dispatcherWithDebug.handleCancel(requestId);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'[ControlDispatcher] Cancelled incoming request: cancel-req-debug',
),
);
consoleSpy.mockRestore();
expect(() => dispatcherWithDebug.handleCancel(requestId)).not.toThrow();
});
});
@ -720,11 +701,8 @@ describe('ControlDispatcher', () => {
expect(secondRejectCount).toBe(firstRejectCount);
});
it('should log input closure in debug mode', () => {
it('should mark input closed in debug mode without throwing', () => {
const context = createMockContext(true);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const dispatcherWithDebug = new ControlDispatcher(context);
const requestId = 'reject-req-debug';
@ -750,15 +728,7 @@ describe('ControlDispatcher', () => {
timeoutId,
);
dispatcherWithDebug.markInputClosed();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'[ControlDispatcher] Input closed, rejecting 1 pending outgoing requests',
),
);
consoleSpy.mockRestore();
expect(() => dispatcherWithDebug.markInputClosed()).not.toThrow();
});
});
@ -848,21 +818,12 @@ describe('ControlDispatcher', () => {
expect(mockSystemController.cleanup).toHaveBeenCalled();
});
it('should log shutdown in debug mode', () => {
it('should shutdown in debug mode without throwing', () => {
const context = createMockContext(true);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const dispatcherWithDebug = new ControlDispatcher(context);
dispatcherWithDebug.shutdown();
expect(consoleSpy).toHaveBeenCalledWith(
'[ControlDispatcher] Shutting down',
);
consoleSpy.mockRestore();
expect(() => dispatcherWithDebug.shutdown()).not.toThrow();
});
});

View file

@ -219,7 +219,7 @@ export class ControlDispatcher implements IPendingRequestRegistry {
const requestIds = Array.from(this.pendingOutgoingRequests.keys());
if (this.context.debugMode) {
console.error(
debugLogger.debug(
`[ControlDispatcher] Input closed, rejecting ${requestIds.length} pending outgoing requests`,
);
}

View file

@ -18,7 +18,6 @@ import { StreamJsonOutputAdapter } from './io/StreamJsonOutputAdapter.js';
import { ControlDispatcher } from './control/ControlDispatcher.js';
import { ControlContext } from './control/ControlContext.js';
import { ControlService } from './control/ControlService.js';
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
const runNonInteractiveMock = vi.fn();
@ -47,10 +46,6 @@ vi.mock('./control/ControlService.js', () => ({
ControlService: vi.fn(),
}));
vi.mock('../ui/utils/ConsolePatcher.js', () => ({
ConsolePatcher: vi.fn(),
}));
interface ConfigOverrides {
getSessionId?: () => string;
getModel?: () => string;
@ -160,24 +155,11 @@ describe('runNonInteractiveStreamJson', () => {
createSendSdkMcpMessage: ReturnType<typeof vi.fn>;
};
};
let mockConsolePatcher: {
patch: ReturnType<typeof vi.fn>;
cleanup: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
config = createConfig();
runNonInteractiveMock.mockReset();
// Setup mocks
mockConsolePatcher = {
patch: vi.fn(),
cleanup: vi.fn(),
};
(ConsolePatcher as unknown as ReturnType<typeof vi.fn>).mockImplementation(
() => mockConsolePatcher,
);
mockOutputAdapter = {
emitResult: vi.fn(),
} as {
@ -236,9 +218,7 @@ describe('runNonInteractiveStreamJson', () => {
await runNonInteractiveStreamJson(config, '');
expect(mockConsolePatcher.patch).toHaveBeenCalledTimes(1);
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(initRequest);
expect(mockConsolePatcher.cleanup).toHaveBeenCalledTimes(1);
});
it('processes user message when received as first message', async () => {
@ -489,8 +469,6 @@ describe('runNonInteractiveStreamJson', () => {
await expect(runNonInteractiveStreamJson(config, '')).rejects.toThrow(
'Stream error',
);
expect(mockConsolePatcher.cleanup).toHaveBeenCalled();
});
it('stops processing when abort signal is triggered', async () => {
@ -567,9 +545,6 @@ describe('runNonInteractiveStreamJson', () => {
};
await runNonInteractiveStreamJson(config, '');
expect(mockConsolePatcher.patch).toHaveBeenCalledTimes(1);
expect(mockConsolePatcher.cleanup).toHaveBeenCalledTimes(1);
});
it('cleans up output adapter on completion', async () => {
@ -598,7 +573,5 @@ describe('runNonInteractiveStreamJson', () => {
};
await runNonInteractiveStreamJson(config, '');
expect(mockConsolePatcher.cleanup).toHaveBeenCalled();
});
});

View file

@ -33,7 +33,6 @@ import {
} from './types.js';
import { createMinimalSettings } from '../config/settings.js';
import { runNonInteractive } from '../nonInteractiveCli.js';
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
const debugLogger = createDebugLogger('NON_INTERACTIVE_SESSION');
@ -607,29 +606,20 @@ export async function runNonInteractiveStreamJson(
config: Config,
input: string,
): Promise<void> {
const consolePatcher = new ConsolePatcher({
debugMode: config.getDebugMode(),
});
consolePatcher.patch();
try {
let initialPrompt: CLIUserMessage | undefined = undefined;
if (input && input.trim().length > 0) {
const sessionId = config.getSessionId();
initialPrompt = {
type: 'user',
session_id: sessionId,
message: {
role: 'user',
content: input.trim(),
},
parent_tool_use_id: null,
};
}
const manager = new Session(config, initialPrompt);
await manager.run();
} finally {
consolePatcher.cleanup();
let initialPrompt: CLIUserMessage | undefined = undefined;
if (input && input.trim().length > 0) {
const sessionId = config.getSessionId();
initialPrompt = {
type: 'user',
session_id: sessionId,
message: {
role: 'user',
content: input.trim(),
},
parent_tool_use_id: null,
};
}
const manager = new Session(config, initialPrompt);
await manager.run();
}

View file

@ -66,7 +66,6 @@ describe('runNonInteractive', () => {
let mockToolRegistry: ToolRegistry;
let mockCoreExecuteToolCall: Mock;
let mockShutdownTelemetry: Mock;
let consoleErrorSpy: MockInstance;
let processStdoutSpy: MockInstance;
let processStderrSpy: MockInstance;
let mockGeminiClient: {
@ -83,7 +82,6 @@ describe('runNonInteractive', () => {
getCommands: mockGetCommands,
});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
processStdoutSpy = vi
.spyOn(process.stdout, 'write')
.mockImplementation(() => true);
@ -360,9 +358,6 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents([toolCallEvent]))
.mockReturnValueOnce(createStreamFromEvents(finalResponse));
// Enable debug mode so handleToolError logs to console.error
(mockConfig.getDebugMode as Mock).mockReturnValue(true);
await runNonInteractive(
mockConfig,
mockSettings,
@ -371,9 +366,6 @@ describe('runNonInteractive', () => {
);
expect(mockCoreExecuteToolCall).toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error executing tool errorTool: Execution failed',
);
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith(
2,
@ -443,9 +435,6 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents([toolCallEvent]))
.mockReturnValueOnce(createStreamFromEvents(finalResponse));
// Enable debug mode so handleToolError logs to console.error
(mockConfig.getDebugMode as Mock).mockReturnValue(true);
await runNonInteractive(
mockConfig,
mockSettings,
@ -454,9 +443,6 @@ describe('runNonInteractive', () => {
);
expect(mockCoreExecuteToolCall).toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error executing tool nonexistentTool: Tool "nonexistentTool" not found in registry.',
);
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
expect(processStdoutSpy).toHaveBeenCalledWith(
"Sorry, I can't find that tool.",
@ -738,11 +724,6 @@ describe('runNonInteractive', () => {
throw testError;
});
// Mock console.error to capture JSON error output
const consoleErrorJsonSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
let thrownError: Error | null = null;
try {
await runNonInteractive(
@ -760,19 +741,18 @@ describe('runNonInteractive', () => {
// Should throw because of mocked process.exit
expect(thrownError?.message).toBe('process.exit(1) called');
expect(consoleErrorJsonSpy).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
type: 'Error',
message: 'Invalid input provided',
code: 1,
},
const jsonError = JSON.stringify(
{
error: {
type: 'Error',
message: 'Invalid input provided',
code: 1,
},
null,
2,
),
},
null,
2,
);
expect(processStderrSpy).toHaveBeenCalledWith(`${jsonError}\n`);
});
it('should handle API errors in text mode and exit with error code', async () => {
@ -830,11 +810,6 @@ describe('runNonInteractive', () => {
throw fatalError;
});
// Mock console.error to capture JSON error output
const consoleErrorJsonSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
let thrownError: Error | null = null;
try {
await runNonInteractive(
@ -852,19 +827,18 @@ describe('runNonInteractive', () => {
// Should throw because of mocked process.exit with custom exit code
expect(thrownError?.message).toBe('process.exit(42) called');
expect(consoleErrorJsonSpy).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
type: 'FatalInputError',
message: 'Invalid command syntax provided',
code: 42,
},
const jsonError = JSON.stringify(
{
error: {
type: 'FatalInputError',
message: 'Invalid command syntax provided',
code: 42,
},
null,
2,
),
},
null,
2,
);
expect(processStderrSpy).toHaveBeenCalledWith(`${jsonError}\n`);
});
it('should execute a slash command that returns a prompt', async () => {

View file

@ -62,7 +62,6 @@ vi.mock('./hooks/useEditorSettings.js');
vi.mock('./hooks/useSettingsCommand.js');
vi.mock('./hooks/useModelCommand.js');
vi.mock('./hooks/slashCommandProcessor.js');
vi.mock('./hooks/useConsoleMessages.js');
vi.mock('./hooks/useTerminalSize.js', () => ({
useTerminalSize: vi.fn(() => ({ columns: 80, rows: 24 })),
}));
@ -85,7 +84,6 @@ vi.mock('./hooks/useLogger.js');
// Mock external utilities
vi.mock('../utils/events.js');
vi.mock('../utils/handleAutoUpdate.js');
vi.mock('./utils/ConsolePatcher.js');
vi.mock('../utils/cleanup.js');
import { useHistory } from './hooks/useHistoryManager.js';
@ -95,7 +93,6 @@ import { useEditorSettings } from './hooks/useEditorSettings.js';
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { useModelCommand } from './hooks/useModelCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useVim } from './hooks/vim.js';
import { useFolderTrust } from './hooks/useFolderTrust.js';
@ -125,7 +122,6 @@ describe('AppContainer State Management', () => {
const mockedUseSettingsCommand = useSettingsCommand as Mock;
const mockedUseModelCommand = useModelCommand as Mock;
const mockedUseSlashCommandProcessor = useSlashCommandProcessor as Mock;
const mockedUseConsoleMessages = useConsoleMessages as Mock;
const mockedUseGeminiStream = useGeminiStream as Mock;
const mockedUseVim = useVim as Mock;
const mockedUseFolderTrust = useFolderTrust as Mock;
@ -206,11 +202,6 @@ describe('AppContainer State Management', () => {
shellConfirmationRequest: null,
confirmationRequest: null,
});
mockedUseConsoleMessages.mockReturnValue({
consoleMessages: [],
handleNewMessage: vi.fn(),
clearConsoleMessages: vi.fn(),
});
mockedUseGeminiStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),

View file

@ -56,7 +56,6 @@ import { useApprovalModeCommand } from './hooks/useApprovalModeCommand.js';
import { useResumeCommand } from './hooks/useResumeCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useVimMode } from './contexts/VimModeContext.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { calculatePromptWidths } from './components/InputPrompt.js';
import { useStdin, useStdout } from 'ink';
@ -82,10 +81,8 @@ import { type IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js';
import { type CommandMigrationNudgeResult } from './CommandFormatMigrationNudge.js';
import { useCommandMigration } from './hooks/useCommandMigration.js';
import { migrateTomlCommands } from '../services/command-migration-tool.js';
import { appEvents, AppEvent } from '../utils/events.js';
import { type UpdateObject } from './utils/updateCheck.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { ConsolePatcher } from './utils/ConsolePatcher.js';
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
@ -316,21 +313,6 @@ export const AppContainer = (props: AppContainerProps) => {
return () => clearInterval(interval);
}, [config, currentModel, getCurrentModel]);
const {
consoleMessages,
handleNewMessage,
clearConsoleMessages: clearConsoleMessagesState,
} = useConsoleMessages();
useEffect(() => {
const consolePatcher = new ConsolePatcher({
onNewMessage: handleNewMessage,
debugMode: config.getDebugMode(),
});
consolePatcher.patch();
registerCleanup(consolePatcher.cleanup);
}, [handleNewMessage, config]);
// Derive widths for InputPrompt using shared helper
const { inputWidth, suggestionsWidth } = useMemo(() => {
const { inputWidth, suggestionsWidth } =
@ -772,10 +754,9 @@ export const AppContainer = (props: AppContainerProps) => {
const handleClearScreen = useCallback(() => {
historyManager.clearItems();
clearConsoleMessagesState();
clearScreen();
refreshStatic();
}, [historyManager, clearConsoleMessagesState, refreshStatic]);
}, [historyManager, refreshStatic]);
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
@ -903,7 +884,6 @@ export const AppContainer = (props: AppContainerProps) => {
setShowMigrationNudge: setShowCommandMigrationNudge,
} = useCommandMigration(settings, config.storage);
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
const [showToolDescriptions, setShowToolDescriptions] =
useState<boolean>(false);
@ -954,28 +934,6 @@ export const AppContainer = (props: AppContainerProps) => {
return unsubscribe;
}, []);
useEffect(() => {
const openDebugConsole = () => {
setShowErrorDetails(true);
setConstrainHeight(false);
};
appEvents.on(AppEvent.OpenDebugConsole, openDebugConsole);
const logErrorHandler = (errorMessage: unknown) => {
handleNewMessage({
type: 'error',
content: String(errorMessage),
count: 1,
});
};
appEvents.on(AppEvent.LogError, logErrorHandler);
return () => {
appEvents.off(AppEvent.OpenDebugConsole, openDebugConsole);
appEvents.off(AppEvent.LogError, logErrorHandler);
};
}, [handleNewMessage]);
const handleEscapePromptChange = useCallback((showPrompt: boolean) => {
setShowEscapePrompt(showPrompt);
}, []);
@ -1217,9 +1175,7 @@ export const AppContainer = (props: AppContainerProps) => {
setConstrainHeight(true);
}
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
setShowErrorDetails((prev) => !prev);
} else if (keyMatchers[Command.TOGGLE_TOOL_DESCRIPTIONS](key)) {
if (keyMatchers[Command.TOGGLE_TOOL_DESCRIPTIONS](key)) {
const newValue = !showToolDescriptions;
setShowToolDescriptions(newValue);
@ -1247,7 +1203,6 @@ export const AppContainer = (props: AppContainerProps) => {
[
constrainHeight,
setConstrainHeight,
setShowErrorDetails,
showToolDescriptions,
setShowToolDescriptions,
config,
@ -1306,22 +1261,6 @@ export const AppContainer = (props: AppContainerProps) => {
stdout,
]);
const filteredConsoleMessages = useMemo(() => {
if (config.getDebugMode()) {
return consoleMessages;
}
return consoleMessages.filter((msg) => msg.type !== 'debug');
}, [consoleMessages, config]);
// Computed values
const errorCount = useMemo(
() =>
filteredConsoleMessages
.filter((msg) => msg.type === 'error')
.reduce((total, msg) => total + msg.count, 0),
[filteredConsoleMessages],
);
const nightly = props.version.includes('nightly');
const dialogsVisible =
@ -1416,8 +1355,6 @@ export const AppContainer = (props: AppContainerProps) => {
isFolderTrustDialogOpen: isFolderTrustDialogOpen ?? false,
isTrustedFolder,
constrainHeight,
showErrorDetails,
filteredConsoleMessages,
ideContextState,
showToolDescriptions,
ctrlCPressedOnce,
@ -1431,7 +1368,6 @@ export const AppContainer = (props: AppContainerProps) => {
showAutoAcceptIndicator,
currentModel,
contextFileNames,
errorCount,
availableTerminalHeight,
mainAreaWidth,
staticAreaMaxItemHeight,
@ -1509,8 +1445,6 @@ export const AppContainer = (props: AppContainerProps) => {
isFolderTrustDialogOpen,
isTrustedFolder,
constrainHeight,
showErrorDetails,
filteredConsoleMessages,
ideContextState,
showToolDescriptions,
ctrlCPressedOnce,
@ -1523,7 +1457,6 @@ export const AppContainer = (props: AppContainerProps) => {
messageQueue,
showAutoAcceptIndicator,
contextFileNames,
errorCount,
availableTerminalHeight,
mainAreaWidth,
staticAreaMaxItemHeight,

View file

@ -34,6 +34,12 @@ describe('copyCommand', () => {
getGeminiClient: () => ({
getChat: mockGetChat,
}),
getDebugLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
},
},
});

View file

@ -43,10 +43,6 @@ vi.mock('./ShellModeIndicator.js', () => ({
ShellModeIndicator: () => <Text>ShellModeIndicator</Text>,
}));
vi.mock('./DetailedMessagesDisplay.js', () => ({
DetailedMessagesDisplay: () => <Text>DetailedMessagesDisplay</Text>,
}));
vi.mock('./InputPrompt.js', () => ({
InputPrompt: () => <Text>InputPrompt</Text>,
calculatePromptWidths: vi.fn(() => ({
@ -60,10 +56,6 @@ vi.mock('./Footer.js', () => ({
Footer: () => <Text>Footer</Text>,
}));
vi.mock('./ShowMoreLines.js', () => ({
ShowMoreLines: () => <Text>ShowMoreLines</Text>,
}));
vi.mock('./QueuedMessageDisplay.js', () => ({
QueuedMessageDisplay: ({ messageQueue }: { messageQueue: string[] }) => {
if (messageQueue.length === 0) {
@ -91,7 +83,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
contextFileNames: [],
showAutoAcceptIndicator: ApprovalMode.DEFAULT,
messageQueue: [],
showErrorDetails: false,
constrainHeight: false,
isInputActive: true,
buffer: '',
@ -111,7 +102,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
ideContextState: null,
geminiMdFileCount: 0,
showToolDescriptions: false,
filteredConsoleMessages: [],
sessionStats: {
lastPromptTokenCount: 0,
sessionTokenCount: 0,
@ -119,7 +109,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
},
branchName: 'main',
debugMessage: '',
errorCount: 0,
nightly: false,
isTrustedFolder: true,
...overrides,
@ -354,31 +343,4 @@ describe('Composer', () => {
expect(lastFrame()).toContain('Footer');
});
});
describe('Error Details Display', () => {
it('shows DetailedMessagesDisplay when showErrorDetails is true', () => {
const uiState = createMockUIState({
showErrorDetails: true,
filteredConsoleMessages: [
{ level: 'error', message: 'Test error', timestamp: new Date() },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain('DetailedMessagesDisplay');
expect(lastFrame()).toContain('ShowMoreLines');
});
it('does not show error details when showErrorDetails is false', () => {
const uiState = createMockUIState({
showErrorDetails: false,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).not.toContain('DetailedMessagesDisplay');
});
});
});

View file

@ -5,15 +5,12 @@
*/
import { Box, useIsScreenReaderEnabled } from 'ink';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useState } from 'react';
import { LoadingIndicator } from './LoadingIndicator.js';
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
import { InputPrompt, calculatePromptWidths } from './InputPrompt.js';
import { InputPrompt } from './InputPrompt.js';
import { Footer } from './Footer.js';
import { ShowMoreLines } from './ShowMoreLines.js';
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
import { KeyboardShortcuts } from './KeyboardShortcuts.js';
import { OverflowProvider } from '../contexts/OverflowContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useUIActions } from '../contexts/UIActionsContext.js';
import { useVimMode } from '../contexts/VimModeContext.js';
@ -29,8 +26,6 @@ export const Composer = () => {
const uiState = useUIState();
const uiActions = useUIActions();
const { vimEnabled } = useVimMode();
const terminalWidth = process.stdout.columns;
const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5));
const { showAutoAcceptIndicator } = uiState;
@ -46,12 +41,6 @@ export const Composer = () => {
setShowSuggestions(visible);
}, []);
// Use the container width of InputPrompt for width of DetailedMessagesDisplay
const { containerWidth } = useMemo(
() => calculatePromptWidths(uiState.terminalWidth),
[uiState.terminalWidth],
);
return (
<Box flexDirection="column" marginTop={1}>
{!uiState.embeddedShellFocused && (
@ -75,21 +64,6 @@ export const Composer = () => {
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
{uiState.showErrorDetails && (
<OverflowProvider>
<Box flexDirection="column">
<DetailedMessagesDisplay
messages={uiState.filteredConsoleMessages}
maxHeight={
uiState.constrainHeight ? debugConsoleMaxHeight : undefined
}
width={containerWidth}
/>
<ShowMoreLines constrainHeight={uiState.constrainHeight} />
</Box>
</OverflowProvider>
)}
{uiState.isFeedbackDialogOpen && <FeedbackDialog />}
{uiState.isInputActive && (

View file

@ -1,35 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
interface ConsoleSummaryDisplayProps {
errorCount: number;
// logCount is not currently in the plan to be displayed in summary
}
export const ConsoleSummaryDisplay: React.FC<ConsoleSummaryDisplayProps> = ({
errorCount,
}) => {
if (errorCount === 0) {
return null;
}
const errorIcon = '\u2716'; // Heavy multiplication x (✖)
return (
<Box>
{errorCount > 0 && (
<Text color={theme.status.error}>
{errorIcon} {errorCount} error{errorCount > 1 ? 's' : ''}{' '}
<Text color={theme.text.secondary}>(ctrl+o for details)</Text>
</Text>
)}
</Box>
);
};

View file

@ -1,83 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import type { ConsoleMessageItem } from '../types.js';
import { MaxSizedBox } from './shared/MaxSizedBox.js';
interface DetailedMessagesDisplayProps {
messages: ConsoleMessageItem[];
maxHeight: number | undefined;
width: number;
// debugMode is not needed here if App.tsx filters debug messages before passing them.
// If DetailedMessagesDisplay should handle filtering, add debugMode prop.
}
export const DetailedMessagesDisplay: React.FC<
DetailedMessagesDisplayProps
> = ({ messages, maxHeight, width }) => {
if (messages.length === 0) {
return null; // Don't render anything if there are no messages
}
const borderAndPadding = 4;
return (
<Box
flexDirection="column"
marginTop={1}
borderStyle="round"
borderColor={theme.border.default}
paddingX={1}
width={width}
>
<Box marginBottom={1}>
<Text bold color={theme.text.primary}>
Debug Console{' '}
<Text color={theme.text.secondary}>(ctrl+o to close)</Text>
</Text>
</Box>
<MaxSizedBox maxHeight={maxHeight} maxWidth={width - borderAndPadding}>
{messages.map((msg, index) => {
let textColor = theme.text.primary;
let icon = '\u2139'; // Information source ()
switch (msg.type) {
case 'warn':
textColor = theme.status.warning;
icon = '\u26A0'; // Warning sign (⚠)
break;
case 'error':
textColor = theme.status.error;
icon = '\u2716'; // Heavy multiplication x (✖)
break;
case 'debug':
textColor = theme.text.secondary; // Or theme.text.secondary
icon = '\u{1F50D}'; // Left-pointing magnifying glass (🔍)
break;
case 'log':
default:
// Default textColor and icon are already set
break;
}
return (
<Box key={index} flexDirection="row">
<Text color={textColor}>{icon} </Text>
<Text color={textColor} wrap="wrap">
{msg.content}
{msg.count && msg.count > 1 && (
<Text color={theme.text.secondary}> (x{msg.count})</Text>
)}
</Text>
</Box>
);
})}
</MaxSizedBox>
</Box>
);
};

View file

@ -7,7 +7,6 @@
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
@ -25,20 +24,11 @@ export const Footer: React.FC = () => {
const config = useConfig();
const { vimEnabled, vimMode } = useVimMode();
const {
errorCount,
showErrorDetails,
promptTokenCount,
showAutoAcceptIndicator,
} = {
errorCount: uiState.errorCount,
showErrorDetails: uiState.showErrorDetails,
const { promptTokenCount, showAutoAcceptIndicator } = {
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
showAutoAcceptIndicator: uiState.showAutoAcceptIndicator,
};
const showErrorIndicator = !showErrorDetails && errorCount > 0;
const { columns: terminalWidth } = useTerminalSize();
const isNarrow = isNarrowWidth(terminalWidth);
@ -103,13 +93,6 @@ export const Footer: React.FC = () => {
),
});
}
if (showErrorIndicator) {
rightItems.push({
key: 'errors',
node: <ConsoleSummaryDisplay errorCount={errorCount} />,
});
}
return (
<Box
justifyContent="space-between"

View file

@ -38,19 +38,13 @@ describe('IdeTrustChangeDialog', () => {
expect(frameText).toContain("Press 'r' to restart Gemini");
});
it('renders a generic message and logs an error for NONE reason', () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('renders a generic message for NONE reason', () => {
const { lastFrame } = renderWithProviders(
<IdeTrustChangeDialog reason="NONE" />,
);
const frameText = lastFrame();
expect(frameText).toContain('Workspace trust has changed.');
expect(consoleErrorSpy).toHaveBeenCalledWith(
'IdeTrustChangeDialog rendered with unexpected reason "NONE"',
);
});
it('calls relaunchApp when "r" is pressed', () => {

View file

@ -475,10 +475,7 @@ describe('InputPrompt', () => {
unmount();
});
it('should handle errors during clipboard operations', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('should handle errors during clipboard operations gracefully', async () => {
vi.mocked(clipboardUtils.clipboardHasImage).mockRejectedValue(
new Error('Clipboard error'),
);
@ -491,13 +488,9 @@ describe('InputPrompt', () => {
stdin.write('\x16'); // Ctrl+V
await wait();
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error handling clipboard image:',
expect.any(Error),
);
// Should not throw and should not set buffer text on error
expect(mockBuffer.setText).not.toHaveBeenCalled();
consoleErrorSpy.mockRestore();
unmount();
});
});

View file

@ -394,10 +394,6 @@ describe('QwenOAuthProgress', () => {
it('should handle QR code generation errors gracefully', async () => {
const qrcode = await import('qrcode-terminal');
const mockGenerate = vi.mocked(qrcode.default.generate);
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockGenerate.mockImplementation(() => {
throw new Error('QR Code generation failed');
});
@ -413,12 +409,6 @@ describe('QwenOAuthProgress', () => {
// Should not crash and should not show QR code section since QR generation failed
const output = lastFrame();
expect(output).not.toContain('Or scan the QR code below:');
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to generate QR code:',
expect.any(Error),
);
consoleErrorSpy.mockRestore();
});
it('should not generate QR code when deviceAuth is null', async () => {

View file

@ -1190,20 +1190,7 @@ describe('KeypressContext - Kitty Protocol', () => {
});
describe('debug keystroke logging', () => {
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
consoleLogSpy.mockRestore();
consoleWarnSpy.mockRestore();
});
it('should not log keystrokes when debugKeystrokeLogging is false', async () => {
it('should handle kitty sequences when debugKeystrokeLogging is false', async () => {
const keyHandler = vi.fn();
const wrapper = ({ children }: { children: React.ReactNode }) => (
@ -1221,138 +1208,20 @@ describe('KeypressContext - Kitty Protocol', () => {
result.current.subscribe(keyHandler);
});
// Send a kitty sequence
// Send a kitty sequence - should work without debug logging
act(() => {
stdin.sendKittySequence('\x1b[27u');
});
expect(keyHandler).toHaveBeenCalled();
expect(consoleLogSpy).not.toHaveBeenCalledWith(
expect.stringContaining('[DEBUG] Kitty'),
);
});
it('should log kitty buffer accumulation when debugKeystrokeLogging is true', async () => {
const keyHandler = vi.fn();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<KeypressProvider
kittyProtocolEnabled={true}
debugKeystrokeLogging={true}
>
{children}
</KeypressProvider>
);
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => {
result.current.subscribe(keyHandler);
});
// Send a complete kitty sequence for escape
act(() => {
stdin.sendKittySequence('\x1b[27u');
});
expect(consoleLogSpy).toHaveBeenCalledWith(
'[DEBUG] Kitty buffer accumulating:',
expect.stringContaining('\x1b[27u'),
);
const parsedCall = consoleLogSpy.mock.calls.find(
(args) =>
typeof args[0] === 'string' &&
args[0].includes('[DEBUG] Kitty sequence parsed successfully'),
);
expect(parsedCall).toBeTruthy();
expect(parsedCall?.[1]).toEqual(expect.stringContaining('\x1b[27u'));
});
it('should log kitty buffer overflow when debugKeystrokeLogging is true', async () => {
const keyHandler = vi.fn();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<KeypressProvider
kittyProtocolEnabled={true}
debugKeystrokeLogging={true}
>
{children}
</KeypressProvider>
);
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => {
result.current.subscribe(keyHandler);
});
// Send an invalid long sequence to trigger overflow
const longInvalidSequence = '\x1b[' + 'x'.repeat(100);
act(() => {
stdin.sendKittySequence(longInvalidSequence);
});
expect(consoleLogSpy).toHaveBeenCalledWith(
'[DEBUG] Kitty buffer overflow, clearing:',
expect.any(String),
);
});
it('should log kitty buffer clear on Ctrl+C when debugKeystrokeLogging is true', async () => {
const keyHandler = vi.fn();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<KeypressProvider
kittyProtocolEnabled={true}
debugKeystrokeLogging={true}
>
{children}
</KeypressProvider>
);
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => {
result.current.subscribe(keyHandler);
});
// Send incomplete kitty sequence
act(() => {
stdin.pressKey({
name: undefined,
ctrl: false,
meta: false,
shift: false,
sequence: '\x1b[1',
});
});
// Send Ctrl+C
act(() => {
stdin.pressKey({
name: 'c',
ctrl: true,
meta: false,
shift: false,
sequence: '\x03',
});
});
expect(consoleLogSpy).toHaveBeenCalledWith(
'[DEBUG] Kitty buffer cleared on Ctrl+C:',
'\x1b[1',
);
// Verify Ctrl+C was handled
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'c',
ctrl: true,
name: 'escape',
kittyProtocol: true,
}),
);
});
it('should show char codes when debugKeystrokeLogging is true even without debug mode', async () => {
it('should handle kitty sequences when debugKeystrokeLogging is true', async () => {
const keyHandler = vi.fn();
const wrapper = ({ children }: { children: React.ReactNode }) => (
@ -1370,29 +1239,44 @@ describe('KeypressContext - Kitty Protocol', () => {
result.current.subscribe(keyHandler);
});
// Send incomplete kitty sequence
const sequence = '\x1b[12';
// Send a complete kitty sequence for escape - should work with debug logging
act(() => {
stdin.pressKey({
name: undefined,
ctrl: false,
meta: false,
shift: false,
sequence,
});
stdin.sendKittySequence('\x1b[27u');
});
// Verify debug logging for accumulation
expect(consoleLogSpy).toHaveBeenCalledWith(
'[DEBUG] Kitty buffer accumulating:',
sequence,
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'escape',
kittyProtocol: true,
}),
);
});
it('should handle kitty buffer overflow without crashing', async () => {
const keyHandler = vi.fn();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<KeypressProvider
kittyProtocolEnabled={true}
debugKeystrokeLogging={true}
>
{children}
</KeypressProvider>
);
// Verify warning for char codes
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Kitty sequence buffer has char codes:',
[27, 91, 49, 50],
);
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => {
result.current.subscribe(keyHandler);
});
// Send an invalid long sequence to trigger overflow - should not crash
const longInvalidSequence = '\x1b[' + 'x'.repeat(100);
expect(() => {
act(() => {
stdin.sendKittySequence(longInvalidSequence);
});
}).not.toThrow();
});
});

View file

@ -8,7 +8,6 @@ import { createContext, useContext } from 'react';
import type {
HistoryItem,
ThoughtSummary,
ConsoleMessageItem,
ShellConfirmationRequest,
ConfirmationRequest,
LoopDetectionConfirmationRequest,
@ -81,8 +80,6 @@ export interface UIState {
isFolderTrustDialogOpen: boolean;
isTrustedFolder: boolean | undefined;
constrainHeight: boolean;
showErrorDetails: boolean;
filteredConsoleMessages: ConsoleMessageItem[];
ideContextState: IdeContext | undefined;
showToolDescriptions: boolean;
ctrlCPressedOnce: boolean;
@ -96,7 +93,6 @@ export interface UIState {
// Quota-related state
currentModel: string;
contextFileNames: string[];
errorCount: number;
availableTerminalHeight: number | undefined;
mainAreaWidth: number;
staticAreaMaxItemHeight: number;

View file

@ -1,147 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { act, renderHook } from '@testing-library/react';
import { vi } from 'vitest';
import { useConsoleMessages } from './useConsoleMessages';
import { useCallback } from 'react';
describe('useConsoleMessages', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.runOnlyPendingTimers();
vi.useRealTimers();
});
const useTestableConsoleMessages = () => {
const { handleNewMessage, ...rest } = useConsoleMessages();
const log = useCallback(
(content: string) => handleNewMessage({ type: 'log', content, count: 1 }),
[handleNewMessage],
);
const error = useCallback(
(content: string) =>
handleNewMessage({ type: 'error', content, count: 1 }),
[handleNewMessage],
);
return {
...rest,
log,
error,
clearConsoleMessages: rest.clearConsoleMessages,
};
};
it('should initialize with an empty array of console messages', () => {
const { result } = renderHook(() => useTestableConsoleMessages());
expect(result.current.consoleMessages).toEqual([]);
});
it('should add a new message when log is called', async () => {
const { result } = renderHook(() => useTestableConsoleMessages());
act(() => {
result.current.log('Test message');
});
await act(async () => {
await vi.advanceTimersByTimeAsync(20);
});
expect(result.current.consoleMessages).toEqual([
{ type: 'log', content: 'Test message', count: 1 },
]);
});
it('should batch and count identical consecutive messages', async () => {
const { result } = renderHook(() => useTestableConsoleMessages());
act(() => {
result.current.log('Test message');
result.current.log('Test message');
result.current.log('Test message');
});
await act(async () => {
await vi.advanceTimersByTimeAsync(20);
});
expect(result.current.consoleMessages).toEqual([
{ type: 'log', content: 'Test message', count: 3 },
]);
});
it('should not batch different messages', async () => {
const { result } = renderHook(() => useTestableConsoleMessages());
act(() => {
result.current.log('First message');
result.current.error('Second message');
});
await act(async () => {
await vi.advanceTimersByTimeAsync(20);
});
expect(result.current.consoleMessages).toEqual([
{ type: 'log', content: 'First message', count: 1 },
{ type: 'error', content: 'Second message', count: 1 },
]);
});
it('should clear all messages when clearConsoleMessages is called', async () => {
const { result } = renderHook(() => useTestableConsoleMessages());
act(() => {
result.current.log('A message');
});
await act(async () => {
await vi.advanceTimersByTimeAsync(20);
});
expect(result.current.consoleMessages).toHaveLength(1);
act(() => {
result.current.clearConsoleMessages();
});
expect(result.current.consoleMessages).toHaveLength(0);
});
it('should clear the pending timeout when clearConsoleMessages is called', () => {
const { result } = renderHook(() => useTestableConsoleMessages());
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
act(() => {
result.current.log('A message');
});
act(() => {
result.current.clearConsoleMessages();
});
expect(clearTimeoutSpy).toHaveBeenCalled();
clearTimeoutSpy.mockRestore();
});
it('should clean up the timeout on unmount', () => {
const { result, unmount } = renderHook(() => useTestableConsoleMessages());
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
act(() => {
result.current.log('A message');
});
unmount();
expect(clearTimeoutSpy).toHaveBeenCalled();
clearTimeoutSpy.mockRestore();
});
});

View file

@ -1,110 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
useCallback,
useEffect,
useReducer,
useRef,
useTransition,
} from 'react';
import type { ConsoleMessageItem } from '../types.js';
export interface UseConsoleMessagesReturn {
consoleMessages: ConsoleMessageItem[];
handleNewMessage: (message: ConsoleMessageItem) => void;
clearConsoleMessages: () => void;
}
type Action =
| { type: 'ADD_MESSAGES'; payload: ConsoleMessageItem[] }
| { type: 'CLEAR' };
function consoleMessagesReducer(
state: ConsoleMessageItem[],
action: Action,
): ConsoleMessageItem[] {
switch (action.type) {
case 'ADD_MESSAGES': {
const newMessages = [...state];
for (const queuedMessage of action.payload) {
const lastMessage = newMessages[newMessages.length - 1];
if (
lastMessage &&
lastMessage.type === queuedMessage.type &&
lastMessage.content === queuedMessage.content
) {
// Create a new object for the last message to ensure React detects
// the change, preventing mutation of the existing state object.
newMessages[newMessages.length - 1] = {
...lastMessage,
count: lastMessage.count + 1,
};
} else {
newMessages.push({ ...queuedMessage, count: 1 });
}
}
return newMessages;
}
case 'CLEAR':
return [];
default:
return state;
}
}
export function useConsoleMessages(): UseConsoleMessagesReturn {
const [consoleMessages, dispatch] = useReducer(consoleMessagesReducer, []);
const messageQueueRef = useRef<ConsoleMessageItem[]>([]);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const [, startTransition] = useTransition();
const processQueue = useCallback(() => {
if (messageQueueRef.current.length > 0) {
const messagesToProcess = messageQueueRef.current;
messageQueueRef.current = [];
startTransition(() => {
dispatch({ type: 'ADD_MESSAGES', payload: messagesToProcess });
});
}
timeoutRef.current = null;
}, []);
const handleNewMessage = useCallback(
(message: ConsoleMessageItem) => {
messageQueueRef.current.push(message);
if (!timeoutRef.current) {
// Batch updates using a timeout. 16ms is a reasonable delay to batch
// rapid-fire messages without noticeable lag.
timeoutRef.current = setTimeout(processQueue, 16);
}
},
[processQueue],
);
const clearConsoleMessages = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
messageQueueRef.current = [];
startTransition(() => {
dispatch({ type: 'CLEAR' });
});
}, []);
// Cleanup on unmount
useEffect(
() => () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
},
[],
);
return { consoleMessages, handleNewMessage, clearConsoleMessages };
}

View file

@ -1602,9 +1602,6 @@ describe('useGeminiStream', () => {
});
it('should handle errors gracefully when auto-approving tool calls', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const mockOnConfirmSuccess = vi.fn().mockResolvedValue(undefined);
const mockOnConfirmError = vi
.fn()
@ -1674,14 +1671,6 @@ describe('useGeminiStream', () => {
// Both confirmation methods should be called
expect(mockOnConfirmSuccess).toHaveBeenCalledTimes(1);
expect(mockOnConfirmError).toHaveBeenCalledTimes(1);
// Error should be logged
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to auto-approve tool call call2:',
expect.any(Error),
);
consoleSpy.mockRestore();
});
it('should skip tool calls without confirmationDetails', async () => {

View file

@ -107,8 +107,6 @@ describe('useInputHistoryStore', () => {
.mockRejectedValue(new Error('Logger error')),
};
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const { result } = renderHook(() => useInputHistoryStore());
await act(async () => {
@ -116,12 +114,6 @@ describe('useInputHistoryStore', () => {
});
expect(result.current.inputHistory).toEqual([]);
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to initialize input history from logger:',
expect.any(Error),
);
consoleSpy.mockRestore();
});
it('should initialize only once', async () => {

View file

@ -43,6 +43,12 @@ vi.mock('@qwen-code/qwen-code-core', () => {
return {
isNodeError: (err: unknown): err is NodeJS.ErrnoException =>
typeof err === 'object' && err !== null && 'code' in err,
createDebugLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
Storage,
};
});

View file

@ -50,7 +50,6 @@ describe('keyMatchers', () => {
[Command.OPEN_EXTERNAL_EDITOR]: (key: Key) =>
key.ctrl && (key.name === 'x' || key.sequence === '\x18'),
[Command.PASTE_CLIPBOARD_IMAGE]: (key: Key) => key.ctrl && key.name === 'v',
[Command.SHOW_ERROR_DETAILS]: (key: Key) => key.ctrl && key.name === 'o',
[Command.TOGGLE_TOOL_DESCRIPTIONS]: (key: Key) =>
key.ctrl && key.name === 't',
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: (key: Key) =>
@ -222,11 +221,6 @@ describe('keyMatchers', () => {
},
// App level bindings
{
command: Command.SHOW_ERROR_DETAILS,
positive: [createKey('o', { ctrl: true })],
negative: [createKey('o'), createKey('e', { ctrl: true })],
},
{
command: Command.TOGGLE_TOOL_DESCRIPTIONS,
positive: [createKey('t', { ctrl: true })],

View file

@ -160,22 +160,14 @@ describe('ThemeManager', () => {
expect(themeManager.getActiveTheme().name).toBe(DEFAULT_THEME.name);
});
it('should not load a theme from an untrusted file path and log a message', () => {
it('should not load a theme from an untrusted file path', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(mockTheme));
const consoleWarnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => {});
const result = themeManager.setActiveTheme('/untrusted/my-theme.json');
expect(result).toBe(false);
expect(themeManager.getActiveTheme().name).toBe(DEFAULT_THEME.name);
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('is outside your home directory'),
);
consoleWarnSpy.mockRestore();
});
});
});

View file

@ -1,71 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import util from 'node:util';
import type { ConsoleMessageItem } from '../types.js';
interface ConsolePatcherParams {
onNewMessage?: (message: Omit<ConsoleMessageItem, 'id'>) => void;
debugMode: boolean;
stderr?: boolean;
}
export class ConsolePatcher {
private originalConsoleLog = console.log;
private originalConsoleWarn = console.warn;
private originalConsoleError = console.error;
private originalConsoleDebug = console.debug;
private originalConsoleInfo = console.info;
private params: ConsolePatcherParams;
constructor(params: ConsolePatcherParams) {
this.params = params;
}
patch() {
console.log = this.patchConsoleMethod('log', this.originalConsoleLog);
console.warn = this.patchConsoleMethod('warn', this.originalConsoleWarn);
console.error = this.patchConsoleMethod('error', this.originalConsoleError);
console.debug = this.patchConsoleMethod('debug', this.originalConsoleDebug);
console.info = this.patchConsoleMethod('info', this.originalConsoleInfo);
}
cleanup = () => {
console.log = this.originalConsoleLog;
console.warn = this.originalConsoleWarn;
console.error = this.originalConsoleError;
console.debug = this.originalConsoleDebug;
console.info = this.originalConsoleInfo;
};
private formatArgs = (args: unknown[]): string => util.format(...args);
private patchConsoleMethod =
(
type: 'log' | 'warn' | 'error' | 'debug' | 'info',
originalMethod: (...args: unknown[]) => void,
) =>
(...args: unknown[]) => {
if (this.params.stderr) {
if (type !== 'debug' || this.params.debugMode) {
this.originalConsoleError(this.formatArgs(args));
}
} else {
if (this.params.debugMode) {
originalMethod.apply(console, args);
}
if (type !== 'debug' || this.params.debugMode) {
this.params.onNewMessage?.({
type,
content: this.formatArgs(args),
count: 1,
});
}
}
};
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
@ -158,28 +158,14 @@ describe('commentJson', () => {
fs.writeFileSync(testFilePath, corruptedContent, 'utf-8');
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
expect(() => {
updateSettingsFilePreservingFormat(testFilePath, {
model: 'gemini-2.5-flash',
});
}).not.toThrow();
expect(consoleSpy).toHaveBeenCalledWith(
'Error parsing settings file:',
expect.any(Error),
);
expect(consoleSpy).toHaveBeenCalledWith(
'Settings file may be corrupted. Please check the JSON syntax.',
);
const unchangedContent = fs.readFileSync(testFilePath, 'utf-8');
expect(unchangedContent).toBe(corruptedContent);
consoleSpy.mockRestore();
});
});
});

View file

@ -19,6 +19,14 @@ import {
handleMaxTurnsExceededError,
} from './errors.js';
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
const debugLoggerSpy = vi.hoisted(() => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}));
// Mock the core modules
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const original =
@ -26,6 +34,12 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
return {
...original,
createDebugLogger: () => ({
debug: debugLoggerSpy.debug,
info: debugLoggerSpy.info,
warn: debugLoggerSpy.warn,
error: debugLoggerSpy.error,
}),
parseAndFormatApiError: vi.fn((error: unknown) => {
if (error instanceof Error) {
return `API Error: ${error.message}`;
@ -66,18 +80,25 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
};
});
vi.mock('./stdioHelpers.js', () => ({
writeStderrLine: mockWriteStderrLine,
writeStdoutLine: vi.fn(),
clearScreen: vi.fn(),
}));
describe('errors', () => {
let mockConfig: Config;
let processExitSpy: MockInstance;
let processStderrWriteSpy: MockInstance;
let consoleErrorSpy: MockInstance;
beforeEach(() => {
// Reset mocks
vi.clearAllMocks();
// Mock console.error
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
mockWriteStderrLine.mockClear();
debugLoggerSpy.debug.mockClear();
debugLoggerSpy.info.mockClear();
debugLoggerSpy.warn.mockClear();
debugLoggerSpy.error.mockClear();
// Mock process.stderr.write
processStderrWriteSpy = vi
@ -99,7 +120,6 @@ describe('errors', () => {
});
afterEach(() => {
consoleErrorSpy.mockRestore();
processStderrWriteSpy.mockRestore();
processExitSpy.mockRestore();
});
@ -163,7 +183,9 @@ describe('errors', () => {
handleError(testError, mockConfig);
}).toThrow(testError);
expect(consoleErrorSpy).toHaveBeenCalledWith('API Error: Test error');
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'API Error: Test error',
);
});
it('should handle non-Error objects', () => {
@ -173,7 +195,9 @@ describe('errors', () => {
handleError(testError, mockConfig);
}).toThrow(testError);
expect(consoleErrorSpy).toHaveBeenCalledWith('API Error: String error');
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'API Error: String error',
);
});
});
@ -191,7 +215,7 @@ describe('errors', () => {
handleError(testError, mockConfig);
}).toThrow('process.exit called with code: 1');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
@ -213,7 +237,7 @@ describe('errors', () => {
handleError(testError, mockConfig, 42);
}).toThrow('process.exit called with code: 42');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
@ -235,7 +259,7 @@ describe('errors', () => {
handleError(fatalError, mockConfig);
}).toThrow('process.exit called with code: 42');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
@ -271,7 +295,7 @@ describe('errors', () => {
handleError(errorWithStatus, mockConfig);
}).toThrow('process.exit called with code: 1'); // string codes become 1
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
@ -307,7 +331,7 @@ describe('errors', () => {
it('should log error message to stderr and not exit', () => {
handleToolError(toolName, toolError, mockConfig);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
@ -322,7 +346,7 @@ describe('errors', () => {
'Custom display message',
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'Error executing tool test-tool: Custom display message',
);
expect(processExitSpy).not.toHaveBeenCalled();
@ -340,7 +364,7 @@ describe('errors', () => {
handleToolError(toolName, toolError, mockConfig);
// In JSON mode, should not exit (just log to stderr when debug mode is on)
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
@ -350,7 +374,7 @@ describe('errors', () => {
handleToolError(toolName, toolError, mockConfig, 'CUSTOM_TOOL_ERROR');
// In JSON mode, should not exit (just log to stderr when debug mode is on)
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
@ -360,7 +384,7 @@ describe('errors', () => {
handleToolError(toolName, toolError, mockConfig, 500);
// In JSON mode, should not exit (just log to stderr when debug mode is on)
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
@ -376,7 +400,7 @@ describe('errors', () => {
);
// In JSON mode, should not exit (just log to stderr when debug mode is on)
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'Error executing tool test-tool: Display message',
);
expect(processExitSpy).not.toHaveBeenCalled();
@ -394,7 +418,7 @@ describe('errors', () => {
handleToolError(toolName, toolError, mockConfig);
// Should not exit in STREAM_JSON mode (just log to stderr when debug mode is on)
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
@ -407,36 +431,42 @@ describe('errors', () => {
(mockConfig.getDebugMode as Mock).mockReturnValue(false);
});
it('should not log and not exit in text mode', () => {
it('should log error and not exit in text mode', () => {
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.TEXT);
handleToolError(toolName, toolError, mockConfig);
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should not log and not exit in JSON mode', () => {
it('should log error and not exit in JSON mode', () => {
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.JSON);
handleToolError(toolName, toolError, mockConfig);
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should not log and not exit in STREAM_JSON mode', () => {
it('should log error and not exit in STREAM_JSON mode', () => {
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.STREAM_JSON);
handleToolError(toolName, toolError, mockConfig);
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(debugLoggerSpy.error).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
});
});
@ -565,7 +595,9 @@ describe('errors', () => {
handleCancellationError(mockConfig);
}).toThrow('process.exit called with code: 130');
expect(consoleErrorSpy).toHaveBeenCalledWith('Operation cancelled.');
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'Operation cancelled.',
);
});
});
@ -581,7 +613,7 @@ describe('errors', () => {
handleCancellationError(mockConfig);
}).toThrow('process.exit called with code: 130');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
@ -611,7 +643,7 @@ describe('errors', () => {
handleMaxTurnsExceededError(mockConfig);
}).toThrow('process.exit called with code: 53');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
);
});
@ -629,7 +661,7 @@ describe('errors', () => {
handleMaxTurnsExceededError(mockConfig);
}).toThrow('process.exit called with code: 53');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
JSON.stringify(
{
error: {

View file

@ -11,9 +11,14 @@ 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', () => ({
isGitRepository: vi.fn(),
}));
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
return {
...actual,
isGitRepository: vi.fn(),
};
});
vi.mock('fs', async (importOriginal) => {
const actualFs = await importOriginal<typeof fs>();
@ -58,8 +63,7 @@ describe('getInstallationInfo', () => {
expect(info.packageManager).toBe(PackageManager.UNKNOWN);
});
it('should return UNKNOWN and log error if realpathSync fails', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
it('should return UNKNOWN if realpathSync fails', () => {
process.argv[1] = '/path/to/cli';
const error = new Error('realpath failed');
mockedRealPathSync.mockImplementation(() => {
@ -69,8 +73,6 @@ describe('getInstallationInfo', () => {
const info = getInstallationInfo(projectRoot, false);
expect(info.packageManager).toBe(PackageManager.UNKNOWN);
expect(consoleSpy).toHaveBeenCalledWith(error);
consoleSpy.mockRestore();
});
it('should detect running from a local git clone', () => {

View file

@ -16,6 +16,8 @@ import {
} from './modelConfigUtils.js';
import type { Settings } from '../config/settings.js';
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const original =
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
@ -25,6 +27,12 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
};
});
vi.mock('./stdioHelpers.js', () => ({
writeStderrLine: mockWriteStderrLine,
writeStdoutLine: vi.fn(),
clearScreen: vi.fn(),
}));
describe('modelConfigUtils', () => {
describe('getAuthTypeFromEnv', () => {
const originalEnv = process.env;
@ -122,17 +130,15 @@ describe('modelConfigUtils', () => {
describe('resolveCliGenerationConfig', () => {
const originalEnv = process.env;
const originalConsoleWarn = console.warn;
beforeEach(() => {
vi.resetModules();
process.env = { ...originalEnv };
console.warn = vi.fn();
mockWriteStderrLine.mockClear();
});
afterEach(() => {
process.env = originalEnv;
console.warn = originalConsoleWarn;
vi.clearAllMocks();
});
@ -521,8 +527,8 @@ describe('modelConfigUtils', () => {
selectedAuthType,
});
expect(console.warn).toHaveBeenCalledWith('Warning 1');
expect(console.warn).toHaveBeenCalledWith('Warning 2');
expect(mockWriteStderrLine).toHaveBeenCalledWith('Warning 1');
expect(mockWriteStderrLine).toHaveBeenCalledWith('Warning 2');
});
it('should use custom env when provided', () => {

View file

@ -33,14 +33,12 @@ import { relaunchAppInChildProcess, relaunchOnExitCode } from './relaunch.js';
describe('relaunchOnExitCode', () => {
let processExitSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let stdinResumeSpy: MockInstance;
beforeEach(() => {
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('PROCESS_EXIT_CALLED');
});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
stdinResumeSpy = vi
.spyOn(process.stdin, 'resume')
.mockImplementation(() => process.stdin);
@ -49,7 +47,6 @@ describe('relaunchOnExitCode', () => {
afterEach(() => {
processExitSpy.mockRestore();
consoleErrorSpy.mockRestore();
stdinResumeSpy.mockRestore();
});
@ -90,10 +87,6 @@ describe('relaunchOnExitCode', () => {
);
expect(runner).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Fatal error: Failed to relaunch the CLI process.',
error,
);
expect(stdinResumeSpy).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
});
@ -101,7 +94,6 @@ describe('relaunchOnExitCode', () => {
describe('relaunchAppInChildProcess', () => {
let processExitSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let stdinPauseSpy: MockInstance;
let stdinResumeSpy: MockInstance;
@ -124,7 +116,6 @@ describe('relaunchAppInChildProcess', () => {
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('PROCESS_EXIT_CALLED');
});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
stdinPauseSpy = vi
.spyOn(process.stdin, 'pause')
.mockImplementation(() => process.stdin);
@ -140,7 +131,6 @@ describe('relaunchAppInChildProcess', () => {
process.execPath = originalExecPath;
processExitSpy.mockRestore();
consoleErrorSpy.mockRestore();
stdinPauseSpy.mockRestore();
stdinResumeSpy.mockRestore();
});

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,14 @@ import * as JsonOutputAdapterModule from './nonInteractive/io/JsonOutputAdapter.
import * as StreamJsonOutputAdapterModule from './nonInteractive/io/StreamJsonOutputAdapter.js';
import * as cleanupModule from './utils/cleanup.js';
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('./utils/stdioHelpers.js', () => ({
writeStderrLine: mockWriteStderrLine,
writeStdoutLine: vi.fn(),
clearScreen: vi.fn(),
}));
type ModelsConfig = ReturnType<Config['getModelsConfig']>;
// Helper to create a mock Config with modelsConfig
@ -42,7 +50,6 @@ describe('validateNonInterActiveAuth', () => {
let originalEnvQwenOauth: string | undefined;
let originalEnvGoogleApiKey: string | undefined;
let originalEnvAnthropicApiKey: string | undefined;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
let processExitSpy: ReturnType<typeof vi.spyOn<[code?: number], never>>;
let refreshAuthMock: ReturnType<typeof vi.fn>;
let mockSettings: LoadedSettings;
@ -62,7 +69,7 @@ describe('validateNonInterActiveAuth', () => {
delete process.env['QWEN_OAUTH'];
delete process.env['GOOGLE_API_KEY'];
delete process.env['ANTHROPIC_API_KEY'];
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
mockWriteStderrLine.mockClear();
processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
throw new Error(`process.exit(${code}) called`);
}) as ReturnType<typeof vi.spyOn<[code?: number], never>>;
@ -149,7 +156,7 @@ describe('validateNonInterActiveAuth', () => {
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
expect.stringContaining('Missing API key'),
);
expect(processExitSpy).toHaveBeenCalledWith(1);
@ -204,7 +211,7 @@ describe('validateNonInterActiveAuth', () => {
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith('Auth error!');
expect(mockWriteStderrLine).toHaveBeenCalledWith('Auth error!');
expect(processExitSpy).toHaveBeenCalledWith(1);
});
@ -226,7 +233,7 @@ describe('validateNonInterActiveAuth', () => {
);
expect(validateAuthMethodSpy).not.toHaveBeenCalled();
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
// refreshAuth is called with the authType from config.getModelsConfig().getCurrentAuthType()
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.QWEN_OAUTH);
@ -272,7 +279,7 @@ describe('validateNonInterActiveAuth', () => {
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'The configured auth type is qwen-oauth, but the current auth type is openai. Please re-authenticate with the correct type.',
);
expect(processExitSpy).toHaveBeenCalledWith(1);
@ -330,7 +337,7 @@ describe('validateNonInterActiveAuth', () => {
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
it('emits error result and exits when enforced auth mismatches current auth', async () => {
@ -369,7 +376,7 @@ describe('validateNonInterActiveAuth', () => {
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
it('emits error result and exits when API key validation fails', async () => {
@ -406,7 +413,7 @@ describe('validateNonInterActiveAuth', () => {
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
});
@ -466,7 +473,7 @@ describe('validateNonInterActiveAuth', () => {
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
it('emits error result and exits when enforced auth mismatches current auth', async () => {
@ -506,7 +513,7 @@ describe('validateNonInterActiveAuth', () => {
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
it('emits error result and exits when API key validation fails', async () => {
@ -544,7 +551,7 @@ describe('validateNonInterActiveAuth', () => {
});
expect(runExitCleanupMock).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
});
});