feat(extensions): add detail command and improve extension validation

- Add /extensions detail command to show extension details
- Allow underscores and dots in extension names
- Fix contextFileName empty array handling to use default QWEN.md
- Fix marketplace extension clone to use correct source URL
- Add inline parameter to extensionToOutputString
- Add comprehensive tests for all changes
This commit is contained in:
LaZzyMan 2026-01-22 19:37:01 +08:00
parent 2aa681f610
commit 674bb6386e
8 changed files with 300 additions and 13 deletions

View file

@ -5,7 +5,8 @@
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getExtensionManager } from './utils.js';
import { getExtensionManager, extensionToOutputString } from './utils.js';
import type { Extension, ExtensionManager } from '@qwen-code/qwen-code-core';
const mockRefreshCache = vi.fn();
const mockExtensionManagerInstance = {
@ -64,3 +65,70 @@ describe('getExtensionManager', () => {
);
});
});
describe('extensionToOutputString', () => {
const mockIsEnabled = vi.fn();
const mockExtensionManager = {
isEnabled: mockIsEnabled,
} as unknown as ExtensionManager;
const createMockExtension = (overrides = {}): Extension => ({
id: 'test-ext-id',
name: 'test-extension',
version: '1.0.0',
isActive: true,
path: '/path/to/extension',
contextFiles: [],
config: { name: 'test-extension', version: '1.0.0' },
...overrides,
});
beforeEach(() => {
vi.clearAllMocks();
mockIsEnabled.mockReturnValue(true);
});
it('should include status icon when inline is false', () => {
const extension = createMockExtension();
const result = extensionToOutputString(
extension,
mockExtensionManager,
'/workspace',
false,
);
// Should contain either ✓ or ✗ (with ANSI color codes)
expect(result).toMatch(/test-extension/);
expect(result).toContain('(1.0.0)');
});
it('should exclude status icon when inline is true', () => {
const extension = createMockExtension();
const result = extensionToOutputString(
extension,
mockExtensionManager,
'/workspace',
true,
);
// Should start with extension name (after stripping potential whitespace)
expect(result.trim()).toMatch(/^test-extension/);
});
it('should default inline to false', () => {
const extension = createMockExtension();
const resultWithoutInline = extensionToOutputString(
extension,
mockExtensionManager,
'/workspace',
);
const resultWithInlineFalse = extensionToOutputString(
extension,
mockExtensionManager,
'/workspace',
false,
);
expect(resultWithoutInline).toEqual(resultWithInlineFalse);
});
});

View file

@ -32,6 +32,7 @@ export function extensionToOutputString(
extension: Extension,
extensionManager: ExtensionManager,
workspaceDir: string,
inline = false,
): string {
const cwd = workspaceDir;
const userEnabled = extensionManager.isEnabled(
@ -44,7 +45,7 @@ export function extensionToOutputString(
);
const status = workspaceEnabled ? chalk.green('✓') : chalk.red('✗');
let output = `${status} ${extension.config.name} (${extension.config.version})`;
let output = `${inline ? '' : status} ${extension.config.name} (${extension.config.version})`;
output += `\n Path: ${extension.path}`;
if (extension.installMetadata) {
output += `\n Source: ${extension.installMetadata.source} (Type: ${extension.installMetadata.type})`;