openreader/compute-worker/tests/compute/control-plane/orchestrator.test.ts
Richard R a56aaa2be9 Refactor PDF parsing and rendering logic; migrate to compute-worker protocol
- Removed deprecated functions related to document parsing and blob storage in blobstore.ts.
- Introduced new PDF rendering logic in pdf-preview-renderer.ts and pdf-preview-pdfjs-runtime.ts.
- Updated previews-render.ts to utilize the new PDF rendering functions.
- Refactored user-whisper-align-job.ts to use the compute-worker client for alignment requests.
- Enhanced artifact.ts and operation.ts to validate parsed PDF artifacts and resolve current PDF parses.
- Updated snapshot.ts to align with new worker operation types.
- Adjusted runtime-config.ts to check for compute-worker availability.
- Modified types in parsed-pdf.ts and tts.ts to reflect changes in the compute-worker protocol.
- Added unit tests for PDF artifact validation and compute-worker client contract.
- Removed obsolete pdf-op-key.vitest.spec.ts test file.
2026-06-12 13:43:33 -06:00

138 lines
4.6 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import type { WorkerOperationRequest } from '../../../src/compute/api-contracts';
import { OperationOrchestrator } from '../../../src/compute/control-plane';
import {
InMemoryOperationEventStream,
InMemoryOperationQueue,
InMemoryOperationStateStore,
} from '../helpers/in-memory-control-plane';
function buildRequest(opKey: string): WorkerOperationRequest {
return {
kind: 'pdf_layout',
opKey,
payload: {
documentId: `doc-${opKey}`,
namespace: null,
documentObjectKey: `s3://bucket/${opKey}.pdf`,
},
};
}
describe('operation orchestrator', () => {
test('reuses fresh operation and replaces stale operation', async () => {
let now = 1_000;
let nextId = 1;
const queue = new InMemoryOperationQueue();
const stateStore = new InMemoryOperationStateStore();
const eventStream = new InMemoryOperationEventStream();
const orchestrator = new OperationOrchestrator({
queue,
stateStore,
eventStream,
config: { opStaleMs: 2_000, maxCasRetries: 5 },
clock: { now: () => now },
idFactory: {
opId: () => `op-${nextId}`,
jobId: () => `job-${nextId++}`,
},
});
const request = buildRequest('shared-op');
const first = await orchestrator.enqueueOrReuse(request);
expect(first.opId).toBe('op-1');
now = 2_000;
const reused = await orchestrator.enqueueOrReuse(request);
expect(reused.opId).toBe('op-1');
await orchestrator.markRunning({ opId: first.opId, updatedAt: 2_100 });
now = 6_000;
const replaced = await orchestrator.enqueueOrReuse(request);
expect(replaced.opId).toBe('op-2');
expect(await stateStore.getOpIndex('shared-op')).toEqual({ opId: 'op-2' });
expect(queue.size('pdf_layout')).toBe(2);
});
test('survives transient CAS conflict and eventually creates operation', async () => {
const queue = new InMemoryOperationQueue();
const eventStream = new InMemoryOperationEventStream();
const store = new InMemoryOperationStateStore();
let firstAttempt = true;
const conflictStore = {
getOpState: store.getOpState.bind(store),
getOpStateRecord: store.getOpStateRecord.bind(store),
putOpState: store.putOpState.bind(store),
compareAndSetOpState: store.compareAndSetOpState.bind(store),
getOpIndex: store.getOpIndex.bind(store),
compareAndSetOpIndex: async (input: { opKey: string; newOpId: string; expectedOpId: string | null }) => {
if (firstAttempt && input.expectedOpId === null) {
firstAttempt = false;
return false;
}
return store.compareAndSetOpIndex(input);
},
};
let id = 1;
const orchestrator = new OperationOrchestrator({
queue,
stateStore: conflictStore,
eventStream,
config: { opStaleMs: 2_000, maxCasRetries: 4 },
idFactory: {
opId: () => `op-${id}`,
jobId: () => `job-${id++}`,
},
});
const created = await orchestrator.enqueueOrReuse(buildRequest('cas-key'));
expect(created.opId).toMatch(/^op-/);
expect(await store.getOpIndex('cas-key')).toEqual({ opId: created.opId });
});
test('markFailedIfUnchanged only writes once for the expected revision', async () => {
const queue = new InMemoryOperationQueue();
const stateStore = new InMemoryOperationStateStore();
const eventStream = new InMemoryOperationEventStream();
const orchestrator = new OperationOrchestrator({
queue,
stateStore,
eventStream,
config: { opStaleMs: 2_000, maxCasRetries: 5 },
});
const created = await orchestrator.enqueueOrReuse(buildRequest('stale-op'));
await orchestrator.markRunning({ opId: created.opId, updatedAt: 2_000 });
const record = await stateStore.getOpStateRecord(created.opId);
expect(record).not.toBeNull();
const first = await orchestrator.markFailedIfUnchanged({
current: record!.state,
expectedRevision: record!.revision,
error: { code: 'WORKER_ORPHANED_OP', message: 'stale op' },
updatedAt: 3_000,
});
const second = await orchestrator.markFailedIfUnchanged({
current: record!.state,
expectedRevision: record!.revision,
error: { code: 'WORKER_ORPHANED_OP', message: 'stale op' },
updatedAt: 3_000,
});
expect(first).toMatchObject({
opId: created.opId,
status: 'failed',
error: { code: 'WORKER_ORPHANED_OP' },
});
expect(second).toBeNull();
const events = await eventStream.listSince(created.opId, 0);
expect(events.filter((event) => event.snapshot.status === 'failed')).toHaveLength(1);
});
});