From 000103d70f6062c042e8efc0f067ed2428f33b42 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:50:59 +0000 Subject: [PATCH] [copilot-finds] Bug: Add missing CANCELED member to OrchestrationStatus enum The client-facing OrchestrationStatus enum was missing the CANCELED member even though the protobuf definition includes ORCHESTRATION_STATUS_CANCELED (4). This caused fromProtobuf() to throw 'Unknown protobuf OrchestrationStatus value: 4' when encountering canceled orchestrations, crashing any client call that reads orchestration state (getOrchestration, waitForCompletion, getAllInstances). Changes: - Add CANCELED to OrchestrationStatus enum (auto-registers in conversion maps) - Add CANCELED case to InMemoryOrchestrationBackend.toClientStatus() - Add CANCELED to isTerminalStatus() in both InMemoryOrchestrationBackend and TestOrchestrationClient - Add comprehensive tests for OrchestrationStatus enum round-trip conversions - Add toClientStatus tests to in-memory backend spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../enum/orchestration-status.enum.ts | 1 + .../src/testing/in-memory-backend.ts | 3 + .../durabletask-js/src/testing/test-client.ts | 1 + .../test/in-memory-backend.spec.ts | 24 ++++++ .../test/orchestration-status.spec.ts | 86 +++++++++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 packages/durabletask-js/test/orchestration-status.spec.ts 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 2c96acb..5a0aa4d 100644 --- a/packages/durabletask-js/src/testing/in-memory-backend.ts +++ b/packages/durabletask-js/src/testing/in-memory-backend.ts @@ -411,6 +411,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: @@ -432,6 +434,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 f7af0a9..284122d 100644 --- a/packages/durabletask-js/src/testing/test-client.ts +++ b/packages/durabletask-js/src/testing/test-client.ts @@ -137,6 +137,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 66fe6a9..c2b4647 100644 --- a/packages/durabletask-js/test/in-memory-backend.spec.ts +++ b/packages/durabletask-js/test/in-memory-backend.spec.ts @@ -377,4 +377,28 @@ describe("In-Memory Backend", () => { expect(state?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED); 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(); + } + }); + }); }); 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); + } + }); + }); +});