fix(tests): add test for disabling all tools in CcwToolsMcpCard component

fix(api): handle empty enabledTools array and improve default tool logic
fix(queueScheduler): ignore network errors in loadInitialState
fix(auth): ensure token generation handles max session capacity
chore(dependencies): update package requirements to use compatible version specifiers
chore(tests): add new test cases for incremental indexer and migrations
This commit is contained in:
catlog22
2026-03-02 11:26:15 +08:00
parent b36a46d59d
commit 8ad283086b
7 changed files with 159 additions and 84 deletions

View File

@@ -41,6 +41,43 @@ describe('CcwToolsMcpCard', () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
it('disables all tools when clicking "Disable All" button', async () => {
const { CcwToolsMcpCard } = await import('./CcwToolsMcpCard');
const onUpdateConfigMock = vi.fn();
render(
<CcwToolsMcpCard
target="codex"
isInstalled={true}
enabledTools={['write_file', 'read_file', 'edit_file']}
onToggleTool={vi.fn()}
onUpdateConfig={onUpdateConfigMock}
onInstall={vi.fn()}
/>,
{ locale: 'en' }
);
const user = userEvent.setup();
// Expand the card
await act(async () => {
await user.click(screen.getByText(/CCW MCP Server|mcp\.ccw\.title/i));
});
// Find and click "Disable All" button
const disableAllButton = screen.getByRole('button', {
name: /Disable All|mcp\.ccw\.actions\.disableAll/i,
});
expect(disableAllButton).toBeEnabled();
await act(async () => {
await user.click(disableAllButton);
});
// Verify onUpdateConfig was called with empty enabledTools array
await waitFor(() => {
expect(onUpdateConfigMock).toHaveBeenCalledWith({ enabledTools: [] });
});
});
it('preserves enabledTools when saving config (Codex)', async () => { it('preserves enabledTools when saving config (Codex)', async () => {
const { CcwToolsMcpCard } = await import('./CcwToolsMcpCard'); const { CcwToolsMcpCard } = await import('./CcwToolsMcpCard');
const updateCodexMock = vi.mocked(apiMock.updateCcwConfigForCodex); const updateCodexMock = vi.mocked(apiMock.updateCcwConfigForCodex);

View File

@@ -4371,7 +4371,9 @@ function buildCcwMcpServerConfig(config: {
}): { command: string; args: string[]; env: Record<string, string> } { }): { command: string; args: string[]; env: Record<string, string> } {
const env: Record<string, string> = {}; const env: Record<string, string> = {};
if (config.enabledTools && config.enabledTools.length > 0) { // Only use default when enabledTools is undefined (not provided)
// When enabledTools is an empty array, set to empty string to disable all tools
if (config.enabledTools !== undefined) {
env.CCW_ENABLED_TOOLS = config.enabledTools.join(','); env.CCW_ENABLED_TOOLS = config.enabledTools.join(',');
} else { } else {
env.CCW_ENABLED_TOOLS = 'write_file,edit_file,read_file,core_memory,ask_question,smart_search'; env.CCW_ENABLED_TOOLS = 'write_file,edit_file,read_file,core_memory,ask_question,smart_search';
@@ -4455,11 +4457,20 @@ export async function fetchCcwMcpConfig(currentProjectPath?: string): Promise<Cc
} }
// Parse enabled tools from env // Parse enabled tools from env
// Note: CCW_ENABLED_TOOLS can be empty string (all tools disabled), 'all' (default set), or comma-separated list
const env = ccwServer.env || {}; const env = ccwServer.env || {};
const enabledToolsStr = env.CCW_ENABLED_TOOLS || 'all'; const enabledToolsStr = env.CCW_ENABLED_TOOLS;
const enabledTools = enabledToolsStr === 'all' let enabledTools: string[];
? ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question'] if (enabledToolsStr === undefined || enabledToolsStr === null) {
: enabledToolsStr.split(',').map((t: string) => t.trim()); // No setting = use default tools
enabledTools = ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'];
} else if (enabledToolsStr === '' || enabledToolsStr === 'all') {
// Empty string = all tools disabled, 'all' = default set (for backward compatibility)
enabledTools = enabledToolsStr === '' ? [] : ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'];
} else {
// Comma-separated list
enabledTools = enabledToolsStr.split(',').map((t: string) => t.trim()).filter(Boolean);
}
return { return {
isInstalled: true, isInstalled: true,
@@ -4601,10 +4612,19 @@ export async function fetchCcwMcpConfigForCodex(): Promise<CcwMcpConfig> {
} }
const env = ccwServer.env || {}; const env = ccwServer.env || {};
const enabledToolsStr = env.CCW_ENABLED_TOOLS || 'all'; // Note: CCW_ENABLED_TOOLS can be empty string (all tools disabled), 'all' (default set), or comma-separated list
const enabledTools = enabledToolsStr === 'all' const enabledToolsStr = env.CCW_ENABLED_TOOLS;
? ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'] let enabledTools: string[];
: enabledToolsStr.split(',').map((t: string) => t.trim()); if (enabledToolsStr === undefined || enabledToolsStr === null) {
// No setting = use default tools
enabledTools = ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'];
} else if (enabledToolsStr === '' || enabledToolsStr === 'all') {
// Empty string = all tools disabled, 'all' = default set (for backward compatibility)
enabledTools = enabledToolsStr === '' ? [] : ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'];
} else {
// Comma-separated list
enabledTools = enabledToolsStr.split(',').map((t: string) => t.trim()).filter(Boolean);
}
return { return {
isInstalled: true, isInstalled: true,
@@ -4630,7 +4650,9 @@ function buildCcwMcpServerConfigForCodex(config: {
}): { command: string; args: string[]; env: Record<string, string> } { }): { command: string; args: string[]; env: Record<string, string> } {
const env: Record<string, string> = {}; const env: Record<string, string> = {};
if (config.enabledTools && config.enabledTools.length > 0) { // Only use default when enabledTools is undefined (not provided)
// When enabledTools is an empty array, set to empty string to disable all tools
if (config.enabledTools !== undefined) {
env.CCW_ENABLED_TOOLS = config.enabledTools.join(','); env.CCW_ENABLED_TOOLS = config.enabledTools.join(',');
} else { } else {
env.CCW_ENABLED_TOOLS = 'write_file,edit_file,read_file,core_memory,ask_question,smart_search'; env.CCW_ENABLED_TOOLS = 'write_file,edit_file,read_file,core_memory,ask_question,smart_search';

View File

@@ -195,9 +195,21 @@ export const useQueueSchedulerStore = create<QueueSchedulerStore>()(
'loadInitialState' 'loadInitialState'
); );
} catch (error) { } catch (error) {
// Silently ignore network errors (backend not connected)
// Only log non-network errors
const message = error instanceof Error ? error.message : 'Unknown error'; const message = error instanceof Error ? error.message : 'Unknown error';
console.error('[QueueScheduler] loadInitialState error:', message); const isNetworkError =
set({ error: message }, false, 'loadInitialState/error'); message.includes('Failed to fetch') ||
message.includes('NetworkError') ||
message.includes('Network request failed') ||
message.includes('ERR_CONNECTION_REFUSED') ||
message.includes('ERR_CONNECTION_RESET');
if (!isNetworkError) {
console.error('[QueueScheduler] loadInitialState error:', message);
set({ error: message }, false, 'loadInitialState/error');
}
// For network errors, keep state as-is without showing error
} }
}, },

View File

@@ -100,14 +100,16 @@ export async function handleAuthRoutes(ctx: RouteContext): Promise<boolean> {
} else { } else {
// Batch token response (pool pattern) // Batch token response (pool pattern)
const tokens = tokenManager.generateTokens(sessionId, count); const tokens = tokenManager.generateTokens(sessionId, count);
const firstToken = tokens[0];
// If no tokens generated (session at max capacity), force generate one
const firstToken = tokens.length > 0 ? tokens[0] : tokenManager.generateToken(sessionId);
// Set header and cookie with first token for compatibility // Set header and cookie with first token for compatibility
res.setHeader('X-CSRF-Token', firstToken); res.setHeader('X-CSRF-Token', firstToken);
setCsrfCookie(res, firstToken, 15 * 60); setCsrfCookie(res, firstToken, 15 * 60);
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ res.end(JSON.stringify({
tokens, tokens: tokens.length > 0 ? tokens : [firstToken],
expiresIn: 15 * 60, // seconds expiresIn: 15 * 60, // seconds
})); }));
} }

