From 15bb7dec5bf7aa05ea078b714135761215e08900 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:34:27 +0000 Subject: [PATCH 1/2] fix: validate first yielded value in orchestrator run() method The run() method in RuntimeOrchestrationContext did not validate that the first value yielded by an orchestrator generator is a Task instance. The resume() method already validates subsequent yields with an instanceof check, but run() silently accepted non-Task values (null, undefined, plain objects, primitives), causing the orchestration to hang indefinitely with no error message. This adds the same validation to run() that already exists in resume(), throwing a clear error when a non-Task value is yielded. This also removes the now-addressed TODO comment and the redundant instanceof guard on the isComplete check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../worker/runtime-orchestration-context.ts | 10 ++- .../test/orchestration_executor.spec.ts | 76 +++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts index a14f5e2..6455e96 100644 --- a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts +++ b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts @@ -109,8 +109,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { async run(generator: Generator, any, any>) { this._generator = generator; - // TODO: do something with this task - // start the generator + // Start the generator const { value, done } = await this._generator.next(); // if the generator finished, complete the orchestration. @@ -119,12 +118,15 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { return; } - // TODO: check if the task is null? + if (!(value instanceof Task)) { + throw new Error("The orchestrator generator yielded a non-Task object"); + } + this._previousTask = value; // If the yielded task is already complete (e.g., whenAll with an empty array), // resume immediately so the generator can continue. - if (this._previousTask instanceof Task && this._previousTask.isComplete) { + if (this._previousTask.isComplete) { await this.resume(); } } diff --git a/packages/durabletask-js/test/orchestration_executor.spec.ts b/packages/durabletask-js/test/orchestration_executor.spec.ts index c4cd604..0871386 100644 --- a/packages/durabletask-js/test/orchestration_executor.spec.ts +++ b/packages/durabletask-js/test/orchestration_executor.spec.ts @@ -1549,6 +1549,82 @@ describe("Orchestration Executor", () => { expect(completeAction?.getResult()?.getValue()).toEqual(JSON.stringify(expectedResult)); }); + it("should fail when orchestrator yields null as its first value", async () => { + // An orchestrator that yields null instead of a Task should fail with a clear error + const badOrchestrator: TOrchestrator = async function* (_ctx: OrchestrationContext): any { + yield null; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(badOrchestrator); + const newEvents = [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), + ]; + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + const completeAction = getAndValidateSingleCompleteOrchestrationAction(result); + expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED); + expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("non-Task"); + }); + + it("should fail when orchestrator yields undefined as its first value", async () => { + // An orchestrator that yields undefined instead of a Task should fail with a clear error + const badOrchestrator: TOrchestrator = async function* (_ctx: OrchestrationContext): any { + yield undefined; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(badOrchestrator); + const newEvents = [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), + ]; + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + const completeAction = getAndValidateSingleCompleteOrchestrationAction(result); + expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED); + expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("non-Task"); + }); + + it("should fail when orchestrator yields a plain object as its first value", async () => { + // An orchestrator that yields a non-Task object should fail with a clear error + const badOrchestrator: TOrchestrator = async function* (_ctx: OrchestrationContext): any { + yield { someProperty: "not a task" }; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(badOrchestrator); + const newEvents = [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), + ]; + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + const completeAction = getAndValidateSingleCompleteOrchestrationAction(result); + expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED); + expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("non-Task"); + }); + + it("should fail when orchestrator yields a primitive as its first value", async () => { + // An orchestrator that yields a primitive (number) instead of a Task should fail + const badOrchestrator: TOrchestrator = async function* (_ctx: OrchestrationContext): any { + yield 42; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(badOrchestrator); + const newEvents = [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), + ]; + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + const completeAction = getAndValidateSingleCompleteOrchestrationAction(result); + expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED); + expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("non-Task"); + }); + it("should propagate inner whenAll failure to outer whenAny in nested composites", async () => { const hello = (_: any, name: string) => { return `Hello ${name}!`; From 56d31f517abf2c0de04b17bbb58f6b7b57ee47d0 Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 11 Mar 2026 11:58:50 -0700 Subject: [PATCH 2/2] Update packages/durabletask-js/test/orchestration_executor.spec.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../test/orchestration_executor.spec.ts | 59 ++++++++----------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/packages/durabletask-js/test/orchestration_executor.spec.ts b/packages/durabletask-js/test/orchestration_executor.spec.ts index 0871386..e2851b8 100644 --- a/packages/durabletask-js/test/orchestration_executor.spec.ts +++ b/packages/durabletask-js/test/orchestration_executor.spec.ts @@ -1549,43 +1549,30 @@ describe("Orchestration Executor", () => { expect(completeAction?.getResult()?.getValue()).toEqual(JSON.stringify(expectedResult)); }); - it("should fail when orchestrator yields null as its first value", async () => { - // An orchestrator that yields null instead of a Task should fail with a clear error - const badOrchestrator: TOrchestrator = async function* (_ctx: OrchestrationContext): any { - yield null; - }; - - const registry = new Registry(); - const name = registry.addOrchestrator(badOrchestrator); - const newEvents = [ - newOrchestratorStartedEvent(), - newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), - ]; - const executor = new OrchestrationExecutor(registry, testLogger); - const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); - const completeAction = getAndValidateSingleCompleteOrchestrationAction(result); - expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED); - expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("non-Task"); - }); - - it("should fail when orchestrator yields undefined as its first value", async () => { - // An orchestrator that yields undefined instead of a Task should fail with a clear error - const badOrchestrator: TOrchestrator = async function* (_ctx: OrchestrationContext): any { - yield undefined; - }; + it.each([ + { description: "null", yieldedValue: null as any }, + { description: "undefined", yieldedValue: undefined as any }, + ])( + "should fail when orchestrator yields $description as its first value", + async ({ description, yieldedValue }) => { + // An orchestrator that yields a non-Task value as its first yield should fail with a clear error + const badOrchestrator: TOrchestrator = async function* (_ctx: OrchestrationContext): any { + yield yieldedValue; + }; - const registry = new Registry(); - const name = registry.addOrchestrator(badOrchestrator); - const newEvents = [ - newOrchestratorStartedEvent(), - newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), - ]; - const executor = new OrchestrationExecutor(registry, testLogger); - const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); - const completeAction = getAndValidateSingleCompleteOrchestrationAction(result); - expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED); - expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("non-Task"); - }); + const registry = new Registry(); + const name = registry.addOrchestrator(badOrchestrator); + const newEvents = [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), + ]; + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + const completeAction = getAndValidateSingleCompleteOrchestrationAction(result); + expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED); + expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain("non-Task"); + } + ); it("should fail when orchestrator yields a plain object as its first value", async () => { // An orchestrator that yields a non-Task object should fail with a clear error