diff --git a/packages/durabletask-js/src/orchestration/enum/orchestration-status.enum.ts b/packages/durabletask-js/src/orchestration/enum/orchestration-status.enum.ts index 64aff91..02f65b7 100644 --- a/packages/durabletask-js/src/orchestration/enum/orchestration-status.enum.ts +++ b/packages/durabletask-js/src/orchestration/enum/orchestration-status.enum.ts @@ -28,6 +28,7 @@ export enum OrchestrationStatus { RUNNING = pb.OrchestrationStatus.ORCHESTRATION_STATUS_RUNNING, COMPLETED = pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED, FAILED = pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED, + CANCELED = pb.OrchestrationStatus.ORCHESTRATION_STATUS_CANCELED, TERMINATED = pb.OrchestrationStatus.ORCHESTRATION_STATUS_TERMINATED, CONTINUED_AS_NEW = pb.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW, PENDING = pb.OrchestrationStatus.ORCHESTRATION_STATUS_PENDING, diff --git a/packages/durabletask-js/src/testing/in-memory-backend.ts b/packages/durabletask-js/src/testing/in-memory-backend.ts index 1d55189..cf81627 100644 --- a/packages/durabletask-js/src/testing/in-memory-backend.ts +++ b/packages/durabletask-js/src/testing/in-memory-backend.ts @@ -490,6 +490,8 @@ export class InMemoryOrchestrationBackend { return ClientOrchestrationStatus.FAILED; case pb.OrchestrationStatus.ORCHESTRATION_STATUS_TERMINATED: return ClientOrchestrationStatus.TERMINATED; + case pb.OrchestrationStatus.ORCHESTRATION_STATUS_CANCELED: + return ClientOrchestrationStatus.CANCELED; case pb.OrchestrationStatus.ORCHESTRATION_STATUS_SUSPENDED: return ClientOrchestrationStatus.SUSPENDED; case pb.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW: @@ -511,6 +513,7 @@ export class InMemoryOrchestrationBackend { return ( status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED || status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED || + status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_CANCELED || status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_TERMINATED ); } diff --git a/packages/durabletask-js/src/testing/test-client.ts b/packages/durabletask-js/src/testing/test-client.ts index 09d1538..5c68335 100644 --- a/packages/durabletask-js/src/testing/test-client.ts +++ b/packages/durabletask-js/src/testing/test-client.ts @@ -151,6 +151,7 @@ export class TestOrchestrationClient { return ( status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED || status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED || + status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_CANCELED || status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_TERMINATED ); } diff --git a/packages/durabletask-js/test/in-memory-backend.spec.ts b/packages/durabletask-js/test/in-memory-backend.spec.ts index 381fcde..86e4d10 100644 --- a/packages/durabletask-js/test/in-memory-backend.spec.ts +++ b/packages/durabletask-js/test/in-memory-backend.spec.ts @@ -602,6 +602,30 @@ describe("In-Memory Backend", () => { expect(state?.serializedOutput).toEqual(JSON.stringify(42)); }); + describe("toClientStatus", () => { + it("should map CANCELED proto status to CANCELED client status", () => { + const { OrchestrationStatus: pbStatus } = require("../src/proto/orchestrator_service_pb"); + expect(backend.toClientStatus(pbStatus.ORCHESTRATION_STATUS_CANCELED)).toBe(OrchestrationStatus.CANCELED); + }); + + it("should map all known proto statuses without throwing", () => { + const { OrchestrationStatus: pbStatus } = require("../src/proto/orchestrator_service_pb"); + const protoStatuses = [ + pbStatus.ORCHESTRATION_STATUS_RUNNING, + pbStatus.ORCHESTRATION_STATUS_COMPLETED, + pbStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW, + pbStatus.ORCHESTRATION_STATUS_FAILED, + pbStatus.ORCHESTRATION_STATUS_CANCELED, + pbStatus.ORCHESTRATION_STATUS_TERMINATED, + pbStatus.ORCHESTRATION_STATUS_PENDING, + pbStatus.ORCHESTRATION_STATUS_SUSPENDED, + ]; + for (const status of protoStatuses) { + expect(() => backend.toClientStatus(status)).not.toThrow(); + } + }); + }); + it("waitForState with zero timeout should wait indefinitely until state matches", async () => { const orchestrator: TOrchestrator = async (_: OrchestrationContext) => "done"; diff --git a/packages/durabletask-js/test/orchestration-status.spec.ts b/packages/durabletask-js/test/orchestration-status.spec.ts new file mode 100644 index 0000000..96368f1 --- /dev/null +++ b/packages/durabletask-js/test/orchestration-status.spec.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as pb from "../src/proto/orchestrator_service_pb"; +import { OrchestrationStatus, fromProtobuf, toProtobuf } from "../src/orchestration/enum/orchestration-status.enum"; + +/** + * Exhaustive mapping between client OrchestrationStatus and proto OrchestrationStatus. + * Every entry in the proto enum (except unspecified/unknown sentinel values) must have + * a corresponding client enum member. + */ +const STATUS_PAIRS: [OrchestrationStatus, pb.OrchestrationStatus][] = [ + [OrchestrationStatus.RUNNING, pb.OrchestrationStatus.ORCHESTRATION_STATUS_RUNNING], + [OrchestrationStatus.COMPLETED, pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED], + [OrchestrationStatus.CONTINUED_AS_NEW, pb.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW], + [OrchestrationStatus.FAILED, pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED], + [OrchestrationStatus.CANCELED, pb.OrchestrationStatus.ORCHESTRATION_STATUS_CANCELED], + [OrchestrationStatus.TERMINATED, pb.OrchestrationStatus.ORCHESTRATION_STATUS_TERMINATED], + [OrchestrationStatus.PENDING, pb.OrchestrationStatus.ORCHESTRATION_STATUS_PENDING], + [OrchestrationStatus.SUSPENDED, pb.OrchestrationStatus.ORCHESTRATION_STATUS_SUSPENDED], +]; + +describe("OrchestrationStatus enum", () => { + describe("fromProtobuf", () => { + it.each(STATUS_PAIRS)( + "should convert client %s from proto %s", + (clientStatus, protoStatus) => { + expect(fromProtobuf(protoStatus)).toBe(clientStatus); + }, + ); + + it("should throw on unknown proto value", () => { + expect(() => fromProtobuf(999 as pb.OrchestrationStatus)).toThrow( + "Unknown protobuf OrchestrationStatus value: 999", + ); + }); + }); + + describe("toProtobuf", () => { + it.each(STATUS_PAIRS)( + "should convert client %s to proto %s", + (clientStatus, protoStatus) => { + expect(toProtobuf(clientStatus)).toBe(protoStatus); + }, + ); + + it("should throw on unknown client value", () => { + expect(() => toProtobuf(999 as OrchestrationStatus)).toThrow( + "Unknown OrchestrationStatus value: 999", + ); + }); + }); + + describe("round-trip", () => { + it.each(STATUS_PAIRS)( + "should round-trip client %s through proto and back", + (clientStatus) => { + expect(fromProtobuf(toProtobuf(clientStatus))).toBe(clientStatus); + }, + ); + }); + + describe("completeness", () => { + it("should cover all non-zero proto OrchestrationStatus values", () => { + // Get all numeric values from the proto enum (protobuf JS enums are plain objects) + const protoValues = new Set(); + for (const [key, value] of Object.entries(pb.OrchestrationStatus)) { + if (typeof value === "number" && key.startsWith("ORCHESTRATION_STATUS_")) { + protoValues.add(value); + } + } + + const coveredProtoValues = new Set(STATUS_PAIRS.map(([, proto]) => proto as number)); + + for (const protoValue of protoValues) { + expect(coveredProtoValues).toContain(protoValue); + } + }); + + it("should have matching numeric values between client and proto enums", () => { + for (const [clientStatus, protoStatus] of STATUS_PAIRS) { + expect(clientStatus as number).toBe(protoStatus as number); + } + }); + }); +});