This commit is contained in:
134
packages/agent/tests/connector-search.test.ts
Normal file
134
packages/agent/tests/connector-search.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createConnectorSearchTools } from '../src/connector-search.js';
|
||||
|
||||
// Isolated smoke test for the find_connector agent tool. Catches regressions
|
||||
// in ranking, synonym expansion, category filtering, and category listing —
|
||||
// the four guarantees the agent relies on when routing natural-language
|
||||
// integration requests into the 148-entry MCP catalog.
|
||||
|
||||
interface SearchPayload {
|
||||
query: string;
|
||||
catalogSize: number;
|
||||
matchCount: number;
|
||||
matches: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
description: string;
|
||||
capabilities: string[];
|
||||
installCmd: string;
|
||||
url: string;
|
||||
official: boolean;
|
||||
matchScore: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface CategoriesPayload {
|
||||
totalServers: number;
|
||||
officialServers: number;
|
||||
categoryCount: number;
|
||||
categories: Array<{ category: string; count: number }>;
|
||||
}
|
||||
|
||||
async function search(query: string, extras: Record<string, unknown> = {}): Promise<SearchPayload> {
|
||||
const [findConnector] = createConnectorSearchTools();
|
||||
const raw = await findConnector.execute({ query, ...extras });
|
||||
return JSON.parse(raw) as SearchPayload;
|
||||
}
|
||||
|
||||
describe('find_connector', () => {
|
||||
it('exposes both tools with offlineCapable = true', () => {
|
||||
const tools = createConnectorSearchTools();
|
||||
expect(tools.map(t => t.name)).toEqual([
|
||||
'find_connector',
|
||||
'list_connector_categories',
|
||||
]);
|
||||
for (const tool of tools) {
|
||||
expect(tool.offlineCapable).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('ranks exact-name matches above thematic matches', async () => {
|
||||
const result = await search('postgres');
|
||||
expect(result.matchCount).toBeGreaterThan(0);
|
||||
const top = result.matches[0];
|
||||
const topId = top.id.toLowerCase();
|
||||
const topName = top.name.toLowerCase();
|
||||
expect(topId.includes('postgres') || topName.includes('postgres')).toBe(true);
|
||||
});
|
||||
|
||||
it('handles multi-word natural-language queries (project management)', async () => {
|
||||
const result = await search('project management tool');
|
||||
expect(result.matchCount).toBeGreaterThan(0);
|
||||
const topIds = result.matches.slice(0, 5).map(m => m.id.toLowerCase());
|
||||
// At least one of the top 5 should be a well-known PM tool.
|
||||
const knownPmTools = ['linear', 'jira', 'asana', 'clickup', 'monday', 'todoist', 'notion'];
|
||||
const hit = topIds.some(id => knownPmTools.some(tool => id.includes(tool)));
|
||||
expect(hit).toBe(true);
|
||||
});
|
||||
|
||||
it('resolves chat synonyms to messaging platforms', async () => {
|
||||
const result = await search('team chat');
|
||||
expect(result.matchCount).toBeGreaterThan(0);
|
||||
const topIds = result.matches.slice(0, 5).map(m => m.id.toLowerCase());
|
||||
const knownChatTools = ['slack', 'discord', 'teams', 'telegram', 'mattermost'];
|
||||
const hit = topIds.some(id => knownChatTools.some(tool => id.includes(tool)));
|
||||
expect(hit).toBe(true);
|
||||
});
|
||||
|
||||
it('respects the limit cap (30) and default (10)', async () => {
|
||||
const defaultResult = await search('database');
|
||||
expect(defaultResult.matches.length).toBeLessThanOrEqual(10);
|
||||
|
||||
const cappedResult = await search('database', { limit: 500 });
|
||||
expect(cappedResult.matches.length).toBeLessThanOrEqual(30);
|
||||
});
|
||||
|
||||
it('filters by category when provided', async () => {
|
||||
// Use an unfiltered search to discover a category that definitely
|
||||
// contains matches for the query, then re-run with that category filter.
|
||||
// This avoids flakiness if a random top-populated category has zero
|
||||
// string hits against our test query.
|
||||
const unfiltered = await search('postgres');
|
||||
expect(unfiltered.matchCount).toBeGreaterThan(0);
|
||||
const pickCategory = unfiltered.matches[0].category;
|
||||
|
||||
const filtered = await search('postgres', { category: pickCategory });
|
||||
expect(filtered.matchCount).toBeGreaterThan(0);
|
||||
expect(filtered.matches).toBeDefined();
|
||||
for (const match of filtered.matches) {
|
||||
expect(match.category).toBe(pickCategory);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns a helpful hint when no matches are found', async () => {
|
||||
const result = await search('zzzzzzzzzzzzzzzzzzzz') as SearchPayload & { hint?: string };
|
||||
expect(result.matchCount).toBe(0);
|
||||
expect(result.hint).toBeTruthy();
|
||||
});
|
||||
|
||||
it('rejects empty queries with a clear error', async () => {
|
||||
const [findConnector] = createConnectorSearchTools();
|
||||
const raw = await findConnector.execute({ query: ' ' });
|
||||
const parsed = JSON.parse(raw) as { error?: string };
|
||||
expect(parsed.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('list_connector_categories', () => {
|
||||
it('returns total server count, official count, and sorted categories', async () => {
|
||||
const [, listCats] = createConnectorSearchTools();
|
||||
const payload = JSON.parse(await listCats.execute({})) as CategoriesPayload;
|
||||
|
||||
expect(payload.totalServers).toBeGreaterThan(100);
|
||||
expect(payload.officialServers).toBeGreaterThanOrEqual(0);
|
||||
expect(payload.categoryCount).toBe(payload.categories.length);
|
||||
|
||||
// Categories should be sorted by count descending.
|
||||
for (let i = 1; i < payload.categories.length; i++) {
|
||||
expect(payload.categories[i - 1].count).toBeGreaterThanOrEqual(
|
||||
payload.categories[i].count,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user