// A second search collection is searched alongside the first and fused by // rank; with one collection the search is the single call it always was. const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const fanout = require('../src/utils/collectionFanout'); const src = fs.readFileSync(path.join(__dirname, '..', 'src/utils/clinicalMcpClient.js'), 'utf8'); function listing(names, defaultName) { return { structuredContent: { collections: names.map(n => ({ name: n, default: n === defaultName, model: 'm' })) } }; } test('the default collection alone means nothing extra to search', () => { assert.deepEqual(fanout.extraCollectionNames(listing(['mcp_bge_m3_1024'], 'mcp_bge_m3_1024')), []); assert.deepEqual(fanout.extraCollectionNames({}), []); assert.deepEqual(fanout.extraCollectionNames(null), []); }); test('collections beyond the default are named, from either envelope', () => { assert.deepEqual(fanout.extraCollectionNames(listing(['a', 'b', 'c'], 'a')), ['b', 'c']); const textEnvelope = { content: [{ type: 'text', text: JSON.stringify({ collections: [{ name: 'a', default: true }, { name: 'peds' }] }) }] }; assert.deepEqual(fanout.extraCollectionNames(textEnvelope), ['peds']); }); test('ranked lists from two collections fuse by reciprocal rank and keep their origin', () => { const first = { structuredContent: { results: [{ id: 'x', score: 0.9 }, { id: 'y', score: 0.8 }], verified_chunk_count: 2 } }; const second = { structuredContent: { results: [{ id: 'z', score: 0.1 }, { id: 'y', score: 0.05 }], verified_chunk_count: 2, dropped_document_count: 1 } }; const merged = fanout.mergeCollectionResults([{ collection: null, response: first }, { collection: 'peds', response: second }], 10); // y appears in both lists (ranks 2 and 2) and outranks either list's first. assert.equal(merged.results[0].id, 'y'); assert.deepEqual(merged.results.map(r => r.id).sort(), ['x', 'y', 'z']); assert.equal(merged.results.find(r => r.id === 'z').collection, 'peds'); assert.equal(merged.results.find(r => r.id === 'x').collection, null); assert.equal(merged.verified_chunk_count, 4); assert.equal(merged.dropped_document_count, 1); assert.deepEqual(merged.collections, [null, 'peds']); }); test('the fused list respects the limit and a failed collection is simply absent', () => { const one = { structuredContent: { results: [{ id: 1 }, { id: 2 }, { id: 3 }] } }; const merged = fanout.mergeCollectionResults([{ collection: null, response: one }], 2); assert.equal(merged.results.length, 2); assert.deepEqual(merged.collections, [null]); }); test('the search itself stays one call when no extra collection is known', () => { // The single-call path is the first thing semanticSearch does, before any // fan-out machinery, and it names no collection. const body = src.slice(src.indexOf('async function semanticSearch'), src.indexOf('// The collections the service will search')); assert.match(body, /if \(extra\.length === 0\) return callMcpTool\(SEARCH_TOOL_NAME, searchArgs\(query, opts\)\)/); assert.match(src, /if \(collection\) args\.collection = collection;/); }); test('the collection list is learned off the query path, never inside a search', () => { const search = src.slice(src.indexOf('async function semanticSearch'), src.indexOf('// The collections the service will search')); assert.doesNotMatch(search, /refreshCollections|clinical_list_collections/); const warm = src.slice(src.indexOf('function warmMcpSession'), src.indexOf('function busyError')); assert.match(warm, /if \(collectionsStale\(\)\) refreshCollections\(\);/); });