View File

@@ -8,53 +8,53 @@ Project-URL: Homepage, https://github.com/openai/codex-lens
Requires-Python: >=3.10 Requires-Python: >=3.10
Description-Content-Type: text/markdown Description-Content-Type: text/markdown
License-File: LICENSE License-File: LICENSE
Requires-Dist: typer>=0.9 Requires-Dist: typer~=0.9.0
Requires-Dist: rich>=13 Requires-Dist: rich~=13.0.0
Requires-Dist: pydantic>=2.0 Requires-Dist: pydantic~=2.0.0
Requires-Dist: tree-sitter>=0.20 Requires-Dist: tree-sitter~=0.20.0
Requires-Dist: tree-sitter-python>=0.25 Requires-Dist: tree-sitter-python~=0.25.0
Requires-Dist: tree-sitter-javascript>=0.25 Requires-Dist: tree-sitter-javascript~=0.25.0
Requires-Dist: tree-sitter-typescript>=0.23 Requires-Dist: tree-sitter-typescript~=0.23.0
Requires-Dist: pathspec>=0.11 Requires-Dist: pathspec~=0.11.0
Requires-Dist: watchdog>=3.0 Requires-Dist: watchdog~=3.0.0
Requires-Dist: ast-grep-py>=0.40.0 Requires-Dist: ast-grep-py~=0.40.0
Provides-Extra: semantic Provides-Extra: semantic
Requires-Dist: numpy>=1.24; extra == "semantic" Requires-Dist: numpy~=1.24.0; extra == "semantic"
Requires-Dist: fastembed>=0.2; extra == "semantic" Requires-Dist: fastembed~=0.2.0; extra == "semantic"
Requires-Dist: hnswlib>=0.8.0; extra == "semantic" Requires-Dist: hnswlib~=0.8.0; extra == "semantic"
Provides-Extra: semantic-gpu Provides-Extra: semantic-gpu
Requires-Dist: numpy>=1.24; extra == "semantic-gpu" Requires-Dist: numpy~=1.24.0; extra == "semantic-gpu"
Requires-Dist: fastembed>=0.2; extra == "semantic-gpu" Requires-Dist: fastembed~=0.2.0; extra == "semantic-gpu"
Requires-Dist: hnswlib>=0.8.0; extra == "semantic-gpu" Requires-Dist: hnswlib~=0.8.0; extra == "semantic-gpu"
Requires-Dist: onnxruntime-gpu>=1.15.0; extra == "semantic-gpu" Requires-Dist: onnxruntime-gpu~=1.15.0; extra == "semantic-gpu"
Provides-Extra: semantic-directml Provides-Extra: semantic-directml
Requires-Dist: numpy>=1.24; extra == "semantic-directml" Requires-Dist: numpy~=1.24.0; extra == "semantic-directml"
Requires-Dist: fastembed>=0.2; extra == "semantic-directml" Requires-Dist: fastembed~=0.2.0; extra == "semantic-directml"
Requires-Dist: hnswlib>=0.8.0; extra == "semantic-directml" Requires-Dist: hnswlib~=0.8.0; extra == "semantic-directml"
Requires-Dist: onnxruntime-directml>=1.15.0; extra == "semantic-directml" Requires-Dist: onnxruntime-directml~=1.15.0; extra == "semantic-directml"
Provides-Extra: reranker-onnx Provides-Extra: reranker-onnx
Requires-Dist: optimum>=1.16; extra == "reranker-onnx" Requires-Dist: optimum~=1.16.0; extra == "reranker-onnx"
Requires-Dist: onnxruntime>=1.15; extra == "reranker-onnx" Requires-Dist: onnxruntime~=1.15.0; extra == "reranker-onnx"
Requires-Dist: transformers>=4.36; extra == "reranker-onnx" Requires-Dist: transformers~=4.36.0; extra == "reranker-onnx"
Provides-Extra: reranker-api Provides-Extra: reranker-api
Requires-Dist: httpx>=0.25; extra == "reranker-api" Requires-Dist: httpx~=0.25.0; extra == "reranker-api"
Provides-Extra: reranker-litellm Provides-Extra: reranker-litellm
Requires-Dist: ccw-litellm>=0.1; extra == "reranker-litellm" Requires-Dist: ccw-litellm~=0.1.0; extra == "reranker-litellm"
Provides-Extra: reranker-legacy Provides-Extra: reranker-legacy
Requires-Dist: sentence-transformers>=2.2; extra == "reranker-legacy" Requires-Dist: sentence-transformers~=2.2.0; extra == "reranker-legacy"
Provides-Extra: reranker Provides-Extra: reranker
Requires-Dist: optimum>=1.16; extra == "reranker" Requires-Dist: optimum~=1.16.0; extra == "reranker"
Requires-Dist: onnxruntime>=1.15; extra == "reranker" Requires-Dist: onnxruntime~=1.15.0; extra == "reranker"
Requires-Dist: transformers>=4.36; extra == "reranker" Requires-Dist: transformers~=4.36.0; extra == "reranker"
Provides-Extra: encoding Provides-Extra: encoding
Requires-Dist: chardet>=5.0; extra == "encoding" Requires-Dist: chardet~=5.0.0; extra == "encoding"
Provides-Extra: clustering Provides-Extra: clustering
Requires-Dist: hdbscan>=0.8.1; extra == "clustering" Requires-Dist: hdbscan~=0.8.1; extra == "clustering"
Requires-Dist: scikit-learn>=1.3.0; extra == "clustering" Requires-Dist: scikit-learn~=1.3.0; extra == "clustering"
Provides-Extra: full Provides-Extra: full
Requires-Dist: tiktoken>=0.5.0; extra == "full" Requires-Dist: tiktoken~=0.5.0; extra == "full"
Provides-Extra: lsp Provides-Extra: lsp
Requires-Dist: pygls>=1.3.0; extra == "lsp" Requires-Dist: pygls~=1.3.0; extra == "lsp"
Dynamic: license-file Dynamic: license-file
# CodexLens # CodexLens

