- Preserve phase results on partial retry and keep interrupted phase context after restart. - Avoid webhook bookkeeping crashes when retention deletes stale jobs. - Add deeper unit, integration, and e2e coverage around queue seams. - Require verify job to pass before publish runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
199 lines
4.9 KiB
TypeScript
199 lines
4.9 KiB
TypeScript
import { SqliteStorage } from '../src/index.js';
|
|
import { cleanupDir, createDbPath, createTempDir } from './helpers.js';
|
|
|
|
describe('SqliteStorage', () => {
|
|
it('creates, updates, and completes jobs', () => {
|
|
const dir = createTempDir();
|
|
const storage = new SqliteStorage<{ url: string }>(createDbPath(dir));
|
|
|
|
try {
|
|
const job = storage.createJob(
|
|
'job-1',
|
|
{ url: 'https://example.com' },
|
|
[
|
|
{
|
|
name: 'download',
|
|
status: 'pending',
|
|
progress: 0,
|
|
message: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
error: null,
|
|
},
|
|
],
|
|
{},
|
|
3,
|
|
);
|
|
|
|
expect(job.status).toBe('pending');
|
|
expect(storage.claimPendingJob(job.id)).toBe(true);
|
|
|
|
const inProgress = storage.saveProgress(
|
|
job.id,
|
|
'download',
|
|
[
|
|
{
|
|
name: 'download',
|
|
status: 'active',
|
|
progress: 50,
|
|
message: 'halfway',
|
|
startedAt: new Date().toISOString(),
|
|
completedAt: null,
|
|
error: null,
|
|
},
|
|
],
|
|
50,
|
|
'halfway',
|
|
);
|
|
|
|
expect(inProgress.status).toBe('active');
|
|
expect(inProgress.progress).toBe(50);
|
|
|
|
const completed = storage.completeJob(
|
|
job.id,
|
|
[
|
|
{
|
|
name: 'download',
|
|
status: 'completed',
|
|
progress: 100,
|
|
message: null,
|
|
startedAt: new Date().toISOString(),
|
|
completedAt: new Date().toISOString(),
|
|
error: null,
|
|
},
|
|
],
|
|
{ download: { filePath: '/tmp/file' } },
|
|
);
|
|
|
|
expect(completed.status).toBe('completed');
|
|
expect(completed.progress).toBe(100);
|
|
expect(completed.phaseResults.download).toEqual({ filePath: '/tmp/file' });
|
|
} finally {
|
|
storage.close();
|
|
cleanupDir(dir);
|
|
}
|
|
});
|
|
|
|
it('preserves phase results when resetting for partial retry', () => {
|
|
const dir = createTempDir();
|
|
const storage = new SqliteStorage<{ url: string }>(createDbPath(dir));
|
|
|
|
try {
|
|
const job = storage.createJob(
|
|
'job-1',
|
|
{ url: 'https://example.com' },
|
|
[
|
|
{
|
|
name: 'download',
|
|
status: 'pending',
|
|
progress: 0,
|
|
message: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
error: null,
|
|
},
|
|
{
|
|
name: 'process',
|
|
status: 'pending',
|
|
progress: 0,
|
|
message: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
error: null,
|
|
},
|
|
],
|
|
{},
|
|
3,
|
|
);
|
|
|
|
const retried = storage.resetForRetry(
|
|
job.id,
|
|
[
|
|
{
|
|
name: 'download',
|
|
status: 'completed',
|
|
progress: 100,
|
|
message: null,
|
|
startedAt: new Date().toISOString(),
|
|
completedAt: new Date().toISOString(),
|
|
error: null,
|
|
},
|
|
{
|
|
name: 'process',
|
|
status: 'pending',
|
|
progress: 0,
|
|
message: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
error: null,
|
|
},
|
|
],
|
|
{ download: { filePath: '/tmp/file' } },
|
|
3,
|
|
null,
|
|
);
|
|
|
|
expect(retried.status).toBe('pending');
|
|
expect(retried.progress).toBe(0);
|
|
expect(retried.phaseResults.download).toEqual({ filePath: '/tmp/file' });
|
|
} finally {
|
|
storage.close();
|
|
cleanupDir(dir);
|
|
}
|
|
});
|
|
|
|
it('preserves interrupted phase when resetting active jobs on restart', () => {
|
|
const dir = createTempDir();
|
|
const storage = new SqliteStorage<{ url: string }>(createDbPath(dir));
|
|
|
|
try {
|
|
const job = storage.createJob(
|
|
'job-1',
|
|
{ url: 'https://example.com' },
|
|
[
|
|
{
|
|
name: 'download',
|
|
status: 'pending',
|
|
progress: 0,
|
|
message: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
error: null,
|
|
},
|
|
],
|
|
{},
|
|
3,
|
|
);
|
|
|
|
expect(storage.claimPendingJob(job.id)).toBe(true);
|
|
|
|
storage.saveProgress(
|
|
job.id,
|
|
'download',
|
|
[
|
|
{
|
|
name: 'download',
|
|
status: 'active',
|
|
progress: 25,
|
|
message: 'working',
|
|
startedAt: new Date().toISOString(),
|
|
completedAt: null,
|
|
error: null,
|
|
},
|
|
],
|
|
25,
|
|
'working',
|
|
);
|
|
|
|
const reset = storage.resetActiveJobs('Interrupted by process restart');
|
|
expect(reset).toHaveLength(1);
|
|
expect(reset[0]?.status).toBe('failed');
|
|
expect(reset[0]?.error?.phase).toBe('download');
|
|
expect(reset[0]?.error?.attempt).toBe(1);
|
|
} finally {
|
|
storage.close();
|
|
cleanupDir(dir);
|
|
}
|
|
});
|
|
});
|