Skip to content
Closed
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,6 @@ storybook-static

# Dev symlink: qc-helper bundled skill docs (created by scripts/dev.js)
packages/core/src/skills/bundled/qc-helper/docs
tmp/
tmp/.prforge/
.prforge-run
.prforge-*
2 changes: 1 addition & 1 deletion packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ export async function parseArguments(): Promise<CliArgs> {
type: 'array',
string: true,
description:
'Additional directories to include in the workspace (comma-separated or multiple --include-directories)',
'Additional directories to include in the workspace. Paths are resolved to absolute paths. Non-existent directories are skipped with a warning. Use comma-separated values or pass the flag multiple times.',
coerce: (dirs: string[]) =>
// Handle comma-separated values
dirs.flatMap((dir) => dir.split(',').map((d) => d.trim())),
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,17 @@ export default {
'Manage workspace directories': 'Manage workspace directories',
'Add directories to the workspace. Use comma to separate multiple paths':
'Add directories to the workspace. Use comma to separate multiple paths',
'Remove a directory from the workspace':
'Remove a directory from the workspace',
'Please provide a directory path to remove.':
'Please provide a directory path to remove.',
'Cannot remove initial workspace directory: {{directory}}':
'Cannot remove initial workspace directory: {{directory}}',
'Directory not found in workspace: {{directory}}':
'Directory not found in workspace: {{directory}}',
'Directory removed from workspace but error updating settings: {{error}}':
'Directory removed from workspace but error updating settings: {{error}}',
'Removed directory: {{directory}}': 'Removed directory: {{directory}}',
'Show all directories in the workspace':
'Show all directories in the workspace',
'set external editor preference': 'set external editor preference',
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,17 @@ export default {
'Manage workspace directories': '管理工作區目錄',
'Add directories to the workspace. Use comma to separate multiple paths':
'將目錄添加到工作區。使用逗號分隔多個路徑',
'Remove a directory from the workspace':
'從工作區中移除目錄',
'Please provide a directory path to remove.':
'請提供要移除的目錄路徑。',
'Cannot remove initial workspace directory: {{directory}}':
'無法移除初始工作區目錄:{{directory}}',
'Directory not found in workspace: {{directory}}':
'工作區中未找到目錄:{{directory}}',
'Directory removed from workspace but error updating settings: {{error}}':
'目錄已從工作區移除,但更新設置時出錯:{{error}}',
'Removed directory: {{directory}}': '已移除目錄:{{directory}}',
'Show all directories in the workspace': '顯示工作區中的所有目錄',
'set external editor preference': '設置外部編輯器首選項',
'Select Editor': '選擇編輯器',
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,17 @@ export default {
'Manage workspace directories': '管理工作区目录',
'Add directories to the workspace. Use comma to separate multiple paths':
'将目录添加到工作区。使用逗号分隔多个路径',
'Remove a directory from the workspace':
'从工作区中移除目录',
'Please provide a directory path to remove.':
'请提供要移除的目录路径。',
'Cannot remove initial workspace directory: {{directory}}':
'无法移除初始工作区目录:{{directory}}',
'Directory not found in workspace: {{directory}}':
'工作区中未找到目录:{{directory}}',
'Directory removed from workspace but error updating settings: {{error}}':
'目录已从工作区移除,但更新设置时出错:{{error}}',
'Removed directory: {{directory}}': '已移除目录:{{directory}}',
'Show all directories in the workspace': '显示工作区中的所有目录',
'set external editor preference': '设置外部编辑器首选项',
'Select Editor': '选择编辑器',
Expand Down
193 changes: 184 additions & 9 deletions packages/cli/src/ui/commands/directoryCommand.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { directoryCommand, expandHomeDir } from './directoryCommand.js';
import type { Config, WorkspaceContext } from '@qwen-code/qwen-code-core';
import type {
Config,
WorkspaceContext,
SettingsFile,
} from '@qwen-code/qwen-code-core';
import type { CommandContext } from './types.js';
import { MessageType } from '../types.js';
// eslint-disable-next-line import/no-internal-modules
import { SettingScope } from '../../config/settings.js';
import * as os from 'node:os';
import * as path from 'node:path';
Expand All @@ -21,6 +26,9 @@ describe('directoryCommand', () => {
const addCommand = directoryCommand.subCommands?.find(
(c) => c.name === 'add',
);
const removeCommand = directoryCommand.subCommands?.find(
(c) => c.name === 'remove',
);
const showCommand = directoryCommand.subCommands?.find(
(c) => c.name === 'show',
);
Expand All @@ -30,6 +38,7 @@ describe('directoryCommand', () => {
path.normalize('/home/user/project1'),
path.normalize('/home/user/project2'),
];
const initialDirs = new Set([path.normalize('/home/user/project1')]);
mockWorkspaceContext = {
addDirectory: vi.fn((directory: string) => {
const normalizedDirectory = path.normalize(directory);
Expand All @@ -38,6 +47,11 @@ describe('directoryCommand', () => {
}
}),
getDirectories: vi.fn(() => [...mockWorkspaceDirectories]),
getInitialDirectories: vi.fn(() => [...initialDirs]),
isInitialDirectory: vi.fn((dir: string) =>
initialDirs.has(path.normalize(dir)),
),
removeDirectory: vi.fn(),
} as unknown as WorkspaceContext;

mockConfig = {
Expand All @@ -56,17 +70,29 @@ describe('directoryCommand', () => {
setGeminiMdFileCount: vi.fn(),
} as unknown as Config;

const createMockSettings = () => ({
merged: {},
workspace: {
settings: {},
originalSettings: {},
} as SettingsFile,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] TypeScript 类型错误:Cannot find name 'SettingsFile'(第 73、77 行)和 'this' implicitly has type 'any'(第 81 行)。createMockSettings 函数中使用了 SettingsFile 类型但未导入,且 forScope 方法中的 this 缺少类型注解。

Suggested change
} as SettingsFile,
import type { SettingsFile } from '../../config/types.js';
// 并为 forScope 添加 this 类型注解

— deepseek-v4-pro via Qwen Code /review

user: {
settings: {},
originalSettings: {},
} as SettingsFile,
setValue: vi.fn(),
forScope: vi.fn(function (this: SettingsFile, scope: string) {
if (scope === 'user') return this.user;
return this.workspace;
}),
});

const mockSettings = createMockSettings();

mockContext = {
services: {
config: mockConfig,
settings: {
merged: {},
workspace: {
settings: {},
originalSettings: {},
},
setValue: vi.fn(),
},
settings: mockSettings,
},
ui: {
addItem: vi.fn(),
Expand Down Expand Up @@ -314,6 +340,155 @@ describe('directoryCommand', () => {
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] remove 的 completion 处理器和层级内存刷新路径(loadServerHierarchicalMemory 成功/失败分支)未被测试覆盖。测试中 mock 了 shouldLoadMemoryFromIncludeDirectories: () => false,导致内存刷新路径完全跳过。建议补充相关测试。

— deepseek-v4-pro via Qwen Code /review

});
});
describe('remove', () => {
it('should show an error if no path is provided', async () => {
if (!removeCommand?.action) throw new Error('No action');
await removeCommand.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: 'Please provide a directory path to remove.',
}),
expect.any(Number),
);
});

it('should show an error when trying to remove the initial directory', async () => {
const initialDir = path.normalize('/home/user/project1');
if (!removeCommand?.action) throw new Error('No action');
await removeCommand.action(mockContext, initialDir);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: `Cannot remove initial workspace directory: ${initialDir}`,
}),
expect.any(Number),
);
});

it('should show an error when directory is not in workspace', async () => {
const nonExistent = path.normalize('/not/in/workspace');
if (!removeCommand?.action) throw new Error('No action');
await removeCommand.action(mockContext, nonExistent);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: `Directory not found in workspace: ${nonExistent}`,
}),
expect.any(Number),
);
});

