From f4911694e37aaa97dbdfefac0b0f712c4e81e435 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 25 Aug 2026 15:35:54 -0400 Subject: [PATCH] fix(websearch): filter disabled legacy CLI probes and skip unused version fetching Closes #1716 --- src/utils/websearch/agy.ts | 58 +++++++----- src/utils/websearch/gemini-cli.ts | 58 +++++++----- src/utils/websearch/grok-cli.ts | 58 +++++++----- src/utils/websearch/opencode-cli.ts | 58 +++++++----- src/utils/websearch/status.ts | 29 ++++-- tests/unit/utils/websearch/status.test.ts | 107 ++++++++++++++++++++++ 6 files changed, 267 insertions(+), 101 deletions(-) diff --git a/src/utils/websearch/agy.ts b/src/utils/websearch/agy.ts index f5528fdf..0a987728 100644 --- a/src/utils/websearch/agy.ts +++ b/src/utils/websearch/agy.ts @@ -22,35 +22,47 @@ let agyCliCache: AgyCliStatus | null = null; * * @returns Antigravity CLI status with path and version */ -export function getAgyCliStatus(): AgyCliStatus { - // Return cached result if available - if (agyCliCache) { +export function getAgyCliStatus(options?: { fetchVersion?: boolean }): AgyCliStatus { + const fetchVersion = options?.fetchVersion !== false; + + if ( + agyCliCache && + (!fetchVersion || agyCliCache.version !== undefined || !agyCliCache.installed) + ) { + if (!fetchVersion) { + return { ...agyCliCache, version: undefined }; + } return agyCliCache; } - const result: AgyCliStatus = { - installed: false, - path: undefined, - version: undefined, - }; + const result: AgyCliStatus = agyCliCache + ? { ...agyCliCache } + : { + installed: false, + path: undefined, + version: undefined, + }; try { - const isWindows = process.platform === 'win32'; - const whichCmd = isWindows ? 'where agy' : 'which agy'; + if (!agyCliCache) { + const isWindows = process.platform === 'win32'; + const whichCmd = isWindows ? 'where agy' : 'which agy'; - const pathResult = execSync(whichCmd, { - encoding: 'utf8', - timeout: 5000, - stdio: ['pipe', 'pipe', 'pipe'], - }); + const pathResult = execSync(whichCmd, { + encoding: 'utf8', + timeout: 5000, + stdio: ['pipe', 'pipe', 'pipe'], + }); - const agyPath = pathResult.trim().split('\n')[0]; // First result on Windows + const agyPath = pathResult.trim().split('\n')[0]; - if (agyPath) { - result.installed = true; - result.path = agyPath; + if (agyPath) { + result.installed = true; + result.path = agyPath; + } + } - // Try to get version + if (result.installed && fetchVersion && result.version === undefined) { try { const versionResult = execSync('agy --version', { encoding: 'utf8', @@ -59,17 +71,17 @@ export function getAgyCliStatus(): AgyCliStatus { }); result.version = versionResult.trim(); } catch { - // Version check failed, but CLI is installed result.version = 'unknown'; } + } else if (!fetchVersion) { + result.version = undefined; } } catch { // Command not found - Antigravity CLI not installed } - // Cache result agyCliCache = result; - return result; + return fetchVersion ? result : { ...result, version: undefined }; } /** diff --git a/src/utils/websearch/gemini-cli.ts b/src/utils/websearch/gemini-cli.ts index 1069355e..6878b855 100644 --- a/src/utils/websearch/gemini-cli.ts +++ b/src/utils/websearch/gemini-cli.ts @@ -23,35 +23,47 @@ let geminiCliCache: GeminiCliStatus | null = null; * * @returns Gemini CLI status with path and version */ -export function getGeminiCliStatus(): GeminiCliStatus { - // Return cached result if available - if (geminiCliCache) { +export function getGeminiCliStatus(options?: { fetchVersion?: boolean }): GeminiCliStatus { + const fetchVersion = options?.fetchVersion !== false; + + if ( + geminiCliCache && + (!fetchVersion || geminiCliCache.version !== undefined || !geminiCliCache.installed) + ) { + if (!fetchVersion) { + return { ...geminiCliCache, version: undefined }; + } return geminiCliCache; } - const result: GeminiCliStatus = { - installed: false, - path: undefined, - version: undefined, - }; + const result: GeminiCliStatus = geminiCliCache + ? { ...geminiCliCache } + : { + installed: false, + path: undefined, + version: undefined, + }; try { - const isWindows = process.platform === 'win32'; - const whichCmd = isWindows ? 'where gemini' : 'which gemini'; + if (!geminiCliCache) { + const isWindows = process.platform === 'win32'; + const whichCmd = isWindows ? 'where gemini' : 'which gemini'; - const pathResult = execSync(whichCmd, { - encoding: 'utf8', - timeout: 5000, - stdio: ['pipe', 'pipe', 'pipe'], - }); + const pathResult = execSync(whichCmd, { + encoding: 'utf8', + timeout: 5000, + stdio: ['pipe', 'pipe', 'pipe'], + }); - const geminiPath = pathResult.trim().split('\n')[0]; // First result on Windows + const geminiPath = pathResult.trim().split('\n')[0]; - if (geminiPath) { - result.installed = true; - result.path = geminiPath; + if (geminiPath) { + result.installed = true; + result.path = geminiPath; + } + } - // Try to get version + if (result.installed && fetchVersion && result.version === undefined) { try { const versionResult = execSync('gemini --version', { encoding: 'utf8', @@ -60,17 +72,17 @@ export function getGeminiCliStatus(): GeminiCliStatus { }); result.version = versionResult.trim(); } catch { - // Version check failed, but CLI is installed result.version = 'unknown'; } + } else if (!fetchVersion) { + result.version = undefined; } } catch { // Command not found - Gemini CLI not installed } - // Cache result geminiCliCache = result; - return result; + return fetchVersion ? result : { ...result, version: undefined }; } /** diff --git a/src/utils/websearch/grok-cli.ts b/src/utils/websearch/grok-cli.ts index 3115e6bb..d84a62e1 100644 --- a/src/utils/websearch/grok-cli.ts +++ b/src/utils/websearch/grok-cli.ts @@ -20,35 +20,47 @@ let grokCliCache: GrokCliStatus | null = null; * * @returns Grok CLI status with path and version */ -export function getGrokCliStatus(): GrokCliStatus { - // Return cached result if available - if (grokCliCache) { +export function getGrokCliStatus(options?: { fetchVersion?: boolean }): GrokCliStatus { + const fetchVersion = options?.fetchVersion !== false; + + if ( + grokCliCache && + (!fetchVersion || grokCliCache.version !== undefined || !grokCliCache.installed) + ) { + if (!fetchVersion) { + return { ...grokCliCache, version: undefined }; + } return grokCliCache; } - const result: GrokCliStatus = { - installed: false, - path: undefined, - version: undefined, - }; + const result: GrokCliStatus = grokCliCache + ? { ...grokCliCache } + : { + installed: false, + path: undefined, + version: undefined, + }; try { - const isWindows = process.platform === 'win32'; - const whichCmd = isWindows ? 'where grok' : 'which grok'; + if (!grokCliCache) { + const isWindows = process.platform === 'win32'; + const whichCmd = isWindows ? 'where grok' : 'which grok'; - const pathResult = execSync(whichCmd, { - encoding: 'utf8', - timeout: 5000, - stdio: ['pipe', 'pipe', 'pipe'], - }); + const pathResult = execSync(whichCmd, { + encoding: 'utf8', + timeout: 5000, + stdio: ['pipe', 'pipe', 'pipe'], + }); - const grokPath = pathResult.trim().split('\n')[0]; // First result on Windows + const grokPath = pathResult.trim().split('\n')[0]; - if (grokPath) { - result.installed = true; - result.path = grokPath; + if (grokPath) { + result.installed = true; + result.path = grokPath; + } + } - // Try to get version + if (result.installed && fetchVersion && result.version === undefined) { try { const versionResult = execSync('grok --version', { encoding: 'utf8', @@ -57,17 +69,17 @@ export function getGrokCliStatus(): GrokCliStatus { }); result.version = versionResult.trim(); } catch { - // Version check failed, but CLI is installed result.version = 'unknown'; } + } else if (!fetchVersion) { + result.version = undefined; } } catch { // Command not found - Grok CLI not installed } - // Cache result grokCliCache = result; - return result; + return fetchVersion ? result : { ...result, version: undefined }; } /** diff --git a/src/utils/websearch/opencode-cli.ts b/src/utils/websearch/opencode-cli.ts index 36a04cfc..4061d2a3 100644 --- a/src/utils/websearch/opencode-cli.ts +++ b/src/utils/websearch/opencode-cli.ts @@ -20,35 +20,47 @@ let opencodeCliCache: OpenCodeCliStatus | null = null; * * @returns OpenCode CLI status with path and version */ -export function getOpenCodeCliStatus(): OpenCodeCliStatus { - // Return cached result if available - if (opencodeCliCache) { +export function getOpenCodeCliStatus(options?: { fetchVersion?: boolean }): OpenCodeCliStatus { + const fetchVersion = options?.fetchVersion !== false; + + if ( + opencodeCliCache && + (!fetchVersion || opencodeCliCache.version !== undefined || !opencodeCliCache.installed) + ) { + if (!fetchVersion) { + return { ...opencodeCliCache, version: undefined }; + } return opencodeCliCache; } - const result: OpenCodeCliStatus = { - installed: false, - path: undefined, - version: undefined, - }; + const result: OpenCodeCliStatus = opencodeCliCache + ? { ...opencodeCliCache } + : { + installed: false, + path: undefined, + version: undefined, + }; try { - const isWindows = process.platform === 'win32'; - const whichCmd = isWindows ? 'where opencode' : 'which opencode'; + if (!opencodeCliCache) { + const isWindows = process.platform === 'win32'; + const whichCmd = isWindows ? 'where opencode' : 'which opencode'; - const pathResult = execSync(whichCmd, { - encoding: 'utf8', - timeout: 5000, - stdio: ['pipe', 'pipe', 'pipe'], - }); + const pathResult = execSync(whichCmd, { + encoding: 'utf8', + timeout: 5000, + stdio: ['pipe', 'pipe', 'pipe'], + }); - const opencodePath = pathResult.trim().split('\n')[0]; // First result on Windows + const opencodePath = pathResult.trim().split('\n')[0]; - if (opencodePath) { - result.installed = true; - result.path = opencodePath; + if (opencodePath) { + result.installed = true; + result.path = opencodePath; + } + } - // Try to get version + if (result.installed && fetchVersion && result.version === undefined) { try { const versionResult = execSync('opencode --version', { encoding: 'utf8', @@ -57,17 +69,17 @@ export function getOpenCodeCliStatus(): OpenCodeCliStatus { }); result.version = versionResult.trim(); } catch { - // Version check failed, but CLI is installed result.version = 'unknown'; } + } else if (!fetchVersion) { + result.version = undefined; } } catch { // Command not found - OpenCode CLI not installed } - // Cache result opencodeCliCache = result; - return result; + return fetchVersion ? result : { ...result, version: undefined }; } /** diff --git a/src/utils/websearch/status.ts b/src/utils/websearch/status.ts index 946fd28f..355c54ed 100644 --- a/src/utils/websearch/status.ts +++ b/src/utils/websearch/status.ts @@ -113,13 +113,23 @@ function applyCooldownStatus( }; } -function getLegacyProviderStatuses(wsConfig: WebSearchConfigSnapshot): WebSearchCliInfo[] { - const agyStatus = getAgyCliStatus(); - const geminiStatus = getGeminiCliStatus(); - const grokStatus = getGrokCliStatus(); - const opencodeStatus = getOpenCodeCliStatus(); - const geminiAuthed = geminiStatus.installed && isGeminiAuthenticated(); +function getLegacyProviderStatuses( + wsConfig: WebSearchConfigSnapshot, + options?: { includeVersions?: boolean } +): WebSearchCliInfo[] { + const fetchVersion = options?.includeVersions !== false; + const agyEnabled = wsConfig.providers?.agy?.enabled ?? false; + const geminiEnabled = wsConfig.providers?.gemini?.enabled ?? false; + const grokEnabled = wsConfig.providers?.grok?.enabled ?? false; + const opencodeEnabled = wsConfig.providers?.opencode?.enabled ?? false; + const agyStatus = agyEnabled ? getAgyCliStatus({ fetchVersion }) : { installed: false }; + const geminiStatus = geminiEnabled ? getGeminiCliStatus({ fetchVersion }) : { installed: false }; + const grokStatus = grokEnabled ? getGrokCliStatus({ fetchVersion }) : { installed: false }; + const opencodeStatus = opencodeEnabled + ? getOpenCodeCliStatus({ fetchVersion }) + : { installed: false }; + const geminiAuthed = geminiEnabled && geminiStatus.installed && isGeminiAuthenticated(); return [ { id: 'agy', @@ -198,7 +208,8 @@ function getLegacyProviderStatuses(wsConfig: WebSearchConfigSnapshot): WebSearch * Get all WebSearch providers with their current status. */ export function getWebSearchCliProviders( - wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig(), + options?: { includeVersions?: boolean } ): WebSearchCliInfo[] { const apiKeyStates = getWebSearchApiKeyStates(); const cooldowns = readProviderCooldowns(); @@ -284,7 +295,7 @@ export function getWebSearchCliProviders( }, ]; - return [...providers, ...getLegacyProviderStatuses(wsConfig)].map((provider) => + return [...providers, ...getLegacyProviderStatuses(wsConfig, options)].map((provider) => applyCooldownStatus(provider, cooldowns) ); } @@ -359,7 +370,7 @@ export function buildWebSearchReadiness( export function getWebSearchReadiness( wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() ): WebSearchStatus { - const providers = getWebSearchCliProviders(wsConfig); + const providers = getWebSearchCliProviders(wsConfig, { includeVersions: false }); return buildWebSearchReadiness(wsConfig.enabled, providers); } diff --git a/tests/unit/utils/websearch/status.test.ts b/tests/unit/utils/websearch/status.test.ts index f625c2a2..7a4116c6 100644 --- a/tests/unit/utils/websearch/status.test.ts +++ b/tests/unit/utils/websearch/status.test.ts @@ -301,6 +301,113 @@ describe('websearch readiness', () => { } }); + it('does not invoke legacy CLI status probes when those providers are disabled', () => { + const getConfigSpy = spyOn(unifiedConfigLoader, 'getWebSearchConfig').mockReturnValue({ + enabled: true, + providers: { + exa: { enabled: false, max_results: 5 }, + tavily: { enabled: false, max_results: 5 }, + brave: { enabled: false, max_results: 5 }, + searxng: { enabled: false, url: '', max_results: 5 }, + duckduckgo: { enabled: true, max_results: 5 }, + agy: { enabled: false }, + gemini: { enabled: false }, + grok: { enabled: false }, + opencode: { enabled: false }, + }, + } as any); + const apiKeySpy = spyOn(providerSecrets, 'getWebSearchApiKeyStates').mockReturnValue({ + exa: { envVar: 'EXA_API_KEY', configured: false, available: false, source: 'none' }, + tavily: { envVar: 'TAVILY_API_KEY', configured: false, available: false, source: 'none' }, + brave: { envVar: 'BRAVE_API_KEY', configured: false, available: false, source: 'none' }, + }); + const agyStatusSpy = spyOn(agyCli, 'getAgyCliStatus').mockReturnValue({ + installed: false, + } as any); + const geminiStatusSpy = spyOn(geminiCli, 'getGeminiCliStatus').mockReturnValue({ + installed: false, + } as any); + const geminiAuthSpy = spyOn(geminiCli, 'isGeminiAuthenticated').mockReturnValue(false); + const grokStatusSpy = spyOn(grokCli, 'getGrokCliStatus').mockReturnValue({ + installed: false, + } as any); + const opencodeStatusSpy = spyOn(opencodeCli, 'getOpenCodeCliStatus').mockReturnValue({ + installed: false, + } as any); + + try { + getWebSearchCliProviders(); + expect(agyStatusSpy).not.toHaveBeenCalled(); + expect(geminiStatusSpy).not.toHaveBeenCalled(); + expect(geminiAuthSpy).not.toHaveBeenCalled(); + expect(grokStatusSpy).not.toHaveBeenCalled(); + expect(opencodeStatusSpy).not.toHaveBeenCalled(); + } finally { + getConfigSpy.mockRestore(); + apiKeySpy.mockRestore(); + agyStatusSpy.mockRestore(); + geminiStatusSpy.mockRestore(); + geminiAuthSpy.mockRestore(); + grokStatusSpy.mockRestore(); + opencodeStatusSpy.mockRestore(); + } + }); + it('does not spawn --version when includeVersions is false', () => { + const wsConfig = { + enabled: true, + providers: { + exa: { enabled: false, max_results: 5 }, + tavily: { enabled: false, max_results: 5 }, + brave: { enabled: false, max_results: 5 }, + searxng: { enabled: false, url: '', max_results: 5 }, + duckduckgo: { enabled: false, max_results: 5 }, + agy: { enabled: true, model: 'gemini-2.5-flash', timeout: 90 }, + gemini: { enabled: true }, + grok: { enabled: true }, + opencode: { enabled: true }, + }, + }; + const apiKeySpy = spyOn(providerSecrets, 'getWebSearchApiKeyStates').mockReturnValue({ + exa: { envVar: 'EXA_API_KEY', configured: false, available: false, source: 'none' }, + tavily: { envVar: 'TAVILY_API_KEY', configured: false, available: false, source: 'none' }, + brave: { envVar: 'BRAVE_API_KEY', configured: false, available: false, source: 'none' }, + }); + const agyStatusSpy = spyOn(agyCli, 'getAgyCliStatus').mockReturnValue({ + installed: true, + version: undefined, + }); + const geminiStatusSpy = spyOn(geminiCli, 'getGeminiCliStatus').mockReturnValue({ + installed: false, + version: undefined, + }); + const geminiAuthSpy = spyOn(geminiCli, 'isGeminiAuthenticated').mockReturnValue(false); + const grokStatusSpy = spyOn(grokCli, 'getGrokCliStatus').mockReturnValue({ + installed: false, + version: undefined, + }); + const opencodeStatusSpy = spyOn(opencodeCli, 'getOpenCodeCliStatus').mockReturnValue({ + installed: false, + version: undefined, + }); + + try { + getWebSearchCliProviders(wsConfig as Parameters[0], { + includeVersions: false, + }); + expect(agyStatusSpy).toHaveBeenCalledWith({ fetchVersion: false }); + expect(geminiStatusSpy).toHaveBeenCalledWith({ fetchVersion: false }); + expect(grokStatusSpy).toHaveBeenCalledWith({ fetchVersion: false }); + expect(opencodeStatusSpy).toHaveBeenCalledWith({ fetchVersion: false }); + } finally { + apiKeySpy.mockRestore(); + agyStatusSpy.mockRestore(); + geminiStatusSpy.mockRestore(); + geminiAuthSpy.mockRestore(); + grokStatusSpy.mockRestore(); + opencodeStatusSpy.mockRestore(); + } + }); + it('treats cooled-down providers as temporarily unavailable in readiness status', () => { const tempHome = mkdtempSync(join(tmpdir(), 'websearch-status-cooldown-')); const statePath = join(tempHome, '.ccs', 'cache', 'websearch-provider-state.json');