diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c949d..fe3fb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ### New +- Expose readonly `OrchestrationContext.name` from execution-start history, preserving logical + aliases and case on initial execution and replay. `RuntimeOrchestrationContext` constructor + arguments are unchanged; manually constructed contexts have an empty name until initialized. - Add an optional `reason` to client `suspendOrchestration()` and `resumeOrchestration()`, forwarded unchanged to the service and recorded in in-memory test history. Empty strings are preserved; omitted reasons remain absent. @@ -57,6 +60,8 @@ ### Breaking changes +- Custom `OrchestrationContext` subclasses and typed test doubles must implement the new + `name` getter/property. Orchestrators consuming SDK-provided contexts require no changes. - Activity calls without an explicit version now inherit the parent's instance version, and child calls inherit the worker's `versioning.defaultVersion`; explicit `""` selects unversioned. Worker acceptance checks now also apply to activities; `Strict` with an empty worker version diff --git a/packages/azure-functions-durable/CHANGELOG.md b/packages/azure-functions-durable/CHANGELOG.md index 09018ef..6801869 100644 --- a/packages/azure-functions-durable/CHANGELOG.md +++ b/packages/azure-functions-durable/CHANGELOG.md @@ -2,6 +2,8 @@ ### New +- Expose readonly `context.df.name`, forwarding the core logical orchestration name from history + and preserving aliases and case across replay. This does not add `InvocationContext.functionName`. - Add optional orchestration version migration support to `context.df.continueAsNew()`. - Added a `durable-functions/testing` entry point with `runOrchestrator`, which runs an orchestrator to a terminal state against inline activity implementations on the in-memory backend and always diff --git a/packages/azure-functions-durable/src/orchestration-context.ts b/packages/azure-functions-durable/src/orchestration-context.ts index 4349d52..fe6dad3 100644 --- a/packages/azure-functions-durable/src/orchestration-context.ts +++ b/packages/azure-functions-durable/src/orchestration-context.ts @@ -32,6 +32,11 @@ export class DurableOrchestrationContext { private readonly _input: unknown, ) {} + /** The recorded logical orchestration name, preserving aliases and case across replay. */ + get name(): string { + return this._ctx.name; + } + /** The instance ID of the current orchestration. */ get instanceId(): string { return this._ctx.instanceId; diff --git a/packages/azure-functions-durable/test/unit/orchestration-context.spec.ts b/packages/azure-functions-durable/test/unit/orchestration-context.spec.ts index 2f9f728..f7a82df 100644 --- a/packages/azure-functions-durable/test/unit/orchestration-context.spec.ts +++ b/packages/azure-functions-durable/test/unit/orchestration-context.spec.ts @@ -36,6 +36,7 @@ function createFakeCoreContext() { debug: jest.fn(), }; const ctx = { + name: "LogicalOrch", instanceId: "instance-1", isReplaying: true, currentUtcDateTime: new Date("2026-01-02T03:04:05.000Z"), @@ -59,6 +60,7 @@ describe("DurableOrchestrationContext", () => { const { ctx } = createFakeCoreContext(); const df = new DurableOrchestrationContext(ctx, { city: "Tokyo" }); + expect(df.name).toBe("LogicalOrch"); expect(df.instanceId).toBe("instance-1"); expect(df.isReplaying).toBe(true); expect(df.currentUtcDateTime).toEqual(new Date("2026-01-02T03:04:05.000Z")); @@ -408,9 +410,10 @@ describe("wrapOrchestrator end-to-end (real core executor)", () => { // Classic v3 sync generator using context.df.*; wrapOrchestrator wraps it so the engine drives it. const classic = function* (context: ClassicOrchestrationContext): Generator, string, unknown> { + const initialName = context.df.name; const input = context.df.getInput(); const result = (yield context.df.callActivity("echo", input)) as string; - return `classic-done:${result}`; + return `${initialName}:${context.df.name}:${result}`; }; worker.addNamedOrchestrator("classicOrch", wrapOrchestrator(classic)); @@ -421,7 +424,7 @@ describe("wrapOrchestrator end-to-end (real core executor)", () => { const state = await client.waitForOrchestrationCompletion(id, true, 10); expect(state?.runtimeStatus).toBe(OrchestrationStatus.COMPLETED); - expect(state?.serializedOutput).toBe(JSON.stringify("classic-done:echo:IN")); + expect(state?.serializedOutput).toBe(JSON.stringify("classicOrch:classicOrch:echo:IN")); } finally { await worker.stop(); } diff --git a/packages/durabletask-js/src/task/context/orchestration-context.ts b/packages/durabletask-js/src/task/context/orchestration-context.ts index 3847a0d..fb4bc29 100644 --- a/packages/durabletask-js/src/task/context/orchestration-context.ts +++ b/packages/durabletask-js/src/task/context/orchestration-context.ts @@ -13,6 +13,16 @@ import { OrchestrationEntityFeature } from "../../entities/orchestration-entity- import { compareVersions } from "../../utils/versioning.util"; export abstract class OrchestrationContext { + /** + * Gets the logical name used to schedule the current orchestration. + * + * This is the name recorded in orchestration history, preserving aliases and case, + * not necessarily the JavaScript function name. It remains the same during replay. + * + * @returns {string} The logical name of the currently executing orchestration. + */ + abstract get name(): string; + /** * The instance ID of the currently executing orchestration. * diff --git a/packages/durabletask-js/src/worker/orchestration-executor.ts b/packages/durabletask-js/src/worker/orchestration-executor.ts index 04392ce..8c47c81 100644 --- a/packages/durabletask-js/src/worker/orchestration-executor.ts +++ b/packages/durabletask-js/src/worker/orchestration-executor.ts @@ -291,7 +291,8 @@ export class OrchestrationExecutor { // Log orchestration start (EventId 600) WorkerLogs.orchestrationStarted(this._logger, ctx._instanceId, this._orchestratorName); - // Set the version from the execution started event + // Set the name and version from the execution started event + ctx._name = executionStartedEvent?.getName() ?? ""; ctx._version = executionStartedEvent?.getVersion()?.getValue() ?? ""; // Extract parent instance info if this is a sub-orchestration diff --git a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts index eddf048..8a89311 100644 --- a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts +++ b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts @@ -43,6 +43,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { _newGuidCounter: number; _currentUtcDatetime: Date; _instanceId: string; + _name: string; _executionId: string = ""; _version: string; _parent?: ParentOrchestrationInstance; @@ -75,6 +76,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { this._newGuidCounter = 0; this._currentUtcDatetime = new Date(1000, 0, 1); this._instanceId = instanceId; + this._name = ""; this._version = ""; this._parent = undefined; this._completionStatus = undefined; @@ -87,6 +89,11 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { this._entityFeature = new RuntimeOrchestrationEntityFeature(this); } + /** The recorded logical name, or an empty string before execution history initializes the context. */ + get name(): string { + return this._name; + } + get instanceId(): string { return this._instanceId; } diff --git a/packages/durabletask-js/test/orchestration_executor.spec.ts b/packages/durabletask-js/test/orchestration_executor.spec.ts index 82f720a..421238e 100644 --- a/packages/durabletask-js/test/orchestration_executor.spec.ts +++ b/packages/durabletask-js/test/orchestration_executor.spec.ts @@ -39,7 +39,61 @@ const testLogger = new NoOpLogger(); const TEST_INSTANCE_ID = "abc123"; +describe("Orchestration Context", () => { + it("keeps manual construction compatible with an empty, getter-only name", () => { + const ctx = new RuntimeOrchestrationContext(TEST_INSTANCE_ID); + expect(ctx).toMatchObject({ instanceId: TEST_INSTANCE_ID, name: "", version: "" }); + expect(Object.getOwnPropertyDescriptor(RuntimeOrchestrationContext.prototype, "name")).toMatchObject({ + get: expect.any(Function), + set: undefined, + }); + expect(Reflect.set(ctx, "name", "changed")).toBe(false); + }); +}); + describe("Orchestration Executor", () => { + it.each([undefined, "1.0.0"])( + "preserves each logical name on initial execution and replay with registration version %s", + async (registrationVersion) => { + async function* implementation(ctx: OrchestrationContext) { + const initialName = ctx.name; + ctx.setCustomStatus([initialName, ctx.isReplaying]); + yield ctx.callActivity("Work"); + return [initialName, ctx.name, ctx.instanceId, ctx.version, ctx.isReplaying]; + } + const registry = new Registry(); + const names = ["LogicalFlow", "logicalFlow"]; + for (const name of names) { + registry.addNamedOrchestrator(name, implementation, registrationVersion); + } + + for (const name of names) { + const startEvents = [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, undefined, undefined, "1.0.0"), + ]; + const initial = await new OrchestrationExecutor(registry, testLogger, undefined, "worker-default").execute( + TEST_INSTANCE_ID, + [], + startEvents, + ); + expect(initial.customStatus).toBe(JSON.stringify([name, false])); + expect(initial.actions).toHaveLength(1); + expect(initial.actions[0].getScheduletask()?.getName()).toBe("Work"); + + const replay = await new OrchestrationExecutor(registry, testLogger, undefined, "worker-default").execute( + TEST_INSTANCE_ID, + [...startEvents, newTaskScheduledEvent(1, "Work")], + [newTaskCompletedEvent(1, '"done"')], + ); + expect(replay.customStatus).toBe(JSON.stringify([name, true])); + const completed = getAndValidateSingleCompleteOrchestrationAction(replay); + expect(completed?.getOrchestrationstatus()).toBe(pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(completed?.getResult()?.getValue()).toBe(JSON.stringify([name, name, TEST_INSTANCE_ID, "1.0.0", false])); + } + }, + ); + it("should validate the orchestrator function input population", async () => { const orchestrator: TOrchestrator = async (ctx: OrchestrationContext, input: any) => { // return all orchestrator inputs back as the output