172 lines
5.2 KiB
TypeScript
172 lines
5.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { runPolledTask } from '@/lib/media/polled-task';
|
|
|
|
describe('runPolledTask', () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('returns an immediately completed submission without waiting or polling', async () => {
|
|
vi.useFakeTimers();
|
|
const submit = vi.fn().mockResolvedValue({ status: 'done', result: 'video-url' });
|
|
const poll = vi.fn();
|
|
|
|
const result = await runPolledTask({
|
|
submit,
|
|
poll,
|
|
intervalMs: 1_000,
|
|
maxAttempts: 3,
|
|
label: 'Test task',
|
|
});
|
|
|
|
expect(result).toBe('video-url');
|
|
expect(submit).toHaveBeenCalledTimes(1);
|
|
expect(poll).not.toHaveBeenCalled();
|
|
expect(vi.getTimerCount()).toBe(0);
|
|
});
|
|
|
|
it('rejects an immediately failed submission without waiting or polling', async () => {
|
|
vi.useFakeTimers();
|
|
const submit = vi.fn().mockResolvedValue({ status: 'failed', message: 'submit failed' });
|
|
const poll = vi.fn();
|
|
|
|
await expect(
|
|
runPolledTask({
|
|
submit,
|
|
poll,
|
|
intervalMs: 1_000,
|
|
maxAttempts: 3,
|
|
label: 'Test task',
|
|
}),
|
|
).rejects.toThrow('submit failed');
|
|
|
|
expect(submit).toHaveBeenCalledTimes(1);
|
|
expect(poll).not.toHaveBeenCalled();
|
|
expect(vi.getTimerCount()).toBe(0);
|
|
});
|
|
|
|
it('waits before each poll and passes the submitted task id until completion', async () => {
|
|
vi.useFakeTimers();
|
|
const submit = vi.fn().mockResolvedValue({ status: 'submitted', taskId: 'task-123' });
|
|
const poll = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ status: 'pending', detail: 'queued' })
|
|
.mockResolvedValueOnce({ status: 'done', result: 'video-url' });
|
|
|
|
const promise = runPolledTask({
|
|
submit,
|
|
poll,
|
|
intervalMs: 1_000,
|
|
maxAttempts: 3,
|
|
label: 'Test task',
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(999);
|
|
expect(poll).not.toHaveBeenCalled();
|
|
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
expect(poll).toHaveBeenNthCalledWith(1, 'task-123');
|
|
|
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
await expect(promise).resolves.toBe('video-url');
|
|
expect(submit).toHaveBeenCalledTimes(1);
|
|
expect(poll).toHaveBeenNthCalledWith(2, 'task-123');
|
|
});
|
|
|
|
it('rejects a terminal poll failure without scheduling another attempt', async () => {
|
|
vi.useFakeTimers();
|
|
const poll = vi.fn().mockResolvedValue({ status: 'failed', message: 'provider failed' });
|
|
const promise = runPolledTask({
|
|
submit: vi.fn().mockResolvedValue({ status: 'submitted', taskId: 'task-123' }),
|
|
poll,
|
|
intervalMs: 1_000,
|
|
maxAttempts: 3,
|
|
label: 'Test task',
|
|
});
|
|
const rejection = expect(promise).rejects.toThrow('provider failed');
|
|
|
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
|
|
await rejection;
|
|
expect(poll).toHaveBeenCalledTimes(1);
|
|
expect(vi.getTimerCount()).toBe(0);
|
|
});
|
|
|
|
it('propagates submit and poll exceptions without wrapping them', async () => {
|
|
const submitError = new TypeError('submit network error');
|
|
await expect(
|
|
runPolledTask({
|
|
submit: vi.fn().mockRejectedValue(submitError),
|
|
poll: vi.fn(),
|
|
intervalMs: 0,
|
|
maxAttempts: 1,
|
|
label: 'Test task',
|
|
}),
|
|
).rejects.toBe(submitError);
|
|
|
|
const pollError = new SyntaxError('invalid poll response');
|
|
await expect(
|
|
runPolledTask({
|
|
submit: vi.fn().mockResolvedValue({ status: 'submitted', taskId: 'task-123' }),
|
|
poll: vi.fn().mockRejectedValue(pollError),
|
|
intervalMs: 0,
|
|
maxAttempts: 1,
|
|
label: 'Test task',
|
|
}),
|
|
).rejects.toBe(pollError);
|
|
});
|
|
|
|
it('polls exactly maxAttempts times before using the default timeout message', async () => {
|
|
vi.useFakeTimers();
|
|
const poll = vi.fn().mockResolvedValue({ status: 'pending' });
|
|
const promise = runPolledTask({
|
|
submit: vi.fn().mockResolvedValue({ status: 'submitted', taskId: 'task-123' }),
|
|
poll,
|
|
intervalMs: 250,
|
|
maxAttempts: 3,
|
|
label: 'Test task',
|
|
});
|
|
const rejection = expect(promise).rejects.toThrow('Test task timed out after 3 polls');
|
|
|
|
await vi.advanceTimersByTimeAsync(750);
|
|
|
|
await rejection;
|
|
expect(poll).toHaveBeenCalledTimes(3);
|
|
expect(vi.getTimerCount()).toBe(0);
|
|
});
|
|
|
|
it('passes the latest pending detail and timing data to a custom timeout formatter', async () => {
|
|
vi.useFakeTimers();
|
|
const formatTimeout = vi.fn(
|
|
({ lastPendingDetail }: { lastPendingDetail?: string }) =>
|
|
`last status: ${lastPendingDetail}`,
|
|
);
|
|
const poll = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ status: 'pending', detail: 'Queued' })
|
|
.mockResolvedValueOnce({ status: 'pending', detail: 'Processing' });
|
|
const promise = runPolledTask({
|
|
submit: vi.fn().mockResolvedValue({ status: 'submitted', taskId: 'task-123' }),
|
|
poll,
|
|
intervalMs: 500,
|
|
maxAttempts: 2,
|
|
label: 'Test task',
|
|
formatTimeout,
|
|
});
|
|
const rejection = expect(promise).rejects.toThrow('last status: Processing');
|
|
|
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
|
|
await rejection;
|
|
expect(formatTimeout).toHaveBeenCalledWith({
|
|
label: 'Test task',
|
|
taskId: 'task-123',
|
|
attempts: 2,
|
|
intervalMs: 500,
|
|
elapsedMs: 1_000,
|
|
lastPendingDetail: 'Processing',
|
|
});
|
|
});
|
|
});
|