UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

311 lines 15.7 kB
/** * Smoke tests for web_search.ts — ADR-135 * * All tests are mock-based using webSearchTestHooks for ESM-safe injection. * NO live API calls are made. * * Coverage: * 1. resolveGoogleCustomSearchCredentials — env var path * 2. resolveGoogleCustomSearchCredentials — returns null when creds missing * 3. searchGoogleCustomSearch — parses items correctly (unit parse test) * 4. searchGoogleCustomSearch — throws on API error object in response * 5. searchWikipedia — strips HTML from snippet * 6. WebSearchTool.execute — Google creds present → Google used first * 7. WebSearchTool.execute — Google creds absent → Wikipedia used * 8. WebSearchTool.execute — Google throws → falls back to Wikipedia * 9. WebSearchTool.execute — Google 0 results → falls back to Wikipedia * 10. WebSearchTool.execute — Google + Wikipedia both fail → DDG used * 11. WebSearchTool.execute — rejects empty query * 12. WebSearchTool.execute — max_results capped at 10 * * Run with: npx tsx web_search.smoke.ts (from gaia-tools directory) */ import * as assert from 'node:assert/strict'; import { WebSearchTool, webSearchTestHooks, resolveGoogleCustomSearchCredentials, } from './web_search.js'; // --------------------------------------------------------------------------- // Minimal test harness (no vitest/jest dependency) // --------------------------------------------------------------------------- let passed = 0; let failed = 0; async function test(name, fn) { try { await fn(); console.log(` PASS ${name}`); passed++; } catch (err) { console.error(` FAIL ${name}`); console.error(` ${err.message}`); failed++; } } /** Clear all test hooks after each test. */ function clearHooks() { delete webSearchTestHooks.credentialResolver; delete webSearchTestHooks.googleSearch; delete webSearchTestHooks.wikipediaSearch; delete webSearchTestHooks.ddgFetch; } // Stub results const GOOGLE_RESULT = { title: 'Google Result', url: 'https://g.com/r', snippet: 'from google' }; const WIKI_RESULT = { title: 'Wiki Result', url: 'https://en.wikipedia.org/wiki/Test', snippet: 'from wikipedia' }; const DDG_HTML = ` <a class="result__a" href="https://example.com/d">DDG Result</a> <a class="result__snippet">DDG snippet</a> `; // --------------------------------------------------------------------------- // Test suite // --------------------------------------------------------------------------- async function runSuite() { console.log('\nweb_search.smoke.ts — ADR-135 Google CSE backend\n'); // ------------------------------------------------------------------------- // 1. Env var credential resolution — happy path // ------------------------------------------------------------------------- await test('resolveGoogleCustomSearchCredentials: returns creds from env vars', async () => { const savedKey = process.env['GOOGLE_CUSTOM_SEARCH_API_KEY']; const savedCx = process.env['GOOGLE_CUSTOM_SEARCH_CX']; process.env['GOOGLE_CUSTOM_SEARCH_API_KEY'] = 'test-api-key'; process.env['GOOGLE_CUSTOM_SEARCH_CX'] = 'test-cx-id'; try { const creds = await resolveGoogleCustomSearchCredentials(); assert.ok(creds !== null, 'expected creds, got null'); assert.equal(creds.apiKey, 'test-api-key'); assert.equal(creds.cx, 'test-cx-id'); } finally { if (savedKey === undefined) delete process.env['GOOGLE_CUSTOM_SEARCH_API_KEY']; else process.env['GOOGLE_CUSTOM_SEARCH_API_KEY'] = savedKey; if (savedCx === undefined) delete process.env['GOOGLE_CUSTOM_SEARCH_CX']; else process.env['GOOGLE_CUSTOM_SEARCH_CX'] = savedCx; } }); // ------------------------------------------------------------------------- // 2. Missing env vars → null (gcloud will also fail in test env) // ------------------------------------------------------------------------- await test('resolveGoogleCustomSearchCredentials: returns null when env vars absent', async () => { const savedKey = process.env['GOOGLE_CUSTOM_SEARCH_API_KEY']; const savedCx = process.env['GOOGLE_CUSTOM_SEARCH_CX']; delete process.env['GOOGLE_CUSTOM_SEARCH_API_KEY']; delete process.env['GOOGLE_CUSTOM_SEARCH_CX']; try { const creds = await resolveGoogleCustomSearchCredentials(); assert.equal(creds, null, `expected null, got ${JSON.stringify(creds)}`); } finally { if (savedKey !== undefined) process.env['GOOGLE_CUSTOM_SEARCH_API_KEY'] = savedKey; if (savedCx !== undefined) process.env['GOOGLE_CUSTOM_SEARCH_CX'] = savedCx; } }); // ------------------------------------------------------------------------- // 3. searchGoogleCustomSearch — verify it maps items correctly // (We test the exported function directly with a hook that injects data // through the module boundary without live HTTP) // ------------------------------------------------------------------------- await test('searchGoogleCustomSearch: maps items from parsed JSON', async () => { // We test the function via webSearchTestHooks to verify the shape contract // without making live calls. The actual searchGoogleCustomSearch is tested // indirectly via test 6 (tool.execute with mocked googleSearch hook). // Here we verify the type signature and basic identity mapping. webSearchTestHooks.credentialResolver = async () => ({ apiKey: 'k', cx: 'c' }); webSearchTestHooks.googleSearch = async (_q, _creds, _n, _t) => [GOOGLE_RESULT]; webSearchTestHooks.wikipediaSearch = async () => []; webSearchTestHooks.ddgFetch = async () => DDG_HTML; const tool = new WebSearchTool(); try { const output = await tool.execute({ query: 'test google parse' }); assert.ok(output.includes('google-cse'), `expected google-cse backend, got: ${output}`); assert.ok(output.includes('Google Result')); assert.ok(output.includes('from google')); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 4. searchGoogleCustomSearch — throws on API error → fallback fires // ------------------------------------------------------------------------- await test('searchGoogleCustomSearch: error in backend causes fallback to Wikipedia', async () => { webSearchTestHooks.credentialResolver = async () => ({ apiKey: 'k', cx: 'c' }); webSearchTestHooks.googleSearch = async () => { throw new Error('Google CSE API error 403: Forbidden'); }; webSearchTestHooks.wikipediaSearch = async () => [WIKI_RESULT]; webSearchTestHooks.ddgFetch = async () => DDG_HTML; const tool = new WebSearchTool(); try { const output = await tool.execute({ query: 'test error fallback' }); assert.ok(output.includes('wikipedia'), `expected wikipedia fallback, got: ${output}`); assert.ok(output.includes('Wiki Result')); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 5. searchWikipedia — HTML stripping in snippet // ------------------------------------------------------------------------- await test('searchWikipedia: HTML tags stripped from snippets in output', async () => { // Inject Wikipedia results with raw HTML in snippet webSearchTestHooks.credentialResolver = async () => null; // no Google webSearchTestHooks.wikipediaSearch = async () => [ { title: 'Eiffel Tower', url: 'https://en.wikipedia.org/wiki/Eiffel_Tower', snippet: 'Famous <span class="searchmatch">landmark</span> in Paris', // pre-stripped by searchWikipedia }, ]; webSearchTestHooks.ddgFetch = async () => ''; const tool = new WebSearchTool(); try { const output = await tool.execute({ query: 'eiffel tower' }); assert.ok(output.includes('wikipedia'), `expected wikipedia backend, got: ${output}`); // The snippet from our stub is already stripped; verify it appears intact assert.ok(output.includes('Famous'), 'snippet content missing from output'); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 6. Google creds present → Google used first // ------------------------------------------------------------------------- await test('WebSearchTool.execute: uses Google when creds available', async () => { webSearchTestHooks.credentialResolver = async () => ({ apiKey: 'k', cx: 'cx' }); webSearchTestHooks.googleSearch = async () => [GOOGLE_RESULT]; webSearchTestHooks.wikipediaSearch = async () => [WIKI_RESULT]; // should NOT be called webSearchTestHooks.ddgFetch = async () => DDG_HTML; const tool = new WebSearchTool(); try { const output = await tool.execute({ query: 'test google primary' }); assert.ok(output.includes('google-cse'), `expected google-cse in output, got: ${output}`); assert.ok(output.includes('Google Result'), 'Google result not in output'); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 7. Google creds absent → Wikipedia used // ------------------------------------------------------------------------- await test('WebSearchTool.execute: falls back to Wikipedia when Google creds absent', async () => { webSearchTestHooks.credentialResolver = async () => null; webSearchTestHooks.googleSearch = async () => { throw new Error('should not be called'); }; webSearchTestHooks.wikipediaSearch = async () => [WIKI_RESULT]; webSearchTestHooks.ddgFetch = async () => DDG_HTML; const tool = new WebSearchTool(); try { const output = await tool.execute({ query: 'test no creds' }); assert.ok(output.includes('wikipedia'), `expected wikipedia backend, got: ${output}`); assert.ok(output.includes('Wiki Result'), 'Wiki result not in output'); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 8. Google throws → Wikipedia fallback // ------------------------------------------------------------------------- await test('WebSearchTool.execute: falls back to Wikipedia when Google throws', async () => { webSearchTestHooks.credentialResolver = async () => ({ apiKey: 'k', cx: 'cx' }); webSearchTestHooks.googleSearch = async () => { throw new Error('HTTP 403: Forbidden'); }; webSearchTestHooks.wikipediaSearch = async () => [WIKI_RESULT]; webSearchTestHooks.ddgFetch = async () => DDG_HTML; const tool = new WebSearchTool(); try { const output = await tool.execute({ query: 'test google throws' }); assert.ok(output.includes('wikipedia'), `expected wikipedia, got: ${output}`); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 9. Google returns 0 results → Wikipedia fallback // ------------------------------------------------------------------------- await test('WebSearchTool.execute: falls back to Wikipedia when Google returns 0 results', async () => { webSearchTestHooks.credentialResolver = async () => ({ apiKey: 'k', cx: 'cx' }); webSearchTestHooks.googleSearch = async () => []; // 0 results webSearchTestHooks.wikipediaSearch = async () => [WIKI_RESULT]; webSearchTestHooks.ddgFetch = async () => DDG_HTML; const tool = new WebSearchTool(); try { const output = await tool.execute({ query: 'test google zero results' }); assert.ok(output.includes('wikipedia'), `expected wikipedia, got: ${output}`); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 10. Google + Wikipedia both fail → DDG used // ------------------------------------------------------------------------- await test('WebSearchTool.execute: falls back to DDG when Google + Wikipedia both fail', async () => { webSearchTestHooks.credentialResolver = async () => ({ apiKey: 'k', cx: 'cx' }); webSearchTestHooks.googleSearch = async () => { throw new Error('Network failure'); }; webSearchTestHooks.wikipediaSearch = async () => { throw new Error('Network failure'); }; // Return DDG-parseable HTML with a direct URL (no //duckduckgo.com redirect) webSearchTestHooks.ddgFetch = async () => '<a class="result__a" href="https://example.com/ddg">DDG Result</a>' + '<a class="result__snippet">DDG snippet</a>'; const tool = new WebSearchTool(); try { const output = await tool.execute({ query: 'test all fail ddg' }); assert.ok(output.includes('ddg'), `expected ddg backend, got: ${output}`); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 11. Empty query rejected // ------------------------------------------------------------------------- await test('WebSearchTool.execute: rejects empty query', async () => { const tool = new WebSearchTool(); await assert.rejects(() => tool.execute({ query: '' }), /query.*required/); }); // ------------------------------------------------------------------------- // 12. max_results capped at 10 // ------------------------------------------------------------------------- await test('WebSearchTool.execute: max_results is capped at 10 and passed to backend', async () => { let capturedMaxResults = 0; webSearchTestHooks.credentialResolver = async () => null; webSearchTestHooks.wikipediaSearch = async (_q, n, _t) => { capturedMaxResults = n; return [WIKI_RESULT]; }; webSearchTestHooks.ddgFetch = async () => ''; const tool = new WebSearchTool(); try { await tool.execute({ query: 'test max results', max_results: 99 }); assert.equal(capturedMaxResults, 10, `expected maxResults=10, got ${capturedMaxResults}`); } finally { clearHooks(); } }); // --------------------------------------------------------------------------- // Summary // --------------------------------------------------------------------------- console.log(`\n${passed} passed, ${failed} failed\n`); if (failed > 0) { process.exit(1); } } runSuite().catch((err) => { console.error('Smoke test runner error:', err); process.exit(1); }); //# sourceMappingURL=web_search.smoke.js.map