← back to AbramsOS

tests/extractor-llm.test.js

83 lines

// Tier-2 LLM enrichment unit tests. Mocks lib/ollama with require-cache override.

const test = require('node:test');
const assert = require('node:assert');

// Mock ollama with fresh module-cache injection. Wipe any pre-loaded extractor
// so it picks up our mock via require().
const fakeOllama = {
  generate: async () => '',
  generateJson: async (_prompt) => ({
    merchant: 'Best Buy',
    order_number: 'BBY01-1234567',
    total: 199.99,
    currency: 'USD',
    items: [{ description: 'Wireless headphones', quantity: 1, unit_price: 199.99 }],
  }),
  reachable: async () => true,
  DEFAULT_BASE: 'http://stub',
  DEFAULT_MODEL: 'stub:tiny',
};

const ollamaPath = require.resolve('../lib/ollama');
const extractorPath = require.resolve('../lib/receipt-extractor');

function installMock(mock) {
  delete require.cache[extractorPath];
  require.cache[ollamaPath] = { id: ollamaPath, filename: ollamaPath, loaded: true, exports: mock };
}

installMock(fakeOllama);
const extractor = require('../lib/receipt-extractor');

test('enrichWithLlm fills missing fields', async () => {
  const summary = {
    headers: { subject: 'Thank you for your order', from: '"Some Store" <orders@example.com>', date: new Date().toISOString() },
    body: { text: 'Thanks for your order. Your shipment will arrive Tuesday.', html: '' },
  };
  const t1 = extractor.extract(summary);
  // Heuristic should find SOMETHING low-confidence (subject hints + body lacks total/order number)
  assert.ok(t1, 'tier-1 should produce a draft');
  assert.ok(t1.confidence < extractor.LLM_THRESHOLD, `tier-1 confidence (${t1?.confidence}) should be below threshold (${extractor.LLM_THRESHOLD})`);

  const t2 = await extractor.enrichWithLlm(summary, t1);
  assert.strictEqual(t2.source, 'heuristic+llm');
  // Heuristic-trust rule: merchant from From: header is kept even though LLM offers 'Best Buy'
  assert.strictEqual(t2.merchant, t1.merchant, 'heuristic merchant should be preserved');
  // LLM fills the fields heuristic missed
  assert.strictEqual(t2.orderNumber, 'BBY01-1234567');
  assert.strictEqual(t2.total, 199.99);
  assert.strictEqual(t2.currency, 'USD');
  assert.strictEqual(t2.items.length, 1);
  assert.ok(t2.confidence > t1.confidence, `confidence should rise (was ${t1.confidence}, now ${t2.confidence})`);
});

test('enrichWithLlm respects high-confidence heuristic (skips LLM)', async () => {
  const summary = {
    headers: { subject: 'Your Amazon.com order #112-7654321-7654321', from: '"Amazon.com" <auto-confirm@amazon.com>', date: new Date().toISOString() },
    body: { text: 'Order Total: $99.99. Thanks for your order.', html: '' },
  };
  const t1 = extractor.extract(summary);
  assert.ok(t1.confidence >= extractor.LLM_THRESHOLD, 'tier-1 should already be high-conf');
  const t2 = await extractor.enrichWithLlm(summary, t1);
  assert.strictEqual(t2.source, 'heuristic', 'LLM should be skipped');
  assert.strictEqual(t2, t1, 'result is the same object');
});

test('enrichWithLlm tolerates Ollama failure gracefully', async () => {
  const failOllama = { ...fakeOllama, generateJson: async () => { throw new Error('mac1 unreachable'); } };
  installMock(failOllama);
  const extractor2 = require('../lib/receipt-extractor');
  const summary = {
    headers: { subject: 'Thank you for your order', from: '"Store" <ship@example.com>', date: new Date().toISOString() },
    body: { text: 'Thanks for your order. Your shipment will arrive tomorrow.', html: '' },
  };
  const t1 = extractor2.extract(summary);
  assert.ok(t1, 'tier-1 should produce a draft for this fixture');
  const t2 = await extractor2.enrichWithLlm(summary, t1);
  assert.ok(t2.llmError, 'should surface the LLM error');
  assert.strictEqual(t2.confidence, t1.confidence, 'confidence unchanged on failure');
  // Restore for any later tests
  installMock(fakeOllama);
});