From 3f9eb9d96ce8d1729931511edb91ed436638131b Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:57:58 +0000 Subject: [PATCH] [copilot-finds] Test: Add unit tests for Task and CompletableTask base classes Add comprehensive unit tests for the foundational Task and CompletableTask classes which previously had zero direct test coverage. Tests cover: - Task: getResult() before completion, after failure, after success - Task: getException() on non-failed and failed tasks - Task: isComplete and isFailed state tracking - CompletableTask: complete() with various result types (null, undefined) - CompletableTask: fail() with and without explicit TaskFailureDetails - CompletableTask: double-completion and cross-state error guards - CompletableTask: parent notification via onChildCompleted() Fixes #232 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/durabletask-js/test/task.spec.ts | 261 ++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 packages/durabletask-js/test/task.spec.ts diff --git a/packages/durabletask-js/test/task.spec.ts b/packages/durabletask-js/test/task.spec.ts new file mode 100644 index 0000000..e5bd53c --- /dev/null +++ b/packages/durabletask-js/test/task.spec.ts @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Task } from "../src/task/task"; +import { CompletableTask } from "../src/task/completable-task"; +import { TaskFailedError } from "../src/task/exception/task-failed-error"; +import { TaskFailureDetails } from "../src/proto/orchestrator_service_pb"; +import { StringValue } from "google-protobuf/google/protobuf/wrappers_pb"; +import { WhenAllTask } from "../src/task/when-all-task"; + +/** + * Helper to create a TaskFailureDetails protobuf message. + */ +function makeFailureDetails( + errorMessage = "test error", + errorType = "TestError", + stackTrace?: string, +): TaskFailureDetails { + const details = new TaskFailureDetails(); + details.setErrormessage(errorMessage); + details.setErrortype(errorType); + if (stackTrace !== undefined) { + const sv = new StringValue(); + sv.setValue(stackTrace); + details.setStacktrace(sv); + } + return details; +} + +describe("Task (base class)", () => { + // Task is not abstract, so we can instantiate it directly for testing + // its base-class behavior. + + it("should start as incomplete and not failed", () => { + const task = new Task(); + expect(task.isComplete).toBe(false); + expect(task.isFailed).toBe(false); + }); + + describe("getResult()", () => { + it("should throw when task is not complete", () => { + const task = new Task(); + expect(() => task.getResult()).toThrow("Task is not complete"); + }); + + it("should re-throw the stored exception when task has failed", () => { + const task = new Task(); + const details = makeFailureDetails("activity failed", "ActivityError"); + task._exception = new TaskFailedError("activity failed", details); + task._isComplete = true; + + expect(() => task.getResult()).toThrow(TaskFailedError); + expect(() => task.getResult()).toThrow("activity failed"); + }); + + it("should return the result when task completed successfully", () => { + const task = new Task(); + task._result = 42; + task._isComplete = true; + + expect(task.getResult()).toBe(42); + }); + + it("should return undefined as a valid result", () => { + const task = new Task(); + task._result = undefined; + task._isComplete = true; + + expect(task.getResult()).toBeUndefined(); + }); + }); + + describe("getException()", () => { + it("should throw when task did not fail", () => { + const task = new Task(); + expect(() => task.getException()).toThrow("Task did not fail"); + }); + + it("should throw 'Task did not fail' even when task completed successfully", () => { + const task = new Task(); + task._result = "ok"; + task._isComplete = true; + + expect(() => task.getException()).toThrow("Task did not fail"); + }); + + it("should return the exception when task has failed", () => { + const task = new Task(); + const details = makeFailureDetails("boom", "RuntimeError"); + const error = new TaskFailedError("boom", details); + task._exception = error; + task._isComplete = true; + + expect(task.getException()).toBe(error); + }); + }); + + describe("isFailed", () => { + it("should be false when no exception is set", () => { + const task = new Task(); + expect(task.isFailed).toBe(false); + }); + + it("should be true when an exception is set", () => { + const task = new Task(); + task._exception = new TaskFailedError("err", makeFailureDetails()); + expect(task.isFailed).toBe(true); + }); + }); +}); + +describe("CompletableTask", () => { + describe("complete()", () => { + it("should mark the task as complete with the given result", () => { + const task = new CompletableTask(); + task.complete(42); + + expect(task.isComplete).toBe(true); + expect(task.isFailed).toBe(false); + expect(task.getResult()).toBe(42); + }); + + it("should accept null as a valid result", () => { + const task = new CompletableTask(); + task.complete(null); + + expect(task.isComplete).toBe(true); + expect(task.getResult()).toBeNull(); + }); + + it("should accept undefined as a valid result", () => { + const task = new CompletableTask(); + task.complete(undefined); + + expect(task.isComplete).toBe(true); + expect(task.getResult()).toBeUndefined(); + }); + + it("should throw on double completion", () => { + const task = new CompletableTask(); + task.complete("first"); + + expect(() => task.complete("second")).toThrow("Task is already completed"); + }); + + it("should throw when completing a task that already failed", () => { + const task = new CompletableTask(); + task.fail("something broke"); + + expect(() => task.complete("result")).toThrow("Task is already completed"); + }); + + it("should notify parent via onChildCompleted when parent is set", () => { + const child = new CompletableTask(); + // Use WhenAllTask as the parent since CompositeTask is abstract + const parent = new WhenAllTask([child]); + + expect(parent.isComplete).toBe(false); + + child.complete(10); + + // Parent should now be complete because its only child completed + expect(parent.isComplete).toBe(true); + expect(parent.getResult()).toEqual([10]); + }); + + it("should complete without error when no parent is set", () => { + const task = new CompletableTask(); + // No parent set — complete() should not throw + task.complete("standalone"); + + expect(task.isComplete).toBe(true); + expect(task.getResult()).toBe("standalone"); + }); + }); + + describe("fail()", () => { + it("should mark the task as failed with the given message", () => { + const task = new CompletableTask(); + task.fail("something went wrong"); + + expect(task.isComplete).toBe(true); + expect(task.isFailed).toBe(true); + }); + + it("should set a TaskFailedError as the exception", () => { + const task = new CompletableTask(); + task.fail("operation failed"); + + const exception = task.getException(); + expect(exception).toBeInstanceOf(TaskFailedError); + expect(exception.message).toBe("operation failed"); + }); + + it("should use provided TaskFailureDetails", () => { + const task = new CompletableTask(); + const details = makeFailureDetails("detailed error", "CustomError", "at line 42"); + task.fail("detailed error", details); + + const exception = task.getException(); + expect(exception.details.message).toBe("detailed error"); + expect(exception.details.errorType).toBe("CustomError"); + expect(exception.details.stackTrace).toBe("at line 42"); + }); + + it("should create default TaskFailureDetails when none provided", () => { + const task = new CompletableTask(); + task.fail("no details"); + + const exception = task.getException(); + // Default TaskFailureDetails has empty strings for message and errorType + expect(exception.details.message).toBe(""); + expect(exception.details.errorType).toBe(""); + expect(exception.details.stackTrace).toBeUndefined(); + }); + + it("should throw on double failure", () => { + const task = new CompletableTask(); + task.fail("first failure"); + + expect(() => task.fail("second failure")).toThrow("Task is already completed"); + }); + + it("should throw when failing a task that already completed", () => { + const task = new CompletableTask(); + task.complete("success"); + + expect(() => task.fail("too late")).toThrow("Task is already completed"); + }); + + it("should cause getResult() to throw the stored exception", () => { + const task = new CompletableTask(); + task.fail("activity crashed"); + + expect(() => task.getResult()).toThrow(TaskFailedError); + expect(() => task.getResult()).toThrow("activity crashed"); + }); + + it("should notify parent via onChildCompleted when parent is set", () => { + const child = new CompletableTask(); + const parent = new WhenAllTask([child]); + + expect(parent.isComplete).toBe(false); + + child.fail("child failed"); + + // WhenAllTask fails fast on first child failure + expect(parent.isComplete).toBe(true); + expect(parent.isFailed).toBe(true); + }); + + it("should fail without error when no parent is set", () => { + const task = new CompletableTask(); + // No parent set — fail() should not throw + task.fail("standalone failure"); + + expect(task.isComplete).toBe(true); + expect(task.isFailed).toBe(true); + }); + }); +});