From c13572958e0defc44e7cfe75ce3322628ad74403 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:41:02 -0800 Subject: [PATCH 1/5] Fix WhenAllTask constructor resetting _completedTasks counter Fixes #131 The WhenAllTask constructor redundantly re-initialized _completedTasks and _failedTasks to 0 after calling super(tasks). Since CompositeTask's constructor already initializes these fields and then processes pre-completed children via onChildCompleted(), the reset wiped out the correct count, causing WhenAllTask to never complete when some children were already complete at construction time. Also removes _failedTasks reset (dead code - never incremented anywhere). Added 8 unit tests for WhenAllTask covering: - Empty task array - All pending children completing - Fail-fast on child failure - Pre-completed children (the bug scenario) - All children pre-completed - Pre-failed child - Post-fail-fast completion - Pending tasks count tracking --- .../durabletask-js/src/task/when-all-task.ts | 7 +- .../durabletask-js/test/when-all-task.spec.ts | 130 ++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 packages/durabletask-js/test/when-all-task.spec.ts diff --git a/packages/durabletask-js/src/task/when-all-task.ts b/packages/durabletask-js/src/task/when-all-task.ts index e0646d5..1b13f2c 100644 --- a/packages/durabletask-js/src/task/when-all-task.ts +++ b/packages/durabletask-js/src/task/when-all-task.ts @@ -11,8 +11,11 @@ export class WhenAllTask extends CompositeTask { constructor(tasks: Task[]) { super(tasks); - this._completedTasks = 0; - this._failedTasks = 0; + // Note: Do NOT re-initialize _completedTasks or _failedTasks here. + // CompositeTask's constructor already initializes them to 0 and then + // processes pre-completed children via onChildCompleted(), which + // increments the counter. Re-initializing would wipe out that count + // and cause the task to hang when some children are already complete. // An empty task list should complete immediately with an empty result if (tasks.length === 0) { diff --git a/packages/durabletask-js/test/when-all-task.spec.ts b/packages/durabletask-js/test/when-all-task.spec.ts new file mode 100644 index 0000000..d499ae6 --- /dev/null +++ b/packages/durabletask-js/test/when-all-task.spec.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { WhenAllTask } from "../src/task/when-all-task"; +import { CompletableTask } from "../src/task/completable-task"; +import { Task } from "../src/task/task"; + +describe("WhenAllTask", () => { + it("should complete immediately when given an empty task array", () => { + const task = new WhenAllTask([]); + + expect(task.isComplete).toBe(true); + expect(task.isFailed).toBe(false); + expect(task.getResult()).toEqual([]); + }); + + it("should complete when all pending children complete", () => { + const child1 = new CompletableTask(); + const child2 = new CompletableTask(); + const task = new WhenAllTask([child1, child2]); + + expect(task.isComplete).toBe(false); + + child1.complete(1); + expect(task.isComplete).toBe(false); + + child2.complete(2); + expect(task.isComplete).toBe(true); + expect(task.isFailed).toBe(false); + expect(task.getResult()).toEqual([1, 2]); + }); + + it("should fail fast when any child fails", () => { + const child1 = new CompletableTask(); + const child2 = new CompletableTask(); + const task = new WhenAllTask([child1, child2]); + + child1.fail("child failed"); + + expect(task.isComplete).toBe(true); + expect(task.isFailed).toBe(true); + expect(task.getException()).toBeDefined(); + }); + + // Issue #131: WhenAllTask constructor resets _completedTasks counter + it("should complete correctly when constructed with pre-completed children", () => { + const child1 = new CompletableTask(); + const child2 = new CompletableTask(); + const child3 = new CompletableTask(); + + // Complete child1 and child2 before constructing WhenAllTask + child1.complete(10); + child2.complete(20); + + const task = new WhenAllTask([child1, child2, child3]); + + // 2 of 3 children already complete — task should not be complete yet + expect(task.isComplete).toBe(false); + expect(task.completedTasks).toBe(2); + + // Complete the last child + child3.complete(30); + + expect(task.isComplete).toBe(true); + expect(task.isFailed).toBe(false); + expect(task.getResult()).toEqual([10, 20, 30]); + }); + + it("should complete immediately when all children are pre-completed", () => { + const child1 = new CompletableTask(); + const child2 = new CompletableTask(); + + child1.complete(1); + child2.complete(2); + + const task = new WhenAllTask([child1, child2]); + + expect(task.isComplete).toBe(true); + expect(task.isFailed).toBe(false); + expect(task.completedTasks).toBe(2); + expect(task.getResult()).toEqual([1, 2]); + }); + + it("should fail immediately when a pre-completed child is failed", () => { + const child1 = new CompletableTask(); + const child2 = new CompletableTask(); + + child1.fail("pre-failed"); + + const task = new WhenAllTask([child1, child2]); + + expect(task.isComplete).toBe(true); + expect(task.isFailed).toBe(true); + expect(task.getException()).toBeDefined(); + }); + + it("should not double-complete when child completes after fail-fast", () => { + const child1 = new CompletableTask(); + const child2 = new CompletableTask(); + const task = new WhenAllTask([child1, child2]); + + child1.fail("first failure"); + + expect(task.isComplete).toBe(true); + expect(task.isFailed).toBe(true); + + // Completing child2 after fail-fast should not change the result + child2.complete(2); + expect(task.isFailed).toBe(true); + expect(task.getException()).toBeDefined(); + }); + + it("should report correct pending tasks count", () => { + const child1 = new CompletableTask(); + const child2 = new CompletableTask(); + const child3 = new CompletableTask(); + + child1.complete(1); + + const task = new WhenAllTask([child1, child2, child3]); + + expect(task.pendingTasks()).toBe(2); + + child2.complete(2); + expect(task.pendingTasks()).toBe(1); + + child3.complete(3); + expect(task.pendingTasks()).toBe(0); + }); +}); From c62c02962db1ed66a8f06b5cb3026130fed26c69 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:44:14 -0800 Subject: [PATCH 2/5] fix: use deterministic time in createTimer instead of Date.now() Replace Date.now() with this._currentUtcDatetime.getTime() in RuntimeOrchestrationContext.createTimer() to ensure the timer fire-at time is computed deterministically from the orchestration time rather than wall clock time. This fixes a determinism contract violation during replay. Fixes #134 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../durabletask-js/src/worker/runtime-orchestration-context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts index 3a71007..a14f5e2 100644 --- a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts +++ b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts @@ -297,7 +297,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { // If a number is passed, we use it as the number of seconds to wait // we use instanceof Date as number is not a native Javascript type if (!(fireAt instanceof Date)) { - fireAt = new Date(Date.now() + fireAt * 1000); + fireAt = new Date(this._currentUtcDatetime.getTime() + fireAt * 1000); } const action = ph.newCreateTimerAction(id, fireAt); From 66ce0fd0cc90148dfa989e61dd906ccba08b6e0a Mon Sep 17 00:00:00 2001 From: wangbill Date: Sun, 8 Mar 2026 19:04:50 -0700 Subject: [PATCH 3/5] test: add e2e test for createTimer(seconds) determinism fix Validates that createTimer(seconds) uses deterministic orchestration time (ctx.currentUtcDateTime) instead of Date.now() during replay. The test uses activities before and after a relative timer to force replays, ensuring the timer fire-at time is computed consistently. Refs #134 --- test/e2e-azuremanaged/orchestration.spec.ts | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/e2e-azuremanaged/orchestration.spec.ts b/test/e2e-azuremanaged/orchestration.spec.ts index 16f9ee9..7c50aec 100644 --- a/test/e2e-azuremanaged/orchestration.spec.ts +++ b/test/e2e-azuremanaged/orchestration.spec.ts @@ -385,6 +385,68 @@ describe("Durable Task Scheduler (DTS) E2E Tests", () => { expect(expectedCompletionSecond).toBeLessThanOrEqual(actualCompletionSecond); }, 31000); + it("should use deterministic time for createTimer(seconds) across replays", async () => { + // Issue #134: createTimer(seconds) used Date.now() instead of ctx.currentUtcDateTime, + // violating the determinism contract. During replay, Date.now() returns a different + // wall-clock time, producing a different timer fire-at value. The fix uses the + // orchestration's deterministic time (ctx.currentUtcDateTime) instead. + // + // This test validates the fix by: + // 1. Using createTimer(seconds) with an activity before and after it + // 2. The activity after the timer forces a replay of the timer yield + // 3. If the timer fire-at time were non-deterministic, the replay would either + // throw a NonDeterminismError or produce incorrect behavior + const sayHello = async (_: ActivityContext, name: string) => `Hello, ${name}!`; + + const timerDeterminismOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + // Record orchestration time BEFORE the timer + const timeBefore = ctx.currentUtcDateTime.toISOString(); + + // Call an activity to force a replay after the timer + const greeting1: string = yield ctx.callActivity(sayHello, "before-timer"); + + // Use createTimer with a relative seconds value (the code path being tested) + const timerDelay = 2; + yield ctx.createTimer(timerDelay); + + // Record orchestration time AFTER the timer + const timeAfter = ctx.currentUtcDateTime.toISOString(); + + // Call another activity — this forces another replay that re-executes + // the createTimer(seconds) path with the replayed history + const greeting2: string = yield ctx.callActivity(sayHello, "after-timer"); + + return { + timeBefore, + timeAfter, + greeting1, + greeting2, + timerDelay, + }; + }; + + taskHubWorker.addOrchestrator(timerDeterminismOrchestrator); + taskHubWorker.addActivity(sayHello); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(timerDeterminismOrchestrator); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.failureDetails).toBeUndefined(); + + const output = JSON.parse(state?.serializedOutput ?? "{}"); + expect(output.greeting1).toEqual("Hello, before-timer!"); + expect(output.greeting2).toEqual("Hello, after-timer!"); + expect(output.timerDelay).toEqual(2); + + // Verify time progressed (timeAfter should be at least timerDelay seconds after timeBefore) + const before = new Date(output.timeBefore).getTime(); + const after = new Date(output.timeAfter).getTime(); + expect(after).toBeGreaterThanOrEqual(before + 2000); + }, 45000); + it("should be able to terminate an orchestration", async () => { const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, _: any): any { const res = yield ctx.waitForExternalEvent("my_event"); From 53861714eb5a16378f1606d7df508c8e816f99a8 Mon Sep 17 00:00:00 2001 From: wangbill Date: Sun, 8 Mar 2026 19:09:05 -0700 Subject: [PATCH 4/5] fix: add @opentelemetry/api as a dev dependency and remove unused import in when-all-task.spec.ts --- package-lock.json | 16 ++++++++++++++++ package.json | 3 ++- .../durabletask-js/test/when-all-task.spec.ts | 1 - 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c302015..3a7c674 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ ], "devDependencies": { "@eslint/js": "^9.39.2", + "@opentelemetry/api": "^1.9.0", "@swc/core": "^1.3.55", "@swc/helpers": "^0.5.1", "@types/jest": "^29.5.1", @@ -226,6 +227,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -897,6 +899,7 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -1493,6 +1496,7 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -1669,6 +1673,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -1884,6 +1889,7 @@ "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -2045,6 +2051,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2118,6 +2125,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -2359,6 +2367,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2683,6 +2692,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3330,6 +3340,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4245,6 +4256,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -5991,6 +6003,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6715,6 +6728,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6866,6 +6880,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -6952,6 +6967,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index b2f8d66..11dd682 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "homepage": "https://github.com/microsoft/durabletask-js#readme", "devDependencies": { "@eslint/js": "^9.39.2", + "@opentelemetry/api": "^1.9.0", "@swc/core": "^1.3.55", "@swc/helpers": "^0.5.1", "@types/jest": "^29.5.1", @@ -62,4 +63,4 @@ "lint-staged": { "*.{js,jsx,ts,tsx}": "eslint" } -} \ No newline at end of file +} diff --git a/packages/durabletask-js/test/when-all-task.spec.ts b/packages/durabletask-js/test/when-all-task.spec.ts index d499ae6..a9e4b97 100644 --- a/packages/durabletask-js/test/when-all-task.spec.ts +++ b/packages/durabletask-js/test/when-all-task.spec.ts @@ -3,7 +3,6 @@ import { WhenAllTask } from "../src/task/when-all-task"; import { CompletableTask } from "../src/task/completable-task"; -import { Task } from "../src/task/task"; describe("WhenAllTask", () => { it("should complete immediately when given an empty task array", () => { From 0c49a94be1b76e2f1aa206545a9f019fbdc1d8d9 Mon Sep 17 00:00:00 2001 From: wangbill Date: Sun, 8 Mar 2026 19:13:02 -0700 Subject: [PATCH 5/5] revert: remove WhenAllTask changes to keep PR focused on createTimer fix --- .../durabletask-js/src/task/when-all-task.ts | 7 +- .../durabletask-js/test/when-all-task.spec.ts | 129 ------------------ 2 files changed, 2 insertions(+), 134 deletions(-) delete mode 100644 packages/durabletask-js/test/when-all-task.spec.ts diff --git a/packages/durabletask-js/src/task/when-all-task.ts b/packages/durabletask-js/src/task/when-all-task.ts index 1b13f2c..e0646d5 100644 --- a/packages/durabletask-js/src/task/when-all-task.ts +++ b/packages/durabletask-js/src/task/when-all-task.ts @@ -11,11 +11,8 @@ export class WhenAllTask extends CompositeTask { constructor(tasks: Task[]) { super(tasks); - // Note: Do NOT re-initialize _completedTasks or _failedTasks here. - // CompositeTask's constructor already initializes them to 0 and then - // processes pre-completed children via onChildCompleted(), which - // increments the counter. Re-initializing would wipe out that count - // and cause the task to hang when some children are already complete. + this._completedTasks = 0; + this._failedTasks = 0; // An empty task list should complete immediately with an empty result if (tasks.length === 0) { diff --git a/packages/durabletask-js/test/when-all-task.spec.ts b/packages/durabletask-js/test/when-all-task.spec.ts deleted file mode 100644 index a9e4b97..0000000 --- a/packages/durabletask-js/test/when-all-task.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { WhenAllTask } from "../src/task/when-all-task"; -import { CompletableTask } from "../src/task/completable-task"; - -describe("WhenAllTask", () => { - it("should complete immediately when given an empty task array", () => { - const task = new WhenAllTask([]); - - expect(task.isComplete).toBe(true); - expect(task.isFailed).toBe(false); - expect(task.getResult()).toEqual([]); - }); - - it("should complete when all pending children complete", () => { - const child1 = new CompletableTask(); - const child2 = new CompletableTask(); - const task = new WhenAllTask([child1, child2]); - - expect(task.isComplete).toBe(false); - - child1.complete(1); - expect(task.isComplete).toBe(false); - - child2.complete(2); - expect(task.isComplete).toBe(true); - expect(task.isFailed).toBe(false); - expect(task.getResult()).toEqual([1, 2]); - }); - - it("should fail fast when any child fails", () => { - const child1 = new CompletableTask(); - const child2 = new CompletableTask(); - const task = new WhenAllTask([child1, child2]); - - child1.fail("child failed"); - - expect(task.isComplete).toBe(true); - expect(task.isFailed).toBe(true); - expect(task.getException()).toBeDefined(); - }); - - // Issue #131: WhenAllTask constructor resets _completedTasks counter - it("should complete correctly when constructed with pre-completed children", () => { - const child1 = new CompletableTask(); - const child2 = new CompletableTask(); - const child3 = new CompletableTask(); - - // Complete child1 and child2 before constructing WhenAllTask - child1.complete(10); - child2.complete(20); - - const task = new WhenAllTask([child1, child2, child3]); - - // 2 of 3 children already complete — task should not be complete yet - expect(task.isComplete).toBe(false); - expect(task.completedTasks).toBe(2); - - // Complete the last child - child3.complete(30); - - expect(task.isComplete).toBe(true); - expect(task.isFailed).toBe(false); - expect(task.getResult()).toEqual([10, 20, 30]); - }); - - it("should complete immediately when all children are pre-completed", () => { - const child1 = new CompletableTask(); - const child2 = new CompletableTask(); - - child1.complete(1); - child2.complete(2); - - const task = new WhenAllTask([child1, child2]); - - expect(task.isComplete).toBe(true); - expect(task.isFailed).toBe(false); - expect(task.completedTasks).toBe(2); - expect(task.getResult()).toEqual([1, 2]); - }); - - it("should fail immediately when a pre-completed child is failed", () => { - const child1 = new CompletableTask(); - const child2 = new CompletableTask(); - - child1.fail("pre-failed"); - - const task = new WhenAllTask([child1, child2]); - - expect(task.isComplete).toBe(true); - expect(task.isFailed).toBe(true); - expect(task.getException()).toBeDefined(); - }); - - it("should not double-complete when child completes after fail-fast", () => { - const child1 = new CompletableTask(); - const child2 = new CompletableTask(); - const task = new WhenAllTask([child1, child2]); - - child1.fail("first failure"); - - expect(task.isComplete).toBe(true); - expect(task.isFailed).toBe(true); - - // Completing child2 after fail-fast should not change the result - child2.complete(2); - expect(task.isFailed).toBe(true); - expect(task.getException()).toBeDefined(); - }); - - it("should report correct pending tasks count", () => { - const child1 = new CompletableTask(); - const child2 = new CompletableTask(); - const child3 = new CompletableTask(); - - child1.complete(1); - - const task = new WhenAllTask([child1, child2, child3]); - - expect(task.pendingTasks()).toBe(2); - - child2.complete(2); - expect(task.pendingTasks()).toBe(1); - - child3.complete(3); - expect(task.pendingTasks()).toBe(0); - }); -});