import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import TurndownService from 'turndown' import { CallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools' import WebService from '@deepseek-ai/dsh-web' import type { WebSearchProvider, WebSearchResult } from '@deepseek-ai/dsh-web' import * as ToolWeb from '@deepseek-ai/dsh-tool-web' import { formatSearchOutput, formatFetchOutput, parseSearchArgs, parseFetchArgs, presentSearchCall, presentFetchCall, presentSearchResult, presentFetchResult, searchMetaFromValue, searchMetaFromResult, fetchMetaFromValue, fetchMetaFromResult, 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' const testToolSignal = new AbortController().signal const available = true function searchProvider(result: WebSearchResult, isAvailable = available): WebSearchProvider { return { id: 'stub-search', available: () => isAvailable, search: () => Promise.resolve(result) } } /** Mount the real registry, seam, and tool-web; return an executor helper. */ async function mountTools(opts: { config?: ToolWeb.Config webConfig?: ConstructorParameters[1] search?: WebSearchProvider fetchProvider?: import('@deepseek-ai/dsh-web').WebFetchProvider } = {}): Promise<{ ctx: Context; fiber: Awaited>; call: (name: string, args: unknown) => Promise }> { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(WebService, opts.webConfig ?? {}) if (opts.search) ctx.web.registerSearchProvider(opts.search) if (opts.fetchProvider) ctx.web.registerFetchProvider(opts.fetchProvider) const fiber = await ctx.plugin(ToolWeb, opts.config ?? {}) let counter = 0 const call = (name: string, args: unknown) => ctx.tools.execute({ signal: testToolSignal, callId: CallId(`call-${++counter}`), name, arguments: args }) return { ctx, fiber, call } } describe('search formatting', () => { it('renders content, sources with titles/hostnames, snippets, and a citation reminder', () => { const out = formatSearchOutput({ content: 'an answer', truncated: false, sources: [ { url: 'https://a.test/x', title: 'A', snippet: 'about a', publishedAt: '2026-01-01' }, { url: 'https://b.test/y' }, ], }) expect(out).toContain('an answer') expect(out).toContain('[A](https://a.test/x) — about a (2026-01-01)') expect(out).toContain('[b.test](https://b.test/y)') expect(out).toContain('Cite the relevant URLs') }) it('reports no results when there is neither content nor sources', () => { expect(formatSearchOutput({ sources: [], truncated: false })) .toContain('No results found.') }) it('renders content alone when there are no sources', () => { const out = formatSearchOutput({ content: 'just an answer', sources: [], truncated: false }) expect(out).toContain('just an answer') expect(out).not.toContain('No results found.') expect(out).not.toContain('Sources:') }) it('notes truncation', () => { const out = formatSearchOutput({ sources: [{ url: 'https://a.test' }], truncated: true }) 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('falls back to the raw URL as a source label when the URL is unparseable', () => { const out = formatSearchOutput({ truncated: false, sources: [{ url: 'not a url' }] }) 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' }) }) }) /** Build a completed non-error tool result with the given meta and text content. */ function toolResult(meta: unknown, text = 'body', isError = false): ToolResult { const content: ContentBlock[] = [{ type: 'text', text }] return { content, isError, ...meta !== undefined ? { meta: meta as never } : {} } } describe('web_search presentation meta and result view', () => { it('projects sources, answer, and truncation into meta, omitting absent optional fields', () => { const meta = searchMetaFromValue({ content: 'an answer', truncated: true, sources: [ { url: 'https://a.test/x', title: 'A', snippet: 'about a', publishedAt: '2026-01-01' }, { url: 'https://b.test/y' }, ], }) expect(meta).toEqual({ answer: 'an answer', truncated: true, sources: [ { url: 'https://a.test/x', title: 'A', snippet: 'about a', publishedAt: '2026-01-01' }, { url: 'https://b.test/y' }, ], }) }) it('omits answer from meta when the provider returned none', () => { const meta = searchMetaFromValue({ truncated: false, sources: [{ url: 'https://a.test' }] }) expect(meta).toEqual({ truncated: false, sources: [{ url: 'https://a.test' }] }) }) it('round-trips projected meta back to a typed search meta', () => { const value = { content: 'ans', truncated: false, sources: [{ url: 'https://a.test', title: 'A', snippet: 's', publishedAt: '2026-01-01' }], } expect(searchMetaFromResult(searchMetaFromValue(value))).toEqual({ answer: 'ans', truncated: false, sources: [{ url: 'https://a.test', title: 'A', snippet: 's', publishedAt: '2026-01-01' }], }) }) it('presents a completed search as a web/search card carrying the structured sources, titled by the query', () => { const meta = searchMetaFromValue({ 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({ card: 'web', kind: 'search', title: 'q', answer: 'an answer', truncated: true, sources: [{ url: 'https://a.test', title: 'A', snippet: 'snip', publishedAt: '2026-07-20' }], }) }) 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)) expect(view).toBeDefined() expect(view && 'answer' in view).toBe(false) expect(view && 'content' in view).toBe(false) }) 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() }) it('falls back to the generic card on absent or malformed meta', () => { expect(presentSearchResult({ query: 'q' }, toolResult(undefined))).toBeUndefined() expect(searchMetaFromResult(undefined)).toBeUndefined() expect(searchMetaFromResult(null)).toBeUndefined() expect(searchMetaFromResult('nope')).toBeUndefined() expect(searchMetaFromResult([])).toBeUndefined() expect(searchMetaFromResult({})).toBeUndefined() expect(searchMetaFromResult({ sources: 'x', truncated: false })).toBeUndefined() expect(searchMetaFromResult({ sources: [], truncated: 'no' })).toBeUndefined() expect(searchMetaFromResult({ sources: [], truncated: false, answer: 1 })).toBeUndefined() expect(searchMetaFromResult({ sources: [null], truncated: false })).toBeUndefined() expect(searchMetaFromResult({ sources: [{ url: 1 }], truncated: false })).toBeUndefined() expect(searchMetaFromResult({ sources: [{ url: 'u', title: 2 }], truncated: false })).toBeUndefined() expect(searchMetaFromResult({ sources: [{ url: 'u', snippet: 2 }], truncated: false })).toBeUndefined() expect(searchMetaFromResult({ sources: [{ url: 'u', publishedAt: 2 }], truncated: false })).toBeUndefined() }) it('accepts an empty source list as valid meta', () => { expect(searchMetaFromResult({ sources: [], truncated: false })).toEqual({ sources: [], truncated: false }) }) }) describe('fetch formatting', () => { const NO_CAP = 1_000_000 const HEADER = 'Fetched https://a.test (HTTP 200)\n\n' const renderHtml = (content: string) => formatFetchOutput({ url: 'https://a.test', statusCode: 200, truncated: false, body: { kind: 'html', content }, }, NO_CAP).slice(HEADER.length) it('renders an html body to markdown text with a status header', () => { const out = formatFetchOutput({ url: 'https://a.test', statusCode: 200, truncated: false, body: { kind: 'html', content: '

Title

Body text

' }, }, NO_CAP) expect(out).toContain('Fetched https://a.test (HTTP 200)') expect(out).toContain('# Title') expect(out).toContain('Body text') }) it('passes a text body through and notes truncation', () => { const out = formatFetchOutput({ url: 'https://a.test', statusCode: 200, truncated: true, body: { kind: 'text', content: 'plain' }, }, NO_CAP) expect(out).toContain('plain') expect(out).toContain('Content truncated') }) it('caps the complete output and notes truncation, even when markdown escaping expands the body', () => { // 1,000 underscores render as 2,000 escaped characters — conversion can // outgrow a provider-side body cap, so the bound applies to the output. const out = formatFetchOutput({ url: 'https://a.test', statusCode: 200, truncated: false, body: { kind: 'html', content: `

${'_'.repeat(1000)}

` }, }, 500) expect(out.length).toBeLessThanOrEqual(500) expect(out).toContain('Fetched https://a.test (HTTP 200)') expect(out).toContain('\\_\\_') expect(out).toContain('Content truncated') // Exact and tiny caps: the complete result is bounded, header and footer included. const exact = formatFetchOutput({ url: 'https://a.test', statusCode: 200, truncated: false, body: { kind: 'text', content: 'abc' }, }, 'Fetched https://a.test (HTTP 200)\n\nabc'.length) expect(exact).toBe('Fetched https://a.test (HTTP 200)\n\nabc') const tiny = formatFetchOutput({ url: 'https://a.test', statusCode: 200, truncated: true, body: { kind: 'text', content: 'abcdef' }, }, 10) expect(tiny.length).toBeLessThanOrEqual(10) expect(tiny).toBe('Fetched ht') }) it('dispatches text and html bodies', () => { expect(formatFetchOutput({ url: 'https://a.test', statusCode: 200, truncated: false, body: { kind: 'text', content: 'x' }, }, NO_CAP)).toBe(`${HEADER}x`) expect(renderHtml('

y

')).toBe('y') }) it('converts html via turndown: entities, links, tables, nesting; drops script/style/noscript', () => { expect(renderHtml('

Tom & Jerry © Résumé

link')) .toBe('Tom & Jerry © Résumé\n\n[link](https://a.test)') expect(renderHtml('

Heading

  • one
  • two
')) .toBe('## Heading\n\n- one\n- two') expect(renderHtml('
AB
12
')) .toBe('| A | B |\n| --- | --- |\n| 1 | 2 |') expect(renderHtml('
LRC
123
')) .toBe('| L | R | C |\n| :--- | ---: | :---: |\n| 1 | 2 | 3 |') expect(renderHtml('

bold italic

quoted

')) .toBe('**bold _italic_**\n\n> quoted') }) it('does not expand numeric colspan attributes into unbounded output', () => { const table = '
A
B
' expect(renderHtml(table)).toBe('| A |\n| --- |\n| B |') }) it('passes deeply nested html through raw without attempting conversion', () => { // Unclosed-tag nesting makes the synchronous conversion superlinear // (seconds at 20k levels, during which the cooperative timeout cannot // fire), so the depth preflight skips conversion entirely; this must // return fast, not merely not-throw. const depth = 20_000 const pathological = '
'.repeat(depth) + 'x' + '
'.repeat(depth) const started = Date.now() expect(formatFetchOutput({ url: 'https://a.test', statusCode: 200, truncated: false, body: { kind: 'html', content: pathological }, }, NO_CAP)).toBe(`${HEADER}${pathological}`) expect(Date.now() - started).toBeLessThan(2_000) }) it('comments and mismatched closing tags cannot hide deep nesting from the preflight', () => { const pathological = '
'.repeat(600) + 'x' expect(formatFetchOutput({ url: 'https://a.test', statusCode: 200, truncated: false, body: { kind: 'html', content: pathological }, }, NO_CAP)).toBe(`${HEADER}${pathological}`) const abruptlyClosedComments = '
'.repeat(600) + 'x' expect(formatFetchOutput({ url: 'https://a.test', statusCode: 200, truncated: false, body: { kind: 'html', content: abruptlyClosedComments }, }, NO_CAP)).toBe(`${HEADER}${abruptlyClosedComments}`) }) it('the preflight accepts ordinary closed, void, self-closing, quoted, and raw-text markup', () => { const paragraphs = '

\'>x

'.repeat(600) const script = `` expect(renderHtml(`<1bad>${paragraphs}${script}`)) .not.toContain('x