- Extract database schema and migrations into @openreader/database - Extract startup and orchestration scripts into @openreader/bootstrap - Move compute-worker into packages/compute-worker - Remove runtime dependency on drizzle-kit - Update Dockerfile to deploy isolated packages without merging node_modules
298 lines
9.1 KiB
TypeScript
298 lines
9.1 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
import { createComputeWorkerApp } from '../../src/api/app';
|
|
import { FakeControlPlane } from '../fixtures/fake-control-plane';
|
|
|
|
const AUTH = { authorization: 'Bearer test-token' };
|
|
|
|
describe('compute worker API routes', () => {
|
|
let fake: FakeControlPlane;
|
|
let runtime: Awaited<ReturnType<typeof createComputeWorkerApp>>;
|
|
|
|
beforeEach(async () => {
|
|
fake = new FakeControlPlane();
|
|
runtime = await createComputeWorkerApp({
|
|
workerToken: 'test-token',
|
|
disableWorkers: true,
|
|
routeDeps: fake.deps,
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await runtime.close();
|
|
});
|
|
|
|
test('allows unauthenticated health checks but protects operation routes', async () => {
|
|
const live = await runtime.app.inject({ method: 'GET', url: '/health/live' });
|
|
expect(live.statusCode).toBe(200);
|
|
|
|
const protectedRoute = await runtime.app.inject({ method: 'GET', url: '/v1/operations/op-1' });
|
|
expect(protectedRoute.statusCode).toBe(401);
|
|
});
|
|
|
|
test('validates operation creation body and returns 400 for invalid payload', async () => {
|
|
const response = await runtime.app.inject({
|
|
method: 'POST',
|
|
url: '/v1/pdf-layout/operations',
|
|
headers: AUTH,
|
|
payload: {
|
|
documentId: '',
|
|
namespace: null,
|
|
documentObjectKey: 'openreader/doc.pdf',
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.json()).toMatchObject({ error: 'Bad Request' });
|
|
});
|
|
|
|
test('creates operation and fetches state by op id', async () => {
|
|
const documentId = 'c'.repeat(64);
|
|
const create = await runtime.app.inject({
|
|
method: 'POST',
|
|
url: '/v1/pdf-layout/operations',
|
|
headers: AUTH,
|
|
payload: {
|
|
documentId,
|
|
namespace: null,
|
|
documentObjectKey: `openreader/${documentId}.pdf`,
|
|
},
|
|
});
|
|
|
|
expect(create.statusCode).toBe(202);
|
|
const created = create.json();
|
|
expect(created).toMatchObject({
|
|
subject: { kind: 'pdf_layout', documentId, namespace: null },
|
|
status: 'queued',
|
|
});
|
|
expect(created).not.toHaveProperty('opKey');
|
|
expect(created).not.toHaveProperty('jobId');
|
|
|
|
const fetch = await runtime.app.inject({
|
|
method: 'GET',
|
|
url: `/v1/operations/${created.opId}`,
|
|
headers: AUTH,
|
|
});
|
|
|
|
expect(fetch.statusCode).toBe(200);
|
|
expect(fetch.json()).toMatchObject({ opId: created.opId, status: 'queued' });
|
|
});
|
|
|
|
test('reuses idempotent PDF requests and replaces them only with an explicit token', async () => {
|
|
const documentId = 'e'.repeat(64);
|
|
const payload = {
|
|
documentId,
|
|
namespace: null,
|
|
documentObjectKey: `openreader/${documentId}.pdf`,
|
|
};
|
|
const create = (body: typeof payload & { replaceToken?: string }) => runtime.app.inject({
|
|
method: 'POST',
|
|
url: '/v1/pdf-layout/operations',
|
|
headers: AUTH,
|
|
payload: body,
|
|
});
|
|
|
|
const initial = (await create(payload)).json();
|
|
const reused = (await create(payload)).json();
|
|
const replacement = (await create({ ...payload, replaceToken: 'replace-1' })).json();
|
|
|
|
expect(reused.opId).toBe(initial.opId);
|
|
expect(replacement.opId).not.toBe(initial.opId);
|
|
expect(replacement.subject).toEqual(initial.subject);
|
|
});
|
|
|
|
test('creates whisper alignment operations without exposing internal keys', async () => {
|
|
const response = await runtime.app.inject({
|
|
method: 'POST',
|
|
url: '/v1/whisper-align/operations',
|
|
headers: AUTH,
|
|
payload: {
|
|
text: 'Canonical worker text',
|
|
audioObjectKey: 'openreader/audio.mp3',
|
|
lang: 'en',
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(202);
|
|
expect(response.json()).toMatchObject({
|
|
subject: { kind: 'whisper_align' },
|
|
status: 'queued',
|
|
});
|
|
expect(response.json()).not.toHaveProperty('opKey');
|
|
});
|
|
|
|
test('resolves the current PDF artifact and operation without exposing parser identity', async () => {
|
|
const documentId = 'a'.repeat(64);
|
|
const create = await runtime.app.inject({
|
|
method: 'POST',
|
|
url: '/v1/pdf-layout/operations',
|
|
headers: AUTH,
|
|
payload: {
|
|
documentId,
|
|
namespace: null,
|
|
documentObjectKey: `openreader/${documentId}.pdf`,
|
|
},
|
|
});
|
|
expect(create.statusCode).toBe(202);
|
|
|
|
const artifactKey = `openreader/documents_v1/parsed_v2/${documentId}/pp-doclayoutv3-onnx%40800%2Bpdfjs%404.8.69.json`;
|
|
fake.seedArtifact(artifactKey);
|
|
const resolve = await runtime.app.inject({
|
|
method: 'POST',
|
|
url: '/v1/pdf-layout/resolve',
|
|
headers: AUTH,
|
|
payload: {
|
|
documentId,
|
|
namespace: null,
|
|
documentObjectKey: `openreader/${documentId}.pdf`,
|
|
},
|
|
});
|
|
|
|
expect(resolve.statusCode).toBe(200);
|
|
expect(resolve.json()).toMatchObject({
|
|
artifact: { objectKey: artifactKey },
|
|
operation: {
|
|
subject: { kind: 'pdf_layout', documentId, namespace: null },
|
|
},
|
|
});
|
|
expect(resolve.json().operation).not.toHaveProperty('opKey');
|
|
});
|
|
|
|
test('resolves a current PDF artifact after operation state has expired', async () => {
|
|
const documentId = 'd'.repeat(64);
|
|
const artifactKey = `openreader/documents_v1/parsed_v2/${documentId}/pp-doclayoutv3-onnx%40800%2Bpdfjs%404.8.69.json`;
|
|
fake.seedArtifact(artifactKey);
|
|
|
|
const resolve = await runtime.app.inject({
|
|
method: 'POST',
|
|
url: '/v1/pdf-layout/resolve',
|
|
headers: AUTH,
|
|
payload: {
|
|
documentId,
|
|
namespace: null,
|
|
documentObjectKey: `openreader/${documentId}.pdf`,
|
|
},
|
|
});
|
|
|
|
expect(resolve.statusCode).toBe(200);
|
|
expect(resolve.json()).toEqual({
|
|
artifact: { objectKey: artifactKey },
|
|
operation: null,
|
|
});
|
|
});
|
|
|
|
test('returns not found for unknown operation and event stream lookups', async () => {
|
|
const opResponse = await runtime.app.inject({
|
|
method: 'GET',
|
|
url: '/v1/operations/missing',
|
|
headers: AUTH,
|
|
});
|
|
expect(opResponse.statusCode).toBe(404);
|
|
|
|
const eventsResponse = await runtime.app.inject({
|
|
method: 'GET',
|
|
url: '/v1/operations/missing/events',
|
|
headers: AUTH,
|
|
});
|
|
expect(eventsResponse.statusCode).toBe(404);
|
|
});
|
|
|
|
test('streams initial SSE snapshot for terminal operation and honors cursor id', async () => {
|
|
fake.seedState({
|
|
opId: 'op-terminal',
|
|
opKey: 'k-terminal',
|
|
kind: 'pdf_layout',
|
|
jobId: 'job-op-terminal',
|
|
status: 'succeeded',
|
|
queuedAt: 1000,
|
|
updatedAt: 2000,
|
|
result: { parsedObjectKey: 'openreader/parsed.json' },
|
|
});
|
|
|
|
const stream = await runtime.app.inject({
|
|
method: 'GET',
|
|
url: '/v1/operations/op-terminal/events?sinceEventId=7',
|
|
headers: AUTH,
|
|
});
|
|
|
|
expect(stream.statusCode).toBe(200);
|
|
expect(stream.headers['content-type']).toContain('text/event-stream');
|
|
expect(stream.body).toContain('event: snapshot');
|
|
expect(stream.body).toContain('id: 7');
|
|
expect(stream.body).toContain('"status":"succeeded"');
|
|
});
|
|
|
|
test('marks stale running whisper and pdf ops failed during request-time orphan recovery but leaves queued ops on the conservative path', async () => {
|
|
const now = Date.now();
|
|
fake.seedState({
|
|
opId: 'op-stale-whisper-running',
|
|
opKey: 'k-stale-whisper-running',
|
|
kind: 'whisper_align',
|
|
jobId: 'job-op-stale-whisper-running',
|
|
status: 'running',
|
|
queuedAt: 1,
|
|
updatedAt: now - 40_000,
|
|
});
|
|
fake.seedState({
|
|
opId: 'op-stale-whisper-queued',
|
|
opKey: 'k-stale-whisper-queued',
|
|
kind: 'whisper_align',
|
|
jobId: 'job-op-stale-whisper-queued',
|
|
status: 'queued',
|
|
queuedAt: 1,
|
|
updatedAt: now - 40_000,
|
|
});
|
|
fake.seedState({
|
|
opId: 'op-stale-pdf-running',
|
|
opKey: 'k-stale-pdf-running',
|
|
kind: 'pdf_layout',
|
|
jobId: 'job-op-stale-pdf-running',
|
|
status: 'running',
|
|
queuedAt: 1,
|
|
updatedAt: now - 310_000,
|
|
});
|
|
fake.seedState({
|
|
opId: 'op-stale-pdf-queued',
|
|
opKey: 'k-stale-pdf-queued',
|
|
kind: 'pdf_layout',
|
|
jobId: 'job-op-stale-pdf-queued',
|
|
status: 'queued',
|
|
queuedAt: 1,
|
|
updatedAt: now - 310_000,
|
|
});
|
|
|
|
// GET /v1/operations/:opId resolves via getOpState(), which first awaits the shared
|
|
// orphanRecoveryPromise path through ensureOrphanedOpRecovery().
|
|
const fetch = await runtime.app.inject({
|
|
method: 'GET',
|
|
url: '/v1/operations/op-stale-whisper-running',
|
|
headers: AUTH,
|
|
});
|
|
|
|
expect(fetch.statusCode).toBe(200);
|
|
expect(fetch.json()).toMatchObject({
|
|
opId: 'op-stale-whisper-running',
|
|
status: 'failed',
|
|
error: {
|
|
code: 'WORKER_ORPHANED_OP',
|
|
},
|
|
});
|
|
expect(fake.getState('op-stale-whisper-running')).toMatchObject({
|
|
status: 'failed',
|
|
error: {
|
|
code: 'WORKER_ORPHANED_OP',
|
|
},
|
|
});
|
|
expect(fake.getState('op-stale-whisper-queued')).toMatchObject({
|
|
status: 'queued',
|
|
});
|
|
expect(fake.getState('op-stale-pdf-running')).toMatchObject({
|
|
status: 'failed',
|
|
error: {
|
|
code: 'WORKER_ORPHANED_OP',
|
|
},
|
|
});
|
|
expect(fake.getState('op-stale-pdf-queued')).toMatchObject({
|
|
status: 'queued',
|
|
});
|
|
});
|
|
});
|