diff --git a/package-lock.json b/package-lock.json index b1da2bc..16ba5eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -226,6 +226,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -897,6 +898,7 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -1598,6 +1600,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -1813,6 +1816,7 @@ "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -1974,6 +1978,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -2047,6 +2052,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -2288,6 +2294,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2612,6 +2619,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3259,6 +3267,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4174,6 +4183,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -5920,6 +5930,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6644,6 +6655,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6795,6 +6807,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -6881,6 +6894,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/packages/durabletask-js/src/index.ts b/packages/durabletask-js/src/index.ts index 75f4a81..8381d30 100644 --- a/packages/durabletask-js/src/index.ts +++ b/packages/durabletask-js/src/index.ts @@ -78,6 +78,7 @@ export { TOrchestrator } from "./types/orchestrator.type"; export { TActivity } from "./types/activity.type"; export { TInput } from "./types/input.type"; export { TOutput } from "./types/output.type"; +export { ParentOrchestrationInstance } from "./types/parent-orchestration-instance.type"; // Logger export { Logger, ConsoleLogger, NoOpLogger } from "./types/logger.type"; diff --git a/packages/durabletask-js/src/task/context/orchestration-context.ts b/packages/durabletask-js/src/task/context/orchestration-context.ts index bca1c4f..ede6ffe 100644 --- a/packages/durabletask-js/src/task/context/orchestration-context.ts +++ b/packages/durabletask-js/src/task/context/orchestration-context.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { ParentOrchestrationInstance } from "../../types/parent-orchestration-instance.type"; import { TActivity } from "../../types/activity.type"; import { TOrchestrator } from "../../types/orchestrator.type"; import { TaskOptions, SubOrchestrationOptions } from "../options"; @@ -17,6 +18,16 @@ export abstract class OrchestrationContext { */ abstract get instanceId(): string; + /** + * Gets the parent orchestration instance, or `undefined` if this is not a sub-orchestration. + * + * This property is useful for determining if the current orchestration was started by another + * orchestration (i.e., it's a sub-orchestration) and for accessing details about the parent. + * + * @returns {ParentOrchestrationInstance | undefined} The parent orchestration details, or `undefined` if this is a top-level orchestration. + */ + abstract get parent(): ParentOrchestrationInstance | undefined; + /** * Get the current date/time as UTC * diff --git a/packages/durabletask-js/src/types/parent-orchestration-instance.type.ts b/packages/durabletask-js/src/types/parent-orchestration-instance.type.ts new file mode 100644 index 0000000..1fa9b5a --- /dev/null +++ b/packages/durabletask-js/src/types/parent-orchestration-instance.type.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Represents the parent orchestration instance details for a sub-orchestration. + * + * This information is available when an orchestration is started as a sub-orchestration + * by another orchestration. It provides details about the parent that started this + * orchestration, which can be useful for debugging and tracing. + */ +export interface ParentOrchestrationInstance { + /** + * The name of the parent orchestration. + */ + name: string; + + /** + * The unique instance ID of the parent orchestration. + */ + instanceId: string; + + /** + * The task scheduled ID that corresponds to this sub-orchestration in the parent's history. + */ + taskScheduledId: number; +} diff --git a/packages/durabletask-js/src/utils/pb-helper.util.ts b/packages/durabletask-js/src/utils/pb-helper.util.ts index ddae925..8c91930 100644 --- a/packages/durabletask-js/src/utils/pb-helper.util.ts +++ b/packages/durabletask-js/src/utils/pb-helper.util.ts @@ -22,7 +22,7 @@ export function newOrchestratorStartedEvent(timestamp?: Date | null): pb.History return event; } -export function newExecutionStartedEvent(name: string, instanceId: string, encodedInput?: string): pb.HistoryEvent { +export function newExecutionStartedEvent(name: string, instanceId: string, encodedInput?: string, parentInstance?: { name: string; instanceId: string; taskScheduledId: number }): pb.HistoryEvent { const ts = new Timestamp(); const orchestrationInstance = new pb.OrchestrationInstance(); @@ -33,6 +33,19 @@ export function newExecutionStartedEvent(name: string, instanceId: string, encod executionStartedEvent.setInput(getStringValue(encodedInput)); executionStartedEvent.setOrchestrationinstance(orchestrationInstance); + // Set parent instance info if provided (for sub-orchestrations) + if (parentInstance) { + const parentOrchestrationInstance = new pb.OrchestrationInstance(); + parentOrchestrationInstance.setInstanceid(parentInstance.instanceId); + + const parentInstanceInfo = new pb.ParentInstanceInfo(); + parentInstanceInfo.setName(getStringValue(parentInstance.name)); + parentInstanceInfo.setOrchestrationinstance(parentOrchestrationInstance); + parentInstanceInfo.setTaskscheduledid(parentInstance.taskScheduledId); + + executionStartedEvent.setParentinstance(parentInstanceInfo); + } + const event = new pb.HistoryEvent(); event.setEventid(-1); event.setTimestamp(ts); diff --git a/packages/durabletask-js/src/worker/orchestration-executor.ts b/packages/durabletask-js/src/worker/orchestration-executor.ts index f0aa183..182d879 100644 --- a/packages/durabletask-js/src/worker/orchestration-executor.ts +++ b/packages/durabletask-js/src/worker/orchestration-executor.ts @@ -129,6 +129,17 @@ export class OrchestrationExecutor { throw new OrchestratorNotRegisteredError(executionStartedEvent?.getName()); } + // Extract parent instance info if this is a sub-orchestration + const parentInstance = executionStartedEvent?.getParentinstance(); + if (parentInstance) { + const parentOrchestrationInstance = parentInstance.getOrchestrationinstance(); + ctx._parent = { + name: parentInstance.getName()?.getValue() ?? "", + instanceId: parentOrchestrationInstance?.getInstanceid() ?? "", + taskScheduledId: parentInstance.getTaskscheduledid(), + }; + } + // Deserialize the input, if any let input = undefined; diff --git a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts index 0a1e3b9..c67b177 100644 --- a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts +++ b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts @@ -4,6 +4,7 @@ import { createHash } from "crypto"; import { getName } from "../task"; import { OrchestrationContext } from "../task/context/orchestration-context"; +import { ParentOrchestrationInstance } from "../types/parent-orchestration-instance.type"; import * as pb from "../proto/orchestrator_service_pb"; import * as ph from "../utils/pb-helper.util"; import { CompletableTask } from "../task/completable-task"; @@ -28,6 +29,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { _newGuidCounter: number; _currentUtcDatetime: Date; _instanceId: string; + _parent?: ParentOrchestrationInstance; _completionStatus?: pb.OrchestrationStatus; _receivedEvents: Record; _pendingEvents: Record[]>; @@ -48,6 +50,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { this._newGuidCounter = 0; this._currentUtcDatetime = new Date(1000, 0, 1); this._instanceId = instanceId; + this._parent = undefined; this._completionStatus = undefined; this._receivedEvents = {}; this._pendingEvents = {}; @@ -60,6 +63,10 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { return this._instanceId; } + get parent(): ParentOrchestrationInstance | undefined { + return this._parent; + } + get currentUtcDateTime(): Date { return this._currentUtcDatetime; } diff --git a/packages/durabletask-js/test/parent-orchestration-instance.spec.ts b/packages/durabletask-js/test/parent-orchestration-instance.spec.ts new file mode 100644 index 0000000..bee9048 --- /dev/null +++ b/packages/durabletask-js/test/parent-orchestration-instance.spec.ts @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { OrchestrationContext } from "../src/task/context/orchestration-context"; +import { ParentOrchestrationInstance } from "../src/types/parent-orchestration-instance.type"; +import { + newExecutionStartedEvent, + newOrchestratorStartedEvent, + newTimerCreatedEvent, + newTimerFiredEvent, +} from "../src/utils/pb-helper.util"; +import { OrchestrationExecutor } from "../src/worker/orchestration-executor"; +import * as pb from "../src/proto/orchestrator_service_pb"; +import { Registry } from "../src/worker/registry"; +import { TOrchestrator } from "../src/types/orchestrator.type"; +import { NoOpLogger } from "../src/types/logger.type"; + +// Use NoOpLogger to suppress log output during tests +const testLogger = new NoOpLogger(); + +const TEST_INSTANCE_ID = "child-instance-123"; +const PARENT_INSTANCE_ID = "parent-instance-456"; +const PARENT_ORCHESTRATOR_NAME = "ParentOrchestrator"; +const PARENT_TASK_SCHEDULED_ID = 5; + +describe("Parent Orchestration Instance", () => { + it("should return undefined for parent when orchestration is not a sub-orchestration", async () => { + let capturedParent: ParentOrchestrationInstance | undefined; + + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + capturedParent = ctx.parent; + return "done"; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + const startTime = new Date(); + const newEvents = [ + newOrchestratorStartedEvent(startTime), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // Verify the orchestration completed successfully + expect(result.actions).not.toBeNull(); + expect(result.actions.length).toEqual(1); + expect(result.actions[0]?.getCompleteorchestration()?.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED + ); + + // Verify parent is undefined for top-level orchestration + expect(capturedParent).toBeUndefined(); + }); + + it("should return parent instance info when orchestration is a sub-orchestration", async () => { + let capturedParent: ParentOrchestrationInstance | undefined; + + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + capturedParent = ctx.parent; + return "done"; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + const startTime = new Date(); + const newEvents = [ + newOrchestratorStartedEvent(startTime), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, { + name: PARENT_ORCHESTRATOR_NAME, + instanceId: PARENT_INSTANCE_ID, + taskScheduledId: PARENT_TASK_SCHEDULED_ID, + }), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // Verify the orchestration completed successfully + expect(result.actions).not.toBeNull(); + expect(result.actions.length).toEqual(1); + expect(result.actions[0]?.getCompleteorchestration()?.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED + ); + + // Verify parent instance info + expect(capturedParent).toBeDefined(); + expect(capturedParent!.name).toEqual(PARENT_ORCHESTRATOR_NAME); + expect(capturedParent!.instanceId).toEqual(PARENT_INSTANCE_ID); + expect(capturedParent!.taskScheduledId).toEqual(PARENT_TASK_SCHEDULED_ID); + }); + + it("should preserve parent info during replay", async () => { + let capturedParentDuringReplay: ParentOrchestrationInstance | undefined; + let capturedParentAfterReplay: ParentOrchestrationInstance | undefined; + let _replayState = true; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + if (ctx.isReplaying) { + capturedParentDuringReplay = ctx.parent; + _replayState = ctx.isReplaying; + } + + // Create a timer to force replay + yield ctx.createTimer(new Date(ctx.currentUtcDateTime.getTime() + 1000)); + + capturedParentAfterReplay = ctx.parent; + return "done"; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + const startTime = new Date(2020, 0, 1, 12, 0, 0); + const fireAt = new Date(startTime.getTime() + 1000); + + // First execution - create timer + const newEvents1 = [ + newOrchestratorStartedEvent(startTime), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, { + name: PARENT_ORCHESTRATOR_NAME, + instanceId: PARENT_INSTANCE_ID, + taskScheduledId: PARENT_TASK_SCHEDULED_ID, + }), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + await executor.execute(TEST_INSTANCE_ID, [], newEvents1); + + // Second execution - replay with timer fired + const oldEvents = [ + newOrchestratorStartedEvent(startTime), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, { + name: PARENT_ORCHESTRATOR_NAME, + instanceId: PARENT_INSTANCE_ID, + taskScheduledId: PARENT_TASK_SCHEDULED_ID, + }), + ]; + + const oldEventsWithTimer = [ + ...oldEvents, + newTimerCreatedEvent(1, fireAt), + ]; + const newEventsWithFired = [ + newTimerFiredEvent(1, fireAt), + ]; + + const executor2 = new OrchestrationExecutor(registry, testLogger); + const result = await executor2.execute(TEST_INSTANCE_ID, oldEventsWithTimer, newEventsWithFired); + + // Verify the orchestration completed successfully + expect(result.actions.length).toEqual(1); + expect(result.actions[0]?.getCompleteorchestration()?.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED + ); + + // Verify parent info was preserved during replay + expect(capturedParentDuringReplay).toBeDefined(); + expect(capturedParentDuringReplay!.name).toEqual(PARENT_ORCHESTRATOR_NAME); + expect(capturedParentDuringReplay!.instanceId).toEqual(PARENT_INSTANCE_ID); + + // Verify parent info is still available after replay + expect(capturedParentAfterReplay).toBeDefined(); + expect(capturedParentAfterReplay!.name).toEqual(PARENT_ORCHESTRATOR_NAME); + expect(capturedParentAfterReplay!.instanceId).toEqual(PARENT_INSTANCE_ID); + }); + + it("should make parent info available in generator orchestrations", async () => { + let capturedParent: ParentOrchestrationInstance | undefined; + + // eslint-disable-next-line require-yield + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + capturedParent = ctx.parent; + return "done"; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + const startTime = new Date(); + const newEvents = [ + newOrchestratorStartedEvent(startTime), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, { + name: PARENT_ORCHESTRATOR_NAME, + instanceId: PARENT_INSTANCE_ID, + taskScheduledId: PARENT_TASK_SCHEDULED_ID, + }), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // Verify parent instance info is accessible in generator functions + expect(capturedParent).toBeDefined(); + expect(capturedParent!.name).toEqual(PARENT_ORCHESTRATOR_NAME); + expect(capturedParent!.instanceId).toEqual(PARENT_INSTANCE_ID); + expect(capturedParent!.taskScheduledId).toEqual(PARENT_TASK_SCHEDULED_ID); + }); + + it("should return parent info that can be used for debugging/logging", async () => { + let logMessage = ""; + + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + if (ctx.parent) { + logMessage = `Sub-orchestration of ${ctx.parent.name} (${ctx.parent.instanceId})`; + } else { + logMessage = "Top-level orchestration"; + } + return logMessage; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + + // Test with parent + const executor1 = new OrchestrationExecutor(registry, testLogger); + const result1 = await executor1.execute( + TEST_INSTANCE_ID, + [], + [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, { + name: PARENT_ORCHESTRATOR_NAME, + instanceId: PARENT_INSTANCE_ID, + taskScheduledId: 1, + }), + ] + ); + + expect(result1.actions[0]?.getCompleteorchestration()?.getResult()?.getValue()).toEqual( + `"Sub-orchestration of ${PARENT_ORCHESTRATOR_NAME} (${PARENT_INSTANCE_ID})"` + ); + + // Test without parent + logMessage = ""; // Reset + const executor2 = new OrchestrationExecutor(registry, testLogger); + const result2 = await executor2.execute( + TEST_INSTANCE_ID, + [], + [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), + ] + ); + + expect(result2.actions[0]?.getCompleteorchestration()?.getResult()?.getValue()).toEqual( + '"Top-level orchestration"' + ); + }); +}); diff --git a/test/e2e-azuremanaged/orchestration.spec.ts b/test/e2e-azuremanaged/orchestration.spec.ts index a372903..46d8ce3 100644 --- a/test/e2e-azuremanaged/orchestration.spec.ts +++ b/test/e2e-azuremanaged/orchestration.spec.ts @@ -738,4 +738,77 @@ describe("Durable Task Scheduler (DTS) E2E Tests", () => { expect(receiverState?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); expect(receiverState?.serializedOutput).toEqual(JSON.stringify("received signal")); }, 45000); + + it("should expose parent orchestration info in sub-orchestrations", async () => { + // Child orchestration that captures and returns its parent info + const childOrchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + if (ctx.parent) { + return { + hasParent: true, + parentName: ctx.parent.name, + parentInstanceId: ctx.parent.instanceId, + taskScheduledId: ctx.parent.taskScheduledId, + }; + } + return { hasParent: false }; + }; + + // Parent orchestration that calls the child + const parentOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + // First verify parent is not set for top-level orchestration + const topLevelParentInfo = ctx.parent; + + // Call sub-orchestration + const childResult = yield ctx.callSubOrchestrator(childOrchestrator); + + return { + topLevelHasParent: topLevelParentInfo !== undefined, + childResult, + }; + }; + + taskHubWorker.addOrchestrator(childOrchestrator); + taskHubWorker.addOrchestrator(parentOrchestrator); + await taskHubWorker.start(); + + const parentId = await taskHubClient.scheduleNewOrchestration(parentOrchestrator); + const state = await taskHubClient.waitForOrchestrationCompletion(parentId, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.failureDetails).toBeUndefined(); + + const result = JSON.parse(state?.serializedOutput || "{}"); + + // Verify top-level orchestration has no parent + expect(result.topLevelHasParent).toBe(false); + + // Verify child orchestration received parent info + expect(result.childResult.hasParent).toBe(true); + expect(result.childResult.parentName).toEqual(getName(parentOrchestrator)); + expect(result.childResult.parentInstanceId).toEqual(parentId); + expect(typeof result.childResult.taskScheduledId).toBe("number"); + }, 31000); + + it("should have undefined parent for top-level orchestration started by client", async () => { + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + return { + hasParent: ctx.parent !== undefined, + parentInfo: ctx.parent, + }; + }; + + taskHubWorker.addOrchestrator(orchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(orchestrator); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + + const result = JSON.parse(state?.serializedOutput || "{}"); + expect(result.hasParent).toBe(false); + expect(result.parentInfo).toBeUndefined(); + }, 31000); });