diff --git a/packages/durabletask-js/src/client/client.ts b/packages/durabletask-js/src/client/client.ts index 572ddcf..29cf387 100644 --- a/packages/durabletask-js/src/client/client.ts +++ b/packages/durabletask-js/src/client/client.ts @@ -21,6 +21,8 @@ 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"; import { Logger, ConsoleLogger } from "../types/logger.type"; // Re-export MetadataGenerator for backward compatibility @@ -735,6 +737,77 @@ 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. + * + * 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. Returns an empty array if the instance is not found. + * @throws {Error} If the instanceId is null or empty. + * @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", () => { + stream.removeAllListeners(); + resolve(historyEvents); + }); + + 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) { + resolve([]); + } 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 63337a6..875a111 100644 --- a/packages/durabletask-js/src/index.ts +++ b/packages/durabletask-js/src/index.ts @@ -18,6 +18,44 @@ 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, + EntityOperationSignaledEvent, + EntityOperationCalledEvent, + EntityOperationCompletedEvent, + EntityOperationFailedEvent, + EntityLockRequestedEvent, + EntityLockGrantedEvent, + EntityUnlockSentEvent, +} 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..73a74f9 --- /dev/null +++ b/packages/durabletask-js/src/utils/history-event-converter.ts @@ -0,0 +1,476 @@ +// 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"; + +// 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. + * @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: convertOrchestrationStatus(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/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()); + }); + }); +}); diff --git a/test/e2e-azuremanaged/history.spec.ts b/test/e2e-azuremanaged/history.spec.ts new file mode 100644 index 0000000..bbf237d --- /dev/null +++ b/test/e2e-azuremanaged/history.spec.ts @@ -0,0 +1,520 @@ +// 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 () => { + 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 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); +});