it('should remove a directory and persist to settings', async () => {
const removableDir = path.normalize('/home/user/project2');
mockWorkspaceContext = {
...mockWorkspaceContext,
removeDirectory: vi.fn().mockReturnValue(true),
isInitialDirectory: vi.fn().mockReturnValue(false),
getInitialDirectories: vi
.fn()
.mockReturnValue([path.normalize('/home/user/project1')]),
} as unknown as WorkspaceContext;

mockConfig = {
...mockConfig,
getWorkspaceContext: () => mockWorkspaceContext,
} as unknown as Config;

mockContext = {
...mockContext,
services: {
...mockContext.services,
config: mockConfig,
settings: {
...mockContext.services.settings,
workspace: {
settings: {},
originalSettings: {
context: {
includeDirectories: [
path.normalize('/home/user/project1'),
removableDir,
],
},
},
},
},
},
} as unknown as CommandContext;

if (!removeCommand?.action) throw new Error('No action');
await removeCommand.action(mockContext, removableDir);

expect(mockWorkspaceContext.removeDirectory).toHaveBeenCalledWith(
removableDir,
);
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'context.includeDirectories',
[path.normalize('/home/user/project1')],
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: `Removed directory: ${removableDir}`,
}),
expect.any(Number),
);
});

it('should show error when settings update fails after removal', async () => {
const removableDir = path.normalize('/home/user/project2');
mockWorkspaceContext = {
...mockWorkspaceContext,
removeDirectory: vi.fn().mockReturnValue(true),
isInitialDirectory: vi.fn().mockReturnValue(false),
getInitialDirectories: vi
.fn()
.mockReturnValue([path.normalize('/home/user/project1')]),
} as unknown as WorkspaceContext;

mockConfig = {
...mockConfig,
getWorkspaceContext: () => mockWorkspaceContext,
} as unknown as Config;

const settingsError = new Error('write failed');
mockContext = {
...mockContext,
services: {
...mockContext.services,
config: mockConfig,
settings: {
...mockContext.services.settings,
workspace: {
settings: {},
originalSettings: {
context: { includeDirectories: [removableDir] },
},
},
setValue: vi.fn().mockImplementation(() => {
throw settingsError;
}),
},
},
} as unknown as CommandContext;

if (!removeCommand?.action) throw new Error('No action');
await removeCommand.action(mockContext, removableDir);

expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: `Failed to persist directory removal: ${settingsError.message}`,
}),
expect.any(Number),
);
// Directory should NOT have been removed from memory since persistence failed
expect(mockWorkspaceContext.removeDirectory).not.toHaveBeenCalled();
});
});

it('should correctly expand a Windows-style home directory path', () => {
const windowsPath = '%userprofile%\\Documents';
const expectedPath = path.win32.join(os.homedir(), 'Documents');
Expand Down
Loading
Loading