pediatric-ai-scribe-v3/test/extension-transfer.test.js

153 lines
7.3 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const transfer = require('../src/utils/extensionTransfer');
test('extension export payload removes ids and keeps portable fields', () => {
const payload = transfer.exportPayload([
{ id: 10, location: ' ED ', name: ' Charge ', number: ' 1234 ', type: 'pager', notes: ' Nights ' },
{ id: 11, location: '', name: 'Invalid', number: '5555', type: 'extension' }
]);
assert.equal(payload.format, 'pedscribe-phone-directory');
assert.equal(payload.version, 1);
assert.deepEqual(payload.items, [
{ location: 'ED', name: 'Charge', number: '1234', type: 'pager', notes: 'Nights' }
]);
});
test('extension import normalizes, clips, and deduplicates entries', () => {
const items = transfer.importItemsFromBody({
items: [
{ location: 'ED', name: 'Charge', number: '1234', type: 'pager', notes: 'Nights' },
{ location: ' ed ', name: ' charge ', number: ' 1234 ', type: 'pager', notes: ' nights ' },
{ location: 'Ward', name: 'Desk', number: '9999', type: 'unknown', notes: null },
{ location: '', name: 'Bad', number: '1111' }
]
});
assert.deepEqual(items, [
{ location: 'ED', name: 'Charge', number: '1234', type: 'pager', notes: 'Nights' },
{ location: 'Ward', name: 'Desk', number: '9999', type: 'extension', notes: '' }
]);
});
test('extension transfer zip round-trips JSON payload', () => {
const payload = transfer.exportPayload([
{ location: 'NICU', name: 'Fellow', number: '7777', type: 'pager', notes: '' }
]);
const zip = transfer.createJsonZip('pedscribe-extensions.json', payload);
const parsed = transfer.parseImportBuffer(zip);
assert.deepEqual(parsed.items, payload.items);
});
test('extension import analysis flags exact and loose duplicates', () => {
const items = transfer.importItemsFromBody({
items: [
{ location: 'ED', name: 'Charge', number: '1234', type: 'pager', notes: 'Nights' },
{ location: 'ED', name: 'Desk', number: '1234', type: 'extension', notes: '' },
{ location: 'Ward', name: 'Clerk', number: '1234', type: 'pager', notes: '' },
{ location: 'NICU', name: 'Fellow', number: '7777', type: 'pager', notes: '' }
]
});
const analysis = transfer.analyzeImport(items, [
{ id: 1, location: ' ed ', name: ' charge ', number: '1234', type: 'pager', notes: 'nights', trashed_at: null },
{ id: 2, location: 'NICU', name: 'Fellow', number: '7777', type: 'pager', notes: '', trashed_at: '2026-05-08T00:00:00Z' }
]);
assert.equal(analysis.summary.exactActive, 1);
assert.equal(analysis.summary.exactTrashed, 1);
assert.equal(analysis.summary.possible, 2);
assert.deepEqual(analysis.entries.map(e => e.status), [
'exact_active',
'possible_duplicate',
'possible_duplicate',
'exact_trashed'
]);
});
test('extension import analysis flags loose duplicates within the same import file', () => {
const items = transfer.importItemsFromBody({
items: [
{ location: 'ED', name: 'Desk', number: '1234', type: 'extension', notes: '' },
{ location: 'ED', name: 'Nurse', number: '1234', type: 'extension', notes: 'alternate label' }
]
});
const analysis = transfer.analyzeImport(items, []);
assert.equal(analysis.summary.new, 1);
assert.equal(analysis.summary.possible, 1);
assert.deepEqual(analysis.entries.map(e => e.status), ['new', 'possible_duplicate']);
});
function deflatedZip(payload, flags = 0) {
const zlib = require('node:zlib');
const stored = transfer.createJsonZip('directory.json', payload);
const start = 30 + stored.readUInt16LE(26);
const end = start + stored.readUInt32LE(18);
const compressed = zlib.deflateRawSync(stored.subarray(start, end));
const header = Buffer.from(stored.subarray(0, start));
const trailer = Buffer.from(stored.subarray(end));
header.writeUInt16LE(flags, 6);
header.writeUInt16LE(8, 8);
header.writeUInt32LE(compressed.length, 18);
trailer.writeUInt16LE(flags, 8);
trailer.writeUInt16LE(8, 10);
trailer.writeUInt32LE(compressed.length, 20);
trailer.writeUInt32LE(start + compressed.length, trailer.length - 6);
return Buffer.concat([header, compressed, trailer]);
}
test('actual parser accepts plain/stored/deflated exports and 1000 maximal multibyte entries below 4 MiB', () => {
const payload = transfer.exportPayload([{ location: '病棟', name: 'Synthetic', number: '123', notes: '合成' }]);
for (const buf of [Buffer.from(JSON.stringify(payload)), transfer.createJsonZip('directory.json', payload), deflatedZip(payload)]) {
assert.deepEqual(transfer.parseImportBuffer(buf), payload);
}
const large = transfer.exportPayload(Array.from({ length: 1000 }, (_, i) => ({
location: '病'.repeat(120), name: '名'.repeat(120), number: String(i).padStart(40, '0'), notes: '語'.repeat(500)
})));
assert.ok(Buffer.byteLength(JSON.stringify(large, null, 2)) < 4 * 1024 * 1024);
assert.equal(transfer.importItemsFromBody(transfer.parseImportBuffer(deflatedZip(large))).length, 1000);
});
test('actual parser bounds plain and inflated payloads even when ZIP header lies about output size', () => {
const over = 'x'.repeat(4 * 1024 * 1024 + 1);
assert.throws(() => transfer.parseImportBuffer(Buffer.from(JSON.stringify(over))), /exceeds 4 MiB/);
const bomb = deflatedZip(over);
assert.ok(bomb.length < 1024 * 1024);
assert.throws(() => transfer.parseImportBuffer(bomb), /exceeds 4 MiB/);
bomb.writeUInt32LE(1, 22); // advertised size must never control the actual inflate bound
assert.throws(() => transfer.parseImportBuffer(bomb), /larger|length|size|buffer/i);
});
test('actual parser rejects truncated, lying, encrypted, descriptor, ZIP64 and unsupported headers', () => {
const zip = deflatedZip({ items: [] });
for (const length of [4, 8, 29, 30, 40]) assert.throws(() => transfer.parseImportBuffer(zip.subarray(0, length)));
for (const [offset, value, width] of [[4, 45, 2], [6, 1, 2], [6, 8, 2], [8, 99, 2], [18, 0xffffffff, 4], [18, zip.readUInt32LE(18) + 1, 4], [22, 1, 4], [26, 0xffff, 2], [28, 0xffff, 2], [14, 0, 4]]) {
const bad = Buffer.from(zip);
if (width === 2) bad.writeUInt16LE(value, offset); else bad.writeUInt32LE(value, offset);
assert.throws(() => transfer.parseImportBuffer(bad), 'offset ' + offset);
}
const stored = transfer.createJsonZip('directory.json', { items: [] });
stored.writeUInt32LE(1, 22);
assert.throws(() => transfer.parseImportBuffer(stored), /Invalid zip payload/);
});
test('Deflate compression-option flags and UTF-8 are valid only for the appropriate methods', () => {
const payload = { items: [], name: '病棟' };
for (const flags of [0x2, 0x4, 0x6, 0x800, 0x802, 0x804, 0x806]) {
assert.deepEqual(transfer.parseImportBuffer(deflatedZip(payload, flags)), payload);
}
const stored = transfer.createJsonZip('directory.json', payload);
const central = 30 + stored.readUInt16LE(26) + stored.readUInt32LE(18);
for (const flags of [0x800, 0x2, 0x4, 0x6]) {
stored.writeUInt16LE(flags, 6);
stored.writeUInt16LE(flags, central + 8);
if (flags === 0x800) assert.deepEqual(transfer.parseImportBuffer(stored), payload);
else assert.throws(() => transfer.parseImportBuffer(stored), /Unsupported zip flags/);
}
for (const flags of [0x807, 0x80e, 0x840]) {
assert.throws(() => transfer.parseImportBuffer(deflatedZip(payload, flags)), /Unsupported zip flags/);
}
});