pediatric-ai-scribe-v3/test/clinical-table-preservation.test.js

78 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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('![](/tmp/pdf-images/a.png) <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'));
});