/** * Tests for the agent-thinking panel renderer in components/progress.js * (the `updateAgentThinking` function). * * Regression coverage for the "Using web_search" display bug * (PR #4470 / fix/display-actual-engine-name): * * The LangGraph strategy puts the friendly engine name in the progress * event's `data.message` (e.g. `🔍 Searching DuckDuckGo: "..."`) while * keeping the stable tool id in `data.tool` ("web_search"). The renderer * must surface the friendly message, not "Using web_search". * * The MCP strategy puts the friendly label in `data.message` * (`ACTION: Using DuckDuckGo - "..."`) AND also supplies `data.arguments`. * The renderer must show the message verbatim without duplicating the * query that's already embedded in it. * * When no message is supplied, the renderer falls back to * `Using ${data.tool}` (+ args), so the panel never renders blank. */ let progressComponent; beforeAll(async () => { // progress.js is an IIFE; importing it runs the module and exposes // window.progressComponent (which now includes updateAgentThinking). await import('@js/components/progress.js'); progressComponent = window.progressComponent; }); beforeEach(() => { // updateAgentThinking queries these two ids and appends step nodes. document.body.innerHTML = ` `; }); function renderToolCall(data) { progressComponent.updateAgentThinking({ phase: 'tool_call', ...data }); const step = document .getElementById('agent-thinking-content') .querySelector('.ldr-agent-step-content'); return step ? step.textContent : null; } describe('updateAgentThinking - tool_call rendering', () => { it('shows the LangGraph friendly message instead of "Using web_search"', () => { const content = renderToolCall({ tool: 'web_search', // stable id, must NOT be the display source message: '🔍 Searching DuckDuckGo: "climate policy"', iteration: 1, }); expect(content).toBe('🔍 Searching DuckDuckGo: "climate policy"'); expect(content).not.toContain('web_search'); expect(content).not.toContain('Using '); }); it('shows the MCP friendly message without duplicating the query', () => { const content = renderToolCall({ tool: 'DuckDuckGo', message: 'ACTION: Using DuckDuckGo - "climate policy"', arguments: { query: 'climate policy' }, }); expect(content).toBe('ACTION: Using DuckDuckGo - "climate policy"'); // The query is embedded in the message; it must appear exactly once // (no extra `\nQuery: "..."` appended from data.arguments). const occurrences = content.split('climate policy').length - 1; expect(occurrences).toBe(1); expect(content).not.toContain('Query:'); }); it('falls back to "Using " with query when no message is present', () => { const content = renderToolCall({ tool: 'web_search', arguments: { query: 'climate policy' }, }); expect(content).toBe('Using web_search\nQuery: "climate policy"'); }); it('falls back to "Using unknown" when neither message nor tool is present', () => { const content = renderToolCall({}); expect(content).toBe('Using unknown'); }); }); function renderObservation(data) { progressComponent.updateAgentThinking({ phase: 'observation', ...data }); const step = document .getElementById('agent-thinking-content') .querySelector('.ldr-agent-step-content'); return step ? step.textContent : null; } describe('updateAgentThinking - observation rendering', () => { it('keeps the "From {engine}" attribution and appends data.content beneath it', () => { const content = renderObservation({ message: '📄 From the web (SearXNG): [1] Some Title (http://a.com) Snip', content: '[1] Some Title (http://a.com)\nThe full snippet body.', }); expect(content).toBe( '📄 From the web (SearXNG): [1] Some Title (http://a.com) Snip\n' + '[1] Some Title (http://a.com)\nThe full snippet body.' ); }); it('shows the message alone when no content is attached (short outputs)', () => { const content = renderObservation({ message: '📄 From PubMed: No results.', }); expect(content).toBe('📄 From PubMed: No results.'); }); it('falls back to content when no message is present', () => { const content = renderObservation({ content: 'raw tool output only', }); expect(content).toBe('raw tool output only'); }); it('truncates the composed result to 800 chars', () => { const content = renderObservation({ message: '📄 From arXiv: long', content: 'z'.repeat(1000), }); expect(content.length).toBe(803); // 800 + '...' expect(content.endsWith('...')).toBe(true); expect(content.startsWith('📄 From arXiv: long\n')).toBe(true); }); });