Files
memind/mindsearch.test.mjs
T
john 53ed38c250
Memind CI / Test, build, and release guards (pull_request) Successful in 4m4s
feat: prepare deep search production runtime
2026-07-24 09:53:06 +08:00

233 lines
10 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('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');
});