From cde063fe135d1ba9af35d83e657ad8c2987a9ee7 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:50:35 +0000 Subject: [PATCH 1/2] fix: add missing action types to getMethodNameForAction() Add SENDEVENT, SENDENTITYMESSAGE, and TERMINATEORCHESTRATION cases to the getMethodNameForAction() switch statement. Without these cases, non-determinism errors involving entity operations or sendEvent would crash with an unhelpful 'Unknown action type' error instead of the proper NonDeterminismError diagnostic message. Also adds comprehensive unit tests for all exported helper functions in the worker/index.ts module. Fixes #233 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/durabletask-js/src/worker/index.ts | 6 + .../test/worker-helpers.spec.ts | 196 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 packages/durabletask-js/test/worker-helpers.spec.ts diff --git a/packages/durabletask-js/src/worker/index.ts b/packages/durabletask-js/src/worker/index.ts index 36f8c0b..d5fa295 100644 --- a/packages/durabletask-js/src/worker/index.ts +++ b/packages/durabletask-js/src/worker/index.ts @@ -49,6 +49,12 @@ export function getMethodNameForAction(action: pb.OrchestratorAction): string { return "callSubOrchestrator"; case pb.OrchestratorAction.OrchestratoractiontypeCase.COMPLETEORCHESTRATION: return "completeOrchestration"; + case pb.OrchestratorAction.OrchestratoractiontypeCase.SENDEVENT: + return "sendEvent"; + case pb.OrchestratorAction.OrchestratoractiontypeCase.SENDENTITYMESSAGE: + return "sendEntityMessage"; + case pb.OrchestratorAction.OrchestratoractiontypeCase.TERMINATEORCHESTRATION: + return "terminateOrchestration"; default: throw new Error(`Unknown action type: ${actionType}`); } diff --git a/packages/durabletask-js/test/worker-helpers.spec.ts b/packages/durabletask-js/test/worker-helpers.spec.ts new file mode 100644 index 0000000..467281d --- /dev/null +++ b/packages/durabletask-js/test/worker-helpers.spec.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as pb from "../src/proto/orchestrator_service_pb"; +import { + getMethodNameForAction, + getWrongActionTypeError, + getNonDeterminismError, + getNewEventSummary, + getActionSummary, + isSuspendable, +} from "../src/worker/index"; +import { NonDeterminismError } from "../src/task/exception/non-determinism-error"; + +/** + * Creates an OrchestratorAction with the given action type set. + */ +function createActionWithType( + actionCase: pb.OrchestratorAction.OrchestratoractiontypeCase, +): pb.OrchestratorAction { + const action = new pb.OrchestratorAction(); + + switch (actionCase) { + case pb.OrchestratorAction.OrchestratoractiontypeCase.SCHEDULETASK: + action.setScheduletask(new pb.ScheduleTaskAction()); + break; + case pb.OrchestratorAction.OrchestratoractiontypeCase.CREATETIMER: + action.setCreatetimer(new pb.CreateTimerAction()); + break; + case pb.OrchestratorAction.OrchestratoractiontypeCase.CREATESUBORCHESTRATION: + action.setCreatesuborchestration(new pb.CreateSubOrchestrationAction()); + break; + case pb.OrchestratorAction.OrchestratoractiontypeCase.COMPLETEORCHESTRATION: + action.setCompleteorchestration(new pb.CompleteOrchestrationAction()); + break; + case pb.OrchestratorAction.OrchestratoractiontypeCase.SENDEVENT: + action.setSendevent(new pb.SendEventAction()); + break; + case pb.OrchestratorAction.OrchestratoractiontypeCase.SENDENTITYMESSAGE: + action.setSendentitymessage(new pb.SendEntityMessageAction()); + break; + case pb.OrchestratorAction.OrchestratoractiontypeCase.TERMINATEORCHESTRATION: + action.setTerminateorchestration(new pb.TerminateOrchestrationAction()); + break; + } + + return action; +} + +describe("Worker helper functions", () => { + describe("getMethodNameForAction", () => { + it("should return 'callActivity' for SCHEDULETASK", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.SCHEDULETASK); + expect(getMethodNameForAction(action)).toBe("callActivity"); + }); + + it("should return 'createTimer' for CREATETIMER", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.CREATETIMER); + expect(getMethodNameForAction(action)).toBe("createTimer"); + }); + + it("should return 'callSubOrchestrator' for CREATESUBORCHESTRATION", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.CREATESUBORCHESTRATION); + expect(getMethodNameForAction(action)).toBe("callSubOrchestrator"); + }); + + it("should return 'completeOrchestration' for COMPLETEORCHESTRATION", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.COMPLETEORCHESTRATION); + expect(getMethodNameForAction(action)).toBe("completeOrchestration"); + }); + + it("should return 'sendEvent' for SENDEVENT", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.SENDEVENT); + expect(getMethodNameForAction(action)).toBe("sendEvent"); + }); + + it("should return 'sendEntityMessage' for SENDENTITYMESSAGE", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.SENDENTITYMESSAGE); + expect(getMethodNameForAction(action)).toBe("sendEntityMessage"); + }); + + it("should return 'terminateOrchestration' for TERMINATEORCHESTRATION", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.TERMINATEORCHESTRATION); + expect(getMethodNameForAction(action)).toBe("terminateOrchestration"); + }); + + it("should throw for ORCHESTRATORACTIONTYPE_NOT_SET", () => { + const action = new pb.OrchestratorAction(); + expect(() => getMethodNameForAction(action)).toThrow("Unknown action type: 0"); + }); + }); + + describe("getWrongActionTypeError", () => { + it("should return a NonDeterminismError with expected and actual method names", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.SENDEVENT); + const error = getWrongActionTypeError(5, "callActivity", action); + + expect(error).toBeInstanceOf(NonDeterminismError); + expect(error.message).toContain("callActivity"); + expect(error.message).toContain("sendEvent"); + expect(error.message).toContain("ID=5"); + }); + + it("should return a NonDeterminismError for entity message action", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.SENDENTITYMESSAGE); + const error = getWrongActionTypeError(3, "createTimer", action); + + expect(error).toBeInstanceOf(NonDeterminismError); + expect(error.message).toContain("createTimer"); + expect(error.message).toContain("sendEntityMessage"); + expect(error.message).toContain("ID=3"); + }); + + it("should return a NonDeterminismError for terminate orchestration action", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.TERMINATEORCHESTRATION); + const error = getWrongActionTypeError(7, "callSubOrchestrator", action); + + expect(error).toBeInstanceOf(NonDeterminismError); + expect(error.message).toContain("callSubOrchestrator"); + expect(error.message).toContain("terminateOrchestration"); + expect(error.message).toContain("ID=7"); + }); + }); + + describe("getNonDeterminismError", () => { + it("should return a NonDeterminismError with task ID and action name", () => { + const error = getNonDeterminismError(42, "callActivity"); + + expect(error).toBeInstanceOf(NonDeterminismError); + expect(error.message).toContain("callActivity"); + expect(error.message).toContain("ID=42"); + }); + }); + + describe("getNewEventSummary", () => { + it("should return '[]' for empty events", () => { + expect(getNewEventSummary([])).toBe("[]"); + }); + + it("should return single event type name for one event", () => { + const event = new pb.HistoryEvent(); + event.setTaskcompleted(new pb.TaskCompletedEvent()); + const result = getNewEventSummary([event]); + expect(result).toBe("[TASKCOMPLETED]"); + }); + + it("should return grouped counts for multiple events", () => { + const event1 = new pb.HistoryEvent(); + event1.setTaskcompleted(new pb.TaskCompletedEvent()); + const event2 = new pb.HistoryEvent(); + event2.setTaskcompleted(new pb.TaskCompletedEvent()); + const result = getNewEventSummary([event1, event2]); + expect(result).toContain("TASKCOMPLETED=2"); + }); + }); + + describe("getActionSummary", () => { + it("should return '[]' for empty actions", () => { + expect(getActionSummary([])).toBe("[]"); + }); + + it("should return action type name for single action", () => { + const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.SCHEDULETASK); + const result = getActionSummary([action]); + expect(result).toBe("SCHEDULETASK"); + }); + + it("should return grouped counts for multiple actions", () => { + const action1 = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.SCHEDULETASK); + const action2 = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.CREATETIMER); + const result = getActionSummary([action1, action2]); + expect(result).toContain("SCHEDULETASK=1"); + expect(result).toContain("CREATETIMER=1"); + }); + }); + + describe("isSuspendable", () => { + it("should return false for EXECUTIONRESUMED", () => { + const event = new pb.HistoryEvent(); + event.setExecutionresumed(new pb.ExecutionResumedEvent()); + expect(isSuspendable(event)).toBe(false); + }); + + it("should return false for EXECUTIONTERMINATED", () => { + const event = new pb.HistoryEvent(); + event.setExecutionterminated(new pb.ExecutionTerminatedEvent()); + expect(isSuspendable(event)).toBe(false); + }); + + it("should return true for other events like TASKCOMPLETED", () => { + const event = new pb.HistoryEvent(); + event.setTaskcompleted(new pb.TaskCompletedEvent()); + expect(isSuspendable(event)).toBe(true); + }); + }); +}); From 1e66858d703c56159724647c87e05c604a75055a Mon Sep 17 00:00:00 2001 From: wangbill Date: Thu, 18 Jun 2026 11:00:12 -0700 Subject: [PATCH 2/2] fix: map entity action subtypes to API names Return user-facing API method names for SendEntityMessage action subtypes in non-determinism diagnostics, including callEntity, signalEntity, lockEntities, and lockRelease. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/durabletask-js/src/worker/index.ts | 13 ++++- .../test/worker-helpers.spec.ts | 47 +++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/durabletask-js/src/worker/index.ts b/packages/durabletask-js/src/worker/index.ts index d5fa295..c3311e2 100644 --- a/packages/durabletask-js/src/worker/index.ts +++ b/packages/durabletask-js/src/worker/index.ts @@ -52,7 +52,18 @@ export function getMethodNameForAction(action: pb.OrchestratorAction): string { case pb.OrchestratorAction.OrchestratoractiontypeCase.SENDEVENT: return "sendEvent"; case pb.OrchestratorAction.OrchestratoractiontypeCase.SENDENTITYMESSAGE: - return "sendEntityMessage"; + switch (action.getSendentitymessage()?.getEntitymessagetypeCase()) { + case pb.SendEntityMessageAction.EntitymessagetypeCase.ENTITYOPERATIONCALLED: + return "callEntity"; + case pb.SendEntityMessageAction.EntitymessagetypeCase.ENTITYOPERATIONSIGNALED: + return "signalEntity"; + case pb.SendEntityMessageAction.EntitymessagetypeCase.ENTITYLOCKREQUESTED: + return "lockEntities"; + case pb.SendEntityMessageAction.EntitymessagetypeCase.ENTITYUNLOCKSENT: + return "lockRelease"; + default: + return "sendEntityMessage"; + } case pb.OrchestratorAction.OrchestratoractiontypeCase.TERMINATEORCHESTRATION: return "terminateOrchestration"; default: diff --git a/packages/durabletask-js/test/worker-helpers.spec.ts b/packages/durabletask-js/test/worker-helpers.spec.ts index 467281d..6050bf8 100644 --- a/packages/durabletask-js/test/worker-helpers.spec.ts +++ b/packages/durabletask-js/test/worker-helpers.spec.ts @@ -47,6 +47,17 @@ function createActionWithType( return action; } +function createSendEntityMessageAction( + configure: (message: pb.SendEntityMessageAction) => void, +): pb.OrchestratorAction { + const sendEntityMessage = new pb.SendEntityMessageAction(); + configure(sendEntityMessage); + + const action = new pb.OrchestratorAction(); + action.setSendentitymessage(sendEntityMessage); + return action; +} + describe("Worker helper functions", () => { describe("getMethodNameForAction", () => { it("should return 'callActivity' for SCHEDULETASK", () => { @@ -74,11 +85,39 @@ describe("Worker helper functions", () => { expect(getMethodNameForAction(action)).toBe("sendEvent"); }); - it("should return 'sendEntityMessage' for SENDENTITYMESSAGE", () => { + it("should return 'sendEntityMessage' for SENDENTITYMESSAGE with unknown subtype", () => { const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.SENDENTITYMESSAGE); expect(getMethodNameForAction(action)).toBe("sendEntityMessage"); }); + it("should return 'callEntity' for SENDENTITYMESSAGE entity operation call", () => { + const action = createSendEntityMessageAction((message) => { + message.setEntityoperationcalled(new pb.EntityOperationCalledEvent()); + }); + expect(getMethodNameForAction(action)).toBe("callEntity"); + }); + + it("should return 'signalEntity' for SENDENTITYMESSAGE entity operation signal", () => { + const action = createSendEntityMessageAction((message) => { + message.setEntityoperationsignaled(new pb.EntityOperationSignaledEvent()); + }); + expect(getMethodNameForAction(action)).toBe("signalEntity"); + }); + + it("should return 'lockEntities' for SENDENTITYMESSAGE entity lock request", () => { + const action = createSendEntityMessageAction((message) => { + message.setEntitylockrequested(new pb.EntityLockRequestedEvent()); + }); + expect(getMethodNameForAction(action)).toBe("lockEntities"); + }); + + it("should return 'lockRelease' for SENDENTITYMESSAGE entity unlock sent", () => { + const action = createSendEntityMessageAction((message) => { + message.setEntityunlocksent(new pb.EntityUnlockSentEvent()); + }); + expect(getMethodNameForAction(action)).toBe("lockRelease"); + }); + it("should return 'terminateOrchestration' for TERMINATEORCHESTRATION", () => { const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.TERMINATEORCHESTRATION); expect(getMethodNameForAction(action)).toBe("terminateOrchestration"); @@ -102,12 +141,14 @@ describe("Worker helper functions", () => { }); it("should return a NonDeterminismError for entity message action", () => { - const action = createActionWithType(pb.OrchestratorAction.OrchestratoractiontypeCase.SENDENTITYMESSAGE); + const action = createSendEntityMessageAction((message) => { + message.setEntityoperationcalled(new pb.EntityOperationCalledEvent()); + }); const error = getWrongActionTypeError(3, "createTimer", action); expect(error).toBeInstanceOf(NonDeterminismError); expect(error.message).toContain("createTimer"); - expect(error.message).toContain("sendEntityMessage"); + expect(error.message).toContain("callEntity"); expect(error.message).toContain("ID=3"); });