redroid-mrnumber/service/queue.test.ts
Natalie eb84d431f3 feat(service): inbound trigger service — POST /api/screening/requests + durable queue + worker
Stand up the HTTP surface Prospector calls (this app had no listening port). Bun
service: bearer-auth (MRNUMBER_SERVICE_TOKEN, constant-time) POST /api/screening/requests
{phone, ref} → 202 {accepted, id}, enqueued in a durable SQLite queue; a single serial
worker (one Android box) drains by invoking mr_lookup.py and records the people-service
signal. GET /api/screening/requests/:id returns the row; GET /health is open. Crash-safe
(requeues stale in-flight rows on restart). 7 bun tests + typecheck; smoke-tested end to
end (auth 401, enqueue 202, drain→verdict, invalid-phone 400).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:57:54 -04:00

78 lines
2.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { Database } from 'bun:sqlite';
import { ScreeningQueue } from './queue.ts';
import { drainOnce, type LookupRunner } from './worker.ts';
function freshQueue(): ScreeningQueue {
return new ScreeningQueue(new Database(':memory:'));
}
describe('ScreeningQueue', () => {
test('enqueue → claimNext is FIFO and flips status to running', () => {
const q = freshQueue();
const a = q.enqueue('+15551112222', 'ref-a');
q.enqueue('+15553334444', null);
expect(q.pendingCount()).toBe(2);
const first = q.claimNext();
expect(first?.id).toBe(a);
expect(first?.phone).toBe('+15551112222');
expect(first?.ref).toBe('ref-a');
expect(first?.status).toBe('running');
expect(q.pendingCount()).toBe(1);
});
test('claimNext returns null when nothing pending', () => {
const q = freshQueue();
expect(q.claimNext()).toBeNull();
});
test('complete and fail record terminal state', () => {
const q = freshQueue();
const id = q.enqueue('+15551112222', null);
q.claimNext();
q.complete(id, 'denied');
const row = q.get(id);
expect(row?.status).toBe('done');
expect(row?.verdict).toBe('denied');
expect(row?.finished_at).not.toBeNull();
});
test('requeueStale recovers a crashed in-flight row', () => {
const q = freshQueue();
const id = q.enqueue('+15551112222', null);
q.claimNext(); // now 'running'
expect(q.pendingCount()).toBe(0);
expect(q.requeueStale()).toBe(1);
expect(q.pendingCount()).toBe(1);
expect(q.claimNext()?.id).toBe(id);
});
});
describe('drainOnce', () => {
test('records the runner verdict on success', async () => {
const q = freshQueue();
const id = q.enqueue('+15551112222', 'r');
const runner: LookupRunner = async (phone) => (phone === '+15551112222' ? 'cop_flag' : null);
expect(await drainOnce(q, runner)).toBe(true);
expect(q.get(id)?.status).toBe('done');
expect(q.get(id)?.verdict).toBe('cop_flag');
});
test('captures the error and marks the row failed', async () => {
const q = freshQueue();
const id = q.enqueue('+15551112222', null);
const runner: LookupRunner = async () => {
throw new Error('adb offline');
};
expect(await drainOnce(q, runner)).toBe(true);
expect(q.get(id)?.status).toBe('error');
expect(q.get(id)?.error).toContain('adb offline');
});
test('returns false when the queue is empty', async () => {
const q = freshQueue();
expect(await drainOnce(q, async () => 'denied')).toBe(false);
});
});