Files
waggle-os/packages/agent/tests/connector-search.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

135 lines
5.1 KiB
TypeScript

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,
);
}
});
});