From 0e4a2bc111c01b6ab176ea89bc03055b94c126d5 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 08:21:24 +0000 Subject: [PATCH 1/5] Fix falsy input values silently dropped in activity, sub-orchestration, and completion paths Replace truthy checks with explicit !== undefined checks when serializing inputs and results in RuntimeOrchestrationContext. Previously, falsy values like 0, empty string, false, and null were silently dropped because the code used 'if (input)' instead of 'if (input !== undefined)'. Affected paths: - callActivity: input serialization - callSubOrchestrator: input serialization - setComplete: orchestration result serialization - getActions (continue-as-new): newInput and carryover event serialization Note: sendEvent, signalEntity, and callEntity already used the correct !== undefined check and were not affected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../worker/runtime-orchestration-context.ts | 10 +- .../test/falsy-input-serialization.spec.ts | 188 ++++++++++++++++++ 2 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 packages/durabletask-js/test/falsy-input-serialization.spec.ts diff --git a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts index c01e19d..57aedd1 100644 --- a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts +++ b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts @@ -206,7 +206,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { let resultJson; - if (result) { + if (result !== undefined) { resultJson = isResultEncoded ? result : JSON.stringify(result); } @@ -252,7 +252,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { // replayed when the new instance starts for (const [eventName, values] of Object.entries(this._receivedEvents)) { for (const eventValue of values) { - const encodedValue = eventValue ? JSON.stringify(eventValue) : undefined; + const encodedValue = eventValue !== undefined ? JSON.stringify(eventValue) : undefined; carryoverEvents.push(ph.newEventRaisedEvent(eventName, encodedValue)); } } @@ -261,7 +261,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { const action = ph.newCompleteOrchestrationAction( this.nextSequenceNumber(), pb.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW, - this._newInput ? JSON.stringify(this._newInput) : undefined, + this._newInput !== undefined ? JSON.stringify(this._newInput) : undefined, undefined, carryoverEvents, ); @@ -308,7 +308,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { ): Task { const id = this.nextSequenceNumber(); const name = typeof activity === "string" ? activity : getName(activity); - const encodedInput = input ? JSON.stringify(input) : undefined; + const encodedInput = input !== undefined ? JSON.stringify(input) : undefined; const action = ph.newScheduleTaskAction(id, name, encodedInput, options?.tags, options?.version); this._pendingActions[action.getId()] = action; @@ -339,7 +339,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { instanceId = `${this._instanceId}:${instanceIdSuffix}`; } - const encodedInput = input ? JSON.stringify(input) : undefined; + const encodedInput = input !== undefined ? JSON.stringify(input) : undefined; const action = ph.newCreateSubOrchestrationAction(id, name, instanceId, encodedInput, options?.tags, options?.version); this._pendingActions[action.getId()] = action; diff --git a/packages/durabletask-js/test/falsy-input-serialization.spec.ts b/packages/durabletask-js/test/falsy-input-serialization.spec.ts new file mode 100644 index 0000000..ef27cdd --- /dev/null +++ b/packages/durabletask-js/test/falsy-input-serialization.spec.ts @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { OrchestrationContext } from "../src/task/context/orchestration-context"; +import { + newExecutionStartedEvent, + newOrchestratorStartedEvent, +} from "../src/utils/pb-helper.util"; +import { OrchestrationExecutor } from "../src/worker/orchestration-executor"; +import * as pb from "../src/proto/orchestrator_service_pb"; +import { Registry } from "../src/worker/registry"; +import { TOrchestrator } from "../src/types/orchestrator.type"; +import { NoOpLogger } from "../src/types/logger.type"; +import { ActivityContext } from "../src/task/context/activity-context"; + +const testLogger = new NoOpLogger(); +const TEST_INSTANCE_ID = "falsy-test-instance"; + +describe("Falsy input serialization", () => { + describe("callActivity with falsy inputs", () => { + it.each([ + { input: 0, label: "zero" }, + { input: "", label: "empty string" }, + { input: false, label: "false" }, + { input: null, label: "null" }, + ])("should correctly serialize $label as activity input", async ({ input }) => { + const myActivity = async (_ctx: ActivityContext, actInput: any) => actInput; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const result = yield ctx.callActivity(myActivity, input as any); + return result; + }; + + const registry = new Registry(); + const orchestratorName = registry.addOrchestrator(orchestrator); + registry.addActivity(myActivity); + + const newEvents = [ + newOrchestratorStartedEvent(new Date()), + newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // Should have a ScheduleTask action with the serialized input + const scheduleAction = result.actions.find((a) => a.hasScheduletask()); + expect(scheduleAction).toBeDefined(); + const inputValue = scheduleAction!.getScheduletask()!.getInput(); + expect(inputValue).toBeDefined(); + expect(inputValue!.getValue()).toEqual(JSON.stringify(input)); + }); + }); + + describe("callSubOrchestrator with falsy inputs", () => { + it.each([ + { input: 0, label: "zero" }, + { input: "", label: "empty string" }, + { input: false, label: "false" }, + { input: null, label: "null" }, + ])("should correctly serialize $label as sub-orchestration input", async ({ input }) => { + const subOrchestrator: TOrchestrator = async (_ctx: OrchestrationContext, subInput: any) => { + return subInput; + }; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const result = yield ctx.callSubOrchestrator(subOrchestrator, input as any); + return result; + }; + + const registry = new Registry(); + const orchestratorName = registry.addOrchestrator(orchestrator); + registry.addOrchestrator(subOrchestrator); + + const newEvents = [ + newOrchestratorStartedEvent(new Date()), + newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // Should have a CreateSubOrchestration action with the serialized input + const subOrchAction = result.actions.find((a) => a.hasCreatesuborchestration()); + expect(subOrchAction).toBeDefined(); + const inputValue = subOrchAction!.getCreatesuborchestration()!.getInput(); + expect(inputValue).toBeDefined(); + expect(inputValue!.getValue()).toEqual(JSON.stringify(input)); + }); + }); + + describe("orchestration completion with falsy results", () => { + it.each([ + { result: 0, label: "zero" }, + { result: "", label: "empty string" }, + { result: false, label: "false" }, + { result: null, label: "null" }, + ])("should correctly serialize $label as orchestration result", async ({ result }) => { + const orchestrator: TOrchestrator = async (_ctx: OrchestrationContext) => { + return result; + }; + + const registry = new Registry(); + const orchestratorName = registry.addOrchestrator(orchestrator); + + const newEvents = [ + newOrchestratorStartedEvent(new Date()), + newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const execResult = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + expect(execResult.actions.length).toEqual(1); + const completeAction = execResult.actions[0].getCompleteorchestration(); + expect(completeAction).toBeDefined(); + expect(completeAction!.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED, + ); + const resultValue = completeAction!.getResult(); + expect(resultValue).toBeDefined(); + expect(resultValue!.getValue()).toEqual(JSON.stringify(result)); + }); + }); + + describe("continueAsNew with falsy input", () => { + it("should correctly serialize zero as continue-as-new input", async () => { + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, input: any): any { + if (input === 0) { + ctx.continueAsNew(0, false); + yield ctx.createTimer(1); // yield to avoid require-yield lint error + return; + } + return input; + }; + + const registry = new Registry(); + const orchestratorName = registry.addOrchestrator(orchestrator); + + const newEvents = [ + newOrchestratorStartedEvent(new Date()), + newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID, JSON.stringify(0)), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + expect(result.actions.length).toEqual(1); + const completeAction = result.actions[0].getCompleteorchestration(); + expect(completeAction).toBeDefined(); + expect(completeAction!.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW, + ); + const resultValue = completeAction!.getResult(); + expect(resultValue).toBeDefined(); + expect(resultValue!.getValue()).toEqual(JSON.stringify(0)); + }); + }); + + describe("undefined inputs are still treated as no input", () => { + it("should not set input when activity input is undefined", async () => { + const myActivity = async (_ctx: ActivityContext) => "done"; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const result = yield ctx.callActivity(myActivity); + return result; + }; + + const registry = new Registry(); + const orchestratorName = registry.addOrchestrator(orchestrator); + registry.addActivity(myActivity); + + const newEvents = [ + newOrchestratorStartedEvent(new Date()), + newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + const scheduleAction = result.actions.find((a) => a.hasScheduletask()); + expect(scheduleAction).toBeDefined(); + // Input should be undefined/null when not provided + const inputValue = scheduleAction!.getScheduletask()!.getInput(); + expect(inputValue).toBeUndefined(); + }); + }); +}); From ca52b66a2a4c0832482c84ab5834af98bed34d3a Mon Sep 17 00:00:00 2001 From: wangbill Date: Thu, 5 Mar 2026 14:12:54 -0800 Subject: [PATCH 2/5] Update packages/durabletask-js/test/falsy-input-serialization.spec.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/durabletask-js/test/falsy-input-serialization.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/durabletask-js/test/falsy-input-serialization.spec.ts b/packages/durabletask-js/test/falsy-input-serialization.spec.ts index ef27cdd..55e9e77 100644 --- a/packages/durabletask-js/test/falsy-input-serialization.spec.ts +++ b/packages/durabletask-js/test/falsy-input-serialization.spec.ts @@ -180,7 +180,7 @@ describe("Falsy input serialization", () => { const scheduleAction = result.actions.find((a) => a.hasScheduletask()); expect(scheduleAction).toBeDefined(); - // Input should be undefined/null when not provided + // Input should be undefined when not provided const inputValue = scheduleAction!.getScheduletask()!.getInput(); expect(inputValue).toBeUndefined(); }); From 37e5907e4329bef121b59364bee24678e6b08187 Mon Sep 17 00:00:00 2001 From: wangbill Date: Thu, 5 Mar 2026 14:13:23 -0800 Subject: [PATCH 3/5] Update packages/durabletask-js/test/falsy-input-serialization.spec.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/durabletask-js/test/falsy-input-serialization.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/durabletask-js/test/falsy-input-serialization.spec.ts b/packages/durabletask-js/test/falsy-input-serialization.spec.ts index 55e9e77..2dde5df 100644 --- a/packages/durabletask-js/test/falsy-input-serialization.spec.ts +++ b/packages/durabletask-js/test/falsy-input-serialization.spec.ts @@ -125,10 +125,9 @@ describe("Falsy input serialization", () => { describe("continueAsNew with falsy input", () => { it("should correctly serialize zero as continue-as-new input", async () => { - const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, input: any): any { + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext, input: any): any => { if (input === 0) { ctx.continueAsNew(0, false); - yield ctx.createTimer(1); // yield to avoid require-yield lint error return; } return input; From 27b36f04da5c98f034fdf41538ca3fc58da71415 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:30:26 -0800 Subject: [PATCH 4/5] Fix TypeScript error in continueAsNew test: async return type must be Promise --- packages/durabletask-js/test/falsy-input-serialization.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/durabletask-js/test/falsy-input-serialization.spec.ts b/packages/durabletask-js/test/falsy-input-serialization.spec.ts index 2dde5df..5c400fb 100644 --- a/packages/durabletask-js/test/falsy-input-serialization.spec.ts +++ b/packages/durabletask-js/test/falsy-input-serialization.spec.ts @@ -125,7 +125,7 @@ describe("Falsy input serialization", () => { describe("continueAsNew with falsy input", () => { it("should correctly serialize zero as continue-as-new input", async () => { - const orchestrator: TOrchestrator = async (ctx: OrchestrationContext, input: any): any => { + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext, input: any): Promise => { if (input === 0) { ctx.continueAsNew(0, false); return; From 4c3d5efa4e68139cc72301d5a036a4b3e48dfaa7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:52:14 -0800 Subject: [PATCH 5/5] Fix activity output falsy serialization and add e2e tests - Fix activity-executor.ts: activityOutput ? activityOutput !== undefined ? (same truthy bug as the input paths, causing 0, '', false, null to be dropped from activity return values) - Add 5 unit tests for activity output falsy values - Add 9 e2e tests verifying falsy value round-trips against real DTS: - 4 activity round-trips (0, '', false, null) - 1 sub-orchestration round-trip (0) - 3 orchestration results (0, false, '') - 1 continueAsNew with zero input --- .../src/worker/activity-executor.ts | 2 +- .../test/falsy-input-serialization.spec.ts | 32 ++++ test/e2e-azuremanaged/orchestration.spec.ts | 176 ++++++++++++++++++ 3 files changed, 209 insertions(+), 1 deletion(-) diff --git a/packages/durabletask-js/src/worker/activity-executor.ts b/packages/durabletask-js/src/worker/activity-executor.ts index 079b9a0..10ba9a9 100644 --- a/packages/durabletask-js/src/worker/activity-executor.ts +++ b/packages/durabletask-js/src/worker/activity-executor.ts @@ -44,7 +44,7 @@ export class ActivityExecutor { } // Return the output - const encodedOutput = activityOutput ? JSON.stringify(activityOutput) : undefined; + const encodedOutput = activityOutput !== undefined ? JSON.stringify(activityOutput) : undefined; // Log activity completion (EventId 604) WorkerLogs.activityCompleted(this._logger, orchestrationId, name); diff --git a/packages/durabletask-js/test/falsy-input-serialization.spec.ts b/packages/durabletask-js/test/falsy-input-serialization.spec.ts index 5c400fb..9fea017 100644 --- a/packages/durabletask-js/test/falsy-input-serialization.spec.ts +++ b/packages/durabletask-js/test/falsy-input-serialization.spec.ts @@ -12,6 +12,7 @@ import { Registry } from "../src/worker/registry"; import { TOrchestrator } from "../src/types/orchestrator.type"; import { NoOpLogger } from "../src/types/logger.type"; import { ActivityContext } from "../src/task/context/activity-context"; +import { ActivityExecutor } from "../src/worker/activity-executor"; const testLogger = new NoOpLogger(); const TEST_INSTANCE_ID = "falsy-test-instance"; @@ -184,4 +185,35 @@ describe("Falsy input serialization", () => { expect(inputValue).toBeUndefined(); }); }); + + describe("activity output with falsy values", () => { + it.each([ + { output: 0, label: "zero" }, + { output: "", label: "empty string" }, + { output: false, label: "false" }, + { output: null, label: "null" }, + ])("should correctly serialize $label as activity output", async ({ output }) => { + const myActivity = async (_ctx: ActivityContext) => output; + + const registry = new Registry(); + registry.addActivity(myActivity); + + const executor = new ActivityExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, "myActivity", 1); + + expect(result).toEqual(JSON.stringify(output)); + }); + + it("should return undefined when activity output is undefined", async () => { + const myActivity = async (_ctx: ActivityContext) => undefined; + + const registry = new Registry(); + registry.addActivity(myActivity); + + const executor = new ActivityExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, "myActivity", 1); + + expect(result).toBeUndefined(); + }); + }); }); diff --git a/test/e2e-azuremanaged/orchestration.spec.ts b/test/e2e-azuremanaged/orchestration.spec.ts index 40d7cc3..a11428a 100644 --- a/test/e2e-azuremanaged/orchestration.spec.ts +++ b/test/e2e-azuremanaged/orchestration.spec.ts @@ -1373,4 +1373,180 @@ describe("Durable Task Scheduler (DTS) E2E Tests", () => { expect(parentStateAfterPurge).toBeUndefined(); }, 60000); }); + + // PR #138: Fix falsy values (0, "", false, null) silently dropped during serialization + describe("falsy value serialization", () => { + it("should pass zero (0) through activity round-trip", async () => { + const echo = async (_: ActivityContext, input: number) => input; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, input: number): any { + const result = yield ctx.callActivity(echo, input); + return result; + }; + + taskHubWorker.addOrchestrator(orchestrator); + taskHubWorker.addActivity(echo); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(orchestrator, 0); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.serializedInput).toEqual(JSON.stringify(0)); + expect(state?.serializedOutput).toEqual(JSON.stringify(0)); + }, 31000); + + it("should pass empty string through activity round-trip", async () => { + const echo = async (_: ActivityContext, input: string) => input; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, input: string): any { + const result = yield ctx.callActivity(echo, input); + return result; + }; + + taskHubWorker.addOrchestrator(orchestrator); + taskHubWorker.addActivity(echo); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(orchestrator, ""); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.serializedInput).toEqual(JSON.stringify("")); + expect(state?.serializedOutput).toEqual(JSON.stringify("")); + }, 31000); + + it("should pass false through activity round-trip", async () => { + const echo = async (_: ActivityContext, input: boolean) => input; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, input: boolean): any { + const result = yield ctx.callActivity(echo, input); + return result; + }; + + taskHubWorker.addOrchestrator(orchestrator); + taskHubWorker.addActivity(echo); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(orchestrator, false); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.serializedInput).toEqual(JSON.stringify(false)); + expect(state?.serializedOutput).toEqual(JSON.stringify(false)); + }, 31000); + + it("should pass null through activity round-trip", async () => { + const echo = async (_: ActivityContext, input: any) => input; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, input: any): any { + const result = yield ctx.callActivity(echo, input); + return result; + }; + + taskHubWorker.addOrchestrator(orchestrator); + taskHubWorker.addActivity(echo); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(orchestrator, null); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.serializedOutput).toEqual(JSON.stringify(null)); + }, 31000); + + it("should pass zero through sub-orchestration round-trip", async () => { + const child: TOrchestrator = async (_ctx: OrchestrationContext, input: number) => { + return input; + }; + + const parent: TOrchestrator = async function* (ctx: OrchestrationContext, input: number): any { + const result = yield ctx.callSubOrchestrator(child, input); + return result; + }; + + taskHubWorker.addOrchestrator(parent); + taskHubWorker.addOrchestrator(child); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(parent, 0); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.serializedOutput).toEqual(JSON.stringify(0)); + }, 31000); + + it("should return zero as orchestration result", async () => { + const orchestrator: TOrchestrator = async (_ctx: OrchestrationContext) => { + return 0; + }; + + taskHubWorker.addOrchestrator(orchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(orchestrator); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.serializedOutput).toEqual(JSON.stringify(0)); + }, 31000); + + it("should return false as orchestration result", async () => { + const orchestrator: TOrchestrator = async (_ctx: OrchestrationContext) => { + return false; + }; + + taskHubWorker.addOrchestrator(orchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(orchestrator); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.serializedOutput).toEqual(JSON.stringify(false)); + }, 31000); + + it("should return empty string as orchestration result", async () => { + const orchestrator: TOrchestrator = async (_ctx: OrchestrationContext) => { + return ""; + }; + + taskHubWorker.addOrchestrator(orchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(orchestrator); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.serializedOutput).toEqual(JSON.stringify("")); + }, 31000); + + it("should continue-as-new with zero input", async () => { + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext, input: number) => { + if (input === 0) { + ctx.continueAsNew(1, false); + return; + } + return input; + }; + + taskHubWorker.addOrchestrator(orchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(orchestrator, 0); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.serializedOutput).toEqual(JSON.stringify(1)); + }, 31000); + }); });