import { beforeEach, describe, expect, test, vi } from 'vitest'; const mockState = vi.hoisted(() => ({ runOutput: { logits: { data: new Float32Array() }, pred_boxes: { data: new Float32Array() }, }, })); vi.mock('onnxruntime-node', () => ({ InferenceSession: { create: vi.fn(async () => ({ run: vi.fn(async () => mockState.runOutput), })), }, Tensor: class Tensor { constructor( public type: string, public data: Float32Array, public dims: number[], ) {} }, })); vi.mock('fs/promises', () => ({ readFile: vi.fn(async (path: string) => { if (path === '/tmp/model-config.json') { return JSON.stringify({ id2label: { 0: 'text', 1: 'table', }, }); } if (path === '/tmp/model-preprocessor.json') { return JSON.stringify({ size: { width: 2, height: 2 }, rescale_factor: 1 / 255, image_mean: [0, 0, 0], image_std: [1, 1, 1], }); } throw new Error(`unexpected readFile path: ${path}`); }), })); vi.mock('@napi-rs/canvas', () => { const createCanvas = (width: number, height: number) => ({ getContext: () => ({ fillStyle: '#ffffff', fillRect: () => {}, drawImage: () => {}, imageSmoothingEnabled: true, getImageData: () => ({ data: new Uint8ClampedArray(width * height * 4).fill(255), }), }), }); const loadImage = vi.fn(async () => ({ width: 2, height: 2 })); return { createCanvas, loadImage, default: { createCanvas, loadImage, }, }; }); vi.mock('../../src/pdf/model', () => ({ ensureModel: vi.fn(async () => '/tmp/model.onnx'), MODEL_CONFIG_PATH: '/tmp/model-config.json', MODEL_PREPROCESSOR_PATH: '/tmp/model-preprocessor.json', })); vi.mock('../../src/config/cpu-budget', () => ({ getOnnxThreadsPerJob: vi.fn(() => 1), })); describe('runLayoutModel', () => { beforeEach(() => { vi.resetModules(); mockState.runOutput = { logits: { data: new Float32Array() }, pred_boxes: { data: new Float32Array() }, }; }); test('keeps one winner per query instead of dropping later queries behind duplicate class rows', async () => { mockState.runOutput = { logits: { data: new Float32Array([ 3, 4, 2.5, 0.1, ]), }, pred_boxes: { data: new Float32Array([ 0.25, 0.25, 0.3, 0.3, 0.75, 0.75, 0.3, 0.3, ]), }, }; const { runLayoutModel } = await import('../../src/pdf/runLayoutModel'); const regions = await runLayoutModel({ pageWidth: 100, pageHeight: 100, textItems: [{} as never], pageImage: Buffer.from([1]), }); expect(regions).toHaveLength(2); expect(regions[0]?.label).toBe('text'); expect(regions[0]?.confidence).toBeCloseTo(0.9168273, 6); expect(regions[0]?.bbox).toEqual([ expect.closeTo(60, 5), expect.closeTo(60, 5), expect.closeTo(90, 5), expect.closeTo(90, 5), ]); expect(regions[1]?.label).toBe('table'); expect(regions[1]?.confidence).toBeCloseTo(0.73105858, 6); expect(regions[1]?.bbox).toEqual([ expect.closeTo(10, 5), expect.closeTo(10, 5), expect.closeTo(40, 5), expect.closeTo(40, 5), ]); }); test('drops unlabeled query winners and keeps only labeled regions', async () => { mockState.runOutput = { logits: { data: new Float32Array([ 0.1, 0.2, 5, 4, 0.1, 0.1, ]), }, pred_boxes: { data: new Float32Array([ 0.25, 0.25, 0.3, 0.3, 0.75, 0.75, 0.3, 0.3, ]), }, }; const { runLayoutModel } = await import('../../src/pdf/runLayoutModel'); const regions = await runLayoutModel({ pageWidth: 100, pageHeight: 100, textItems: [{} as never], pageImage: Buffer.from([1]), }); expect(regions).toHaveLength(1); expect(regions[0]?.label).toBe('text'); expect(regions[0]?.confidence).toBeCloseTo(0.96109135, 5); expect(regions[0]?.bbox).toEqual([ expect.closeTo(60, 5), expect.closeTo(60, 5), expect.closeTo(90, 5), expect.closeTo(90, 5), ]); }); });