Buckets:
evalstate/sandbox-testing-2 / hf-mcp-server /packages /app /test /server /utils /tool-selection-strategy.test.ts
| import { describe, it, expect, beforeEach, afterEach } from 'vitest'; | |
| import { ToolSelectionStrategy, ToolSelectionMode, type ToolSelectionContext } from '../../../src/server/utils/tool-selection-strategy.js'; | |
| import { McpApiClient, type ApiClientConfig } from '../../../src/server/utils/mcp-api-client.js'; | |
| import type { AppSettings } from '../../../src/shared/settings.js'; | |
| import type { TransportInfo } from '../../../src/shared/transport-info.js'; | |
| import { ALL_BUILTIN_TOOL_IDS, CREATE_REPO_TOOL_ID, REPO_SEARCH_TOOL_ID, TOOL_ID_GROUPS } from '@llmindset/hf-mcp'; | |
| import { extractAuthBouquetAndMix } from '../../../src/server/utils/auth-utils.js'; | |
| import { normalizeBuiltInTools } from '../../../src/shared/tool-normalizer.js'; | |
| import { BOUQUETS } from '../../../src/shared/bouquet-presets.js'; | |
| describe('extractBouquetAndMix', () => { | |
| it('should extract bouquet from headers', () => { | |
| const headers = { 'x-mcp-bouquet': 'search' }; | |
| const { bouquet, mix } = extractAuthBouquetAndMix(headers); | |
| expect(bouquet).toBe('search'); | |
| expect(mix).toBeUndefined(); | |
| }); | |
| it('should extract mix from headers', () => { | |
| const headers = { 'x-mcp-mix': 'hf_api' }; | |
| const result = extractAuthBouquetAndMix(headers); | |
| expect(result.bouquet).toBeUndefined(); | |
| expect(result.mix).toEqual(['hf_api']); | |
| }); | |
| it('should extract both bouquet and mix from headers', () => { | |
| const headers = { | |
| 'x-mcp-bouquet': 'search', | |
| 'x-mcp-mix': 'hf_api', | |
| }; | |
| const result = extractAuthBouquetAndMix(headers); | |
| expect(result.bouquet).toBe('search'); | |
| expect(result.mix).toEqual(['hf_api']); | |
| }); | |
| it('should handle null headers', () => { | |
| const result = extractAuthBouquetAndMix(null); | |
| expect(result.bouquet).toBeUndefined(); | |
| expect(result.mix).toBeUndefined(); | |
| }); | |
| it('should handle empty headers', () => { | |
| const result = extractAuthBouquetAndMix({}); | |
| expect(result.bouquet).toBeUndefined(); | |
| expect(result.mix).toBeUndefined(); | |
| }); | |
| it('should parse comma-separated mix list', () => { | |
| const headers = { 'x-mcp-mix': 'hf_api, jobs ,hub_repo_details_readme' }; | |
| const result = extractAuthBouquetAndMix(headers); | |
| expect(result.mix).toEqual(['hf_api', 'jobs', 'hub_repo_details_readme']); | |
| }); | |
| }); | |
| describe('BOUQUETS configuration', () => { | |
| it('should have correct hf_api bouquet', () => { | |
| const bouquet = BOUQUETS.hf_api; | |
| expect(bouquet).toBeDefined(); | |
| if (bouquet) { | |
| expect(bouquet.builtInTools).toEqual(TOOL_ID_GROUPS.hf_api); | |
| expect(bouquet.spaceTools).toEqual([]); | |
| } | |
| }); | |
| it('should have correct spaces bouquet', () => { | |
| const bouquet = BOUQUETS.spaces; | |
| expect(bouquet).toBeDefined(); | |
| if (bouquet) { | |
| expect(bouquet.builtInTools).toEqual(TOOL_ID_GROUPS.spaces); | |
| expect(bouquet.spaceTools).toEqual([]); | |
| } | |
| }); | |
| it('should have correct search bouquet', () => { | |
| const bouquet = BOUQUETS.search; | |
| expect(bouquet).toBeDefined(); | |
| if (bouquet) { | |
| expect(bouquet.builtInTools).toEqual(TOOL_ID_GROUPS.search); | |
| expect(bouquet.spaceTools).toEqual([]); | |
| } | |
| }); | |
| it('should have correct all bouquet', () => { | |
| const bouquet = BOUQUETS.all; | |
| expect(bouquet).toBeDefined(); | |
| if (bouquet) { | |
| expect(bouquet.builtInTools).toEqual(ALL_BUILTIN_TOOL_IDS); | |
| expect(bouquet.spaceTools).toEqual([]); | |
| } | |
| }); | |
| }); | |
| describe('ToolSelectionStrategy', () => { | |
| let strategy: ToolSelectionStrategy; | |
| let mockApiClient: McpApiClient; | |
| // Create a real API client with minimal config for testing | |
| beforeEach(() => { | |
| const config: ApiClientConfig = { | |
| type: 'polling', | |
| baseUrl: 'http://localhost:3000', | |
| pollInterval: 5000, | |
| staticGradioEndpoints: [], | |
| }; | |
| const transportInfo: TransportInfo = { | |
| transport: 'streamableHttpJson', | |
| port: 3000, | |
| defaultHfTokenSet: false, | |
| jsonResponseEnabled: true, | |
| externalApiMode: false, | |
| stdioClient: null, | |
| }; | |
| mockApiClient = new McpApiClient(config, transportInfo); | |
| strategy = new ToolSelectionStrategy(mockApiClient); | |
| }); | |
| describe('Bouquet Override (Highest Precedence)', () => { | |
| it('should hide authenticated tools from anonymous bouquet users', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-bouquet': 'hf_api' }, | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.BOUQUET_OVERRIDE); | |
| expect(TOOL_ID_GROUPS.hf_api).toContain(CREATE_REPO_TOOL_ID); | |
| expect(result.enabledToolIds).not.toContain(CREATE_REPO_TOOL_ID); | |
| }); | |
| it('should use bouquet override for search bouquet', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-bouquet': 'search' }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.BOUQUET_OVERRIDE); | |
| expect(result.enabledToolIds).toEqual(TOOL_ID_GROUPS.search); | |
| expect(result.reason).toBe('Bouquet override: search'); | |
| expect(result.baseSettings).toBeUndefined(); | |
| expect(result.mixedBouquet).toBeUndefined(); | |
| }); | |
| it('should use bouquet override for hf_api bouquet', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-bouquet': 'hf_api' }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.BOUQUET_OVERRIDE); | |
| expect(result.enabledToolIds).toEqual(TOOL_ID_GROUPS.hf_api); | |
| expect(result.reason).toBe('Bouquet override: hf_api'); | |
| }); | |
| it('should use bouquet override for spaces bouquet', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-bouquet': 'spaces' }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.BOUQUET_OVERRIDE); | |
| expect(result.enabledToolIds).toEqual(TOOL_ID_GROUPS.spaces); | |
| expect(result.reason).toBe('Bouquet override: spaces'); | |
| }); | |
| it('should use bouquet override for all bouquet', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-bouquet': 'all' }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.BOUQUET_OVERRIDE); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(ALL_BUILTIN_TOOL_IDS)); | |
| expect(result.reason).toBe('Bouquet override: all'); | |
| }); | |
| it('should ignore invalid bouquet names', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-bouquet': 'invalid_bouquet' }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| // Should fall through to fallback since no valid bouquet or user settings | |
| expect(result.mode).toBe(ToolSelectionMode.FALLBACK); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(ALL_BUILTIN_TOOL_IDS)); | |
| }); | |
| it('should prefer bouquet over mix when both are present', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_semantic_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { | |
| 'x-mcp-bouquet': 'search', | |
| 'x-mcp-mix': 'hf_api', | |
| }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.BOUQUET_OVERRIDE); | |
| expect(result.enabledToolIds).toEqual(TOOL_ID_GROUPS.search); | |
| expect(result.reason).toBe('Bouquet override: search'); | |
| }); | |
| }); | |
| describe('Mix Mode (Second Precedence)', () => { | |
| it('should mix hf_api tools with user settings', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_semantic_search', 'hf_dataset_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-mix': 'hf_api' }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.MIX); | |
| expect(result.reason).toBe('User settings + mix(hf_api)'); | |
| expect(result.baseSettings).toEqual(userSettings); | |
| expect(result.mixedBouquet).toEqual(['hf_api']); | |
| // Should contain user tools + hf_api tools (deduplicated) | |
| const expectedTools = normalizeBuiltInTools([ | |
| ...new Set([...userSettings.builtInTools, ...TOOL_ID_GROUPS.hf_api]), | |
| ]); | |
| expect(result.enabledToolIds).toEqual(expectedTools); | |
| }); | |
| it('should mix search tools with user settings', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_whoami', 'hf_duplicate_space'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-mix': 'search' }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.MIX); | |
| expect(result.reason).toBe('User settings + mix(search)'); | |
| const expectedTools = [...new Set([...userSettings.builtInTools, ...TOOL_ID_GROUPS.search])]; | |
| expect(result.enabledToolIds).toEqual(expectedTools); | |
| }); | |
| it('should deduplicate tools when mixing', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_semantic_search', 'hf_model_search'], // Already has some search tools | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-mix': 'search' }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.MIX); | |
| // Should not have duplicates | |
| const uniqueTools = [...new Set(result.enabledToolIds)]; | |
| expect(result.enabledToolIds).toEqual(uniqueTools); | |
| expect(result.enabledToolIds.length).toBe(uniqueTools.length); | |
| }); | |
| it('should mix multiple bouquets when comma separated', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_whoami'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-mix': 'hf_api,search' }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.MIX); | |
| expect(result.reason).toBe('User settings + mix(hf_api,search)'); | |
| expect(result.mixedBouquet).toEqual(['hf_api', 'search']); | |
| const expectedTools = normalizeBuiltInTools([ | |
| ...new Set([...userSettings.builtInTools, ...TOOL_ID_GROUPS.hf_api, ...TOOL_ID_GROUPS.search]), | |
| ]); | |
| expect(result.enabledToolIds).toEqual(expectedTools); | |
| }); | |
| it('should ignore mix when no user settings available', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-mix': 'hf_api' }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| // Should fall through to fallback since no user settings to mix with | |
| expect(result.mode).toBe(ToolSelectionMode.FALLBACK); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(ALL_BUILTIN_TOOL_IDS)); | |
| }); | |
| it('should ignore invalid mix bouquet names', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_semantic_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-mix': 'invalid_mix' }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| // Should use user settings without mixing | |
| expect(result.mode).toBe(ToolSelectionMode.INTERNAL_API); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(userSettings.builtInTools)); | |
| }); | |
| }); | |
| describe('User Settings Mode (Third Precedence)', () => { | |
| it('should hide authenticated tools from anonymous user settings', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: [CREATE_REPO_TOOL_ID, REPO_SEARCH_TOOL_ID], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| userSettings, | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.INTERNAL_API); | |
| expect(result.enabledToolIds).toEqual([REPO_SEARCH_TOOL_ID]); | |
| }); | |
| it('should use provided user settings in internal API mode', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_semantic_search', 'hf_model_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.INTERNAL_API); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(userSettings.builtInTools)); | |
| expect(result.reason).toBe('Internal API user settings'); | |
| expect(result.baseSettings).toEqual(userSettings); | |
| }); | |
| it('should use provided user settings in external API mode', async () => { | |
| // Create external API mode client | |
| const externalConfig: ApiClientConfig = { | |
| type: 'external', | |
| externalUrl: 'https://api.example.com/settings', | |
| hfToken: 'test-token', | |
| }; | |
| const externalTransportInfo: TransportInfo = { | |
| transport: 'streamableHttpJson', | |
| port: 3000, | |
| defaultHfTokenSet: false, | |
| jsonResponseEnabled: true, | |
| externalApiMode: true, | |
| stdioClient: null, | |
| }; | |
| const externalApiClient = new McpApiClient(externalConfig, externalTransportInfo); | |
| const externalStrategy = new ToolSelectionStrategy(externalApiClient); | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_paper_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await externalStrategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.EXTERNAL_API); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(userSettings.builtInTools)); | |
| expect(result.reason).toBe('External API user settings'); | |
| }); | |
| }); | |
| describe('Fallback Mode (Lowest Precedence)', () => { | |
| it('should use fallback when no configuration is available', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.FALLBACK); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(ALL_BUILTIN_TOOL_IDS)); | |
| expect(result.reason).toBe('Fallback - no settings available'); | |
| expect(result.baseSettings).toBeUndefined(); | |
| }); | |
| it('should use fallback when headers are null', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: null, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.FALLBACK); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(ALL_BUILTIN_TOOL_IDS)); | |
| }); | |
| }); | |
| describe('Complex Scenarios', () => { | |
| it('should handle empty user settings', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: [], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.INTERNAL_API); | |
| expect(result.enabledToolIds).toEqual([]); | |
| expect(result.baseSettings).toEqual(userSettings); | |
| }); | |
| it('should handle mix with empty user settings', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: [], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-mix': 'search' }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.MIX); | |
| expect(result.enabledToolIds).toEqual(TOOL_ID_GROUPS.search); | |
| expect(result.mixedBouquet).toEqual(['search']); | |
| }); | |
| it('should handle all possible tool types in mix', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_whoami'], // Start with one tool | |
| spaceTools: [], | |
| }; | |
| // Test mixing with each bouquet type | |
| for (const [bouquetName, bouquetConfig] of Object.entries(BOUQUETS)) { | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-mix': bouquetName }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.MIX); | |
| expect(result.mixedBouquet).toEqual([bouquetName]); | |
| const expectedTools = [...new Set([...userSettings.builtInTools, ...bouquetConfig.builtInTools])]; | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(expectedTools)); | |
| } | |
| }); | |
| it('should preserve gradio endpoints when mixing with all bouquet in internal API mode', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_whoami'], // Most tools disabled via frontend | |
| spaceTools: [ | |
| { | |
| name: 'My Custom GPT', | |
| subdomain: 'user123-my-custom-gpt', | |
| _id: 'custom-1', | |
| emoji: '🤖', | |
| }, | |
| { | |
| name: 'Company Analytics', | |
| subdomain: 'corp-analytics-tool', | |
| _id: 'custom-2', | |
| emoji: '📊', | |
| }, | |
| ], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-mix': 'all' }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.MIX); | |
| expect(result.mixedBouquet).toEqual(['all']); | |
| expect(result.reason).toBe('User settings + mix(all)'); | |
| // Should get user's minimal tools + ALL built-in tools (deduplicated) | |
| const expectedBuiltInTools = [...new Set([...userSettings.builtInTools, ...ALL_BUILTIN_TOOL_IDS])]; | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(expectedBuiltInTools)); | |
| // Should preserve base settings including gradio endpoints | |
| expect(result.baseSettings).toEqual(userSettings); | |
| expect(result.baseSettings?.spaceTools).toHaveLength(2); | |
| expect(result.baseSettings?.spaceTools).toEqual([ | |
| { | |
| name: 'My Custom GPT', | |
| subdomain: 'user123-my-custom-gpt', | |
| _id: 'custom-1', | |
| emoji: '🤖', | |
| }, | |
| { | |
| name: 'Company Analytics', | |
| subdomain: 'corp-analytics-tool', | |
| _id: 'custom-2', | |
| emoji: '📊', | |
| }, | |
| ]); | |
| }); | |
| }); | |
| describe('SEARCH_ENABLES_FETCH feature', () => { | |
| const originalEnv = process.env.SEARCH_ENABLES_FETCH; | |
| afterEach(() => { | |
| // Restore original env value | |
| if (originalEnv === undefined) { | |
| delete process.env.SEARCH_ENABLES_FETCH; | |
| } else { | |
| process.env.SEARCH_ENABLES_FETCH = originalEnv; | |
| } | |
| }); | |
| it('should not auto-enable hf_doc_fetch when SEARCH_ENABLES_FETCH is not set', async () => { | |
| delete process.env.SEARCH_ENABLES_FETCH; | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_doc_search', 'hf_model_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(['hf_doc_search', 'hf_model_search'])); | |
| expect(result.enabledToolIds).not.toContain('hf_doc_fetch'); | |
| }); | |
| it('should not auto-enable hf_doc_fetch when SEARCH_ENABLES_FETCH is false', async () => { | |
| process.env.SEARCH_ENABLES_FETCH = 'false'; | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_doc_search', 'hf_model_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(['hf_doc_search', 'hf_model_search'])); | |
| expect(result.enabledToolIds).not.toContain('hf_doc_fetch'); | |
| }); | |
| it('should auto-enable hf_doc_fetch when SEARCH_ENABLES_FETCH=true and hf_doc_search is enabled', async () => { | |
| process.env.SEARCH_ENABLES_FETCH = 'true'; | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_doc_search', 'hf_model_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.enabledToolIds).toContain('hf_doc_search'); | |
| expect(result.enabledToolIds).toContain('hf_doc_fetch'); | |
| expect(result.enabledToolIds).toContain(REPO_SEARCH_TOOL_ID); | |
| expect(result.enabledToolIds).toHaveLength(3); | |
| }); | |
| it('should not add hf_doc_fetch when hf_doc_search is not enabled', async () => { | |
| process.env.SEARCH_ENABLES_FETCH = 'true'; | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_model_search', 'hf_dataset_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.enabledToolIds).not.toContain('hf_doc_search'); | |
| expect(result.enabledToolIds).not.toContain('hf_doc_fetch'); | |
| expect(result.enabledToolIds).toEqual(normalizeBuiltInTools(['hf_model_search', 'hf_dataset_search'])); | |
| }); | |
| it('should not duplicate hf_doc_fetch if already enabled', async () => { | |
| process.env.SEARCH_ENABLES_FETCH = 'true'; | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_doc_search', 'hf_doc_fetch', 'hf_model_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.enabledToolIds).toEqual( | |
| normalizeBuiltInTools(['hf_doc_search', 'hf_doc_fetch', 'hf_model_search']) | |
| ); | |
| expect(result.enabledToolIds.filter((id) => id === 'hf_doc_fetch')).toHaveLength(1); | |
| }); | |
| it('should work with bouquet override', async () => { | |
| process.env.SEARCH_ENABLES_FETCH = 'true'; | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-bouquet': 'search' }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.BOUQUET_OVERRIDE); | |
| expect(result.enabledToolIds).toContain('hf_doc_search'); | |
| expect(result.enabledToolIds).toContain('hf_doc_fetch'); | |
| }); | |
| it('should work with mix mode', async () => { | |
| process.env.SEARCH_ENABLES_FETCH = 'true'; | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_model_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-mix': 'search' }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.MIX); | |
| expect(result.enabledToolIds).toContain('hf_doc_search'); | |
| expect(result.enabledToolIds).toContain('hf_doc_fetch'); | |
| expect(result.enabledToolIds).toContain(REPO_SEARCH_TOOL_ID); | |
| }); | |
| it('should work with fallback mode when all tools are enabled', async () => { | |
| process.env.SEARCH_ENABLES_FETCH = 'true'; | |
| const context: ToolSelectionContext = { | |
| headers: {}, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.FALLBACK); | |
| // In fallback mode, all tools are enabled, so both should already be there | |
| expect(result.enabledToolIds).toContain('hf_doc_search'); | |
| expect(result.enabledToolIds).toContain('hf_doc_fetch'); | |
| }); | |
| }); | |
| describe('Gradio endpoint handling', () => { | |
| it('should include gradio endpoints in bouquet override mode', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { | |
| 'x-mcp-bouquet': 'search', | |
| 'x-mcp-gradio': 'microsoft/Florence-2-large,meta-llama/Llama-2-7b-chat-hf', | |
| }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.BOUQUET_OVERRIDE); | |
| expect(result.enabledToolIds).toEqual(TOOL_ID_GROUPS.search); | |
| expect(result.reason).toBe('Bouquet override: search + 2 gradio endpoints'); | |
| expect(result.gradioSpaceTools).toBeDefined(); | |
| expect(result.gradioSpaceTools).toHaveLength(2); | |
| expect(result.gradioSpaceTools?.[0].name).toBe('microsoft/Florence-2-large'); | |
| expect(result.gradioSpaceTools?.[1].name).toBe('meta-llama/Llama-2-7b-chat-hf'); | |
| }); | |
| it('should include gradio endpoints in mix mode', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_whoami'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { | |
| 'x-mcp-mix': 'hf_api', | |
| 'x-mcp-gradio': 'foo/bar', | |
| }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.MIX); | |
| expect(result.reason).toBe('User settings + mix(hf_api) + 1 gradio endpoints'); | |
| expect(result.gradioSpaceTools).toBeDefined(); | |
| expect(result.gradioSpaceTools).toHaveLength(1); | |
| expect(result.gradioSpaceTools?.[0].name).toBe('foo/bar'); | |
| }); | |
| it('should include gradio endpoints in user settings mode', async () => { | |
| const userSettings: AppSettings = { | |
| builtInTools: ['hf_semantic_search'], | |
| spaceTools: [], | |
| }; | |
| const context: ToolSelectionContext = { | |
| headers: { | |
| 'x-mcp-gradio': 'test/space', | |
| }, | |
| userSettings, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.INTERNAL_API); | |
| expect(result.reason).toBe('Internal API user settings + 1 gradio endpoints'); | |
| expect(result.gradioSpaceTools).toBeDefined(); | |
| expect(result.gradioSpaceTools).toHaveLength(1); | |
| }); | |
| it('should include gradio endpoints in fallback mode', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { | |
| 'x-mcp-gradio': 'fallback/test', | |
| }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.FALLBACK); | |
| expect(result.reason).toBe('Fallback - no settings available + 1 gradio endpoints'); | |
| expect(result.gradioSpaceTools).toBeDefined(); | |
| expect(result.gradioSpaceTools).toHaveLength(1); | |
| }); | |
| it('should not include gradio endpoints when not specified', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { 'x-mcp-bouquet': 'search' }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.mode).toBe(ToolSelectionMode.BOUQUET_OVERRIDE); | |
| expect(result.reason).toBe('Bouquet override: search'); | |
| expect(result.gradioSpaceTools).toBeUndefined(); | |
| }); | |
| it('should handle multiple gradio endpoints with various formats', async () => { | |
| const context: ToolSelectionContext = { | |
| headers: { | |
| 'x-mcp-bouquet': 'hf_api', | |
| 'x-mcp-gradio': 'user/space-one,org/space-two', | |
| }, | |
| hfToken: 'test-token', | |
| }; | |
| const result = await strategy.selectTools(context); | |
| expect(result.gradioSpaceTools).toBeDefined(); | |
| expect(result.gradioSpaceTools).toHaveLength(2); | |
| expect(result.gradioSpaceTools?.map(s => s.name)).toEqual([ | |
| 'user/space-one', | |
| 'org/space-two', | |
| ]); | |
| }); | |
| }); | |
| }); | |
Xet Storage Details
- Size:
- 27.1 kB
- Xet hash:
- d0617455ee7be1d005a04306e16300b3127124e61ea63cece33044a2ebe59536
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.