Restart failed saves through a guarded server retry
This commit is contained in:
52
server/download-retry.test.js
Normal file
52
server/download-retry.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { test, expect } from 'bun:test';
|
||||
import { Hono } from 'hono';
|
||||
import { registerDownloadRetryRoute } from './download-retry.js';
|
||||
function fixture(status = 'failed') {
|
||||
const app = new Hono(), calls = [], hints = [];
|
||||
let clock = 1000;
|
||||
registerDownloadRetryRoute(app, {
|
||||
media: { status: async () => ({ status }), ensureCached: async (id, options) => { calls.push([id, options]); } },
|
||||
priority: 0, clearHints: id => hints.push(id), now: () => clock, log: { warn() {} },
|
||||
});
|
||||
return { app, calls, hints, advance: () => { clock += 16000; } };
|
||||
}
|
||||
const request = app => app.request('/api/download/wZzRoXymOUU/retry', { method: 'POST' });
|
||||
test('explicit retry starts a fresh guarded cache job and clears stale hints', async () => {
|
||||
const { app, calls, hints } = fixture();
|
||||
const response = await request(app);
|
||||
expect(response.status).toBe(202);
|
||||
expect(await response.json()).toEqual({ ok: true, state: 'working' });
|
||||
expect(calls).toEqual([['wZzRoXymOUU', { priority: 0, force: true }]]);
|
||||
expect(hints).toEqual(['wZzRoXymOUU']);
|
||||
expect(response.headers.get('cache-control')).toBe('no-store');
|
||||
});
|
||||
test('ready and in-progress songs are reused instead of redownloaded', async () => {
|
||||
for (const status of ['ready', 'queued', 'downloading', 'validating']) {
|
||||
const { app, calls } = fixture(status);
|
||||
expect((await request(app)).status).toBe(200);
|
||||
expect(calls).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
test('failed-job retries are bounded while later deliberate retries work', async () => {
|
||||
const f = fixture();
|
||||
expect((await request(f.app)).status).toBe(202);
|
||||
const blocked = await request(f.app);
|
||||
expect(blocked.status).toBe(429);
|
||||
expect(blocked.headers.get('retry-after')).toBe('15');
|
||||
expect(f.calls).toHaveLength(1);
|
||||
f.advance();
|
||||
expect((await request(f.app)).status).toBe(202);
|
||||
expect(f.calls).toHaveLength(2);
|
||||
});
|
||||
test('invalid ids never reach cache machinery and GET cannot force a retry', async () => {
|
||||
const { app, calls } = fixture();
|
||||
expect((await app.request('/api/download/bad-id/retry', { method: 'POST' })).status).toBe(400);
|
||||
expect((await app.request('/api/download/wZzRoXymOUU/retry')).status).toBe(404);
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
test('asynchronous upstream failure is handled and remains available through status', async () => {
|
||||
const app = new Hono(), warnings = [];
|
||||
registerDownloadRetryRoute(app, { priority: 0, media: { status: async () => ({ status: 'failed' }), ensureCached: async () => { throw new Error('Video unavailable'); } }, log: { warn: detail => warnings.push(detail) } });
|
||||
expect((await request(app)).status).toBe(202);
|
||||
expect(warnings[0]).toContain('Video unavailable');
|
||||
});
|
||||
Reference in New Issue
Block a user