From e4116176f3d5377c275ef2df91feeb2606984151 Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Wed, 13 Aug 2025 12:47:32 -0400 Subject: [PATCH 01/10] Add Node 24 to test matrix --- .github/workflows/ci.yml | 2 +- .github/workflows/conventions.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/stress.yml | 2 +- CONTRIBUTING.md | 3 ++- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 915f77d6f..ab417a0b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 22] # Min and max supported Node versions + node: [18, 22, 24] # Min and max officially supported Node versions + next max platform: [linux-x64, linux-arm, macos-x64, macos-arm, windows-x64] include: - platform: linux-x64 diff --git a/.github/workflows/conventions.yml b/.github/workflows/conventions.yml index 60288f307..20c9acd10 100644 --- a/.github/workflows/conventions.yml +++ b/.github/workflows/conventions.yml @@ -19,7 +19,7 @@ jobs: - name: Install Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Get NPM cache directory id: npm-cache-dir diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 97642527b..c6e39cc48 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -38,7 +38,7 @@ jobs: - name: Install Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Get NPM cache directory id: npm-cache-dir diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bd26530d..38ba0f02e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -168,7 +168,7 @@ jobs: - name: Install Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Get NPM cache directory id: npm-cache-dir @@ -213,7 +213,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 22] # Min and max supported Node versions + node: [18, 22, 24] # Min and max officially supported Node versions + next max platform: [linux-x64, linux-arm, macos-x64, macos-arm, windows-x64] sample: [hello-world, fetch-esm, hello-world-mtls] server: [cli, cloud] diff --git a/.github/workflows/stress.yml b/.github/workflows/stress.yml index de4254853..43006263d 100644 --- a/.github/workflows/stress.yml +++ b/.github/workflows/stress.yml @@ -62,7 +62,7 @@ jobs: - name: Install Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Get NPM cache directory id: npm-cache-dir diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e796fb32..beca69c93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,8 @@ See [sdk-structure.md](./docs/sdk-structure.md) ### Environment setup -The TS SDK can be executed on 18, 20 or 22. However, we recommend using Node 22 for SDK development. +TS SDK is officially supported on Node 18, 20, 22, or 24. However, we recommend using the +[Active LTS](https://nodejs.org/en/about/previous-releases#nodejs-releases) for SDK development. For easier testing during development you may want to use a version manager, such as [fnm](https://github.com/Schniz/fnm) or [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md). From c85562f7d91e3135d64318514812376a621bf1fb Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Thu, 25 Sep 2025 14:57:39 -0400 Subject: [PATCH 02/10] chore: update tests to support Node 24 stack traces --- .gitignore | 1 + package-lock.json | 21 ++++ packages/test/package.json | 1 + packages/test/src/test-workflows.ts | 153 +++++++++++++++------------- 4 files changed, 105 insertions(+), 71 deletions(-) diff --git a/.gitignore b/.gitignore index 1d0edde80..e20a6eebd 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ packages/*/package-lock.json # One test creates persisted SQLite DBs; they should normally be deleted automatically, # but may be left behind in some error scenarios. packages/test/temporal-db-*.sqlite +.DS_Store diff --git a/package-lock.json b/package-lock.json index 5a35a5f73..729cdffac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18548,11 +18548,25 @@ "@types/node-fetch": "^2.6.10", "@types/pidusage": "^2.0.5", "@types/uuid": "^9.0.7", + "escape-string-regexp": "^5.0.0", "fs-extra": "^11.2.0", "npm-run-all": "^4.1.5", "pidusage": "^3.0.2" } }, + "packages/test/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/test/node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -20678,6 +20692,7 @@ "async-retry": "^1.3.3", "ava": "^5.3.1", "dedent": "^1.5.1", + "escape-string-regexp": "^5.0.0", "fs-extra": "^11.2.0", "glob": "^10.3.10", "istanbul-lib-coverage": "^3.2.2", @@ -20694,6 +20709,12 @@ "uuid": "^9.0.1" }, "dependencies": { + "escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true + }, "node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", diff --git a/packages/test/package.json b/packages/test/package.json index 8e2d4537a..08428551d 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -68,6 +68,7 @@ "@types/node-fetch": "^2.6.10", "@types/pidusage": "^2.0.5", "@types/uuid": "^9.0.7", + "escape-string-regexp": "^5.0.0", "fs-extra": "^11.2.0", "npm-run-all": "^4.1.5", "pidusage": "^3.0.2" diff --git a/packages/test/src/test-workflows.ts b/packages/test/src/test-workflows.ts index cc8ddab9a..731327ba8 100644 --- a/packages/test/src/test-workflows.ts +++ b/packages/test/src/test-workflows.ts @@ -4,6 +4,7 @@ import vm from 'node:vm'; import anyTest, { ExecutionContext, TestFn } from 'ava'; import dedent from 'dedent'; import Long from 'long'; // eslint-disable-line import/no-named-as-default +import escapeStringRexep from 'escape-string-regexp'; import { ApplicationFailure, defaultFailureConverter, @@ -172,6 +173,14 @@ function compareCompletion( ); } +// Compare stack traces while handling $CLASS keyword to match any identifier that isn't consistent across Node versions. +// As of Node 24.6.0 type names are now present on source mapped stack traces +// See [f33e0fcc83954f728fcfd2ef6ae59435bc4af059](https://github.com/nodejs/node/commit/f33e0fcc83954f728fcfd2ef6ae59435bc4af059) +function compareFailureStackTrace(t: ExecutionContext, actual: string, expected: string) { + const escapedTrace = escapeStringRexep(expected).replaceAll('\\$CLASS', '(?:[A-Za-z]+)'); + t.regex(actual, RegExp(`^${escapedTrace}$`)); +} + function makeSuccess( commands: coresdk.workflow_commands.IWorkflowCommand[] = [makeCompleteWorkflowExecution()], usedInternalFlags: SdkFlag[] = [] @@ -307,13 +316,12 @@ function makeCompleteWorkflowExecution(result?: Payload): coresdk.workflow_comma function makeFailWorkflowExecution( message: string, - stackTrace: string, type = 'Error', nonRetryable = true ): coresdk.workflow_commands.IWorkflowCommand { return { failWorkflowExecution: { - failure: { message, stackTrace, applicationFailureInfo: { type, nonRetryable }, source: 'TypeScriptSDK' }, + failure: { message, applicationFailureInfo: { type, nonRetryable }, source: 'TypeScriptSDK' }, }, }; } @@ -453,22 +461,28 @@ function cleanWorkflowQueryFailureStackTrace( return req; } +function removeWorkflowFailureStackTrace( + req: coresdk.workflow_completion.IWorkflowActivationCompletion, + commandIndex = 0 +) { + const stackTrace = req.successful!.commands![commandIndex].failWorkflowExecution!.failure!.stackTrace!; + delete req.successful!.commands![commandIndex].failWorkflowExecution!.failure!.stackTrace; + return stackTrace; +} + test('throwAsync', async (t) => { const { workflowType } = t.context; const req = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType))); - compareCompletion( + const actualStackTrace = removeWorkflowFailureStackTrace(req); + compareCompletion(t, req, makeSuccess([makeFailWorkflowExecution('failure')])); + compareFailureStackTrace( t, - req, - makeSuccess([ - makeFailWorkflowExecution( - 'failure', - dedent` + actualStackTrace, + dedent` ApplicationFailure: failure - at Function.nonRetryable (common/src/failure.ts) + at $CLASS.nonRetryable (common/src/failure.ts) at throwAsync (test/src/workflows/throw-async.ts) ` - ), - ]) ); }); @@ -768,27 +782,26 @@ test('interruptableWorkflow', async (t) => { const req = cleanWorkflowFailureStackTrace( await activate(t, await makeSignalWorkflow('interrupt', ['just because'])) ); + const stackTrace = removeWorkflowFailureStackTrace(req); compareCompletion( t, req, makeSuccess( - [ - makeFailWorkflowExecution( - 'just because', - // The stack trace is weird here and might confuse users, it might be a JS limitation - // since the Error stack trace is generated in the constructor. - dedent` - ApplicationFailure: just because - at Function.retryable (common/src/failure.ts) - at test/src/workflows/interrupt-signal.ts - `, - 'Error', - false - ), - ], + [makeFailWorkflowExecution('just because', 'Error', false)], [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] ) ); + compareFailureStackTrace( + t, + stackTrace, + // The stack trace is weird here and might confuse users, it might be a JS limitation + // since the Error stack trace is generated in the constructor. + dedent` + ApplicationFailure: just because + at $CLASS.retryable (common/src/failure.ts) + at test/src/workflows/interrupt-signal.ts + ` + ); } }); @@ -800,24 +813,24 @@ test('failSignalWorkflow', async (t) => { } { const req = cleanWorkflowFailureStackTrace(await activate(t, await makeSignalWorkflow('fail', []))); + const stackTrace = removeWorkflowFailureStackTrace(req); compareCompletion( t, req, makeSuccess( - [ - makeFailWorkflowExecution( - 'Signal failed', - dedent` - ApplicationFailure: Signal failed - at Function.nonRetryable (common/src/failure.ts) - at test/src/workflows/fail-signal.ts - `, - 'Error' - ), - ], + [makeFailWorkflowExecution('Signal failed', 'Error')], [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] ) ); + compareFailureStackTrace( + t, + stackTrace, + dedent` + ApplicationFailure: Signal failed + at $CLASS.nonRetryable (common/src/failure.ts) + at test/src/workflows/fail-signal.ts + ` + ); } }); @@ -840,23 +853,23 @@ test('asyncFailSignalWorkflow', async (t) => { } { const req = cleanWorkflowFailureStackTrace(await activate(t, makeFireTimer(2))); + const stackTrace = removeWorkflowFailureStackTrace(req); compareCompletion( t, req, makeSuccess( - [ - makeFailWorkflowExecution( - 'Signal failed', - dedent` - ApplicationFailure: Signal failed - at Function.nonRetryable (common/src/failure.ts) - at test/src/workflows/async-fail-signal.ts`, - 'Error' - ), - ], + [makeFailWorkflowExecution('Signal failed', 'Error')], [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] ) ); + compareFailureStackTrace( + t, + stackTrace, + dedent` + ApplicationFailure: Signal failed + at $CLASS.nonRetryable (common/src/failure.ts) + at test/src/workflows/async-fail-signal.ts` + ); } }); @@ -1445,6 +1458,7 @@ test('nonCancellableInNonCancellable', async (t) => { test('cancellationErrorIsPropagated', async (t) => { const { workflowType, logs } = t.context; const req = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType)), 2); + const stackTrace = removeWorkflowFailureStackTrace(req, 2); compareCompletion( t, req, @@ -1455,15 +1469,6 @@ test('cancellationErrorIsPropagated', async (t) => { failWorkflowExecution: { failure: { message: 'Cancellation scope cancelled', - stackTrace: dedent` - CancelledFailure: Cancellation scope cancelled - at CancellationScope.cancel (workflow/src/cancellation-scope.ts) - at test/src/workflows/cancellation-error-is-propagated.ts - at CancellationScope.runInContext (workflow/src/cancellation-scope.ts) - at CancellationScope.run (workflow/src/cancellation-scope.ts) - at Function.cancellable (workflow/src/cancellation-scope.ts) - at cancellationErrorIsPropagated (test/src/workflows/cancellation-error-is-propagated.ts) - `, canceledFailureInfo: {}, source: 'TypeScriptSDK', }, @@ -1471,6 +1476,19 @@ test('cancellationErrorIsPropagated', async (t) => { }, ]) ); + compareFailureStackTrace( + t, + stackTrace, + dedent` + CancelledFailure: Cancellation scope cancelled + at CancellationScope.cancel (workflow/src/cancellation-scope.ts) + at test/src/workflows/cancellation-error-is-propagated.ts + at CancellationScope.runInContext (workflow/src/cancellation-scope.ts) + at CancellationScope.run (workflow/src/cancellation-scope.ts) + at $CLASS.cancellable (workflow/src/cancellation-scope.ts) + at cancellationErrorIsPropagated (test/src/workflows/cancellation-error-is-propagated.ts) + ` + ); t.deepEqual(logs, []); }); @@ -1655,13 +1673,9 @@ test('resolve activity with failure - http', async (t) => { }, }) ); - compareCompletion( - t, - completion, - makeSuccess([ - makeFailWorkflowExecution('Connection timeout', 'ApplicationFailure: Connection timeout', 'MockError'), - ]) - ); + const stackTrace = removeWorkflowFailureStackTrace(completion); + compareCompletion(t, completion, makeSuccess([makeFailWorkflowExecution('Connection timeout', 'MockError')])); + compareFailureStackTrace(t, stackTrace, 'ApplicationFailure: Connection timeout'); } }); @@ -1872,19 +1886,16 @@ test('tryToContinueAfterCompletion', async (t) => { const { workflowType } = t.context; { const completion = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType))); - compareCompletion( + const stackTrace = removeWorkflowFailureStackTrace(completion); + compareCompletion(t, completion, makeSuccess([makeFailWorkflowExecution('fail before continue')])); + compareFailureStackTrace( t, - completion, - makeSuccess([ - makeFailWorkflowExecution( - 'fail before continue', - dedent` + stackTrace, + dedent` ApplicationFailure: fail before continue - at Function.nonRetryable (common/src/failure.ts) + at $CLASS.nonRetryable (common/src/failure.ts) at tryToContinueAfterCompletion (test/src/workflows/try-to-continue-after-completion.ts) ` - ), - ]) ); } }); From 2a8873dc3d68678fbad25144c4265cebcdd5af2a Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Thu, 25 Sep 2025 16:22:29 -0400 Subject: [PATCH 03/10] chore: inline regexp escaping --- package-lock.json | 23 ++--------------------- packages/test/package.json | 1 - packages/test/src/test-workflows.ts | 6 ++++-- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 729cdffac..ca1bb91e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18432,6 +18432,7 @@ "@temporalio/client": "file:../client", "@temporalio/common": "file:../common", "@temporalio/interceptors-opentelemetry": "file:../interceptors-opentelemetry", + "@temporalio/nexus": "file:../nexus", "@temporalio/proto": "file:../proto", "@temporalio/testing": "file:../testing", "@temporalio/worker": "file:../worker", @@ -18548,25 +18549,11 @@ "@types/node-fetch": "^2.6.10", "@types/pidusage": "^2.0.5", "@types/uuid": "^9.0.7", - "escape-string-regexp": "^5.0.0", "fs-extra": "^11.2.0", "npm-run-all": "^4.1.5", "pidusage": "^3.0.2" } }, - "packages/test/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "packages/test/node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -20692,7 +20679,6 @@ "async-retry": "^1.3.3", "ava": "^5.3.1", "dedent": "^1.5.1", - "escape-string-regexp": "^5.0.0", "fs-extra": "^11.2.0", "glob": "^10.3.10", "istanbul-lib-coverage": "^3.2.2", @@ -20709,12 +20695,6 @@ "uuid": "^9.0.1" }, "dependencies": { - "escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true - }, "node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -30438,6 +30418,7 @@ "@temporalio/client": "file:../client", "@temporalio/common": "file:../common", "@temporalio/interceptors-opentelemetry": "file:../interceptors-opentelemetry", + "@temporalio/nexus": "file:../nexus", "@temporalio/proto": "file:../proto", "@temporalio/testing": "file:../testing", "@temporalio/worker": "file:../worker", diff --git a/packages/test/package.json b/packages/test/package.json index 08428551d..8e2d4537a 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -68,7 +68,6 @@ "@types/node-fetch": "^2.6.10", "@types/pidusage": "^2.0.5", "@types/uuid": "^9.0.7", - "escape-string-regexp": "^5.0.0", "fs-extra": "^11.2.0", "npm-run-all": "^4.1.5", "pidusage": "^3.0.2" diff --git a/packages/test/src/test-workflows.ts b/packages/test/src/test-workflows.ts index 731327ba8..6b9aada08 100644 --- a/packages/test/src/test-workflows.ts +++ b/packages/test/src/test-workflows.ts @@ -4,7 +4,6 @@ import vm from 'node:vm'; import anyTest, { ExecutionContext, TestFn } from 'ava'; import dedent from 'dedent'; import Long from 'long'; // eslint-disable-line import/no-named-as-default -import escapeStringRexep from 'escape-string-regexp'; import { ApplicationFailure, defaultFailureConverter, @@ -177,7 +176,10 @@ function compareCompletion( // As of Node 24.6.0 type names are now present on source mapped stack traces // See [f33e0fcc83954f728fcfd2ef6ae59435bc4af059](https://github.com/nodejs/node/commit/f33e0fcc83954f728fcfd2ef6ae59435bc4af059) function compareFailureStackTrace(t: ExecutionContext, actual: string, expected: string) { - const escapedTrace = escapeStringRexep(expected).replaceAll('\\$CLASS', '(?:[A-Za-z]+)'); + const escapedTrace = expected + .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') + .replace(/-/g, '\\x2d') + .replaceAll('\\$CLASS', '(?:[A-Za-z]+)'); t.regex(actual, RegExp(`^${escapedTrace}$`)); } From 1bcb466273da3275d0cff5dcf5d4105a9cffb230 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Thu, 25 Sep 2025 17:40:08 -0400 Subject: [PATCH 04/10] revert package-lock.json changes --- package-lock.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca1bb91e5..5a35a5f73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18432,7 +18432,6 @@ "@temporalio/client": "file:../client", "@temporalio/common": "file:../common", "@temporalio/interceptors-opentelemetry": "file:../interceptors-opentelemetry", - "@temporalio/nexus": "file:../nexus", "@temporalio/proto": "file:../proto", "@temporalio/testing": "file:../testing", "@temporalio/worker": "file:../worker", @@ -30418,7 +30417,6 @@ "@temporalio/client": "file:../client", "@temporalio/common": "file:../common", "@temporalio/interceptors-opentelemetry": "file:../interceptors-opentelemetry", - "@temporalio/nexus": "file:../nexus", "@temporalio/proto": "file:../proto", "@temporalio/testing": "file:../testing", "@temporalio/worker": "file:../worker", From c6a8429226ac6edfa43c1db409ec50336e9cc752 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 26 Sep 2025 09:32:04 -0400 Subject: [PATCH 05/10] chore: use published version of nexus-rpc instead of git ref --- package-lock.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 5a35a5f73..921b96cd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12424,7 +12424,8 @@ }, "node_modules/nexus-rpc": { "version": "0.0.1", - "resolved": "git+ssh://git@github.com/nexus-rpc/sdk-typescript.git#f594a7fd9e33bd14e5ce1ed04c5225fc708e7866", + "resolved": "https://registry.npmjs.org/nexus-rpc/-/nexus-rpc-0.0.1.tgz", + "integrity": "sha512-hAWn8Hh2eewpB5McXR5EW81R3pR/ziuGhKCF3wFyUVCklanPqrIgMNr7jKCbzXeNVad0nUDfWpFRqh2u+zxQtw==", "license": "MIT", "engines": { "node": ">= 18.0.0" From 32ed3df94f50bfa2ead96e6e141d02de909e0601 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 26 Sep 2025 14:30:14 -0400 Subject: [PATCH 06/10] chore: update additional tests to use new stacktrace comparison --- packages/test/src/helpers.ts | 55 ++++++++++++------- .../test/src/test-integration-split-one.ts | 20 ++++--- packages/test/src/test-interceptors.ts | 9 +-- packages/test/src/test-nexus-handler.ts | 12 ++-- packages/test/src/test-workflows.ts | 12 +--- 5 files changed, 59 insertions(+), 49 deletions(-) diff --git a/packages/test/src/helpers.ts b/packages/test/src/helpers.ts index ead48b903..dba7d0025 100644 --- a/packages/test/src/helpers.ts +++ b/packages/test/src/helpers.ts @@ -3,7 +3,7 @@ import * as net from 'net'; import path from 'path'; import * as grpc from '@grpc/grpc-js'; import asyncRetry from 'async-retry'; -import ava, { TestFn } from 'ava'; +import ava, { TestFn, ExecutionContext } from 'ava'; import StackUtils from 'stack-utils'; import { v4 as uuid4 } from 'uuid'; import { Client, Connection } from '@temporalio/client'; @@ -54,7 +54,7 @@ export async function waitUntil( intervalMs: number = 100 ): Promise { const endTime = Date.now() + timeoutMs; - for (;;) { + for (; ;) { if (await condition()) { return; } else if (Date.now() >= endTime) { @@ -98,6 +98,21 @@ export function cleanStackTrace(ostack: string): string { return normalizedStack ? `${firstLine}\n${normalizedStack}` : firstLine; } +/** + * Compare stack traces using $CLASS keyword to match any inconsistent identifiers + * + * As of Node 24.6.0 type names are now present on source mapped stack traces which leads + * to different stack traces depending on Node version. + * See [f33e0fcc83954f728fcfd2ef6ae59435bc4af059](https://github.com/nodejs/node/commit/f33e0fcc83954f728fcfd2ef6ae59435bc4af059) + */ +export function compareFailureStackTrace(t: ExecutionContext, actual: string, expected: string) { + const escapedTrace = expected + .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') + .replace(/-/g, '\\x2d') + .replaceAll('\\$CLASS', '(?:[A-Za-z]+)'); + t.regex(actual, RegExp(`^${escapedTrace}$`)); +} + function noopTest(): void { // eslint: this function body is empty and it's okay. } @@ -168,11 +183,11 @@ export class ByteSkewerPayloadCodec implements PayloadCodec { if (inWorkflowContext()) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - worker.Worker = class {}; // eslint-disable-line import/namespace + worker.Worker = class { }; // eslint-disable-line import/namespace // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - RealTestWorkflowEnvironment = class {}; // eslint-disable-line import/namespace + RealTestWorkflowEnvironment = class { }; // eslint-disable-line import/namespace } export class Worker extends worker.Worker { @@ -190,15 +205,15 @@ export class TestWorkflowEnvironment extends RealTestWorkflowEnvironment { ...opts, ...(TESTS_CLI_VERSION ? { - server: { - ...opts?.server, - executable: { - ...opts?.server?.executable, - type: 'cached-download', - version: TESTS_CLI_VERSION, - }, + server: { + ...opts?.server, + executable: { + ...opts?.server?.executable, + type: 'cached-download', + version: TESTS_CLI_VERSION, }, - } + }, + } : undefined), }); } @@ -208,15 +223,15 @@ export class TestWorkflowEnvironment extends RealTestWorkflowEnvironment { ...opts, ...(TESTS_TIME_SKIPPING_SERVER_VERSION ? { - server: { - ...opts?.server, - executable: { - ...opts?.server?.executable, - type: 'cached-download', - version: TESTS_TIME_SKIPPING_SERVER_VERSION, - }, + server: { + ...opts?.server, + executable: { + ...opts?.server?.executable, + type: 'cached-download', + version: TESTS_TIME_SKIPPING_SERVER_VERSION, }, - } + }, + } : undefined), }); } diff --git a/packages/test/src/test-integration-split-one.ts b/packages/test/src/test-integration-split-one.ts index f3df37222..cd0724996 100644 --- a/packages/test/src/test-integration-split-one.ts +++ b/packages/test/src/test-integration-split-one.ts @@ -36,7 +36,7 @@ import { } from '@temporalio/workflow'; import { configurableHelpers, createTestWorkflowBundle } from './helpers-integration'; import * as activities from './activities'; -import { cleanOptionalStackTrace, u8, Worker } from './helpers'; +import { cleanOptionalStackTrace, compareFailureStackTrace, u8, Worker } from './helpers'; import { configMacro, makeTestFn } from './helpers-integration-multi-codec'; import * as workflows from './workflows'; @@ -204,11 +204,12 @@ test.serial('activity-failure with ApplicationFailure', configMacro, async (t, c t.is(err.cause.cause.message, 'Fail me'); t.is(err.cause.cause.type, 'Error'); t.deepEqual(err.cause.cause.details, ['details', 123, false]); - t.is( - cleanOptionalStackTrace(err.cause.cause.stack), + compareFailureStackTrace( + t, + cleanOptionalStackTrace(err.cause.cause.stack)!, dedent` ApplicationFailure: Fail me - at Function.nonRetryable (common/src/failure.ts) + at $CLASS.nonRetryable (common/src/failure.ts) at throwAnError (test/src/activities/index.ts) ` ); @@ -258,11 +259,12 @@ test.serial('child-workflow-failure', configMacro, async (t, config) => { return t.fail('Expected err.cause.cause to be an instance of ApplicationFailure'); } t.is(err.cause.cause.message, 'failure'); - t.is( - cleanOptionalStackTrace(err.cause.cause.stack), + compareFailureStackTrace( + t, + cleanOptionalStackTrace(err.cause.cause.stack)!, dedent` ApplicationFailure: failure - at Function.nonRetryable (common/src/failure.ts) + at $CLASS.nonRetryable (common/src/failure.ts) at throwAsync (test/src/workflows/throw-async.ts) ` ); @@ -694,8 +696,8 @@ test.serial('Workflow can upsert Search Attributes', configMacro, async (t, conf } t.true( typeof checksum === 'string' && - checksum.includes(`@temporalio/worker@${pkg.version}+`) && - /\+[a-f0-9]{64}$/.test(checksum) // bundle checksum + checksum.includes(`@temporalio/worker@${pkg.version}+`) && + /\+[a-f0-9]{64}$/.test(checksum) // bundle checksum ); }); diff --git a/packages/test/src/test-interceptors.ts b/packages/test/src/test-interceptors.ts index 34b2a4b3d..284f3c4bb 100644 --- a/packages/test/src/test-interceptors.ts +++ b/packages/test/src/test-interceptors.ts @@ -12,7 +12,7 @@ import { WorkflowClient, WorkflowFailedError } from '@temporalio/client'; import { ApplicationFailure, TerminatedFailure } from '@temporalio/common'; import { DefaultLogger, Runtime } from '@temporalio/worker'; import { defaultPayloadConverter, WorkflowInfo } from '@temporalio/workflow'; -import { cleanOptionalStackTrace, RUN_INTEGRATION_TESTS, Worker } from './helpers'; +import { cleanOptionalStackTrace, compareFailureStackTrace, RUN_INTEGRATION_TESTS, Worker } from './helpers'; import { defaultOptions } from './mock-native-worker'; import { continueAsNewToDifferentWorkflow, @@ -241,11 +241,12 @@ if (RUN_INTEGRATION_TESTS) { return; } t.deepEqual(err.cause.message, 'Expected anything other than 1'); - t.is( - cleanOptionalStackTrace(err.cause.stack), + compareFailureStackTrace( + t, + cleanOptionalStackTrace(err.cause.stack)!, dedent` ApplicationFailure: Expected anything other than 1 - at Function.nonRetryable (common/src/failure.ts) + at $CLASS.nonRetryable (common/src/failure.ts) at Object.continueAsNew (test/src/workflows/interceptor-example.ts) at workflow/src/workflow.ts at continueAsNewToDifferentWorkflow (test/src/workflows/continue-as-new-to-different-workflow.ts) diff --git a/packages/test/src/test-nexus-handler.ts b/packages/test/src/test-nexus-handler.ts index 6cc7299fa..dc08ff5ca 100644 --- a/packages/test/src/test-nexus-handler.ts +++ b/packages/test/src/test-nexus-handler.ts @@ -19,7 +19,7 @@ import { convertWorkflowEventLinkToNexusLink, convertNexusLinkToWorkflowEventLink, } from '@temporalio/nexus/lib/link-converter'; -import { cleanStackTrace, getRandomPort } from './helpers'; +import { cleanStackTrace, compareFailureStackTrace, getRandomPort } from './helpers'; export interface Context { httpPort: number; @@ -314,10 +314,11 @@ test('start Operation Handler errors', async (t) => { }); t.true(err instanceof ApplicationFailure); t.is(err.message, ''); - t.is( + compareFailureStackTrace( + t, cleanStackTrace(err.stack!), `ApplicationFailure: deliberate failure - at Function.create (common/src/failure.ts) + at $CLASS.create (common/src/failure.ts) at op (test/src/test-nexus-handler.ts) at Object.start (nexus-rpc/src/handler/operation-handler.ts) at ServiceRegistry.start (nexus-rpc/src/handler/service-registry.ts)` @@ -460,10 +461,11 @@ test('cancel Operation Handler errors', async (t) => { }); t.true(err instanceof ApplicationFailure); t.is(err.message, ''); - t.is( + compareFailureStackTrace( + t, cleanStackTrace(err.stack!), `ApplicationFailure: deliberate failure - at Function.create (common/src/failure.ts) + at $CLASS.create (common/src/failure.ts) at Object.cancel (test/src/test-nexus-handler.ts) at ServiceRegistry.cancel (nexus-rpc/src/handler/service-registry.ts)` ); diff --git a/packages/test/src/test-workflows.ts b/packages/test/src/test-workflows.ts index 6b9aada08..b2245a8b6 100644 --- a/packages/test/src/test-workflows.ts +++ b/packages/test/src/test-workflows.ts @@ -22,7 +22,7 @@ import { SdkFlag, SdkFlags } from '@temporalio/workflow/lib/flags'; import { ReusableVMWorkflow, ReusableVMWorkflowCreator } from '@temporalio/worker/lib/workflow/reusable-vm'; import { parseWorkflowCode } from '@temporalio/worker/lib/worker'; import * as activityFunctions from './activities'; -import { cleanStackTrace, REUSE_V8_CONTEXT, u8 } from './helpers'; +import { cleanStackTrace, compareFailureStackTrace, REUSE_V8_CONTEXT, u8 } from './helpers'; import { ProcessedSignal } from './workflows'; export interface Context { @@ -172,16 +172,6 @@ function compareCompletion( ); } -// Compare stack traces while handling $CLASS keyword to match any identifier that isn't consistent across Node versions. -// As of Node 24.6.0 type names are now present on source mapped stack traces -// See [f33e0fcc83954f728fcfd2ef6ae59435bc4af059](https://github.com/nodejs/node/commit/f33e0fcc83954f728fcfd2ef6ae59435bc4af059) -function compareFailureStackTrace(t: ExecutionContext, actual: string, expected: string) { - const escapedTrace = expected - .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') - .replace(/-/g, '\\x2d') - .replaceAll('\\$CLASS', '(?:[A-Za-z]+)'); - t.regex(actual, RegExp(`^${escapedTrace}$`)); -} function makeSuccess( commands: coresdk.workflow_commands.IWorkflowCommand[] = [makeCompleteWorkflowExecution()], From c87ea5f828b48c18ddf5287326c9de9c1fb2dbc2 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 29 Sep 2025 09:35:26 -0400 Subject: [PATCH 07/10] update trace matching regex in vm-shared --- packages/test/src/helpers.ts | 52 +++++++++---------- .../test/src/test-integration-split-one.ts | 10 ++-- .../test/src/test-integration-split-three.ts | 11 ++-- packages/test/src/test-interceptors.ts | 4 +- packages/test/src/test-nexus-handler.ts | 6 +-- packages/test/src/test-workflows.ts | 17 +++--- packages/worker/src/workflow/vm-shared.ts | 4 +- 7 files changed, 54 insertions(+), 50 deletions(-) diff --git a/packages/test/src/helpers.ts b/packages/test/src/helpers.ts index dba7d0025..5f9bdeb5e 100644 --- a/packages/test/src/helpers.ts +++ b/packages/test/src/helpers.ts @@ -54,7 +54,7 @@ export async function waitUntil( intervalMs: number = 100 ): Promise { const endTime = Date.now() + timeoutMs; - for (; ;) { + for (;;) { if (await condition()) { return; } else if (Date.now() >= endTime) { @@ -99,13 +99,13 @@ export function cleanStackTrace(ostack: string): string { } /** - * Compare stack traces using $CLASS keyword to match any inconsistent identifiers - * - * As of Node 24.6.0 type names are now present on source mapped stack traces which leads - * to different stack traces depending on Node version. - * See [f33e0fcc83954f728fcfd2ef6ae59435bc4af059](https://github.com/nodejs/node/commit/f33e0fcc83954f728fcfd2ef6ae59435bc4af059) - */ -export function compareFailureStackTrace(t: ExecutionContext, actual: string, expected: string) { + * Compare stack traces using $CLASS keyword to match any inconsistent identifiers + * + * As of Node 24.6.0 type names are now present on source mapped stack traces which leads + * to different stack traces depending on Node version. + * See [f33e0fcc83954f728fcfd2ef6ae59435bc4af059](https://github.com/nodejs/node/commit/f33e0fcc83954f728fcfd2ef6ae59435bc4af059) + */ +export function compareStackTraceIdentifiers(t: ExecutionContext, actual: string, expected: string): void { const escapedTrace = expected .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') .replace(/-/g, '\\x2d') @@ -183,11 +183,11 @@ export class ByteSkewerPayloadCodec implements PayloadCodec { if (inWorkflowContext()) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - worker.Worker = class { }; // eslint-disable-line import/namespace + worker.Worker = class {}; // eslint-disable-line import/namespace // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - RealTestWorkflowEnvironment = class { }; // eslint-disable-line import/namespace + RealTestWorkflowEnvironment = class {}; // eslint-disable-line import/namespace } export class Worker extends worker.Worker { @@ -205,15 +205,15 @@ export class TestWorkflowEnvironment extends RealTestWorkflowEnvironment { ...opts, ...(TESTS_CLI_VERSION ? { - server: { - ...opts?.server, - executable: { - ...opts?.server?.executable, - type: 'cached-download', - version: TESTS_CLI_VERSION, + server: { + ...opts?.server, + executable: { + ...opts?.server?.executable, + type: 'cached-download', + version: TESTS_CLI_VERSION, + }, }, - }, - } + } : undefined), }); } @@ -223,15 +223,15 @@ export class TestWorkflowEnvironment extends RealTestWorkflowEnvironment { ...opts, ...(TESTS_TIME_SKIPPING_SERVER_VERSION ? { - server: { - ...opts?.server, - executable: { - ...opts?.server?.executable, - type: 'cached-download', - version: TESTS_TIME_SKIPPING_SERVER_VERSION, + server: { + ...opts?.server, + executable: { + ...opts?.server?.executable, + type: 'cached-download', + version: TESTS_TIME_SKIPPING_SERVER_VERSION, + }, }, - }, - } + } : undefined), }); } diff --git a/packages/test/src/test-integration-split-one.ts b/packages/test/src/test-integration-split-one.ts index cd0724996..c77cb23f6 100644 --- a/packages/test/src/test-integration-split-one.ts +++ b/packages/test/src/test-integration-split-one.ts @@ -36,7 +36,7 @@ import { } from '@temporalio/workflow'; import { configurableHelpers, createTestWorkflowBundle } from './helpers-integration'; import * as activities from './activities'; -import { cleanOptionalStackTrace, compareFailureStackTrace, u8, Worker } from './helpers'; +import { cleanOptionalStackTrace, compareStackTraceIdentifiers, u8, Worker } from './helpers'; import { configMacro, makeTestFn } from './helpers-integration-multi-codec'; import * as workflows from './workflows'; @@ -204,7 +204,7 @@ test.serial('activity-failure with ApplicationFailure', configMacro, async (t, c t.is(err.cause.cause.message, 'Fail me'); t.is(err.cause.cause.type, 'Error'); t.deepEqual(err.cause.cause.details, ['details', 123, false]); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, cleanOptionalStackTrace(err.cause.cause.stack)!, dedent` @@ -259,7 +259,7 @@ test.serial('child-workflow-failure', configMacro, async (t, config) => { return t.fail('Expected err.cause.cause to be an instance of ApplicationFailure'); } t.is(err.cause.cause.message, 'failure'); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, cleanOptionalStackTrace(err.cause.cause.stack)!, dedent` @@ -696,8 +696,8 @@ test.serial('Workflow can upsert Search Attributes', configMacro, async (t, conf } t.true( typeof checksum === 'string' && - checksum.includes(`@temporalio/worker@${pkg.version}+`) && - /\+[a-f0-9]{64}$/.test(checksum) // bundle checksum + checksum.includes(`@temporalio/worker@${pkg.version}+`) && + /\+[a-f0-9]{64}$/.test(checksum) // bundle checksum ); }); diff --git a/packages/test/src/test-integration-split-three.ts b/packages/test/src/test-integration-split-three.ts index cf553c58f..c7572a991 100644 --- a/packages/test/src/test-integration-split-three.ts +++ b/packages/test/src/test-integration-split-three.ts @@ -8,7 +8,7 @@ import { configMacro, makeTestFn } from './helpers-integration-multi-codec'; import { configurableHelpers } from './helpers-integration'; import { withZeroesHTTPServer } from './zeroes-http-server'; import * as activities from './activities'; -import { approximatelyEqual, cleanOptionalStackTrace } from './helpers'; +import { approximatelyEqual, cleanOptionalStackTrace, compareStackTraceIdentifiers } from './helpers'; import * as workflows from './workflows'; const test = makeTestFn(() => bundleWorkflowCode({ workflowsPath: require.resolve('./workflows') })); @@ -48,7 +48,6 @@ if ('promiseHooks' in v8) { t.true( stack1.endsWith( ` - at Function.all () at stackTracer (test/src/workflows/stack-tracer.ts) at stackTracer (test/src/workflows/stack-tracer.ts) @@ -91,11 +90,17 @@ if ('promiseHooks' in v8) { })); t.is(enhancedStack.sdk.name, 'typescript'); t.is(enhancedStack.sdk.version, pkg.version); // Expect workflow and worker versions to match + { + const functionName = stacks[0]!.locations[0]!.function_name!; + delete stacks[0]!.locations[0]!.function_name; + compareStackTraceIdentifiers(t, functionName, '$CLASS.all'); + } t.deepEqual(stacks, [ { locations: [ { - function_name: 'Function.all', + // Checked sperately above to handle Node 24 behavior change with respect to identifiers in stack traces + // function_name: 'Function.all', internal_code: false, }, { diff --git a/packages/test/src/test-interceptors.ts b/packages/test/src/test-interceptors.ts index 284f3c4bb..bd3ec45b6 100644 --- a/packages/test/src/test-interceptors.ts +++ b/packages/test/src/test-interceptors.ts @@ -12,7 +12,7 @@ import { WorkflowClient, WorkflowFailedError } from '@temporalio/client'; import { ApplicationFailure, TerminatedFailure } from '@temporalio/common'; import { DefaultLogger, Runtime } from '@temporalio/worker'; import { defaultPayloadConverter, WorkflowInfo } from '@temporalio/workflow'; -import { cleanOptionalStackTrace, compareFailureStackTrace, RUN_INTEGRATION_TESTS, Worker } from './helpers'; +import { cleanOptionalStackTrace, compareStackTraceIdentifiers, RUN_INTEGRATION_TESTS, Worker } from './helpers'; import { defaultOptions } from './mock-native-worker'; import { continueAsNewToDifferentWorkflow, @@ -241,7 +241,7 @@ if (RUN_INTEGRATION_TESTS) { return; } t.deepEqual(err.cause.message, 'Expected anything other than 1'); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, cleanOptionalStackTrace(err.cause.stack)!, dedent` diff --git a/packages/test/src/test-nexus-handler.ts b/packages/test/src/test-nexus-handler.ts index dc08ff5ca..a7cbe063f 100644 --- a/packages/test/src/test-nexus-handler.ts +++ b/packages/test/src/test-nexus-handler.ts @@ -19,7 +19,7 @@ import { convertWorkflowEventLinkToNexusLink, convertNexusLinkToWorkflowEventLink, } from '@temporalio/nexus/lib/link-converter'; -import { cleanStackTrace, compareFailureStackTrace, getRandomPort } from './helpers'; +import { cleanStackTrace, compareStackTraceIdentifiers, getRandomPort } from './helpers'; export interface Context { httpPort: number; @@ -314,7 +314,7 @@ test('start Operation Handler errors', async (t) => { }); t.true(err instanceof ApplicationFailure); t.is(err.message, ''); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, cleanStackTrace(err.stack!), `ApplicationFailure: deliberate failure @@ -461,7 +461,7 @@ test('cancel Operation Handler errors', async (t) => { }); t.true(err instanceof ApplicationFailure); t.is(err.message, ''); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, cleanStackTrace(err.stack!), `ApplicationFailure: deliberate failure diff --git a/packages/test/src/test-workflows.ts b/packages/test/src/test-workflows.ts index b2245a8b6..12711ee82 100644 --- a/packages/test/src/test-workflows.ts +++ b/packages/test/src/test-workflows.ts @@ -22,7 +22,7 @@ import { SdkFlag, SdkFlags } from '@temporalio/workflow/lib/flags'; import { ReusableVMWorkflow, ReusableVMWorkflowCreator } from '@temporalio/worker/lib/workflow/reusable-vm'; import { parseWorkflowCode } from '@temporalio/worker/lib/worker'; import * as activityFunctions from './activities'; -import { cleanStackTrace, compareFailureStackTrace, REUSE_V8_CONTEXT, u8 } from './helpers'; +import { cleanStackTrace, compareStackTraceIdentifiers, REUSE_V8_CONTEXT, u8 } from './helpers'; import { ProcessedSignal } from './workflows'; export interface Context { @@ -172,7 +172,6 @@ function compareCompletion( ); } - function makeSuccess( commands: coresdk.workflow_commands.IWorkflowCommand[] = [makeCompleteWorkflowExecution()], usedInternalFlags: SdkFlag[] = [] @@ -467,7 +466,7 @@ test('throwAsync', async (t) => { const req = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType))); const actualStackTrace = removeWorkflowFailureStackTrace(req); compareCompletion(t, req, makeSuccess([makeFailWorkflowExecution('failure')])); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, actualStackTrace, dedent` @@ -783,7 +782,7 @@ test('interruptableWorkflow', async (t) => { [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] ) ); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, stackTrace, // The stack trace is weird here and might confuse users, it might be a JS limitation @@ -814,7 +813,7 @@ test('failSignalWorkflow', async (t) => { [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] ) ); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, stackTrace, dedent` @@ -854,7 +853,7 @@ test('asyncFailSignalWorkflow', async (t) => { [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] ) ); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, stackTrace, dedent` @@ -1468,7 +1467,7 @@ test('cancellationErrorIsPropagated', async (t) => { }, ]) ); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, stackTrace, dedent` @@ -1667,7 +1666,7 @@ test('resolve activity with failure - http', async (t) => { ); const stackTrace = removeWorkflowFailureStackTrace(completion); compareCompletion(t, completion, makeSuccess([makeFailWorkflowExecution('Connection timeout', 'MockError')])); - compareFailureStackTrace(t, stackTrace, 'ApplicationFailure: Connection timeout'); + compareStackTraceIdentifiers(t, stackTrace, 'ApplicationFailure: Connection timeout'); } }); @@ -1880,7 +1879,7 @@ test('tryToContinueAfterCompletion', async (t) => { const completion = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType))); const stackTrace = removeWorkflowFailureStackTrace(completion); compareCompletion(t, completion, makeSuccess([makeFailWorkflowExecution('fail before continue')])); - compareFailureStackTrace( + compareStackTraceIdentifiers( t, stackTrace, dedent` diff --git a/packages/worker/src/workflow/vm-shared.ts b/packages/worker/src/workflow/vm-shared.ts index 96b76fd44..6db33fbf6 100644 --- a/packages/worker/src/workflow/vm-shared.ts +++ b/packages/worker/src/workflow/vm-shared.ts @@ -250,7 +250,7 @@ export class GlobalHandlers { if ( currentAggregation && - /^\s+at\sPromise\.then \(\)\n\s+at Function\.(race|all|allSettled|any) \(\)\n/.test( + /^\s+at\sPromise\.then \(\)\n\s+at (Function|Promise)\.(race|all|allSettled|any) \(\)\n/.test( formatted ) ) { @@ -258,7 +258,7 @@ export class GlobalHandlers { promise = currentAggregation; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion stackTrace = store.promiseToStack.get(currentAggregation)!; // Must exist - } else if (/^\s+at Function\.(race|all|allSettled|any) \(\)\n/.test(formatted)) { + } else if (/^\s+at (Function|Promise)\.(race|all|allSettled|any) \(\)\n/.test(formatted)) { currentAggregation = promise; } else { currentAggregation = undefined; From 4c24aa235f521832e2559af1aff28f049de7fade Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 29 Sep 2025 15:56:10 -0400 Subject: [PATCH 08/10] rename helper function and remove generic --- packages/test/src/helpers.ts | 2 +- packages/test/src/test-integration-split-one.ts | 6 +++--- .../test/src/test-integration-split-three.ts | 4 ++-- packages/test/src/test-interceptors.ts | 4 ++-- packages/test/src/test-nexus-handler.ts | 6 +++--- packages/test/src/test-workflows.ts | 16 ++++++++-------- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/test/src/helpers.ts b/packages/test/src/helpers.ts index 5f9bdeb5e..a8dd11995 100644 --- a/packages/test/src/helpers.ts +++ b/packages/test/src/helpers.ts @@ -105,7 +105,7 @@ export function cleanStackTrace(ostack: string): string { * to different stack traces depending on Node version. * See [f33e0fcc83954f728fcfd2ef6ae59435bc4af059](https://github.com/nodejs/node/commit/f33e0fcc83954f728fcfd2ef6ae59435bc4af059) */ -export function compareStackTraceIdentifiers(t: ExecutionContext, actual: string, expected: string): void { +export function compareStackTrace(t: ExecutionContext, actual: string, expected: string): void { const escapedTrace = expected .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') .replace(/-/g, '\\x2d') diff --git a/packages/test/src/test-integration-split-one.ts b/packages/test/src/test-integration-split-one.ts index c77cb23f6..68efcdcdf 100644 --- a/packages/test/src/test-integration-split-one.ts +++ b/packages/test/src/test-integration-split-one.ts @@ -36,7 +36,7 @@ import { } from '@temporalio/workflow'; import { configurableHelpers, createTestWorkflowBundle } from './helpers-integration'; import * as activities from './activities'; -import { cleanOptionalStackTrace, compareStackTraceIdentifiers, u8, Worker } from './helpers'; +import { cleanOptionalStackTrace, compareStackTrace, u8, Worker } from './helpers'; import { configMacro, makeTestFn } from './helpers-integration-multi-codec'; import * as workflows from './workflows'; @@ -204,7 +204,7 @@ test.serial('activity-failure with ApplicationFailure', configMacro, async (t, c t.is(err.cause.cause.message, 'Fail me'); t.is(err.cause.cause.type, 'Error'); t.deepEqual(err.cause.cause.details, ['details', 123, false]); - compareStackTraceIdentifiers( + compareStackTrace( t, cleanOptionalStackTrace(err.cause.cause.stack)!, dedent` @@ -259,7 +259,7 @@ test.serial('child-workflow-failure', configMacro, async (t, config) => { return t.fail('Expected err.cause.cause to be an instance of ApplicationFailure'); } t.is(err.cause.cause.message, 'failure'); - compareStackTraceIdentifiers( + compareStackTrace( t, cleanOptionalStackTrace(err.cause.cause.stack)!, dedent` diff --git a/packages/test/src/test-integration-split-three.ts b/packages/test/src/test-integration-split-three.ts index c7572a991..c4af367f5 100644 --- a/packages/test/src/test-integration-split-three.ts +++ b/packages/test/src/test-integration-split-three.ts @@ -8,7 +8,7 @@ import { configMacro, makeTestFn } from './helpers-integration-multi-codec'; import { configurableHelpers } from './helpers-integration'; import { withZeroesHTTPServer } from './zeroes-http-server'; import * as activities from './activities'; -import { approximatelyEqual, cleanOptionalStackTrace, compareStackTraceIdentifiers } from './helpers'; +import { approximatelyEqual, cleanOptionalStackTrace, compareStackTrace } from './helpers'; import * as workflows from './workflows'; const test = makeTestFn(() => bundleWorkflowCode({ workflowsPath: require.resolve('./workflows') })); @@ -93,7 +93,7 @@ if ('promiseHooks' in v8) { { const functionName = stacks[0]!.locations[0]!.function_name!; delete stacks[0]!.locations[0]!.function_name; - compareStackTraceIdentifiers(t, functionName, '$CLASS.all'); + compareStackTrace(t, functionName, '$CLASS.all'); } t.deepEqual(stacks, [ { diff --git a/packages/test/src/test-interceptors.ts b/packages/test/src/test-interceptors.ts index bd3ec45b6..5449ff585 100644 --- a/packages/test/src/test-interceptors.ts +++ b/packages/test/src/test-interceptors.ts @@ -12,7 +12,7 @@ import { WorkflowClient, WorkflowFailedError } from '@temporalio/client'; import { ApplicationFailure, TerminatedFailure } from '@temporalio/common'; import { DefaultLogger, Runtime } from '@temporalio/worker'; import { defaultPayloadConverter, WorkflowInfo } from '@temporalio/workflow'; -import { cleanOptionalStackTrace, compareStackTraceIdentifiers, RUN_INTEGRATION_TESTS, Worker } from './helpers'; +import { cleanOptionalStackTrace, compareStackTrace, RUN_INTEGRATION_TESTS, Worker } from './helpers'; import { defaultOptions } from './mock-native-worker'; import { continueAsNewToDifferentWorkflow, @@ -241,7 +241,7 @@ if (RUN_INTEGRATION_TESTS) { return; } t.deepEqual(err.cause.message, 'Expected anything other than 1'); - compareStackTraceIdentifiers( + compareStackTrace( t, cleanOptionalStackTrace(err.cause.stack)!, dedent` diff --git a/packages/test/src/test-nexus-handler.ts b/packages/test/src/test-nexus-handler.ts index a7cbe063f..f13f2b427 100644 --- a/packages/test/src/test-nexus-handler.ts +++ b/packages/test/src/test-nexus-handler.ts @@ -19,7 +19,7 @@ import { convertWorkflowEventLinkToNexusLink, convertNexusLinkToWorkflowEventLink, } from '@temporalio/nexus/lib/link-converter'; -import { cleanStackTrace, compareStackTraceIdentifiers, getRandomPort } from './helpers'; +import { cleanStackTrace, compareStackTrace, getRandomPort } from './helpers'; export interface Context { httpPort: number; @@ -314,7 +314,7 @@ test('start Operation Handler errors', async (t) => { }); t.true(err instanceof ApplicationFailure); t.is(err.message, ''); - compareStackTraceIdentifiers( + compareStackTrace( t, cleanStackTrace(err.stack!), `ApplicationFailure: deliberate failure @@ -461,7 +461,7 @@ test('cancel Operation Handler errors', async (t) => { }); t.true(err instanceof ApplicationFailure); t.is(err.message, ''); - compareStackTraceIdentifiers( + compareStackTrace( t, cleanStackTrace(err.stack!), `ApplicationFailure: deliberate failure diff --git a/packages/test/src/test-workflows.ts b/packages/test/src/test-workflows.ts index 12711ee82..5848c892e 100644 --- a/packages/test/src/test-workflows.ts +++ b/packages/test/src/test-workflows.ts @@ -22,7 +22,7 @@ import { SdkFlag, SdkFlags } from '@temporalio/workflow/lib/flags'; import { ReusableVMWorkflow, ReusableVMWorkflowCreator } from '@temporalio/worker/lib/workflow/reusable-vm'; import { parseWorkflowCode } from '@temporalio/worker/lib/worker'; import * as activityFunctions from './activities'; -import { cleanStackTrace, compareStackTraceIdentifiers, REUSE_V8_CONTEXT, u8 } from './helpers'; +import { cleanStackTrace, compareStackTrace, REUSE_V8_CONTEXT, u8 } from './helpers'; import { ProcessedSignal } from './workflows'; export interface Context { @@ -466,7 +466,7 @@ test('throwAsync', async (t) => { const req = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType))); const actualStackTrace = removeWorkflowFailureStackTrace(req); compareCompletion(t, req, makeSuccess([makeFailWorkflowExecution('failure')])); - compareStackTraceIdentifiers( + compareStackTrace( t, actualStackTrace, dedent` @@ -782,7 +782,7 @@ test('interruptableWorkflow', async (t) => { [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] ) ); - compareStackTraceIdentifiers( + compareStackTrace( t, stackTrace, // The stack trace is weird here and might confuse users, it might be a JS limitation @@ -813,7 +813,7 @@ test('failSignalWorkflow', async (t) => { [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] ) ); - compareStackTraceIdentifiers( + compareStackTrace( t, stackTrace, dedent` @@ -853,7 +853,7 @@ test('asyncFailSignalWorkflow', async (t) => { [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] ) ); - compareStackTraceIdentifiers( + compareStackTrace( t, stackTrace, dedent` @@ -1467,7 +1467,7 @@ test('cancellationErrorIsPropagated', async (t) => { }, ]) ); - compareStackTraceIdentifiers( + compareStackTrace( t, stackTrace, dedent` @@ -1666,7 +1666,7 @@ test('resolve activity with failure - http', async (t) => { ); const stackTrace = removeWorkflowFailureStackTrace(completion); compareCompletion(t, completion, makeSuccess([makeFailWorkflowExecution('Connection timeout', 'MockError')])); - compareStackTraceIdentifiers(t, stackTrace, 'ApplicationFailure: Connection timeout'); + compareStackTrace(t, stackTrace, 'ApplicationFailure: Connection timeout'); } }); @@ -1879,7 +1879,7 @@ test('tryToContinueAfterCompletion', async (t) => { const completion = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType))); const stackTrace = removeWorkflowFailureStackTrace(completion); compareCompletion(t, completion, makeSuccess([makeFailWorkflowExecution('fail before continue')])); - compareStackTraceIdentifiers( + compareStackTrace( t, stackTrace, dedent` From 8b47a215a31e197978e44e0b25c1a8b92f59b9e9 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 30 Sep 2025 10:26:58 -0400 Subject: [PATCH 09/10] move stack trace comparison logic into compareCompletion --- packages/test/src/test-workflows.ts | 172 +++++++++++++++++----------- 1 file changed, 104 insertions(+), 68 deletions(-) diff --git a/packages/test/src/test-workflows.ts b/packages/test/src/test-workflows.ts index 5848c892e..7330e0bf9 100644 --- a/packages/test/src/test-workflows.ts +++ b/packages/test/src/test-workflows.ts @@ -163,6 +163,7 @@ function compareCompletion( req: coresdk.workflow_completion.IWorkflowActivationCompletion, expected: coresdk.workflow_completion.IWorkflowActivationCompletion ) { + const stackTraces = extractFailureStackTraces(req, expected); t.deepEqual( coresdk.workflow_completion.WorkflowActivationCompletion.create(req).toJSON(), coresdk.workflow_completion.WorkflowActivationCompletion.create({ @@ -170,6 +171,43 @@ function compareCompletion( runId: t.context.runId, }).toJSON() ); + + if (stackTraces) { + for (const { actual, expected } of stackTraces) { + compareStackTrace(t, actual, expected); + } + } +} + +// Extracts failure stack traces from completions if structure matches, leaving them unchanged if structure differs. +// We leave them unchanged on structure differences as ava's `deepEqual` provides a better failure message. +function extractFailureStackTraces( + req: coresdk.workflow_completion.IWorkflowActivationCompletion, + expected: coresdk.workflow_completion.IWorkflowActivationCompletion +): { actual: string; expected: string }[] | undefined { + const reqCommands = req.successful?.commands; + const expectedCommands = expected.successful?.commands; + if (!reqCommands || !expectedCommands || reqCommands.length !== expectedCommands.length) { + return; + } + for (let commandIndex = 0; commandIndex < reqCommands.length; commandIndex++) { + const reqStack = reqCommands[commandIndex].failWorkflowExecution?.failure?.stackTrace; + const expectedStack = expectedCommands[commandIndex].failWorkflowExecution?.failure?.stackTrace; + if (typeof reqStack !== typeof expectedStack) { + return; + } + } + const stackTraces: { actual: string; expected: string }[] = []; + for (let commandIndex = 0; commandIndex < reqCommands.length; commandIndex++) { + const reqStack = reqCommands[commandIndex].failWorkflowExecution?.failure?.stackTrace; + const expectedStack = expectedCommands[commandIndex].failWorkflowExecution?.failure?.stackTrace; + if (reqStack && expectedStack) { + stackTraces.push({ actual: reqStack, expected: expectedStack }); + delete reqCommands[commandIndex].failWorkflowExecution?.failure?.stackTrace; + delete expectedCommands[commandIndex].failWorkflowExecution?.failure?.stackTrace; + } + } + return stackTraces; } function makeSuccess( @@ -307,12 +345,13 @@ function makeCompleteWorkflowExecution(result?: Payload): coresdk.workflow_comma function makeFailWorkflowExecution( message: string, + stackTrace: string, type = 'Error', nonRetryable = true ): coresdk.workflow_commands.IWorkflowCommand { return { failWorkflowExecution: { - failure: { message, applicationFailureInfo: { type, nonRetryable }, source: 'TypeScriptSDK' }, + failure: { message, stackTrace, applicationFailureInfo: { type, nonRetryable }, source: 'TypeScriptSDK' }, }, }; } @@ -452,28 +491,22 @@ function cleanWorkflowQueryFailureStackTrace( return req; } -function removeWorkflowFailureStackTrace( - req: coresdk.workflow_completion.IWorkflowActivationCompletion, - commandIndex = 0 -) { - const stackTrace = req.successful!.commands![commandIndex].failWorkflowExecution!.failure!.stackTrace!; - delete req.successful!.commands![commandIndex].failWorkflowExecution!.failure!.stackTrace; - return stackTrace; -} - test('throwAsync', async (t) => { const { workflowType } = t.context; const req = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType))); - const actualStackTrace = removeWorkflowFailureStackTrace(req); - compareCompletion(t, req, makeSuccess([makeFailWorkflowExecution('failure')])); - compareStackTrace( + compareCompletion( t, - actualStackTrace, - dedent` + req, + makeSuccess([ + makeFailWorkflowExecution( + 'failure', + dedent` ApplicationFailure: failure at $CLASS.nonRetryable (common/src/failure.ts) at throwAsync (test/src/workflows/throw-async.ts) ` + ), + ]) ); }); @@ -773,25 +806,26 @@ test('interruptableWorkflow', async (t) => { const req = cleanWorkflowFailureStackTrace( await activate(t, await makeSignalWorkflow('interrupt', ['just because'])) ); - const stackTrace = removeWorkflowFailureStackTrace(req); compareCompletion( t, req, makeSuccess( - [makeFailWorkflowExecution('just because', 'Error', false)], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - compareStackTrace( - t, - stackTrace, - // The stack trace is weird here and might confuse users, it might be a JS limitation - // since the Error stack trace is generated in the constructor. - dedent` + [ + makeFailWorkflowExecution( + 'just because', + // The stack trace is weird here and might confuse users, it might be a JS limitation + // since the Error stack trace is generated in the constructor. + dedent` ApplicationFailure: just because at $CLASS.retryable (common/src/failure.ts) at test/src/workflows/interrupt-signal.ts - ` + `, + 'Error', + false + ), + ], + [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] + ) ); } }); @@ -804,23 +838,23 @@ test('failSignalWorkflow', async (t) => { } { const req = cleanWorkflowFailureStackTrace(await activate(t, await makeSignalWorkflow('fail', []))); - const stackTrace = removeWorkflowFailureStackTrace(req); compareCompletion( t, req, makeSuccess( - [makeFailWorkflowExecution('Signal failed', 'Error')], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - compareStackTrace( - t, - stackTrace, - dedent` + [ + makeFailWorkflowExecution( + 'Signal failed', + dedent` ApplicationFailure: Signal failed at $CLASS.nonRetryable (common/src/failure.ts) at test/src/workflows/fail-signal.ts - ` + `, + 'Error' + ), + ], + [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] + ) ); } }); @@ -844,22 +878,22 @@ test('asyncFailSignalWorkflow', async (t) => { } { const req = cleanWorkflowFailureStackTrace(await activate(t, makeFireTimer(2))); - const stackTrace = removeWorkflowFailureStackTrace(req); compareCompletion( t, req, makeSuccess( - [makeFailWorkflowExecution('Signal failed', 'Error')], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - compareStackTrace( - t, - stackTrace, - dedent` + [ + makeFailWorkflowExecution( + 'Signal failed', + dedent` ApplicationFailure: Signal failed at $CLASS.nonRetryable (common/src/failure.ts) - at test/src/workflows/async-fail-signal.ts` + at test/src/workflows/async-fail-signal.ts`, + 'Error' + ), + ], + [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] + ) ); } }); @@ -1449,7 +1483,6 @@ test('nonCancellableInNonCancellable', async (t) => { test('cancellationErrorIsPropagated', async (t) => { const { workflowType, logs } = t.context; const req = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType)), 2); - const stackTrace = removeWorkflowFailureStackTrace(req, 2); compareCompletion( t, req, @@ -1460,17 +1493,7 @@ test('cancellationErrorIsPropagated', async (t) => { failWorkflowExecution: { failure: { message: 'Cancellation scope cancelled', - canceledFailureInfo: {}, - source: 'TypeScriptSDK', - }, - }, - }, - ]) - ); - compareStackTrace( - t, - stackTrace, - dedent` + stackTrace: dedent` CancelledFailure: Cancellation scope cancelled at CancellationScope.cancel (workflow/src/cancellation-scope.ts) at test/src/workflows/cancellation-error-is-propagated.ts @@ -1478,7 +1501,13 @@ test('cancellationErrorIsPropagated', async (t) => { at CancellationScope.run (workflow/src/cancellation-scope.ts) at $CLASS.cancellable (workflow/src/cancellation-scope.ts) at cancellationErrorIsPropagated (test/src/workflows/cancellation-error-is-propagated.ts) - ` + `, + canceledFailureInfo: {}, + source: 'TypeScriptSDK', + }, + }, + }, + ]) ); t.deepEqual(logs, []); }); @@ -1664,9 +1693,13 @@ test('resolve activity with failure - http', async (t) => { }, }) ); - const stackTrace = removeWorkflowFailureStackTrace(completion); - compareCompletion(t, completion, makeSuccess([makeFailWorkflowExecution('Connection timeout', 'MockError')])); - compareStackTrace(t, stackTrace, 'ApplicationFailure: Connection timeout'); + compareCompletion( + t, + completion, + makeSuccess([ + makeFailWorkflowExecution('Connection timeout', 'ApplicationFailure: Connection timeout', 'MockError'), + ]) + ); } }); @@ -1877,16 +1910,19 @@ test('tryToContinueAfterCompletion', async (t) => { const { workflowType } = t.context; { const completion = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType))); - const stackTrace = removeWorkflowFailureStackTrace(completion); - compareCompletion(t, completion, makeSuccess([makeFailWorkflowExecution('fail before continue')])); - compareStackTrace( + compareCompletion( t, - stackTrace, - dedent` + completion, + makeSuccess([ + makeFailWorkflowExecution( + 'fail before continue', + dedent` ApplicationFailure: fail before continue at $CLASS.nonRetryable (common/src/failure.ts) at tryToContinueAfterCompletion (test/src/workflows/try-to-continue-after-completion.ts) ` + ), + ]) ); } }); From 1f55ff006c5c3c4c53fa64f05d7725c007e0a823 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 30 Sep 2025 14:47:11 -0400 Subject: [PATCH 10/10] run problematic tests serially --- packages/test/src/test-integration-workflows.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/test/src/test-integration-workflows.ts b/packages/test/src/test-integration-workflows.ts index 3c91b745c..56722cfd7 100644 --- a/packages/test/src/test-integration-workflows.ts +++ b/packages/test/src/test-integration-workflows.ts @@ -1075,7 +1075,7 @@ export function setAndClearTimeoutInterceptors(): workflow.WorkflowInterceptors } if (RUN_TIME_SKIPPING_TESTS) { - test('setTimeout and clearTimeout - works before and after 1.10.3', async (t) => { + test.serial('setTimeout and clearTimeout - works before and after 1.10.3', async (t) => { const env = await TestWorkflowEnvironment.createTimeSkipping(); const { createWorker, startWorkflow } = helpers(t, env); try { @@ -1292,7 +1292,7 @@ test('Count workflow executions', async (t) => { }); }); -test('can register search attributes to dev server', async (t) => { +test.serial('can register search attributes to dev server', async (t) => { const key = defineSearchAttributeKey('new-search-attr', SearchAttributeType.INT); const newSearchAttribute: SearchAttributePair = { key, value: 12 };