From fda5daa0e6ac6d763a85450be254c7cc6e6cd907 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:10:09 -0800 Subject: [PATCH 1/9] Get Orchestration History --- packages/durabletask-js/src/client/client.ts | 118 ++++- packages/durabletask-js/src/index.ts | 33 +- .../src/orchestration/history-event.ts | 380 +++++++++++++++ .../src/utils/history-event-converter.ts | 460 ++++++++++++++++++ test/e2e-azuremanaged/history.spec.ts | 381 +++++++++++++++ 5 files changed, 1368 insertions(+), 4 deletions(-) create mode 100644 packages/durabletask-js/src/orchestration/history-event.ts create mode 100644 packages/durabletask-js/src/utils/history-event-converter.ts create mode 100644 test/e2e-azuremanaged/history.spec.ts diff --git a/packages/durabletask-js/src/client/client.ts b/packages/durabletask-js/src/client/client.ts index 5e48f11..92fc4fe 100644 --- a/packages/durabletask-js/src/client/client.ts +++ b/packages/durabletask-js/src/client/client.ts @@ -21,10 +21,26 @@ import { callWithMetadata, MetadataGenerator } from "../utils/grpc-helper.util"; import { OrchestrationQuery, ListInstanceIdsOptions, DEFAULT_PAGE_SIZE } from "../orchestration/orchestration-query"; import { Page, AsyncPageable, createAsyncPageable } from "../orchestration/page"; import { FailureDetails } from "../task/failure-details"; +import { HistoryEvent } from "../orchestration/history-event"; +import { convertProtoHistoryEvent } from "../utils/history-event-converter"; // Re-export MetadataGenerator for backward compatibility export { MetadataGenerator } from "../utils/grpc-helper.util"; +/** + * Options for scheduling a new orchestration instance. + */ +export interface StartOrchestrationOptions { + /** The input to pass to the orchestration. */ + input?: TInput; + /** The unique ID for the orchestration instance. If not specified, a new GUID is generated. */ + instanceId?: string; + /** The time when the orchestration should start executing. If not specified, starts immediately. */ + startAt?: Date; + /** Tags to associate with the orchestration instance. */ + tags?: Record; +} + export class TaskHubGrpcClient { private _stub: stubs.TaskHubSidecarServiceClient; private _metadataGenerator?: MetadataGenerator; @@ -61,14 +77,36 @@ export class TaskHubGrpcClient { * Schedules a new orchestrator using the DurableTask client. * * @param {TOrchestrator | string} orchestrator - The orchestrator or the name of the orchestrator to be scheduled. + * @param {TInput | StartOrchestrationOptions} inputOrOptions - The input to pass to the orchestrator, or an options object. + * @param {string} instanceId - (Deprecated) Use options object instead. The unique ID for the orchestration instance. + * @param {Date} startAt - (Deprecated) Use options object instead. The time when the orchestration should start. * @return {Promise} A Promise resolving to the unique ID of the scheduled orchestrator instance. */ async scheduleNewOrchestration( orchestrator: TOrchestrator | string, - input?: TInput, + inputOrOptions?: TInput | StartOrchestrationOptions, instanceId?: string, startAt?: Date, ): Promise { + // Determine if inputOrOptions is an options object or raw input + let input: TInput | undefined; + let resolvedInstanceId: string | undefined = instanceId; + let resolvedStartAt: Date | undefined = startAt; + let tags: Record | undefined; + + if (inputOrOptions !== null && typeof inputOrOptions === 'object' && !Array.isArray(inputOrOptions) && + ('input' in inputOrOptions || 'instanceId' in inputOrOptions || 'startAt' in inputOrOptions || 'tags' in inputOrOptions)) { + // It's an options object + const options = inputOrOptions as StartOrchestrationOptions; + input = options.input; + resolvedInstanceId = options.instanceId ?? instanceId; + resolvedStartAt = options.startAt ?? startAt; + tags = options.tags; + } else { + // It's raw input (backward compatible) + input = inputOrOptions as TInput; + } + let name; if (typeof orchestrator === "string") { name = orchestrator; @@ -77,17 +115,25 @@ export class TaskHubGrpcClient { } const req = new pb.CreateInstanceRequest(); req.setName(name); - req.setInstanceid(instanceId ?? randomUUID()); + req.setInstanceid(resolvedInstanceId ?? randomUUID()); const i = new StringValue(); i.setValue(JSON.stringify(input)); const ts = new Timestamp(); - ts.fromDate(new Date(startAt?.getTime() ?? 0)); + ts.fromDate(new Date(resolvedStartAt?.getTime() ?? 0)); req.setInput(i); req.setScheduledstarttimestamp(ts); + // Set tags if provided + if (tags) { + const tagsMap = req.getTagsMap(); + for (const [key, value] of Object.entries(tags)) { + tagsMap.set(key, value); + } + } + console.log(`Starting new ${name} instance with ID = ${req.getInstanceid()}`); const res = await callWithMetadata( @@ -670,6 +716,72 @@ export class TaskHubGrpcClient { return new Page(instanceIds, lastInstanceKey); } + /** + * Retrieves the history of the specified orchestration instance as a list of HistoryEvent objects. + * + * This method streams the history events from the backend and returns them as an array. + * The history includes all events that occurred during the orchestration execution, + * such as task scheduling, completion, failure, timer events, and more. + * + * @param instanceId - The unique identifier of the orchestration instance. + * @returns A Promise that resolves to an array of HistoryEvent objects representing + * the orchestration's history. + * @throws {Error} If the instanceId is null or empty. + * @throws {Error} If an orchestration with the specified instanceId is not found. + * @throws {Error} If the operation is canceled. + * @throws {Error} If an internal error occurs while retrieving the history. + * + * @example + * ```typescript + * const history = await client.getOrchestrationHistory(instanceId); + * for (const event of history) { + * console.log(`Event ${event.eventId}: ${event.type} at ${event.timestamp}`); + * } + * ``` + */ + async getOrchestrationHistory(instanceId: string): Promise { + if (!instanceId) { + throw new Error("instanceId is required"); + } + + const req = new pb.StreamInstanceHistoryRequest(); + req.setInstanceid(instanceId); + req.setForworkitemprocessing(false); + + const metadata = this._metadataGenerator ? await this._metadataGenerator() : new grpc.Metadata(); + const stream = this._stub.streamInstanceHistory(req, metadata); + + return new Promise((resolve, reject) => { + const historyEvents: HistoryEvent[] = []; + + stream.on("data", (chunk: pb.HistoryChunk) => { + const protoEvents = chunk.getEventsList(); + for (const protoEvent of protoEvents) { + const event = convertProtoHistoryEvent(protoEvent); + if (event) { + historyEvents.push(event); + } + } + }); + + stream.on("end", () => { + resolve(historyEvents); + }); + + stream.on("error", (err: grpc.ServiceError) => { + if (err.code === grpc.status.NOT_FOUND) { + reject(new Error(`An orchestration with the instanceId '${instanceId}' was not found.`)); + } else if (err.code === grpc.status.CANCELLED) { + reject(new Error(`The getOrchestrationHistory operation was canceled.`)); + } else if (err.code === grpc.status.INTERNAL) { + reject(new Error(`An error occurred while retrieving the history for orchestration with instanceId '${instanceId}'.`)); + } else { + reject(err); + } + }); + }); + } + /** * Helper method to create an OrchestrationState from a protobuf OrchestrationState. */ diff --git a/packages/durabletask-js/src/index.ts b/packages/durabletask-js/src/index.ts index 535fba9..42da71d 100644 --- a/packages/durabletask-js/src/index.ts +++ b/packages/durabletask-js/src/index.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. // Client and Worker -export { TaskHubGrpcClient, MetadataGenerator } from "./client/client"; +export { TaskHubGrpcClient, MetadataGenerator, StartOrchestrationOptions } from "./client/client"; export { TaskHubGrpcWorker } from "./worker/task-hub-grpc-worker"; // Contexts @@ -18,6 +18,37 @@ export { OrchestrationState } from "./orchestration/orchestration-state"; export { OrchestrationQuery, ListInstanceIdsOptions, DEFAULT_PAGE_SIZE } from "./orchestration/orchestration-query"; export { Page, AsyncPageable, createAsyncPageable } from "./orchestration/page"; +// History event types +export { + HistoryEvent, + HistoryEventType, + HistoryEventBase, + ExecutionStartedEvent, + ExecutionCompletedEvent, + ExecutionTerminatedEvent, + ExecutionSuspendedEvent, + ExecutionResumedEvent, + ExecutionRewoundEvent, + TaskScheduledEvent, + TaskCompletedEvent, + TaskFailedEvent, + SubOrchestrationInstanceCreatedEvent, + SubOrchestrationInstanceCompletedEvent, + SubOrchestrationInstanceFailedEvent, + TimerCreatedEvent, + TimerFiredEvent, + OrchestratorStartedEvent, + OrchestratorCompletedEvent, + EventSentEvent, + EventRaisedEvent, + GenericEvent, + HistoryStateEvent, + ContinueAsNewEvent, + OrchestrationInstance, + ParentInstanceInfo, + TraceContext, +} from "./orchestration/history-event"; + // Proto types (for advanced usage) export { OrchestrationStatus as ProtoOrchestrationStatus } from "./proto/orchestrator_service_pb"; diff --git a/packages/durabletask-js/src/orchestration/history-event.ts b/packages/durabletask-js/src/orchestration/history-event.ts new file mode 100644 index 0000000..d660116 --- /dev/null +++ b/packages/durabletask-js/src/orchestration/history-event.ts @@ -0,0 +1,380 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { FailureDetails } from "../task/failure-details"; + +/** + * Enumeration of all possible history event types. + */ +export enum HistoryEventType { + ExecutionStarted = "ExecutionStarted", + ExecutionCompleted = "ExecutionCompleted", + ExecutionTerminated = "ExecutionTerminated", + ExecutionSuspended = "ExecutionSuspended", + ExecutionResumed = "ExecutionResumed", + ExecutionRewound = "ExecutionRewound", + TaskScheduled = "TaskScheduled", + TaskCompleted = "TaskCompleted", + TaskFailed = "TaskFailed", + SubOrchestrationInstanceCreated = "SubOrchestrationInstanceCreated", + SubOrchestrationInstanceCompleted = "SubOrchestrationInstanceCompleted", + SubOrchestrationInstanceFailed = "SubOrchestrationInstanceFailed", + TimerCreated = "TimerCreated", + TimerFired = "TimerFired", + OrchestratorStarted = "OrchestratorStarted", + OrchestratorCompleted = "OrchestratorCompleted", + EventSent = "EventSent", + EventRaised = "EventRaised", + GenericEvent = "GenericEvent", + HistoryState = "HistoryState", + ContinueAsNew = "ContinueAsNew", + EntityOperationSignaled = "EntityOperationSignaled", + EntityOperationCalled = "EntityOperationCalled", + EntityOperationCompleted = "EntityOperationCompleted", + EntityOperationFailed = "EntityOperationFailed", + EntityLockRequested = "EntityLockRequested", + EntityLockGranted = "EntityLockGranted", + EntityUnlockSent = "EntityUnlockSent", +} + +/** + * Represents an orchestration instance identifier. + */ +export interface OrchestrationInstance { + instanceId: string; + executionId?: string; +} + +/** + * Represents parent instance information for sub-orchestrations. + */ +export interface ParentInstanceInfo { + name?: string; + version?: string; + taskScheduledId: number; + orchestrationInstance?: OrchestrationInstance; +} + +/** + * Represents trace context for distributed tracing. + */ +export interface TraceContext { + traceParent: string; + /** @deprecated Use traceParent instead */ + spanId?: string; + traceState?: string; +} + +/** + * Base interface for all history events. + */ +export interface HistoryEventBase { + /** The unique identifier for this event within the orchestration history. */ + eventId: number; + /** The timestamp when this event occurred. */ + timestamp: Date; + /** The type of history event. */ + type: HistoryEventType; +} + +/** + * Event that marks the start of an orchestration execution. + */ +export interface ExecutionStartedEvent extends HistoryEventBase { + type: HistoryEventType.ExecutionStarted; + name: string; + version?: string; + input?: string; + orchestrationInstance?: OrchestrationInstance; + parentInstance?: ParentInstanceInfo; + scheduledStartTimestamp?: Date; + tags?: Record; +} + +/** + * Event that marks the completion of an orchestration execution. + */ +export interface ExecutionCompletedEvent extends HistoryEventBase { + type: HistoryEventType.ExecutionCompleted; + orchestrationStatus: string; + result?: string; + failureDetails?: FailureDetails; +} + +/** + * Event that marks the termination of an orchestration execution. + */ +export interface ExecutionTerminatedEvent extends HistoryEventBase { + type: HistoryEventType.ExecutionTerminated; + input?: string; + recurse?: boolean; +} + +/** + * Event that marks the suspension of an orchestration execution. + */ +export interface ExecutionSuspendedEvent extends HistoryEventBase { + type: HistoryEventType.ExecutionSuspended; + input?: string; +} + +/** + * Event that marks the resumption of an orchestration execution. + */ +export interface ExecutionResumedEvent extends HistoryEventBase { + type: HistoryEventType.ExecutionResumed; + input?: string; +} + +/** + * Event that marks the rewind of an orchestration execution. + */ +export interface ExecutionRewoundEvent extends HistoryEventBase { + type: HistoryEventType.ExecutionRewound; + reason?: string; + parentExecutionId?: string; + instanceId?: string; + parentTraceContext?: TraceContext; + name?: string; + version?: string; + input?: string; + parentInstance?: ParentInstanceInfo; + tags?: Record; +} + +/** + * Event that marks the scheduling of an activity task. + */ +export interface TaskScheduledEvent extends HistoryEventBase { + type: HistoryEventType.TaskScheduled; + name: string; + version?: string; + input?: string; + tags?: Record; +} + +/** + * Event that marks the completion of an activity task. + */ +export interface TaskCompletedEvent extends HistoryEventBase { + type: HistoryEventType.TaskCompleted; + taskScheduledId: number; + result?: string; +} + +/** + * Event that marks the failure of an activity task. + */ +export interface TaskFailedEvent extends HistoryEventBase { + type: HistoryEventType.TaskFailed; + taskScheduledId: number; + failureDetails?: FailureDetails; +} + +/** + * Event that marks the creation of a sub-orchestration instance. + */ +export interface SubOrchestrationInstanceCreatedEvent extends HistoryEventBase { + type: HistoryEventType.SubOrchestrationInstanceCreated; + name: string; + version?: string; + instanceId?: string; + input?: string; + tags?: Record; +} + +/** + * Event that marks the completion of a sub-orchestration instance. + */ +export interface SubOrchestrationInstanceCompletedEvent extends HistoryEventBase { + type: HistoryEventType.SubOrchestrationInstanceCompleted; + taskScheduledId: number; + result?: string; +} + +/** + * Event that marks the failure of a sub-orchestration instance. + */ +export interface SubOrchestrationInstanceFailedEvent extends HistoryEventBase { + type: HistoryEventType.SubOrchestrationInstanceFailed; + taskScheduledId: number; + failureDetails?: FailureDetails; +} + +/** + * Event that marks the creation of a timer. + */ +export interface TimerCreatedEvent extends HistoryEventBase { + type: HistoryEventType.TimerCreated; + fireAt: Date; +} + +/** + * Event that marks the firing of a timer. + */ +export interface TimerFiredEvent extends HistoryEventBase { + type: HistoryEventType.TimerFired; + fireAt: Date; + timerId: number; +} + +/** + * Event that marks the start of an orchestrator replay. + */ +export interface OrchestratorStartedEvent extends HistoryEventBase { + type: HistoryEventType.OrchestratorStarted; +} + +/** + * Event that marks the completion of an orchestrator replay. + */ +export interface OrchestratorCompletedEvent extends HistoryEventBase { + type: HistoryEventType.OrchestratorCompleted; +} + +/** + * Event that marks the sending of an event to another orchestration. + */ +export interface EventSentEvent extends HistoryEventBase { + type: HistoryEventType.EventSent; + name: string; + instanceId?: string; + input?: string; +} + +/** + * Event that marks the receiving of an external event. + */ +export interface EventRaisedEvent extends HistoryEventBase { + type: HistoryEventType.EventRaised; + name: string; + input?: string; +} + +/** + * Generic event for extensibility. + */ +export interface GenericEvent extends HistoryEventBase { + type: HistoryEventType.GenericEvent; + data?: string; +} + +/** + * Event that captures the history state. + */ +export interface HistoryStateEvent extends HistoryEventBase { + type: HistoryEventType.HistoryState; +} + +/** + * Event that marks continue-as-new. + */ +export interface ContinueAsNewEvent extends HistoryEventBase { + type: HistoryEventType.ContinueAsNew; + input?: string; +} + +/** + * Event for entity operation signaled. + */ +export interface EntityOperationSignaledEvent extends HistoryEventBase { + type: HistoryEventType.EntityOperationSignaled; + requestId: string; + operation: string; + targetInstanceId?: string; + scheduledTime?: Date; + input?: string; +} + +/** + * Event for entity operation called. + */ +export interface EntityOperationCalledEvent extends HistoryEventBase { + type: HistoryEventType.EntityOperationCalled; + requestId: string; + operation: string; + targetInstanceId?: string; + parentInstanceId?: string; + scheduledTime?: Date; + input?: string; +} + +/** + * Event for entity operation completed. + */ +export interface EntityOperationCompletedEvent extends HistoryEventBase { + type: HistoryEventType.EntityOperationCompleted; + requestId: string; + output?: string; +} + +/** + * Event for entity operation failed. + */ +export interface EntityOperationFailedEvent extends HistoryEventBase { + type: HistoryEventType.EntityOperationFailed; + requestId: string; + failureDetails?: FailureDetails; +} + +/** + * Event for entity lock requested. + */ +export interface EntityLockRequestedEvent extends HistoryEventBase { + type: HistoryEventType.EntityLockRequested; + criticalSectionId: string; + lockSet: string[]; + position: number; + parentInstanceId?: string; +} + +/** + * Event for entity lock granted. + */ +export interface EntityLockGrantedEvent extends HistoryEventBase { + type: HistoryEventType.EntityLockGranted; + criticalSectionId: string; +} + +/** + * Event for entity unlock sent. + */ +export interface EntityUnlockSentEvent extends HistoryEventBase { + type: HistoryEventType.EntityUnlockSent; + criticalSectionId: string; + parentInstanceId?: string; + targetInstanceId?: string; +} + +/** + * Union type of all history events. + */ +export type HistoryEvent = + | ExecutionStartedEvent + | ExecutionCompletedEvent + | ExecutionTerminatedEvent + | ExecutionSuspendedEvent + | ExecutionResumedEvent + | ExecutionRewoundEvent + | TaskScheduledEvent + | TaskCompletedEvent + | TaskFailedEvent + | SubOrchestrationInstanceCreatedEvent + | SubOrchestrationInstanceCompletedEvent + | SubOrchestrationInstanceFailedEvent + | TimerCreatedEvent + | TimerFiredEvent + | OrchestratorStartedEvent + | OrchestratorCompletedEvent + | EventSentEvent + | EventRaisedEvent + | GenericEvent + | HistoryStateEvent + | ContinueAsNewEvent + | EntityOperationSignaledEvent + | EntityOperationCalledEvent + | EntityOperationCompletedEvent + | EntityOperationFailedEvent + | EntityLockRequestedEvent + | EntityLockGrantedEvent + | EntityUnlockSentEvent; diff --git a/packages/durabletask-js/src/utils/history-event-converter.ts b/packages/durabletask-js/src/utils/history-event-converter.ts new file mode 100644 index 0000000..be6fb70 --- /dev/null +++ b/packages/durabletask-js/src/utils/history-event-converter.ts @@ -0,0 +1,460 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as pb from "../proto/orchestrator_service_pb"; +import { + HistoryEvent, + HistoryEventType, + OrchestrationInstance, + ParentInstanceInfo, + TraceContext, +} from "../orchestration/history-event"; +import { FailureDetails } from "../task/failure-details"; + +/** + * Converts a protobuf HistoryEvent to a TypeScript HistoryEvent. + * @param protoEvent The protobuf HistoryEvent to convert. + * @returns The converted HistoryEvent, or undefined if the event type is not recognized. + */ +export function convertProtoHistoryEvent(protoEvent: pb.HistoryEvent): HistoryEvent | undefined { + const eventId = protoEvent.getEventid(); + const timestamp = protoEvent.getTimestamp()?.toDate() ?? new Date(0); + const eventTypeCase = protoEvent.getEventtypeCase(); + + switch (eventTypeCase) { + case pb.HistoryEvent.EventtypeCase.EXECUTIONSTARTED: { + const event = protoEvent.getExecutionstarted(); + if (!event) return undefined; + + const orchInstance = event.getOrchestrationinstance(); + const parentInfo = event.getParentinstance(); + const scheduledTime = event.getScheduledstarttimestamp(); + const tagsMap = event.getTagsMap(); + + return { + eventId, + timestamp, + type: HistoryEventType.ExecutionStarted, + name: event.getName(), + version: event.getVersion()?.getValue(), + input: event.getInput()?.getValue(), + orchestrationInstance: orchInstance ? convertOrchestrationInstance(orchInstance) : undefined, + parentInstance: parentInfo ? convertParentInstanceInfo(parentInfo) : undefined, + scheduledStartTimestamp: scheduledTime ? scheduledTime.toDate() : undefined, + tags: tagsMap ? convertTagsMap(tagsMap) : undefined, + }; + } + + case pb.HistoryEvent.EventtypeCase.EXECUTIONCOMPLETED: { + const event = protoEvent.getExecutioncompleted(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.ExecutionCompleted, + orchestrationStatus: pb.OrchestrationStatus[event.getOrchestrationstatus()], + result: event.getResult()?.getValue(), + failureDetails: convertFailureDetails(event.getFailuredetails()), + }; + } + + case pb.HistoryEvent.EventtypeCase.EXECUTIONTERMINATED: { + const event = protoEvent.getExecutionterminated(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.ExecutionTerminated, + input: event.getInput()?.getValue(), + recurse: event.getRecurse(), + }; + } + + case pb.HistoryEvent.EventtypeCase.EXECUTIONSUSPENDED: { + const event = protoEvent.getExecutionsuspended(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.ExecutionSuspended, + input: event.getInput()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.EXECUTIONRESUMED: { + const event = protoEvent.getExecutionresumed(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.ExecutionResumed, + input: event.getInput()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.EXECUTIONREWOUND: { + const event = protoEvent.getExecutionrewound(); + if (!event) return undefined; + + const parentInfo = event.getParentinstance(); + const traceCtx = event.getParenttracecontext(); + const tagsMap = event.getTagsMap(); + + return { + eventId, + timestamp, + type: HistoryEventType.ExecutionRewound, + reason: event.getReason()?.getValue(), + parentExecutionId: event.getParentexecutionid()?.getValue(), + instanceId: event.getInstanceid()?.getValue(), + parentTraceContext: traceCtx ? convertTraceContext(traceCtx) : undefined, + name: event.getName()?.getValue(), + version: event.getVersion()?.getValue(), + input: event.getInput()?.getValue(), + parentInstance: parentInfo ? convertParentInstanceInfo(parentInfo) : undefined, + tags: tagsMap && tagsMap.getLength() > 0 ? convertTagsMap(tagsMap) : undefined, + }; + } + + case pb.HistoryEvent.EventtypeCase.TASKSCHEDULED: { + const event = protoEvent.getTaskscheduled(); + if (!event) return undefined; + + const tagsMap = event.getTagsMap(); + + return { + eventId, + timestamp, + type: HistoryEventType.TaskScheduled, + name: event.getName(), + version: event.getVersion()?.getValue(), + input: event.getInput()?.getValue(), + tags: tagsMap ? convertTagsMap(tagsMap) : undefined, + }; + } + + case pb.HistoryEvent.EventtypeCase.TASKCOMPLETED: { + const event = protoEvent.getTaskcompleted(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.TaskCompleted, + taskScheduledId: event.getTaskscheduledid(), + result: event.getResult()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.TASKFAILED: { + const event = protoEvent.getTaskfailed(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.TaskFailed, + taskScheduledId: event.getTaskscheduledid(), + failureDetails: convertFailureDetails(event.getFailuredetails()), + }; + } + + case pb.HistoryEvent.EventtypeCase.SUBORCHESTRATIONINSTANCECREATED: { + const event = protoEvent.getSuborchestrationinstancecreated(); + if (!event) return undefined; + + const tagsMap = event.getTagsMap(); + + return { + eventId, + timestamp, + type: HistoryEventType.SubOrchestrationInstanceCreated, + name: event.getName(), + version: event.getVersion()?.getValue(), + instanceId: event.getInstanceid(), + input: event.getInput()?.getValue(), + tags: tagsMap ? convertTagsMap(tagsMap) : undefined, + }; + } + + case pb.HistoryEvent.EventtypeCase.SUBORCHESTRATIONINSTANCECOMPLETED: { + const event = protoEvent.getSuborchestrationinstancecompleted(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.SubOrchestrationInstanceCompleted, + taskScheduledId: event.getTaskscheduledid(), + result: event.getResult()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.SUBORCHESTRATIONINSTANCEFAILED: { + const event = protoEvent.getSuborchestrationinstancefailed(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.SubOrchestrationInstanceFailed, + taskScheduledId: event.getTaskscheduledid(), + failureDetails: convertFailureDetails(event.getFailuredetails()), + }; + } + + case pb.HistoryEvent.EventtypeCase.TIMERCREATED: { + const event = protoEvent.getTimercreated(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.TimerCreated, + fireAt: event.getFireat()?.toDate() ?? new Date(0), + }; + } + + case pb.HistoryEvent.EventtypeCase.TIMERFIRED: { + const event = protoEvent.getTimerfired(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.TimerFired, + fireAt: event.getFireat()?.toDate() ?? new Date(0), + timerId: event.getTimerid(), + }; + } + + case pb.HistoryEvent.EventtypeCase.ORCHESTRATORSTARTED: { + return { + eventId, + timestamp, + type: HistoryEventType.OrchestratorStarted, + }; + } + + case pb.HistoryEvent.EventtypeCase.ORCHESTRATORCOMPLETED: { + return { + eventId, + timestamp, + type: HistoryEventType.OrchestratorCompleted, + }; + } + + case pb.HistoryEvent.EventtypeCase.EVENTSENT: { + const event = protoEvent.getEventsent(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.EventSent, + name: event.getName(), + instanceId: event.getInstanceid(), + input: event.getInput()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.EVENTRAISED: { + const event = protoEvent.getEventraised(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.EventRaised, + name: event.getName(), + input: event.getInput()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.GENERICEVENT: { + const event = protoEvent.getGenericevent(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.GenericEvent, + data: event.getData()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.HISTORYSTATE: { + return { + eventId, + timestamp, + type: HistoryEventType.HistoryState, + }; + } + + case pb.HistoryEvent.EventtypeCase.CONTINUEASNEW: { + const event = protoEvent.getContinueasnew(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.ContinueAsNew, + input: event.getInput()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.ENTITYOPERATIONSIGNALED: { + const event = protoEvent.getEntityoperationsignaled(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.EntityOperationSignaled, + requestId: event.getRequestid(), + operation: event.getOperation(), + targetInstanceId: event.getTargetinstanceid()?.getValue(), + scheduledTime: event.getScheduledtime()?.toDate(), + input: event.getInput()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.ENTITYOPERATIONCALLED: { + const event = protoEvent.getEntityoperationcalled(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.EntityOperationCalled, + requestId: event.getRequestid(), + operation: event.getOperation(), + targetInstanceId: event.getTargetinstanceid()?.getValue(), + parentInstanceId: event.getParentinstanceid()?.getValue(), + scheduledTime: event.getScheduledtime()?.toDate(), + input: event.getInput()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.ENTITYOPERATIONCOMPLETED: { + const event = protoEvent.getEntityoperationcompleted(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.EntityOperationCompleted, + requestId: event.getRequestid(), + output: event.getOutput()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.ENTITYOPERATIONFAILED: { + const event = protoEvent.getEntityoperationfailed(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.EntityOperationFailed, + requestId: event.getRequestid(), + failureDetails: convertFailureDetails(event.getFailuredetails()), + }; + } + + case pb.HistoryEvent.EventtypeCase.ENTITYLOCKREQUESTED: { + const event = protoEvent.getEntitylockrequested(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.EntityLockRequested, + criticalSectionId: event.getCriticalsectionid(), + lockSet: event.getLocksetList(), + position: event.getPosition(), + parentInstanceId: event.getParentinstanceid()?.getValue(), + }; + } + + case pb.HistoryEvent.EventtypeCase.ENTITYLOCKGRANTED: { + const event = protoEvent.getEntitylockgranted(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.EntityLockGranted, + criticalSectionId: event.getCriticalsectionid(), + }; + } + + case pb.HistoryEvent.EventtypeCase.ENTITYUNLOCKSENT: { + const event = protoEvent.getEntityunlocksent(); + if (!event) return undefined; + + return { + eventId, + timestamp, + type: HistoryEventType.EntityUnlockSent, + criticalSectionId: event.getCriticalsectionid(), + parentInstanceId: event.getParentinstanceid()?.getValue(), + targetInstanceId: event.getTargetinstanceid()?.getValue(), + }; + } + + default: + return undefined; + } +} + +function convertOrchestrationInstance(instance: pb.OrchestrationInstance): OrchestrationInstance { + return { + instanceId: instance.getInstanceid(), + executionId: instance.getExecutionid()?.getValue(), + }; +} + +function convertParentInstanceInfo(parent: pb.ParentInstanceInfo): ParentInstanceInfo { + const orchInstance = parent.getOrchestrationinstance(); + return { + name: parent.getName()?.getValue(), + version: parent.getVersion()?.getValue(), + taskScheduledId: parent.getTaskscheduledid(), + orchestrationInstance: orchInstance ? convertOrchestrationInstance(orchInstance) : undefined, + }; +} + +function convertTraceContext(traceContext: pb.TraceContext): TraceContext { + return { + traceParent: traceContext.getTraceparent(), + spanId: traceContext.getSpanid(), + traceState: traceContext.getTracestate()?.getValue(), + }; +} + +function convertFailureDetails(details: pb.TaskFailureDetails | undefined): FailureDetails | undefined { + if (!details) return undefined; + + return new FailureDetails( + details.getErrormessage(), + details.getErrortype(), + details.getStacktrace()?.getValue(), + ); +} + +function convertTagsMap(tagsMap: ReturnType): Record | undefined { + const result: Record = {}; + let hasEntries = false; + + tagsMap.forEach((value: string, key: string) => { + result[key] = value; + hasEntries = true; + }); + + return hasEntries ? result : undefined; +} diff --git a/test/e2e-azuremanaged/history.spec.ts b/test/e2e-azuremanaged/history.spec.ts new file mode 100644 index 0000000..d4e8733 --- /dev/null +++ b/test/e2e-azuremanaged/history.spec.ts @@ -0,0 +1,381 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * E2E tests for getOrchestrationHistory in Durable Task Scheduler (DTS). + * + * Environment variables (choose one): + * - DTS_CONNECTION_STRING: Full connection string (e.g., "Endpoint=https://...;Authentication=DefaultAzure;TaskHub=...") + * OR + * - ENDPOINT: The endpoint for the DTS emulator (default: localhost:8080) + * - TASKHUB: The task hub name (default: default) + */ + +import { + TaskHubGrpcClient, + TaskHubGrpcWorker, + getName, + whenAll, + ActivityContext, + OrchestrationContext, + Task, + TOrchestrator, + HistoryEventType, + ExecutionStartedEvent, + ExecutionCompletedEvent, + TaskScheduledEvent, + TaskCompletedEvent, + TimerCreatedEvent, + TimerFiredEvent, + SubOrchestrationInstanceCreatedEvent, + SubOrchestrationInstanceCompletedEvent, + EventRaisedEvent, +} from "@microsoft/durabletask-js"; +import { + DurableTaskAzureManagedClientBuilder, + DurableTaskAzureManagedWorkerBuilder, +} from "@microsoft/durabletask-js-azuremanaged"; + +// Read environment variables +const connectionString = process.env.DTS_CONNECTION_STRING; +const endpoint = process.env.ENDPOINT || "localhost:8080"; +const taskHub = process.env.TASKHUB || "default"; + +function createClient(): TaskHubGrpcClient { + if (connectionString) { + return new DurableTaskAzureManagedClientBuilder().connectionString(connectionString).build(); + } + return new DurableTaskAzureManagedClientBuilder() + .endpoint(endpoint, taskHub, null) // null credential for emulator (no auth) + .build(); +} + +function createWorker(): TaskHubGrpcWorker { + if (connectionString) { + return new DurableTaskAzureManagedWorkerBuilder().connectionString(connectionString).build(); + } + return new DurableTaskAzureManagedWorkerBuilder() + .endpoint(endpoint, taskHub, null) // null credential for emulator (no auth) + .build(); +} + +describe("getOrchestrationHistory E2E Tests", () => { + let taskHubClient: TaskHubGrpcClient; + let taskHubWorker: TaskHubGrpcWorker; + + beforeEach(async () => { + taskHubClient = createClient(); + taskHubWorker = createWorker(); + }); + + afterEach(async () => { + try { + await taskHubWorker.stop(); + } catch { + // Worker may not have been started in some tests + } + await taskHubClient.stop(); + }); + + it("should retrieve history for a simple orchestration", async () => { + const simpleOrchestrator: TOrchestrator = async (_: OrchestrationContext) => { + return "Hello, World!"; + }; + + taskHubWorker.addOrchestrator(simpleOrchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(simpleOrchestrator); + await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + const history = await taskHubClient.getOrchestrationHistory(id); + + expect(history).toBeDefined(); + expect(Array.isArray(history)).toBe(true); + expect(history.length).toBeGreaterThan(0); + + // Check for ExecutionStarted event + const startedEvents = history.filter(e => e.type === HistoryEventType.ExecutionStarted) as ExecutionStartedEvent[]; + expect(startedEvents.length).toBe(1); + expect(startedEvents[0].name).toBe(getName(simpleOrchestrator)); + + // Check for ExecutionCompleted event + const completedEvents = history.filter(e => e.type === HistoryEventType.ExecutionCompleted) as ExecutionCompletedEvent[]; + expect(completedEvents.length).toBe(1); + expect(completedEvents[0].result).toBe(JSON.stringify("Hello, World!")); + }, 31000); + + it("should retrieve history for an orchestration with activities", async () => { + const addOne = async (_: ActivityContext, input: number) => { + return input + 1; + }; + + const activityOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, startVal: number): any { + const result = yield ctx.callActivity(addOne, startVal); + return result; + }; + + taskHubWorker.addOrchestrator(activityOrchestrator); + taskHubWorker.addActivity(addOne); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(activityOrchestrator, 5); + await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + const history = await taskHubClient.getOrchestrationHistory(id); + + expect(history).toBeDefined(); + expect(history.length).toBeGreaterThan(0); + + // Check for TaskScheduled event + const scheduledEvents = history.filter(e => e.type === HistoryEventType.TaskScheduled) as TaskScheduledEvent[]; + expect(scheduledEvents.length).toBe(1); + expect(scheduledEvents[0].name).toBe(getName(addOne)); + expect(scheduledEvents[0].input).toBe(JSON.stringify(5)); + + // Check for TaskCompleted event + const taskCompletedEvents = history.filter(e => e.type === HistoryEventType.TaskCompleted) as TaskCompletedEvent[]; + expect(taskCompletedEvents.length).toBe(1); + expect(taskCompletedEvents[0].result).toBe(JSON.stringify(6)); + }, 31000); + + it("should retrieve history for an orchestration with a timer", async () => { + const timerOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const fireAt = new Date(ctx.currentUtcDateTime.getTime() + 1000); // 1 second timer + yield ctx.createTimer(fireAt); + return "Timer completed"; + }; + + taskHubWorker.addOrchestrator(timerOrchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(timerOrchestrator); + await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + const history = await taskHubClient.getOrchestrationHistory(id); + + expect(history).toBeDefined(); + expect(history.length).toBeGreaterThan(0); + + // Check for TimerCreated event + const timerCreatedEvents = history.filter(e => e.type === HistoryEventType.TimerCreated) as TimerCreatedEvent[]; + expect(timerCreatedEvents.length).toBe(1); + expect(timerCreatedEvents[0].fireAt).toBeDefined(); + + // Check for TimerFired event + const timerFiredEvents = history.filter(e => e.type === HistoryEventType.TimerFired) as TimerFiredEvent[]; + expect(timerFiredEvents.length).toBe(1); + }, 31000); + + it("should retrieve history for an orchestration with sub-orchestration", async () => { + const childOrchestrator: TOrchestrator = async (_: OrchestrationContext) => { + return "Child completed"; + }; + + const parentOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const result = yield ctx.callSubOrchestrator(childOrchestrator); + return result; + }; + + taskHubWorker.addOrchestrator(childOrchestrator); + taskHubWorker.addOrchestrator(parentOrchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(parentOrchestrator); + await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + const history = await taskHubClient.getOrchestrationHistory(id); + + expect(history).toBeDefined(); + expect(history.length).toBeGreaterThan(0); + + // Check for SubOrchestrationInstanceCreated event + const subOrchCreatedEvents = history.filter(e => e.type === HistoryEventType.SubOrchestrationInstanceCreated) as SubOrchestrationInstanceCreatedEvent[]; + expect(subOrchCreatedEvents.length).toBe(1); + expect(subOrchCreatedEvents[0].name).toBe(getName(childOrchestrator)); + + // Check for SubOrchestrationInstanceCompleted event + const subOrchCompletedEvents = history.filter(e => e.type === HistoryEventType.SubOrchestrationInstanceCompleted) as SubOrchestrationInstanceCompletedEvent[]; + expect(subOrchCompletedEvents.length).toBe(1); + expect(subOrchCompletedEvents[0].result).toBe(JSON.stringify("Child completed")); + }, 31000); + + it("should retrieve history for an orchestration with external events", async () => { + const eventOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const event = yield ctx.waitForExternalEvent("MyEvent"); + return event; + }; + + taskHubWorker.addOrchestrator(eventOrchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(eventOrchestrator); + + // Wait a bit for the orchestration to start + await new Promise(resolve => setTimeout(resolve, 1000)); + + await taskHubClient.raiseOrchestrationEvent(id, "MyEvent", { data: "event payload" }); + await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + const history = await taskHubClient.getOrchestrationHistory(id); + + expect(history).toBeDefined(); + expect(history.length).toBeGreaterThan(0); + + // Check for EventRaised event + const eventRaisedEvents = history.filter(e => e.type === HistoryEventType.EventRaised) as EventRaisedEvent[]; + expect(eventRaisedEvents.length).toBe(1); + expect(eventRaisedEvents[0].name).toBe("MyEvent"); + expect(eventRaisedEvents[0].input).toBe(JSON.stringify({ data: "event payload" })); + }, 31000); + + it("should retrieve history for an orchestration with multiple activities", async () => { + const activity1 = async (_: ActivityContext) => "Activity 1 result"; + const activity2 = async (_: ActivityContext) => "Activity 2 result"; + + const multiActivityOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const tasks: Task[] = [ + ctx.callActivity(activity1), + ctx.callActivity(activity2), + ]; + const results = yield whenAll(tasks); + return results; + }; + + taskHubWorker.addOrchestrator(multiActivityOrchestrator); + taskHubWorker.addActivity(activity1); + taskHubWorker.addActivity(activity2); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(multiActivityOrchestrator); + await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + const history = await taskHubClient.getOrchestrationHistory(id); + + expect(history).toBeDefined(); + expect(history.length).toBeGreaterThan(0); + + // Check for TaskScheduled events + const scheduledEvents = history.filter(e => e.type === HistoryEventType.TaskScheduled) as TaskScheduledEvent[]; + expect(scheduledEvents.length).toBe(2); + + // Check for TaskCompleted events + const taskCompletedEvents = history.filter(e => e.type === HistoryEventType.TaskCompleted) as TaskCompletedEvent[]; + expect(taskCompletedEvents.length).toBe(2); + }, 31000); + + it("should return empty array for non-existent orchestration", async () => { + // Note: The DTS emulator returns an empty history for non-existent instances + // rather than throwing an error. This is different from the .NET SDK behavior. + const history = await taskHubClient.getOrchestrationHistory("non-existent-instance-id"); + expect(history).toEqual([]); + }, 31000); + + it("should throw error for empty instanceId", async () => { + await expect(taskHubClient.getOrchestrationHistory("")) + .rejects + .toThrow("instanceId is required"); + }, 5000); + + it("should have correct eventId ordering in history", async () => { + const addOne = async (_: ActivityContext, input: number) => input + 1; + + const sequenceOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + let value = 0; + for (let i = 0; i < 3; i++) { + value = yield ctx.callActivity(addOne, value); + } + return value; + }; + + taskHubWorker.addOrchestrator(sequenceOrchestrator); + taskHubWorker.addActivity(addOne); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(sequenceOrchestrator); + await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + const history = await taskHubClient.getOrchestrationHistory(id); + + expect(history).toBeDefined(); + expect(history.length).toBeGreaterThan(0); + + // Verify that eventIds are generally increasing (note: some events may have eventId -1) + // Filter out events with -1 eventId and verify remaining ones are in order + const eventsWithPositiveIds = history.filter(e => e.eventId >= 0); + let previousEventId = -1; + for (const event of eventsWithPositiveIds) { + expect(event.eventId).toBeGreaterThan(previousEventId); + previousEventId = event.eventId; + } + + // Verify timestamps are present and valid + for (const event of history) { + expect(event.timestamp).toBeDefined(); + expect(event.timestamp instanceof Date).toBe(true); + } + }, 31000); + + it("should have OrchestratorStarted events in history", async () => { + const simpleOrchestrator: TOrchestrator = async (_: OrchestrationContext) => { + return "Done"; + }; + + taskHubWorker.addOrchestrator(simpleOrchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(simpleOrchestrator); + await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + const history = await taskHubClient.getOrchestrationHistory(id); + + expect(history).toBeDefined(); + + // Check for OrchestratorStarted events (there may be multiple due to replay) + const orchestratorStartedEvents = history.filter(e => e.type === HistoryEventType.OrchestratorStarted); + expect(orchestratorStartedEvents.length).toBeGreaterThanOrEqual(1); + + // Check that ExecutionStarted and ExecutionCompleted are present + const executionStartedEvents = history.filter(e => e.type === HistoryEventType.ExecutionStarted); + expect(executionStartedEvents.length).toBe(1); + + const executionCompletedEvents = history.filter(e => e.type === HistoryEventType.ExecutionCompleted); + expect(executionCompletedEvents.length).toBe(1); + }, 31000); + + it("should include tags in ExecutionStarted event", async () => { + const taggedOrchestrator: TOrchestrator = async (_: OrchestrationContext) => { + return "Tagged result"; + }; + + taskHubWorker.addOrchestrator(taggedOrchestrator); + await taskHubWorker.start(); + + const tags = { + "environment": "test", + "owner": "copilot", + "priority": "high" + }; + + const id = await taskHubClient.scheduleNewOrchestration(taggedOrchestrator, { + tags: tags + }); + await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + const history = await taskHubClient.getOrchestrationHistory(id); + + expect(history).toBeDefined(); + expect(history.length).toBeGreaterThan(0); + + // Check for ExecutionStarted event with tags + const startedEvents = history.filter(e => e.type === HistoryEventType.ExecutionStarted) as ExecutionStartedEvent[]; + expect(startedEvents.length).toBe(1); + + const startedEvent = startedEvents[0]; + expect(startedEvent.tags).toBeDefined(); + expect(startedEvent.tags).toEqual(tags); + expect(startedEvent.tags?.["environment"]).toBe("test"); + expect(startedEvent.tags?.["owner"]).toBe("copilot"); + expect(startedEvent.tags?.["priority"]).toBe("high"); + }, 31000); +}); From 97cee76fdc732d3d46bbf6523f049fcace7ebf86 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:41:08 -0800 Subject: [PATCH 2/9] test --- .../src/utils/history-event-converter.ts | 18 +- test/e2e-azuremanaged/history.spec.ts | 177 ++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) diff --git a/packages/durabletask-js/src/utils/history-event-converter.ts b/packages/durabletask-js/src/utils/history-event-converter.ts index be6fb70..73a74f9 100644 --- a/packages/durabletask-js/src/utils/history-event-converter.ts +++ b/packages/durabletask-js/src/utils/history-event-converter.ts @@ -11,6 +11,22 @@ import { } from "../orchestration/history-event"; import { FailureDetails } from "../task/failure-details"; +// Map OrchestrationStatus enum values to their string names +const ORCHESTRATION_STATUS_MAP: Record = { + [pb.OrchestrationStatus.ORCHESTRATION_STATUS_RUNNING]: "ORCHESTRATION_STATUS_RUNNING", + [pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED]: "ORCHESTRATION_STATUS_COMPLETED", + [pb.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW]: "ORCHESTRATION_STATUS_CONTINUED_AS_NEW", + [pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED]: "ORCHESTRATION_STATUS_FAILED", + [pb.OrchestrationStatus.ORCHESTRATION_STATUS_CANCELED]: "ORCHESTRATION_STATUS_CANCELED", + [pb.OrchestrationStatus.ORCHESTRATION_STATUS_TERMINATED]: "ORCHESTRATION_STATUS_TERMINATED", + [pb.OrchestrationStatus.ORCHESTRATION_STATUS_PENDING]: "ORCHESTRATION_STATUS_PENDING", + [pb.OrchestrationStatus.ORCHESTRATION_STATUS_SUSPENDED]: "ORCHESTRATION_STATUS_SUSPENDED", +}; + +function convertOrchestrationStatus(status: number): string { + return ORCHESTRATION_STATUS_MAP[status] ?? `UNKNOWN_STATUS_${status}`; +} + /** * Converts a protobuf HistoryEvent to a TypeScript HistoryEvent. * @param protoEvent The protobuf HistoryEvent to convert. @@ -53,7 +69,7 @@ export function convertProtoHistoryEvent(protoEvent: pb.HistoryEvent): HistoryEv eventId, timestamp, type: HistoryEventType.ExecutionCompleted, - orchestrationStatus: pb.OrchestrationStatus[event.getOrchestrationstatus()], + orchestrationStatus: convertOrchestrationStatus(event.getOrchestrationstatus()), result: event.getResult()?.getValue(), failureDetails: convertFailureDetails(event.getFailuredetails()), }; diff --git a/test/e2e-azuremanaged/history.spec.ts b/test/e2e-azuremanaged/history.spec.ts index d4e8733..2772ceb 100644 --- a/test/e2e-azuremanaged/history.spec.ts +++ b/test/e2e-azuremanaged/history.spec.ts @@ -378,4 +378,181 @@ describe("getOrchestrationHistory E2E Tests", () => { expect(startedEvent.tags?.["owner"]).toBe("copilot"); expect(startedEvent.tags?.["priority"]).toBe("high"); }, 31000); + + it("should validate complete history event sequence for orchestration with activity, sub-orchestration, and timer", async () => { + // Define activity + const greetActivity = async (_: ActivityContext, name: string) => { + return `Hello, ${name}!`; + }; + + // Define child orchestrator + const childOrchestrator: TOrchestrator = async (_: OrchestrationContext) => { + return "Child completed"; + }; + + // Define parent orchestrator that uses activity, sub-orchestration, and timer + const complexOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + // Step 1: Call an activity + const greeting: string = yield ctx.callActivity(greetActivity, "World"); + + // Step 2: Create a timer (short delay) + const fireAt = new Date(ctx.currentUtcDateTime.getTime() + 1000); + yield ctx.createTimer(fireAt); + + // Step 3: Call a sub-orchestration + const childResult: string = yield ctx.callSubOrchestrator(childOrchestrator); + + return { greeting, childResult }; + }; + + taskHubWorker.addActivity(greetActivity); + taskHubWorker.addOrchestrator(childOrchestrator); + taskHubWorker.addOrchestrator(complexOrchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(complexOrchestrator); + await taskHubClient.waitForOrchestrationCompletion(id, undefined, 60); + + const history = await taskHubClient.getOrchestrationHistory(id); + + expect(history).toBeDefined(); + expect(Array.isArray(history)).toBe(true); + + // Validate the complete history event sequence + // Expected sequence of significant events (ignoring OrchestratorStarted/Completed which may repeat): + // 1. ExecutionStarted + // 2. TaskScheduled (activity) + // 3. TaskCompleted (activity) + // 4. TimerCreated + // 5. TimerFired + // 6. SubOrchestrationInstanceCreated + // 7. SubOrchestrationInstanceCompleted + // 8. ExecutionCompleted + + // === ExecutionStarted === + const executionStartedEvents = history.filter(e => e.type === HistoryEventType.ExecutionStarted) as ExecutionStartedEvent[]; + expect(executionStartedEvents.length).toBe(1); + expect(executionStartedEvents[0].name).toBe(getName(complexOrchestrator)); + expect(executionStartedEvents[0].eventId).toBeDefined(); + expect(executionStartedEvents[0].timestamp).toBeInstanceOf(Date); + + // === TaskScheduled (activity) === + const taskScheduledEvents = history.filter(e => e.type === HistoryEventType.TaskScheduled) as TaskScheduledEvent[]; + expect(taskScheduledEvents.length).toBe(1); + expect(taskScheduledEvents[0].name).toBe(getName(greetActivity)); + expect(taskScheduledEvents[0].input).toBe(JSON.stringify("World")); + + // === TaskCompleted (activity) === + const taskCompletedEvents = history.filter(e => e.type === HistoryEventType.TaskCompleted) as TaskCompletedEvent[]; + expect(taskCompletedEvents.length).toBe(1); + expect(taskCompletedEvents[0].taskScheduledId).toBe(taskScheduledEvents[0].eventId); + expect(taskCompletedEvents[0].result).toBe(JSON.stringify("Hello, World!")); + + // === TimerCreated === + const timerCreatedEvents = history.filter(e => e.type === HistoryEventType.TimerCreated) as TimerCreatedEvent[]; + expect(timerCreatedEvents.length).toBe(1); + expect(timerCreatedEvents[0].fireAt).toBeInstanceOf(Date); + + // === TimerFired === + const timerFiredEvents = history.filter(e => e.type === HistoryEventType.TimerFired) as TimerFiredEvent[]; + expect(timerFiredEvents.length).toBe(1); + expect(timerFiredEvents[0].fireAt).toBeInstanceOf(Date); + expect(timerFiredEvents[0].timerId).toBe(timerCreatedEvents[0].eventId); + + // === SubOrchestrationInstanceCreated === + const subOrchCreatedEvents = history.filter(e => e.type === HistoryEventType.SubOrchestrationInstanceCreated) as SubOrchestrationInstanceCreatedEvent[]; + expect(subOrchCreatedEvents.length).toBe(1); + expect(subOrchCreatedEvents[0].name).toBe(getName(childOrchestrator)); + expect(subOrchCreatedEvents[0].instanceId).toBeDefined(); + + // === SubOrchestrationInstanceCompleted === + const subOrchCompletedEvents = history.filter(e => e.type === HistoryEventType.SubOrchestrationInstanceCompleted) as SubOrchestrationInstanceCompletedEvent[]; + expect(subOrchCompletedEvents.length).toBe(1); + expect(subOrchCompletedEvents[0].taskScheduledId).toBe(subOrchCreatedEvents[0].eventId); + expect(subOrchCompletedEvents[0].result).toBe(JSON.stringify("Child completed")); + + // === ExecutionCompleted === + const executionCompletedEvents = history.filter(e => e.type === HistoryEventType.ExecutionCompleted) as ExecutionCompletedEvent[]; + expect(executionCompletedEvents.length).toBe(1); + expect(executionCompletedEvents[0].orchestrationStatus).toBe("ORCHESTRATION_STATUS_COMPLETED"); + const finalResult = JSON.parse(executionCompletedEvents[0].result!); + expect(finalResult.greeting).toBe("Hello, World!"); + expect(finalResult.childResult).toBe("Child completed"); + + // === Validate EXACT event positions in history array === + // The history array should contain events at exact positions. + // Expected sequence (based on observed DTS behavior): + // Index 0: OrchestratorStarted + // Index 1: ExecutionStarted + // Index 2: TaskScheduled + // Index 3: OrchestratorStarted (replay) + // Index 4: TaskCompleted + // Index 5: TimerCreated + // Index 6: OrchestratorStarted (replay) + // Index 7: TimerFired + // Index 8: SubOrchestrationInstanceCreated + // Index 9: OrchestratorStarted (replay) + // Index 10: SubOrchestrationInstanceCompleted + // Index 11: ExecutionCompleted + + expect(history.length).toBe(12); + + // Validate exact position of each event + expect(history[0].type).toBe(HistoryEventType.OrchestratorStarted); + expect(history[1].type).toBe(HistoryEventType.ExecutionStarted); + expect(history[2].type).toBe(HistoryEventType.TaskScheduled); + expect(history[3].type).toBe(HistoryEventType.OrchestratorStarted); + expect(history[4].type).toBe(HistoryEventType.TaskCompleted); + expect(history[5].type).toBe(HistoryEventType.TimerCreated); + expect(history[6].type).toBe(HistoryEventType.OrchestratorStarted); + expect(history[7].type).toBe(HistoryEventType.TimerFired); + expect(history[8].type).toBe(HistoryEventType.SubOrchestrationInstanceCreated); + expect(history[9].type).toBe(HistoryEventType.OrchestratorStarted); + expect(history[10].type).toBe(HistoryEventType.SubOrchestrationInstanceCompleted); + expect(history[11].type).toBe(HistoryEventType.ExecutionCompleted); + + // === Validate event data at each position === + // ExecutionStarted (index 1) + const executionStarted = history[1] as ExecutionStartedEvent; + expect(executionStarted.name).toBe(getName(complexOrchestrator)); + + // TaskScheduled (index 2) + const taskScheduled = history[2] as TaskScheduledEvent; + expect(taskScheduled.name).toBe(getName(greetActivity)); + expect(taskScheduled.input).toBe(JSON.stringify("World")); + + // TaskCompleted (index 4) + const taskCompleted = history[4] as TaskCompletedEvent; + expect(taskCompleted.taskScheduledId).toBe(taskScheduled.eventId); + expect(taskCompleted.result).toBe(JSON.stringify("Hello, World!")); + + // TimerCreated (index 5) + const timerCreated = history[5] as TimerCreatedEvent; + expect(timerCreated.fireAt).toBeInstanceOf(Date); + + // TimerFired (index 7) + const timerFired = history[7] as TimerFiredEvent; + expect(timerFired.timerId).toBe(timerCreated.eventId); + expect(timerFired.fireAt).toBeInstanceOf(Date); + + // SubOrchestrationInstanceCreated (index 8) + const subOrchCreated = history[8] as SubOrchestrationInstanceCreatedEvent; + expect(subOrchCreated.name).toBe(getName(childOrchestrator)); + expect(subOrchCreated.instanceId).toBeDefined(); + + // SubOrchestrationInstanceCompleted (index 10) + const subOrchCompleted = history[10] as SubOrchestrationInstanceCompletedEvent; + expect(subOrchCompleted.taskScheduledId).toBe(subOrchCreated.eventId); + expect(subOrchCompleted.result).toBe(JSON.stringify("Child completed")); + + // ExecutionCompleted (index 11) + const executionCompleted = history[11] as ExecutionCompletedEvent; + expect(executionCompleted.orchestrationStatus).toBe("ORCHESTRATION_STATUS_COMPLETED"); + const result = JSON.parse(executionCompleted.result!); + expect(result.greeting).toBe("Hello, World!"); + expect(result.childResult).toBe("Child completed"); + + console.log(`Complete history validation passed with ${history.length} events at exact positions`); + console.log(`Event types: ${history.map(e => e.type).join(', ')}`); + }, 61000); }); From c63831aac64227e45b28de9ce8850f0acb148466 Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 3 Feb 2026 13:29:06 -0800 Subject: [PATCH 3/9] Update packages/durabletask-js/src/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/durabletask-js/src/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/durabletask-js/src/index.ts b/packages/durabletask-js/src/index.ts index ec1b75f..67f35c4 100644 --- a/packages/durabletask-js/src/index.ts +++ b/packages/durabletask-js/src/index.ts @@ -47,6 +47,13 @@ export { OrchestrationInstance, ParentInstanceInfo, TraceContext, + EntityOperationSignaledEvent, + EntityOperationCalledEvent, + EntityOperationCompletedEvent, + EntityOperationFailedEvent, + EntityLockRequestedEvent, + EntityLockGrantedEvent, + EntityUnlockSentEvent, } from "./orchestration/history-event"; // Proto types (for advanced usage) From 91655e01e5f931a135da88703737bfb68135fca7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:36:31 -0800 Subject: [PATCH 4/9] Remove MetadataGenerator re-export from client and update index exports for clarity --- packages/durabletask-js/src/client/client.ts | 3 --- packages/durabletask-js/src/index.ts | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/durabletask-js/src/client/client.ts b/packages/durabletask-js/src/client/client.ts index fb8b160..31a4db9 100644 --- a/packages/durabletask-js/src/client/client.ts +++ b/packages/durabletask-js/src/client/client.ts @@ -25,9 +25,6 @@ import { HistoryEvent } from "../orchestration/history-event"; import { convertProtoHistoryEvent } from "../utils/history-event-converter"; import { Logger, ConsoleLogger } from "../types/logger.type"; -// Re-export MetadataGenerator for backward compatibility -export { MetadataGenerator } from "../utils/grpc-helper.util"; - /** * Options for scheduling a new orchestration instance. */ diff --git a/packages/durabletask-js/src/index.ts b/packages/durabletask-js/src/index.ts index ec1b75f..a5e521d 100644 --- a/packages/durabletask-js/src/index.ts +++ b/packages/durabletask-js/src/index.ts @@ -2,7 +2,8 @@ // Licensed under the MIT License. // Client and Worker -export { TaskHubGrpcClient, TaskHubGrpcClientOptions, StartOrchestrationOptions, MetadataGenerator } from "./client/client"; +export { TaskHubGrpcClient, TaskHubGrpcClientOptions, StartOrchestrationOptions } from "./client/client"; +export { MetadataGenerator } from "./utils/grpc-helper.util"; export { TaskHubGrpcWorker, TaskHubGrpcWorkerOptions } from "./worker/task-hub-grpc-worker"; // Contexts From b665e1f697ab3546e650f92f3b02f41c582b022a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:38:53 -0800 Subject: [PATCH 5/9] Remove all listeners from stream on end and error events for cleanup --- packages/durabletask-js/src/client/client.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/durabletask-js/src/client/client.ts b/packages/durabletask-js/src/client/client.ts index 31a4db9..0373d4e 100644 --- a/packages/durabletask-js/src/client/client.ts +++ b/packages/durabletask-js/src/client/client.ts @@ -827,10 +827,12 @@ export class TaskHubGrpcClient { }); stream.on("end", () => { + stream.removeAllListeners(); resolve(historyEvents); }); stream.on("error", (err: grpc.ServiceError) => { + stream.removeAllListeners(); if (err.code === grpc.status.NOT_FOUND) { reject(new Error(`An orchestration with the instanceId '${instanceId}' was not found.`)); } else if (err.code === grpc.status.CANCELLED) { From 8e69c61914c22aa860d100ac12566443a6191208 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:48:27 -0800 Subject: [PATCH 6/9] Remove note about DTS emulator behavior for non-existent orchestration history --- test/e2e-azuremanaged/history.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/e2e-azuremanaged/history.spec.ts b/test/e2e-azuremanaged/history.spec.ts index 2772ceb..8dd5916 100644 --- a/test/e2e-azuremanaged/history.spec.ts +++ b/test/e2e-azuremanaged/history.spec.ts @@ -265,8 +265,6 @@ describe("getOrchestrationHistory E2E Tests", () => { }, 31000); it("should return empty array for non-existent orchestration", async () => { - // Note: The DTS emulator returns an empty history for non-existent instances - // rather than throwing an error. This is different from the .NET SDK behavior. const history = await taskHubClient.getOrchestrationHistory("non-existent-instance-id"); expect(history).toEqual([]); }, 31000); From b4eb4cfcd610a8dd36174f82dc6e1cd4fbf8818d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:12:20 -0800 Subject: [PATCH 7/9] remove startorch changes from this pr --- packages/durabletask-js/src/client/client.ts | 51 ++------------------ packages/durabletask-js/src/index.ts | 3 +- test/e2e-azuremanaged/history.spec.ts | 36 -------------- 3 files changed, 6 insertions(+), 84 deletions(-) diff --git a/packages/durabletask-js/src/client/client.ts b/packages/durabletask-js/src/client/client.ts index 0373d4e..3a1bda2 100644 --- a/packages/durabletask-js/src/client/client.ts +++ b/packages/durabletask-js/src/client/client.ts @@ -25,19 +25,8 @@ import { HistoryEvent } from "../orchestration/history-event"; import { convertProtoHistoryEvent } from "../utils/history-event-converter"; import { Logger, ConsoleLogger } from "../types/logger.type"; -/** - * Options for scheduling a new orchestration instance. - */ -export interface StartOrchestrationOptions { - /** The input to pass to the orchestration. */ - input?: TInput; - /** The unique ID for the orchestration instance. If not specified, a new GUID is generated. */ - instanceId?: string; - /** The time when the orchestration should start executing. If not specified, starts immediately. */ - startAt?: Date; - /** Tags to associate with the orchestration instance. */ - tags?: Record; -} +// Re-export MetadataGenerator for backward compatibility +export { MetadataGenerator } from "../utils/grpc-helper.util"; /** * Options for creating a TaskHubGrpcClient. @@ -139,36 +128,14 @@ export class TaskHubGrpcClient { * Schedules a new orchestrator using the DurableTask client. * * @param {TOrchestrator | string} orchestrator - The orchestrator or the name of the orchestrator to be scheduled. - * @param {TInput | StartOrchestrationOptions} inputOrOptions - The input to pass to the orchestrator, or an options object. - * @param {string} instanceId - (Deprecated) Use options object instead. The unique ID for the orchestration instance. - * @param {Date} startAt - (Deprecated) Use options object instead. The time when the orchestration should start. * @return {Promise} A Promise resolving to the unique ID of the scheduled orchestrator instance. */ async scheduleNewOrchestration( orchestrator: TOrchestrator | string, - inputOrOptions?: TInput | StartOrchestrationOptions, + input?: TInput, instanceId?: string, startAt?: Date, ): Promise { - // Determine if inputOrOptions is an options object or raw input - let input: TInput | undefined; - let resolvedInstanceId: string | undefined = instanceId; - let resolvedStartAt: Date | undefined = startAt; - let tags: Record | undefined; - - if (inputOrOptions !== null && typeof inputOrOptions === 'object' && !Array.isArray(inputOrOptions) && - ('input' in inputOrOptions || 'instanceId' in inputOrOptions || 'startAt' in inputOrOptions || 'tags' in inputOrOptions)) { - // It's an options object - const options = inputOrOptions as StartOrchestrationOptions; - input = options.input; - resolvedInstanceId = options.instanceId ?? instanceId; - resolvedStartAt = options.startAt ?? startAt; - tags = options.tags; - } else { - // It's raw input (backward compatible) - input = inputOrOptions as TInput; - } - let name; if (typeof orchestrator === "string") { name = orchestrator; @@ -177,25 +144,17 @@ export class TaskHubGrpcClient { } const req = new pb.CreateInstanceRequest(); req.setName(name); - req.setInstanceid(resolvedInstanceId ?? randomUUID()); + req.setInstanceid(instanceId ?? randomUUID()); const i = new StringValue(); i.setValue(JSON.stringify(input)); const ts = new Timestamp(); - ts.fromDate(new Date(resolvedStartAt?.getTime() ?? 0)); + ts.fromDate(new Date(startAt?.getTime() ?? 0)); req.setInput(i); req.setScheduledstarttimestamp(ts); - // Set tags if provided - if (tags) { - const tagsMap = req.getTagsMap(); - for (const [key, value] of Object.entries(tags)) { - tagsMap.set(key, value); - } - } - this._logger.info(`Starting new ${name} instance with ID = ${req.getInstanceid()}`); const res = await callWithMetadata( diff --git a/packages/durabletask-js/src/index.ts b/packages/durabletask-js/src/index.ts index ec9fdf6..875a111 100644 --- a/packages/durabletask-js/src/index.ts +++ b/packages/durabletask-js/src/index.ts @@ -2,8 +2,7 @@ // Licensed under the MIT License. // Client and Worker -export { TaskHubGrpcClient, TaskHubGrpcClientOptions, StartOrchestrationOptions } from "./client/client"; -export { MetadataGenerator } from "./utils/grpc-helper.util"; +export { TaskHubGrpcClient, TaskHubGrpcClientOptions, MetadataGenerator } from "./client/client"; export { TaskHubGrpcWorker, TaskHubGrpcWorkerOptions } from "./worker/task-hub-grpc-worker"; // Contexts diff --git a/test/e2e-azuremanaged/history.spec.ts b/test/e2e-azuremanaged/history.spec.ts index 8dd5916..bbf237d 100644 --- a/test/e2e-azuremanaged/history.spec.ts +++ b/test/e2e-azuremanaged/history.spec.ts @@ -341,42 +341,6 @@ describe("getOrchestrationHistory E2E Tests", () => { expect(executionCompletedEvents.length).toBe(1); }, 31000); - it("should include tags in ExecutionStarted event", async () => { - const taggedOrchestrator: TOrchestrator = async (_: OrchestrationContext) => { - return "Tagged result"; - }; - - taskHubWorker.addOrchestrator(taggedOrchestrator); - await taskHubWorker.start(); - - const tags = { - "environment": "test", - "owner": "copilot", - "priority": "high" - }; - - const id = await taskHubClient.scheduleNewOrchestration(taggedOrchestrator, { - tags: tags - }); - await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); - - const history = await taskHubClient.getOrchestrationHistory(id); - - expect(history).toBeDefined(); - expect(history.length).toBeGreaterThan(0); - - // Check for ExecutionStarted event with tags - const startedEvents = history.filter(e => e.type === HistoryEventType.ExecutionStarted) as ExecutionStartedEvent[]; - expect(startedEvents.length).toBe(1); - - const startedEvent = startedEvents[0]; - expect(startedEvent.tags).toBeDefined(); - expect(startedEvent.tags).toEqual(tags); - expect(startedEvent.tags?.["environment"]).toBe("test"); - expect(startedEvent.tags?.["owner"]).toBe("copilot"); - expect(startedEvent.tags?.["priority"]).toBe("high"); - }, 31000); - it("should validate complete history event sequence for orchestration with activity, sub-orchestration, and timer", async () => { // Define activity const greetActivity = async (_: ActivityContext, name: string) => { From 2796d4b423ea385acc40b9ce51342679b46d833b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:21:42 -0800 Subject: [PATCH 8/9] Enhance getOrchestrationHistory method to return empty array for non-existent instances and update documentation for clarity --- packages/durabletask-js/src/client/client.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/durabletask-js/src/client/client.ts b/packages/durabletask-js/src/client/client.ts index 3a1bda2..29cf387 100644 --- a/packages/durabletask-js/src/client/client.ts +++ b/packages/durabletask-js/src/client/client.ts @@ -744,11 +744,12 @@ export class TaskHubGrpcClient { * The history includes all events that occurred during the orchestration execution, * such as task scheduling, completion, failure, timer events, and more. * + * If the orchestration instance does not exist, an empty array is returned. + * * @param instanceId - The unique identifier of the orchestration instance. * @returns A Promise that resolves to an array of HistoryEvent objects representing - * the orchestration's history. + * the orchestration's history. Returns an empty array if the instance is not found. * @throws {Error} If the instanceId is null or empty. - * @throws {Error} If an orchestration with the specified instanceId is not found. * @throws {Error} If the operation is canceled. * @throws {Error} If an internal error occurs while retrieving the history. * @@ -792,8 +793,10 @@ export class TaskHubGrpcClient { stream.on("error", (err: grpc.ServiceError) => { stream.removeAllListeners(); + // Return empty array for NOT_FOUND to be consistent with DTS behavior + // (DTS returns empty stream for non-existent instances) and user-friendly if (err.code === grpc.status.NOT_FOUND) { - reject(new Error(`An orchestration with the instanceId '${instanceId}' was not found.`)); + resolve([]); } else if (err.code === grpc.status.CANCELLED) { reject(new Error(`The getOrchestrationHistory operation was canceled.`)); } else if (err.code === grpc.status.INTERNAL) { From cdace742a24a10687c8b91d0a8dfb64c026f74c7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:40:06 -0800 Subject: [PATCH 9/9] Add unit tests for convertProtoHistoryEvent function in history-event-converter --- .../test/history-event-converter.spec.ts | 821 ++++++++++++++++++ 1 file changed, 821 insertions(+) create mode 100644 packages/durabletask-js/test/history-event-converter.spec.ts diff --git a/packages/durabletask-js/test/history-event-converter.spec.ts b/packages/durabletask-js/test/history-event-converter.spec.ts new file mode 100644 index 0000000..50758dd --- /dev/null +++ b/packages/durabletask-js/test/history-event-converter.spec.ts @@ -0,0 +1,821 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; +import { StringValue } from "google-protobuf/google/protobuf/wrappers_pb"; +import * as pb from "../src/proto/orchestrator_service_pb"; +import { convertProtoHistoryEvent } from "../src/utils/history-event-converter"; +import { HistoryEventType } from "../src/orchestration/history-event"; + +/** + * Helper function to create a mock Timestamp. + */ +function createMockTimestamp(date: Date): Timestamp { + const timestamp = new Timestamp(); + timestamp.setSeconds(Math.floor(date.getTime() / 1000)); + timestamp.setNanos((date.getTime() % 1000) * 1000000); + return timestamp; +} + +/** + * Helper function to create a mock StringValue. + */ +function createMockStringValue(value: string): StringValue { + const stringValue = new StringValue(); + stringValue.setValue(value); + return stringValue; +} + +/** + * Helper function to create a base HistoryEvent with common properties. + */ +function createBaseHistoryEvent(eventId: number, timestamp?: Date): pb.HistoryEvent { + const event = new pb.HistoryEvent(); + event.setEventid(eventId); + if (timestamp) { + event.setTimestamp(createMockTimestamp(timestamp)); + } + return event; +} + +describe("convertProtoHistoryEvent", () => { + describe("ExecutionStarted event", () => { + it("should convert ExecutionStartedEvent with minimal fields", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(1, new Date("2024-01-01T00:00:00Z")); + const executionStarted = new pb.ExecutionStartedEvent(); + executionStarted.setName("TestOrchestrator"); + protoEvent.setExecutionstarted(executionStarted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.ExecutionStarted); + expect(result?.eventId).toBe(1); + if (result?.type === HistoryEventType.ExecutionStarted) { + expect(result.name).toBe("TestOrchestrator"); + } + }); + + it("should convert ExecutionStartedEvent with all fields", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(1, new Date("2024-01-01T00:00:00Z")); + const executionStarted = new pb.ExecutionStartedEvent(); + executionStarted.setName("TestOrchestrator"); + executionStarted.setVersion(createMockStringValue("1.0")); + executionStarted.setInput(createMockStringValue(JSON.stringify({ data: "test" }))); + + const orchInstance = new pb.OrchestrationInstance(); + orchInstance.setInstanceid("instance-123"); + orchInstance.setExecutionid(createMockStringValue("exec-456")); + executionStarted.setOrchestrationinstance(orchInstance); + + const scheduledTime = createMockTimestamp(new Date("2024-01-01T01:00:00Z")); + executionStarted.setScheduledstarttimestamp(scheduledTime); + + protoEvent.setExecutionstarted(executionStarted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.ExecutionStarted); + if (result?.type === HistoryEventType.ExecutionStarted) { + expect(result.name).toBe("TestOrchestrator"); + expect(result.version).toBe("1.0"); + expect(result.input).toBe(JSON.stringify({ data: "test" })); + expect(result.orchestrationInstance?.instanceId).toBe("instance-123"); + expect(result.orchestrationInstance?.executionId).toBe("exec-456"); + expect(result.scheduledStartTimestamp).toBeDefined(); + } + }); + + it("should return undefined when ExecutionStartedEvent is not set", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(1, new Date("2024-01-01T00:00:00Z")); + protoEvent.clearExecutionstarted(); + // Force the event type case to EXECUTIONSTARTED but don't set the event + (protoEvent as any).getEventtypeCase = () => pb.HistoryEvent.EventtypeCase.EXECUTIONSTARTED; + (protoEvent as any).getExecutionstarted = () => undefined; + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeUndefined(); + }); + }); + + describe("ExecutionCompleted event", () => { + it("should convert ExecutionCompletedEvent with success result", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(2, new Date("2024-01-01T00:01:00Z")); + const executionCompleted = new pb.ExecutionCompletedEvent(); + executionCompleted.setOrchestrationstatus(pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + executionCompleted.setResult(createMockStringValue(JSON.stringify({ result: "success" }))); + protoEvent.setExecutioncompleted(executionCompleted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.ExecutionCompleted); + if (result?.type === HistoryEventType.ExecutionCompleted) { + expect(result.orchestrationStatus).toBe("ORCHESTRATION_STATUS_COMPLETED"); + expect(result.result).toBe(JSON.stringify({ result: "success" })); + expect(result.failureDetails).toBeUndefined(); + } + }); + + it("should convert ExecutionCompletedEvent with failure details", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(2, new Date("2024-01-01T00:01:00Z")); + const executionCompleted = new pb.ExecutionCompletedEvent(); + executionCompleted.setOrchestrationstatus(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED); + + const failureDetails = new pb.TaskFailureDetails(); + failureDetails.setErrortype("TestError"); + failureDetails.setErrormessage("Test error message"); + failureDetails.setStacktrace(createMockStringValue("Error stack trace")); + executionCompleted.setFailuredetails(failureDetails); + protoEvent.setExecutioncompleted(executionCompleted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.ExecutionCompleted); + if (result?.type === HistoryEventType.ExecutionCompleted) { + expect(result.orchestrationStatus).toBe("ORCHESTRATION_STATUS_FAILED"); + expect(result.failureDetails).toBeDefined(); + expect(result.failureDetails?.errorType).toBe("TestError"); + expect(result.failureDetails?.message).toBe("Test error message"); + expect(result.failureDetails?.stackTrace).toBe("Error stack trace"); + } + }); + }); + + describe("ExecutionTerminated event", () => { + it("should convert ExecutionTerminatedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(3, new Date("2024-01-01T00:02:00Z")); + const executionTerminated = new pb.ExecutionTerminatedEvent(); + executionTerminated.setInput(createMockStringValue("Terminated reason")); + executionTerminated.setRecurse(true); + protoEvent.setExecutionterminated(executionTerminated); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.ExecutionTerminated); + if (result?.type === HistoryEventType.ExecutionTerminated) { + expect(result.input).toBe("Terminated reason"); + expect(result.recurse).toBe(true); + } + }); + }); + + describe("ExecutionSuspended event", () => { + it("should convert ExecutionSuspendedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(4, new Date("2024-01-01T00:03:00Z")); + const executionSuspended = new pb.ExecutionSuspendedEvent(); + executionSuspended.setInput(createMockStringValue("Suspended reason")); + protoEvent.setExecutionsuspended(executionSuspended); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.ExecutionSuspended); + if (result?.type === HistoryEventType.ExecutionSuspended) { + expect(result.input).toBe("Suspended reason"); + } + }); + }); + + describe("ExecutionResumed event", () => { + it("should convert ExecutionResumedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(5, new Date("2024-01-01T00:04:00Z")); + const executionResumed = new pb.ExecutionResumedEvent(); + executionResumed.setInput(createMockStringValue("Resumed reason")); + protoEvent.setExecutionresumed(executionResumed); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.ExecutionResumed); + if (result?.type === HistoryEventType.ExecutionResumed) { + expect(result.input).toBe("Resumed reason"); + } + }); + }); + + describe("ExecutionRewound event", () => { + it("should convert ExecutionRewoundEvent with all fields", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(6, new Date("2024-01-01T00:05:00Z")); + const executionRewound = new pb.ExecutionRewoundEvent(); + executionRewound.setReason(createMockStringValue("Rewind reason")); + executionRewound.setParentexecutionid(createMockStringValue("parent-exec-123")); + executionRewound.setInstanceid(createMockStringValue("instance-123")); + executionRewound.setName(createMockStringValue("TestOrchestrator")); + executionRewound.setVersion(createMockStringValue("1.0")); + executionRewound.setInput(createMockStringValue(JSON.stringify({ data: "rewound" }))); + + const traceContext = new pb.TraceContext(); + traceContext.setTraceparent("00-trace-123"); + traceContext.setSpanid("span-456"); + traceContext.setTracestate(createMockStringValue("state=value")); + executionRewound.setParenttracecontext(traceContext); + + protoEvent.setExecutionrewound(executionRewound); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.ExecutionRewound); + if (result?.type === HistoryEventType.ExecutionRewound) { + expect(result.reason).toBe("Rewind reason"); + expect(result.parentExecutionId).toBe("parent-exec-123"); + expect(result.instanceId).toBe("instance-123"); + expect(result.name).toBe("TestOrchestrator"); + expect(result.version).toBe("1.0"); + expect(result.input).toBe(JSON.stringify({ data: "rewound" })); + expect(result.parentTraceContext?.traceParent).toBe("00-trace-123"); + expect(result.parentTraceContext?.spanId).toBe("span-456"); + expect(result.parentTraceContext?.traceState).toBe("state=value"); + } + }); + }); + + describe("TaskScheduled event", () => { + it("should convert TaskScheduledEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(7, new Date("2024-01-01T00:06:00Z")); + const taskScheduled = new pb.TaskScheduledEvent(); + taskScheduled.setName("TestActivity"); + taskScheduled.setVersion(createMockStringValue("2.0")); + taskScheduled.setInput(createMockStringValue(JSON.stringify({ param: "value" }))); + protoEvent.setTaskscheduled(taskScheduled); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.TaskScheduled); + if (result?.type === HistoryEventType.TaskScheduled) { + expect(result.name).toBe("TestActivity"); + expect(result.version).toBe("2.0"); + expect(result.input).toBe(JSON.stringify({ param: "value" })); + } + }); + }); + + describe("TaskCompleted event", () => { + it("should convert TaskCompletedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(8, new Date("2024-01-01T00:07:00Z")); + const taskCompleted = new pb.TaskCompletedEvent(); + taskCompleted.setTaskscheduledid(7); + taskCompleted.setResult(createMockStringValue(JSON.stringify({ output: "done" }))); + protoEvent.setTaskcompleted(taskCompleted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.TaskCompleted); + if (result?.type === HistoryEventType.TaskCompleted) { + expect(result.taskScheduledId).toBe(7); + expect(result.result).toBe(JSON.stringify({ output: "done" })); + } + }); + }); + + describe("TaskFailed event", () => { + it("should convert TaskFailedEvent with failure details", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(9, new Date("2024-01-01T00:08:00Z")); + const taskFailed = new pb.TaskFailedEvent(); + taskFailed.setTaskscheduledid(7); + + const failureDetails = new pb.TaskFailureDetails(); + failureDetails.setErrortype("ActivityError"); + failureDetails.setErrormessage("Activity failed"); + taskFailed.setFailuredetails(failureDetails); + protoEvent.setTaskfailed(taskFailed); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.TaskFailed); + if (result?.type === HistoryEventType.TaskFailed) { + expect(result.taskScheduledId).toBe(7); + expect(result.failureDetails?.errorType).toBe("ActivityError"); + expect(result.failureDetails?.message).toBe("Activity failed"); + } + }); + }); + + describe("SubOrchestrationInstanceCreated event", () => { + it("should convert SubOrchestrationInstanceCreatedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(10, new Date("2024-01-01T00:09:00Z")); + const subOrchCreated = new pb.SubOrchestrationInstanceCreatedEvent(); + subOrchCreated.setName("SubOrchestrator"); + subOrchCreated.setInstanceid("sub-instance-123"); + subOrchCreated.setVersion(createMockStringValue("1.0")); + subOrchCreated.setInput(createMockStringValue(JSON.stringify({ subInput: "data" }))); + protoEvent.setSuborchestrationinstancecreated(subOrchCreated); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.SubOrchestrationInstanceCreated); + if (result?.type === HistoryEventType.SubOrchestrationInstanceCreated) { + expect(result.name).toBe("SubOrchestrator"); + expect(result.instanceId).toBe("sub-instance-123"); + expect(result.version).toBe("1.0"); + expect(result.input).toBe(JSON.stringify({ subInput: "data" })); + } + }); + }); + + describe("SubOrchestrationInstanceCompleted event", () => { + it("should convert SubOrchestrationInstanceCompletedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(11, new Date("2024-01-01T00:10:00Z")); + const subOrchCompleted = new pb.SubOrchestrationInstanceCompletedEvent(); + subOrchCompleted.setTaskscheduledid(10); + subOrchCompleted.setResult(createMockStringValue(JSON.stringify({ subResult: "completed" }))); + protoEvent.setSuborchestrationinstancecompleted(subOrchCompleted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.SubOrchestrationInstanceCompleted); + if (result?.type === HistoryEventType.SubOrchestrationInstanceCompleted) { + expect(result.taskScheduledId).toBe(10); + expect(result.result).toBe(JSON.stringify({ subResult: "completed" })); + } + }); + }); + + describe("SubOrchestrationInstanceFailed event", () => { + it("should convert SubOrchestrationInstanceFailedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(12, new Date("2024-01-01T00:11:00Z")); + const subOrchFailed = new pb.SubOrchestrationInstanceFailedEvent(); + subOrchFailed.setTaskscheduledid(10); + + const failureDetails = new pb.TaskFailureDetails(); + failureDetails.setErrortype("SubOrchestrationError"); + failureDetails.setErrormessage("Sub-orchestration failed"); + subOrchFailed.setFailuredetails(failureDetails); + protoEvent.setSuborchestrationinstancefailed(subOrchFailed); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.SubOrchestrationInstanceFailed); + if (result?.type === HistoryEventType.SubOrchestrationInstanceFailed) { + expect(result.taskScheduledId).toBe(10); + expect(result.failureDetails?.errorType).toBe("SubOrchestrationError"); + expect(result.failureDetails?.message).toBe("Sub-orchestration failed"); + } + }); + }); + + describe("TimerCreated event", () => { + it("should convert TimerCreatedEvent", () => { + // Arrange + const fireAt = new Date("2024-01-01T01:00:00Z"); + const protoEvent = createBaseHistoryEvent(13, new Date("2024-01-01T00:12:00Z")); + const timerCreated = new pb.TimerCreatedEvent(); + timerCreated.setFireat(createMockTimestamp(fireAt)); + protoEvent.setTimercreated(timerCreated); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.TimerCreated); + if (result?.type === HistoryEventType.TimerCreated) { + expect(result.fireAt.getTime()).toBe(fireAt.getTime()); + } + }); + + it("should use default date when fireAt is not set", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(13, new Date("2024-01-01T00:12:00Z")); + const timerCreated = new pb.TimerCreatedEvent(); + protoEvent.setTimercreated(timerCreated); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.TimerCreated); + if (result?.type === HistoryEventType.TimerCreated) { + expect(result.fireAt.getTime()).toBe(new Date(0).getTime()); + } + }); + }); + + describe("TimerFired event", () => { + it("should convert TimerFiredEvent", () => { + // Arrange + const fireAt = new Date("2024-01-01T01:00:00Z"); + const protoEvent = createBaseHistoryEvent(14, new Date("2024-01-01T01:00:00Z")); + const timerFired = new pb.TimerFiredEvent(); + timerFired.setFireat(createMockTimestamp(fireAt)); + timerFired.setTimerid(13); + protoEvent.setTimerfired(timerFired); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.TimerFired); + if (result?.type === HistoryEventType.TimerFired) { + expect(result.fireAt.getTime()).toBe(fireAt.getTime()); + expect(result.timerId).toBe(13); + } + }); + }); + + describe("OrchestratorStarted event", () => { + it("should convert OrchestratorStartedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(15, new Date("2024-01-01T00:14:00Z")); + const orchestratorStarted = new pb.OrchestratorStartedEvent(); + protoEvent.setOrchestratorstarted(orchestratorStarted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.OrchestratorStarted); + expect(result?.eventId).toBe(15); + }); + }); + + describe("OrchestratorCompleted event", () => { + it("should convert OrchestratorCompletedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(16, new Date("2024-01-01T00:15:00Z")); + const orchestratorCompleted = new pb.OrchestratorCompletedEvent(); + protoEvent.setOrchestratorcompleted(orchestratorCompleted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.OrchestratorCompleted); + expect(result?.eventId).toBe(16); + }); + }); + + describe("EventSent event", () => { + it("should convert EventSentEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(17, new Date("2024-01-01T00:16:00Z")); + const eventSent = new pb.EventSentEvent(); + eventSent.setName("TestEvent"); + eventSent.setInstanceid("target-instance-123"); + eventSent.setInput(createMockStringValue(JSON.stringify({ eventData: "payload" }))); + protoEvent.setEventsent(eventSent); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.EventSent); + if (result?.type === HistoryEventType.EventSent) { + expect(result.name).toBe("TestEvent"); + expect(result.instanceId).toBe("target-instance-123"); + expect(result.input).toBe(JSON.stringify({ eventData: "payload" })); + } + }); + }); + + describe("EventRaised event", () => { + it("should convert EventRaisedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(18, new Date("2024-01-01T00:17:00Z")); + const eventRaised = new pb.EventRaisedEvent(); + eventRaised.setName("ReceivedEvent"); + eventRaised.setInput(createMockStringValue(JSON.stringify({ received: "data" }))); + protoEvent.setEventraised(eventRaised); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.EventRaised); + if (result?.type === HistoryEventType.EventRaised) { + expect(result.name).toBe("ReceivedEvent"); + expect(result.input).toBe(JSON.stringify({ received: "data" })); + } + }); + }); + + describe("GenericEvent event", () => { + it("should convert GenericEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(19, new Date("2024-01-01T00:18:00Z")); + const genericEvent = new pb.GenericEvent(); + genericEvent.setData(createMockStringValue(JSON.stringify({ generic: "data" }))); + protoEvent.setGenericevent(genericEvent); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.GenericEvent); + if (result?.type === HistoryEventType.GenericEvent) { + expect(result.data).toBe(JSON.stringify({ generic: "data" })); + } + }); + }); + + describe("HistoryState event", () => { + it("should convert HistoryStateEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(20, new Date("2024-01-01T00:19:00Z")); + const historyState = new pb.HistoryStateEvent(); + protoEvent.setHistorystate(historyState); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.HistoryState); + expect(result?.eventId).toBe(20); + }); + }); + + describe("ContinueAsNew event", () => { + it("should convert ContinueAsNewEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(21, new Date("2024-01-01T00:20:00Z")); + const continueAsNew = new pb.ContinueAsNewEvent(); + continueAsNew.setInput(createMockStringValue(JSON.stringify({ newInput: "data" }))); + protoEvent.setContinueasnew(continueAsNew); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.ContinueAsNew); + if (result?.type === HistoryEventType.ContinueAsNew) { + expect(result.input).toBe(JSON.stringify({ newInput: "data" })); + } + }); + }); + + describe("EntityOperationSignaled event", () => { + it("should convert EntityOperationSignaledEvent", () => { + // Arrange + const scheduledTime = new Date("2024-01-01T02:00:00Z"); + const protoEvent = createBaseHistoryEvent(22, new Date("2024-01-01T00:21:00Z")); + const entityOpSignaled = new pb.EntityOperationSignaledEvent(); + entityOpSignaled.setRequestid("request-123"); + entityOpSignaled.setOperation("testOperation"); + entityOpSignaled.setTargetinstanceid(createMockStringValue("entity-instance-456")); + entityOpSignaled.setScheduledtime(createMockTimestamp(scheduledTime)); + entityOpSignaled.setInput(createMockStringValue(JSON.stringify({ signal: "data" }))); + protoEvent.setEntityoperationsignaled(entityOpSignaled); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.EntityOperationSignaled); + if (result?.type === HistoryEventType.EntityOperationSignaled) { + expect(result.requestId).toBe("request-123"); + expect(result.operation).toBe("testOperation"); + expect(result.targetInstanceId).toBe("entity-instance-456"); + expect(result.scheduledTime?.getTime()).toBe(scheduledTime.getTime()); + expect(result.input).toBe(JSON.stringify({ signal: "data" })); + } + }); + }); + + describe("EntityOperationCalled event", () => { + it("should convert EntityOperationCalledEvent", () => { + // Arrange + const scheduledTime = new Date("2024-01-01T02:00:00Z"); + const protoEvent = createBaseHistoryEvent(23, new Date("2024-01-01T00:22:00Z")); + const entityOpCalled = new pb.EntityOperationCalledEvent(); + entityOpCalled.setRequestid("request-456"); + entityOpCalled.setOperation("callOperation"); + entityOpCalled.setTargetinstanceid(createMockStringValue("entity-target-789")); + entityOpCalled.setParentinstanceid(createMockStringValue("parent-instance-123")); + entityOpCalled.setScheduledtime(createMockTimestamp(scheduledTime)); + entityOpCalled.setInput(createMockStringValue(JSON.stringify({ call: "data" }))); + protoEvent.setEntityoperationcalled(entityOpCalled); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.EntityOperationCalled); + if (result?.type === HistoryEventType.EntityOperationCalled) { + expect(result.requestId).toBe("request-456"); + expect(result.operation).toBe("callOperation"); + expect(result.targetInstanceId).toBe("entity-target-789"); + expect(result.parentInstanceId).toBe("parent-instance-123"); + expect(result.scheduledTime?.getTime()).toBe(scheduledTime.getTime()); + expect(result.input).toBe(JSON.stringify({ call: "data" })); + } + }); + }); + + describe("EntityOperationCompleted event", () => { + it("should convert EntityOperationCompletedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(24, new Date("2024-01-01T00:23:00Z")); + const entityOpCompleted = new pb.EntityOperationCompletedEvent(); + entityOpCompleted.setRequestid("request-789"); + entityOpCompleted.setOutput(createMockStringValue(JSON.stringify({ output: "result" }))); + protoEvent.setEntityoperationcompleted(entityOpCompleted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.EntityOperationCompleted); + if (result?.type === HistoryEventType.EntityOperationCompleted) { + expect(result.requestId).toBe("request-789"); + expect(result.output).toBe(JSON.stringify({ output: "result" })); + } + }); + }); + + describe("EntityOperationFailed event", () => { + it("should convert EntityOperationFailedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(25, new Date("2024-01-01T00:24:00Z")); + const entityOpFailed = new pb.EntityOperationFailedEvent(); + entityOpFailed.setRequestid("request-failed-123"); + + const failureDetails = new pb.TaskFailureDetails(); + failureDetails.setErrortype("EntityError"); + failureDetails.setErrormessage("Entity operation failed"); + entityOpFailed.setFailuredetails(failureDetails); + protoEvent.setEntityoperationfailed(entityOpFailed); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.EntityOperationFailed); + if (result?.type === HistoryEventType.EntityOperationFailed) { + expect(result.requestId).toBe("request-failed-123"); + expect(result.failureDetails?.errorType).toBe("EntityError"); + expect(result.failureDetails?.message).toBe("Entity operation failed"); + } + }); + }); + + describe("EntityLockRequested event", () => { + it("should convert EntityLockRequestedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(26, new Date("2024-01-01T00:25:00Z")); + const entityLockRequested = new pb.EntityLockRequestedEvent(); + entityLockRequested.setCriticalsectionid("critical-section-123"); + entityLockRequested.setLocksetList(["entity1", "entity2", "entity3"]); + entityLockRequested.setPosition(2); + entityLockRequested.setParentinstanceid(createMockStringValue("parent-orch-456")); + protoEvent.setEntitylockrequested(entityLockRequested); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.EntityLockRequested); + if (result?.type === HistoryEventType.EntityLockRequested) { + expect(result.criticalSectionId).toBe("critical-section-123"); + expect(result.lockSet).toEqual(["entity1", "entity2", "entity3"]); + expect(result.position).toBe(2); + expect(result.parentInstanceId).toBe("parent-orch-456"); + } + }); + }); + + describe("EntityLockGranted event", () => { + it("should convert EntityLockGrantedEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(27, new Date("2024-01-01T00:26:00Z")); + const entityLockGranted = new pb.EntityLockGrantedEvent(); + entityLockGranted.setCriticalsectionid("critical-section-granted-123"); + protoEvent.setEntitylockgranted(entityLockGranted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.EntityLockGranted); + if (result?.type === HistoryEventType.EntityLockGranted) { + expect(result.criticalSectionId).toBe("critical-section-granted-123"); + } + }); + }); + + describe("EntityUnlockSent event", () => { + it("should convert EntityUnlockSentEvent", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(28, new Date("2024-01-01T00:27:00Z")); + const entityUnlockSent = new pb.EntityUnlockSentEvent(); + entityUnlockSent.setCriticalsectionid("critical-section-unlock-123"); + entityUnlockSent.setParentinstanceid(createMockStringValue("parent-unlock-456")); + entityUnlockSent.setTargetinstanceid(createMockStringValue("target-entity-789")); + protoEvent.setEntityunlocksent(entityUnlockSent); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.type).toBe(HistoryEventType.EntityUnlockSent); + if (result?.type === HistoryEventType.EntityUnlockSent) { + expect(result.criticalSectionId).toBe("critical-section-unlock-123"); + expect(result.parentInstanceId).toBe("parent-unlock-456"); + expect(result.targetInstanceId).toBe("target-entity-789"); + } + }); + }); + + describe("Unknown event type", () => { + it("should return undefined for unknown event type", () => { + // Arrange + const protoEvent = createBaseHistoryEvent(99, new Date("2024-01-01T00:00:00Z")); + // Don't set any event type + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeUndefined(); + }); + }); + + describe("Timestamp handling", () => { + it("should use default date when timestamp is not set", () => { + // Arrange + const protoEvent = new pb.HistoryEvent(); + protoEvent.setEventid(1); + const executionStarted = new pb.ExecutionStartedEvent(); + executionStarted.setName("TestOrchestrator"); + protoEvent.setExecutionstarted(executionStarted); + + // Act + const result = convertProtoHistoryEvent(protoEvent); + + // Assert + expect(result).toBeDefined(); + expect(result?.timestamp.getTime()).toBe(new Date(0).getTime()); + }); + }); +});