mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 23:42:03 +00:00
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:
parent
135df54f27
commit
89e3c2cd7a
64 changed files with 1240 additions and 2416 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 }],
|
||||
|
|
|
|||
|
|
@ -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".',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ describe('copyCommand', () => {
|
|||
getGeminiClient: () => ({
|
||||
getChat: mockGetChat,
|
||||
}),
|
||||
getDebugLogger: () => ({
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 })],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue