@@ -35,19 +35,21 @@ describe('SearchProviderService', () => {
3535 // First provider fails
3636 mockFetch . mockRejectedValueOnce ( new Error ( 'Serper API error' ) ) ;
3737
38- // Second provider succeeds
38+ // Second provider (brave) succeeds
3939 mockFetch . mockResolvedValueOnce ( {
4040 ok : true ,
4141 json : async ( ) => ( {
42- organic_results : [
43- { title : 'Test' , link : 'https://example.com' , snippet : 'Test snippet' }
44- ]
42+ web : {
43+ results : [
44+ { title : 'Test' , url : 'https://example.com' , description : 'Test snippet' }
45+ ]
46+ }
4547 } )
4648 } ) ;
47-
49+
4850 const result = await service . search ( 'test query' ) ;
49-
50- expect ( result . provider ) . toBe ( 'serpapi ' ) ;
51+
52+ expect ( result . provider ) . toBe ( 'brave ' ) ;
5153 expect ( result . results ) . toHaveLength ( 1 ) ;
5254 expect ( result . results [ 0 ] . title ) . toBe ( 'Test' ) ;
5355 } ) ;
@@ -138,7 +140,7 @@ describe('SearchProviderService', () => {
138140 describe ( 'getAvailableProviders' , ( ) => {
139141 it ( 'should return providers with configured API keys' , ( ) => {
140142 const providers = service . getAvailableProviders ( ) ;
141- expect ( providers ) . toEqual ( [ 'serper' , 'serpapi ' , 'brave ' , 'searxng' ] ) ;
143+ expect ( providers ) . toEqual ( [ 'serper' , 'brave ' , 'serpapi ' , 'searxng' ] ) ;
142144 } ) ;
143145
144146 it ( 'should include searxng when URL is configured' , ( ) => {
@@ -384,6 +386,75 @@ describe('SearchProviderService', () => {
384386 } ) ;
385387 } ) ;
386388
389+ describe ( 'searchTavily' , ( ) => {
390+ it ( 'should search via Tavily API and return formatted results' , async ( ) => {
391+ const tavilyService = new SearchProviderService ( { tavilyApiKey : 'test-tavily-key' } ) ;
392+ const mockFetch = global . fetch as any ;
393+ mockFetch . mockResolvedValueOnce ( {
394+ ok : true ,
395+ json : ( ) => Promise . resolve ( {
396+ results : [
397+ { title : 'Tavily Result 1' , url : 'https://example.com/tavily1' , content : 'AI search snippet' , score : 0.95 } ,
398+ { title : 'Tavily Result 2' , url : 'https://example.com/tavily2' , content : 'Another snippet' , score : 0.8 } ,
399+ ] ,
400+ } ) ,
401+ } ) ;
402+
403+ const result = await tavilyService . search ( 'AI agents' , { provider : 'tavily' } ) ;
404+ expect ( result . results . length ) . toBe ( 2 ) ;
405+ expect ( result . results [ 0 ] . title ) . toBe ( 'Tavily Result 1' ) ;
406+ expect ( result . provider ) . toBe ( 'tavily' ) ;
407+ expect ( mockFetch ) . toHaveBeenCalledWith ( 'https://api.tavily.com/search' , expect . objectContaining ( { method : 'POST' } ) ) ;
408+ } ) ;
409+
410+ it ( 'should include tavily in available providers when API key is set' , ( ) => {
411+ const tavilyService = new SearchProviderService ( { tavilyApiKey : 'test-key' } ) ;
412+ expect ( tavilyService . getAvailableProviders ( ) ) . toContain ( 'tavily' ) ;
413+ } ) ;
414+
415+ it ( 'should not include tavily when no API key' , ( ) => {
416+ const noKeyService = new SearchProviderService ( { } ) ;
417+ expect ( noKeyService . getAvailableProviders ( ) ) . not . toContain ( 'tavily' ) ;
418+ } ) ;
419+ } ) ;
420+
421+ describe ( 'searchFirecrawl' , ( ) => {
422+ it ( 'should search via Firecrawl API and return formatted results' , async ( ) => {
423+ const fcService = new SearchProviderService ( { firecrawlApiKey : 'test-fc-key' } ) ;
424+ const mockFetch = global . fetch as any ;
425+ mockFetch . mockResolvedValueOnce ( {
426+ ok : true ,
427+ json : ( ) => Promise . resolve ( {
428+ success : true ,
429+ data : [
430+ { url : 'https://example.com/fc1' , title : 'Firecrawl Result' , description : 'Crawled content' } ,
431+ { url : 'https://example.com/fc2' , title : 'Another Page' , markdown : 'Full markdown content here' } ,
432+ ] ,
433+ } ) ,
434+ } ) ;
435+
436+ const result = await fcService . search ( 'web scraping' , { provider : 'firecrawl' } ) ;
437+ expect ( result . results . length ) . toBe ( 2 ) ;
438+ expect ( result . results [ 0 ] . title ) . toBe ( 'Firecrawl Result' ) ;
439+ expect ( result . results [ 0 ] . snippet ) . toBe ( 'Crawled content' ) ;
440+ expect ( result . results [ 1 ] . snippet ) . toBe ( 'Full markdown content here' ) ;
441+ expect ( result . provider ) . toBe ( 'firecrawl' ) ;
442+ } ) ;
443+
444+ it ( 'should include firecrawl in available providers when API key is set' , ( ) => {
445+ const fcService = new SearchProviderService ( { firecrawlApiKey : 'test-key' } ) ;
446+ expect ( fcService . getAvailableProviders ( ) ) . toContain ( 'firecrawl' ) ;
447+ } ) ;
448+
449+ it ( 'should handle Firecrawl API errors gracefully' , async ( ) => {
450+ const fcService = new SearchProviderService ( { firecrawlApiKey : 'bad-key' } ) ;
451+ const mockFetch = global . fetch as any ;
452+ mockFetch . mockResolvedValueOnce ( { ok : false , status : 401 , statusText : 'Unauthorized' } ) ;
453+
454+ await expect ( fcService . search ( 'test' , { provider : 'firecrawl' } ) ) . rejects . toThrow ( 'Firecrawl API error' ) ;
455+ } ) ;
456+ } ) ;
457+
387458 describe ( 'getRecommendedProviders' , ( ) => {
388459 it ( 'should return provider recommendations including SearXNG' , ( ) => {
389460 const providers = SearchProviderService . getRecommendedProviders ( ) ;
0 commit comments