255 lines
11 KiB
JavaScript
255 lines
11 KiB
JavaScript
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('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');
|
|
});
|