import test from 'node:test'; import assert from 'node:assert/strict'; import { buildCitations, isSafeHttpUrl, validateSearchRequest } from './search-capability.mjs'; import { MINDSEARCH_DEFAULT_CONFIG, normalizeMindSearchConfig, probeMindSearchService, resolveMindSearchService, } from './mindsearch-config.mjs'; import { buildAgentExtensionPolicy, DEFAULT_USER_CAPABILITIES, } from './capabilities.mjs'; import { cancelResearchTask, getResearchTask, searchGithubCode, searchGithubGateway, searchResearchService, searchSearxng, startResearchTask, } from './mindsearch-providers.mjs'; test('MindSearch defaults to disabled and normalizes unsafe modes', () => { assert.equal(MINDSEARCH_DEFAULT_CONFIG.enabled, false); const config = normalizeMindSearchConfig({ enabled: true, mode: 'invalid', providers: { github: true } }); assert.equal(config.enabled, false); assert.equal(config.mode, 'off'); assert.deepEqual(config.providers, { searxng: false, github: true, reader: false }); assert.equal(config.services.find((service) => service.id === 'github').enabled, true); assert.deepEqual(config.routes, { web: 'searxng', news: 'searxng', code: 'github', read: 'reader', research: 'deep-search' }); assert.equal(config.services.find((service) => service.id === 'deep-search').enabled, false); }); test('MindSearch service registry accepts research orchestrators and rejects unsafe endpoints', () => { const config = normalizeMindSearchConfig({ enabled: true, mode: 'assist', services: [ { id: 'research-main', name: 'Research', kind: 'orchestrator', adapter: 'research-http', endpoint: 'http://127.0.0.1:20100', healthPath: '/health', capabilities: ['search.web', 'research.execute', 'research.execute', 'INVALID CAPABILITY'], enabled: true, timeoutMs: 500000, llmProviderKeyId: 'provider-key-1', llmModel: 'research-model', }, { id: 'metadata', adapter: 'research-http', endpoint: 'http://169.254.169.254/latest', enabled: true }, ], routes: { research: 'research-main', web: 'research-main' }, }); assert.equal(config.services.some((service) => service.id === 'metadata'), false); assert.deepEqual(config.services.find((service) => service.id === 'research-main').capabilities, ['search.web', 'research.execute']); assert.equal(config.services.find((service) => service.id === 'research-main').timeoutMs, 120000); assert.equal(config.services.find((service) => service.id === 'research-main').llmProviderKeyId, 'provider-key-1'); assert.equal(config.services.find((service) => service.id === 'research-main').llmModel, 'research-model'); assert.equal(resolveMindSearchService(config, 'research').id, 'research-main'); }); test('MindSearch service probe supports loopback SearXNG without exposing arbitrary response data', async () => { let requested; const result = await probeMindSearchService({ id: 'searxng-local', name: 'Local', kind: 'provider', adapter: 'searxng', endpoint: 'http://127.0.0.1:8080/search', enabled: true, }, { fetchImpl: async (url) => { requested = String(url); return { ok: true, status: 200, json: async () => ({ results: [{ title: 'A' }] }) }; }, }); assert.match(requested, /q=OpenAI/); assert.equal(result.ok, true); assert.equal(result.resultCount, 1); }); test('search request validation and citations are deterministic', () => { assert.equal(validateSearchRequest({ query: '' }).code, 'INVALID_REQUEST'); const checked = validateSearchRequest({ query: ' Goose ', limit: 999 }); assert.deepEqual(checked.value, { query: 'Goose', type: 'web', limit: 20 }); assert.deepEqual(buildCitations([{ title: 'A', url: 'https://example.com', source: 'test' }]), [{ id: '[1]', title: 'A', url: 'https://example.com', source: 'test' }]); assert.equal(isSafeHttpUrl('http://127.0.0.1:8081/private'), false); assert.equal(isSafeHttpUrl('https://example.com/a'), true); }); test('MindSearch never changes legacy web extension and is gated by capability/config', () => { const base = { ...DEFAULT_USER_CAPABILITIES, web: true, search_external: false }; let policy = buildAgentExtensionPolicy(base, { mindSearchConfig: { enabled: true, mode: 'assist', providers: {} } }); assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'web')); assert.equal(policy.extensionOverrides.some((ext) => ext.name === 'tkmind-search'), false); policy = buildAgentExtensionPolicy({ ...base, search_external: true }, { mindSearchConfig: { enabled: true, mode: 'assist', providers: {} }, policies: { network_egress: 'deny' } }); assert.equal(policy.extensionOverrides.some((ext) => ext.name === 'tkmind-search'), false); policy = buildAgentExtensionPolicy({ ...base, search_external: true }, { mindSearchConfig: { enabled: true, mode: 'assist', providers: {} } }); assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'tkmind-search')); policy = buildAgentExtensionPolicy({ ...base, search_external: true }, { userId: 'user-123', mindSearchConfig: { enabled: true, mode: 'assist', providers: {}, settings: { searxngEndpoint: 'http://127.0.0.1:20080/search' }, services: [ { id: 'searxng', enabled: true, adapter: 'searxng', endpoint: 'http://127.0.0.1:20080/search', }, { id: 'deep-search', enabled: true, adapter: 'research-http', endpoint: 'http://127.0.0.1:20100', }, ], routes: { research: 'deep-search' }, }, }); const extension = policy.extensionOverrides.find((ext) => ext.name === 'tkmind-search'); assert.equal(extension.envs.TKMIND_SEARCH_USER_ID, 'user-123'); assert.equal( extension.envs.TKMIND_SEARCH_SEARXNG_URL, 'http://host.docker.internal:20080/search', ); assert.equal( extension.envs.TKMIND_SEARCH_GITHUB_GATEWAY_URL, 'http://host.docker.internal:20100/', ); assert.equal(extension.envs.GITHUB_TOKEN, undefined); const mcpServices = JSON.parse(extension.envs.TKMIND_SEARCH_SERVICES_JSON); assert.equal( mcpServices.find((service) => service.id === 'deep-search').endpoint, 'http://host.docker.internal:20100/', ); assert.ok(extension.available_tools.includes('tkmind_research_status')); assert.ok(extension.available_tools.includes('tkmind_research_cancel')); }); test('MindSearch uses the container-visible bundled MCP directory', () => { const policy = buildAgentExtensionPolicy( { ...DEFAULT_USER_CAPABILITIES, search_external: true, }, { sandboxMcp: { containerized: true, serverPath: '/opt/portal/mindspace-sandbox-mcp.mjs', }, mindSearchConfig: { enabled: true, mode: 'assist', providers: {}, }, }, ); const extension = policy.extensionOverrides.find((ext) => ext.name === 'tkmind-search'); assert.equal(extension.args[0], '/opt/portal/tkmind-search-mcp.mjs'); }); test('MindSearch keeps container MCP path even when sandboxMcp is absent', () => { const previousContainerized = process.env.GOOSED_MCP_CONTAINERIZED; const previousServerPath = process.env.GOOSED_MCP_SERVER_PATH; process.env.GOOSED_MCP_CONTAINERIZED = '1'; process.env.GOOSED_MCP_SERVER_PATH = '/opt/portal/mindspace-sandbox-mcp.mjs'; try { const policy = buildAgentExtensionPolicy( { ...DEFAULT_USER_CAPABILITIES, search_external: true, }, { mindSearchConfig: { enabled: true, mode: 'assist', providers: {}, }, }, ); const extension = policy.extensionOverrides.find((ext) => ext.name === 'tkmind-search'); assert.equal(extension.args[0], '/opt/portal/tkmind-search-mcp.mjs'); } finally { if (previousContainerized === undefined) delete process.env.GOOSED_MCP_CONTAINERIZED; else process.env.GOOSED_MCP_CONTAINERIZED = previousContainerized; if (previousServerPath === undefined) delete process.env.GOOSED_MCP_SERVER_PATH; else process.env.GOOSED_MCP_SERVER_PATH = previousServerPath; } }); test('SearXNG adapter normalizes provider results without requiring a live network', async () => { const result = await searchSearxng('goose', { endpoint: 'http://search.local', fetchImpl: async () => ({ ok: true, json: async () => ({ results: [{ title: 'Goose', url: 'https://example.com', content: 'snippet' }] }) }) }); assert.deepEqual(result[0], { title: 'Goose', url: 'https://example.com', snippet: 'snippet', source: 'searxng', rank: 1 }); }); test('GitHub code adapter sends bounded queries and normalizes results', async () => { let requested; const result = await searchGithubCode('repo:openai goose', { limit: 50, fetchImpl: async (url, options) => { requested = { url: String(url), options }; return { ok: true, json: async () => ({ items: [{ repository: { full_name: 'openai/goose', description: 'agent' }, path: 'src/goose.rs', html_url: 'https://github.com/openai/goose/blob/main/src/goose.rs' }] }) }; } }); assert.match(requested.url, /per_page=20/); assert.equal(requested.options.headers.accept, 'application/vnd.github+json'); assert.equal(result[0].source, 'github'); }); test('GitHub gateway keeps the provider token outside the MCP process', async () => { let requested; const result = await searchGithubGateway('repo:openai goose', { endpoint: 'http://deep-search.local:20100', secret: 'gateway-secret', fetchImpl: async (url, options) => { requested = { url: String(url), options }; return { ok: true, json: async () => ({ results: [{ title: 'openai/goose:src/goose.rs', url: 'https://github.com/openai/goose/blob/main/src/goose.rs', snippet: 'agent', }], }), }; }, }); assert.equal(requested.url, 'http://deep-search.local:20100/v1/providers/github/search'); assert.equal(requested.options.headers['x-secret-key'], 'gateway-secret'); assert.equal(requested.options.headers.authorization, undefined); assert.equal(result[0].source, 'github'); }); test('Research service adapter uses the stable HTTP contract', async () => { const requests = []; const fetchImpl = async (url, options) => { requests.push({ url: String(url), options }); if (String(url).endsWith('/v1/research')) { return { ok: true, json: async () => ({ task_id: 'task-1', status: 'running' }) }; } if (String(url).endsWith('/v1/research/task-1')) { return { ok: true, status: 200, json: async () => ({ id: 'task-1', status: options?.method === 'DELETE' ? 'cancelling' : 'researching', progress: 55, }), }; } return { ok: true, json: async () => ({ results: [{ title: 'Report', url: 'https://example.com/report', snippet: 'evidence' }] }) }; }; const results = await searchResearchService('AI Agent', { endpoint: 'http://research.local', limit: 5, fetchImpl }); const task = await startResearchTask('Analyze AI Agent', { endpoint: 'http://research.local', depth: 'deep', userId: 'user-1', llmProviderKeyId: 'provider-key-1', llmModel: 'research-model', secret: 'secret-1', fetchImpl, }); const status = await getResearchTask('task-1', { endpoint: 'http://research.local', fetchImpl }); const cancelled = await cancelResearchTask('task-1', { endpoint: 'http://research.local', fetchImpl }); assert.equal(results[0].source, 'research-service'); assert.deepEqual(task, { taskId: 'task-1', status: 'running', service: 'research-service' }); assert.equal(status.status, 'researching'); assert.equal(cancelled.status, 'cancelling'); assert.equal(JSON.parse(requests[0].options.body).query, 'AI Agent'); assert.deepEqual(JSON.parse(requests[1].options.body), { question: 'Analyze AI Agent', depth: 'deep', userId: 'user-1', llm_provider_key_id: 'provider-key-1', llm_model: 'research-model', }); assert.equal(requests[1].options.headers['x-user-id'], 'user-1'); assert.equal(requests[1].options.headers['x-secret-key'], 'secret-1'); assert.equal(requests[3].options.method, 'DELETE'); });