mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-03 14:10:43 +00:00
fix test
This commit is contained in:
parent
f8e41fb7fa
commit
8b4626a2be
12 changed files with 1339 additions and 1493 deletions
273
packages/cli/src/commands/extensions/consent.test.ts
Normal file
273
packages/cli/src/commands/extensions/consent.test.ts
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { extensionConsentString, requestConsentOrFail } from './consent.js';
|
||||
import type { ExtensionConfig } from '@qwen-code/qwen-code-core';
|
||||
|
||||
vi.mock('../../i18n/index.js', () => ({
|
||||
t: vi.fn((str: string, params?: Record<string, string>) => {
|
||||
if (params) {
|
||||
return Object.entries(params).reduce(
|
||||
(acc, [key, value]) => acc.replace(`{{${key}}}`, value),
|
||||
str,
|
||||
);
|
||||
}
|
||||
return str;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('extensionConsentString', () => {
|
||||
it('should include extension name', () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
const result = extensionConsentString(config);
|
||||
|
||||
expect(result).toContain('Installing extension "test-extension".');
|
||||
});
|
||||
|
||||
it('should include warning message', () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
const result = extensionConsentString(config);
|
||||
|
||||
expect(result).toContain('Extensions may introduce unexpected behavior');
|
||||
});
|
||||
|
||||
it('should include MCP servers when present', () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
mcpServers: {
|
||||
'test-server': {
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = extensionConsentString(config);
|
||||
|
||||
expect(result).toContain(
|
||||
'This extension will run the following MCP servers',
|
||||
);
|
||||
expect(result).toContain('test-server');
|
||||
expect(result).toContain('local');
|
||||
expect(result).toContain('node server.js');
|
||||
});
|
||||
|
||||
it('should include remote MCP servers', () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
mcpServers: {
|
||||
'remote-server': {
|
||||
httpUrl: 'https://example.com/mcp',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = extensionConsentString(config);
|
||||
|
||||
expect(result).toContain('remote');
|
||||
expect(result).toContain('https://example.com/mcp');
|
||||
});
|
||||
|
||||
it('should include commands when present', () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
const result = extensionConsentString(config, ['command1', 'command2']);
|
||||
|
||||
expect(result).toContain('This extension will add the following commands');
|
||||
expect(result).toContain('command1, command2');
|
||||
});
|
||||
|
||||
it('should include context file name when present (string)', () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
contextFileName: 'CUSTOM.md',
|
||||
};
|
||||
|
||||
const result = extensionConsentString(config);
|
||||
|
||||
expect(result).toContain('CUSTOM.md');
|
||||
});
|
||||
|
||||
it('should include context file name when present (array)', () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
contextFileName: ['FILE1.md', 'FILE2.md'],
|
||||
};
|
||||
|
||||
const result = extensionConsentString(config);
|
||||
|
||||
expect(result).toContain('FILE1.md, FILE2.md');
|
||||
});
|
||||
|
||||
it('should include excluded tools when present', () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
excludeTools: ['tool1', 'tool2'],
|
||||
};
|
||||
|
||||
const result = extensionConsentString(config);
|
||||
|
||||
expect(result).toContain(
|
||||
'This extension will exclude the following core tools',
|
||||
);
|
||||
expect(result).toContain('tool1, tool2');
|
||||
});
|
||||
|
||||
it('should include skills when present', () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
const result = extensionConsentString(
|
||||
config,
|
||||
[],
|
||||
[
|
||||
{
|
||||
name: 'skill1',
|
||||
description: 'Skill 1 description',
|
||||
level: 'extension',
|
||||
filePath: '/test/skill1',
|
||||
body: 'skill body',
|
||||
},
|
||||
{
|
||||
name: 'skill2',
|
||||
description: 'Skill 2 description',
|
||||
level: 'extension',
|
||||
filePath: '/test/skill2',
|
||||
body: 'skill body',
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toContain(
|
||||
'This extension will install the following skills',
|
||||
);
|
||||
expect(result).toContain('skill1');
|
||||
expect(result).toContain('Skill 1 description');
|
||||
});
|
||||
|
||||
it('should include subagents when present', () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
const result = extensionConsentString(
|
||||
config,
|
||||
[],
|
||||
[],
|
||||
[
|
||||
{
|
||||
name: 'agent1',
|
||||
description: 'Agent 1 description',
|
||||
systemPrompt: 'You are agent1',
|
||||
level: 'extension',
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toContain(
|
||||
'This extension will install the following subagents',
|
||||
);
|
||||
expect(result).toContain('agent1');
|
||||
expect(result).toContain('Agent 1 description');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestConsentOrFail', () => {
|
||||
let mockRequestConsent: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequestConsent = vi.fn();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should do nothing when options is undefined', async () => {
|
||||
await requestConsentOrFail(mockRequestConsent, undefined);
|
||||
|
||||
expect(mockRequestConsent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should request consent for new extension', async () => {
|
||||
mockRequestConsent.mockResolvedValueOnce(true);
|
||||
|
||||
await requestConsentOrFail(mockRequestConsent, {
|
||||
extensionConfig: { name: 'test-extension', version: '1.0.0' },
|
||||
});
|
||||
|
||||
expect(mockRequestConsent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when user declines consent', async () => {
|
||||
mockRequestConsent.mockResolvedValueOnce(false);
|
||||
|
||||
await expect(
|
||||
requestConsentOrFail(mockRequestConsent, {
|
||||
extensionConfig: { name: 'test-extension', version: '1.0.0' },
|
||||
}),
|
||||
).rejects.toThrow('Installation cancelled for "test-extension".');
|
||||
});
|
||||
|
||||
it('should skip consent when consent string is unchanged', async () => {
|
||||
const extensionConfig: ExtensionConfig = {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
await requestConsentOrFail(mockRequestConsent, {
|
||||
extensionConfig,
|
||||
previousExtensionConfig: extensionConfig,
|
||||
});
|
||||
|
||||
expect(mockRequestConsent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should request consent when consent string changes', async () => {
|
||||
mockRequestConsent.mockResolvedValueOnce(true);
|
||||
|
||||
await requestConsentOrFail(mockRequestConsent, {
|
||||
extensionConfig: {
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
excludeTools: ['tool1'],
|
||||
},
|
||||
previousExtensionConfig: { name: 'test-extension', version: '1.0.0' },
|
||||
});
|
||||
|
||||
expect(mockRequestConsent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should request consent when commands change', async () => {
|
||||
mockRequestConsent.mockResolvedValueOnce(true);
|
||||
|
||||
await requestConsentOrFail(mockRequestConsent, {
|
||||
extensionConfig: { name: 'test-extension', version: '1.0.0' },
|
||||
commands: ['command1'],
|
||||
previousExtensionConfig: { name: 'test-extension', version: '1.0.0' },
|
||||
previousCommands: [],
|
||||
});
|
||||
|
||||
expect(mockRequestConsent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
129
packages/cli/src/commands/extensions/disable.test.ts
Normal file
129
packages/cli/src/commands/extensions/disable.test.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
type MockInstance,
|
||||
} from 'vitest';
|
||||
import { disableCommand, handleDisable } from './disable.js';
|
||||
import yargs from 'yargs';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
|
||||
const mockDisableExtension = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('./utils.js', () => ({
|
||||
getExtensionManager: vi.fn().mockResolvedValue({
|
||||
disableExtension: mockDisableExtension,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/errors.js', () => ({
|
||||
getErrorMessage: vi.fn((error: Error) => error.message),
|
||||
}));
|
||||
|
||||
describe('extensions disable command', () => {
|
||||
it('should fail if no name is provided', () => {
|
||||
const validationParser = yargs([])
|
||||
.command(disableCommand)
|
||||
.fail(false)
|
||||
.locale('en');
|
||||
expect(() => validationParser.parse('disable')).toThrow(
|
||||
'Not enough non-option arguments: got 0, need at least 1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if invalid scope is provided', () => {
|
||||
const validationParser = yargs([])
|
||||
.command(disableCommand)
|
||||
.fail(false)
|
||||
.locale('en');
|
||||
expect(() =>
|
||||
validationParser.parse('disable test-extension --scope=invalid'),
|
||||
).toThrow(/Invalid scope: invalid/);
|
||||
});
|
||||
|
||||
it('should accept valid scope values', () => {
|
||||
const parser = yargs([]).command(disableCommand).fail(false).locale('en');
|
||||
// Just check that the scope option is recognized, actual execution needs name first
|
||||
expect(() =>
|
||||
parser.parse('disable my-extension --scope=user'),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
await handleDisable({
|
||||
name: 'test-extension',
|
||||
scope: 'user',
|
||||
});
|
||||
|
||||
expect(mockDisableExtension).toHaveBeenCalledWith(
|
||||
'test-extension',
|
||||
SettingScope.User,
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "test-extension" successfully disabled for scope "user".',
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable an extension with workspace scope', async () => {
|
||||
await handleDisable({
|
||||
name: 'test-extension',
|
||||
scope: 'workspace',
|
||||
});
|
||||
|
||||
expect(mockDisableExtension).toHaveBeenCalledWith(
|
||||
'test-extension',
|
||||
SettingScope.Workspace,
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "test-extension" successfully disabled for scope "workspace".',
|
||||
);
|
||||
});
|
||||
|
||||
it('should default to user scope when no scope is provided', async () => {
|
||||
await handleDisable({
|
||||
name: 'test-extension',
|
||||
});
|
||||
|
||||
expect(mockDisableExtension).toHaveBeenCalledWith(
|
||||
'test-extension',
|
||||
SettingScope.User,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors and exit with code 1', async () => {
|
||||
mockDisableExtension.mockImplementationOnce(() => {
|
||||
throw new Error('Disable failed');
|
||||
});
|
||||
|
||||
await handleDisable({
|
||||
name: 'test-extension',
|
||||
scope: 'user',
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Disable failed');
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
136
packages/cli/src/commands/extensions/enable.test.ts
Normal file
136
packages/cli/src/commands/extensions/enable.test.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
type MockInstance,
|
||||
} from 'vitest';
|
||||
import { enableCommand, handleEnable } from './enable.js';
|
||||
import yargs from 'yargs';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
|
||||
const mockEnableExtension = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('./utils.js', () => ({
|
||||
getExtensionManager: vi.fn().mockResolvedValue({
|
||||
enableExtension: mockEnableExtension,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
|
||||
return {
|
||||
...actual,
|
||||
FatalConfigError: class FatalConfigError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'FatalConfigError';
|
||||
}
|
||||
},
|
||||
getErrorMessage: (error: Error) => error.message,
|
||||
};
|
||||
});
|
||||
|
||||
describe('extensions enable command', () => {
|
||||
it('should fail if no name is provided', () => {
|
||||
const validationParser = yargs([])
|
||||
.command(enableCommand)
|
||||
.fail(false)
|
||||
.locale('en');
|
||||
expect(() => validationParser.parse('enable')).toThrow(
|
||||
'Not enough non-option arguments: got 0, need at least 1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if invalid scope is provided', () => {
|
||||
const validationParser = yargs([])
|
||||
.command(enableCommand)
|
||||
.fail(false)
|
||||
.locale('en');
|
||||
expect(() =>
|
||||
validationParser.parse('enable test-extension --scope=invalid'),
|
||||
).toThrow(/Invalid scope: invalid/);
|
||||
});
|
||||
|
||||
it('should accept valid scope values', () => {
|
||||
const parser = yargs([]).command(enableCommand).fail(false).locale('en');
|
||||
// Just check that the scope option is recognized, actual execution needs name first
|
||||
expect(() =>
|
||||
parser.parse('enable my-extension --scope=user'),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleEnable', () => {
|
||||
let consoleLogSpy: MockInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should enable an extension with user scope', async () => {
|
||||
await handleEnable({
|
||||
name: 'test-extension',
|
||||
scope: 'user',
|
||||
});
|
||||
|
||||
expect(mockEnableExtension).toHaveBeenCalledWith(
|
||||
'test-extension',
|
||||
SettingScope.User,
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "test-extension" successfully enabled for scope "user".',
|
||||
);
|
||||
});
|
||||
|
||||
it('should enable an extension with workspace scope', async () => {
|
||||
await handleEnable({
|
||||
name: 'test-extension',
|
||||
scope: 'workspace',
|
||||
});
|
||||
|
||||
expect(mockEnableExtension).toHaveBeenCalledWith(
|
||||
'test-extension',
|
||||
SettingScope.Workspace,
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "test-extension" successfully enabled for scope "workspace".',
|
||||
);
|
||||
});
|
||||
|
||||
it('should default to user scope when no scope is provided', async () => {
|
||||
await handleEnable({
|
||||
name: 'test-extension',
|
||||
});
|
||||
|
||||
expect(mockEnableExtension).toHaveBeenCalledWith(
|
||||
'test-extension',
|
||||
SettingScope.User,
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "test-extension" successfully enabled in all scopes.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw FatalConfigError when enable fails', async () => {
|
||||
mockEnableExtension.mockImplementationOnce(() => {
|
||||
throw new Error('Enable failed');
|
||||
});
|
||||
|
||||
await expect(
|
||||
handleEnable({
|
||||
name: 'test-extension',
|
||||
scope: 'user',
|
||||
}),
|
||||
).rejects.toThrow('Enable failed');
|
||||
});
|
||||
});
|
||||
95
packages/cli/src/commands/extensions/link.test.ts
Normal file
95
packages/cli/src/commands/extensions/link.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
type MockInstance,
|
||||
} from 'vitest';
|
||||
import { linkCommand, handleLink } from './link.js';
|
||||
import yargs from 'yargs';
|
||||
|
||||
const mockInstallExtension = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('./utils.js', () => ({
|
||||
getExtensionManager: vi.fn().mockResolvedValue({
|
||||
installExtension: mockInstallExtension,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('./consent.js', () => ({
|
||||
requestConsentNonInteractive: vi.fn().mockResolvedValue(true),
|
||||
requestConsentOrFail: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/errors.js', () => ({
|
||||
getErrorMessage: vi.fn((error: Error) => error.message),
|
||||
}));
|
||||
|
||||
describe('extensions link command', () => {
|
||||
it('should fail if no path is provided', () => {
|
||||
const validationParser = yargs([])
|
||||
.command(linkCommand)
|
||||
.fail(false)
|
||||
.locale('en');
|
||||
expect(() => validationParser.parse('link')).toThrow(
|
||||
'Not enough non-option arguments: got 0, need at least 1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept a path argument', () => {
|
||||
const parser = yargs([]).command(linkCommand).fail(false).locale('en');
|
||||
expect(() => parser.parse('link /some/path')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
mockInstallExtension.mockResolvedValueOnce({ name: 'linked-extension' });
|
||||
|
||||
await handleLink({
|
||||
path: '/some/local/path',
|
||||
});
|
||||
|
||||
expect(mockInstallExtension).toHaveBeenCalledWith(
|
||||
{
|
||||
source: '/some/local/path',
|
||||
type: 'link',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "linked-extension" linked successfully and enabled.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors and exit with code 1', async () => {
|
||||
mockInstallExtension.mockRejectedValueOnce(new Error('Link failed'));
|
||||
|
||||
await handleLink({
|
||||
path: '/some/local/path',
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Link failed');
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
90
packages/cli/src/commands/extensions/list.test.ts
Normal file
90
packages/cli/src/commands/extensions/list.test.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
type MockInstance,
|
||||
} from 'vitest';
|
||||
import { listCommand, handleList } from './list.js';
|
||||
import yargs from 'yargs';
|
||||
|
||||
const mockGetLoadedExtensions = vi.hoisted(() => vi.fn());
|
||||
const mockToOutputString = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('./utils.js', () => ({
|
||||
getExtensionManager: vi.fn().mockResolvedValue({
|
||||
getLoadedExtensions: mockGetLoadedExtensions,
|
||||
toOutputString: mockToOutputString,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/errors.js', () => ({
|
||||
getErrorMessage: vi.fn((error: Error) => error.message),
|
||||
}));
|
||||
|
||||
describe('extensions list command', () => {
|
||||
it('should parse the list command', () => {
|
||||
const parser = yargs([]).command(listCommand).fail(false).locale('en');
|
||||
expect(() => parser.parse('list')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
mockGetLoadedExtensions.mockReturnValueOnce([]);
|
||||
|
||||
await handleList();
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith('No extensions installed.');
|
||||
});
|
||||
|
||||
it('should list installed extensions', async () => {
|
||||
const mockExtensions = [
|
||||
{ name: 'extension-1', version: '1.0.0' },
|
||||
{ name: 'extension-2', version: '2.0.0' },
|
||||
];
|
||||
mockGetLoadedExtensions.mockReturnValueOnce(mockExtensions);
|
||||
mockToOutputString.mockImplementation(
|
||||
(ext) => `${ext.name} (${ext.version})`,
|
||||
);
|
||||
|
||||
await handleList();
|
||||
|
||||
expect(mockGetLoadedExtensions).toHaveBeenCalled();
|
||||
expect(mockToOutputString).toHaveBeenCalledTimes(2);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'extension-1 (1.0.0)\n\nextension-2 (2.0.0)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors and exit with code 1', async () => {
|
||||
mockGetLoadedExtensions.mockImplementationOnce(() => {
|
||||
throw new Error('List failed');
|
||||
});
|
||||
|
||||
await handleList();
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('List failed');
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
262
packages/cli/src/commands/extensions/update.test.ts
Normal file
262
packages/cli/src/commands/extensions/update.test.ts
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
type MockInstance,
|
||||
} from 'vitest';
|
||||
import { updateCommand, handleUpdate } from './update.js';
|
||||
import yargs from 'yargs';
|
||||
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
|
||||
const mockGetLoadedExtensions = vi.hoisted(() => vi.fn());
|
||||
const mockUpdateExtension = vi.hoisted(() => vi.fn());
|
||||
const mockCheckForAllExtensionUpdates = vi.hoisted(() => vi.fn());
|
||||
const mockUpdateAllUpdatableExtensions = vi.hoisted(() => vi.fn());
|
||||
const mockCheckForExtensionUpdate = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('./utils.js', () => ({
|
||||
getExtensionManager: vi.fn().mockResolvedValue({
|
||||
getLoadedExtensions: mockGetLoadedExtensions,
|
||||
updateExtension: mockUpdateExtension,
|
||||
checkForAllExtensionUpdates: mockCheckForAllExtensionUpdates,
|
||||
updateAllUpdatableExtensions: mockUpdateAllUpdatableExtensions,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@qwen-code/qwen-code-core', () => ({
|
||||
checkForExtensionUpdate: mockCheckForExtensionUpdate,
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/errors.js', () => ({
|
||||
getErrorMessage: vi.fn((error: Error) => error.message),
|
||||
}));
|
||||
|
||||
vi.mock('../../ui/state/extensions.js', () => ({
|
||||
ExtensionUpdateState: {
|
||||
UPDATE_AVAILABLE: 'update available',
|
||||
UP_TO_DATE: 'up to date',
|
||||
ERROR: 'error',
|
||||
},
|
||||
}));
|
||||
|
||||
describe('extensions update command', () => {
|
||||
it('should fail if neither name nor --all is provided', () => {
|
||||
const validationParser = yargs([])
|
||||
.command(updateCommand)
|
||||
.fail(false)
|
||||
.locale('en');
|
||||
expect(() => validationParser.parse('update')).toThrow(
|
||||
'Either an extension name or --all must be provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if both name and --all are provided', () => {
|
||||
const validationParser = yargs([])
|
||||
.command(updateCommand)
|
||||
.fail(false)
|
||||
.locale('en');
|
||||
expect(() => validationParser.parse('update test-extension --all')).toThrow(
|
||||
/Arguments .* are mutually exclusive/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept --all flag', () => {
|
||||
const parser = yargs([]).command(updateCommand).fail(false).locale('en');
|
||||
expect(() => parser.parse('update --all')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept an extension name', () => {
|
||||
const parser = yargs([]).command(updateCommand).fail(false).locale('en');
|
||||
expect(() => parser.parse('update test-extension')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleUpdate', () => {
|
||||
let consoleLogSpy: MockInstance;
|
||||
let consoleErrorSpy: MockInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('update by name', () => {
|
||||
it('should show message when extension is not found', async () => {
|
||||
mockGetLoadedExtensions.mockReturnValueOnce([]);
|
||||
|
||||
await handleUpdate({ name: 'non-existent-extension' });
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "non-existent-extension" not found.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show message when extension has no install metadata', async () => {
|
||||
mockGetLoadedExtensions.mockReturnValueOnce([
|
||||
{ name: 'test-extension', installMetadata: undefined },
|
||||
]);
|
||||
|
||||
await handleUpdate({ name: 'test-extension' });
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Unable to install extension "test-extension" due to missing install metadata',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show message when extension is already up to date', async () => {
|
||||
const mockExtension = {
|
||||
name: 'test-extension',
|
||||
installMetadata: { source: 'test' },
|
||||
};
|
||||
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
|
||||
mockCheckForExtensionUpdate.mockResolvedValueOnce(
|
||||
ExtensionUpdateState.UP_TO_DATE,
|
||||
);
|
||||
|
||||
await handleUpdate({ name: 'test-extension' });
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "test-extension" is already up to date.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should update extension when update is available', async () => {
|
||||
const mockExtension = {
|
||||
name: 'test-extension',
|
||||
installMetadata: { source: 'test' },
|
||||
};
|
||||
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
|
||||
mockCheckForExtensionUpdate.mockResolvedValueOnce(
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
);
|
||||
mockUpdateExtension.mockResolvedValueOnce({
|
||||
name: 'test-extension',
|
||||
originalVersion: '1.0.0',
|
||||
updatedVersion: '2.0.0',
|
||||
});
|
||||
|
||||
await handleUpdate({ name: 'test-extension' });
|
||||
|
||||
expect(mockUpdateExtension).toHaveBeenCalledWith(
|
||||
mockExtension,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "test-extension" successfully updated: 1.0.0 → 2.0.0.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show up to date message when versions are the same after update', async () => {
|
||||
const mockExtension = {
|
||||
name: 'test-extension',
|
||||
installMetadata: { source: 'test' },
|
||||
};
|
||||
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
|
||||
mockCheckForExtensionUpdate.mockResolvedValueOnce(
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
);
|
||||
mockUpdateExtension.mockResolvedValueOnce({
|
||||
name: 'test-extension',
|
||||
originalVersion: '1.0.0',
|
||||
updatedVersion: '1.0.0',
|
||||
});
|
||||
|
||||
await handleUpdate({ name: 'test-extension' });
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "test-extension" is already up to date.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors during update', async () => {
|
||||
const mockExtension = {
|
||||
name: 'test-extension',
|
||||
installMetadata: { source: 'test' },
|
||||
};
|
||||
mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]);
|
||||
mockCheckForExtensionUpdate.mockRejectedValueOnce(
|
||||
new Error('Update check failed'),
|
||||
);
|
||||
|
||||
await handleUpdate({ name: 'test-extension' });
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Update check failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update all', () => {
|
||||
it('should show message when no extensions to update', async () => {
|
||||
mockCheckForAllExtensionUpdates.mockResolvedValueOnce(undefined);
|
||||
mockUpdateAllUpdatableExtensions.mockResolvedValueOnce([]);
|
||||
|
||||
await handleUpdate({ all: true });
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith('No extensions to update.');
|
||||
});
|
||||
|
||||
it('should update all extensions with updates available', async () => {
|
||||
mockCheckForAllExtensionUpdates.mockResolvedValueOnce(undefined);
|
||||
mockUpdateAllUpdatableExtensions.mockResolvedValueOnce([
|
||||
{
|
||||
name: 'extension-1',
|
||||
originalVersion: '1.0.0',
|
||||
updatedVersion: '2.0.0',
|
||||
},
|
||||
{
|
||||
name: 'extension-2',
|
||||
originalVersion: '1.0.0',
|
||||
updatedVersion: '1.5.0',
|
||||
},
|
||||
]);
|
||||
|
||||
await handleUpdate({ all: true });
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "extension-1" successfully updated: 1.0.0 → 2.0.0.\n' +
|
||||
'Extension "extension-2" successfully updated: 1.0.0 → 1.5.0.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter out extensions with same version after update', async () => {
|
||||
mockCheckForAllExtensionUpdates.mockResolvedValueOnce(undefined);
|
||||
mockUpdateAllUpdatableExtensions.mockResolvedValueOnce([
|
||||
{
|
||||
name: 'extension-1',
|
||||
originalVersion: '1.0.0',
|
||||
updatedVersion: '2.0.0',
|
||||
},
|
||||
{
|
||||
name: 'extension-2',
|
||||
originalVersion: '1.0.0',
|
||||
updatedVersion: '1.0.0',
|
||||
},
|
||||
]);
|
||||
|
||||
await handleUpdate({ all: true });
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "extension-1" successfully updated: 1.0.0 → 2.0.0.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors during update all', async () => {
|
||||
mockCheckForAllExtensionUpdates.mockRejectedValueOnce(
|
||||
new Error('Update all failed'),
|
||||
);
|
||||
|
||||
await handleUpdate({ all: true });
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Update all failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
66
packages/cli/src/commands/extensions/utils.test.ts
Normal file
66
packages/cli/src/commands/extensions/utils.test.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getExtensionManager } from './utils.js';
|
||||
|
||||
const mockRefreshCache = vi.fn();
|
||||
const mockExtensionManagerInstance = {
|
||||
refreshCache: mockRefreshCache,
|
||||
};
|
||||
|
||||
vi.mock('@qwen-code/qwen-code-core', () => ({
|
||||
ExtensionManager: vi
|
||||
.fn()
|
||||
.mockImplementation(() => mockExtensionManagerInstance),
|
||||
}));
|
||||
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
loadSettings: vi.fn().mockReturnValue({
|
||||
merged: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../config/trustedFolders.js', () => ({
|
||||
isWorkspaceTrusted: vi.fn().mockReturnValue({ isTrusted: true }),
|
||||
}));
|
||||
|
||||
vi.mock('./consent.js', () => ({
|
||||
requestConsentOrFail: vi.fn(),
|
||||
requestConsentNonInteractive: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('getExtensionManager', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockRefreshCache.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should return an ExtensionManager instance', async () => {
|
||||
const manager = await getExtensionManager();
|
||||
|
||||
expect(manager).toBeDefined();
|
||||
expect(manager).toBe(mockExtensionManagerInstance);
|
||||
});
|
||||
|
||||
it('should call refreshCache on the ExtensionManager', async () => {
|
||||
await getExtensionManager();
|
||||
|
||||
expect(mockRefreshCache).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use current working directory as workspace', async () => {
|
||||
const { ExtensionManager } = await import('@qwen-code/qwen-code-core');
|
||||
|
||||
await getExtensionManager();
|
||||
|
||||
expect(ExtensionManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceDir: process.cwd(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue