fix: harden queue lifecycle and publish gate

- 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>
This commit is contained in:
2026-05-16 18:39:19 +02:00
parent 679053b27d
commit a9429e2118
16 changed files with 1867 additions and 87 deletions

View File

@@ -73,4 +73,126 @@ describe('SqliteStorage', () => {
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);
}
});
});