From a94b9c0bf941fe72a80f598b34457a45ae34befb Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 08:56:29 +0000 Subject: [PATCH 1/4] fix: Registry._getFunctionName() returns "*" for anonymous generators Fix _getFunctionName fallback parser to handle generator functions and arrow functions correctly: - Return "" for arrow functions (no "function" keyword to parse) - Skip "*" after "function" keyword in generator functions - Guard against missing "(" in malformed input Previously, anonymous generator functions registered under name "*", silently passing validation but never matching scheduled orchestrations. Update test helper in orchestration_executor.spec.ts to handle anonymous inline orchestrators by falling back to addNamedOrchestrator. Fixes #245 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../durabletask-js/src/worker/registry.ts | 18 +++- .../test/orchestration_executor.spec.ts | 9 +- packages/durabletask-js/test/registry.spec.ts | 84 +++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/packages/durabletask-js/src/worker/registry.ts b/packages/durabletask-js/src/worker/registry.ts index 21e1179..cd9eb87 100644 --- a/packages/durabletask-js/src/worker/registry.ts +++ b/packages/durabletask-js/src/worker/registry.ts @@ -176,8 +176,24 @@ export class Registry { } const fnStr = fn.toString(); - const start = fnStr.indexOf("function") + "function".length; + const funcIdx = fnStr.indexOf("function"); + + // Arrow functions and other non-traditional syntax don't contain "function" + if (funcIdx === -1) { + return ""; + } + + let start = funcIdx + "function".length; + + // Skip the '*' for generator functions (function*() {}) + if (start < fnStr.length && fnStr[start] === "*") { + start++; + } + const end = fnStr.indexOf("(", start); + if (end === -1) { + return ""; + } return fnStr.slice(start, end).trim() || ""; } diff --git a/packages/durabletask-js/test/orchestration_executor.spec.ts b/packages/durabletask-js/test/orchestration_executor.spec.ts index 599453f..8286250 100644 --- a/packages/durabletask-js/test/orchestration_executor.spec.ts +++ b/packages/durabletask-js/test/orchestration_executor.spec.ts @@ -1130,7 +1130,14 @@ describe("Orchestration Executor", () => { startTime?: Date, ) { const registry = new Registry(); - const name = registry.addOrchestrator(orchestrator); + let name: string; + try { + name = registry.addOrchestrator(orchestrator); + } catch { + // Anonymous inline orchestrators in tests: register with a default name + name = "testOrchestrator"; + registry.addNamedOrchestrator(name, orchestrator); + } const allEvents = [ newOrchestratorStartedEvent(startTime), newExecutionStartedEvent(name, TEST_INSTANCE_ID, input !== undefined ? JSON.stringify(input) : undefined), diff --git a/packages/durabletask-js/test/registry.spec.ts b/packages/durabletask-js/test/registry.spec.ts index 748cf81..5c6faa3 100644 --- a/packages/durabletask-js/test/registry.spec.ts +++ b/packages/durabletask-js/test/registry.spec.ts @@ -192,4 +192,88 @@ describe("Registry", () => { expect(registry.getEntity("myEntity")).toBe(entityFactory); }); }); + + describe("_getFunctionName", () => { + let registry: Registry; + + beforeEach(() => { + registry = new Registry(); + }); + + it("should return the name of a named function", () => { + function myFunction() {} + expect(registry._getFunctionName(myFunction)).toBe("myFunction"); + }); + + it("should return the name of a named generator function", () => { + function* myGenerator() {} + expect(registry._getFunctionName(myGenerator)).toBe("myGenerator"); + }); + + it("should return the name of a named async generator function", () => { + async function* myAsyncGenerator() { + yield 1; + } + expect(registry._getFunctionName(myAsyncGenerator)).toBe("myAsyncGenerator"); + }); + + it("should return empty string for anonymous generator function (not '*')", () => { + // This is the key bug fix: previously returned "*" for function*() {} + // which would silently register under the name "*" + const result = registry._getFunctionName(Function("return function*() {}")()); + expect(result).toBe(""); + }); + + it("should return empty string for anonymous async generator function (not '*')", () => { + // Previously returned "*" for async function*() {} + const result = registry._getFunctionName(Function("return async function*() {}")()); + expect(result).toBe(""); + }); + + it("should return empty string for anonymous arrow function (not garbage)", () => { + // Arrow functions don't contain "function" keyword, should return "" + // Previously could return garbage from string parsing + const result = registry._getFunctionName(Function("return (x) => x")()); + expect(result).toBe(""); + }); + + it("should return empty string for anonymous async arrow function (not garbage)", () => { + // Previously returned garbage like "x) =>" for async arrow functions + const result = registry._getFunctionName(Function("return async (x) => x")()); + expect(result).toBe(""); + }); + + it("should return empty string for anonymous function expression", () => { + const result = registry._getFunctionName(Function("return function() {}")()); + expect(result).toBe(""); + }); + + it("should extract name from named function expression", () => { + const result = registry._getFunctionName(Function("return function namedExpr() {}")()); + expect(result).toBe("namedExpr"); + }); + + it("should extract name from named generator expression", () => { + const result = registry._getFunctionName(Function("return function* namedGen() {}")()); + expect(result).toBe("namedGen"); + }); + + it("should use fn.name when available (variable-assigned functions)", () => { + // When assigned to a variable, JS engines set fn.name automatically + const myOrchestrator = function* () {}; + expect(registry._getFunctionName(myOrchestrator)).toBe("myOrchestrator"); + }); + + it("addOrchestrator should throw for anonymous generator function", () => { + // With the fix, anonymous generators return "" which triggers the name validation + const anonGen = Function("return function*() {}")(); + expect(() => registry.addOrchestrator(anonGen)).toThrow("A non-empty orchestrator name is required."); + }); + + it("addActivity should throw for anonymous arrow function", () => { + // With the fix, arrow functions return "" which triggers the name validation + const anonArrow = Function("return (x) => x")(); + expect(() => registry.addActivity(anonArrow)).toThrow("A non-empty activity name is required."); + }); + }); }); From ff11b488015126239cd16bd1c85e5ac325ff2374 Mon Sep 17 00:00:00 2001 From: wangbill Date: Thu, 11 Jun 2026 14:50:37 -0700 Subject: [PATCH 2/4] fix: parse only leading function signatures Constrain Registry._getFunctionName() fallback parsing to leading function declarations so arrow function bodies containing nested functions do not produce bogus names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../durabletask-js/src/worker/registry.ts | 21 +++---------------- packages/durabletask-js/test/registry.spec.ts | 5 +++++ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/packages/durabletask-js/src/worker/registry.ts b/packages/durabletask-js/src/worker/registry.ts index cd9eb87..84102f9 100644 --- a/packages/durabletask-js/src/worker/registry.ts +++ b/packages/durabletask-js/src/worker/registry.ts @@ -175,26 +175,11 @@ export class Registry { return fn.name; } - const fnStr = fn.toString(); - const funcIdx = fnStr.indexOf("function"); - - // Arrow functions and other non-traditional syntax don't contain "function" - if (funcIdx === -1) { - return ""; - } - - let start = funcIdx + "function".length; - - // Skip the '*' for generator functions (function*() {}) - if (start < fnStr.length && fnStr[start] === "*") { - start++; - } - - const end = fnStr.indexOf("(", start); - if (end === -1) { + const match = /^\s*(?:async\s+)?function\s*\*?\s*([^(]*)\(/.exec(fn.toString()); + if (!match) { return ""; } - return fnStr.slice(start, end).trim() || ""; + return match[1].trim(); } } diff --git a/packages/durabletask-js/test/registry.spec.ts b/packages/durabletask-js/test/registry.spec.ts index 5c6faa3..b0c4230 100644 --- a/packages/durabletask-js/test/registry.spec.ts +++ b/packages/durabletask-js/test/registry.spec.ts @@ -243,6 +243,11 @@ describe("Registry", () => { expect(result).toBe(""); }); + it("should return empty string for arrow function with nested named function", () => { + const result = registry._getFunctionName(Function("return () => { function inner() {} }")()); + expect(result).toBe(""); + }); + it("should return empty string for anonymous function expression", () => { const result = registry._getFunctionName(Function("return function() {}")()); expect(result).toBe(""); From bdf08986ca2c1de5558237b0bbb32d8dc04ace52 Mon Sep 17 00:00:00 2001 From: wangbill Date: Thu, 11 Jun 2026 14:56:25 -0700 Subject: [PATCH 3/4] fix: avoid regex in function name fallback Replace the fallback parser regex with linear string parsing so function name detection only considers leading function signatures and avoids CodeQL polynomial-regex findings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../durabletask-js/src/worker/registry.ts | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/durabletask-js/src/worker/registry.ts b/packages/durabletask-js/src/worker/registry.ts index 84102f9..9fc5dc3 100644 --- a/packages/durabletask-js/src/worker/registry.ts +++ b/packages/durabletask-js/src/worker/registry.ts @@ -175,11 +175,43 @@ export class Registry { return fn.name; } - const match = /^\s*(?:async\s+)?function\s*\*?\s*([^(]*)\(/.exec(fn.toString()); - if (!match) { + const fnStr = fn.toString().trimStart(); + let start = 0; + const isWhitespace = (char: string | undefined) => char !== undefined && char.trim() === ""; + + if (fnStr.startsWith("async")) { + const afterAsync = "async".length; + if (!isWhitespace(fnStr[afterAsync])) { + return ""; + } + + start = afterAsync; + while (isWhitespace(fnStr[start])) { + start++; + } + } + + if (!fnStr.startsWith("function", start)) { + return ""; + } + + start += "function".length; + while (isWhitespace(fnStr[start])) { + start++; + } + + if (fnStr[start] === "*") { + start++; + while (isWhitespace(fnStr[start])) { + start++; + } + } + + const end = fnStr.indexOf("(", start); + if (end === -1) { return ""; } - return match[1].trim(); + return fnStr.slice(start, end).trim(); } } From bbbfc1bd8b3aa90c5369866f14452f169997a847 Mon Sep 17 00:00:00 2001 From: wangbill Date: Thu, 11 Jun 2026 15:45:43 -0700 Subject: [PATCH 4/4] test: narrow anonymous orchestrator fallback Only fall back to a default orchestrator name when addOrchestrator fails with the expected missing-name validation error; rethrow unexpected registration failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/durabletask-js/test/orchestration_executor.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/durabletask-js/test/orchestration_executor.spec.ts b/packages/durabletask-js/test/orchestration_executor.spec.ts index 8286250..0f1581f 100644 --- a/packages/durabletask-js/test/orchestration_executor.spec.ts +++ b/packages/durabletask-js/test/orchestration_executor.spec.ts @@ -1133,7 +1133,10 @@ describe("Orchestration Executor", () => { let name: string; try { name = registry.addOrchestrator(orchestrator); - } catch { + } catch (e: unknown) { + if (!(e instanceof Error) || e.message !== "A non-empty orchestrator name is required.") { + throw e; + } // Anonymous inline orchestrators in tests: register with a default name name = "testOrchestrator"; registry.addNamedOrchestrator(name, orchestrator);