|
|
@@ -10,7 +10,6 @@ import * as ToolWeb from '@deepseek-ai/dsh-tool-web'
|
|
|
import {
|
|
|
formatSearchOutput,
|
|
|
formatFetchOutput,
|
|
|
- parseSearchArgs,
|
|
|
parseFetchArgs,
|
|
|
presentSearchCall,
|
|
|
presentFetchCall,
|
|
|
@@ -20,10 +19,12 @@ import {
|
|
|
searchMetaFromResult,
|
|
|
fetchMetaFromValue,
|
|
|
fetchMetaFromResult,
|
|
|
+ WEB_SEARCH_MAX_QUERIES,
|
|
|
WEB_SEARCH_MAX_RESULTS,
|
|
|
} from '@deepseek-ai/dsh-tool-web'
|
|
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
|
import type { ToolResult } from '@deepseek-ai/dsh-tools'
|
|
|
+import { parseSearchArgs } from '../src/search.ts'
|
|
|
|
|
|
const testToolSignal = new AbortController().signal
|
|
|
|
|
|
@@ -84,9 +85,14 @@ describe('search formatting', () => {
|
|
|
expect(out).toContain('Showing the first 1 sources')
|
|
|
})
|
|
|
|
|
|
- it('validates the query', () => {
|
|
|
- expect(() => parseSearchArgs({ query: ' ' })).toThrow('non-empty')
|
|
|
- expect(parseSearchArgs({ query: 'hi' })).toEqual({ query: 'hi' })
|
|
|
+ it('validates queries', () => {
|
|
|
+ expect(parseSearchArgs({ queries: ['hi'] }, WEB_SEARCH_MAX_QUERIES)).toEqual(['hi'])
|
|
|
+ expect(parseSearchArgs({ queries: ['one', 'one', ' two '] }, WEB_SEARCH_MAX_QUERIES))
|
|
|
+ .toEqual(['one', ' two '])
|
|
|
+ expect(() => parseSearchArgs({ queries: [] }, WEB_SEARCH_MAX_QUERIES)).toThrow('at least one query')
|
|
|
+ expect(() => parseSearchArgs({ queries: ['one', 'two'] }, 1)).toThrow('at most 1 query')
|
|
|
+ expect(() => parseSearchArgs({ queries: ['one', 'two', 'three'] }, 2)).toThrow('at most 2 queries')
|
|
|
+ expect(() => parseSearchArgs({ queries: ['ok', ' '] }, WEB_SEARCH_MAX_QUERIES)).toThrow('each query must be a non-empty string')
|
|
|
})
|
|
|
|
|
|
it('falls back to the raw URL as a source label when the URL is unparseable', () => {
|
|
|
@@ -94,8 +100,8 @@ describe('search formatting', () => {
|
|
|
expect(out).toContain('[not a url](not a url)')
|
|
|
})
|
|
|
|
|
|
- it('presents a search call as a search-kind card titled by the query', () => {
|
|
|
- expect(presentSearchCall({ query: 'find me' })).toEqual({ card: 'generic', title: 'find me', kind: 'search', rawInput: 'find me' })
|
|
|
+ it('presents a search call with a joined query title', () => {
|
|
|
+ expect(presentSearchCall({ queries: ['one', 'two'] })).toEqual({ card: 'generic', title: 'one, two', kind: 'search', rawInput: 'one, two' })
|
|
|
})
|
|
|
})
|
|
|
|
|
|
@@ -145,7 +151,7 @@ describe('web_search presentation meta and result view', () => {
|
|
|
content: 'an answer', truncated: true,
|
|
|
sources: [{ url: 'https://a.test', title: 'A', snippet: 'snip', publishedAt: '2026-07-20' }],
|
|
|
})
|
|
|
- expect(presentSearchResult({ query: 'q' }, toolResult(meta, 'rendered'))).toEqual({
|
|
|
+ expect(presentSearchResult({ queries: ['q'] }, toolResult(meta, 'rendered'))).toEqual({
|
|
|
card: 'web',
|
|
|
kind: 'search',
|
|
|
title: 'q',
|
|
|
@@ -157,7 +163,7 @@ describe('web_search presentation meta and result view', () => {
|
|
|
|
|
|
it('omits the answer from the view when meta carries none', () => {
|
|
|
const meta = searchMetaFromValue({ truncated: false, sources: [{ url: 'https://a.test' }] })
|
|
|
- const view = presentSearchResult({ query: 'q' }, toolResult(meta))
|
|
|
+ const view = presentSearchResult({ queries: ['q'] }, toolResult(meta))
|
|
|
expect(view).toBeDefined()
|
|
|
expect(view && 'answer' in view).toBe(false)
|
|
|
expect(view && 'content' in view).toBe(false)
|
|
|
@@ -165,11 +171,11 @@ describe('web_search presentation meta and result view', () => {
|
|
|
|
|
|
it('falls back to the generic card on an error result', () => {
|
|
|
const meta = searchMetaFromValue({ truncated: false, sources: [{ url: 'https://a.test' }] })
|
|
|
- expect(presentSearchResult({ query: 'q' }, toolResult(meta, 'body', true))).toBeUndefined()
|
|
|
+ expect(presentSearchResult({ queries: ['q'] }, toolResult(meta, 'body', true))).toBeUndefined()
|
|
|
})
|
|
|
|
|
|
it('falls back to the generic card on absent or malformed meta', () => {
|
|
|
- expect(presentSearchResult({ query: 'q' }, toolResult(undefined))).toBeUndefined()
|
|
|
+ expect(presentSearchResult({ queries: ['q'] }, toolResult(undefined))).toBeUndefined()
|
|
|
expect(searchMetaFromResult(undefined)).toBeUndefined()
|
|
|
expect(searchMetaFromResult(null)).toBeUndefined()
|
|
|
expect(searchMetaFromResult('nope')).toBeUndefined()
|
|
|
@@ -445,7 +451,7 @@ describe('tool-web registration', () => {
|
|
|
const names = ctx.tools.schemas().map(s => s.name)
|
|
|
expect(names).toContain('web_search')
|
|
|
expect(names).toContain('web_fetch')
|
|
|
- expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('search-safe'), name: 'web_search', arguments: { query: 'q' } }))
|
|
|
+ expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('search-safe'), name: 'web_search', arguments: { queries: ['q'] } }))
|
|
|
.toEqual({ kind: 'parallel' })
|
|
|
expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('fetch-safe'), name: 'web_fetch', arguments: { url: 'https://a.test' } }))
|
|
|
.toEqual({ kind: 'parallel' })
|
|
|
@@ -474,7 +480,7 @@ describe('tool-web registration', () => {
|
|
|
expect(ctx.tools.schemas().map(s => s.name)).toContain('web_search')
|
|
|
// No provider is registered: the schema stays visible and execution reports
|
|
|
// the structured unavailability instead.
|
|
|
- const out = await call('web_search', { query: 'q' })
|
|
|
+ const out = await call('web_search', { queries: ['q'] })
|
|
|
expect(out.error?.info?.code).toBe('WEB_PROVIDER_UNAVAILABLE')
|
|
|
await fiber.dispose()
|
|
|
})
|
|
|
@@ -483,7 +489,7 @@ describe('tool-web registration', () => {
|
|
|
const { fiber, ctx } = await mountTools()
|
|
|
const prompt = await ctx.systemPrompt.assemble()
|
|
|
const text = prompt.sections.map(s => s.text).join('\n')
|
|
|
- expect(text).toContain('Use the web_search tool to discover current information on the web. It returns an optional answer plus a list of source URLs. Follow up with web_fetch when you need the full content of a specific result, and cite the relevant URLs as markdown links.')
|
|
|
+ expect(text).toContain(`Use the web_search tool to discover current information on the web. The required queries array accepts 1–${WEB_SEARCH_MAX_QUERIES} non-empty search queries; use a one-item array for a single search. It returns an optional answer plus a list of source URLs. Follow up with web_fetch when you need the full content of a specific result, and cite the relevant URLs as markdown links.`)
|
|
|
expect(text).toContain('Use the web_fetch tool to retrieve the content of a specific HTTP(S) URL')
|
|
|
await fiber.dispose()
|
|
|
})
|
|
|
@@ -505,25 +511,156 @@ describe('tool-web execution through the real registry', () => {
|
|
|
sources: [{ url: 'https://a.test', title: 'A', snippet: 'snip', publishedAt: '2026-07-20' }],
|
|
|
}
|
|
|
const { fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: searchProvider(result) })
|
|
|
- const out = await call('web_search', { query: 'q' })
|
|
|
+ const out = await call('web_search', { queries: ['q'] })
|
|
|
expect(out.isError).toBe(false)
|
|
|
expect(out.value).toEqual(result)
|
|
|
expect(out.content.map(b => b.type === 'text' ? b.text : '').join('')).toContain('[A](https://a.test)')
|
|
|
await fiber.dispose()
|
|
|
})
|
|
|
|
|
|
+ it('executes web_search with multiple queries concurrently and merges results', async () => {
|
|
|
+ const seen: string[] = []
|
|
|
+ let releaseFirst: (() => void) | undefined
|
|
|
+ const firstResult = new Promise<WebSearchResult>((resolve) => {
|
|
|
+ releaseFirst = () => {
|
|
|
+ resolve({
|
|
|
+ content: 'answer one', truncated: false,
|
|
|
+ sources: [
|
|
|
+ { url: 'https://a.test', title: 'A' },
|
|
|
+ { url: 'https://shared.test' },
|
|
|
+ ],
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ const provider: WebSearchProvider = {
|
|
|
+ id: 'stub-search',
|
|
|
+ available: () => available,
|
|
|
+ search: (request) => {
|
|
|
+ seen.push(request.query)
|
|
|
+ if (request.query === 'one') return firstResult
|
|
|
+ return Promise.resolve({
|
|
|
+ content: 'answer two', truncated: false,
|
|
|
+ sources: [
|
|
|
+ { url: 'https://b.test', title: 'B' },
|
|
|
+ { url: 'https://shared.test' },
|
|
|
+ ],
|
|
|
+ })
|
|
|
+ },
|
|
|
+ }
|
|
|
+ const { fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: provider })
|
|
|
+ const pending = call('web_search', { queries: ['one', 'one', 'two'] })
|
|
|
+ try {
|
|
|
+ await vi.waitFor(() => { expect(seen).toEqual(['one', 'two']) })
|
|
|
+ } finally {
|
|
|
+ releaseFirst?.()
|
|
|
+ }
|
|
|
+ const out = await pending
|
|
|
+ expect(out.isError).toBe(false)
|
|
|
+ expect(out.value).toEqual({
|
|
|
+ content: '### one\n\nanswer one\n\n### two\n\nanswer two',
|
|
|
+ sources: [
|
|
|
+ { url: 'https://a.test', title: 'A' },
|
|
|
+ { url: 'https://b.test', title: 'B' },
|
|
|
+ { url: 'https://shared.test' },
|
|
|
+ ],
|
|
|
+ truncated: false,
|
|
|
+ })
|
|
|
+ const body = out.content.map(b => b.type === 'text' ? b.text : '').join('')
|
|
|
+ expect(body).toContain('### one')
|
|
|
+ expect(body).toContain('### two')
|
|
|
+ await fiber.dispose()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('continues round-robin merging after a shorter result is exhausted', async () => {
|
|
|
+ const provider: WebSearchProvider = {
|
|
|
+ id: 'stub-search',
|
|
|
+ available: () => available,
|
|
|
+ search: request => Promise.resolve(request.query === 'one'
|
|
|
+ ? { content: '', sources: [{ url: 'https://a.test' }], truncated: false }
|
|
|
+ : { sources: [{ url: 'https://b.test' }, { url: 'https://c.test' }], truncated: false }),
|
|
|
+ }
|
|
|
+ const { fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: provider })
|
|
|
+ const out = await call('web_search', { queries: ['one', 'two'] })
|
|
|
+ expect(out.isError).toBe(false)
|
|
|
+ expect(out.value).toEqual({
|
|
|
+ sources: [
|
|
|
+ { url: 'https://a.test' },
|
|
|
+ { url: 'https://b.test' },
|
|
|
+ { url: 'https://c.test' },
|
|
|
+ ],
|
|
|
+ truncated: false,
|
|
|
+ })
|
|
|
+ await fiber.dispose()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('aborts sibling searches and waits for them to settle before reporting a batch failure', async () => {
|
|
|
+ let siblingAborted = false
|
|
|
+ let releaseSibling: (() => void) | undefined
|
|
|
+ const provider: WebSearchProvider = {
|
|
|
+ id: 'stub-search',
|
|
|
+ available: () => available,
|
|
|
+ search: (request, signal) => {
|
|
|
+ if (request.query === 'one') return Promise.reject(new Error('first search failed'))
|
|
|
+ return new Promise((_resolve, reject) => {
|
|
|
+ releaseSibling = () => { reject(new Error('sibling search stopped')) }
|
|
|
+ signal?.addEventListener('abort', () => {
|
|
|
+ siblingAborted = true
|
|
|
+ }, { once: true })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ }
|
|
|
+ const { fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: provider })
|
|
|
+ const pending = call('web_search', { queries: ['one', 'two'] })
|
|
|
+ let callSettled = false
|
|
|
+ void pending.then(() => { callSettled = true })
|
|
|
+ try {
|
|
|
+ await vi.waitFor(() => { expect(siblingAborted).toBe(true) })
|
|
|
+ await Promise.resolve()
|
|
|
+ expect(callSettled).toBe(false)
|
|
|
+ } finally {
|
|
|
+ releaseSibling?.()
|
|
|
+ }
|
|
|
+ const out = await pending
|
|
|
+ expect(out.isError).toBe(true)
|
|
|
+ expect(out.content).toEqual([{ type: 'text', text: 'Error: first search failed' }])
|
|
|
+ await fiber.dispose()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('caps combined multi-query results to searchMaxResults', async () => {
|
|
|
+ const provider: WebSearchProvider = {
|
|
|
+ id: 'stub-search',
|
|
|
+ available: () => available,
|
|
|
+ search: request => Promise.resolve({
|
|
|
+ sources: request.query === 'one'
|
|
|
+ ? [{ url: 'https://a.test' }, { url: 'https://b.test' }]
|
|
|
+ : [{ url: 'https://c.test' }, { url: 'https://d.test' }],
|
|
|
+ truncated: false,
|
|
|
+ }),
|
|
|
+ }
|
|
|
+ const { fiber, call } = await mountTools({ config: { searchMaxResults: 2 }, webConfig: { searchProvider: 'stub-search' }, search: provider })
|
|
|
+ const out = await call('web_search', { queries: ['one', 'two'] })
|
|
|
+ expect(out.isError).toBe(false)
|
|
|
+ expect(out.value).toEqual({
|
|
|
+ sources: [{ url: 'https://a.test' }, { url: 'https://c.test' }],
|
|
|
+ truncated: true,
|
|
|
+ })
|
|
|
+ const body = out.content.map(b => b.type === 'text' ? b.text : '').join('')
|
|
|
+ expect(body).toContain('Showing the first 2 sources.')
|
|
|
+ await fiber.dispose()
|
|
|
+ })
|
|
|
+
|
|
|
it('projects the search sources into the tool result meta and derives its web/search view', async () => {
|
|
|
const result: WebSearchResult = {
|
|
|
content: 'answer', truncated: true,
|
|
|
sources: [{ url: 'https://a.test', title: 'A', snippet: 'snip', publishedAt: '2026-07-20' }],
|
|
|
}
|
|
|
const { ctx, fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: searchProvider(result) })
|
|
|
- const out = await call('web_search', { query: 'q' })
|
|
|
+ const out = await call('web_search', { queries: ['q'] })
|
|
|
expect(out.meta).toEqual({
|
|
|
answer: 'answer', truncated: true,
|
|
|
sources: [{ url: 'https://a.test', title: 'A', snippet: 'snip', publishedAt: '2026-07-20' }],
|
|
|
})
|
|
|
- const view = ctx.tools.get('web_search')?.presentResult?.({ query: 'q' }, { content: out.content, isError: out.isError, ...out.meta !== undefined ? { meta: out.meta } : {} })
|
|
|
+ const view = ctx.tools.get('web_search')?.presentResult?.({ queries: ['q'] }, { content: out.content, isError: out.isError, ...out.meta !== undefined ? { meta: out.meta } : {} })
|
|
|
expect(view).toMatchObject({ card: 'web', kind: 'search', truncated: true, answer: 'answer' })
|
|
|
await fiber.dispose()
|
|
|
})
|
|
|
@@ -546,7 +683,7 @@ describe('tool-web execution through the real registry', () => {
|
|
|
|
|
|
it('surfaces a structured WebError when no provider is available', async () => {
|
|
|
const { fiber, call } = await mountTools()
|
|
|
- const out = await call('web_search', { query: 'q' })
|
|
|
+ const out = await call('web_search', { queries: ['q'] })
|
|
|
expect(out.isError).toBe(true)
|
|
|
expect(out.error?.info?.code).toBe('WEB_PROVIDER_UNAVAILABLE')
|
|
|
await fiber.dispose()
|
|
|
@@ -555,15 +692,15 @@ describe('tool-web execution through the real registry', () => {
|
|
|
it('surfaces WEB_PROVIDER_AMBIGUOUS for multiple unconfigured providers', async () => {
|
|
|
const { ctx, fiber, call } = await mountTools({ search: searchProvider({ sources: [], truncated: false }) })
|
|
|
ctx.web.registerSearchProvider({ id: 'other', available: () => available, search: () => Promise.resolve({ sources: [], truncated: false }) })
|
|
|
- const out = await call('web_search', { query: 'q' })
|
|
|
+ const out = await call('web_search', { queries: ['q'] })
|
|
|
expect(out.isError).toBe(true)
|
|
|
expect(out.error?.info?.code).toBe('WEB_PROVIDER_AMBIGUOUS')
|
|
|
await fiber.dispose()
|
|
|
})
|
|
|
|
|
|
- it('rejects invalid arguments with a structured INVALID_ARGS error', async () => {
|
|
|
+ it.each([{}, { queries: [123] }])('rejects absent or wrongly typed queries with a structured INVALID_ARGS error', async (args) => {
|
|
|
const { fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: searchProvider({ sources: [], truncated: false }) })
|
|
|
- const out = await call('web_search', { query: 123 })
|
|
|
+ const out = await call('web_search', args)
|
|
|
expect(out.isError).toBe(true)
|
|
|
expect(out.error?.info?.code).toBe('INVALID_ARGS')
|
|
|
await fiber.dispose()
|
|
|
@@ -635,10 +772,34 @@ describe('tool-web execution through the real registry', () => {
|
|
|
}
|
|
|
const { ctx, fiber } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: provider })
|
|
|
const controller = new AbortController()
|
|
|
- await ctx.tools.execute({ callId: CallId('search-1'), name: 'web_search', arguments: { query: 'q' }, signal: controller.signal })
|
|
|
+ await ctx.tools.execute({ callId: CallId('search-1'), name: 'web_search', arguments: { queries: ['q'] }, signal: controller.signal })
|
|
|
expect(seen.signal).toBe(controller.signal)
|
|
|
await fiber.dispose()
|
|
|
})
|
|
|
+
|
|
|
+ it('cascades caller cancellation to every multi-query search', async () => {
|
|
|
+ const signals: (AbortSignal | undefined)[] = []
|
|
|
+ const provider: WebSearchProvider = {
|
|
|
+ id: 'stub-search',
|
|
|
+ available: () => available,
|
|
|
+ search: (_request, signal) => {
|
|
|
+ signals.push(signal)
|
|
|
+ return new Promise((_resolve, reject) => {
|
|
|
+ signal?.addEventListener('abort', () => { reject(new Error('search aborted')) }, { once: true })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ }
|
|
|
+ const { ctx, fiber } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: provider })
|
|
|
+ const controller = new AbortController()
|
|
|
+ const pending = ctx.tools.execute({ callId: CallId('search-multi-1'), name: 'web_search', arguments: { queries: ['one', 'two'] }, signal: controller.signal })
|
|
|
+ await vi.waitFor(() => { expect(signals).toHaveLength(2) })
|
|
|
+ expect(signals[0]).toBe(signals[1])
|
|
|
+ expect(signals[0]).not.toBe(controller.signal)
|
|
|
+ controller.abort(new Error('caller cancelled'))
|
|
|
+ await pending
|
|
|
+ expect(signals.every(signal => signal?.aborted === true)).toBe(true)
|
|
|
+ await fiber.dispose()
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
describe('searchMaxResults is plugin config', () => {
|
|
|
@@ -650,7 +811,7 @@ describe('searchMaxResults is plugin config', () => {
|
|
|
search: (request) => { seen.maxResults = request.maxResults; return Promise.resolve({ sources: [], truncated: false }) },
|
|
|
}
|
|
|
const { fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: provider })
|
|
|
- await call('web_search', { query: 'q' })
|
|
|
+ await call('web_search', { queries: ['q'] })
|
|
|
expect(seen.maxResults).toBe(WEB_SEARCH_MAX_RESULTS)
|
|
|
await fiber.dispose()
|
|
|
})
|
|
|
@@ -663,7 +824,7 @@ describe('searchMaxResults is plugin config', () => {
|
|
|
search: () => Promise.resolve({ sources, truncated: false }),
|
|
|
}
|
|
|
const { fiber, call } = await mountTools({ config: { searchMaxResults: 2 }, webConfig: { searchProvider: 'stub-search' }, search: provider })
|
|
|
- const out = await call('web_search', { query: 'q' })
|
|
|
+ const out = await call('web_search', { queries: ['q'] })
|
|
|
expect(out.isError).toBe(false)
|
|
|
const body = out.content.map(b => b.type === 'text' ? b.text : '').join('')
|
|
|
expect(body).toContain('https://s1.test')
|
|
|
@@ -686,6 +847,43 @@ describe('searchMaxResults is plugin config', () => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
+describe('searchMaxQueries is plugin config', () => {
|
|
|
+ it('exposes the configured cap to the model and enforces it before provider calls', async () => {
|
|
|
+ const seen: string[] = []
|
|
|
+ const provider: WebSearchProvider = {
|
|
|
+ id: 'stub-search',
|
|
|
+ available: () => available,
|
|
|
+ search: (request) => {
|
|
|
+ seen.push(request.query)
|
|
|
+ return Promise.resolve({ sources: [], truncated: false })
|
|
|
+ },
|
|
|
+ }
|
|
|
+ const { fiber, ctx, call } = await mountTools({
|
|
|
+ config: { searchMaxQueries: 2 },
|
|
|
+ webConfig: { searchProvider: 'stub-search' },
|
|
|
+ search: provider,
|
|
|
+ })
|
|
|
+ const schema = ctx.tools.schemas().find(item => item.name === 'web_search')
|
|
|
+ expect(schema?.description).toContain('1–2 queries')
|
|
|
+ const prompt = await ctx.systemPrompt.assemble()
|
|
|
+ expect(prompt.sections.map(section => section.text).join('\n')).toContain('accepts 1–2 non-empty search queries')
|
|
|
+ const out = await call('web_search', { queries: ['one', 'two', 'three'] })
|
|
|
+ expect(out.isError).toBe(true)
|
|
|
+ expect(out.content).toEqual([{ type: 'text', text: 'Error: queries must contain at most 2 queries' }])
|
|
|
+ expect(seen).toEqual([])
|
|
|
+ await fiber.dispose()
|
|
|
+ })
|
|
|
+
|
|
|
+ it.each([0, -1, 1.5])('rejects an invalid searchMaxQueries value %s at load', async (value) => {
|
|
|
+ const ctx = new Context()
|
|
|
+ await ctx.plugin(SystemPrompt)
|
|
|
+ await ctx.plugin(ToolRuntime)
|
|
|
+ await ctx.plugin(WebRuntime, {})
|
|
|
+ await expect(ctx.plugin(ToolWeb, { searchMaxQueries: value }))
|
|
|
+ .rejects.toThrow(/tool-web: searchMaxQueries must be a positive integer/)
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
describe('tool-call timeout budget is plugin config', () => {
|
|
|
it('attaches the default 30s budget to web_fetch and web_search', async () => {
|
|
|
const { fiber, ctx } = await mountTools()
|