openreader/tests/unit/compute-control-plane.spec.ts

201 lines
5.6 KiB
TypeScript

import { expect, test } from '@playwright/test';
import type { WorkerOperationRequest, WorkerOperationState } from '../../compute/core/src/api-contracts';
import {
encodeSseFrame,
explainReplacementReason,
InMemoryOperationEventStream,
InMemoryOperationQueue,
InMemoryOperationStateStore,
OperationOrchestrator,
parseSseEventId,
parseSsePayload,
shouldReuseExistingOperation,
} from '../../compute/core/src/control-plane';
function buildPdfLayoutRequest(opKey: string): WorkerOperationRequest {
return {
kind: 'pdf_layout',
opKey,
payload: {
documentId: `doc-${opKey}`,
documentObjectKey: `s3://bucket/${opKey}.pdf`,
namespace: null,
},
};
}
test.describe('compute control-plane', () => {
test('in-memory queue and state store support enqueue/claim/CAS', async () => {
const queue = new InMemoryOperationQueue();
const store = new InMemoryOperationStateStore();
await queue.enqueue({
jobId: 'job-1',
opId: 'op-1',
opKey: 'k-1',
kind: 'pdf_layout',
queuedAt: 1000,
payload: { documentId: 'd1', documentObjectKey: 'obj1', namespace: null },
});
await queue.enqueue({
jobId: 'job-2',
opId: 'op-2',
opKey: 'k-2',
kind: 'whisper_align',
queuedAt: 1100,
payload: { text: 'hello', audioObjectKey: 'obj2' },
});
expect(queue.size()).toBe(2);
expect(queue.size('pdf_layout')).toBe(1);
expect(queue.size('whisper_align')).toBe(1);
const claimedLayout = await queue.claimNext('pdf_layout');
expect(claimedLayout?.opId).toBe('op-1');
expect(queue.size('pdf_layout')).toBe(0);
const firstCas = await store.compareAndSetOpIndex({
opKey: 'k-1',
newOpId: 'op-1',
expectedOpId: null,
});
const secondCas = await store.compareAndSetOpIndex({
opKey: 'k-1',
newOpId: 'op-2',
expectedOpId: null,
});
expect(firstCas).toBeTruthy();
expect(secondCas).toBeFalsy();
expect(await store.getOpIndex('k-1')).toEqual({ opId: 'op-1' });
});
test('in-memory event stream replays from sinceEventId and streams live events', async () => {
const stream = new InMemoryOperationEventStream();
const queued: WorkerOperationState = {
opId: 'op-1',
opKey: 'k-1',
kind: 'pdf_layout',
jobId: 'job-1',
status: 'queued',
queuedAt: 1000,
updatedAt: 1000,
};
const running: WorkerOperationState = {
...queued,
status: 'running',
startedAt: 1200,
updatedAt: 1200,
};
const succeeded: WorkerOperationState = {
...running,
status: 'succeeded',
updatedAt: 1400,
result: { ok: true },
};
await stream.append('op-1', queued);
await stream.append('op-1', running);
const receivedEventIds: number[] = [];
const unsubscribe = await stream.subscribe({
opId: 'op-1',
sinceEventId: 1,
onEvent: (event) => {
receivedEventIds.push(event.eventId);
},
});
await stream.append('op-1', succeeded);
unsubscribe();
expect(receivedEventIds).toEqual([2, 3]);
});
test('orchestrator reuses fresh inflight operations and replaces stale ones', 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 = buildPdfLayoutRequest('same-op-key');
const first = await orchestrator.enqueueOrReuse(request);
expect(first.opId).toBe('op-1');
expect(queue.size('pdf_layout')).toBe(1);
now = 2_000;
const reused = await orchestrator.enqueueOrReuse(request);
expect(reused.opId).toBe('op-1');
expect(queue.size('pdf_layout')).toBe(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(queue.size('pdf_layout')).toBe(2);
expect(await stateStore.getOpIndex('same-op-key')).toEqual({ opId: 'op-2' });
const op1Events = await eventStream.listSince('op-1', 0);
const op2Events = await eventStream.listSince('op-2', 0);
expect(op1Events.map((event) => event.eventId)).toEqual([1, 2]);
expect(op2Events.map((event) => event.eventId)).toEqual([1]);
});
test('state-machine helpers return consistent reuse/replacement decisions', () => {
const current: WorkerOperationState = {
opId: 'op-1',
opKey: 'same-op-key',
kind: 'pdf_layout',
jobId: 'job-1',
status: 'running',
queuedAt: 1000,
updatedAt: 2000,
};
expect(shouldReuseExistingOperation({
current,
requestKind: 'pdf_layout',
now: 2500,
opStaleMs: 1_000,
})).toBeTruthy();
expect(shouldReuseExistingOperation({
current,
requestKind: 'pdf_layout',
now: 4005,
opStaleMs: 1_000,
})).toBeFalsy();
expect(explainReplacementReason({
current,
requestKind: 'pdf_layout',
now: 4005,
opStaleMs: 1_000,
})).toBe('stale_running');
});
test('sse helpers encode and decode frame payload and id', () => {
const frame = encodeSseFrame({
id: 42,
event: 'snapshot',
data: { ok: true },
});
expect(parseSseEventId(frame)).toBe(42);
expect(parseSsePayload(frame)).toBe('{"ok":true}');
});
});