78 lines
4.6 KiB
JavaScript
78 lines
4.6 KiB
JavaScript
const { test } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
const vm = require('node:vm');
|
||
const retrieval = require('../src/utils/clinicalRetrieval');
|
||
|
||
// Run the actual route formatter without booting its DB/auth/provider imports.
|
||
const route = fs.readFileSync(path.join(__dirname, '../src/routes/clinicalAssistant.js'), 'utf8');
|
||
const formatter = route.slice(route.indexOf('function formatSourcesForPrompt('), route.indexOf('function sanitizeSourcesForClient('));
|
||
const sandbox = { cleanSourceExcerpt: retrieval.cleanSourceExcerpt };
|
||
vm.runInNewContext(formatter, sandbox);
|
||
const formatSourcesForPrompt = sandbox.formatSourcesForPrompt;
|
||
|
||
const table = 'Table 1. Dose (mg/kg)\n\n| Drug | Dose (mg/kg) |\n|---|---|\n| α\\|β | 2 mg/kg |\n| Other | 5 mg/kg |\n\nNote: Test fixture only.';
|
||
|
||
test('actual normalize and prompt formatter preserve hit tables ahead of long surroundings', () => {
|
||
const sources = retrieval.normalizeMcpSearchResponse({ results: [{
|
||
id: 42, title: 'Test.pdf', page_number: 7, chunk_index: 3,
|
||
before_context: 'Unrelated background '.repeat(100), excerpt: table,
|
||
after_context: 'Later background '.repeat(100)
|
||
}] });
|
||
assert.equal(sources.length, 1);
|
||
assert.ok(sources[0].excerpt.startsWith('Table 1.'));
|
||
assert.ok(sources[0].excerpt.length <= 1800);
|
||
const prompt = formatSourcesForPrompt(sources);
|
||
assert.match(prompt, /^\[1\] Test, page 7\n/);
|
||
assert.ok(prompt.includes('| Drug | Dose (mg/kg) |\n|---|---|\n| α\\|β | 2 mg/kg |'));
|
||
assert.ok(prompt.includes('Note: Test fixture only.'));
|
||
assert.ok(prompt.includes('[Content omitted]'));
|
||
assert.equal(retrieval.cleanSourceExcerpt(sources[0].excerpt), sources[0].excerpt);
|
||
assert.match(route, /cleanSourceExcerpt,[\s\S]*require\('\.\.\/utils\/clinicalRetrieval'\)/);
|
||
assert.doesNotMatch(route, /function cleanSourceExcerpt\(/);
|
||
});
|
||
|
||
test('large tables clip only whole rows, and oversized rows are explicitly omitted', () => {
|
||
const header = '| Drug | Dose |\n|---|---|\n';
|
||
const rows = Array.from({ length: 100 }, (_, i) => '| Drug ' + i + ' | ' + i + ' mg/kg |');
|
||
const excerpt = retrieval.clipSourceExcerpt(header + rows.join('\n'), 1800);
|
||
assert.ok(excerpt.length <= 1800);
|
||
assert.ok(excerpt.startsWith(header));
|
||
assert.ok(excerpt.endsWith('[Content omitted]'));
|
||
for (const row of excerpt.split('\n').slice(2).filter(line => line.startsWith('|'))) {
|
||
assert.ok(rows.includes(row), 'no partial clinical row: ' + row);
|
||
}
|
||
const withNotes = retrieval.clipSourceExcerpt('Table 2. Test\n\n' + header + rows.join('\n') + '\n\nNote: Values require adjustment.\n\n† Synthetic fixture.', 1800);
|
||
assert.ok(withNotes.length <= 1800);
|
||
assert.ok(withNotes.includes('Note: Values require adjustment.'));
|
||
assert.ok(withNotes.endsWith('† Synthetic fixture.'));
|
||
assert.ok(withNotes.includes('[Content omitted]'));
|
||
const oversized = retrieval.clipSourceExcerpt(header + '| ' + 'x'.repeat(2000) + ' | 2 mg |', 1800);
|
||
assert.equal(oversized, '[Content omitted]');
|
||
for (const n of [0, 16, 17, 18, 19, 30]) assert.ok(retrieval.clipSourceExcerpt(table, n).length <= n);
|
||
});
|
||
|
||
test('cleaning keeps safety/prose behavior and does not identify fenced pipes as tables', () => {
|
||
const clean = retrieval.cleanSourceExcerpt(' <script>bad()</script><b>Useful</b> /var/private/a.png\n normal prose<br>next');
|
||
assert.equal(clean, 'Useful normal prose next');
|
||
assert.doesNotMatch(clean, /script|tmp|private|bad|<b>/);
|
||
const code = retrieval.clipSourceExcerpt('```\n|A|B|\n|---|---|\n|1|2|\n```', 1800);
|
||
assert.doesNotMatch(code, /\|A\|B\|\n/);
|
||
assert.equal(retrieval.cleanSourceExcerpt('One\n two three'), 'One two three');
|
||
});
|
||
|
||
test('dedup retains document/page/chunk citation identity rather than merging unrelated tables', () => {
|
||
const sources = retrieval.normalizeMcpSearchResponse({ results: [
|
||
{ id: 1, title: 'Same', page_number: 2, chunk_index: 0, excerpt: table },
|
||
{ id: 2, title: 'Same', page_number: 2, chunk_index: 0, excerpt: table },
|
||
{ id: 1, title: 'Same', page_number: 2, chunk_index: 1, excerpt: table },
|
||
{ id: 1, title: 'Same', page_number: 3, chunk_index: 0, excerpt: table },
|
||
{ id: 1, title: 'Same', page_number: 2, chunk_index: 0, excerpt: table }
|
||
] });
|
||
const deduped = retrieval.dedupeSources(sources);
|
||
assert.equal(deduped.length, 4);
|
||
assert.deepEqual(deduped.map(s => [s.number, s.id, s.page, s.chunk_index]), [[1, 1, 2, 0], [2, 2, 2, 0], [3, 1, 2, 1], [4, 1, 3, 0]]);
|
||
assert.equal(deduped[0].excerpt, sources[0].excerpt);
|
||
assert.ok(formatSourcesForPrompt(deduped).includes('[4] Same, page 3'));
|
||
});
|