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

276 lines 14 kB
/** * Smoke tests for grounded_query.ts — ADR-135 / iter-33 * * All tests are mock-based using groundedQueryTestHooks for ESM-safe injection. * NO live API calls are made. * * Coverage: * 1. resolveGoogleAIApiKey — returns key from env var * 2. resolveGoogleAIApiKey — throws when env var absent and gcloud unavailable * 3. parseGeminiResponse — well-formed response → correct GroundedQueryResult shape * 4. parseGeminiResponse — empty grounding metadata → grounded=false, empty sources * 5. parseGeminiResponse — deduplicates sources by URI * 6. parseGeminiResponse — cost calculation uses token counts * 7. GroundedQueryTool.execute — happy path: answer + sources in output * 8. GroundedQueryTool.execute — HTTP 4xx surfaces useful error message * 9. GroundedQueryTool.execute — ungrounded response includes note in output * 10. GroundedQueryTool.execute — rejects empty query * 11. GroundedQueryTool.execute — max_tokens clamped to 8192 * 12. GroundedQueryTool.execute — uses injected apiKey from constructor * * Run with: npx tsx grounded_query.smoke.ts (from gaia-tools directory) */ import * as assert from 'node:assert/strict'; import { GroundedQueryTool, groundedQueryTestHooks, resolveGoogleAIApiKey, parseGeminiResponse, createGroundedQueryTool, } from './grounded_query.js'; // --------------------------------------------------------------------------- // Minimal test harness (identical to web_search.smoke.ts — no external deps) // --------------------------------------------------------------------------- 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++; } } function clearHooks() { delete groundedQueryTestHooks.resolveKey; delete groundedQueryTestHooks.fetchResponse; } // --------------------------------------------------------------------------- // Shared stub data // --------------------------------------------------------------------------- const STUB_RESPONSE_GROUNDED = { candidates: [ { content: { parts: [{ text: 'Mercedes Sosa released 5 studio albums in that period.' }] }, groundingMetadata: { groundingChunks: [ { web: { title: 'Mercedes Sosa discography', uri: 'https://example.com/mercedes-sosa' } }, { web: { title: 'AllMusic Mercedes Sosa', uri: 'https://allmusic.com/sosa' } }, { web: { title: 'Wikipedia Mercedes Sosa', uri: 'https://en.wikipedia.org/wiki/Mercedes_Sosa' } }, { web: { title: 'Duplicate entry', uri: 'https://example.com/mercedes-sosa' } }, // duplicate URI ], webSearchQueries: ['Mercedes Sosa studio albums count'], }, }, ], usageMetadata: { promptTokenCount: 200, candidatesTokenCount: 50 }, }; const STUB_RESPONSE_UNGROUNDED = { candidates: [ { content: { parts: [{ text: 'I cannot verify this without current data.' }] }, // No groundingMetadata }, ], usageMetadata: { promptTokenCount: 100, candidatesTokenCount: 30 }, }; const STUB_RESPONSE_EMPTY_GROUNDING = { candidates: [ { content: { parts: [{ text: 'Some answer.' }] }, groundingMetadata: { groundingChunks: [], webSearchQueries: [], }, }, ], usageMetadata: { promptTokenCount: 50, candidatesTokenCount: 20 }, }; // --------------------------------------------------------------------------- // Test suite // --------------------------------------------------------------------------- async function runSuite() { console.log('\ngrounded_query.smoke.ts — ADR-135 / iter-33 Gemini Grounding\n'); // ------------------------------------------------------------------------- // 1. resolveGoogleAIApiKey — env var happy path // ------------------------------------------------------------------------- await test('resolveGoogleAIApiKey: returns key from env var', async () => { const savedKey = process.env['GOOGLE_AI_API_KEY']; process.env['GOOGLE_AI_API_KEY'] = 'test-gemini-key-123'; try { const key = await resolveGoogleAIApiKey(); assert.equal(key, 'test-gemini-key-123'); } finally { if (savedKey === undefined) delete process.env['GOOGLE_AI_API_KEY']; else process.env['GOOGLE_AI_API_KEY'] = savedKey; } }); // ------------------------------------------------------------------------- // 2. resolveGoogleAIApiKey — throws when missing (tested via tool hook path // to avoid depending on whether GOOGLE_AI_API_KEY is set in CI or dev env) // ------------------------------------------------------------------------- await test('resolveGoogleAIApiKey: throws with guidance when no key available', async () => { // Use the resolveKey hook to simulate the case where no key is configured. // This avoids a brittle env-var delete that breaks when the key IS present. groundedQueryTestHooks.resolveKey = async () => { throw new Error('grounded_query: No Google AI API key found.\n' + 'Set GOOGLE_AI_API_KEY env var, or ensure `gcloud` is authenticated.\n' + 'Get a free key at: https://aistudio.google.com/apikey'); }; const tool = new GroundedQueryTool(); try { await assert.rejects(() => tool.execute({ query: 'test missing key' }), /GOOGLE_AI_API_KEY/); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 3. parseGeminiResponse — well-formed grounded response // ------------------------------------------------------------------------- await test('parseGeminiResponse: maps answer + sources + queries correctly', async () => { const result = parseGeminiResponse(STUB_RESPONSE_GROUNDED); assert.equal(result.model, 'gemini-2.5-flash'); assert.ok(result.answer.includes('5 studio albums')); assert.equal(result.grounded, true); // 4 chunks but one duplicate URI → 3 unique sources assert.equal(result.sources.length, 3, `expected 3 unique sources, got ${result.sources.length}`); assert.equal(result.search_queries_used.length, 1); assert.equal(result.search_queries_used[0], 'Mercedes Sosa studio albums count'); }); // ------------------------------------------------------------------------- // 4. parseGeminiResponse — empty grounding → grounded=false // ------------------------------------------------------------------------- await test('parseGeminiResponse: empty grounding metadata → grounded=false', async () => { const result = parseGeminiResponse(STUB_RESPONSE_EMPTY_GROUNDING); assert.equal(result.grounded, false); assert.equal(result.sources.length, 0); assert.equal(result.search_queries_used.length, 0); }); // ------------------------------------------------------------------------- // 5. parseGeminiResponse — deduplicates sources by URI (already tested in #3 // but let's verify explicitly) // ------------------------------------------------------------------------- await test('parseGeminiResponse: deduplicates sources by URI', async () => { const result = parseGeminiResponse(STUB_RESPONSE_GROUNDED); const uris = result.sources.map((s) => s.uri); const uniqueUris = new Set(uris); assert.equal(uris.length, uniqueUris.size, 'duplicate URIs found in sources'); }); // ------------------------------------------------------------------------- // 6. parseGeminiResponse — cost calculation // ------------------------------------------------------------------------- await test('parseGeminiResponse: cost_usd calculated from token counts', async () => { const result = parseGeminiResponse(STUB_RESPONSE_GROUNDED); // 200 input @ $0.075/M + 50 output @ $0.30/M const expectedMin = (200 / 1_000_000) * 0.075 + (50 / 1_000_000) * 0.30; const expectedMax = expectedMin * 1.01; // 1% tolerance for float arithmetic assert.ok(result.cost_usd >= expectedMin && result.cost_usd <= expectedMax, `cost_usd=${result.cost_usd} not in [${expectedMin}, ${expectedMax}]`); }); // ------------------------------------------------------------------------- // 7. GroundedQueryTool.execute — happy path // ------------------------------------------------------------------------- await test('GroundedQueryTool.execute: happy path produces answer + sources in output', async () => { groundedQueryTestHooks.resolveKey = async () => 'injected-key'; groundedQueryTestHooks.fetchResponse = async () => STUB_RESPONSE_GROUNDED; const tool = new GroundedQueryTool(); try { const output = await tool.execute({ query: 'How many albums did Mercedes Sosa release?' }); assert.ok(output.includes('gemini-2.5-flash'), 'model missing from output'); assert.ok(output.includes('5 studio albums'), 'answer text missing from output'); assert.ok(output.includes('Sources:'), 'Sources section missing from output'); assert.ok(output.includes('example.com/mercedes-sosa'), 'source URI missing from output'); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 8. GroundedQueryTool.execute — HTTP 4xx surfaces useful error // ------------------------------------------------------------------------- await test('GroundedQueryTool.execute: HTTP error surfaces in thrown Error', async () => { groundedQueryTestHooks.resolveKey = async () => 'bad-key'; groundedQueryTestHooks.fetchResponse = async () => { throw new Error('grounded_query: Gemini HTTP 400: API key not valid'); }; const tool = new GroundedQueryTool(); try { await assert.rejects(() => tool.execute({ query: 'test error' }), /Gemini HTTP 400/); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 9. GroundedQueryTool.execute — ungrounded response includes note // ------------------------------------------------------------------------- await test('GroundedQueryTool.execute: ungrounded response includes note in output', async () => { groundedQueryTestHooks.resolveKey = async () => 'injected-key'; groundedQueryTestHooks.fetchResponse = async () => STUB_RESPONSE_UNGROUNDED; const tool = new GroundedQueryTool(); try { const output = await tool.execute({ query: 'some query' }); assert.ok(output.includes('unverified'), `expected unverified note in: ${output}`); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 10. GroundedQueryTool.execute — rejects empty query // ------------------------------------------------------------------------- await test('GroundedQueryTool.execute: rejects empty query', async () => { const tool = new GroundedQueryTool(); await assert.rejects(() => tool.execute({ query: '' }), /query.*required/); }); // ------------------------------------------------------------------------- // 11. GroundedQueryTool.execute — max_tokens clamped // ------------------------------------------------------------------------- await test('GroundedQueryTool.execute: max_tokens clamped to 8192', async () => { let capturedMaxTokens = 0; groundedQueryTestHooks.resolveKey = async () => 'injected-key'; groundedQueryTestHooks.fetchResponse = async (_q, maxTokens) => { capturedMaxTokens = maxTokens; return STUB_RESPONSE_GROUNDED; }; const tool = new GroundedQueryTool(); try { await tool.execute({ query: 'test max tokens', max_tokens: 99_999 }); assert.equal(capturedMaxTokens, 8192, `expected 8192, got ${capturedMaxTokens}`); } finally { clearHooks(); } }); // ------------------------------------------------------------------------- // 12. GroundedQueryTool — uses injected apiKey from constructor // ------------------------------------------------------------------------- await test('createGroundedQueryTool: uses apiKey passed to constructor', async () => { let capturedKey = ''; groundedQueryTestHooks.fetchResponse = async (_q, _max, key) => { capturedKey = key; return STUB_RESPONSE_GROUNDED; }; const tool = createGroundedQueryTool({ apiKey: 'constructor-key' }); try { await tool.execute({ query: 'test constructor key' }); assert.equal(capturedKey, 'constructor-key', `expected 'constructor-key', got '${capturedKey}'`); } 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=grounded_query.smoke.js.map