From 7236490a900861b55a7666803bcc5e51776ecd1c Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 08:46:12 +0000 Subject: [PATCH] test: add missing RetryableTask coverage for non-retriable, remaining-timeout, and complete() override Add three new test cases to retryable-task.spec.ts covering previously untested code paths: 1. Non-retriable failure guard (computeNextDelayInMilliseconds line 63-65): Verifies that failures marked isNonRetriable=true immediately return undefined, preventing retries of fundamentally broken operations like unregistered activities. 2. Delay-exceeds-remaining-timeout guard (lines 106-113): Verifies that when elapsed time is within the timeout but the computed retry delay would overshoot the remaining window, no retry is scheduled. This is distinct from the existing elapsed-timeout test which checks lines 86-91. 3. complete() override from RetryTaskBase (lines 129-136): Verifies that successful retry completion clears _exception and _lastFailure, ensuring stale failure state does not leak into the completed task. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/retryable-task.spec.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/packages/durabletask-js/test/retryable-task.spec.ts b/packages/durabletask-js/test/retryable-task.spec.ts index be72860..78b5011 100644 --- a/packages/durabletask-js/test/retryable-task.spec.ts +++ b/packages/durabletask-js/test/retryable-task.spec.ts @@ -296,6 +296,53 @@ describe("RetryableTask", () => { expect(delay).toBeDefined(); expect(delay).toBeGreaterThan(0); }); + + it("should return undefined when last failure is marked non-retriable", () => { + // Arrange + const retryPolicy = new RetryPolicy({ + maxNumberOfAttempts: 10, + firstRetryIntervalInMilliseconds: 1000, + }); + const action = new pb.OrchestratorAction(); + const startTime = new Date(); + const task = new RetryableTask(retryPolicy, action, startTime, "activity"); + + // Record a non-retriable failure (e.g., activity not found, version mismatch) + const failureDetails = new pb.TaskFailureDetails(); + failureDetails.setErrortype("ActivityNotFoundError"); + failureDetails.setErrormessage("Activity 'DoWork' is not registered"); + failureDetails.setIsnonretriable(true); + task.recordFailure("Activity 'DoWork' is not registered", failureDetails); + + const currentTime = new Date(startTime.getTime() + 100); + + // Act + const delay = task.computeNextDelayInMilliseconds(currentTime); + + // Assert - non-retriable failures must not be retried regardless of policy + expect(delay).toBeUndefined(); + }); + + it("should return undefined when computed delay exceeds remaining timeout", () => { + // Arrange - timeout is 5000ms, elapsed is 4500ms, so remaining is 500ms. + // With firstRetryInterval of 1000ms, the computed delay (1000) > remaining (500). + const retryPolicy = new RetryPolicy({ + maxNumberOfAttempts: 10, + firstRetryIntervalInMilliseconds: 1000, + retryTimeoutInMilliseconds: 5000, + }); + const startTime = new Date(); + const action = new pb.OrchestratorAction(); + const task = new RetryableTask(retryPolicy, action, startTime, "activity"); + // 4500ms elapsed — within the 5000ms timeout, but delay (1000ms) exceeds remaining (500ms) + const currentTime = new Date(startTime.getTime() + 4500); + + // Act + const delay = task.computeNextDelayInMilliseconds(currentTime); + + // Assert - delay would overshoot the timeout, so no more retries + expect(delay).toBeUndefined(); + }); }); describe("incrementAttemptCount", () => { @@ -361,6 +408,36 @@ describe("RetryableTask", () => { }); }); + describe("complete", () => { + it("should clear failure state when retry succeeds", () => { + // Arrange + const retryPolicy = new RetryPolicy({ + maxNumberOfAttempts: 5, + firstRetryIntervalInMilliseconds: 1000, + }); + const action = new pb.OrchestratorAction(); + const task = new RetryableTask(retryPolicy, action, new Date(), "activity"); + + // Simulate a failed attempt followed by a successful retry + const failureDetails = new pb.TaskFailureDetails(); + failureDetails.setErrortype("TransientError"); + failureDetails.setErrormessage("Temporary failure"); + task.recordFailure("Temporary failure", failureDetails); + + // Verify failure is recorded + expect(task.lastFailure).toBeDefined(); + + // Act - retry succeeds + task.complete("success"); + + // Assert - failure state is cleared, result is set + expect(task.lastFailure).toBeUndefined(); + expect(task.isFailed).toBe(false); + expect(task.getResult()).toBe("success"); + expect(task.isComplete).toBe(true); + }); + }); + describe("task types", () => { it("should support activity task type", () => { // Arrange