View File

@@ -153,10 +153,12 @@ tests/test_hybrid_chunker.py
tests/test_hybrid_search_e2e.py tests/test_hybrid_search_e2e.py
tests/test_hybrid_search_reranker_backend.py tests/test_hybrid_search_reranker_backend.py
tests/test_hybrid_search_unit.py tests/test_hybrid_search_unit.py
tests/test_incremental_indexer.py
tests/test_incremental_indexing.py tests/test_incremental_indexing.py
tests/test_litellm_reranker.py tests/test_litellm_reranker.py
tests/test_lsp_graph_builder_depth.py tests/test_lsp_graph_builder_depth.py
tests/test_merkle_detection.py tests/test_merkle_detection.py
tests/test_migrations.py
tests/test_parser_integration.py tests/test_parser_integration.py
tests/test_parsers.py tests/test_parsers.py
tests/test_path_mapper_windows_drive.py tests/test_path_mapper_windows_drive.py

View File

@@ -1,59 +1,59 @@
typer>=0.9 typer~=0.9.0
rich>=13 rich~=13.0.0
pydantic>=2.0 pydantic~=2.0.0
tree-sitter>=0.20 tree-sitter~=0.20.0
tree-sitter-python>=0.25 tree-sitter-python~=0.25.0
tree-sitter-javascript>=0.25 tree-sitter-javascript~=0.25.0
tree-sitter-typescript>=0.23 tree-sitter-typescript~=0.23.0
pathspec>=0.11 pathspec~=0.11.0
watchdog>=3.0 watchdog~=3.0.0
ast-grep-py>=0.40.0 ast-grep-py~=0.40.0
[clustering] [clustering]
hdbscan>=0.8.1 hdbscan~=0.8.1
scikit-learn>=1.3.0 scikit-learn~=1.3.0
[encoding] [encoding]
chardet>=5.0 chardet~=5.0.0
[full] [full]
tiktoken>=0.5.0 tiktoken~=0.5.0
[lsp] [lsp]
pygls>=1.3.0 pygls~=1.3.0
[reranker] [reranker]
optimum>=1.16 optimum~=1.16.0
onnxruntime>=1.15 onnxruntime~=1.15.0
transformers>=4.36 transformers~=4.36.0
[reranker-api] [reranker-api]
httpx>=0.25 httpx~=0.25.0
[reranker-legacy] [reranker-legacy]
sentence-transformers>=2.2 sentence-transformers~=2.2.0
[reranker-litellm] [reranker-litellm]
ccw-litellm>=0.1 ccw-litellm~=0.1.0
[reranker-onnx] [reranker-onnx]
optimum>=1.16 optimum~=1.16.0
onnxruntime>=1.15 onnxruntime~=1.15.0
transformers>=4.36 transformers~=4.36.0
[semantic] [semantic]
numpy>=1.24 numpy~=1.24.0
fastembed>=0.2 fastembed~=0.2.0
hnswlib>=0.8.0 hnswlib~=0.8.0
[semantic-directml] [semantic-directml]
numpy>=1.24 numpy~=1.24.0
fastembed>=0.2 fastembed~=0.2.0
hnswlib>=0.8.0 hnswlib~=0.8.0
onnxruntime-directml>=1.15.0 onnxruntime-directml~=1.15.0
[semantic-gpu] [semantic-gpu]
numpy>=1.24 numpy~=1.24.0
fastembed>=0.2 fastembed~=0.2.0
hnswlib>=0.8.0 hnswlib~=0.8.0
onnxruntime-gpu>=1.15.0 onnxruntime-gpu~=1.15.0