From 7cb9a29e7716a3148c38f44f289a745190c771ab Mon Sep 17 00:00:00 2001 From: Harvmaster Date: Sun, 19 Jul 2026 17:15:31 +0000 Subject: [PATCH] Add tests for abort fn in exponential backoff --- test/exponential-backoff.test.ts | 96 +++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/test/exponential-backoff.test.ts b/test/exponential-backoff.test.ts index 4e073e8..7c29a96 100644 --- a/test/exponential-backoff.test.ts +++ b/test/exponential-backoff.test.ts @@ -1,6 +1,6 @@ import { expect, test, vi } from 'vitest'; import { ExponentialBackoff } from '../source/exponential-backoff.ts'; -import { ExponentialBackoffMaxRetriesHitError } from '../source/errors.ts'; +import { ExponentialBackoffMaxRetriesHitError, ExponentialBackoffStoppedRetriesError } from '../source/errors.ts'; /** * A valid options object that satisfies {@link ExponentialBackoff.validateOptions}. @@ -204,6 +204,97 @@ const testExponentialBackoffWrapsNonErrorThrows = async (): Promise => { expect(onError.mock.calls[0][0].message).toBe('not-an-error'); }; +/** + * Tests that when the task function succeeds and the abort signal is aborted, the result is returned + * and the onError callback is not called. + */ +const testExponentialBackoffRunSuccessAndAbortSignal = async (): Promise => { + // Define the function which aborts the exponential backoff and succeeds + const abortAndSucceedFn = vi.fn(({ stopRetries }) => { + stopRetries(new Error('retry me')); + + return Promise.resolve('success'); + }); + const onErrorFn = vi.fn(); + + // Run the exponential backoff with the function and the onError callback + const result = await ExponentialBackoff.run(abortAndSucceedFn, onErrorFn, { + baseDelay: 0, + jitter: 0, + }); + + // Expect the result to be the success message + expect(result).toBe('success'); + expect(abortAndSucceedFn).toHaveBeenCalledOnce(); + + // Expect the onError callback to not have been called + expect(onErrorFn).not.toHaveBeenCalled(); +}; + +/** + * Tests that when the abort signal is aborted with an error, an ExponentialBackoffStoppedRetriesError is thrown + * with the error as the message. + */ +const testExponentialBackoffRunWithAbortSignal = async (): Promise => { + // Define the function which aborts the exponential backoff and throws an error + const abortAndThrowFn = vi.fn(({ stopRetries }) => { + stopRetries(new Error('exponential backoff aborted')); + throw new Error('error message'); + }); + const onErrorFn = vi.fn(); + + // Define the expected error + const expectedError = new ExponentialBackoffStoppedRetriesError(new Error('exponential backoff aborted')); + + // Run the exponential backoff with the function and the onError callback and expect the error to be thrown + await expect(ExponentialBackoff.run(abortAndThrowFn, onErrorFn, { + baseDelay: 0, + jitter: 0, + })).rejects.toThrow(expectedError); + + // Expect the onError callback to have been called once with the error + expect(onErrorFn).toHaveBeenCalledOnce(); + expect(onErrorFn.mock.calls[0][0]).toBeInstanceOf(Error); + expect(onErrorFn.mock.calls[0][0].message).toBe('error message'); + + // Expect the function to have been called once and not to have resolved + expect(abortAndThrowFn).toHaveBeenCalledOnce(); + expect(abortAndThrowFn).not.toHaveResolved(); +}; + +/** + * Tests that when the abort signal is aborted with a string, an ExponentialBackoffStoppedRetriesError is thrown + * with the string as the message. + */ +const testExponentialBackoffRunAbortedStringCreatesError = async (): Promise => { + // Define the function which aborts the exponential backoff and throws an error + const abortAndThrowStringFn = vi.fn(({ stopRetries }) => { + stopRetries('exponential backoff aborted'); + + // eslint-disable-next-line + throw 'error message'; + }); + const onErrorFn = vi.fn(); + + // Define the expected error, Note that we "stopRetries" with just a string, not an error. They are treated equivalently. + const expectedError = new ExponentialBackoffStoppedRetriesError(new Error('exponential backoff aborted')); + + // Run the exponential backoff with the function and the onError callback and expect the error to be thrown + await expect(ExponentialBackoff.run(abortAndThrowStringFn, onErrorFn, { + baseDelay: 0, + jitter: 0, + })).rejects.toThrow(expectedError); + + // Expect the onError callback to have been called once with the error + expect(onErrorFn).toHaveBeenCalledOnce(); + expect(onErrorFn.mock.calls[0][0]).toBeInstanceOf(Error); + expect(onErrorFn.mock.calls[0][0].message).toBe('error message'); + + // Expect the function to have been called once and not to have resolved + expect(abortAndThrowStringFn).toHaveBeenCalledOnce(); + expect(abortAndThrowStringFn).not.toHaveResolved(); +}; + /** * Tests the {@link ExponentialBackoff.from} factory and subsequent instance {@link ExponentialBackoff.run} * as an alternative to the static helper. @@ -486,6 +577,9 @@ const runTests = async (): Promise => { testExponentialBackoffThrowsMaxRetriesHitErrorWhenExhausted, ); test('ExponentialBackoff: wraps non-Error throws before calling onError', testExponentialBackoffWrapsNonErrorThrows); + test('ExponentialBackoff: succeeds and aborts with abort signal', testExponentialBackoffRunSuccessAndAbortSignal); + test('ExponentialBackoff: aborts with abort signal', testExponentialBackoffRunWithAbortSignal); + test('ExponentialBackoff: aborts with aborted string creates error', testExponentialBackoffRunAbortedStringCreatesError); test('ExponentialBackoff: works via from and instance run', testExponentialBackoffFromAndInstanceRun); test('ExponentialBackoff: retries indefinitely when maxAttempts is 0', testExponentialBackoffRetriesIndefinitelyWhenMaxAttemptsIsZero); test('ExponentialBackoff: increases delay exponentially between attempts', testExponentialBackoffIncreasesDelayExponentially);