pediatric-ai-scribe-v3/test/mcp-collection-fanout.test.js
Daniel f762e91af7 feat: the assistant searches every collection the library declares, fused by rank — one collection stays one call
The search service can hold several Milvus collections, each with its own
embedder, but a search names one or gets the default, so a second collection
was invisible to the assistant. The client now learns the list off the query
path (at warm-up and on the session timer), and only when the service lists
more than the default does a search fan out — one call per collection in
parallel, fused by reciprocal rank so scores from different embedders are
never compared. With one collection, today's case, the request is byte-for-
byte what it was and no listing call is made while anyone waits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
2026-09-14 13:57:56 +02:00

60 lines
3.6 KiB
JavaScript

// 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\(\);/);
});