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:
@@ -1,5 +1,29 @@
|
||||
import { RetryStrategy } from '../src/index.js';
|
||||
|
||||
function createJob(retryCount = 0, maxAttempts = 4) {
|
||||
return {
|
||||
id: 'job-1',
|
||||
status: 'active' as const,
|
||||
data: {},
|
||||
currentPhase: 'run',
|
||||
phases: [],
|
||||
phaseResults: {},
|
||||
progress: 0,
|
||||
progressMessage: null,
|
||||
error: null,
|
||||
retryCount,
|
||||
maxAttempts,
|
||||
webhookUrl: null,
|
||||
webhookSent: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
scheduledAt: null,
|
||||
cancelledAt: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe('RetryStrategy', () => {
|
||||
it('uses exponential backoff for recoverable errors', async () => {
|
||||
const strategy = new RetryStrategy({
|
||||
@@ -8,60 +32,64 @@ describe('RetryStrategy', () => {
|
||||
classifyError: async () => 'recoverable',
|
||||
});
|
||||
|
||||
const decision = await strategy.shouldRetry(new Error('boom'), {
|
||||
id: 'job-1',
|
||||
status: 'active',
|
||||
data: {},
|
||||
currentPhase: 'run',
|
||||
phases: [],
|
||||
phaseResults: {},
|
||||
progress: 0,
|
||||
progressMessage: null,
|
||||
error: null,
|
||||
retryCount: 1,
|
||||
maxAttempts: 4,
|
||||
webhookUrl: null,
|
||||
webhookSent: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
scheduledAt: null,
|
||||
cancelledAt: null,
|
||||
});
|
||||
const decision = await strategy.shouldRetry(new Error('boom'), createJob(1));
|
||||
|
||||
expect(decision.retry).toBe(true);
|
||||
expect(decision.delayMs).toBe(200);
|
||||
expect(decision.disposition).toBe('recoverable');
|
||||
});
|
||||
|
||||
it('uses fixed backoff when configured', async () => {
|
||||
const strategy = new RetryStrategy({
|
||||
maxAttempts: 4,
|
||||
strategy: 'fixed',
|
||||
baseDelayMs: 75,
|
||||
classifyError: async () => 'recoverable',
|
||||
});
|
||||
|
||||
const decision = await strategy.shouldRetry(new Error('boom'), createJob(2));
|
||||
|
||||
expect(decision.retry).toBe(true);
|
||||
expect(decision.delayMs).toBe(75);
|
||||
});
|
||||
|
||||
it('uses linear backoff and caps to maxDelayMs', async () => {
|
||||
const strategy = new RetryStrategy({
|
||||
maxAttempts: 6,
|
||||
strategy: 'linear',
|
||||
baseDelayMs: 100,
|
||||
maxDelayMs: 250,
|
||||
classifyError: async () => 'recoverable',
|
||||
});
|
||||
|
||||
const decision = await strategy.shouldRetry(new Error('boom'), createJob(4, 6));
|
||||
|
||||
expect(decision.retry).toBe(true);
|
||||
expect(decision.delayMs).toBe(250);
|
||||
});
|
||||
|
||||
it('stops retrying when max attempts are exhausted', async () => {
|
||||
const strategy = new RetryStrategy({
|
||||
maxAttempts: 3,
|
||||
classifyError: async () => 'recoverable',
|
||||
});
|
||||
|
||||
const decision = await strategy.shouldRetry(new Error('boom'), createJob(2, 3));
|
||||
|
||||
expect(decision).toEqual({
|
||||
retry: false,
|
||||
delayMs: 0,
|
||||
disposition: 'recoverable',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not retry fatal errors', async () => {
|
||||
const strategy = new RetryStrategy({
|
||||
maxAttempts: 4,
|
||||
classifyError: async () => 'fatal',
|
||||
});
|
||||
|
||||
const decision = await strategy.shouldRetry(new Error('fatal'), {
|
||||
id: 'job-1',
|
||||
status: 'active',
|
||||
data: {},
|
||||
currentPhase: 'run',
|
||||
phases: [],
|
||||
phaseResults: {},
|
||||
progress: 0,
|
||||
progressMessage: null,
|
||||
error: null,
|
||||
retryCount: 0,
|
||||
maxAttempts: 4,
|
||||
webhookUrl: null,
|
||||
webhookSent: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
scheduledAt: null,
|
||||
cancelledAt: null,
|
||||
});
|
||||
const decision = await strategy.shouldRetry(new Error('fatal'), createJob());
|
||||
|
||||
expect(decision).toEqual({
|
||||
retry: false,
|
||||
|
||||
Reference in New Issue
Block a user