From 8d8f4469b6537faf0918773c52ea2b71abef11de Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 12 Apr 2026 14:46:56 -0400 Subject: [PATCH] fix(websearch): reject blank searxng status urls --- src/utils/websearch/status.ts | 3 +- tests/unit/utils/websearch/status.test.ts | 54 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/utils/websearch/status.ts b/src/utils/websearch/status.ts index e4efdf98..5f496624 100644 --- a/src/utils/websearch/status.ts +++ b/src/utils/websearch/status.ts @@ -29,7 +29,8 @@ function hasEnvValue(name: string): boolean { } function hasValidSearxngUrl(url: string | undefined): boolean { - return normalizeSearxngBaseUrl(url) !== null; + const normalized = normalizeSearxngBaseUrl(url); + return normalized !== null && normalized !== ''; } function getProviderStatePath(): string { diff --git a/tests/unit/utils/websearch/status.test.ts b/tests/unit/utils/websearch/status.test.ts index 3f15cf15..a24c76a6 100644 --- a/tests/unit/utils/websearch/status.test.ts +++ b/tests/unit/utils/websearch/status.test.ts @@ -169,6 +169,60 @@ describe('websearch readiness', () => { } }); + it('marks SearXNG as unavailable when enabled with a blank URL', () => { + 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: true, + url: '', + max_results: 5, + }, + duckduckgo: { enabled: false, max_results: 5 }, + 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 geminiStatusSpy = spyOn(geminiCli, 'getGeminiCliStatus').mockReturnValue({ + installed: false, + version: null, + } as any); + const geminiAuthSpy = spyOn(geminiCli, 'isGeminiAuthenticated').mockReturnValue(false); + const grokStatusSpy = spyOn(grokCli, 'getGrokCliStatus').mockReturnValue({ + installed: false, + version: null, + } as any); + const opencodeStatusSpy = spyOn(opencodeCli, 'getOpenCodeCliStatus').mockReturnValue({ + installed: false, + version: null, + } as any); + + try { + const providers = getWebSearchCliProviders(); + const searxng = providers.find((entry) => entry.id === 'searxng'); + + expect(searxng?.enabled).toBe(true); + expect(searxng?.available).toBe(false); + expect(searxng?.detail).toContain('Set a valid SearXNG base URL'); + } finally { + getConfigSpy.mockRestore(); + apiKeySpy.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');