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 })