Restart failed saves through a guarded server retry

This commit is contained in:
Jonathan Sykes
2026-10-03 19:16:08 +08:00
parent 5485a3c2f4
commit f341b0c76e
7 changed files with 162 additions and 4 deletions

View 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');
});