/ tests / web-tool-execution.test.ts
web-tool-execution.test.ts
  1  import { beforeEach, describe, expect, it, vi } from 'vitest'
  2  
  3  import { performBrowserFetch } from '@/server/tools/browser-fetch'
  4  import { performWebFetch } from '@/server/tools/web-fetch'
  5  import { performWebSearch } from '@/server/tools/web-search'
  6  import {
  7    executeBrowserFetchTool,
  8    executeWebFetchTool,
  9    executeWebSearchTool,
 10  } from '@/server/tools/web-tool-execution'
 11  
 12  vi.mock('@/server/tools/web-search', () => ({
 13    performWebSearch: vi.fn(),
 14  }))
 15  
 16  vi.mock('@/server/tools/web-fetch', () => ({
 17    performWebFetch: vi.fn(),
 18  }))
 19  
 20  vi.mock('@/server/tools/browser-fetch', () => ({
 21    performBrowserFetch: vi.fn(),
 22  }))
 23  
 24  describe('web-tool-execution', () => {
 25    beforeEach(() => {
 26      vi.clearAllMocks()
 27    })
 28  
 29    it('executes search with bounded maxResults and slices output', async () => {
 30      vi.mocked(performWebSearch).mockResolvedValue({
 31        provider: 'duckduckgo',
 32        cached: false,
 33        searchTimeMs: 12,
 34        results: [
 35          { title: 'one', url: 'https://example.com/1', snippet: '1' },
 36          { title: 'two', url: 'https://example.com/2', snippet: '2' },
 37          { title: 'three', url: 'https://example.com/3', snippet: '3' },
 38        ],
 39      })
 40  
 41      const result = await executeWebSearchTool({
 42        query: 'cats',
 43        maxResults: 2,
 44        searchType: 'image',
 45      })
 46  
 47      expect(performWebSearch).toHaveBeenCalledWith({
 48        query: 'cats',
 49        maxResults: 2,
 50        searchType: 'image',
 51      })
 52      expect(result).toEqual({
 53        ok: true,
 54        response: {
 55          provider: 'duckduckgo',
 56          cached: false,
 57          searchTimeMs: 12,
 58          results: [
 59            { title: 'one', url: 'https://example.com/1', snippet: '1' },
 60            { title: 'two', url: 'https://example.com/2', snippet: '2' },
 61            { title: 'three', url: 'https://example.com/3', snippet: '3' },
 62          ],
 63        },
 64        results: [
 65          { title: 'one', url: 'https://example.com/1', snippet: '1' },
 66          { title: 'two', url: 'https://example.com/2', snippet: '2' },
 67        ],
 68      })
 69    })
 70  
 71    it('keeps search unbounded when maxResults is invalid', async () => {
 72      vi.mocked(performWebSearch).mockResolvedValue({
 73        provider: 'duckduckgo',
 74        cached: true,
 75        searchTimeMs: 1,
 76        results: [{ title: 'one', url: 'https://example.com/1', snippet: '1' }],
 77      })
 78  
 79      const result = await executeWebSearchTool({
 80        query: 'cats',
 81        maxResults: -1,
 82      })
 83  
 84      expect(performWebSearch).toHaveBeenCalledWith({
 85        query: 'cats',
 86        maxResults: undefined,
 87        searchType: undefined,
 88      })
 89      expect(result).toEqual({
 90        ok: true,
 91        response: {
 92          provider: 'duckduckgo',
 93          cached: true,
 94          searchTimeMs: 1,
 95          results: [{ title: 'one', url: 'https://example.com/1', snippet: '1' }],
 96        },
 97        results: [{ title: 'one', url: 'https://example.com/1', snippet: '1' }],
 98      })
 99    })
100  
101    it('returns normalized errors for web search failures', async () => {
102      vi.mocked(performWebSearch).mockRejectedValue(new Error('search unavailable'))
103  
104      const result = await executeWebSearchTool({ query: 'cats' })
105      expect(result).toEqual({
106        ok: false,
107        error: 'search unavailable',
108      })
109    })
110  
111    it('executes fetch with bounded maxBytes', async () => {
112      vi.mocked(performWebFetch).mockResolvedValue({
113        cached: false,
114        fetchTimeMs: 10,
115        result: {
116          title: 'Example',
117          url: 'https://example.com',
118          content: 'hello',
119          contentType: 'markdown',
120          truncated: false,
121          bytesRead: 5,
122        },
123      })
124  
125      const result = await executeWebFetchTool({
126        url: 'https://example.com',
127        maxBytes: 999_999,
128      })
129  
130      expect(performWebFetch).toHaveBeenCalledWith({
131        url: 'https://example.com',
132        maxBytes: 500_000,
133      })
134      expect(result).toEqual({
135        ok: true,
136        response: {
137          cached: false,
138          fetchTimeMs: 10,
139          result: {
140            title: 'Example',
141            url: 'https://example.com',
142            content: 'hello',
143            contentType: 'markdown',
144            truncated: false,
145            bytesRead: 5,
146          },
147        },
148      })
149    })
150  
151    it('returns normalized errors for web fetch failures', async () => {
152      vi.mocked(performWebFetch).mockRejectedValue(new Error('fetch unavailable'))
153  
154      const result = await executeWebFetchTool({ url: 'https://example.com' })
155      expect(result).toEqual({
156        ok: false,
157        error: 'fetch unavailable',
158      })
159    })
160  
161    it('executes browser fetch with bounded maxBytes', async () => {
162      vi.mocked(performBrowserFetch).mockResolvedValue({
163        cached: false,
164        fetchTimeMs: 12,
165        result: {
166          title: 'Example',
167          url: 'https://example.com',
168          finalUrl: 'https://example.com/final',
169          content: 'hello',
170          contentType: 'markdown',
171          truncated: false,
172          bytesRead: 5,
173          fetchMethod: 'browser',
174        },
175      })
176  
177      const result = await executeBrowserFetchTool({
178        url: 'https://example.com',
179        maxBytes: 999_999,
180      })
181  
182      expect(performBrowserFetch).toHaveBeenCalledWith({
183        url: 'https://example.com',
184        maxBytes: 500_000,
185      })
186      expect(result).toEqual({
187        ok: true,
188        response: {
189          cached: false,
190          fetchTimeMs: 12,
191          result: {
192            title: 'Example',
193            url: 'https://example.com',
194            finalUrl: 'https://example.com/final',
195            content: 'hello',
196            contentType: 'markdown',
197            truncated: false,
198            bytesRead: 5,
199            fetchMethod: 'browser',
200          },
201        },
202      })
203    })
204  
205    it('passes preserveLinks through to browser fetch when requested', async () => {
206      vi.mocked(performBrowserFetch).mockResolvedValue({
207        cached: false,
208        fetchTimeMs: 5,
209        result: {
210          title: 'Example',
211          url: 'https://example.com',
212          finalUrl: 'https://example.com/final',
213          content: 'hello',
214          contentType: 'markdown',
215          truncated: false,
216          bytesRead: 5,
217          fetchMethod: 'browser',
218        },
219      })
220  
221      await executeBrowserFetchTool({
222        url: 'https://example.com',
223        maxBytes: 700_000,
224        preserveLinks: true,
225      })
226  
227      expect(performBrowserFetch).toHaveBeenCalledWith({
228        url: 'https://example.com',
229        maxBytes: 500_000,
230        preserveLinks: true,
231      })
232    })
233  
234    it('returns normalized errors for browser fetch failures', async () => {
235      vi.mocked(performBrowserFetch).mockRejectedValue(
236        new Error('browser unavailable'),
237      )
238  
239      const result = await executeBrowserFetchTool({ url: 'https://example.com' })
240      expect(result).toEqual({
241        ok: false,
242        error: 'browser unavailable',
243      })
244    })
245  })