diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bf2b13aa..303936c74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,12 +60,12 @@ jobs: run: shell: bash steps: - - name: 'Checkout code' + - name: "Checkout code" uses: actions/checkout@v4 with: submodules: recursive - - name: 'Cache index.node' + - name: "Cache index.node" id: cached-artifact uses: actions/cache@v4 with: @@ -77,7 +77,7 @@ jobs: uses: arduino/setup-protoc@v3 with: # TODO: Upgrade proto once https://github.com/arduino/setup-protoc/issues/99 is fixed - version: '23.x' + version: "23.x" repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Upgrade Rust to latest stable @@ -90,7 +90,7 @@ jobs: workspaces: packages/core-bridge -> target prefix-key: corebridge-buildcache-debug shared-key: ${{ matrix.platform }} - env-vars: '' + env-vars: "" save-if: ${{ env.IS_MAIN_OR_RELEASE == 'true' }} - name: Compile rust code @@ -111,7 +111,7 @@ jobs: # Run integration tests. # Uses the native binaries built in compile-native-binaries, but build `@temporalio/*` packages locally. integration-tests: - timeout-minutes: 20 + timeout-minutes: 10 needs: - compile-native-binaries-debug strategy: @@ -148,7 +148,7 @@ jobs: - name: Set git config run: git config --global core.autocrlf false - - name: 'Checkout code' + - name: "Checkout code" uses: actions/checkout@v4 with: submodules: recursive @@ -209,11 +209,20 @@ jobs: --db-filename temporal.sqlite \ --sqlite-pragma journal_mode=WAL \ --sqlite-pragma synchronous=OFF \ + --dynamic-config-value system.enableEagerWorkflowStart=true \ + --dynamic-config-value system.enableNexus=true \ + --dynamic-config-value frontend.workerVersioningWorkflowAPIs=true \ + --dynamic-config-value frontend.workerVersioningDataAPIs=true \ + --dynamic-config-value system.enableDeploymentVersions=true \ + --dynamic-config-value component.nexusoperations.recordCancelRequestCompletionEvents=true \ + --dynamic-config-value frontend.WorkerHeartbeatsEnabled=true \ + --dynamic-config-value frontend.ListWorkersEnabled=true \ --headless &> ./devserver.log & - - name: Run Tests - run: npm run test + - name: Run Tests # DEBUG,temporal_sdk_core=DEBUG,temporal_client=DEBUG,temporal_sdk=DEBUG + run: RUST_LOG=DEBUG,temporal_sdk_core=DEBUG,temporal_client=DEBUG,temporal_sdk=DEBUG npm run test env: + TEMPORAL_TRACE_NATIVE_CALLS: true RUN_INTEGRATION_TESTS: true REUSE_V8_CONTEXT: ${{ matrix.reuse-v8-context }} @@ -233,104 +242,103 @@ jobs: TEMPORAL_CLOUD_OPS_TEST_NAMESPACE: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }} TEMPORAL_CLOUD_OPS_TEST_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} TEMPORAL_CLOUD_OPS_TEST_API_VERSION: 2024-05-13-00 - - # FIXME: Move samples tests to a custom activity - # Sample 1: hello-world to local server - - name: Instantiate sample project using verdaccio artifacts - Hello World - run: | - node scripts/init-from-verdaccio.js --registry-dir ${{ steps.tmp-dir.outputs.dir }}/npm-registry --sample https://github.com/temporalio/samples-typescript/tree/main/hello-world --target-dir ${{ steps.tmp-dir.outputs.dir }}/sample-hello-world - node scripts/test-example.js --work-dir "${{ steps.tmp-dir.outputs.dir }}/sample-hello-world" - - # Sample 2: hello-world-mtls to cloud server - - name: Instantiate sample project using verdaccio artifacts - Hello World MTLS - run: | - if [ -z "$TEMPORAL_ADDRESS" ] || [ -z "$TEMPORAL_NAMESPACE" ] || [ -z "$TEMPORAL_CLIENT_CERT" ] || [ -z "$TEMPORAL_CLIENT_KEY" ]; then - echo "Skipping hello-world-mtls sample test as required environment variables are not set" - exit 0 - fi - - node scripts/create-certs-dir.js ${{ steps.tmp-dir.outputs.dir }}/certs - node scripts/init-from-verdaccio.js --registry-dir ${{ steps.tmp-dir.outputs.dir }}/npm-registry --sample https://github.com/temporalio/samples-typescript/tree/main/hello-world-mtls --target-dir ${{ steps.tmp-dir.outputs.dir }}/sample-hello-world-mtls - node scripts/test-example.js --work-dir "${{ steps.tmp-dir.outputs.dir }}/sample-hello-world-mtls" - env: - # These env vars are used by the hello-world-mtls sample - TEMPORAL_ADDRESS: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }}.tmprl.cloud:7233 - TEMPORAL_NAMESPACE: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }} - TEMPORAL_CLIENT_CERT: ${{ secrets.TEMPORAL_CLIENT_CERT }} - TEMPORAL_CLIENT_KEY: ${{ secrets.TEMPORAL_CLIENT_KEY }} - TEMPORAL_TASK_QUEUE: ${{ format('tssdk-ci-{0}-{1}-sample-hello-world-mtls-{2}-{3}', matrix.platform, matrix.node, github.run_id, github.run_attempt) }} - - TEMPORAL_CLIENT_CERT_PATH: ${{ steps.tmp-dir.outputs.dir }}/certs/client.pem - TEMPORAL_CLIENT_KEY_PATH: ${{ steps.tmp-dir.outputs.dir }}/certs/client.key - - - name: Destroy certs dir - if: always() - run: rm -rf ${{ steps.tmp-dir.outputs.dir }}/certs - continue-on-error: true - - # Sample 3: fetch-esm to local server - - name: Instantiate sample project using verdaccio artifacts - Fetch ESM - run: | - node scripts/init-from-verdaccio.js --registry-dir ${{ steps.tmp-dir.outputs.dir }}/npm-registry --sample https://github.com/temporalio/samples-typescript/tree/main/fetch-esm --target-dir ${{ steps.tmp-dir.outputs.dir }}/sample-fetch-esm - node scripts/test-example.js --work-dir "${{ steps.tmp-dir.outputs.dir }}/sample-fetch-esm" - - # End samples - - - name: Upload NPM logs - uses: actions/upload-artifact@v4 - if: failure() || cancelled() - with: - name: integration-tests-${{ matrix.platform }}-node${{ matrix.node }}-${{ matrix.reuse-v8-context && 'reuse' || 'noreuse' }}-logs - path: ${{ startsWith(matrix.platform, 'windows') && 'C:\\npm\\_logs\\' || '~/.npm/_logs/' }} - - - name: Upload Dev Server logs - uses: actions/upload-artifact@v4 - if: failure() || cancelled() - with: - name: integration-tests-${{ matrix.platform }}-node${{ matrix.node }}-${{ matrix.reuse-v8-context && 'reuse' || 'noreuse' }}-devserver-logs - path: ${{ steps.tmp-dir.outputs.dir }}/devserver.log - - conventions: - name: Lint and Prune - uses: ./.github/workflows/conventions.yml - - # Runs the features repo tests with this repo's current SDK code - # FIXME: Update this job to reuse native build artifacts from compile-native-binaries - features-tests: - name: Features Tests - uses: temporalio/features/.github/workflows/typescript.yaml@main - with: - typescript-repo-path: ${{github.event.pull_request.head.repo.full_name}} - version: ${{github.event.pull_request.head.ref}} - version-is-repo-ref: true - features-repo-ref: main - - stress-tests-no-reuse-context: - name: Stress Tests (No Reuse V8 Context) - # FIXME: Update this job to reuse native build artifacts from compile-native-binaries - uses: ./.github/workflows/stress.yml - with: - test-type: ci-stress - test-timeout-minutes: 20 - reuse-v8-context: false - - stress-tests-reuse-context: - name: Stress Tests (Reuse V8 Context) - # FIXME: Update this job to reuse native build artifacts from compile-native-binaries - uses: ./.github/workflows/stress.yml - with: - test-type: ci-stress - test-timeout-minutes: 20 - reuse-v8-context: true - - docs: - name: Build Docs - uses: ./.github/workflows/docs.yml - with: - # Can't publish from forks, as secrets won't be available - publish_target: ${{ vars.IS_TEMPORALIO_SDK_TYPESCRIPT_REPO == 'true' && 'draft' || '' }} - secrets: - ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +# # FIXME: Move samples tests to a custom activity +# # Sample 1: hello-world to local server +# - name: Instantiate sample project using verdaccio artifacts - Hello World +# run: | +# node scripts/init-from-verdaccio.js --registry-dir ${{ steps.tmp-dir.outputs.dir }}/npm-registry --sample https://github.com/temporalio/samples-typescript/tree/main/hello-world --target-dir ${{ steps.tmp-dir.outputs.dir }}/sample-hello-world +# node scripts/test-example.js --work-dir "${{ steps.tmp-dir.outputs.dir }}/sample-hello-world" +# +# # Sample 2: hello-world-mtls to cloud server +# - name: Instantiate sample project using verdaccio artifacts - Hello World MTLS +# run: | +# if [ -z "$TEMPORAL_ADDRESS" ] || [ -z "$TEMPORAL_NAMESPACE" ] || [ -z "$TEMPORAL_CLIENT_CERT" ] || [ -z "$TEMPORAL_CLIENT_KEY" ]; then +# echo "Skipping hello-world-mtls sample test as required environment variables are not set" +# exit 0 +# fi +# +# node scripts/create-certs-dir.js ${{ steps.tmp-dir.outputs.dir }}/certs +# node scripts/init-from-verdaccio.js --registry-dir ${{ steps.tmp-dir.outputs.dir }}/npm-registry --sample https://github.com/temporalio/samples-typescript/tree/main/hello-world-mtls --target-dir ${{ steps.tmp-dir.outputs.dir }}/sample-hello-world-mtls +# node scripts/test-example.js --work-dir "${{ steps.tmp-dir.outputs.dir }}/sample-hello-world-mtls" +# env: +# # These env vars are used by the hello-world-mtls sample +# TEMPORAL_ADDRESS: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }}.tmprl.cloud:7233 +# TEMPORAL_NAMESPACE: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }} +# TEMPORAL_CLIENT_CERT: ${{ secrets.TEMPORAL_CLIENT_CERT }} +# TEMPORAL_CLIENT_KEY: ${{ secrets.TEMPORAL_CLIENT_KEY }} +# TEMPORAL_TASK_QUEUE: ${{ format('tssdk-ci-{0}-{1}-sample-hello-world-mtls-{2}-{3}', matrix.platform, matrix.node, github.run_id, github.run_attempt) }} +# +# TEMPORAL_CLIENT_CERT_PATH: ${{ steps.tmp-dir.outputs.dir }}/certs/client.pem +# TEMPORAL_CLIENT_KEY_PATH: ${{ steps.tmp-dir.outputs.dir }}/certs/client.key +# +# - name: Destroy certs dir +# if: always() +# run: rm -rf ${{ steps.tmp-dir.outputs.dir }}/certs +# continue-on-error: true +# +# # Sample 3: fetch-esm to local server +# - name: Instantiate sample project using verdaccio artifacts - Fetch ESM +# run: | +# node scripts/init-from-verdaccio.js --registry-dir ${{ steps.tmp-dir.outputs.dir }}/npm-registry --sample https://github.com/temporalio/samples-typescript/tree/main/fetch-esm --target-dir ${{ steps.tmp-dir.outputs.dir }}/sample-fetch-esm +# node scripts/test-example.js --work-dir "${{ steps.tmp-dir.outputs.dir }}/sample-fetch-esm" +# +# # End samples +# +# - name: Upload NPM logs +# uses: actions/upload-artifact@v4 +# if: failure() || cancelled() +# with: +# name: integration-tests-${{ matrix.platform }}-node${{ matrix.node }}-${{ matrix.reuse-v8-context && 'reuse' || 'noreuse' }}-logs +# path: ${{ startsWith(matrix.platform, 'windows') && 'C:\\npm\\_logs\\' || '~/.npm/_logs/' }} +# +# - name: Upload Dev Server logs +# uses: actions/upload-artifact@v4 +# if: failure() || cancelled() +# with: +# name: integration-tests-${{ matrix.platform }}-node${{ matrix.node }}-${{ matrix.reuse-v8-context && 'reuse' || 'noreuse' }}-devserver-logs +# path: ${{ steps.tmp-dir.outputs.dir }}/devserver.log + +#conventions: +# name: Lint and Prune +# uses: ./.github/workflows/conventions.yml + +# # Runs the features repo tests with this repo's current SDK code +# # FIXME: Update this job to reuse native build artifacts from compile-native-binaries +# features-tests: +# name: Features Tests +# uses: temporalio/features/.github/workflows/typescript.yaml@main +# with: +# typescript-repo-path: ${{github.event.pull_request.head.repo.full_name}} +# version: ${{github.event.pull_request.head.ref}} +# version-is-repo-ref: true +# features-repo-ref: main + +# stress-tests-no-reuse-context: +# name: Stress Tests (No Reuse V8 Context) +# # FIXME: Update this job to reuse native build artifacts from compile-native-binaries +# uses: ./.github/workflows/stress.yml +# with: +# test-type: ci-stress +# test-timeout-minutes: 20 +# reuse-v8-context: false +# +# stress-tests-reuse-context: +# name: Stress Tests (Reuse V8 Context) +# # FIXME: Update this job to reuse native build artifacts from compile-native-binaries +# uses: ./.github/workflows/stress.yml +# with: +# test-type: ci-stress +# test-timeout-minutes: 20 +# reuse-v8-context: true + +# docs: +# name: Build Docs +# uses: ./.github/workflows/docs.yml +# with: +# # Can't publish from forks, as secrets won't be available +# publish_target: ${{ vars.IS_TEMPORALIO_SDK_TYPESCRIPT_REPO == 'true' && 'draft' || '' }} +# secrets: +# ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} +# VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} +# VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} +# VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} diff --git a/packages/core-bridge/Cargo.lock b/packages/core-bridge/Cargo.lock index a3f0f9339..0fb1f9b6e 100644 --- a/packages/core-bridge/Cargo.lock +++ b/packages/core-bridge/Cargo.lock @@ -1375,23 +1375,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "opentelemetry" -version = "0.29.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e87237e2775f74896f9ad219d26a2081751187eb7c9f5c58dde20a23b95d16c" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.14", - "tracing", -] - -[[package]] -name = "opentelemetry" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" dependencies = [ "futures-core", "futures-sink", @@ -1403,25 +1389,25 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", "http", - "opentelemetry 0.30.0", + "opentelemetry", "reqwest", ] [[package]] name = "opentelemetry-otlp" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" dependencies = [ "http", - "opentelemetry 0.30.0", + "opentelemetry", "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", @@ -1435,29 +1421,29 @@ dependencies = [ [[package]] name = "opentelemetry-proto" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ - "opentelemetry 0.30.0", + "opentelemetry", "opentelemetry_sdk", "prost", "tonic", + "tonic-prost", ] [[package]] name = "opentelemetry_sdk" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry 0.30.0", + "opentelemetry", "percent-encoding", "rand 0.9.2", - "serde_json", "thiserror 2.0.14", "tokio", "tokio-stream", @@ -1654,9 +1640,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ "bytes", "prost-derive", @@ -1664,9 +1650,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" dependencies = [ "heck", "itertools", @@ -1677,6 +1663,8 @@ dependencies = [ "prettyplease", "prost", "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", "regex", "syn", "tempfile", @@ -1684,9 +1672,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", "itertools", @@ -1697,18 +1685,18 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" dependencies = [ "prost", ] [[package]] name = "prost-wkt" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497e1e938f0c09ef9cabe1d49437b4016e03e8f82fbbe5d1c62a9b61b9decae1" +checksum = "655944d0ce015e71b3ec21279437e6a09e58433e50c7b0677901f3d5235e74f5" dependencies = [ "chrono", "inventory", @@ -1721,9 +1709,9 @@ dependencies = [ [[package]] name = "prost-wkt-build" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b8bf115b70a7aa5af1fd5d6e9418492e9ccb6e4785e858c938e28d132a884b" +checksum = "f869f1443fee474b785e935d92e1007f57443e485f51668ed41943fc01a321a2" dependencies = [ "heck", "prost", @@ -1734,9 +1722,9 @@ dependencies = [ [[package]] name = "prost-wkt-types" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8cdde6df0a98311c839392ca2f2f0bcecd545f86a62b4e3c6a49c336e970fe5" +checksum = "eeeffd6b9becd4600dd461399f3f71aeda2ff0848802a9ed526cf12e8f42902a" dependencies = [ "chrono", "prost", @@ -1770,6 +1758,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5b6a0769a491a08b31ea5c62494a8f144ee0987d86d670a8af4df1e1b7cde75" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "quanta" version = "0.12.6" @@ -2040,29 +2048,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustfsm" -version = "0.1.0" -dependencies = [ - "rustfsm_procmacro", - "rustfsm_trait", -] - -[[package]] -name = "rustfsm_procmacro" -version = "0.1.0" -dependencies = [ - "derive_more", - "proc-macro2", - "quote", - "rustfsm_trait", - "syn", -] - -[[package]] -name = "rustfsm_trait" -version = "0.1.0" - [[package]] name = "rustix" version = "1.0.8" @@ -2404,7 +2389,34 @@ dependencies = [ ] [[package]] -name = "temporal-client" +name = "temporal-sdk-typescript-bridge" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bridge-macros", + "futures", + "neon", + "opentelemetry", + "os_pipe", + "parking_lot", + "prost", + "prost-types", + "serde", + "serde_json", + "temporalio-client", + "temporalio-common", + "temporalio-sdk-core", + "thiserror 2.0.14", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "temporalio-client" version = "0.1.0" dependencies = [ "anyhow", @@ -2422,9 +2434,9 @@ dependencies = [ "hyper", "hyper-util", "parking_lot", + "rand 0.9.2", "slotmap", - "temporal-sdk-core-api", - "temporal-sdk-core-protos", + "temporalio-common", "thiserror 2.0.14", "tokio", "tonic", @@ -2435,7 +2447,43 @@ dependencies = [ ] [[package]] -name = "temporal-sdk-core" +name = "temporalio-common" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "derive_builder", + "derive_more", + "opentelemetry", + "prost", + "prost-wkt", + "prost-wkt-types", + "rand 0.9.2", + "serde", + "serde_json", + "thiserror 2.0.14", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tracing", + "tracing-core", + "url", + "uuid", +] + +[[package]] +name = "temporalio-macros" +version = "0.1.0" +dependencies = [ + "derive_more", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "temporalio-sdk-core" version = "0.1.0" dependencies = [ "anyhow", @@ -2459,7 +2507,7 @@ dependencies = [ "itertools", "lru", "mockall", - "opentelemetry 0.30.0", + "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", "parking_lot", @@ -2471,16 +2519,15 @@ dependencies = [ "rand 0.9.2", "reqwest", "ringbuf", - "rustfsm", "serde", "serde_json", "siphasher", "slotmap", "sysinfo", "tar", - "temporal-client", - "temporal-sdk-core-api", - "temporal-sdk-core-protos", + "temporalio-client", + "temporalio-common", + "temporalio-macros", "thiserror 2.0.14", "tokio", "tokio-stream", @@ -2493,71 +2540,6 @@ dependencies = [ "zip", ] -[[package]] -name = "temporal-sdk-core-api" -version = "0.1.0" -dependencies = [ - "async-trait", - "derive_builder", - "derive_more", - "opentelemetry 0.30.0", - "prost", - "serde_json", - "temporal-sdk-core-protos", - "thiserror 2.0.14", - "tonic", - "tracing", - "tracing-core", - "url", -] - -[[package]] -name = "temporal-sdk-core-protos" -version = "0.1.0" -dependencies = [ - "anyhow", - "base64", - "derive_more", - "prost", - "prost-build", - "prost-wkt", - "prost-wkt-build", - "prost-wkt-types", - "rand 0.9.2", - "serde", - "serde_json", - "thiserror 2.0.14", - "tonic", - "tonic-build", - "uuid", -] - -[[package]] -name = "temporal-sdk-typescript-bridge" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "bridge-macros", - "futures", - "neon", - "opentelemetry 0.29.1", - "os_pipe", - "parking_lot", - "prost", - "prost-types", - "serde", - "serde_json", - "temporal-client", - "temporal-sdk-core", - "thiserror 2.0.14", - "tokio", - "tokio-stream", - "tonic", - "tracing", - "tracing-subscriber", -] - [[package]] name = "termtree" version = "0.5.1" @@ -2705,9 +2687,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.13.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", "axum", @@ -2722,9 +2704,9 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", "rustls-native-certs", - "socket2 0.5.10", + "socket2 0.6.0", + "sync_wrapper", "tokio", "tokio-rustls", "tokio-stream", @@ -2736,9 +2718,32 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.13.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847" +checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" dependencies = [ "prettyplease", "proc-macro2", @@ -2746,6 +2751,8 @@ dependencies = [ "prost-types", "quote", "syn", + "tempfile", + "tonic-build", ] [[package]] @@ -2882,6 +2889,12 @@ dependencies = [ "syn", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -2925,9 +2938,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", diff --git a/packages/core-bridge/Cargo.toml b/packages/core-bridge/Cargo.toml index 1831c7b59..41be46b2b 100644 --- a/packages/core-bridge/Cargo.toml +++ b/packages/core-bridge/Cargo.toml @@ -25,28 +25,29 @@ async-trait = "0.1.83" bridge-macros = { path = "bridge-macros" } futures = { version = "0.3", features = ["executor"] } neon = { version = "1.0.0", default-features = false, features = [ - "napi-6", - "futures", + "napi-6", + "futures", ] } -opentelemetry = "0.29" +opentelemetry = "0.31" os_pipe = "1.2.1" parking_lot = "0.12" -prost = "0.13" -prost-types = "0.13" +prost = "0.14" +prost-types = "0.14" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -temporal-sdk-core = { version = "*", path = "./sdk-core/core", features = [ - "ephemeral-server", +temporalio-sdk-core = { version = "*", path = "./sdk-core/crates/sdk-core", features = [ + "ephemeral-server", ] } -temporal-client = { version = "*", path = "./sdk-core/client" } +temporalio-client = { version = "*", path = "./sdk-core/crates/client" } +temporalio-common = { version = "*", path = "./sdk-core/crates/common" } thiserror = "2" tokio = "1.13" tokio-stream = "0.1" -tonic = "0.13" +tonic = "0.14" tracing = "0.1" tracing-subscriber = { version = "0.3", default-features = false, features = [ - "parking_lot", - "env-filter", - "registry", - "ansi", + "parking_lot", + "env-filter", + "registry", + "ansi", ] } diff --git a/packages/core-bridge/sdk-core b/packages/core-bridge/sdk-core index de674173c..79e0fa5b2 160000 --- a/packages/core-bridge/sdk-core +++ b/packages/core-bridge/sdk-core @@ -1 +1 @@ -Subproject commit de674173c664d42f85d0dee1ff3b2ac47e36d545 +Subproject commit 79e0fa5b2c5ae6b08fd40f2a272671f9e123bc93 diff --git a/packages/core-bridge/src/client.rs b/packages/core-bridge/src/client.rs index 7b6fc6957..a8309876d 100644 --- a/packages/core-bridge/src/client.rs +++ b/packages/core-bridge/src/client.rs @@ -5,10 +5,10 @@ use std::{collections::HashMap, sync::Arc}; use neon::prelude::*; use tonic::metadata::{BinaryMetadataValue, MetadataKey}; -use temporal_sdk_core::{ClientOptions as CoreClientOptions, CoreRuntime, RetryClient}; +use temporalio_sdk_core::{ClientOptions as CoreClientOptions, CoreRuntime, RetryClient}; use bridge_macros::{TryFromJs, js_function}; -use temporal_client::{ClientInitError, ConfiguredClient, TemporalServiceClient}; +use temporalio_client::{ClientInitError, ConfiguredClient, TemporalServiceClient}; use crate::runtime::Runtime; use crate::{helpers::*, runtime::RuntimeExt as _}; @@ -55,33 +55,36 @@ pub fn client_new( let runtime = runtime.borrow()?.core_runtime.clone(); let config: CoreClientOptions = config.try_into()?; - runtime.clone().future_to_promise(async move { - let metric_meter = runtime.clone().telemetry().get_temporal_metric_meter(); - let res = config.connect_no_namespace(metric_meter).await; - - let core_client = match res { - Ok(core_client) => core_client, - Err(ClientInitError::InvalidHeaders(e)) => Err(BridgeError::TypeError { - message: format!("Invalid metadata key: {e}"), - field: None, - })?, - Err(ClientInitError::SystemInfoCallError(e)) => Err(BridgeError::TransportError( - format!("Failed to call GetSystemInfo: {e}"), - ))?, - Err(ClientInitError::TonicTransportError(e)) => { - Err(BridgeError::TransportError(format!("{e:?}")))? - } - Err(ClientInitError::InvalidUri(e)) => Err(BridgeError::TypeError { - message: e.to_string(), - field: None, - })?, - }; - - Ok(OpaqueOutboundHandle::new(Client { - core_runtime: runtime, - core_client, - })) - }) + runtime.clone().future_to_promise_named( + async move { + let metric_meter = runtime.clone().telemetry().get_temporal_metric_meter(); + let res = config.connect_no_namespace(metric_meter).await; + + let core_client = match res { + Ok(core_client) => core_client, + Err(ClientInitError::InvalidHeaders(e)) => Err(BridgeError::TypeError { + message: format!("Invalid metadata key: {e}"), + field: None, + })?, + Err(ClientInitError::SystemInfoCallError(e)) => Err(BridgeError::TransportError( + format!("Failed to call GetSystemInfo: {e}"), + ))?, + Err(ClientInitError::TonicTransportError(e)) => { + Err(BridgeError::TransportError(format!("{e:?}")))? + } + Err(ClientInitError::InvalidUri(e)) => Err(BridgeError::TypeError { + message: e.to_string(), + field: None, + })?, + }; + + Ok(OpaqueOutboundHandle::new(Client { + core_runtime: runtime, + core_client, + })) + }, + "client_new", + ) } /// Update a Client's HTTP request headers @@ -257,7 +260,7 @@ async fn client_invoke_workflow_service( mut retry_client: CoreClient, call: RpcCall, ) -> BridgeResult> { - use temporal_client::WorkflowService; + use temporalio_client::WorkflowService; match call.rpc.as_str() { "CountWorkflowExecutions" => { @@ -290,6 +293,9 @@ async fn client_invoke_workflow_service( "DescribeDeployment" => { rpc_call!(retry_client, call, describe_deployment) } + "DescribeWorker" => { + rpc_call!(retry_client, call, describe_worker) + } "DeprecateNamespace" => rpc_call!(retry_client, call, deprecate_namespace), "DescribeNamespace" => rpc_call!(retry_client, call, describe_namespace), "DescribeSchedule" => rpc_call!(retry_client, call, describe_schedule), @@ -450,6 +456,9 @@ async fn client_invoke_workflow_service( "SetWorkerDeploymentCurrentVersion" => { rpc_call!(retry_client, call, set_worker_deployment_current_version) } + "SetWorkerDeploymentManager" => { + rpc_call!(retry_client, call, set_worker_deployment_manager) + } "SetWorkerDeploymentRampingVersion" => { rpc_call!(retry_client, call, set_worker_deployment_ramping_version) } @@ -522,7 +531,7 @@ async fn client_invoke_operator_service( mut retry_client: CoreClient, call: RpcCall, ) -> BridgeResult> { - use temporal_client::OperatorService; + use temporalio_client::OperatorService; match call.rpc.as_str() { "AddOrUpdateRemoteCluster" => { @@ -560,7 +569,7 @@ async fn client_invoke_test_service( mut retry_client: CoreClient, call: RpcCall, ) -> BridgeResult> { - use temporal_client::TestService; + use temporalio_client::TestService; match call.rpc.as_str() { "GetCurrentTime" => rpc_call!(retry_client, call, get_current_time), @@ -582,7 +591,7 @@ async fn client_invoke_health_service( mut retry_client: CoreClient, call: RpcCall, ) -> BridgeResult> { - use temporal_client::HealthService; + use temporalio_client::HealthService; match call.rpc.as_str() { "Check" => rpc_call!(retry_client, call, check), @@ -652,8 +661,8 @@ mod config { use anyhow::Context as _; - use temporal_client::HttpConnectProxyOptions; - use temporal_sdk_core::{ + use temporalio_client::HttpConnectProxyOptions; + use temporalio_sdk_core::{ ClientOptions as CoreClientOptions, ClientOptionsBuilder, ClientTlsConfig as CoreClientTlsConfig, TlsConfig as CoreTlsConfig, Url, }; diff --git a/packages/core-bridge/src/helpers/try_from_js.rs b/packages/core-bridge/src/helpers/try_from_js.rs index 472ad2010..62e6aff11 100644 --- a/packages/core-bridge/src/helpers/try_from_js.rs +++ b/packages/core-bridge/src/helpers/try_from_js.rs @@ -9,7 +9,7 @@ use neon::{ Value, buffer::TypedArray, }, }; -use temporal_sdk_core::Url; +use temporalio_sdk_core::Url; use super::{AppendFieldContext, BridgeError, BridgeResult}; diff --git a/packages/core-bridge/src/logs.rs b/packages/core-bridge/src/logs.rs index 694d3c7e2..dc8ecc485 100644 --- a/packages/core-bridge/src/logs.rs +++ b/packages/core-bridge/src/logs.rs @@ -6,7 +6,7 @@ use std::{ use neon::prelude::*; use serde::{Serialize, ser::SerializeMap as _}; -use temporal_sdk_core::api::telemetry::CoreLog; +use temporalio_common::telemetry::CoreLog; use bridge_macros::js_function; diff --git a/packages/core-bridge/src/metrics.rs b/packages/core-bridge/src/metrics.rs index b4831043b..d03c71ed8 100644 --- a/packages/core-bridge/src/metrics.rs +++ b/packages/core-bridge/src/metrics.rs @@ -4,14 +4,14 @@ use anyhow::Context as _; use neon::prelude::*; use serde::Deserialize; -use temporal_sdk_core::api::telemetry::metrics::{ +use temporalio_common::telemetry::metrics::{ CoreMeter, Counter as CoreCounter, Gauge as CoreGauge, Histogram as CoreHistogram, MetricParametersBuilder, NewAttributes, TemporalMeter, }; -use temporal_sdk_core::api::telemetry::metrics::{ +use temporalio_common::telemetry::metrics::{ GaugeF64 as CoreGaugeF64, HistogramF64 as CoreHistogramF64, }; -use temporal_sdk_core::api::telemetry::metrics::{ +use temporalio_common::telemetry::metrics::{ MetricKeyValue as CoreMetricKeyValue, MetricValue as CoreMetricValue, }; diff --git a/packages/core-bridge/src/runtime.rs b/packages/core-bridge/src/runtime.rs index 42ad067ef..0f0371d19 100644 --- a/packages/core-bridge/src/runtime.rs +++ b/packages/core-bridge/src/runtime.rs @@ -3,14 +3,14 @@ use std::{sync::Arc, time::Duration}; use anyhow::Context as _; use futures::channel::mpsc::Receiver; use neon::prelude::*; -use tracing::{Instrument, warn}; +use tracing::{Instrument, debug, warn}; -use temporal_sdk_core::{ - CoreRuntime, TokioRuntimeBuilder, - api::telemetry::{ - CoreLog, OtelCollectorOptions as CoreOtelCollectorOptions, - PrometheusExporterOptions as CorePrometheusExporterOptions, metrics::CoreMeter, - }, +use temporalio_common::telemetry::{ + CoreLog, OtelCollectorOptions as CoreOtelCollectorOptions, + PrometheusExporterOptions as CorePrometheusExporterOptions, metrics::CoreMeter, +}; +use temporalio_sdk_core::{ + CoreRuntime, RuntimeOptionsBuilder, TokioRuntimeBuilder, telemetry::{build_otlp_metric_exporter, start_prometheus_metric_exporter}, }; @@ -62,8 +62,15 @@ pub fn runtime_new( let (telemetry_options, metrics_options, logging_options) = bridge_options.try_into()?; // Create core runtime which starts tokio multi-thread runtime - let mut core_runtime = CoreRuntime::new(telemetry_options, TokioRuntimeBuilder::default()) - .context("Failed to initialize Core Runtime")?; + let mut core_runtime = CoreRuntime::new( + RuntimeOptionsBuilder::default() + .telemetry_options(telemetry_options) + .heartbeat_interval(None) + .build() + .unwrap(), + TokioRuntimeBuilder::default(), + ) + .context("Failed to initialize Core Runtime")?; enter_sync!(core_runtime); @@ -130,6 +137,7 @@ pub fn runtime_new( /// runtimes at a high pace, e.g. during tests execution. #[js_function] pub fn runtime_shutdown(runtime: OpaqueInboundHandle) -> BridgeResult<()> { + debug!("dropping runtime"); std::mem::drop(runtime.take()?); Ok(()) } @@ -160,6 +168,15 @@ pub trait RuntimeExt { F: Future> + Send + 'static, R: TryIntoJs + Send + 'static; + fn future_to_promise_named( + &self, + future: F, + caller: &'static str, + ) -> BridgeResult> + where + F: Future> + Send + 'static, + R: TryIntoJs + Send + 'static; + /// Spawn a future on the Tokio runtime, and let it run to completion without waiting for it to /// complete. Should any error occur, we'll try to send them to this Runtime's logger, but may /// end up or silently dropping entries in some extreme cases. @@ -180,6 +197,21 @@ impl RuntimeExt for CoreRuntime { ))) } + fn future_to_promise_named( + &self, + future: F, + caller: &'static str, + ) -> BridgeResult> + where + F: Future> + Send + 'static, + R: TryIntoJs + Send + 'static, + { + enter_sync!(self); + Ok(BridgeFuture::new(Box::pin(future.instrument( + tracing::info_span!("future_to_promise_named", caller), + )))) + } + fn spawn_and_forget(&self, future: F) where F: Future> + Send + 'static, @@ -209,6 +241,18 @@ impl RuntimeExt for Arc { { self.as_ref().spawn_and_forget(future); } + + fn future_to_promise_named( + &self, + future: F, + caller: &'static str, + ) -> BridgeResult> + where + F: Future> + Send + 'static, + R: TryIntoJs + Send + 'static, + { + self.as_ref().future_to_promise_named(future, caller) + } } //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -235,17 +279,14 @@ mod config { use anyhow::Context as _; use neon::prelude::*; - use temporal_sdk_core::{ - Url, - api::telemetry::{ - HistogramBucketOverrides, Logger as CoreTelemetryLogger, MetricTemporality, - OtelCollectorOptions as CoreOtelCollectorOptions, OtelCollectorOptionsBuilder, - OtlpProtocol, PrometheusExporterOptions as CorePrometheusExporterOptions, - PrometheusExporterOptionsBuilder, TelemetryOptions as CoreTelemetryOptions, - TelemetryOptionsBuilder, - }, - telemetry::CoreLogStreamConsumer, + use temporalio_common::telemetry::{ + HistogramBucketOverrides, Logger as CoreTelemetryLogger, MetricTemporality, + OtelCollectorOptions as CoreOtelCollectorOptions, OtelCollectorOptionsBuilder, + OtlpProtocol, PrometheusExporterOptions as CorePrometheusExporterOptions, + PrometheusExporterOptionsBuilder, TelemetryOptions as CoreTelemetryOptions, + TelemetryOptionsBuilder, }; + use temporalio_sdk_core::{Url, telemetry::CoreLogStreamConsumer}; use bridge_macros::TryFromJs; diff --git a/packages/core-bridge/src/testing.rs b/packages/core-bridge/src/testing.rs index 6be02eebf..c7c02049e 100644 --- a/packages/core-bridge/src/testing.rs +++ b/packages/core-bridge/src/testing.rs @@ -4,13 +4,13 @@ use std::{process::Stdio, sync::Arc}; use anyhow::Context as _; use neon::prelude::*; -use temporal_sdk_core::ephemeral_server::{ +use temporalio_sdk_core::ephemeral_server::{ EphemeralServer as CoreEphemeralServer, TemporalDevServerConfig as CoreTemporalDevServerConfig, TestServerConfig as CoreTestServerConfig, }; use bridge_macros::js_function; -use temporal_sdk_core::CoreRuntime; +use temporalio_sdk_core::CoreRuntime; use crate::helpers::*; use crate::runtime::{Runtime, RuntimeExt as _}; @@ -191,7 +191,7 @@ mod config { use anyhow::Context as _; - use temporal_sdk_core::ephemeral_server::{ + use temporalio_sdk_core::ephemeral_server::{ EphemeralExe, EphemeralExeVersion, TemporalDevServerConfig as CoreTemporalDevServerConfig, TemporalDevServerConfigBuilder, TestServerConfig as CoreTestServerConfig, TestServerConfigBuilder, diff --git a/packages/core-bridge/src/worker.rs b/packages/core-bridge/src/worker.rs index f92184f41..7f34d04ee 100644 --- a/packages/core-bridge/src/worker.rs +++ b/packages/core-bridge/src/worker.rs @@ -6,22 +6,22 @@ use prost::Message; use tokio::sync::mpsc::{Sender, channel}; use tokio_stream::wrappers::ReceiverStream; -use temporal_sdk_core::{ - CoreRuntime, - api::{ - Worker as CoreWorkerTrait, - errors::{CompleteActivityError, CompleteNexusError, CompleteWfError, PollError}, - }, - init_replay_worker, init_worker, - protos::{ - coresdk::{ - ActivityHeartbeat, ActivityTaskCompletion, nexus::NexusTaskCompletion, - workflow_completion::WorkflowActivationCompletion, - }, - temporal::api::history::v1::History, +use temporalio_common::Worker as CoreWorkerTrait; +use temporalio_common::errors::{ + CompleteActivityError, CompleteNexusError, CompleteWfError, PollError, +}; +use temporalio_common::protos::{ + coresdk::{ + ActivityHeartbeat, ActivityTaskCompletion, nexus::NexusTaskCompletion, + workflow_completion::WorkflowActivationCompletion, }, + temporal::api::history::v1::History, +}; +use temporalio_sdk_core::{ + CoreRuntime, init_replay_worker, init_worker, replay::{HistoryForReplay, ReplayWorkerInput}, }; +use tracing::warn; use bridge_macros::js_function; @@ -70,7 +70,7 @@ pub struct Worker { core_runtime: Arc, // Arc so that we can send reference into async closures - core_worker: Arc, + core_worker: Arc, } /// Create a new worker. @@ -103,12 +103,15 @@ pub fn worker_validate(worker: OpaqueInboundHandle) -> BridgeResult) -> BridgeResult, ) -> BridgeResult>> { + warn!("borrowing worker"); let worker_ref = worker.borrow()?; + warn!("worker borrowed"); let worker = worker_ref.core_worker.clone(); let runtime = worker_ref.core_runtime.clone(); - runtime.future_to_promise(async move { - let result = worker.poll_workflow_activation().await; + warn!("poll_workflow_activation started"); - match result { - Ok(task) => Ok(task.encode_to_vec()), - Err(err) => match err { - PollError::ShutDown => Err(BridgeError::WorkerShutdown)?, - PollError::TonicError(status) => { - Err(BridgeError::TransportError(status.message().to_string()))? - } - }, - } - }) + runtime.future_to_promise_named( + async move { + warn!("calling poll_workflow_activation"); + let result = worker.poll_workflow_activation().await; + warn!("poll_workflow_activation result: {:?}", result); + + match result { + Ok(task) => Ok(task.encode_to_vec()), + Err(err) => match err { + PollError::ShutDown => Err(BridgeError::WorkerShutdown)?, + PollError::TonicError(status) => { + Err(BridgeError::TransportError(status.message().to_string()))? + } + }, + } + }, + "worker_poll_workflow_activation", + ) } /// Submit a workflow activation completion to core. @@ -154,21 +166,24 @@ pub fn worker_complete_workflow_activation( let worker = worker_ref.core_worker.clone(); let runtime = worker_ref.core_runtime.clone(); - runtime.future_to_promise(async move { - worker - .complete_workflow_activation(workflow_completion) - .await - .map_err(|err| match err { - CompleteWfError::MalformedWorkflowCompletion { reason, run_id } => { - BridgeError::TypeError { - field: None, - message: format!( - "Malformed Workflow Completion: {reason:?} for RunID={run_id}" - ), + runtime.future_to_promise_named( + async move { + worker + .complete_workflow_activation(workflow_completion) + .await + .map_err(|err| match err { + CompleteWfError::MalformedWorkflowCompletion { reason, run_id } => { + BridgeError::TypeError { + field: None, + message: format!( + "Malformed Workflow Completion: {reason:?} for RunID={run_id}" + ), + } } - } - }) - }) + }) + }, + "worker_complete_workflow_activation", + ) } /// Initiate a single activity task poll request. @@ -181,19 +196,22 @@ pub fn worker_poll_activity_task( let worker = worker_ref.core_worker.clone(); let runtime = worker_ref.core_runtime.clone(); - runtime.future_to_promise(async move { - let result = worker.poll_activity_task().await; + runtime.future_to_promise_named( + async move { + let result = worker.poll_activity_task().await; - match result { - Ok(task) => Ok(task.encode_to_vec()), - Err(err) => match err { - PollError::ShutDown => Err(BridgeError::WorkerShutdown)?, - PollError::TonicError(status) => { - Err(BridgeError::TransportError(status.message().to_string()))? - } - }, - } - }) + match result { + Ok(task) => Ok(task.encode_to_vec()), + Err(err) => match err { + PollError::ShutDown => Err(BridgeError::WorkerShutdown)?, + PollError::TonicError(status) => { + Err(BridgeError::TransportError(status.message().to_string()))? + } + }, + } + }, + "worker_poll_activity_task", + ) } /// Submit an activity task completion to core. @@ -214,20 +232,23 @@ pub fn worker_complete_activity_task( let worker = worker_ref.core_worker.clone(); let runtime = worker_ref.core_runtime.clone(); - runtime.future_to_promise(async move { - worker - .complete_activity_task(activity_completion) - .await - .map_err(|err| match err { - CompleteActivityError::MalformedActivityCompletion { - reason, - completion: _, - } => BridgeError::TypeError { - field: None, - message: format!("Malformed Activity Completion: {reason:?}"), - }, - }) - }) + runtime.future_to_promise_named( + async move { + worker + .complete_activity_task(activity_completion) + .await + .map_err(|err| match err { + CompleteActivityError::MalformedActivityCompletion { + reason, + completion: _, + } => BridgeError::TypeError { + field: None, + message: format!("Malformed Activity Completion: {reason:?}"), + }, + }) + }, + "worker_complete_activity_task", + ) } /// Submit an activity heartbeat to core. @@ -260,19 +281,22 @@ pub fn worker_poll_nexus_task( let worker = worker_ref.core_worker.clone(); let runtime = worker_ref.core_runtime.clone(); - runtime.future_to_promise(async move { - let result = worker.poll_nexus_task().await; + runtime.future_to_promise_named( + async move { + let result = worker.poll_nexus_task().await; - match result { - Ok(task) => Ok(task.encode_to_vec()), - Err(err) => match err { - PollError::ShutDown => Err(BridgeError::WorkerShutdown)?, - PollError::TonicError(status) => { - Err(BridgeError::TransportError(status.message().to_string()))? - } - }, - } - }) + match result { + Ok(task) => Ok(task.encode_to_vec()), + Err(err) => match err { + PollError::ShutDown => Err(BridgeError::WorkerShutdown)?, + PollError::TonicError(status) => { + Err(BridgeError::TransportError(status.message().to_string()))? + } + }, + } + }, + "worker_poll_nexus_task", + ) } /// Submit an nexus task completion to core. @@ -291,20 +315,25 @@ pub fn worker_complete_nexus_task( let worker = worker_ref.core_worker.clone(); let runtime = worker_ref.core_runtime.clone(); - runtime.future_to_promise(async move { - worker - .complete_nexus_task(nexus_completion) - .await - .map_err(|err| match err { - CompleteNexusError::NexusNotEnabled => { - BridgeError::UnexpectedError(format!("{err}")) - } - CompleteNexusError::MalformedNexusCompletion { reason } => BridgeError::TypeError { - field: None, - message: format!("Malformed nexus Completion: {reason:?}"), - }, - }) - }) + runtime.future_to_promise_named( + async move { + worker + .complete_nexus_task(nexus_completion) + .await + .map_err(|err| match err { + CompleteNexusError::NexusNotEnabled {} => { + BridgeError::UnexpectedError(format!("{err}")) + } + CompleteNexusError::MalformedNexusCompletion { reason } => { + BridgeError::TypeError { + field: None, + message: format!("Malformed nexus Completion: {reason:?}"), + } + } + }) + }, + "worker_complete_nexus_task", + ) } /// Request shutdown of the worker. @@ -313,6 +342,7 @@ pub fn worker_complete_nexus_task( /// the loop to ensure graceful shutdown. #[js_function] pub fn worker_initiate_shutdown(worker: OpaqueInboundHandle) -> BridgeResult<()> { + tracing::info!("Typescript initiate worker shutdown"); let worker_ref = worker.borrow()?; worker_ref.core_worker.initiate_shutdown(); Ok(()) @@ -337,10 +367,13 @@ pub fn worker_finalize_shutdown( } })?; - worker_ref.core_runtime.future_to_promise(async move { - worker.finalize_shutdown().await; - Ok(()) - }) + worker_ref.core_runtime.future_to_promise_named( + async move { + worker.finalize_shutdown().await; + Ok(()) + }, + "worker_finalize_shutdown", + ) } impl MutableFinalize for Worker { @@ -466,16 +499,16 @@ impl MutableFinalize for HistoryForReplayTunnelHandle {} mod config { use std::{sync::Arc, time::Duration}; - use temporal_sdk_core::{ + use temporalio_common::protos::temporal::api::enums::v1::VersioningBehavior as CoreVersioningBehavior; + use temporalio_common::worker::{ + ActivitySlotKind, LocalActivitySlotKind, NexusSlotKind, + PollerBehavior as CorePollerBehavior, SlotKind, WorkerConfig, WorkerConfigBuilder, + WorkerConfigBuilderError, WorkerDeploymentOptions as CoreWorkerDeploymentOptions, + WorkerDeploymentVersion as CoreWorkerDeploymentVersion, WorkflowSlotKind, + }; + use temporalio_sdk_core::{ ResourceBasedSlotsOptions, ResourceBasedSlotsOptionsBuilder, ResourceSlotOptions, SlotSupplierOptions as CoreSlotSupplierOptions, TunerHolder, TunerHolderOptionsBuilder, - api::worker::{ - ActivitySlotKind, LocalActivitySlotKind, NexusSlotKind, - PollerBehavior as CorePollerBehavior, SlotKind, WorkerConfig, WorkerConfigBuilder, - WorkerConfigBuilderError, WorkerDeploymentOptions as CoreWorkerDeploymentOptions, - WorkerDeploymentVersion as CoreWorkerDeploymentVersion, WorkflowSlotKind, - }, - protos::temporal::api::enums::v1::VersioningBehavior as CoreVersioningBehavior, }; use super::custom_slot_supplier::CustomSlotSupplierOptions; @@ -485,7 +518,7 @@ mod config { use neon::object::Object; use neon::prelude::JsResult; use neon::types::JsObject; - use temporal_sdk_core::api::worker::WorkerVersioningStrategy; + use temporalio_common::worker::WorkerVersioningStrategy; #[derive(TryFromJs)] pub struct BridgeWorkerOptions { @@ -575,6 +608,7 @@ mod config { .max_task_queue_activities_per_second(self.max_task_queue_activities_per_second) .max_worker_activities_per_second(self.max_activities_per_second) .graceful_shutdown_period(self.shutdown_grace_time) + .skip_client_worker_set_check(true) .build() } } @@ -749,16 +783,14 @@ mod custom_slot_supplier { use neon::{context::Context, handle::Handle, prelude::*}; - use temporal_sdk_core::{ - SlotSupplierOptions as CoreSlotSupplierOptions, - api::worker::{ - SlotInfo as CoreSlotInfo, SlotInfoTrait as _, SlotKind, - SlotKindType as CoreSlotKindType, SlotMarkUsedContext as CoreSlotMarkUsedContext, - SlotReleaseContext as CoreSlotReleaseContext, - SlotReservationContext as CoreSlotReservationContext, SlotSupplier as CoreSlotSupplier, - SlotSupplierPermit as CoreSlotSupplierPermit, - }, + use temporalio_common::worker::{ + SlotInfo as CoreSlotInfo, SlotInfoTrait as _, SlotKind, SlotKindType as CoreSlotKindType, + SlotMarkUsedContext as CoreSlotMarkUsedContext, + SlotReleaseContext as CoreSlotReleaseContext, + SlotReservationContext as CoreSlotReservationContext, SlotSupplier as CoreSlotSupplier, + SlotSupplierPermit as CoreSlotSupplierPermit, }; + use temporalio_sdk_core::SlotSupplierOptions as CoreSlotSupplierOptions; use bridge_macros::{TryFromJs, TryIntoJs}; use tracing::warn; diff --git a/packages/proto/scripts/compile-proto.js b/packages/proto/scripts/compile-proto.js index 5c11a70ae..ea86ef355 100644 --- a/packages/proto/scripts/compile-proto.js +++ b/packages/proto/scripts/compile-proto.js @@ -10,7 +10,7 @@ const outputDir = resolve(__dirname, '../protos'); const jsOutputFile = resolve(outputDir, 'json-module.js'); const tempFile = resolve(outputDir, 'temp.js'); -const protoBaseDir = resolve(__dirname, '../../core-bridge/sdk-core/sdk-core-protos/protos'); +const protoBaseDir = resolve(__dirname, '../../core-bridge/sdk-core/crates/common/protos'); function mtime(path) { try { diff --git a/packages/test/package.json b/packages/test/package.json index 023d01308..200b9e300 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -7,7 +7,7 @@ "build": "npm-run-all build:protos build:ts", "build:ts": "tsc --build", "build:protos": "node ./scripts/compile-proto.js", - "test": "ava ./lib/test-*.js", + "test": "ava ./lib/test-runtime.js", "test.watch": "ava --watch ./lib/test-*.js" }, "ava": { diff --git a/packages/test/src/helpers-integration.ts b/packages/test/src/helpers-integration.ts index 9d0d6fab0..7374be345 100644 --- a/packages/test/src/helpers-integration.ts +++ b/packages/test/src/helpers-integration.ts @@ -17,6 +17,8 @@ import { DefaultLogger, LogEntry, LogLevel, + NativeConnection, + NativeConnectionOptions, ReplayWorkerOptions, Runtime, RuntimeOptions, @@ -47,6 +49,8 @@ const defaultDynamicConfigOptions = [ 'system.forceSearchAttributesCacheRefreshOnRead=true', 'worker.buildIdScavengerEnabled=true', 'worker.removableBuildIdDurationSinceDefault=1', + 'frontend.WorkerHeartbeatsEnabled=true', + 'frontend.ListWorkersEnabled=true', ]; function setupRuntime(recordedLogs?: { [workflowId: string]: LogEntry[] }, runtimeOpts?: Partial) { @@ -184,6 +188,7 @@ export async function createTestWorkflowEnvironment( export interface Helpers { taskQueue: string; createWorker(opts?: Partial): Promise; + createNativeConnection(opts?: Partial): Promise; runReplayHistory(opts: Partial, history: temporal.api.history.v1.IHistory): Promise; executeWorkflow Promise>(workflowType: T): Promise>; executeWorkflow( @@ -218,6 +223,9 @@ export function configurableHelpers( ...opts, }); }, + async createNativeConnection(opts?: Partial): Promise { + return await NativeConnection.connect({ address: testEnv.address, ...opts }); + }, async runReplayHistory( opts: Partial, history: temporal.api.history.v1.IHistory diff --git a/packages/test/src/run-a-worker.ts b/packages/test/src/run-a-worker.ts deleted file mode 100644 index b0afc6fc8..000000000 --- a/packages/test/src/run-a-worker.ts +++ /dev/null @@ -1,50 +0,0 @@ -import arg from 'arg'; -import * as nexus from 'nexus-rpc'; -import { Worker, Runtime, DefaultLogger, LogLevel, makeTelemetryFilterString } from '@temporalio/worker'; -import * as activities from './activities'; - -async function main() { - const argv = arg({ - '--log-level': String, - }); - if (argv['--log-level']) { - const logLevel = argv['--log-level'].toUpperCase(); - Runtime.install({ - logger: new DefaultLogger(logLevel as LogLevel), - telemetryOptions: { - logging: { - filter: makeTelemetryFilterString({ core: logLevel as LogLevel, other: logLevel as LogLevel }), - forward: {}, - }, - }, - }); - } - const worker = await Worker.create({ - activities, - workflowsPath: require.resolve('./workflows'), - nexusServices: [ - nexus.serviceHandler( - nexus.service('foo', { - bar: nexus.operation(), - }), - { - async bar(_ctx, input) { - return input; - }, - } - ), - ], - taskQueue: 'test', - nonStickyToStickyPollRatio: 0.5, - }); - await worker.run(); - console.log('Worker gracefully shutdown'); -} - -main().then( - () => void process.exit(0), - (err) => { - console.error(err); - process.exit(1); - } -); diff --git a/packages/test/src/run-a-workflow.ts b/packages/test/src/run-a-workflow.ts deleted file mode 100644 index 16de86516..000000000 --- a/packages/test/src/run-a-workflow.ts +++ /dev/null @@ -1,36 +0,0 @@ -import arg from 'arg'; -import { NativeConnection } from '@temporalio/worker'; -import { Connection, WorkflowClient } from '@temporalio/client'; -import * as workflows from './workflows'; - -async function main() { - const argv = arg({ - '--workflow-id': String, - '--use-native': Boolean, - }); - const [workflowType, ...argsRaw] = argv._; - const args = argsRaw.map((v) => JSON.parse(v)); - const workflowId = argv['--workflow-id'] ?? 'test'; - const useNative = !!argv['--use-native']; - if (!Object.prototype.hasOwnProperty.call(workflows, workflowType)) { - throw new TypeError(`Invalid workflowType ${workflowType}`); - } - console.log('running', { workflowType, args }); - - const connection = useNative ? await NativeConnection.connect() : await Connection.connect(); - const client = new WorkflowClient({ connection }); - const result = await client.execute(workflowType, { - workflowId, - taskQueue: 'test', - args, - }); - console.log('complete', { result }); -} - -main().then( - () => void process.exit(0), - (err) => { - console.error(err); - process.exit(1); - } -); diff --git a/packages/test/src/run-activation-perf-tests.ts b/packages/test/src/run-activation-perf-tests.ts deleted file mode 100644 index d65608721..000000000 --- a/packages/test/src/run-activation-perf-tests.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { randomUUID } from 'crypto'; -import Long from 'long'; -import { msToTs } from '@temporalio/common/lib/time'; -import { coresdk } from '@temporalio/proto'; -import { ReusableVMWorkflowCreator } from '@temporalio/worker/lib/workflow/reusable-vm'; -import { WorkflowCodeBundler } from '@temporalio/worker/lib/workflow/bundler'; -import { parseWorkflowCode } from '@temporalio/worker/lib/worker'; -import { VMWorkflow, VMWorkflowCreator } from '@temporalio/worker/lib/workflow/vm'; -import * as wf from '@temporalio/workflow'; -import { TypedSearchAttributes } from '@temporalio/common'; - -// WARNING: This file is a quick and dirty utility to run Workflow Activation performance testing -// localy. It is not part of our regular test suite and hasn't been reviewed. - -function isSet(env: string | undefined, def: boolean): boolean { - if (env === undefined) return def; - env = env.toLocaleLowerCase(); - return env === '1' || env === 't' || env === 'true'; -} - -export const REUSE_V8_CONTEXT = wf.inWorkflowContext() || isSet(process.env.REUSE_V8_CONTEXT, true); - -export const bundlerOptions = { - // This is a bit ugly but it does the trick, when a test that includes workflow - // code tries to import a forbidden workflow module, add it to this list: - ignoreModules: [ - '@temporalio/common/lib/internal-non-workflow', - '@temporalio/activity', - '@temporalio/client', - '@temporalio/testing', - '@temporalio/worker', - '@temporalio/proto', - 'inspector', - 'ava', - 'crypto', - 'timers/promises', - 'fs', - 'module', - 'path', - 'perf_hooks', - 'stack-utils', - '@grpc/grpc-js', - 'async-retry', - 'uuid', - 'net', - 'fs/promises', - '@temporalio/worker/lib/workflow/bundler', - require.resolve('./activities'), - ], -}; - -export interface Context { - workflowCreator: VMWorkflowCreator | ReusableVMWorkflowCreator; -} - -if (!wf.inWorkflowContext()) { - // eslint-disable-next-line no-inner-declarations - async function runPerfTest() { - const bundler = new WorkflowCodeBundler({ - workflowsPath: __filename, - ignoreModules: [...bundlerOptions.ignoreModules], - }); - - const workflowBundle = parseWorkflowCode((await bundler.createBundle()).code); - - const workflowCreator = REUSE_V8_CONTEXT - ? await ReusableVMWorkflowCreator.create(workflowBundle, 400, new Set()) - : await VMWorkflowCreator.create(workflowBundle, 400, new Set()); - - async function createWorkflow(workflowType: wf.Workflow): Promise<{ workflow: VMWorkflow; info: wf.WorkflowInfo }> { - const startTime = Date.now(); - const runId = randomUUID(); // That one is using a strong entropy; could this slow doen our tests? - - const info: wf.WorkflowInfo = { - workflowType: workflowType.name, - runId, - workflowId: 'test-workflowId', - namespace: 'default', - firstExecutionRunId: runId, - attempt: 1, - taskTimeoutMs: 1000, - taskQueue: 'test', - searchAttributes: {}, - typedSearchAttributes: new TypedSearchAttributes(), - historyLength: 3, - historySize: 300, - continueAsNewSuggested: false, - unsafe: { isReplaying: false, now: Date.now }, - startTime: new Date(), - runStartTime: new Date(), - }; - - const workflow = (await workflowCreator.createWorkflow({ - info, - randomnessSeed: Long.fromInt(1337).toBytes(), - now: startTime, - showStackTraceSources: true, - })) as VMWorkflow; - - return { workflow, info }; - } - - async function activate(workflow: VMWorkflow, activation: coresdk.workflow_activation.IWorkflowActivation) { - // Core guarantees the following jobs ordering: - // initWf -> patches -> update random seed -> signals+update -> others -> Resolve LA - // reference: github.com/temporalio/sdk-core/blob/a8150d5c7c3fc1bfd5a941fd315abff1556cd9dc/core/src/worker/workflow/mod.rs#L1363-L1378 - // Tests are likely to fail if we artifically make an activation that does not follow that order - const jobs: coresdk.workflow_activation.IWorkflowActivationJob[] = activation.jobs ?? []; - function getPriority(job: coresdk.workflow_activation.IWorkflowActivationJob) { - if (job.initializeWorkflow) return 0; - if (job.notifyHasPatch) return 1; - if (job.updateRandomSeed) return 2; - if (job.signalWorkflow || job.doUpdate) return 3; - if (job.resolveActivity && job.resolveActivity.isLocal) return 5; - return 4; - } - jobs.reduce((prevPriority: number, currJob) => { - const currPriority = getPriority(currJob); - if (prevPriority > currPriority) { - throw new Error('Jobs are not correctly sorted'); - } - return currPriority; - }, 0); - - const completion = await workflow.activate(coresdk.workflow_activation.WorkflowActivation.fromObject(activation)); - const sinkCalls = await workflow.getAndResetSinkCalls(); - - return { completion, sinkCalls }; - } - - function makeActivation( - info: wf.WorkflowInfo, - timestamp: number = Date.now(), - ...jobs: coresdk.workflow_activation.IWorkflowActivationJob[] - ): coresdk.workflow_activation.IWorkflowActivation { - return { - runId: info.runId, - timestamp: msToTs(timestamp), - jobs, - }; - } - function makeStartWorkflow(info: wf.WorkflowInfo): coresdk.workflow_activation.IWorkflowActivation { - const timestamp = Date.now(); - return makeActivation(info, timestamp, makeInitializeWorkflowJob(info)); - } - - function makeInitializeWorkflowJob(info: wf.WorkflowInfo): { - initializeWorkflow: coresdk.workflow_activation.IInitializeWorkflow; - } { - return { - initializeWorkflow: { workflowId: info.workflowId, workflowType: info.workflowType, arguments: [] }, - }; - } - - function makeFireTimer( - info: wf.WorkflowInfo, - seq: number, - timestamp: number = Date.now() - ): coresdk.workflow_activation.IWorkflowActivation { - return makeActivation(info, timestamp, makeFireTimerJob(seq)); - } - - function makeFireTimerJob(seq: number): coresdk.workflow_activation.IWorkflowActivationJob { - return { - fireTimer: { seq }, - }; - } - - const workflows = []; - for (let i = 0; i < 5; i++) { - const { workflow, info } = await createWorkflow(xxxWorkflow); - let lastCompletion = await activate(workflow, makeStartWorkflow(info)); - - // eslint-disable-next-line no-inner-declarations - function getTimerSeq(): number { - const startTimerCommand = lastCompletion.completion.successful?.commands?.filter((c) => c.startTimer)[0]; - return startTimerCommand?.startTimer?.seq || 0; - } - - // eslint-disable-next-line no-inner-declarations - async function doActivate() { - lastCompletion = await activate(workflow, makeFireTimer(info, getTimerSeq())); - } - - workflows.push({ doActivate }); - } - - const startTime = Date.now(); - for (let i = 1; i <= 50_000; i++) { - await workflows[Math.floor(Math.random() * workflows.length)].doActivate(); - if (i % 10_000 === 0) { - console.log(` ${i}: ${Math.round(((Date.now() - startTime) / i) * 1000)}us per activation`); - } - } - } - - runPerfTest() - .catch((err) => { - console.error(err); - }) - .finally(() => {}); -} - -export async function xxxWorkflow(): Promise { - // We don't care about history size, as this workflow is only to be used with synthetic activations - for (;;) { - await wf.sleep(1); - } -} diff --git a/packages/test/src/test-activity-log-interceptor.ts b/packages/test/src/test-activity-log-interceptor.ts deleted file mode 100644 index 75ccc0154..000000000 --- a/packages/test/src/test-activity-log-interceptor.ts +++ /dev/null @@ -1,193 +0,0 @@ -import test from 'ava'; -import { ActivityInboundLogInterceptor, DefaultLogger, LogEntry, Runtime } from '@temporalio/worker'; -import { activityLogAttributes } from '@temporalio/worker/lib/activity'; -import { MockActivityEnvironment, defaultActivityInfo } from '@temporalio/testing'; -import { isCancellation } from '@temporalio/workflow'; -import { isAbortError } from '@temporalio/common/lib/type-helpers'; -import * as activity from '@temporalio/activity'; -import { SdkComponent } from '@temporalio/common'; -import { withZeroesHTTPServer } from './zeroes-http-server'; -import { cancellableFetch } from './activities'; - -interface MyTestActivityContext extends activity.Context { - logs: Array; -} - -const mockLogger = new DefaultLogger('DEBUG', (entry) => { - try { - (activity.Context.current() as MyTestActivityContext).logs ??= []; - (activity.Context.current() as MyTestActivityContext).logs.push(entry); - } catch (e) { - // Ignore messages produced from non activity context - if ((e as Error).message !== 'Activity context not initialized') throw e; - } -}); -Runtime.install({ - logger: mockLogger, -}); - -test('Activity Context logger funnel through the parent Logger', async (t) => { - const env = new MockActivityEnvironment({}, { logger: mockLogger }); - await env.run(async () => { - activity.log.debug('log message from activity'); - }); - const logs = (env.context as MyTestActivityContext).logs; - const entry = logs.find((x) => x.level === 'DEBUG' && x.message === 'log message from activity'); - t.not(entry, undefined); - t.deepEqual(entry?.meta, { ...activityLogAttributes(defaultActivityInfo), sdkComponent: SdkComponent.activity }); -}); - -test('Activity Worker logs when activity starts', async (t) => { - const env = new MockActivityEnvironment({}, { logger: mockLogger }); - await env.run(async () => { - activity.log.debug('log message from activity'); - }); - const logs = (env.context as MyTestActivityContext).logs; - const entry = logs.find((x) => x.level === 'DEBUG' && x.message === 'Activity started'); - t.not(entry, undefined); - t.deepEqual(entry?.meta, { ...activityLogAttributes(defaultActivityInfo), sdkComponent: SdkComponent.worker }); -}); - -test('Activity Worker logs warning when activity fails', async (t) => { - const err = new Error('Failed for test'); - const env = new MockActivityEnvironment({}, { logger: mockLogger }); - try { - await env.run(async () => { - throw err; - }); - } catch (e) { - if (e !== err) throw e; - } - const logs = (env.context as MyTestActivityContext).logs; - console.log(logs); - const entry = logs.find((entry) => entry.level === 'WARN' && entry.message === 'Activity failed'); - t.not(entry, undefined); - const { durationMs, error, ...rest } = entry?.meta ?? {}; - t.true(Number.isInteger(durationMs)); - t.is(err, error); - t.deepEqual(rest, { ...activityLogAttributes(defaultActivityInfo), sdkComponent: SdkComponent.worker }); -}); - -test('Activity Worker logs when activity completes async', async (t) => { - const env = new MockActivityEnvironment({}, { logger: mockLogger }); - try { - await env.run(async () => { - throw new activity.CompleteAsyncError(); - }); - } catch (e) { - if (!(e instanceof activity.CompleteAsyncError)) throw e; - } - const logs = (env.context as MyTestActivityContext).logs; - const entry = logs.find((x) => x.level === 'DEBUG' && x.message === 'Activity will complete asynchronously'); - t.not(entry, undefined); - const { durationMs, ...rest } = entry?.meta ?? {}; - t.true(Number.isInteger(durationMs)); - t.deepEqual(rest, { ...activityLogAttributes(defaultActivityInfo), sdkComponent: SdkComponent.worker }); -}); - -test('Activity Worker logs when activity is cancelled with promise', async (t) => { - const env = new MockActivityEnvironment({}, { logger: mockLogger }); - env.on('heartbeat', () => env.cancel()); - try { - await env.run(async () => { - activity.Context.current().heartbeat(); - await activity.Context.current().cancelled; - }); - } catch (e) { - if (!isCancellation(e)) throw e; - } - const logs = (env.context as MyTestActivityContext).logs; - const entry = logs.find((x) => x.level === 'DEBUG' && x.message === 'Activity completed as cancelled'); - t.not(entry, undefined); - const { durationMs, ...rest } = entry?.meta ?? {}; - t.true(Number.isInteger(durationMs)); - t.deepEqual(rest, { ...activityLogAttributes(defaultActivityInfo), sdkComponent: SdkComponent.worker }); -}); - -test('Activity Worker logs when activity is cancelled with signal', async (t) => { - const env = new MockActivityEnvironment({}, { logger: mockLogger }); - env.on('heartbeat', () => env.cancel()); - try { - await env.run(async () => { - await withZeroesHTTPServer(async (port) => { - await cancellableFetch(`http:127.0.0.1:${port}`); - }); - }); - } catch (e) { - if (!isAbortError(e)) throw e; - } - const logs = (env.context as MyTestActivityContext).logs; - const entry = logs.find((x) => x.level === 'DEBUG' && x.message === 'Activity completed as cancelled'); - t.not(entry, undefined); - const { durationMs, ...rest } = entry?.meta ?? {}; - t.true(Number.isInteger(durationMs)); - t.deepEqual(rest, { ...activityLogAttributes(defaultActivityInfo), sdkComponent: SdkComponent.worker }); -}); - -test('(Legacy) ActivityInboundLogInterceptor does not override Context.log by default', async (t) => { - const env = new MockActivityEnvironment( - {}, - { - // eslint-disable-next-line deprecation/deprecation - interceptors: [(ctx) => ({ inbound: new ActivityInboundLogInterceptor(ctx) })], - logger: mockLogger, - } - ); - await env.run(async () => { - activity.log.debug('log message from activity'); - }); - const logs = (env.context as MyTestActivityContext).logs; - const activityLogEntry = logs.find((entry) => entry.message === 'log message from activity'); - t.not(activityLogEntry, undefined); - t.is(activityLogEntry?.level, 'DEBUG'); -}); - -test('(Legacy) ActivityInboundLogInterceptor overrides Context.log if a logger is specified', async (t) => { - const logs: LogEntry[] = []; - const logger = new DefaultLogger('DEBUG', (entry) => { - logs.push(entry); - }); - const env = new MockActivityEnvironment( - {}, - { - // eslint-disable-next-line deprecation/deprecation - interceptors: [(ctx) => ({ inbound: new ActivityInboundLogInterceptor(ctx, logger) })], - logger: mockLogger, - } - ); - await env.run(async () => { - activity.log.debug('log message from activity'); - }); - const entry = logs.find((x) => x.level === 'DEBUG' && x.message === 'log message from activity'); - t.not(entry, undefined); -}); - -test('(Legacy) ActivityInboundLogInterceptor overrides Context.log if class is extended', async (t) => { - // eslint-disable-next-line deprecation/deprecation - class CustomActivityInboundLogInterceptor extends ActivityInboundLogInterceptor { - protected logAttributes(): Record { - const { namespace: _, ...rest } = super.logAttributes(); - return { - ...rest, - custom: 'attribute', - }; - } - } - const env = new MockActivityEnvironment( - {}, - { - interceptors: [(ctx) => ({ inbound: new CustomActivityInboundLogInterceptor(ctx) })], - logger: mockLogger, - } - ); - await env.run(async () => { - activity.log.debug('log message from activity'); - }); - const logs = (env.context as MyTestActivityContext).logs; - const activityLogEntry = logs.find((entry) => entry.message === 'log message from activity'); - t.not(activityLogEntry, undefined); - t.is(activityLogEntry?.level, 'DEBUG'); - t.is(activityLogEntry?.meta?.taskQueue, env.context.info.taskQueue); - t.is(activityLogEntry?.meta?.custom, 'attribute'); - t.false('namespace' in (activityLogEntry?.meta ?? {})); -}); diff --git a/packages/test/src/test-async-completion.ts b/packages/test/src/test-async-completion.ts deleted file mode 100644 index 38ff2f7d8..000000000 --- a/packages/test/src/test-async-completion.ts +++ /dev/null @@ -1,286 +0,0 @@ -import anyTest, { TestFn, ExecutionContext } from 'ava'; -import { Observable, Subject, firstValueFrom } from 'rxjs'; -import { filter } from 'rxjs/operators'; -import { v4 as uuid4 } from 'uuid'; -import { - ActivityCancelledError, - Client, - ActivityNotFoundError, - WorkflowFailedError, - Connection, -} from '@temporalio/client'; -import { Info } from '@temporalio/activity'; -import { rootCause } from '@temporalio/common'; -import { isCancellation } from '@temporalio/workflow'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { runAnAsyncActivity } from './workflows'; -import { createActivities } from './activities/async-completer'; - -export interface Context { - worker: Worker; - client: Client; - activityStarted$: Observable; - runPromise: Promise; - notFoundTaskToken: Uint8Array; -} - -// This is a valid server generated token. Since the token contains the id of the namespace from -// which it was generated, trying to use it as-is will produce a namespace not found error. To avoid -// this, use `makeNotFoundTaskToken` to replace the namespace Id by the one of the current namespace. -// Protobuf reference: https://github.com/temporalio/temporal/blob/7314ea42e92f09e42d4edef76cbb5cee00b6c74c/proto/internal/temporal/server/api/token/v1/message.proto#L54 -const NOT_FOUND_TASK_TOKEN = new Uint8Array([ - // 1. Namespace Id: string (len 36, UUID format) - 10, 36, 52, 101, 98, 48, 102, 49, 56, 52, 45, 101, 48, 51, 49, 45, 52, 52, 102, 97, 45, 56, 98, 48, 99, 45, 102, 48, - 50, 55, 100, 48, 53, 53, 98, 101, 100, 99, - // 2. Workflow Id: string (len 36, UUID format) - 18, 36, 99, 55, 56, 55, 102, 55, 97, 102, 45, 50, 51, 99, 52, 45, 52, 56, 54, 98, 45, 57, 56, 98, 50, 45, 102, 53, 55, - 57, 57, 55, 97, 100, 48, 97, 101, 54, - // 3. Run Id: string (len 36, UUID format) - 26, 36, 51, 51, 102, 101, 49, 50, 99, 56, 45, 98, 101, 52, 57, 45, 52, 49, 97, 50, 45, 56, 97, 98, 100, 45, 49, 55, - 54, 53, 52, 57, 54, 101, 100, 101, 57, 101, - // 4. Scheduled Event Id: int64 - 32, 5, - // 5. Attempt: int32 - 40, 1, - // 6. Activity Id: string (len 1) - 50, 1, 49, - // 8. Activity Type: string (len 13) - 66, 13, 99, 111, 109, 112, 108, 101, 116, 101, 65, 115, 121, 110, 99, -]); - -// -async function makeNotFoundTaskToken(conn: Connection, namespace: string): Promise { - const { namespaceInfo } = await conn.workflowService.describeNamespace({ namespace }); - const buf = Buffer.alloc(NOT_FOUND_TASK_TOKEN.length); - buf.set(NOT_FOUND_TASK_TOKEN); - buf.subarray(2, 38).set(Buffer.from(namespaceInfo?.id as string)); - return new Uint8Array(buf); -} - -const taskQueue = 'async-activity-completion'; -const test = anyTest as TestFn; - -async function activityStarted(t: ExecutionContext, workflowId: string): Promise { - return await firstValueFrom( - t.context.activityStarted$.pipe(filter((info) => info.workflowExecution.workflowId === workflowId)) - ); -} - -if (RUN_INTEGRATION_TESTS) { - test.before(async (t) => { - const infoSubject = new Subject(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const worker = await Worker.create({ - workflowsPath: require.resolve('./workflows'), - activities: createActivities(infoSubject), - taskQueue, - }); - const runPromise = worker.run(); - // Catch the error here to avoid unhandled rejection - runPromise.catch((err) => { - console.error('Caught error while worker was running', err); - }); - const connection = await Connection.connect(); - t.context = { - worker, - runPromise, - activityStarted$: infoSubject, - client: new Client({ connection }), - notFoundTaskToken: await makeNotFoundTaskToken(connection, 'default'), - }; - }); - - test.after.always(async (t) => { - t.context.worker.shutdown(); - await t.context.runPromise; - }); - - test('Activity can complete asynchronously', async (t) => { - const { client } = t.context; - const workflowId = uuid4(); - const handle = await client.workflow.start(runAnAsyncActivity, { - workflowId, - taskQueue, - }); - - const info = await activityStarted(t, workflowId); - await client.activity.complete(info.taskToken, 'success'); - t.is(await handle.result(), 'success'); - }); - - test('Activity can complete asynchronously by ID', async (t) => { - const { client } = t.context; - const workflowId = uuid4(); - const handle = await client.workflow.start(runAnAsyncActivity, { - workflowId, - taskQueue, - }); - - const info = await activityStarted(t, workflowId); - await client.activity.complete({ workflowId, activityId: info.activityId }, 'success'); - t.is(await handle.result(), 'success'); - }); - - test('Non existing activity async completion throws meaningful message', async (t) => { - await t.throwsAsync(t.context.client.activity.complete(t.context.notFoundTaskToken, 'success'), { - instanceOf: ActivityNotFoundError, - }); - }); - - test('Non existing activity async completion by ID throws meaningful message', async (t) => { - await t.throwsAsync(t.context.client.activity.complete({ workflowId: uuid4(), activityId: '1' }, 'success'), { - instanceOf: ActivityNotFoundError, - }); - }); - - test('Activity can fail asynchronously', async (t) => { - const { client } = t.context; - const workflowId = uuid4(); - const handle = await client.workflow.start(runAnAsyncActivity, { - workflowId, - taskQueue, - }); - - const info = await activityStarted(t, workflowId); - await client.activity.fail(info.taskToken, new Error('failed')); - const err = (await t.throwsAsync(handle.result(), { - instanceOf: WorkflowFailedError, - })) as WorkflowFailedError; - t.is(rootCause(err.cause), 'failed'); - }); - - test('Activity can fail asynchronously by ID', async (t) => { - const { client } = t.context; - const workflowId = uuid4(); - const handle = await client.workflow.start(runAnAsyncActivity, { - workflowId, - taskQueue, - }); - - const info = await activityStarted(t, workflowId); - await client.activity.fail({ workflowId, activityId: info.activityId }, new Error('failed')); - const err = (await t.throwsAsync(handle.result(), { - instanceOf: WorkflowFailedError, - })) as WorkflowFailedError; - t.is(rootCause(err.cause), 'failed'); - }); - - test('Non existing activity async failure throws meaningful message', async (t) => { - await t.throwsAsync(t.context.client.activity.fail(t.context.notFoundTaskToken, new Error('failure')), { - instanceOf: ActivityNotFoundError, - }); - }); - - test('Non existing activity async failure by ID throws meaningful message', async (t) => { - await t.throwsAsync( - t.context.client.activity.fail( - { - workflowId: uuid4(), - activityId: '1', - }, - new Error('failure') - ), - { - instanceOf: ActivityNotFoundError, - } - ); - }); - - test('Non existing activity async cancellation throws meaningful message', async (t) => { - await t.throwsAsync(t.context.client.activity.reportCancellation(t.context.notFoundTaskToken, 'cancelled'), { - instanceOf: ActivityNotFoundError, - }); - }); - - test('Non existing activity async cancellation by ID throws meaningful message', async (t) => { - await t.throwsAsync( - t.context.client.activity.reportCancellation( - { - workflowId: uuid4(), - activityId: '1', - }, - 'cancelled' - ), - { - instanceOf: ActivityNotFoundError, - } - ); - }); - - test('Activity can heartbeat and get cancelled with AsyncCompletionClient', async (t) => { - const { client } = t.context; - const workflowId = uuid4(); - const handle = await client.workflow.start(runAnAsyncActivity, { - workflowId, - taskQueue, - args: [true], - }); - - const info = await activityStarted(t, workflowId); - await t.throwsAsync( - async () => { - for (;;) { - await client.activity.heartbeat(info.taskToken, 'details'); - await new Promise((resolve) => setTimeout(resolve, 100)); - } - }, - { instanceOf: ActivityCancelledError } - ); - - await client.activity.reportCancellation(info.taskToken, 'cancelled'); - - const err = (await t.throwsAsync(handle.result(), { - instanceOf: WorkflowFailedError, - })) as WorkflowFailedError; - t.true(isCancellation(err.cause)); - }); - - test('Activity can heartbeat and get cancelled by ID with AsyncCompletionClient', async (t) => { - const { client } = t.context; - const workflowId = uuid4(); - const handle = await client.workflow.start(runAnAsyncActivity, { - workflowId, - taskQueue, - args: [true], - }); - - const info = await activityStarted(t, workflowId); - await t.throwsAsync( - async () => { - for (;;) { - await client.activity.heartbeat({ workflowId, activityId: info.activityId }, 'details'); - await new Promise((resolve) => setTimeout(resolve, 100)); - } - }, - { instanceOf: ActivityCancelledError } - ); - - await client.activity.reportCancellation({ workflowId, activityId: info.activityId }, 'cancelled'); - - const err = (await t.throwsAsync(handle.result(), { - instanceOf: WorkflowFailedError, - })) as WorkflowFailedError; - t.true(isCancellation(err.cause)); - }); - - test('Non existing activity async heartbeat throws meaningful message', async (t) => { - await t.throwsAsync(t.context.client.activity.heartbeat(t.context.notFoundTaskToken, 'details'), { - instanceOf: ActivityNotFoundError, - }); - }); - - test('Non existing activity async heartbeat by ID throws meaningful message', async (t) => { - await t.throwsAsync( - t.context.client.activity.heartbeat( - { - workflowId: uuid4(), - activityId: '1', - }, - 'details' - ), - { - instanceOf: ActivityNotFoundError, - } - ); - }); -} diff --git a/packages/test/src/test-bridge.ts b/packages/test/src/test-bridge.ts index df14e0183..1f5ce57fa 100644 --- a/packages/test/src/test-bridge.ts +++ b/packages/test/src/test-bridge.ts @@ -7,10 +7,9 @@ import { native, errors } from '@temporalio/core-bridge'; // // - Tests in this file requires an external Temporal server to be running, because using the ephemeral // server support provided by Core SDK would affect the behavior that we're testing here. -// - Tests in this file can't be run in parallel, since the bridge is mostly a singleton. // - Some of these tests explicitly use the native bridge, without going through the lang side Runtime/Worker. -test('Can instantiate and shutdown the native runtime', async (t) => { +test.serial('Can instantiate and shutdown the native runtime', async (t) => { const runtime = native.newRuntime(GenericConfigs.runtime.basic); t.is(typeof runtime, 'object'); native.runtimeShutdown(runtime); @@ -29,7 +28,7 @@ test('Can instantiate and shutdown the native runtime', async (t) => { }); }); -test('Can run multiple runtime concurrently', async (t) => { +test.serial('Can run multiple runtime concurrently', async (t) => { const runtime1 = native.newRuntime(GenericConfigs.runtime.basic); const runtime2 = native.newRuntime(GenericConfigs.runtime.basic); const runtime3 = native.newRuntime(GenericConfigs.runtime.basic); @@ -54,7 +53,7 @@ test('Can run multiple runtime concurrently', async (t) => { t.pass(); }); -test('Missing/invalid properties in config throws appropriately', async (t) => { +test.serial('Missing/invalid properties in config throws appropriately', async (t) => { // required string = undefined ==> missing property t.throws( () => @@ -123,7 +122,7 @@ test('Missing/invalid properties in config throws appropriately', async (t) => { ); }); -test(`get_time_of_day() returns a bigint`, async (t) => { +test.serial(`get_time_of_day() returns a bigint`, async (t) => { const time_1 = native.getTimeOfDay(); const time_2 = native.getTimeOfDay(); await setTimeout(100); @@ -136,18 +135,18 @@ test(`get_time_of_day() returns a bigint`, async (t) => { t.true(time_2 + 40_000_000n < time_3); }); -test("Creating Runtime without shutting it down doesn't hang process", (t) => { +test.serial("Creating Runtime without shutting it down doesn't hang process", (t) => { const _runtime = native.newRuntime(GenericConfigs.runtime.basic); t.pass(); }); -test("Dropping Client without closing doesn't hang process", (t) => { +test.serial("Dropping Client without closing doesn't hang process", (t) => { const runtime = native.newRuntime(GenericConfigs.runtime.basic); const _client = native.newClient(runtime, GenericConfigs.client.basic); t.pass(); }); -test("Dropping Worker without shutting it down doesn't hang process", async (t) => { +test.serial("Dropping Worker without shutting it down doesn't hang process", async (t) => { const runtime = native.newRuntime(GenericConfigs.runtime.basic); const client = await native.newClient(runtime, GenericConfigs.client.basic); const worker = native.newWorker(client, GenericConfigs.worker.basic); @@ -156,13 +155,13 @@ test("Dropping Worker without shutting it down doesn't hang process", async (t) }); // FIXME(JWH): This is causing hangs on shutdown on Windows. -test("Dropping EphemeralServer without shutting it down doesn't hang process", async (t) => { - const runtime = native.newRuntime(GenericConfigs.runtime.basic); - const _ephemeralServer = await native.newEphemeralServer(runtime, GenericConfigs.ephemeralServer.basic); - t.pass(); -}); +// test.serial("Dropping EphemeralServer without shutting it down doesn't hang process", async (t) => { +// const runtime = native.newRuntime(GenericConfigs.runtime.basic); +// const _ephemeralServer = await native.newEphemeralServer(runtime, GenericConfigs.ephemeralServer.basic); +// t.pass(); +// }); -test("Stopping Worker after creating another runtime doesn't fail", async (t) => { +test.serial("Stopping Worker after creating another runtime doesn't fail", async (t) => { async function expectShutdownError(taskPromise: Promise) { await t.throwsAsync(taskPromise, { instanceOf: errors.ShutdownError, @@ -173,36 +172,47 @@ test("Stopping Worker after creating another runtime doesn't fail", async (t) => const runtime1 = native.newRuntime(GenericConfigs.runtime.basic); // Starts Worker 0 + console.log('Starting Worker 0'); const client0 = await native.newClient(runtime0, GenericConfigs.client.basic); const worker0 = native.newWorker(client0, GenericConfigs.worker.basic); await native.workerValidate(worker0); // Start Worker 1 + console.log('Starting Worker 1'); const client1 = await native.newClient(runtime1, GenericConfigs.client.basic); const worker1 = native.newWorker(client1, GenericConfigs.worker.basic); await native.workerValidate(worker1); // Start polling on Worker 1 (note reverse order of Worker 0) + console.log('Starting polling on Worker 1'); const wftPromise1 = native.workerPollWorkflowActivation(worker1); const atPromise1 = native.workerPollActivityTask(worker1); // Start polling on Worker 0 + console.log('Starting polling on Worker 0'); const wftPromise0 = native.workerPollWorkflowActivation(worker0); const atPromise0 = native.workerPollActivityTask(worker0); // Cleanly shutdown Worker 1 + console.log('Shutting down Worker 1'); native.workerInitiateShutdown(worker1); + console.log('Shutting down Worker 1.1'); await expectShutdownError(wftPromise1); + console.log('Shutting down Worker 1.2'); await expectShutdownError(atPromise1); + console.log('Shutting down Worker 1.3'); await native.workerFinalizeShutdown(worker1); // Leave Client 1 and Runtime 1 alive + console.log('Leave Client 1 and Runtime 1 alive'); // Create Runtime 2 and Worker 2, but don't immediately use them + console.log('Creating Worker 2'); const runtime2 = native.newRuntime(GenericConfigs.runtime.basic); const client2 = await native.newClient(runtime2, GenericConfigs.client.basic); const worker2 = native.newWorker(client2, GenericConfigs.worker.basic); // Cleanly shutdown Worker 0 + console.log('Shutting down Worker 0'); native.workerInitiateShutdown(worker0); await expectShutdownError(wftPromise0); await expectShutdownError(atPromise0); @@ -211,11 +221,13 @@ test("Stopping Worker after creating another runtime doesn't fail", async (t) => native.runtimeShutdown(runtime0); // Start yet another runtime, we really won't use it + console.log("Start yet another runtime, we really won't use it"); const _runtime3 = native.newRuntime(GenericConfigs.runtime.basic); // Start polling on Worker 2, then shut it down cleanly + console.log('Starting polling on Worker 2'); await native.workerValidate(worker2); - const wftPromise2 = native.workerPollWorkflowActivation(worker2); + const wftPromise2 = native.workerPollWorkflowActivation(worker2); // TODO: failing here const atPromise2 = native.workerPollActivityTask(worker2); native.workerInitiateShutdown(worker2); await expectShutdownError(wftPromise2); @@ -223,6 +235,7 @@ test("Stopping Worker after creating another runtime doesn't fail", async (t) => await native.workerFinalizeShutdown(worker2); native.clientClose(client2); native.runtimeShutdown(runtime2); + console.log('t.pass()'); t.pass(); }); @@ -234,7 +247,7 @@ const GenericConfigs = { basic: { logExporter: { type: 'console', - filter: 'ERROR', + filter: 'INFO', }, telemetry: { metricPrefix: 'test', diff --git a/packages/test/src/test-bundler.ts b/packages/test/src/test-bundler.ts deleted file mode 100644 index 69dbfc1d1..000000000 --- a/packages/test/src/test-bundler.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Test the various states of a Worker. - * Most tests use a mocked core, some tests run serially because they emit signals to the process - */ -import { unlink, writeFile } from 'node:fs/promises'; -import os from 'node:os'; -import { join as pathJoin } from 'node:path'; -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { moduleMatches } from '@temporalio/worker/lib/workflow/bundler'; -import { bundleWorkflowCode, DefaultLogger, LogEntry } from '@temporalio/worker'; -import { WorkflowClient } from '@temporalio/client'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { issue516 } from './mocks/workflows-with-node-dependencies/issue-516'; -import { successString } from './workflows'; - -test('moduleMatches works', (t) => { - t.true(moduleMatches('fs', ['fs'])); - t.true(moduleMatches('fs/lib/foo', ['fs'])); - t.false(moduleMatches('fs', ['foo'])); -}); - -if (RUN_INTEGRATION_TESTS) { - test('Worker can be created from bundle code', async (t) => { - const taskQueue = `${t.title}-${uuid4()}`; - const workflowBundle = await bundleWorkflowCode({ - workflowsPath: require.resolve('./workflows'), - }); - const worker = await Worker.create({ - taskQueue, - workflowBundle, - }); - const client = new WorkflowClient(); - await worker.runUntil(client.execute(successString, { taskQueue, workflowId: uuid4() })); - t.pass(); - }); - - test('Worker can be created from bundle path', async (t) => { - const taskQueue = `${t.title}-${uuid4()}`; - const { code } = await bundleWorkflowCode({ - workflowsPath: require.resolve('./workflows'), - }); - const uid = uuid4(); - const codePath = pathJoin(os.tmpdir(), `workflow-bundle-${uid}.js`); - await writeFile(codePath, code); - const workflowBundle = { codePath }; - const worker = await Worker.create({ - taskQueue, - workflowBundle, - }); - const client = new WorkflowClient(); - try { - await worker.runUntil(client.execute(successString, { taskQueue, workflowId: uuid4() })); - } finally { - await unlink(codePath); - } - t.pass(); - }); - - test('Workflow bundle can be created from code using ignoreModules', async (t) => { - const taskQueue = `${t.title}-${uuid4()}`; - const workflowBundle = await bundleWorkflowCode({ - workflowsPath: require.resolve('./mocks/workflows-with-node-dependencies/issue-516'), - ignoreModules: ['dns'], - }); - const worker = await Worker.create({ - taskQueue, - workflowBundle, - }); - const client = new WorkflowClient(); - await worker.runUntil(client.execute(issue516, { taskQueue, workflowId: uuid4() })); - t.pass(); - }); - - test('An error is thrown when workflow depends on a node built-in module', async (t) => { - const logs: LogEntry[] = []; - const logger = new DefaultLogger('WARN', (entry: LogEntry) => { - logs.push(entry); - console.warn(entry.message); - }); - - await t.throwsAsync( - bundleWorkflowCode({ - workflowsPath: require.resolve('./mocks/workflows-with-node-dependencies/issue-516'), - logger, - }), - { - instanceOf: Error, - message: /is importing the following disallowed modules.*dns/s, - } - ); - }); - - test('WorkerOptions.bundlerOptions.webpackConfigHook works', async (t) => { - const taskQueue = `${t.title}-${uuid4()}`; - await t.throwsAsync( - Worker.create({ - taskQueue, - workflowsPath: require.resolve('./workflows'), - bundlerOptions: { - webpackConfigHook: (config) => { - t.is(config.mode, 'development'); - config.mode = 'invalid' as any; - return config; - }, - }, - }), - { - name: 'ValidationError', - message: /Invalid configuration object./, - } - ); - }); -} diff --git a/packages/test/src/test-client-connection.ts b/packages/test/src/test-client-connection.ts deleted file mode 100644 index 7545c916a..000000000 --- a/packages/test/src/test-client-connection.ts +++ /dev/null @@ -1,612 +0,0 @@ -import { fork } from 'node:child_process'; -import * as http2 from 'node:http2'; -import * as util from 'node:util'; -import * as path from 'node:path'; -import * as fs from 'node:fs/promises'; -import assert from 'node:assert'; -import test, { TestFn } from 'ava'; -import * as grpc from '@grpc/grpc-js'; -import * as protoLoader from '@grpc/proto-loader'; -import { - Client, - Connection, - defaultGrpcRetryOptions, - isGrpcCancelledError, - isGrpcServiceError, - isRetryableError, - makeGrpcRetryInterceptor, -} from '@temporalio/client'; -import pkg from '@temporalio/client/lib/pkg'; -import { temporal, grpc as grpcProto } from '@temporalio/proto'; - -const workflowServicePackageDefinition = protoLoader.loadSync( - path.resolve( - __dirname, - '../../core-bridge/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/service.proto' - ), - { includeDirs: [path.resolve(__dirname, '../../core-bridge/sdk-core/sdk-core-protos/protos/api_upstream')] } -); -const workflowServiceProtoDescriptor = grpc.loadPackageDefinition(workflowServicePackageDefinition) as any; - -const healthServicePackageDefinition = protoLoader.loadSync( - path.resolve(__dirname, '../../core-bridge/sdk-core/sdk-core-protos/protos/grpc/health/v1/health.proto') -); -const healthServicePackageDescriptor = grpc.loadPackageDefinition(healthServicePackageDefinition) as any; - -async function bindLocalhost(server: grpc.Server): Promise { - return await util.promisify(server.bindAsync.bind(server))('127.0.0.1:0', grpc.ServerCredentials.createInsecure()); -} - -async function bindLocalhostIpv6(server: grpc.Server): Promise { - return await util.promisify(server.bindAsync.bind(server))('[::1]:0', grpc.ServerCredentials.createInsecure()); -} - -async function bindLocalhostTls(server: grpc.Server): Promise { - const caCert = await fs.readFile(path.resolve(__dirname, `../tls_certs/test-ca.crt`)); - const serverChainCert = await fs.readFile(path.resolve(__dirname, `../tls_certs/test-server-chain.crt`)); - const serverKey = await fs.readFile(path.resolve(__dirname, `../tls_certs/test-server.key`)); - const credentials = grpc.ServerCredentials.createSsl( - caCert, - [ - { - cert_chain: serverChainCert, - private_key: serverKey, - }, - ], - true - ); - return await util.promisify(server.bindAsync.bind(server))('127.0.0.1:0', credentials); -} - -test('withMetadata / withDeadline / withAbortSignal set the CallContext for RPC call', async (t) => { - let gotTestHeaders = false; - let gotStaticBinValue; - let gotOtherBinValue; - let gotDeadline = false; - const authTokens: string[] = []; - const deadline = Date.now() + 10000; - - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - registerNamespace( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IRegisterNamespaceRequest, - temporal.api.workflowservice.v1.IRegisterNamespaceResponse - >, - callback: grpc.sendUnaryData - ) { - const [testValue] = call.metadata.get('test'); - const [otherValue] = call.metadata.get('otherKey'); - const [staticValue] = call.metadata.get('staticKey'); - const [clientName] = call.metadata.get('client-name'); - const [clientVersion] = call.metadata.get('client-version'); - const [auth] = call.metadata.get('Authorization'); - if ( - testValue === 'true' && - otherValue === 'set' && - staticValue === 'set' && - clientName === 'temporal-typescript' && - clientVersion === pkg.version && - auth === 'Bearer test-token' - ) { - gotTestHeaders = true; - } - gotStaticBinValue = call.metadata.get('staticKey-bin'); - gotOtherBinValue = call.metadata.get('otherKey-bin'); - const receivedDeadline = call.getDeadline(); - // For some reason the deadline the server gets is slightly different from the one we send in the client - if (typeof receivedDeadline === 'number' && receivedDeadline >= deadline && receivedDeadline - deadline < 1000) { - gotDeadline = true; - } - callback(null, {}); - }, - startWorkflowExecution(call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) { - const [auth] = call.metadata.get('Authorization'); - authTokens.push(auth.toString()); - callback(null, {}); - }, - updateNamespace() { - // Simulate a hanging call to test abort signal support. - }, - }); - const port = await bindLocalhost(server); - const conn = await Connection.connect({ - address: `127.0.0.1:${port}`, - metadata: { staticKey: 'set', 'staticKey-bin': Buffer.from([0x00]) }, - apiKey: 'test-token', - }); - await conn.withMetadata({ test: 'true' }, () => - conn.withMetadata({ otherKey: 'set', 'otherKey-bin': Buffer.from([0x01]) }, () => - conn.withDeadline(deadline, () => conn.workflowService.registerNamespace({})) - ) - ); - t.true(gotTestHeaders); - t.true(gotDeadline); - t.deepEqual(gotStaticBinValue, [Buffer.from([0x00])]); - t.deepEqual(gotOtherBinValue, [Buffer.from([0x01])]); - await conn.withApiKey('tt-2', () => conn.workflowService.startWorkflowExecution({})); - conn.setApiKey('tt-3'); - await conn.workflowService.startWorkflowExecution({}); - const nextTTs = ['tt-4', 'tt-5']; - conn.setApiKey(() => nextTTs.shift()!); - await conn.workflowService.startWorkflowExecution({}); - await conn.workflowService.startWorkflowExecution({}); - t.deepEqual(authTokens, ['Bearer tt-2', 'Bearer tt-3', 'Bearer tt-4', 'Bearer tt-5']); - const ctrl = new AbortController(); - setTimeout(() => ctrl.abort(), 10); - const err = await t.throwsAsync(conn.withAbortSignal(ctrl.signal, () => conn.workflowService.updateNamespace({}))); - t.true(isGrpcCancelledError(err)); -}); - -test('apiKey sets temporal-namespace header appropriately', async (t) => { - let getSystemInfoHeaders: grpc.Metadata = new grpc.Metadata(); - let startWorkflowExecutionHeaders: grpc.Metadata = new grpc.Metadata(); - - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - getSystemInfo( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IGetSystemInfoRequest, - temporal.api.workflowservice.v1.IGetSystemInfoResponse - >, - callback: grpc.sendUnaryData - ) { - getSystemInfoHeaders = call.metadata.clone(); - callback(null, {}); - }, - startWorkflowExecution(call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) { - startWorkflowExecutionHeaders = call.metadata.clone(); - callback(null, {}); - }, - }); - const port = await bindLocalhost(server); - const conn = await Connection.connect({ - address: `127.0.0.1:${port}`, - metadata: { staticKey: 'set' }, - apiKey: 'test-token', - }); - - await conn.workflowService.startWorkflowExecution({ namespace: 'test-namespace' }); - - assert(getSystemInfoHeaders !== undefined); - t.deepEqual(getSystemInfoHeaders.get('temporal-namespace'), []); - t.deepEqual(getSystemInfoHeaders.get('authorization'), ['Bearer test-token']); - t.deepEqual(getSystemInfoHeaders.get('staticKey'), ['set']); - - assert(startWorkflowExecutionHeaders); - t.deepEqual(startWorkflowExecutionHeaders.get('temporal-namespace'), ['test-namespace']); - t.deepEqual(startWorkflowExecutionHeaders.get('authorization'), ['Bearer test-token']); - t.deepEqual(startWorkflowExecutionHeaders.get('staticKey'), ['set']); -}); - -test('Connection can connect using "[ipv6]:port" address', async (t) => { - let gotRequest = false; - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - getSystemInfo( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IGetSystemInfoRequest, - temporal.api.workflowservice.v1.IGetSystemInfoResponse - >, - callback: grpc.sendUnaryData - ) { - gotRequest = true; - callback(null, {}); - }, - }); - const port = await bindLocalhostIpv6(server); - const connection = await Connection.connect({ - address: `[::1]:${port}`, - }); - await new Client({ connection }); - t.true(gotRequest); -}); - -test('healthService works', async (t) => { - const server = new grpc.Server(); - server.addService(healthServicePackageDescriptor.grpc.health.v1.Health.service, { - check( - _call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData - ) { - callback( - null, - grpcProto.health.v1.HealthCheckResponse.create({ - status: grpcProto.health.v1.HealthCheckResponse.ServingStatus.SERVING, - }) - ); - }, - }); - const port = await bindLocalhost(server); - const conn = await Connection.connect({ address: `127.0.0.1:${port}` }); - const response = await conn.healthService.check({}); - t.is(response.status, grpcProto.health.v1.HealthCheckResponse.ServingStatus.SERVING); -}); - -test('grpc retry passes request and headers on retry, propagates responses', async (t) => { - let attempt = 0; - let successAttempt = 3; - - const meta = Array(); - const namespaces = Array(); - - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - describeWorkflowExecution( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IDescribeWorkflowExecutionRequest, - temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse - >, - callback: grpc.sendUnaryData - ) { - const { namespace } = call.request; - if (typeof namespace === 'string') { - namespaces.push(namespace); - } - const [aValue] = call.metadata.get('a'); - if (typeof aValue === 'string') { - meta.push(aValue); - } - - attempt++; - if (attempt < successAttempt) { - callback({ code: grpc.status.UNKNOWN }); - return; - } - const response: temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse = { - workflowExecutionInfo: { execution: { workflowId: 'test' } }, - }; - callback(null, response); - }, - }); - const port = await bindLocalhost(server); - // Default interceptor config with backoff factor of 1 to speed things up - const interceptor = makeGrpcRetryInterceptor(defaultGrpcRetryOptions({ factor: 1 })); - const conn = await Connection.connect({ - address: `127.0.0.1:${port}`, - metadata: { a: 'bc' }, - interceptors: [interceptor], - }); - const response = await conn.workflowService.describeWorkflowExecution({ namespace: 'a' }); - // Check that response is sent correctly - t.is(response.workflowExecutionInfo?.execution?.workflowId, 'test'); - t.is(attempt, 3); - // Check that request is sent correctly in each attempt - t.deepEqual(namespaces, ['a', 'a', 'a']); - // Check that metadata is sent correctly in each attempt - t.deepEqual(meta, ['bc', 'bc', 'bc']); - - // Reset and rerun expecting error in the response - attempt = 0; - successAttempt = 11; // never - - await t.throwsAsync(() => conn.workflowService.describeWorkflowExecution({ namespace: 'a' }), { - message: '2 UNKNOWN: Unknown Error', - }); - t.is(attempt, 10); -}); - -test('Default keepalive settings are set while maintaining user provided channelArgs', async (t) => { - const conn = Connection.lazy({ - channelArgs: { 'grpc.enable_channelz': 1, 'grpc.keepalive_permit_without_calls': 0 }, - }); - const { channelArgs } = conn.options; - t.is(channelArgs['grpc.keepalive_time_ms'], 30_000); - t.is(channelArgs['grpc.enable_channelz'], 1); - // User setting overrides default - t.is(channelArgs['grpc.keepalive_permit_without_calls'], 0); -}); - -test('Can configure TLS + call credentials', async (t) => { - const meta = Array(); - - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - getSystemInfo( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IGetSystemInfoRequest, - temporal.api.workflowservice.v1.IGetSystemInfoResponse - >, - callback: grpc.sendUnaryData - ) { - const [aValue] = call.metadata.get('a'); - const [authorizationValue] = call.metadata.get('authorization'); - if (typeof aValue === 'string' && typeof authorizationValue === 'string') { - meta.push([aValue, authorizationValue]); - } - - const response: temporal.api.workflowservice.v1.IGetSystemInfoResponse = { - serverVersion: 'test', - capabilities: undefined, - }; - callback(null, response); - }, - - describeWorkflowExecution( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IDescribeWorkflowExecutionRequest, - temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse - >, - callback: grpc.sendUnaryData - ) { - const [aValue] = call.metadata.get('a'); - const [authorizationValue] = call.metadata.get('authorization'); - if (typeof aValue === 'string' && typeof authorizationValue === 'string') { - meta.push([aValue, authorizationValue]); - } - - const response: temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse = { - workflowExecutionInfo: { execution: { workflowId: 'test' } }, - }; - callback(null, response); - }, - }); - const port = await bindLocalhostTls(server); - let callNumber = 0; - const oauth2Client: grpc.OAuth2Client = { - getRequestHeaders: async () => { - const accessToken = `oauth2-access-token-${++callNumber}`; - return { authorization: `Bearer ${accessToken}` }; - }, - }; - - // Default interceptor config with backoff factor of 1 to speed things up - // const interceptor = makeGrpcRetryInterceptor(defaultGrpcRetryOptions({ factor: 1 })); - const conn = await Connection.connect({ - address: `127.0.0.1:${port}`, - metadata: { a: 'bc' }, - tls: { - serverRootCACertificate: await fs.readFile(path.resolve(__dirname, `../tls_certs/test-ca.crt`)), - clientCertPair: { - crt: await fs.readFile(path.resolve(__dirname, `../tls_certs/test-client-chain.crt`)), - key: await fs.readFile(path.resolve(__dirname, `../tls_certs/test-client.key`)), - }, - serverNameOverride: 'server', - }, - callCredentials: [grpc.credentials.createFromGoogleCredential(oauth2Client)], - }); - - // Make three calls - await conn.workflowService.describeWorkflowExecution({ namespace: 'a' }); - await conn.workflowService.describeWorkflowExecution({ namespace: 'b' }); - await conn.workflowService.describeWorkflowExecution({ namespace: 'c' }); - - // Check that both connection level metadata and call credentials metadata are sent correctly - t.deepEqual(meta, [ - ['bc', 'Bearer oauth2-access-token-1'], - ['bc', 'Bearer oauth2-access-token-2'], - ['bc', 'Bearer oauth2-access-token-3'], - ['bc', 'Bearer oauth2-access-token-4'], - ]); -}); - -{ - const testWithRejectingServer = test as TestFn<{ - rejectingServer: { - server: grpc.Server; - port: number; - attemptsPerRequestId: Record; - }; - }>; - - testWithRejectingServer.before(async (t) => { - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - describeWorkflowExecution( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IDescribeWorkflowExecutionRequest, - temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse - >, - callback: grpc.sendUnaryData - ) { - const [requestId] = call.metadata.get('request-id') as string[]; - const [statusCode] = call.metadata.get('status-code') as string[]; - t.context.rejectingServer.attemptsPerRequestId[requestId] = - (t.context.rejectingServer.attemptsPerRequestId[requestId] ?? 0) + 1; - callback({ code: Number(statusCode) }); - }, - }); - const port = await bindLocalhost(server); - t.context.rejectingServer = { server, port, attemptsPerRequestId: {} }; - }); - - testWithRejectingServer.after((t) => { - t.context.rejectingServer.server.forceShutdown(); - }); - - // Refer to grpc.status for list and description of status codes. - for (let grpcStatusCode = 1; grpcStatusCode < 16; grpcStatusCode++) { - testWithRejectingServer( - `Retry policy is correctly applied on Client Connection (gRPC status code ${grpcStatusCode})`, - async (t) => { - const requestId = `request-${grpcStatusCode}`; - - const conn = await Connection.connect({ - address: `127.0.0.1:${t.context.rejectingServer.port}`, - interceptors: [ - makeGrpcRetryInterceptor( - // initialInterval divided by 10 compared to actual defaults, and backoff factor set to 1, - // both to speed things up. Also, no jitter, to make the test slightly more predictable. - defaultGrpcRetryOptions({ - factor: 1, - maxJitter: 0, - maxAttempts: 10, - initialIntervalMs(status) { - return status.code === grpc.status.RESOURCE_EXHAUSTED ? 100 : 10; - }, - }) - ), - ], - metadata: { - 'request-id': requestId, - 'status-code': String(grpcStatusCode), - }, - }); - - try { - const startTime = Date.now(); - const err = await t.throwsAsync(() => conn.workflowService.describeWorkflowExecution({}), { - message: (s) => s.startsWith(String(grpcStatusCode)), - }); - if (!err || !isGrpcServiceError(err)) { - return t.fail(`Expected a grpc service error, got ${err}`); - } - - const expectedAttempts = isRetryableError(err) ? 10 : 1; - const actualAttempts = t.context.rejectingServer.attemptsPerRequestId[requestId]; - t.is(actualAttempts, expectedAttempts); - - const expectedMinDuration = - (expectedAttempts - 1) * (grpcStatusCode === grpc.status.RESOURCE_EXHAUSTED ? 100 : 10); - // Here, we really just want to confirm that gRPC is not playing tricks on us by adding - // extra wait time over our own retry policy's backoff. But being too strict may cause - // flakes on very busy CI. Hence, we allow for very generous overhead. - // Allow an overhead of 500ms for the first attempt, then 200ms per retry. - const expectedMaxDuration = expectedMinDuration + 500 + (expectedAttempts - 1) * 200; - const actualDuration = Date.now() - startTime; - t.true( - actualDuration >= expectedMinDuration, - `Expected total duration to be less than ${expectedMinDuration}ms; got ${actualDuration}ms` - ); - t.true( - actualDuration <= expectedMaxDuration, - `Expected total duration to be at most ${expectedMaxDuration}ms; got ${actualDuration}ms` - ); - } finally { - await conn.close(); - } - } - ); - } -} - -// See https://github.com/temporalio/sdk-typescript/issues/1023 -test('No 10s delay on close due to grpc-js', async (t) => { - const server = new grpc.Server(); - try { - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, {}); - const port = await bindLocalhost(server); - const script = ` - const { Connection } = require("@temporalio/client"); - Connection.connect({ address: '127.0.0.1:${port}' }).catch(console.log); - `; - const startTime = Date.now(); - await new Promise((resolve, reject) => { - try { - const childProcess = fork('-e', [script]); - childProcess.on('exit', resolve); - childProcess.on('error', reject); - } catch (e) { - reject(e); - } - }); - const duration = Date.now() - startTime; - t.true(duration < 5000, `Expected duration to be less than 5s, got ${duration / 1000}s`); - } finally { - server.forceShutdown(); - } -}); - -test('Retry on "RST_STREAM with code 0"', async (t) => { - let receivedRequests = 0; - const requestHandler = (_req: http2.Http2ServerRequest, res: http2.Http2ServerResponse) => { - if (++receivedRequests < 4) { - // Just a 200 OK response, without the mandatory gRPC headers - res.writeHead(200); - res.end(); - } else { - // This time, send a complete gRPC response - res.statusCode = 200; - res.addTrailers({ - 'grpc-status': '0', - 'grpc-message': 'OK', - }); - res.write( - // This is a raw gRPC response, of length 0 - Buffer.from([ - // Frame Type: Data; Not Compressed - 0, - // Message Length: 0 - 0, 0, 0, 0, - ]) - ); - res.end(); - } - }; - - await withHttp2Server(async (port) => { - const connection = await Connection.connect({ address: `127.0.0.1:${port}` }); - try { - await new Client({ connection }); - t.is(receivedRequests, 4); - } finally { - await connection.close(); - } - }, requestHandler); -}); - -test('Retry on "RST_STREAM with code 2"', async (t) => { - let receivedRequests = 0; - - const requestHandler = (_req: http2.Http2ServerRequest, res: http2.Http2ServerResponse) => { - if (++receivedRequests < 4) { - // Sends a RST_STREAM with code 2 - res.stream.close(http2.constants.NGHTTP2_INTERNAL_ERROR); - } else { - // This time, send a complete gRPC response - res.statusCode = 200; - res.addTrailers({ - 'grpc-status': '0', - 'grpc-message': 'OK', - }); - res.write( - // This is a raw gRPC response, of length 0 - Buffer.from([ - // Frame Type: Data; Not Compressed - 0, - // Message Length: 0 - 0, 0, 0, 0, - ]) - ); - res.end(); - } - }; - - await withHttp2Server(async (port) => { - const connection = await Connection.connect({ - address: `127.0.0.1:${port}`, - }); - try { - await new Client({ connection }); - t.is(receivedRequests, 4); - } finally { - await connection.close(); - } - }, requestHandler); -}); - -async function withHttp2Server( - fn: (port: number) => Promise, - requestListener?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void -): Promise { - return new Promise((resolve, reject) => { - const srv = http2.createServer(); - srv.listen({ port: 0, host: '127.0.0.1' }, () => { - const addr = srv.address(); - if (typeof addr === 'string' || addr === null) { - throw new Error('Unexpected server address type'); - } - srv.on('request', async (req, res) => { - if (requestListener) await requestListener(req, res); - try { - res.end(); - } catch (_e) { - // requestListener may have messed up the HTTP2 connection. Just ignore. - } - }); - fn(addr.port) - .catch((e) => reject(e)) - .finally(() => srv.close((_) => resolve())); - }); - }); -} diff --git a/packages/test/src/test-client-errors.ts b/packages/test/src/test-client-errors.ts deleted file mode 100644 index c40879c31..000000000 --- a/packages/test/src/test-client-errors.ts +++ /dev/null @@ -1,217 +0,0 @@ -import anyTest, { TestFn } from 'ava'; -import { - ApplicationFailure, - Client, - NamespaceNotFoundError, - Next, - TerminateWorkflowExecutionResponse, - ValueError, - WorkflowClientInterceptor, - WorkflowTerminateInput, -} from '@temporalio/client'; -import { TestWorkflowEnvironment } from './helpers'; - -interface Context { - testEnv: TestWorkflowEnvironment; -} - -const test = anyTest as TestFn; - -const unserializableObject = { - toJSON() { - throw new TypeError('Unserializable Object'); - }, -}; - -test.before(async (t) => { - t.context = { - testEnv: await TestWorkflowEnvironment.createLocal(), - }; -}); - -test.after.always(async (t) => { - await t.context.testEnv?.teardown(); -}); - -test('WorkflowClient - namespace not found', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection, namespace: 'non-existent' }); - await t.throwsAsync( - client.workflow.start('test', { - workflowId: 'test', - taskQueue: 'test', - }), - { - instanceOf: NamespaceNotFoundError, - message: "Namespace not found: 'non-existent'", - } - ); -}); - -test('WorkflowClient - listWorkflows - namespace not found', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection, namespace: 'non-existent' }); - await t.throwsAsync(client.workflow.list()[Symbol.asyncIterator]().next(), { - instanceOf: NamespaceNotFoundError, - message: "Namespace not found: 'non-existent'", - }); -}); - -test('WorkflowClient - start - invalid input payload', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection }); - await t.throwsAsync( - client.workflow.start('test', { - workflowId: 'test', - taskQueue: 'test', - args: [unserializableObject], - }), - { - instanceOf: ValueError, - message: 'Unable to convert [object Object] to payload', - } - ); -}); - -test('WorkflowClient - signalWithStart - invalid input payload', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection }); - await t.throwsAsync( - client.workflow.signalWithStart('test', { - workflowId: 'test', - taskQueue: 'test', - args: [unserializableObject], - signal: 'testSignal', - }), - { - instanceOf: ValueError, - message: 'Unable to convert [object Object] to payload', - } - ); -}); - -test('WorkflowClient - signalWorkflow - invalid input payload', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection }); - await t.throwsAsync(client.workflow.getHandle('existant').signal('test', [unserializableObject]), { - instanceOf: ValueError, - message: 'Unable to convert [object Object] to payload', - }); -}); - -test('WorkflowClient - queryWorkflow - invalid input payload', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection }); - await t.throwsAsync(client.workflow.getHandle('existant').query('test', [unserializableObject]), { - instanceOf: ValueError, - message: 'Unable to convert [object Object] to payload', - }); -}); - -test('WorkflowClient - terminateWorkflow - invalid details payload', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ - connection, - interceptors: { - workflow: [ - { - async terminate( - input: WorkflowTerminateInput, - next: Next - ): Promise { - return next({ - ...input, - details: [unserializableObject], - }); - }, - }, - ], - }, - }); - await t.throwsAsync(client.workflow.getHandle('existant').terminate('reason'), { - instanceOf: ValueError, - message: 'Unable to convert [object Object] to payload', - }); -}); - -test('ScheduleClient - namespace not found', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection, namespace: 'non-existent' }); - await t.throwsAsync( - client.schedule.create({ - scheduleId: 'test', - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action: { - type: 'startWorkflow', - workflowType: 'test', - taskQueue: 'test', - }, - }), - { - instanceOf: NamespaceNotFoundError, - message: "Namespace not found: 'non-existent'", - } - ); -}); - -test('ScheduleClient - listSchedule - namespace not found', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection, namespace: 'non-existent' }); - await t.throwsAsync(client.schedule.list()[Symbol.asyncIterator]().next(), { - instanceOf: NamespaceNotFoundError, - message: "Namespace not found: 'non-existent'", - }); -}); - -test('AsyncCompletionClient - namespace not found', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection, namespace: 'non-existent' }); - await t.throwsAsync(client.activity.complete(new Uint8Array([1]), 'result'), { - instanceOf: NamespaceNotFoundError, - message: "Namespace not found: 'non-existent'", - }); -}); - -test('AsyncCompletionClient - complete - invalid payload', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection }); - await t.throwsAsync(client.activity.complete(new Uint8Array([1]), unserializableObject), { - instanceOf: ValueError, - message: 'Unable to convert [object Object] to payload', - }); -}); - -test('AsyncCompletionClient - fail - invalid payload', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection }); - await t.throwsAsync( - client.activity.fail( - new Uint8Array([1]), - ApplicationFailure.create({ type: 'test', details: [unserializableObject] }) - ), - { - instanceOf: ValueError, - message: 'Unable to convert [object Object] to payload', - } - ); -}); - -test('AsyncCompletionClient - reportCancellation - invalid payload', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection }); - await t.throwsAsync(client.activity.reportCancellation(new Uint8Array([1]), unserializableObject), { - instanceOf: ValueError, - message: 'Unable to convert [object Object] to payload', - }); -}); - -test('AsyncCompletionClient - heartbeat - invalid payload', async (t) => { - const { connection } = t.context.testEnv; - const client = new Client({ connection }); - await t.throwsAsync(client.activity.heartbeat(new Uint8Array([1]), unserializableObject), { - instanceOf: ValueError, - message: 'Unable to convert [object Object] to payload', - }); -}); diff --git a/packages/test/src/test-custom-payload-codec.ts b/packages/test/src/test-custom-payload-codec.ts deleted file mode 100644 index 34d4b8b49..000000000 --- a/packages/test/src/test-custom-payload-codec.ts +++ /dev/null @@ -1,269 +0,0 @@ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { WorkflowClient } from '@temporalio/client'; -import { Payload, PayloadCodec } from '@temporalio/common'; -import { decode } from '@temporalio/common/lib/encoding'; -import { InjectedSinks } from '@temporalio/worker'; -import { createConcatActivity } from './activities/create-concat-activity'; -import { RUN_INTEGRATION_TESTS, u8, Worker } from './helpers'; -import { defaultOptions } from './mock-native-worker'; -import { LogSinks, twoStrings, twoStringsActivity } from './workflows'; - -class TestEncodeCodec implements PayloadCodec { - async encode(payloads: Payload[]): Promise { - return payloads.map((payload) => { - payload.data = u8('"encoded"'); - return payload; - }); - } - - async decode(payloads: Payload[]): Promise { - return payloads; - } -} - -class TestDecodeCodec implements PayloadCodec { - async encode(payloads: Payload[]): Promise { - return payloads; - } - - async decode(payloads: Payload[]): Promise { - return payloads.map((payload) => { - payload.data = u8('"decoded"'); - return payload; - }); - } -} - -if (RUN_INTEGRATION_TESTS) { - test('Workflow arguments and retvals are encoded', async (t) => { - const logs: string[] = []; - const sinks: InjectedSinks = { - logger: { - log: { - fn(_, message) { - logs.push(message); - }, - }, - }, - }; - - const dataConverter = { payloadCodecs: [new TestEncodeCodec()] }; - const taskQueue = 'test-workflow-encoded'; - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - dataConverter, - sinks, - }); - const client = new WorkflowClient({ dataConverter }); - await worker.runUntil(async () => { - const result = await client.execute(twoStrings, { - args: ['arg1', 'arg2'], - workflowId: uuid4(), - taskQueue, - }); - - t.is(result, 'encoded'); // workflow retval encoded by worker - }); - t.is(logs[0], 'encodedencoded'); // workflow args encoded by client - }); - - test('Workflow arguments and retvals are decoded', async (t) => { - const logs: string[] = []; - const sinks: InjectedSinks = { - logger: { - log: { - fn(_, message) { - logs.push(message); - }, - }, - }, - }; - - const dataConverter = { payloadCodecs: [new TestDecodeCodec()] }; - const taskQueue = 'test-workflow-decoded'; - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - dataConverter, - sinks, - }); - const client = new WorkflowClient({ dataConverter }); - await worker.runUntil(async () => { - const result = await client.execute(twoStrings, { - args: ['arg1', 'arg2'], - workflowId: uuid4(), - taskQueue, - }); - - t.is(result, 'decoded'); // workflow retval decoded by client - }); - t.is(logs[0], 'decodeddecoded'); // workflow args decoded by worker - }); - - test('Activity arguments and retvals are encoded', async (t) => { - const workflowLogs: string[] = []; - const sinks: InjectedSinks = { - logger: { - log: { - fn(_, message) { - workflowLogs.push(message); - }, - }, - }, - }; - const activityLogs: string[] = []; - - const dataConverter = { payloadCodecs: [new TestEncodeCodec()] }; - const taskQueue = 'test-activity-encoded'; - const worker = await Worker.create({ - ...defaultOptions, - activities: createConcatActivity(activityLogs), - taskQueue, - dataConverter, - sinks, - }); - const client = new WorkflowClient({ dataConverter }); - await worker.runUntil(async () => { - await client.execute(twoStringsActivity, { - workflowId: uuid4(), - taskQueue, - }); - }); - t.is(workflowLogs[0], 'encoded'); // activity retval encoded by worker - t.is(activityLogs[0], 'Activityencodedencoded'); // activity args encoded by worker - }); - - test('Activity arguments and retvals are decoded', async (t) => { - const workflowLogs: string[] = []; - const sinks: InjectedSinks = { - logger: { - log: { - fn(_, message) { - workflowLogs.push(message); - }, - }, - }, - }; - const activityLogs: string[] = []; - - const dataConverter = { payloadCodecs: [new TestDecodeCodec()] }; - const taskQueue = 'test-activity-decoded'; - const worker = await Worker.create({ - ...defaultOptions, - activities: createConcatActivity(activityLogs), - taskQueue, - dataConverter, - sinks, - }); - const client = new WorkflowClient({ dataConverter }); - await worker.runUntil(async () => { - await client.execute(twoStringsActivity, { - workflowId: uuid4(), - taskQueue, - }); - }); - t.is(workflowLogs[0], 'decoded'); // activity retval decoded by worker - t.is(activityLogs[0], 'Activitydecodeddecoded'); // activity args decoded by worker - }); - - test('Multiple encodes happen in the correct order', async (t) => { - const logs: string[] = []; - const sinks: InjectedSinks = { - logger: { - log: { - fn(_, message) { - logs.push(message); - }, - }, - }, - }; - - const dataConverter = { - payloadCodecs: [ - new TestEncodeCodec(), - { - async encode(payloads: Payload[]): Promise { - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - if (decode(payloads[0]!.data!) !== '"encoded"') { - throw new Error('wrong order'); - } - return payloads; - }, - async decode(payloads: Payload[]): Promise { - return payloads; - }, - }, - ], - }; - const taskQueue = 'test-workflow-encoded-order'; - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - dataConverter, - sinks, - }); - const client = new WorkflowClient({ dataConverter }); - await worker.runUntil(async () => { - const result = await client.execute(twoStrings, { - args: ['arg1', 'arg2'], - workflowId: uuid4(), - taskQueue, - }); - - t.is(result, 'encoded'); // workflow retval encoded by worker - }); - t.is(logs[0], 'encodedencoded'); // workflow args encoded by client - }); - - test('Multiple decodes happen in the correct order', async (t) => { - const logs: string[] = []; - const sinks: InjectedSinks = { - logger: { - log: { - fn(_, message) { - logs.push(message); - }, - }, - }, - }; - - const dataConverter = { - payloadCodecs: [ - { - async encode(payloads: Payload[]): Promise { - return payloads; - }, - async decode(payloads: Payload[]): Promise { - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - if (decode(payloads[0]!.data!) !== '"decoded"') { - throw new Error('wrong order'); - } - - return payloads; - }, - }, - new TestDecodeCodec(), - ], - }; - const taskQueue = 'test-workflow-decoded-order'; - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - dataConverter, - sinks, - }); - const client = new WorkflowClient({ dataConverter }); - await worker.runUntil(async () => { - const result = await client.execute(twoStrings, { - args: ['arg1', 'arg2'], - workflowId: uuid4(), - taskQueue, - }); - - t.is(result, 'decoded'); // workflow retval decoded by client - }); - t.is(logs[0], 'decodeddecoded'); // workflow args decoded by worker - }); -} diff --git a/packages/test/src/test-custom-payload-converter.ts b/packages/test/src/test-custom-payload-converter.ts deleted file mode 100644 index 3e0277cba..000000000 --- a/packages/test/src/test-custom-payload-converter.ts +++ /dev/null @@ -1,153 +0,0 @@ -import test, { ExecutionContext } from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { WorkflowClient } from '@temporalio/client'; -import { toPayloads } from '@temporalio/common'; -import { coresdk } from '@temporalio/proto'; -import { ProtoActivityResult } from '../protos/root'; -import { protoActivity } from './activities'; -import { cleanOptionalStackTrace, RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { defaultOptions, isolateFreeWorker } from './mock-native-worker'; -import { messageInstance, payloadConverter } from './payload-converters/proto-payload-converter'; -import * as workflows from './workflows'; -import { protobufWorkflow } from './workflows/protobufs'; - -function compareCompletion( - t: ExecutionContext, - actual: coresdk.activity_result.IActivityExecutionResult | null | undefined, - expected: coresdk.activity_result.IActivityExecutionResult -) { - if (actual?.failed?.failure) { - const { stackTrace, ...rest } = actual.failed.failure; - actual = { failed: { failure: { stackTrace: cleanOptionalStackTrace(stackTrace), ...rest } } }; - } - t.deepEqual( - coresdk.activity_result.ActivityExecutionResult.create(actual ?? undefined).toJSON(), - coresdk.activity_result.ActivityExecutionResult.create(expected).toJSON() - ); -} - -if (RUN_INTEGRATION_TESTS) { - test('Client and Worker work with provided dataConverter', async (t) => { - const dataConverter = { payloadConverterPath: require.resolve('./payload-converters/proto-payload-converter') }; - const taskQueue = 'test-custom-payload-converter'; - const worker = await Worker.create({ - ...defaultOptions, - workflowsPath: require.resolve('./workflows/protobufs'), - taskQueue, - dataConverter, - }); - const client = new WorkflowClient({ dataConverter }); - await worker.runUntil(async () => { - const result = await client.execute(protobufWorkflow, { - args: [messageInstance], - workflowId: uuid4(), - taskQueue, - }); - - t.deepEqual(result, ProtoActivityResult.create({ sentence: `Proto is 1 years old.` })); - }); - }); - - test('fromPayload throws on Client when receiving result from client.execute()', async (t) => { - const worker = await Worker.create({ - ...defaultOptions, - }); - - const client = new WorkflowClient({ - dataConverter: { - payloadConverterPath: require.resolve('./payload-converters/payload-converter-throws-from-payload'), - }, - }); - await worker.runUntil( - t.throwsAsync( - client.execute(workflows.successString, { - taskQueue: 'test', - workflowId: uuid4(), - }), - { - instanceOf: Error, - message: 'test fromPayload', - } - ) - ); - }); -} - -test('Worker throws on invalid payloadConverterPath', async (t) => { - t.throws( - () => - isolateFreeWorker({ - ...defaultOptions, - dataConverter: { payloadConverterPath: './wrong-path' }, - }), - { - message: /Could not find a file at the specified payloadConverterPath/, - } - ); - - t.throws( - () => - isolateFreeWorker({ - ...defaultOptions, - dataConverter: { payloadConverterPath: require.resolve('./payload-converters/payload-converter-no-export') }, - }), - { - message: /Module .* does not have a `payloadConverter` named export/, - } - ); - - t.throws( - () => - isolateFreeWorker({ - ...defaultOptions, - dataConverter: { payloadConverterPath: require.resolve('./payload-converters/payload-converter-bad-export') }, - }), - { - message: /payloadConverter export at .* must be an object with toPayload and fromPayload methods/, - } - ); - - t.throws( - () => - isolateFreeWorker({ - ...defaultOptions, - dataConverter: { failureConverterPath: require.resolve('./payload-converters/payload-converter-bad-export') }, - }), - { - message: /Module .* does not have a `failureConverter` named export/, - } - ); - - t.throws( - () => - isolateFreeWorker({ - ...defaultOptions, - dataConverter: { failureConverterPath: require.resolve('./payload-converters/failure-converter-bad-export') }, - }), - { - message: /failureConverter export at .* must be an object with errorToFailure and failureToError methods/, - } - ); -}); - -test('Worker with proto data converter runs an activity and reports completion', async (t) => { - const worker = isolateFreeWorker({ - ...defaultOptions, - dataConverter: { payloadConverterPath: require.resolve('./payload-converters/proto-payload-converter') }, - }); - - await worker.runUntil(async () => { - const taskToken = Buffer.from(uuid4()); - const completion = await worker.native.runActivityTask({ - taskToken, - start: { - activityType: 'protoActivity', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(payloadConverter, messageInstance), - }, - }); - compareCompletion(t, completion.result, { - completed: { result: payloadConverter.toPayload(await protoActivity(messageInstance)) }, - }); - }); -}); diff --git a/packages/test/src/test-default-activity.ts b/packages/test/src/test-default-activity.ts deleted file mode 100644 index 7cec75dd9..000000000 --- a/packages/test/src/test-default-activity.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as activities from './activities/default-and-defined'; -import { helpers, makeTestFunction } from './helpers-integration'; -import { workflowWithMaybeDefinedActivity } from './workflows/default-activity-wf'; - -const test = makeTestFunction({ workflowsPath: require.resolve('./workflows/default-activity-wf') }); - -test('Uses default activity if no matching activity exists', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities, - }); - - await worker.runUntil(async () => { - const activityArgs = ['test', 'args']; - const res = await executeWorkflow(workflowWithMaybeDefinedActivity, { - args: [false, activityArgs], - }); - t.deepEqual(res, { name: 'default', activityName: 'nonExistentActivity', args: activityArgs }); - }); -}); - -test('Does not use default activity if matching activity exists', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities, - }); - - await worker.runUntil(async () => { - const activityArgs = ['test', 'args']; - const res = await executeWorkflow(workflowWithMaybeDefinedActivity, { - args: [true, activityArgs], - }); - t.deepEqual(res, { name: 'definedActivity', args: activityArgs }); - }); -}); diff --git a/packages/test/src/test-default-workflow.ts b/packages/test/src/test-default-workflow.ts deleted file mode 100644 index 49fe21362..000000000 --- a/packages/test/src/test-default-workflow.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Test usage of a default workflow handler - */ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { TestWorkflowEnvironment, Worker } from './helpers'; -import { existing } from './workflows/default-workflow-function'; - -test('Default workflow handler is used if requested workflow does not exist', async (t) => { - const env = await TestWorkflowEnvironment.createLocal(); - try { - const taskQueue = `${t.title}-${uuid4()}`; - const worker = await Worker.create({ - connection: env.nativeConnection, - taskQueue, - workflowsPath: require.resolve('./workflows/default-workflow-function'), - }); - await worker.runUntil(async () => { - const result = env.client.workflow.execute('non-existing', { - taskQueue, - workflowId: uuid4(), - args: ['test', 'foo', 'bar'], - }); - t.is((await result).handler, 'default'); - t.is((await result).workflowType, 'non-existing'); - t.deepEqual((await result).args, ['test', 'foo', 'bar']); - }); - } finally { - await env.teardown(); - } -}); - -test('Default workflow handler is not used if requested workflow exists', async (t) => { - const env = await TestWorkflowEnvironment.createLocal(); - try { - const taskQueue = `${t.title}-${uuid4()}`; - const worker = await Worker.create({ - connection: env.nativeConnection, - taskQueue, - workflowsPath: require.resolve('./workflows/default-workflow-function'), - }); - await worker.runUntil(async () => { - const result = env.client.workflow.execute(existing, { - taskQueue, - workflowId: uuid4(), - args: ['test', 'foo', 'bar'], - }); - t.is((await result).handler, 'existing'); - t.deepEqual((await result).args, ['test', 'foo', 'bar']); - }); - } finally { - await env.teardown(); - } -}); diff --git a/packages/test/src/test-enums-helpers.ts b/packages/test/src/test-enums-helpers.ts deleted file mode 100644 index 60a85b03b..000000000 --- a/packages/test/src/test-enums-helpers.ts +++ /dev/null @@ -1,494 +0,0 @@ -import test from 'ava'; -import { coresdk } from '@temporalio/proto'; -import { makeProtoEnumConverters as makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow/enums-helpers'; - -// ASSERTION: There MUST be a corresponding `KEY: 'KEY'` in the const object of strings enum (must be present) -{ - type ParentClosePolicyMissingEntry = - (typeof ParentClosePolicyMissingEntry)[keyof typeof ParentClosePolicyMissingEntry]; - const ParentClosePolicyMissingEntry = { - TERMINATE: 'TERMINATE', - // ABANDON: 'ABANDON', // Missing entry! - REQUEST_CANCEL: 'REQUEST_CANCEL', - } as const; - - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - // @ts-expect-error 2344 Property 'ABANDON' is missing in type '{...}' but required in type '...' - typeof ParentClosePolicyMissingEntry, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicyMissingEntry.TERMINATE]: 1, - [ParentClosePolicyMissingEntry.REQUEST_CANCEL]: 3, - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -// ASSERTION: There MUST be a corresponding `KEY: 'KEY'` in the const object of strings enum (must have correct value) -{ - type ParentClosePolicyIncorectEntry = - (typeof ParentClosePolicyIncorectEntry)[keyof typeof ParentClosePolicyIncorectEntry]; - const ParentClosePolicyIncorectEntry = { - TERMINATE: 'TERMINATE', - ABANDON: 'INCORRECT', // Incorrect entry! - REQUEST_CANCEL: 'REQUEST_CANCEL', - } as const; - - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - // @ts-expect-error 2344 Type '"INCORRECT"' is not assignable to type '"ABANDON"' - typeof ParentClosePolicyIncorectEntry, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicyIncorectEntry.TERMINATE]: 1, - [ParentClosePolicyIncorectEntry.ABANDON]: 2, - [ParentClosePolicyIncorectEntry.REQUEST_CANCEL]: 3, - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -// ASSERTION: There MAY be a corresponding `PREFIX_KEY: 'KEY'` in the const object of strings enum (may be present) -{ - type ParentClosePolicyWithPrefixedEntries = - (typeof ParentClosePolicyWithPrefixedEntries)[keyof typeof ParentClosePolicyWithPrefixedEntries]; - const ParentClosePolicyWithPrefixedEntries = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - - PARENT_CLOSE_POLICY_TERMINATE: 'TERMINATE', - PARENT_CLOSE_POLICY_ABANDON: 'ABANDON', - PARENT_CLOSE_POLICY_REQUEST_CANCEL: 'REQUEST_CANCEL', - } as const; - - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicyWithPrefixedEntries, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicyWithPrefixedEntries.TERMINATE]: 1, - [ParentClosePolicyWithPrefixedEntries.ABANDON]: 2, - [ParentClosePolicyWithPrefixedEntries.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -// ASSERTION: There MAY be a corresponding `PREFIX_KEY: 'KEY'` in the const object of strings enum (may not be present) -{ - type ParentClosePolicyWithoutPrefixedEntries = - (typeof ParentClosePolicyWithoutPrefixedEntries)[keyof typeof ParentClosePolicyWithoutPrefixedEntries]; - const ParentClosePolicyWithoutPrefixedEntries = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - } as const; - - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicyWithoutPrefixedEntries, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicyWithoutPrefixedEntries.TERMINATE]: 1, - [ParentClosePolicyWithoutPrefixedEntries.ABANDON]: 2, - [ParentClosePolicyWithoutPrefixedEntries.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -// ASSERTION: There MAY be a corresponding `PREFIX_KEY: 'KEY'` in the const object of strings enum (if present, must have correct value) -{ - type ParentClosePolicyWithPrefixedEntries = - (typeof ParentClosePolicyWithPrefixedEntries)[keyof typeof ParentClosePolicyWithPrefixedEntries]; - const ParentClosePolicyWithPrefixedEntries = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - - PARENT_CLOSE_POLICY_TERMINATE: 'TERMINATE', - PARENT_CLOSE_POLICY_ABANDON: 'ABANDON', - PARENT_CLOSE_POLICY_REQUEST_CANCEL: 'INCORRECT', // Incorrect entry! - } as const; - - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - // @ts-expect-error 2344 Type '"INCORRECT"' is not assignable to type '"REQUEST_CANCEL"' - typeof ParentClosePolicyWithPrefixedEntries, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicyWithPrefixedEntries.TERMINATE]: 1, - [ParentClosePolicyWithPrefixedEntries.ABANDON]: 2, - [ParentClosePolicyWithPrefixedEntries.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -{ - type ParentClosePolicy = (typeof ParentClosePolicy)[keyof typeof ParentClosePolicy]; - const ParentClosePolicy = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - - PARENT_CLOSE_POLICY_UNSPECIFIED: undefined, - PARENT_CLOSE_POLICY_TERMINATE: 'TERMINATE', - PARENT_CLOSE_POLICY_ABANDON: 'ABANDON', - PARENT_CLOSE_POLICY_REQUEST_CANCEL: 'REQUEST_CANCEL', - } as const; - - // ASSERTION: There MUST be a corresponding `KEY: number` in the mapping table (must be there) - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicy, - 'PARENT_CLOSE_POLICY_' - >( - // @ts-expect-error 2345 Property '...' is missing in type '{...}' but required in type '...' - { - [ParentClosePolicy.TERMINATE]: 1, - // [ParentClosePolicy.ABANDON]: 2, // Missing entry! - [ParentClosePolicy.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); - - // ASSERTION: There MUST be a corresponding `KEY: number` in the mapping table (must be correct) - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicy, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicy.TERMINATE]: 1, - // @ts-expect-error 2418 Type of computed property's value is '...', which is not assignable to type '...' - [ParentClosePolicy.ABANDON]: 4, // Incorrect value! - [ParentClosePolicy.REQUEST_CANCEL]: 3, - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -// ASSERTION: There MAY be a corresponding `PREFIX_UNSPECIFIED: undefined` in the const object of strings enum (may be there) -{ - const ParentClosePolicyWithUnspecified = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - PARENT_CLOSE_POLICY_UNSPECIFIED: undefined, - } as const; - - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicyWithUnspecified, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicyWithUnspecified.TERMINATE]: 1, - [ParentClosePolicyWithUnspecified.ABANDON]: 2, - [ParentClosePolicyWithUnspecified.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -// ASSERTION: There MAY be a corresponding `PREFIX_UNSPECIFIED: undefined` in the const object of strings enum (may not be there) -{ - const ParentClosePolicyWithoutUnspecified = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - } as const; - - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicyWithoutUnspecified, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicyWithoutUnspecified.TERMINATE]: 1, - [ParentClosePolicyWithoutUnspecified.ABANDON]: 2, - [ParentClosePolicyWithoutUnspecified.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -// ASSERTION: There MAY be a corresponding `PREFIX_UNSPECIFIED: undefined` in the const object of strings enum (if present, must have correct value) -{ - const ParentClosePolicyWithUnspecifiedIncorrectValue = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - PARENT_CLOSE_POLICY_UNSPECIFIED: 'UNSPECIFIED', // Incorrect value! - } as const; - - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - // @ts-expect-error 2344 Type '"UNSPECIFIED"' is not assignable to type 'undefined' - typeof ParentClosePolicyWithUnspecifiedIncorrectValue, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicyWithUnspecifiedIncorrectValue.TERMINATE]: 1, - [ParentClosePolicyWithUnspecifiedIncorrectValue.ABANDON]: 2, - [ParentClosePolicyWithUnspecifiedIncorrectValue.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -// ASSERTION: There MUST be an `UNSPECIFIED: 0` in the mapping table -{ - const ParentClosePolicyWithoutUnspecified = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - } as const; - - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicyWithoutUnspecified, - 'PARENT_CLOSE_POLICY_' - >( - // @ts-expect-error 2345 Property 'UNSPECIFIED' is missing in type '{...}' but required in type '...' - { - [ParentClosePolicyWithoutUnspecified.TERMINATE]: 1, - [ParentClosePolicyWithoutUnspecified.ABANDON]: 2, - [ParentClosePolicyWithoutUnspecified.REQUEST_CANCEL]: 3, - // UNSPECIFIED: 0, // Missing UNSPECIFIED entry! - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -// ASSERTION: The const object of strings enum MUST NOT contain any other keys than the ones mandated or optionally allowed above. -{ - type ParentClosePolicyWithExtra = (typeof ParentClosePolicyWithExtra)[keyof typeof ParentClosePolicyWithExtra]; - const ParentClosePolicyWithExtra = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - EXTRA: 'EXTRA', // Extra entry! - } as const; - - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - // @ts-expect-error 2344 Types of property 'EXTRA' are incompatible — Type 'string' is not assignable to type 'never' - typeof ParentClosePolicyWithExtra, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicyWithExtra.TERMINATE]: 1, - [ParentClosePolicyWithExtra.ABANDON]: 2, - [ParentClosePolicyWithExtra.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); -} - -{ - type ParentClosePolicy = (typeof ParentClosePolicy)[keyof typeof ParentClosePolicy]; - const ParentClosePolicy = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - - PARENT_CLOSE_POLICY_UNSPECIFIED: undefined, - PARENT_CLOSE_POLICY_TERMINATE: 'TERMINATE', - PARENT_CLOSE_POLICY_ABANDON: 'ABANDON', - PARENT_CLOSE_POLICY_REQUEST_CANCEL: 'REQUEST_CANCEL', - } as const; - - // ASSERTION: The mapping table MUST NOT contain any other keys than the ones mandated above - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicy, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicy.TERMINATE]: 1, - [ParentClosePolicy.ABANDON]: 2, - // @ts-expect-error 2353 Object literal may only specify known properties, and '...' does not exist in type - extraEntry: 1, - [ParentClosePolicy.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); - - // ASSERTION: The mapping table MUST NOT contain any other keys than the ones mandated above (duplicate entry using prefixed key) - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicy, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicy.TERMINATE]: 1, - [ParentClosePolicy.ABANDON]: 2, - //@ts-expect-error 1117 An object literal cannot have multiple properties with the same name - [ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON]: 2, // Duplicate entry using prefixed key! - [ParentClosePolicy.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); - - // ASSERTION: The mapping table MUST NOT contain any other keys than the ones mandated above (duplicate entry using prefixewd key as raw string) - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicy, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicy.TERMINATE]: 1, - [ParentClosePolicy.ABANDON]: 2, - // @ts-expect-error 2353 Object literal may only specify known properties, and '...' does not exist in type - PARENT_CLOSE_POLICY_ABANDON: 2, // Duplicate entry using prefixed key! - [ParentClosePolicy.REQUEST_CANCEL]: 3, - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); - - // ASSERTION: If a prefix is provided, then all values in the proto enum MUST start with that prefix - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - // @ts-expect-error 2344 Type '...' does not satisfy the constraint '...'. - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicy, - 'INVALID_PREFIX_' // Incorrect prefix! - >( - { - [ParentClosePolicy.TERMINATE]: 1, - [ParentClosePolicy.ABANDON]: 2, - [ParentClosePolicy.REQUEST_CANCEL]: 3, - } as const, - 'INVALID_PREFIX_' - ); -} - -// Functionnal tests -{ - type ParentClosePolicy = (typeof ParentClosePolicy)[keyof typeof ParentClosePolicy]; - const ParentClosePolicy = { - TERMINATE: 'TERMINATE', - ABANDON: 'ABANDON', - REQUEST_CANCEL: 'REQUEST_CANCEL', - - PARENT_CLOSE_POLICY_UNSPECIFIED: undefined, - PARENT_CLOSE_POLICY_TERMINATE: 'TERMINATE', - PARENT_CLOSE_POLICY_ABANDON: 'ABANDON', - PARENT_CLOSE_POLICY_REQUEST_CANCEL: 'REQUEST_CANCEL', - } as const; - - const [encodeParentClosePolicy, decodeParentClosePolicy] = // - makeProtoEnumConverters< - coresdk.child_workflow.ParentClosePolicy, - typeof coresdk.child_workflow.ParentClosePolicy, - keyof typeof coresdk.child_workflow.ParentClosePolicy, - typeof ParentClosePolicy, - 'PARENT_CLOSE_POLICY_' - >( - { - [ParentClosePolicy.TERMINATE]: 1, - [ParentClosePolicy.ABANDON]: 2, - [ParentClosePolicy.REQUEST_CANCEL]: 3, - - UNSPECIFIED: 0, - } as const, - 'PARENT_CLOSE_POLICY_' - ); - - test('Protobuf Enum to Const Object of Strings conversion works', (t) => { - t.is(encodeParentClosePolicy(undefined), undefined); - t.is(encodeParentClosePolicy(ParentClosePolicy.PARENT_CLOSE_POLICY_UNSPECIFIED), undefined); - - t.is( - encodeParentClosePolicy(ParentClosePolicy.TERMINATE), - coresdk.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE - ); - t.is( - encodeParentClosePolicy(ParentClosePolicy.ABANDON), - coresdk.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON - ); - t.is( - encodeParentClosePolicy(ParentClosePolicy.REQUEST_CANCEL), - coresdk.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_REQUEST_CANCEL - ); - - t.is( - encodeParentClosePolicy(ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON), - coresdk.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON - ); - t.is( - encodeParentClosePolicy(ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE), - coresdk.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE - ); - t.is( - encodeParentClosePolicy(ParentClosePolicy.PARENT_CLOSE_POLICY_REQUEST_CANCEL), - coresdk.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_REQUEST_CANCEL - ); - }); - - test('Const Object of Strings to Protobuf Enum conversion works', (t) => { - t.is( - decodeParentClosePolicy(coresdk.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE), - ParentClosePolicy.TERMINATE - ); - t.is( - decodeParentClosePolicy(coresdk.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON), - ParentClosePolicy.ABANDON - ); - t.is( - decodeParentClosePolicy(coresdk.child_workflow.ParentClosePolicy.PARENT_CLOSE_POLICY_REQUEST_CANCEL), - ParentClosePolicy.REQUEST_CANCEL - ); - }); -} diff --git a/packages/test/src/test-envconfig.ts b/packages/test/src/test-envconfig.ts deleted file mode 100644 index db18d1647..000000000 --- a/packages/test/src/test-envconfig.ts +++ /dev/null @@ -1,1044 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import test from 'ava'; -import dedent from 'dedent'; -import { Connection, Client } from '@temporalio/client'; -import { TestWorkflowEnvironment } from '@temporalio/testing'; -import { - ClientConfig, - ClientConfigProfile, - ConfigDataSource, - fromTomlConfig, - fromTomlProfile, - loadClientConfig, - loadClientConfigProfile, - loadClientConnectConfig, - toClientOptions, - toTomlConfig, - toTomlProfile, -} from '@temporalio/envconfig'; -import { toPathAndData } from '@temporalio/envconfig/lib/utils'; -import { NativeConnection } from '@temporalio/worker'; -import { encode } from '@temporalio/common/lib/encoding'; - -// Focused TOML fixtures -const TOML_CONFIG_BASE = dedent` - [profile.default] - address = "default-address" - namespace = "default-namespace" - - [profile.custom] - address = "custom-address" - namespace = "custom-namespace" - api_key = "custom-api-key" - [profile.custom.tls] - server_name = "custom-server-name" - [profile.custom.grpc_meta] - custom-header = "custom-value" -`; - -const TOML_CONFIG_STRICT_FAIL = dedent` - [profile.default] - address = "default-address" - unrecognized_field = "should-fail" -`; - -const TOML_CONFIG_TLS_DETAILED = dedent` - [profile.tls_disabled] - address = "localhost:1234" - [profile.tls_disabled.tls] - disabled = true - server_name = "should-be-ignored" - - [profile.tls_with_certs] - address = "localhost:5678" - [profile.tls_with_certs.tls] - server_name = "custom-server" - server_ca_cert_data = "ca-pem-data" - client_cert_data = "client-crt-data" - client_key_data = "client-key-data" -`; - -function withTempFile(content: string, fn: (filepath: string) => void): void { - const tempDir = os.tmpdir(); - const filepath = path.join(tempDir, `temporal-test-config-${Date.now()}-${Math.random()}.toml`); - fs.writeFileSync(filepath, content); - try { - fn(filepath); - } finally { - fs.unlinkSync(filepath); - } -} - -function pathSource(p: string): ConfigDataSource { - return { path: p }; -} -function dataSource(d: Uint8Array | string): ConfigDataSource { - return { data: typeof d === 'string' ? encode(d) : d }; -} - -// ============================================================================= -// 🔧 PROFILE LOADING -// ============================================================================= - -test('Load default profile from file', (t) => { - withTempFile(TOML_CONFIG_BASE, (filepath) => { - const profile = loadClientConfigProfile({ configSource: pathSource(filepath) }); - t.is(profile.address, 'default-address'); - t.is(profile.namespace, 'default-namespace'); - t.is(profile.apiKey, undefined); - t.is(profile.tls, undefined); - t.is(profile.grpcMeta, undefined); - - const { connectionOptions, namespace } = toClientOptions(profile); - t.is(connectionOptions.address, 'default-address'); - t.is(namespace, 'default-namespace'); - t.is(connectionOptions.apiKey, undefined); - t.is(connectionOptions.tls, undefined); - t.is(connectionOptions.metadata, undefined); - }); -}); - -test('Load custom profile from file', (t) => { - withTempFile(TOML_CONFIG_BASE, (filepath) => { - const profile = loadClientConfigProfile({ profile: 'custom', configSource: pathSource(filepath) }); - t.is(profile.address, 'custom-address'); - t.is(profile.namespace, 'custom-namespace'); - t.is(profile.apiKey, 'custom-api-key'); - t.truthy(profile.tls); - t.is(profile.tls?.serverName, 'custom-server-name'); - t.is(profile.grpcMeta?.['custom-header'], 'custom-value'); - - const { connectionOptions, namespace } = toClientOptions(profile); - t.is(connectionOptions.address, 'custom-address'); - t.is(namespace, 'custom-namespace'); - t.is(connectionOptions.apiKey, 'custom-api-key'); - const tls1 = connectionOptions.tls; - if (tls1 && typeof tls1 === 'object') { - t.is(tls1.serverNameOverride, 'custom-server-name'); - } else { - t.fail('expected TLS config object'); - } - t.is(connectionOptions.metadata?.['custom-header'], 'custom-value'); - }); -}); - -test('Load default profile from data', (t) => { - const profile = loadClientConfigProfile({ configSource: dataSource(TOML_CONFIG_BASE) }); - t.is(profile.address, 'default-address'); - t.is(profile.namespace, 'default-namespace'); - t.is(profile.tls, undefined); -}); - -test('Load custom profile from data', (t) => { - const profile = loadClientConfigProfile({ - profile: 'custom', - configSource: dataSource(TOML_CONFIG_BASE), - }); - t.is(profile.address, 'custom-address'); - t.is(profile.namespace, 'custom-namespace'); - t.is(profile.apiKey, 'custom-api-key'); - t.is(profile.tls?.serverName, 'custom-server-name'); -}); - -test('Load profile from data with env overrides', (t) => { - const env = { - TEMPORAL_ADDRESS: 'env-address', - TEMPORAL_NAMESPACE: 'env-namespace', - }; - const profile = loadClientConfigProfile({ - configSource: dataSource(TOML_CONFIG_BASE), - overrideEnvVars: env, - }); - t.is(profile.address, 'env-address'); - t.is(profile.namespace, 'env-namespace'); -}); - -test('Load custom profile with env overrides', (t) => { - withTempFile(TOML_CONFIG_BASE, (filepath) => { - const env = { - TEMPORAL_ADDRESS: 'env-address', - TEMPORAL_NAMESPACE: 'env-namespace', - TEMPORAL_API_KEY: 'env-api-key', - TEMPORAL_TLS: 'true', - TEMPORAL_TLS_SERVER_NAME: 'env-server-name', - TEMPORAL_GRPC_META_CUSTOM_HEADER: 'env-value', - TEMPORAL_GRPC_META_ANOTHER_HEADER: 'another-value', - }; - const profile = loadClientConfigProfile({ - profile: 'custom', - configSource: pathSource(filepath), - overrideEnvVars: env, - }); - t.is(profile.address, 'env-address'); - t.is(profile.namespace, 'env-namespace'); - t.is(profile.apiKey, 'env-api-key'); - t.truthy(profile.tls); - t.is(profile.tls?.serverName, 'env-server-name'); - t.is(profile.grpcMeta?.['custom-header'], 'env-value'); - t.is(profile.grpcMeta?.['another-header'], 'another-value'); - }); -}); - -test('Load profiles with string content', (t) => { - const stringContent = TOML_CONFIG_BASE; - const profile = loadClientConfigProfile({ configSource: dataSource(stringContent) }); - t.is(profile.address, 'default-address'); - t.is(profile.namespace, 'default-namespace'); - - // Test with custom profile from string - const profileCustom = loadClientConfigProfile({ - profile: 'custom', - configSource: dataSource(stringContent), - }); - t.is(profileCustom.address, 'custom-address'); - t.is(profileCustom.apiKey, 'custom-api-key'); -}); - -test('loadClientConnectConfig works with file path and env overrides', (t) => { - withTempFile(TOML_CONFIG_BASE, (filepath) => { - // From file - let cc = loadClientConnectConfig({ configSource: pathSource(filepath) }); - t.is(cc.connectionOptions.address, 'default-address'); - t.is(cc.namespace, 'default-namespace'); - - // With env overrides - cc = loadClientConnectConfig({ - configSource: pathSource(filepath), - overrideEnvVars: { TEMPORAL_NAMESPACE: 'env-namespace-override' }, - }); - t.is(cc.namespace, 'env-namespace-override'); - }); -}); - -// ============================================================================= -// 🌍 ENVIRONMENT VARIABLES -// ============================================================================= - -test('Load profile with grpc metadata env overrides', (t) => { - const toml = dedent` - [profile.default] - address = "addr" - [profile.default.grpc_meta] - original-header = "original-value" - `; - const env = { - TEMPORAL_GRPC_META_NEW_HEADER: 'new-value', - TEMPORAL_GRPC_META_OVERRIDE_HEADER: 'overridden-value', - }; - const profile = loadClientConfigProfile({ - configSource: dataSource(toml), - overrideEnvVars: env, - }); - t.is(profile.grpcMeta?.['original-header'], 'original-value'); - t.is(profile.grpcMeta?.['new-header'], 'new-value'); - t.is(profile.grpcMeta?.['override-header'], 'overridden-value'); -}); - -test('gRPC metadata normalization from TOML', (t) => { - const toml = dedent` - [profile.foo] - address = "addr" - [profile.foo.grpc_meta] - sOme-hEader_key = "some-value" - `; - const conf = loadClientConfig({ configSource: dataSource(toml) }); - const prof = conf.profiles['foo']; - t.truthy(prof); - t.is(prof.grpcMeta?.['some-header-key'], 'some-value'); -}); - -test('gRPC metadata deletion via empty env value', (t) => { - const toml = dedent` - [profile.default] - address = "addr" - [profile.default.grpc_meta] - some-header = "keep" - remove-me = "to-be-removed" - `; - const env = { - TEMPORAL_GRPC_META_REMOVE_ME: '', - TEMPORAL_GRPC_META_NEW_HEADER: 'added', - }; - const prof = loadClientConfigProfile({ configSource: dataSource(toml), overrideEnvVars: env }); - t.is(prof.grpcMeta?.['some-header'], 'keep'); - t.is(prof.grpcMeta?.['new-header'], 'added'); - t.false(Object.prototype.hasOwnProperty.call(prof.grpcMeta, 'remove-me')); -}); - -test('Load profile with disable env flag', (t) => { - withTempFile(TOML_CONFIG_BASE, (filepath) => { - const env = { TEMPORAL_ADDRESS: 'env-address' }; - const profile = loadClientConfigProfile({ - configSource: pathSource(filepath), - overrideEnvVars: env, - disableEnv: true, - }); - t.is(profile.address, 'default-address'); - }); -}); - -// ============================================================================= -// 🎛️ CONTROL FLAGS -// ============================================================================= - -test('Load profile with disabled file flag', (t) => { - const env = { TEMPORAL_ADDRESS: 'env-address', TEMPORAL_NAMESPACE: 'env-namespace' }; - const profile = loadClientConfigProfile({ - configSource: pathSource('/non_existent_file.toml'), - disableFile: true, - overrideEnvVars: env, - }); - t.is(profile.address, 'env-address'); - t.is(profile.namespace, 'env-namespace'); -}); - -test('Load profiles without profile-level env overrides', (t) => { - withTempFile(TOML_CONFIG_BASE, (filepath) => { - const env = { TEMPORAL_ADDRESS: 'should-be-ignored' }; - // loadClientConfig doesn't apply env overrides, so we test it loads correctly - const conf = loadClientConfig({ - configSource: pathSource(filepath), - overrideEnvVars: env, - }); - t.is(conf.profiles['default'].address, 'default-address'); - - // Test that profile-level loading with disableEnv ignores environment - const profile = loadClientConfigProfile({ - configSource: pathSource(filepath), - overrideEnvVars: env, - disableEnv: true, - }); - t.is(profile.address, 'default-address'); - }); -}); - -test('Cannot disable both file and env override flags', (t) => { - const err = t.throws(() => - loadClientConfigProfile({ - configSource: pathSource('/non_existent_file.toml'), - disableFile: true, - disableEnv: true, - }) - ); - t.truthy(err); - t.true(String(err?.message).includes('Cannot disable both')); -}); - -// ============================================================================= -// 📁 CONFIG DISCOVERY -// ============================================================================= - -test('Load all profiles from file', (t) => { - const conf = loadClientConfig({ configSource: dataSource(TOML_CONFIG_BASE) }); - t.truthy(conf.profiles['default']); - t.truthy(conf.profiles['custom']); - t.is(conf.profiles['default'].address, 'default-address'); - t.is(conf.profiles['custom'].apiKey, 'custom-api-key'); -}); - -test('Load all profiles from data', (t) => { - const configData = dedent` - [profile.alpha] - address = "alpha-address" - namespace = "alpha-namespace" - - [profile.beta] - address = "beta-address" - api_key = "beta-key" - `; - const conf = loadClientConfig({ configSource: dataSource(configData) }); - t.truthy(conf.profiles['alpha']); - t.truthy(conf.profiles['beta']); - t.is(conf.profiles['alpha'].address, 'alpha-address'); - t.is(conf.profiles['beta'].apiKey, 'beta-key'); -}); - -test('Load profiles from non-existent file', (t) => { - const conf = loadClientConfig({ - configSource: pathSource('/non_existent_file.toml'), - }); - t.deepEqual(conf.profiles, {}); -}); - -test('Load all profiles with overridden file path', (t) => { - withTempFile(TOML_CONFIG_BASE, (filepath) => { - const conf = loadClientConfig({ overrideEnvVars: { TEMPORAL_CONFIG_FILE: filepath } }); - t.truthy(conf.profiles['default']); - t.is(conf.profiles['default'].address, 'default-address'); - }); -}); - -test('Default profile not found returns empty profile', (t) => { - const toml = dedent` - [profile.existing] - address = "my-address" - `; - const prof = loadClientConfigProfile({ configSource: dataSource(toml) }); - t.is(prof.address, undefined); - t.is(prof.namespace, undefined); - t.is(prof.apiKey, undefined); - t.is(prof.grpcMeta, undefined); - t.is(prof.tls, undefined); - t.deepEqual(prof, {}); -}); - -// ============================================================================= -// 🔐 TLS CONFIGURATION -// ============================================================================= - -test('Load profile with api key (enables TLS)', (t) => { - const toml = dedent` - [profile.default] - address = "my-address" - api_key = "my-api-key" - `; - const profile = loadClientConfigProfile({ configSource: dataSource(toml) }); - t.is(profile.tls, undefined); - t.is(profile.tls?.disabled, undefined); - const { connectionOptions } = toClientOptions(profile); - t.true(connectionOptions.tls); -}); - -test('Load profile with TLS options', (t) => { - const configSource = dataSource(TOML_CONFIG_TLS_DETAILED); - - const profileDisabled = loadClientConfigProfile({ configSource, profile: 'tls_disabled' }); - t.truthy(profileDisabled.tls); - t.true(profileDisabled.tls?.disabled); - t.is(profileDisabled.tls?.serverName, 'should-be-ignored'); - const { connectionOptions: connOptsDisabled } = toClientOptions(profileDisabled); - t.is(connOptsDisabled.tls, false); - - const profileCerts = loadClientConfigProfile({ configSource, profile: 'tls_with_certs' }); - t.truthy(profileCerts.tls); - t.is(profileCerts.tls?.serverName, 'custom-server'); - - const serverCACert = toPathAndData(profileCerts.tls?.serverCACert); - t.deepEqual(serverCACert?.data, encode('ca-pem-data')); - t.is(serverCACert?.path, undefined); - - const clientCert = toPathAndData(profileCerts.tls?.clientCert); - t.deepEqual(clientCert?.data, encode('client-crt-data')); - t.is(clientCert?.path, undefined); - - const clientKey = toPathAndData(profileCerts.tls?.clientKey); - t.deepEqual(clientKey?.data, encode('client-key-data')); - t.is(clientKey?.path, undefined); - - const { connectionOptions: connOptsCerts } = toClientOptions(profileCerts); - const tls2 = connOptsCerts.tls; - if (tls2 && typeof tls2 === 'object') { - t.is(tls2.serverNameOverride, 'custom-server'); - t.deepEqual(tls2.serverRootCACertificate, encode('ca-pem-data')); - t.deepEqual(tls2.clientCertPair?.crt, encode('client-crt-data')); - t.deepEqual(tls2.clientCertPair?.key, encode('client-key-data')); - } else { - t.fail('expected TLS config object'); - } -}); - -test('Load profile with TLS options as file paths', (t) => { - withTempFile('ca-pem-data', (caPath) => { - withTempFile('client-crt-data', (certPath) => { - withTempFile('client-key-data', (keyPath) => { - // Normalize paths to use forward slashes for TOML compatibility (Windows uses backslashes) - const normalizedCaPath = caPath.replace(/\\/g, '/'); - const normalizedCertPath = certPath.replace(/\\/g, '/'); - const normalizedKeyPath = keyPath.replace(/\\/g, '/'); - - const tomlConfig = dedent` - [profile.default] - address = "localhost:5678" - [profile.default.tls] - server_name = "custom-server" - server_ca_cert_path = "${normalizedCaPath}" - client_cert_path = "${normalizedCertPath}" - client_key_path = "${normalizedKeyPath}" - `; - const profile = loadClientConfigProfile({ configSource: dataSource(tomlConfig) }); - t.truthy(profile.tls); - t.is(profile.tls?.serverName, 'custom-server'); - - const serverCACert = toPathAndData(profile.tls?.serverCACert); - t.is(serverCACert?.data, undefined); - t.deepEqual(serverCACert?.path, normalizedCaPath); - - const clientCert = toPathAndData(profile.tls?.clientCert); - t.is(clientCert?.data, undefined); - t.deepEqual(clientCert?.path, normalizedCertPath); - - const clientKey = toPathAndData(profile.tls?.clientKey); - t.is(clientKey?.data, undefined); - t.deepEqual(clientKey?.path, normalizedKeyPath); - - const { connectionOptions: connOpts } = toClientOptions(profile); - const tls3 = connOpts.tls; - if (tls3 && typeof tls3 === 'object') { - t.is(tls3.serverNameOverride, 'custom-server'); - t.deepEqual(tls3.serverRootCACertificate, encode('ca-pem-data')); - t.deepEqual(tls3.clientCertPair?.crt, encode('client-crt-data')); - t.deepEqual(tls3.clientCertPair?.key, encode('client-key-data')); - } else { - t.fail('expected TLS config object'); - } - }); - }); - }); -}); - -test('Load profile with conflicting cert source fails', (t) => { - const toml = dedent` - [profile.default] - address = "addr" - [profile.default.tls] - client_cert_path = "some-path" - client_cert_data = "some-data" - `; - const err = t.throws(() => loadClientConfigProfile({ configSource: dataSource(toml) })); - t.truthy(err); - t.true(String(err?.message).includes('Cannot specify both')); -}); - -test('TLS conflict across sources: path in TOML, data in env should error', (t) => { - const toml = dedent` - [profile.default] - address = "addr" - [profile.default.tls] - client_cert_path = "some-path" - `; - const env = { TEMPORAL_TLS_CLIENT_CERT_DATA: 'some-data' }; - const err = t.throws(() => loadClientConfigProfile({ configSource: dataSource(toml), overrideEnvVars: env })); - t.truthy(err); - t.true( - String(err?.message) - .toLowerCase() - .includes('path') - ); -}); - -test('TLS conflict across sources: data in TOML, path in env should error', (t) => { - const toml = dedent` - [profile.default] - address = "addr" - [profile.default.tls] - client_cert_data = "some-data" - `; - const env = { TEMPORAL_TLS_CLIENT_CERT_PATH: 'some-path' }; - const err = t.throws(() => loadClientConfigProfile({ configSource: dataSource(toml), overrideEnvVars: env })); - t.truthy(err); - t.true( - String(err?.message) - .toLowerCase() - .includes('data') - ); -}); - -test('TLS disabled tri-state behavior', (t) => { - // Test 1: disabled=null (unset) with API key -> TLS enabled - const tomlNull = dedent` - [profile.default] - address = "my-address" - api_key = "my-api-key" - [profile.default.tls] - server_name = "my-server" - `; - const profileNull = loadClientConfigProfile({ configSource: dataSource(tomlNull) }); - t.truthy(profileNull.tls); - t.is(profileNull.tls?.disabled, undefined); // disabled is null (unset) - const configNull = toClientOptions(profileNull); - t.truthy(configNull.connectionOptions.tls); // TLS enabled - - // Test 2: disabled=false (explicitly enabled) -> TLS enabled - const tomlFalse = dedent` - [profile.default] - address = "my-address" - [profile.default.tls] - disabled = false - server_name = "my-server" - `; - const profileFalse = loadClientConfigProfile({ configSource: dataSource(tomlFalse) }); - t.truthy(profileFalse.tls); - t.is(profileFalse.tls?.disabled, false); // explicitly disabled=false - const configFalse = toClientOptions(profileFalse); - t.truthy(configFalse.connectionOptions.tls); // TLS enabled - - // Test 3: disabled=true (explicitly disabled) -> TLS disabled even with API key - const tomlTrue = dedent` - [profile.default] - address = "my-address" - api_key = "my-api-key" - [profile.default.tls] - disabled = true - server_name = "should-be-ignored" - `; - const profileTrue = loadClientConfigProfile({ configSource: dataSource(tomlTrue) }); - t.truthy(profileTrue.tls); - t.is(profileTrue.tls?.disabled, true); // explicitly disabled=true - const configTrue = toClientOptions(profileTrue); - t.is(configTrue.connectionOptions.tls, false); // TLS explicitly disabled even with API key -}); - -// ============================================================================= -// 🚫 ERROR HANDLING -// ============================================================================= - -test('Load non-existent profile', (t) => { - withTempFile(TOML_CONFIG_BASE, (filepath) => { - const err = t.throws(() => loadClientConfigProfile({ configSource: pathSource(filepath), profile: 'nonexistent' })); - t.truthy(err); - t.true(String(err?.message).includes("Profile 'nonexistent' not found")); - }); -}); - -test('Load invalid config with strict mode enabled', (t) => { - const toml = dedent` - [unrecognized_table] - foo = "bar" - `; - const err = t.throws(() => loadClientConfig({ configSource: dataSource(toml), configFileStrict: true })); - t.truthy(err); - t.true(String(err?.message).includes('unrecognized_table')); -}); - -test('Load invalid profile with strict mode enabled', (t) => { - withTempFile(TOML_CONFIG_STRICT_FAIL, (filepath) => { - const err = t.throws(() => loadClientConfigProfile({ configSource: pathSource(filepath), configFileStrict: true })); - t.truthy(err); - t.true(String(err?.message).includes('unrecognized_field')); - }); -}); - -test('Load profiles with malformed TOML', (t) => { - const err = t.throws(() => loadClientConfig({ configSource: dataSource('this is not valid toml') })); - t.truthy(err); - t.true( - String(err?.message) - .toLowerCase() - .includes('toml') - ); -}); - -// ============================================================================= -// 🔄 SERIALIZATION -// ============================================================================= - -test('Client config profile to/from TOML round-trip', (t) => { - const profile: ClientConfigProfile = { - address: 'some-address', - namespace: 'some-namespace', - apiKey: 'some-api-key', - tls: { - serverName: 'some-server', - serverCACert: { data: encode('ca') }, - clientCert: { path: '/path/to/client.crt' }, - clientKey: { data: encode('key') }, - }, - grpcMeta: { 'some-header': 'some-value' }, - }; - const tomlProfile = toTomlProfile(profile); - const back = fromTomlProfile(tomlProfile); - t.is(back.address, 'some-address'); - t.is(back.namespace, 'some-namespace'); - t.is(back.apiKey, 'some-api-key'); - t.truthy(back.tls); - t.is(back.tls?.serverName, 'some-server'); - - const serverCACert = toPathAndData(back.tls?.serverCACert); - t.deepEqual(serverCACert?.data, encode('ca')); - t.is(serverCACert?.path, undefined); - - const clientCert = toPathAndData(back.tls?.clientCert); - t.is(clientCert?.data, undefined); - t.deepEqual(clientCert?.path, '/path/to/client.crt'); - - const clientKey = toPathAndData(back.tls?.clientKey); - t.deepEqual(clientKey?.data, encode('key')); - t.is(clientKey?.path, undefined); - - t.is(back.grpcMeta?.['some-header'], 'some-value'); -}); - -test('Client config to/from TOML round-trip', (t) => { - const conf: ClientConfig = { - profiles: { - default: { address: 'addr', namespace: 'ns', grpcMeta: {} }, - custom: { address: 'addr2', apiKey: 'key2', grpcMeta: { h: 'v' } }, - }, - }; - const tomlConfig = toTomlConfig(conf); - const back = fromTomlConfig(tomlConfig); - t.is(back.profiles['default'].address, 'addr'); - t.is(back.profiles['default'].namespace, 'ns'); - t.is(back.profiles['custom'].address, 'addr2'); - t.is(back.profiles['custom'].apiKey, 'key2'); - t.is(back.profiles['custom'].grpcMeta?.['h'], 'v'); -}); - -// ============================================================================= -// 🎯 INTEGRATION/E2E -// ============================================================================= - -test('Create client with default profile, no config', async (t) => { - // Start a local test server - const env = await TestWorkflowEnvironment.createLocal(); - - try { - const { address } = env.connection.options; - // Load config via envconfig - const { connectionOptions, namespace } = loadClientConnectConfig(); - // Override address with test env address. - connectionOptions.address = address; - - // Create connection and client with loaded config - const connection = await Connection.connect(connectionOptions); - const client = new Client({ - connection, - namespace, - }); - - // If we got here without throwing, the connection is working - t.truthy(client); - t.truthy(client.connection); - - // Clean up - await connection.close(); - } finally { - await env.teardown(); - } -}); - -test('Create client from default profile', async (t) => { - // Start a local test server - const env = await TestWorkflowEnvironment.createLocal(); - - try { - const { address } = env.connection.options; - - // Create TOML config with test server address - const toml = dedent` - [profile.default] - address = "${address}" - namespace = "default" - `; - - // Load config via envconfig - const { connectionOptions, namespace } = loadClientConnectConfig({ - configSource: dataSource(toml), - }); - - // Verify loaded values - t.is(connectionOptions.address, address); - t.is(namespace, 'default'); - - // Create connection and client with loaded config - const connection = await Connection.connect(connectionOptions); - const client = new Client({ - connection, - namespace: namespace || 'default', - }); - - // If we got here without throwing, the connection is working - t.truthy(client); - t.truthy(client.connection); - - // Clean up - await connection.close(); - } finally { - await env.teardown(); - } -}); - -test('Create client with NativeConnection from default profile', async (t) => { - // Start a local test server - const env = await TestWorkflowEnvironment.createLocal(); - - try { - const { address } = env.connection.options; - - // Create TOML config with test server address - const toml = dedent` - [profile.default] - address = "${address}" - namespace = "default" - `; - - // Load config via envconfig - const { connectionOptions, namespace } = loadClientConnectConfig({ - configSource: dataSource(toml), - }); - - // Verify loaded values - t.is(connectionOptions.address, address); - t.is(namespace, 'default'); - - // Create connection and client with loaded config - const connection = await NativeConnection.connect(connectionOptions); - const client = new Client({ - connection, - namespace: namespace || 'default', - }); - - // If we got here without throwing, the connection is working - t.truthy(client); - t.truthy(client.connection); - - // Clean up - await connection.close(); - } finally { - await env.teardown(); - } -}); - -test('Create client from custom profile', async (t) => { - const env = await TestWorkflowEnvironment.createLocal(); - - try { - const { address } = env.connection.options; - - // Create basic development profile configuration - const toml = dedent` - [profile.development] - address = "${address}" - namespace = "development-namespace" - `; - - // Load profile and create connection - const profile = loadClientConfigProfile({ - profile: 'development', - configSource: dataSource(toml), - }); - - t.is(profile.address, address); - t.is(profile.namespace, 'development-namespace'); - t.is(profile.apiKey, undefined); - t.is(profile.tls, undefined); - - const { connectionOptions, namespace } = toClientOptions(profile); - const connection = await Connection.connect(connectionOptions); - const client = new Client({ connection, namespace: namespace || 'default' }); - - // Verify the client can perform basic operations - t.truthy(client); - t.truthy(client.connection); - t.is(client.options.namespace, 'development-namespace'); - - await connection.close(); - } finally { - await env.teardown(); - } -}); - -test('Create client from custom profile with TLS options', async (t) => { - const env = await TestWorkflowEnvironment.createLocal(); - - try { - const { address } = env.connection.options; - - // Create production profile with API key (auto-enables TLS but disabled for local test) - const toml = dedent` - [profile.production] - address = "${address}" - namespace = "production-namespace" - api_key = "prod-api-key-12345" - [profile.production.tls] - disabled = true - `; - - // Load profile and verify TLS/API key handling - const profile = loadClientConfigProfile({ - profile: 'production', - configSource: dataSource(toml), - }); - - t.is(profile.address, address); - t.is(profile.namespace, 'production-namespace'); - t.is(profile.apiKey, 'prod-api-key-12345'); - t.truthy(profile.tls); - t.true(!!profile.tls?.disabled); - - const { connectionOptions, namespace } = toClientOptions(profile); - - // Verify API key is present but TLS is disabled for local testing - t.is(connectionOptions.apiKey, 'prod-api-key-12345'); - t.is(connectionOptions.tls, false); // disabled = true results in tls being false - - const connection = await Connection.connect(connectionOptions); - const client = new Client({ connection, namespace: namespace || 'default' }); - - t.truthy(client); - t.is(client.options.namespace, 'production-namespace'); - - await connection.close(); - } finally { - await env.teardown(); - } -}); - -test('Create client from default profile with env overrides', async (t) => { - const env = await TestWorkflowEnvironment.createLocal(); - - try { - const { address } = env.connection.options; - - // Base config that will be overridden by environment - const toml = dedent` - [profile.default] - address = "original-address" - namespace = "original-namespace" - `; - - // Environment overrides - const envOverrides = { - TEMPORAL_ADDRESS: address, // Override with test server address - TEMPORAL_NAMESPACE: 'env-override-namespace', - TEMPORAL_GRPC_META_CUSTOM_HEADER: 'env-header-value', - }; - - // Load profile with environment overrides - const profile = loadClientConfigProfile({ - configSource: dataSource(toml), - overrideEnvVars: envOverrides, - }); - - // Verify environment variables took precedence - t.is(profile.address, address); - t.is(profile.namespace, 'env-override-namespace'); - t.is(profile.grpcMeta?.['custom-header'], 'env-header-value'); - - const { connectionOptions, namespace } = toClientOptions(profile); - const connection = await Connection.connect(connectionOptions); - const client = new Client({ connection, namespace: namespace || 'default' }); - - // Verify client uses overridden values - t.truthy(client); - t.is(client.options.namespace, 'env-override-namespace'); - t.is(connectionOptions.metadata?.['custom-header'], 'env-header-value'); - - await connection.close(); - } finally { - await env.teardown(); - } -}); - -test('Create clients from multi-profile config', async (t) => { - const env = await TestWorkflowEnvironment.createLocal(); - - try { - const { address } = env.connection.options; - - // Multi-profile configuration - const toml = dedent` - [profile.service-a] - address = "${address}" - namespace = "service-a-namespace" - [profile.service-a.grpc_meta] - service-name = "service-a" - - [profile.service-b] - address = "${address}" - namespace = "service-b-namespace" - [profile.service-b.grpc_meta] - service-name = "service-b" - priority = "high" - `; - - // Load different profiles and create separate clients - const profileA = loadClientConfigProfile({ - profile: 'service-a', - configSource: dataSource(toml), - }); - - const profileB = loadClientConfigProfile({ - profile: 'service-b', - configSource: dataSource(toml), - }); - - // Verify profiles are distinct - t.is(profileA.namespace, 'service-a-namespace'); - t.is(profileA.grpcMeta?.['service-name'], 'service-a'); - t.false('priority' in (profileA.grpcMeta ?? {})); - - t.is(profileB.namespace, 'service-b-namespace'); - t.is(profileB.grpcMeta?.['service-name'], 'service-b'); - t.is(profileB.grpcMeta?.['priority'], 'high'); - - // Create separate client connections - const configA = toClientOptions(profileA); - const configB = toClientOptions(profileB); - - const connectionA = await Connection.connect(configA.connectionOptions); - const connectionB = await Connection.connect(configB.connectionOptions); - - const clientA = new Client({ connection: connectionA, namespace: configA.namespace || 'default' }); - const clientB = new Client({ connection: connectionB, namespace: configB.namespace || 'default' }); - - // Verify both clients work with their respective configurations - t.truthy(clientA); - t.truthy(clientB); - t.is(clientA.options.namespace, 'service-a-namespace'); - t.is(clientB.options.namespace, 'service-b-namespace'); - - // Verify metadata is correctly set for each connection - t.is(configA.connectionOptions.metadata?.['service-name'], 'service-a'); - t.is(configB.connectionOptions.metadata?.['service-name'], 'service-b'); - t.is(configB.connectionOptions.metadata?.['priority'], 'high'); - - await connectionA.close(); - await connectionB.close(); - } finally { - await env.teardown(); - } -}); - -test('Comprehensive E2E validation test', (t) => { - // Test comprehensive end-to-end configuration loading with all features - const tomlContent = dedent` - [profile.production] - address = "prod.temporal.com:443" - namespace = "production-ns" - api_key = "prod-api-key" - - [profile.production.tls] - server_name = "prod.temporal.com" - server_ca_cert_data = "prod-ca-cert" - - [profile.production.grpc_meta] - authorization = "Bearer prod-token" - "x-custom-header" = "prod-value" - `; - - const envOverrides = { - TEMPORAL_GRPC_META_X_ENVIRONMENT: 'production', - TEMPORAL_TLS_SERVER_NAME: 'override.temporal.com', - }; - - const { connectionOptions, namespace } = loadClientConnectConfig({ - profile: 'production', - configSource: dataSource(tomlContent), - overrideEnvVars: envOverrides, - }); - - // Validate all configuration aspects - t.is(connectionOptions.address, 'prod.temporal.com:443'); - t.is(namespace, 'production-ns'); - t.is(connectionOptions.apiKey, 'prod-api-key'); - - // TLS configuration (API key should auto-enable TLS) - t.truthy(connectionOptions.tls); - const tls = connectionOptions.tls; - if (tls && typeof tls === 'object') { - t.is(tls.serverNameOverride, 'override.temporal.com'); // Env override - t.deepEqual(tls.serverRootCACertificate, encode('prod-ca-cert')); - } else { - t.fail('expected TLS config object'); - } - - // gRPC metadata with normalization and env overrides - t.truthy(connectionOptions.metadata); - const metadata = connectionOptions.metadata!; - t.is(metadata['authorization'], 'Bearer prod-token'); - t.is(metadata['x-custom-header'], 'prod-value'); - t.is(metadata['x-environment'], 'production'); // From env -}); diff --git a/packages/test/src/test-ephemeral-server.ts b/packages/test/src/test-ephemeral-server.ts deleted file mode 100644 index a42b70461..000000000 --- a/packages/test/src/test-ephemeral-server.ts +++ /dev/null @@ -1,160 +0,0 @@ -import fs from 'fs/promises'; -import anyTest, { ExecutionContext, TestFn } from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { bundleWorkflowCode, WorkflowBundle } from '@temporalio/worker'; -import { Connection } from '@temporalio/client'; -import { TestWorkflowEnvironment as RealTestWorkflowEnvironment } from '@temporalio/testing'; -import { Worker, TestWorkflowEnvironment, testTimeSkipping as anyTestTimeSkipping, getRandomPort } from './helpers'; - -interface Context { - bundle: WorkflowBundle; - taskQueue: string; -} - -const test = anyTest as TestFn; -const testTimeSkipping = anyTestTimeSkipping as TestFn; - -test.before(async (t) => { - t.context.bundle = await bundleWorkflowCode({ workflowsPath: require.resolve('./workflows') }); -}); - -test.beforeEach(async (t) => { - t.context.taskQueue = t.title.replace(/ /g, '_'); -}); - -async function runSimpleWorkflow(t: ExecutionContext, testEnv: TestWorkflowEnvironment) { - try { - const { taskQueue } = t.context; - const { client, nativeConnection, namespace } = testEnv; - const worker = await Worker.create({ - connection: nativeConnection, - namespace, - taskQueue, - workflowBundle: t.context.bundle, - }); - await worker.runUntil( - client.workflow.execute('successString', { - workflowId: uuid4(), - taskQueue, - }) - ); - } finally { - await testEnv.teardown(); - } - t.pass(); -} - -testTimeSkipping('TestEnvironment sets up test server and is able to run a single workflow', async (t) => { - const testEnv = await TestWorkflowEnvironment.createTimeSkipping(); - await runSimpleWorkflow(t, testEnv); -}); - -test('TestEnvironment sets up dev server and is able to run a single workflow', async (t) => { - const testEnv = await TestWorkflowEnvironment.createLocal(); - await runSimpleWorkflow(t, testEnv); -}); - -test.todo('TestEnvironment sets up test server with extra args'); -test.todo('TestEnvironment sets up test server with specified port'); -test.todo('TestEnvironment sets up test server with latest version'); -test.todo('TestEnvironment sets up test server from executable path'); - -test.todo('TestEnvironment sets up dev server with extra args'); -test.todo('TestEnvironment sets up dev server with latest version'); -test.todo('TestEnvironment sets up dev server from executable path'); -test.todo('TestEnvironment sets up dev server with custom log level'); -test.todo('TestEnvironment sets up dev server with custom namespace, IP and UI'); - -test('TestEnvironment sets up dev server with db filename', async (t) => { - const dbFilename = `temporal-db-${uuid4()}.sqlite`; - try { - const testEnv = await TestWorkflowEnvironment.createLocal({ - server: { - dbFilename, - }, - }); - t.truthy(await fs.stat(dbFilename).catch(() => false), 'DB file exists'); - await testEnv.teardown(); - } finally { - await fs.unlink(dbFilename).catch(() => { - /* ignore errors */ - }); - } -}); - -test('TestEnvironment sets up dev server with custom port and ui', async (t) => { - // FIXME: We'd really need to assert that the UI port is not being used by another process. - let port = await getRandomPort(); - if (port > 65535 - 1000) port = 65535 - 1000; - - const testEnv = await TestWorkflowEnvironment.createLocal({ - server: { - ip: '127.0.0.1', - port, - ui: true, - }, - }); - - try { - // Check that we can connect to the server using the connection provided by the testEnv. - await testEnv.connection.ensureConnected(); - - // Check that we can connect to the server _on the expected port_. - const connection = await Connection.connect({ - address: `127.0.0.1:${port}`, - connectTimeout: 500, - }); - await connection.ensureConnected(); - - // With UI enabled but no ui port specified, the UI should be listening on port + 1000. - await fetch(`http://127.0.0.1:${port + 1000}/namespaces`); - - t.pass(); - } finally { - await testEnv.teardown(); - } -}); - -test('TestEnvironment sets up dev server with custom ui port', async (t) => { - const port = await getRandomPort(); - const testEnv = await RealTestWorkflowEnvironment.createLocal({ - server: { - uiPort: port, - }, - }); - try { - await fetch(`http://127.0.0.1:${port}/namespaces`); - t.pass(); - } finally { - await testEnv.teardown(); - } -}); - -test("TestEnvironment doesn't hang on fail to download", async (t) => { - try { - // Our internal TestWorkflowEnvironment helper may override the executable version - // if TESTS_CLI_VERSION is set, which would cause this test to fail. To avoid that, - // we use the RealTestWorkflowEnvironment directly. - await RealTestWorkflowEnvironment.createLocal({ - server: { - executable: { - type: 'cached-download', - version: '999.999.999', - }, - }, - }); - } catch (_e) { - t.pass(); - } -}); - -test('TestEnvironment.createLocal correctly populates address', async (t) => { - const testEnv = await RealTestWorkflowEnvironment.createLocal(); - t.teardown(() => testEnv.teardown()); - await t.notThrowsAsync(async () => { - await Connection.connect({ - address: testEnv.address, - connectTimeout: 500, - }); - }, 'should be able to connect to test server'); -}); diff --git a/packages/test/src/test-failure-converter.ts b/packages/test/src/test-failure-converter.ts deleted file mode 100644 index a5f614101..000000000 --- a/packages/test/src/test-failure-converter.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { randomUUID } from 'crypto'; -import { - DefaultFailureConverter, - ApplicationFailure, - DataConverter, - DefaultEncodedFailureAttributes, - TemporalFailure, -} from '@temporalio/common'; -import { proxyActivities } from '@temporalio/workflow'; -import { WorkflowFailedError } from '@temporalio/client'; -import { decodeFromPayloadsAtIndex } from '@temporalio/common/lib/internal-non-workflow'; -import { test, bundlerOptions, ByteSkewerPayloadCodec, Worker, TestWorkflowEnvironment } from './helpers'; - -export const failureConverter = new DefaultFailureConverter({ encodeCommonAttributes: true }); - -class Activities { - public raise = async () => { - throw ApplicationFailure.nonRetryable('error message'); - }; -} - -export async function workflow(): Promise { - const activities = proxyActivities({ startToCloseTimeout: '1m' }); - await activities.raise(); -} - -test('Client and Worker use provided failureConverter', async (t) => { - const dataConverter: DataConverter = { - // Use a payload codec to verify that it's being utilized to encode / decode the failure - payloadCodecs: [new ByteSkewerPayloadCodec()], - failureConverterPath: __filename, - }; - const env = await TestWorkflowEnvironment.createLocal({ client: { dataConverter } }); - try { - const taskQueue = 'test'; - const worker = await Worker.create({ - connection: env.nativeConnection, - activities: new Activities(), - workflowsPath: __filename, - taskQueue, - dataConverter, - bundlerOptions, - }); - - // Run the workflow, expect error with message and stack trace - const handle = await env.client.workflow.start(workflow, { taskQueue, workflowId: randomUUID() }); - const err = (await worker.runUntil(t.throwsAsync(handle.result()))) as WorkflowFailedError; - t.is(err.cause?.message, 'Activity task failed'); - if (!(err.cause instanceof TemporalFailure)) { - t.fail('expected error cause to be a TemporalFailure'); - return; - } - t.is(err.cause?.cause?.message, 'error message'); - t.true(err.cause?.cause?.stack?.includes('ApplicationFailure: error message\n')); - - // Verify failure was indeed encoded - const { events } = await handle.fetchHistory(); - const { failure } = events?.[events.length - 1].workflowExecutionFailedEventAttributes ?? {}; - { - const payload = failure?.encodedAttributes; - const attrs = await decodeFromPayloadsAtIndex( - env.client.options.loadedDataConverter, - 0, - payload ? [payload] : undefined - ); - t.is(failure?.message, 'Encoded failure'); - t.is(failure?.stackTrace, ''); - t.is(attrs.message, 'Activity task failed'); - t.is(attrs.stack_trace, ''); - } - { - const payload = failure?.cause?.encodedAttributes; - const attrs = await decodeFromPayloadsAtIndex( - env.client.options.loadedDataConverter, - 0, - payload ? [payload] : undefined - ); - t.is(failure?.cause?.message, 'Encoded failure'); - t.is(failure?.stackTrace, ''); - t.is(attrs.message, 'error message'); - t.true(attrs.stack_trace.includes('ApplicationFailure: error message\n')); - } - } finally { - await env.teardown(); - } -}); diff --git a/packages/test/src/test-integration-split-one.ts b/packages/test/src/test-integration-split-one.ts deleted file mode 100644 index 68efcdcdf..000000000 --- a/packages/test/src/test-integration-split-one.ts +++ /dev/null @@ -1,782 +0,0 @@ -/* eslint @typescript-eslint/no-non-null-assertion: 0 */ -import asyncRetry from 'async-retry'; -import { v4 as uuid4 } from 'uuid'; -import dedent from 'dedent'; -import * as iface from '@temporalio/proto'; -import { - ActivityFailure, - ChildWorkflowFailure, - QueryNotRegisteredError, - WorkflowFailedError, -} from '@temporalio/client'; -import { - ApplicationFailure, - CancelledFailure, - RetryState, - SearchAttributes, - TerminatedFailure, - TimeoutFailure, - TimeoutType, - WorkflowExecution, - WorkflowExecutionAlreadyStartedError, -} from '@temporalio/common'; -import { tsToMs } from '@temporalio/common/lib/time'; -import pkg from '@temporalio/worker/lib/pkg'; -import { UnsafeWorkflowInfo, WorkflowInfo } from '@temporalio/workflow/lib/interfaces'; - -import { - CancellationScope, - defineQuery, - executeChild, - proxyActivities, - setHandler, - sleep, - startChild, - workflowInfo, -} from '@temporalio/workflow'; -import { configurableHelpers, createTestWorkflowBundle } from './helpers-integration'; -import * as activities from './activities'; -import { cleanOptionalStackTrace, compareStackTrace, u8, Worker } from './helpers'; -import { configMacro, makeTestFn } from './helpers-integration-multi-codec'; -import * as workflows from './workflows'; - -// Note: re-export shared workflows (or long workflows) -// - review the files where these workflows are shared -export * from './workflows'; - -const { EVENT_TYPE_TIMER_STARTED, EVENT_TYPE_TIMER_FIRED, EVENT_TYPE_TIMER_CANCELED } = - iface.temporal.api.enums.v1.EventType; - -const timerEventTypes = new Set([EVENT_TYPE_TIMER_STARTED, EVENT_TYPE_TIMER_FIRED, EVENT_TYPE_TIMER_CANCELED]); -const CHANGE_MARKER_NAME = 'core_patch'; - -const test = makeTestFn(() => createTestWorkflowBundle({ workflowsPath: __filename })); -test.macro(configMacro); - -// FIXME: Unless we add .serial() here, ava tries to start all async tests in parallel, which -// is ok in most environments, but has been causing flakyness in CI, especially on Windows. -// We can probably avoid this by using larger runners, and there is some opportunity for -// optimization here, but for now, let's just run these tests serially. -test.serial('Workflow not found results in task retry', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handle = await client.workflow.start('not-found', { - taskQueue, - workflowId: uuid4(), - }); - - await worker.runUntil(async () => { - await asyncRetry( - async () => { - const history = await handle.fetchHistory(); - if ( - !history?.events?.some( - ({ workflowTaskFailedEventAttributes }) => - workflowTaskFailedEventAttributes?.failure?.message === - "Failed to initialize workflow of type 'not-found': no such function is exported by the workflow bundle" - ) - ) { - throw new Error('Cannot find workflow task failed event'); - } - }, - { - retries: 60, - maxTimeout: 1000, - } - ); - }); - - t.pass(); -}); - -test.serial('args-and-return', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const res = await worker.runUntil( - executeWorkflow(workflows.argsAndReturn, { - args: ['Hello', undefined, u8('world!')], - }) - ); - t.is(res, 'Hello, world!'); -}); - -export async function urlEcho(url: string): Promise { - const parsedURL = new URL(url); - const searchParams = new URLSearchParams({ counter: '1' }); - parsedURL.search = searchParams.toString(); - return parsedURL.toString(); -} - -test.serial('url-whatwg', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const res = await worker.runUntil( - executeWorkflow(urlEcho, { - args: ['http://foo.com'], - }) - ); - t.is(res, 'http://foo.com/?counter=1'); -}); - -test.serial('cancel-fake-progress', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - - const worker = await createWorkerWithDefaults(t, { - activities, - }); - await worker.runUntil(executeWorkflow(workflows.cancelFakeProgress)); - t.pass(); -}); - -export async function activityFailure(useApplicationFailure: boolean): Promise { - const { throwAnError } = proxyActivities({ - startToCloseTimeout: '5s', - retry: { initialInterval: '1s', maximumAttempts: 1 }, - }); - if (useApplicationFailure) { - await throwAnError(true, 'Fail me'); - } else { - await throwAnError(false, 'Fail me'); - } -} - -test.serial('activity-failure with Error', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const err: WorkflowFailedError | undefined = await t.throwsAsync( - worker.runUntil( - executeWorkflow(activityFailure, { - args: [false], - }) - ), - { - instanceOf: WorkflowFailedError, - } - ); - t.is(err?.message, 'Workflow execution failed'); - if (!(err?.cause instanceof ActivityFailure)) { - t.fail('Expected err.cause to be an instance of ActivityFailure'); - return; - } - if (!(err.cause.cause instanceof ApplicationFailure)) { - t.fail('Expected err.cause.cause to be an instance of ApplicationFailure'); - return; - } - t.is(err.cause.cause.message, 'Fail me'); - t.is( - cleanOptionalStackTrace(err.cause.cause.stack), - dedent` - Error: Fail me - at throwAnError (test/src/activities/index.ts) - ` - ); -}); - -test.serial('activity-failure with ApplicationFailure', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const err: WorkflowFailedError | undefined = await t.throwsAsync( - worker.runUntil( - executeWorkflow(activityFailure, { - args: [true], - }) - ), - { - instanceOf: WorkflowFailedError, - } - ); - t.is(err?.message, 'Workflow execution failed'); - if (!(err?.cause instanceof ActivityFailure)) { - t.fail('Expected err.cause to be an instance of ActivityFailure'); - return; - } - if (!(err.cause.cause instanceof ApplicationFailure)) { - t.fail('Expected err.cause.cause to be an instance of ApplicationFailure'); - return; - } - t.is(err.cause.cause.message, 'Fail me'); - t.is(err.cause.cause.type, 'Error'); - t.deepEqual(err.cause.cause.details, ['details', 123, false]); - compareStackTrace( - t, - cleanOptionalStackTrace(err.cause.cause.stack)!, - dedent` - ApplicationFailure: Fail me - at $CLASS.nonRetryable (common/src/failure.ts) - at throwAnError (test/src/activities/index.ts) - ` - ); -}); - -export async function childWorkflowInvoke(): Promise<{ - workflowId: string; - runId: string; - execResult: string; - result: string; -}> { - const child = await startChild(workflows.successString, {}); - const execResult = await executeChild(workflows.successString, {}); - return { workflowId: child.workflowId, runId: child.firstExecutionRunId, result: await child.result(), execResult }; -} - -test.serial('child-workflow-invoke', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(childWorkflowInvoke); - const { workflowId, runId, execResult, result } = await worker.runUntil(handle.result()); - t.is(execResult, 'success'); - t.is(result, 'success'); - const client = env.client; - const child = client.workflow.getHandle(workflowId, runId); - t.is(await child.result(), 'success'); -}); - -export async function childWorkflowFailure(): Promise { - await executeChild(workflows.throwAsync); -} - -test.serial('child-workflow-failure', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil(async () => { - const err: WorkflowFailedError | undefined = await t.throwsAsync(executeWorkflow(childWorkflowFailure), { - instanceOf: WorkflowFailedError, - }); - - if (!(err?.cause instanceof ChildWorkflowFailure)) { - return t.fail('Expected err.cause to be an instance of ChildWorkflowFailure'); - } - if (!(err.cause.cause instanceof ApplicationFailure)) { - return t.fail('Expected err.cause.cause to be an instance of ApplicationFailure'); - } - t.is(err.cause.cause.message, 'failure'); - compareStackTrace( - t, - cleanOptionalStackTrace(err.cause.cause.stack)!, - dedent` - ApplicationFailure: failure - at $CLASS.nonRetryable (common/src/failure.ts) - at throwAsync (test/src/workflows/throw-async.ts) - ` - ); - }); -}); - -const childExecutionQuery = defineQuery('childExecution'); -export async function childWorkflowTermination(): Promise { - let workflowExecution: WorkflowExecution | undefined = undefined; - setHandler(childExecutionQuery, () => workflowExecution); - - const child = await startChild(workflows.unblockOrCancel, {}); - workflowExecution = { workflowId: child.workflowId, runId: child.firstExecutionRunId }; - await child.result(); -} - -test.serial('child-workflow-termination', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(childWorkflowTermination); - const client = env.client; - - let childExecution: WorkflowExecution | undefined = undefined; - - await worker.runUntil(async () => { - const err: WorkflowFailedError | undefined = await t.throwsAsync( - async () => { - while (childExecution === undefined) { - childExecution = await handle.query(childExecutionQuery); - } - const child = client.workflow.getHandle(childExecution.workflowId!, childExecution.runId!); - await child.terminate(); - await handle.result(); - }, - { - instanceOf: WorkflowFailedError, - } - ); - - if (!(err?.cause instanceof ChildWorkflowFailure)) { - return t.fail('Expected err.cause to be an instance of ChildWorkflowFailure'); - } - t.is(err.cause.retryState, RetryState.NON_RETRYABLE_FAILURE); - if (!(err.cause.cause instanceof TerminatedFailure)) { - return t.fail('Expected err.cause.cause to be an instance of TerminatedFailure'); - } - }); -}); - -export async function childWorkflowTimeout(): Promise { - await executeChild(workflows.unblockOrCancel, { - workflowExecutionTimeout: '10ms', - retry: { maximumAttempts: 1 }, - }); -} - -test.serial('child-workflow-timeout', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const err: WorkflowFailedError | undefined = await t.throwsAsync( - worker.runUntil(executeWorkflow(childWorkflowTimeout)), - { - instanceOf: WorkflowFailedError, - } - ); - - if (!(err?.cause instanceof ChildWorkflowFailure)) { - return t.fail('Expected err.cause to be an instance of ChildWorkflowFailure'); - } - t.is(err.cause.retryState, RetryState.TIMEOUT); - if (!(err.cause.cause instanceof TimeoutFailure)) { - return t.fail('Expected err.cause.cause to be an instance of TimeoutFailure'); - } - t.is(err.cause.cause.timeoutType, TimeoutType.START_TO_CLOSE); -}); - -export async function childWorkflowStartFail(): Promise { - const child = await startChild(workflows.successString, { - workflowIdReusePolicy: 'REJECT_DUPLICATE', - }); - await child.result(); - - try { - await startChild(workflows.successString, { - taskQueue: 'test', - workflowId: child.workflowId, // duplicate - workflowIdReusePolicy: 'REJECT_DUPLICATE', - }); - throw new Error('Managed to start a Workflow with duplicate workflowId'); - } catch (err) { - if (!(err instanceof WorkflowExecutionAlreadyStartedError)) { - throw new Error(`Got invalid error: ${err}`); - } - } -} - -test.serial('child-workflow-start-fail', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil(executeWorkflow(childWorkflowStartFail)); - // Assertions in workflow code - t.pass(); -}); - -test.serial('child-workflow-cancel', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil(executeWorkflow(workflows.childWorkflowCancel)); - // Assertions in workflow code - t.pass(); -}); - -test.serial('child-workflow-signals', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil(executeWorkflow(workflows.childWorkflowSignals)); - // Assertions in workflow code - t.pass(); -}); - -test.serial('query not found', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.unblockOrCancel); - await worker.runUntil(async () => { - await handle.signal(workflows.unblockSignal); - await handle.result(); - await t.throwsAsync(handle.query('not found'), { - instanceOf: QueryNotRegisteredError, - message: - 'Workflow did not register a handler for not found. Registered queries: [__stack_trace __enhanced_stack_trace __temporal_workflow_metadata isBlocked]', - }); - }); -}); - -test.serial('query and unblock', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.unblockOrCancel); - await worker.runUntil(async () => { - t.true(await handle.query(workflows.isBlockedQuery)); - await handle.signal(workflows.unblockSignal); - await handle.result(); - t.false(await handle.query(workflows.isBlockedQuery)); - }); -}); - -test.serial('interrupt-signal', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.interruptableWorkflow); - await worker.runUntil(async () => { - await handle.signal(workflows.interruptSignal, 'just because'); - const err: WorkflowFailedError | undefined = await t.throwsAsync(handle.result(), { - instanceOf: WorkflowFailedError, - }); - if (!(err?.cause instanceof ApplicationFailure)) { - return t.fail('Expected err.cause to be an instance of ApplicationFailure'); - } - t.is(err.cause.message, 'just because'); - }); -}); - -test.serial('fail-signal', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.failSignalWorkflow); - await worker.runUntil(async () => { - await handle.signal(workflows.failSignal); - const err: WorkflowFailedError | undefined = await t.throwsAsync(handle.result(), { - instanceOf: WorkflowFailedError, - }); - if (!(err?.cause instanceof ApplicationFailure)) { - return t.fail('Expected err.cause to be an instance of ApplicationFailure'); - } - t.is(err.cause.message, 'Signal failed'); - }); -}); - -test.serial('async-fail-signal', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.asyncFailSignalWorkflow); - await handle.signal(workflows.failSignal); - await worker.runUntil(async () => { - const err: WorkflowFailedError | undefined = await t.throwsAsync(handle.result(), { - instanceOf: WorkflowFailedError, - }); - if (!(err?.cause instanceof ApplicationFailure)) { - return t.fail('Expected err.cause to be an instance of ApplicationFailure'); - } - t.is(err.cause.message, 'Signal failed'); - }); -}); - -test.serial('http', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const res = await worker.runUntil(executeWorkflow(workflows.http)); - t.deepEqual(res, await activities.httpGet('https://temporal.io')); -}); - -test.serial('sleep', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.sleeper); - const res = await worker.runUntil(handle.result()); - t.is(res, undefined); - const history = await handle.fetchHistory(); - const timerEvents = history.events!.filter(({ eventType }) => timerEventTypes.has(eventType!)); - t.is(timerEvents.length, 2); - t.is(timerEvents[0].timerStartedEventAttributes!.timerId, '1'); - t.is(tsToMs(timerEvents[0].timerStartedEventAttributes!.startToFireTimeout), 100); - t.is(timerEvents[1].timerFiredEventAttributes!.timerId, '1'); -}); - -test.serial('cancel-timer-immediately', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.cancelTimer); - const res = await worker.runUntil(handle.result()); - t.is(res, undefined); - const history = await handle.fetchHistory(); - const timerEvents = history.events!.filter(({ eventType }) => timerEventTypes.has(eventType!)); - // Timer is cancelled before it is scheduled - t.is(timerEvents.length, 0); -}); - -export async function cancelTimerWithDelay(): Promise { - const scope = new CancellationScope(); - const promise = scope.run(() => sleep(10000)); - await sleep(1).then(() => scope.cancel()); - try { - await promise; - } catch (e) { - if (e instanceof CancelledFailure) { - console.log('Timer cancelled 👍'); - } else { - throw e; - } - } -} - -test.serial('cancel-timer-with-delay', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(cancelTimerWithDelay); - const res = await worker.runUntil(handle.result()); - t.is(res, undefined); - const history = await handle.fetchHistory(); - const timerEvents = history.events!.filter(({ eventType }) => timerEventTypes.has(eventType!)); - t.is(timerEvents.length, 4); - t.is(timerEvents[0].timerStartedEventAttributes!.timerId, '1'); - t.is(tsToMs(timerEvents[0].timerStartedEventAttributes!.startToFireTimeout), 10000); - t.is(timerEvents[1].timerStartedEventAttributes!.timerId, '2'); - t.is(tsToMs(timerEvents[1].timerStartedEventAttributes!.startToFireTimeout), 1); - t.is(timerEvents[2].timerFiredEventAttributes!.timerId, '2'); - t.is(timerEvents[3].timerCanceledEventAttributes!.timerId, '1'); -}); - -test.serial('patched', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.patchedWorkflow); - const res = await worker.runUntil(handle.result()); - t.is(res, undefined); - const history = await handle.fetchHistory(); - const hasChangeEvents = history.events!.filter( - ({ eventType }) => eventType === iface.temporal.api.enums.v1.EventType.EVENT_TYPE_MARKER_RECORDED - ); - // There will only be one marker despite there being 2 hasChange calls because they have the - // same ID and core will only record one marker per id. - t.is(hasChangeEvents.length, 1); - t.is(hasChangeEvents[0].markerRecordedEventAttributes!.markerName, CHANGE_MARKER_NAME); -}); - -test.serial('deprecate-patch', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.deprecatePatchWorkflow); - const res = await worker.runUntil(handle.result()); - t.is(res, undefined); - const history = await handle.fetchHistory(); - const hasChangeEvents = history.events!.filter( - ({ eventType }) => eventType === iface.temporal.api.enums.v1.EventType.EVENT_TYPE_MARKER_RECORDED - ); - t.is(hasChangeEvents.length, 1); - t.is(hasChangeEvents[0].markerRecordedEventAttributes!.markerName, CHANGE_MARKER_NAME); -}); - -test.serial('Worker default ServerOptions are generated correctly', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.argsAndReturn, { - args: ['hey', undefined, Buffer.from('abc')], - }); - await worker.runUntil(handle.result()); - const history = await handle.fetchHistory(); - const events = history.events!.filter( - ({ eventType }) => eventType === iface.temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_TASK_COMPLETED - ); - t.is(events.length, 1); - const [event] = events; - t.regex(event.workflowTaskCompletedEventAttributes!.identity!, /\d+@.+/); - let binid = event.workflowTaskCompletedEventAttributes!.binaryChecksum!; - if (binid === '') { - binid = event.workflowTaskCompletedEventAttributes!.workerVersion!.buildId!; - } - t.regex(binid, /@temporalio\/worker@\d+\.\d+\.\d+/); -}); - -test.serial('WorkflowHandle.describe result is wrapped', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const date = new Date(); - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.argsAndReturn, { - args: ['hey', undefined, Buffer.from('abc')], - searchAttributes: { - CustomKeywordField: ['test-value'], - CustomIntField: [1], - CustomDatetimeField: [date], - }, - memo: { - note: 'foo', - }, - }); - await worker.runUntil(handle.result()); - const execution = await handle.describe(); - t.deepEqual(execution.type, 'argsAndReturn'); - t.deepEqual(execution.memo, { note: 'foo' }); - t.true(execution.startTime instanceof Date); - t.deepEqual(execution.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation - t.deepEqual(execution.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation - t.deepEqual(execution.searchAttributes!.CustomDatetimeField, [date]); // eslint-disable-line deprecation/deprecation - const binSum = execution.searchAttributes!.BinaryChecksums as string[]; // eslint-disable-line deprecation/deprecation - if (binSum != null) { - t.regex(binSum[0], /@temporalio\/worker@/); - } else { - t.deepEqual(execution.searchAttributes!.BuildIds, ['unversioned', `unversioned:${worker.options.buildId}`]); // eslint-disable-line deprecation/deprecation - } -}); - -// eslint-disable-next-line deprecation/deprecation -export async function returnSearchAttributes(): Promise { - const sa = workflowInfo().searchAttributes!; // eslint-disable-line @typescript-eslint/no-non-null-assertion, deprecation/deprecation - const datetime = (sa.CustomDatetimeField as Array)[0]; - return { - ...sa, - datetimeType: [Object.getPrototypeOf(datetime).constructor.name], - datetimeInstanceofWorks: [datetime instanceof Date], - arrayInstanceofWorks: [sa.CustomIntField instanceof Array], - }; -} - -test.serial('Workflow can read Search Attributes set at start', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const date = new Date(); - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(returnSearchAttributes, { - searchAttributes: { - CustomKeywordField: ['test-value'], - CustomIntField: [1], - CustomDatetimeField: [date], - }, - }); - const res = await worker.runUntil(handle.result()); - t.deepEqual(res, { - CustomKeywordField: ['test-value'], - CustomIntField: [1], - CustomDatetimeField: [date.toISOString()], - datetimeInstanceofWorks: [true], - arrayInstanceofWorks: [true], - datetimeType: ['Date'], - }); -}); - -test.serial('Workflow can upsert Search Attributes', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const date = new Date(); - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.upsertAndReadSearchAttributes, { - args: [date.getTime()], - }); - const res = await worker.runUntil(handle.result()); - t.deepEqual(res, { - CustomBoolField: [true], - CustomKeywordField: ['durable code'], - CustomTextField: ['is useful'], - CustomDatetimeField: [date.toISOString()], - CustomDoubleField: [3.14], - }); - const { searchAttributes } = await handle.describe(); // eslint-disable-line deprecation/deprecation - const { BinaryChecksums, BuildIds, ...rest } = searchAttributes; - t.deepEqual(rest, { - CustomBoolField: [true], - CustomKeywordField: ['durable code'], - CustomTextField: ['is useful'], - CustomDatetimeField: [date], - CustomDoubleField: [3.14], - }); - let checksum: any; - if (BinaryChecksums != null) { - t.true(BinaryChecksums.length === 1); - checksum = BinaryChecksums[0]; - } else { - t.true(BuildIds!.length === 2); - t.deepEqual(BuildIds![0], 'unversioned'); - checksum = BuildIds![1]; - } - t.true( - typeof checksum === 'string' && - checksum.includes(`@temporalio/worker@${pkg.version}+`) && - /\+[a-f0-9]{64}$/.test(checksum) // bundle checksum - ); -}); - -export async function returnWorkflowInfo(): Promise { - return workflowInfo(); -} - -test.serial('Workflow can read WorkflowInfo', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(returnWorkflowInfo, { - memo: { - nested: { object: true }, - }, - }); - const res = await worker.runUntil(handle.result()); - t.assert(res.historySize > 100); - t.deepEqual(res, { - memo: { - nested: { object: true }, - }, - attempt: 1, - firstExecutionRunId: handle.firstExecutionRunId, - namespace: 'default', - taskTimeoutMs: 10_000, - runId: handle.firstExecutionRunId, - taskQueue, - searchAttributes: {}, - // Typed search attributes gets serialized as an array. - typedSearchAttributes: [], - workflowType: 'returnWorkflowInfo', - workflowId: handle.workflowId, - historyLength: 3, - continueAsNewSuggested: false, - // values ignored for the purpose of comparison - historySize: res.historySize, - startTime: res.startTime, - runStartTime: res.runStartTime, - currentBuildId: res.currentBuildId, // eslint-disable-line deprecation/deprecation - currentDeploymentVersion: res.currentDeploymentVersion, - // unsafe.now is a function, so doesn't make it through serialization, but .now is required, so we need to cast - unsafe: { isReplaying: false } as UnsafeWorkflowInfo, - priority: {}, - }); -}); - -/** - * NOTE: this test uses the `IN` operator API which requires advanced visibility as of server 1.18. - * It will silently succeed on servers that only support standard visibility (can't dynamically skip a test). - */ -test.serial('Download and replay multiple executions with client list method', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const client = env.client; - try { - const fns = [workflows.http, workflows.cancelFakeProgress, childWorkflowInvoke, workflows.activityFailures]; - const handles = await Promise.all(fns.map((fn) => startWorkflow(fn))); - // Wait for the workflows to complete first - await worker.runUntil(Promise.all(handles.map((h) => h.result()))); - // Test the list API too while we're at it - const workflowIds = handles.map(({ workflowId }) => `'${workflowId}'`); - const histories = client.workflow.list({ query: `WorkflowId IN (${workflowIds.join(', ')})` }).intoHistories(); - const results = Worker.runReplayHistories( - { - workflowBundle: worker.options.workflowBundle, - dataConverter: env.options.client.dataConverter, - }, - histories - ); - - for await (const result of results) { - t.is(result.error, undefined); - } - } catch (e) { - // Don't report a test failure if the server does not support extended query - if (!(e as Error).message?.includes(`operator 'in' not allowed`)) throw e; - } - t.pass(); -}); diff --git a/packages/test/src/test-integration-split-three.ts b/packages/test/src/test-integration-split-three.ts deleted file mode 100644 index c4af367f5..000000000 --- a/packages/test/src/test-integration-split-three.ts +++ /dev/null @@ -1,208 +0,0 @@ -import path from 'node:path'; -import v8 from 'node:v8'; -import { readFileSync } from 'node:fs'; -import pkg from '@temporalio/worker/lib/pkg'; -import { bundleWorkflowCode } from '@temporalio/worker'; -import { temporal } from '@temporalio/proto'; -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, compareStackTrace } from './helpers'; -import * as workflows from './workflows'; - -const test = makeTestFn(() => bundleWorkflowCode({ workflowsPath: require.resolve('./workflows') })); -test.macro(configMacro); - -test('cancel-http-request', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - await withZeroesHTTPServer(async (port) => { - const url = `http://127.0.0.1:${port}`; - await worker.runUntil( - executeWorkflow(workflows.cancellableHTTPRequest, { - args: [url], - }) - ); - }); - t.pass(); -}); - -if ('promiseHooks' in v8) { - // Skip in old node versions - test('Stack trace query returns stack that makes sense', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const rawStacks = await worker.runUntil(executeWorkflow(workflows.stackTracer)); - - const [stack1, stack2] = rawStacks.map((r) => - r - .split('\n\n') - .map((s) => cleanOptionalStackTrace(`\n${s}`)) - .join('\n') - ); - // Can't get the Trigger stack cleaned, this is okay for now - // NOTE: we check endsWith because under certain conditions we might see Promise.race in the trace - t.true( - stack1.endsWith( - ` - at stackTracer (test/src/workflows/stack-tracer.ts) - - at stackTracer (test/src/workflows/stack-tracer.ts) - - at Promise.then () - at Trigger.then (workflow/src/trigger.ts)` - ), - `Got invalid stack:\n--- clean ---\n${stack1}\n--- raw ---\n${rawStacks[0]}` - ); - - t.is( - stack2, - ` - at executeChild (workflow/src/workflow.ts) - at stackTracer (test/src/workflows/stack-tracer.ts) - - at new Promise () - at timerNextHandler (workflow/src/workflow.ts) - at sleep (workflow/src/workflow.ts) - at stackTracer (test/src/workflows/stack-tracer.ts) - - at stackTracer (test/src/workflows/stack-tracer.ts)` - ); - }); - - test('Enhanced stack trace returns trace that makes sense', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const enhancedStack = await worker.runUntil(executeWorkflow(workflows.enhancedStackTracer)); - - const stacks = enhancedStack.stacks.map((s) => ({ - locations: s.locations.map((l) => ({ - ...l, - ...(l.file_path - ? { file_path: l.file_path.replace(path.resolve(__dirname, '../../../'), '').replace(/\\/g, '/') } - : undefined), - })), - })); - 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; - compareStackTrace(t, functionName, '$CLASS.all'); - } - t.deepEqual(stacks, [ - { - locations: [ - { - // Checked sperately above to handle Node 24 behavior change with respect to identifiers in stack traces - // function_name: 'Function.all', - internal_code: false, - }, - { - file_path: '/packages/test/src/workflows/stack-tracer.ts', - function_name: 'enhancedStackTracer', - line: 32, - column: 35, - internal_code: false, - }, - ], - }, - { - locations: [ - { - file_path: '/packages/test/src/workflows/stack-tracer.ts', - function_name: 'enhancedStackTracer', - line: 32, - column: 35, - internal_code: false, - }, - ], - }, - { - locations: [ - { - function_name: 'Promise.then', - internal_code: false, - }, - { - file_path: '/packages/workflow/src/trigger.ts', - function_name: 'Trigger.then', - line: 47, - column: 24, - internal_code: false, - }, - ], - }, - ]); - const expectedSources = ['../src/workflows/stack-tracer.ts', '../../workflow/src/trigger.ts'].map((p) => [ - path.resolve(__dirname, p), - [{ content: readFileSync(path.resolve(__dirname, p), 'utf8'), line_offset: 0 }], - ]); - t.deepEqual(Object.entries(enhancedStack.sources), expectedSources); - }); -} - -test( - 'priorities can be specified and propagated across child workflows and activities', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const handle = await startWorkflow(workflows.priorityWorkflow, { - args: [false, 1], - priority: { priorityKey: 1, fairnessKey: 'main-workflow', fairnessWeight: 3.0 }, - }); - await worker.runUntil(handle.result()); - let firstChild = true; - const history = await handle.fetchHistory(); - for (const event of history?.events ?? []) { - switch (event.eventType) { - case temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: - t.deepEqual(event.workflowExecutionStartedEventAttributes?.priority?.priorityKey, 1); - t.deepEqual(event.workflowExecutionStartedEventAttributes?.priority?.fairnessKey, 'main-workflow'); - t.deepEqual(event.workflowExecutionStartedEventAttributes?.priority?.fairnessWeight, 3.0); - break; - case temporal.api.enums.v1.EventType.EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED: { - const priority = event.startChildWorkflowExecutionInitiatedEventAttributes?.priority; - if (firstChild) { - t.deepEqual(priority?.priorityKey, 4); - t.deepEqual(priority?.fairnessKey, 'child-workflow-1'); - t.deepEqual(priority?.fairnessWeight, 2.5); - firstChild = false; - } else { - t.deepEqual(priority?.priorityKey, 2); - t.deepEqual(priority?.fairnessKey, 'child-workflow-2'); - t.deepEqual(priority?.fairnessWeight, 1.0); - } - break; - } - case temporal.api.enums.v1.EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED: - t.deepEqual(event.activityTaskScheduledEventAttributes?.priority?.priorityKey, 5); - t.deepEqual(event.activityTaskScheduledEventAttributes?.priority?.fairnessKey, 'fair-activity'); - // For some insane reason when proto reads this event it mangles the number to 4.19999999 something. Thanks Javascript. - t.assert(approximatelyEqual(event.activityTaskScheduledEventAttributes?.priority?.fairnessWeight, 4.2)); - break; - } - } - } -); - -test('workflow start without priorities sees undefined for the key', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - - const handle1 = await startWorkflow(workflows.priorityWorkflow, { - args: [true, undefined], - }); - await worker.runUntil(handle1.result()); - - // check occurs in the workflow, need an assert in the test itself in order to run - t.true(true); -}); diff --git a/packages/test/src/test-integration-split-two.ts b/packages/test/src/test-integration-split-two.ts deleted file mode 100644 index cfdb1d322..000000000 --- a/packages/test/src/test-integration-split-two.ts +++ /dev/null @@ -1,995 +0,0 @@ -/* eslint @typescript-eslint/no-non-null-assertion: 0 */ -import asyncRetry from 'async-retry'; -import { v4 as uuid4 } from 'uuid'; -import * as iface from '@temporalio/proto'; -import { WorkflowContinuedAsNewError, WorkflowFailedError } from '@temporalio/client'; -import { - ApplicationFailure, - defaultPayloadConverter, - Payload, - WorkflowExecutionAlreadyStartedError, - WorkflowNotFoundError, -} from '@temporalio/common'; -import { searchAttributePayloadConverter } from '@temporalio/common/lib/converter/payload-search-attributes'; -import { msToNumber, tsToMs } from '@temporalio/common/lib/time'; -import { - decode as payloadDecode, - decodeFromPayloadsAtIndex, - decodeOptionalSinglePayload, -} from '@temporalio/common/lib/internal-non-workflow'; - -import { Context } from '@temporalio/activity'; -import { - condition, - defineQuery, - defineSignal, - getCurrentDetails, - proxyActivities, - proxyLocalActivities, - setCurrentDetails, - setDefaultQueryHandler, - setHandler, - sleep, - startChild, -} from '@temporalio/workflow'; -import { temporal } from '@temporalio/proto'; -import { configurableHelpers, createTestWorkflowBundle } from './helpers-integration'; -import * as activities from './activities'; -import * as workflows from './workflows'; -import { makeTestFn, configMacro } from './helpers-integration-multi-codec'; - -// Note: re-export shared workflows (or long workflows) -// - review the files where these workflows are shared -export * from './workflows'; - -const test = makeTestFn(() => createTestWorkflowBundle({ workflowsPath: __filename })); -test.macro(configMacro); - -// FIXME: Unless we add .serial() here, ava tries to start all async tests in parallel, which -// is ok in most environments, but has been causing flakyness in CI, especially on Windows. -// We can probably avoid this by using larger runners, and there is some opportunity for -// optimization here, but for now, let's just run these tests serially. -test.serial('WorkflowOptions are passed correctly with defaults', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.argsAndReturn, { - args: ['hey', undefined, Buffer.from('def')], - }); - await worker.runUntil(handle.result()); - const execution = await handle.describe(); - t.deepEqual(execution.type, 'argsAndReturn'); - const indexedFields = execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!; - const indexedFieldKeys = Object.keys(indexedFields); - - let encodedId: any; - if (indexedFieldKeys.includes('BinaryChecksums')) { - encodedId = indexedFields.BinaryChecksums!; - } else { - encodedId = indexedFields.BuildIds!; - } - t.true(encodedId != null); - - const checksums = searchAttributePayloadConverter.fromPayload(encodedId); - console.log(checksums); - t.true(Array.isArray(checksums)); - t.regex((checksums as string[]).pop()!, /@temporalio\/worker@\d+\.\d+\.\d+/); - t.is(execution.raw.executionConfig?.taskQueue?.name, taskQueue); - t.is( - execution.raw.executionConfig?.taskQueue?.kind, - iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL - ); - t.is(execution.raw.executionConfig?.workflowRunTimeout, null); - t.is(execution.raw.executionConfig?.workflowExecutionTimeout, null); -}); - -test.serial('WorkflowOptions are passed correctly', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - // Throws because we use a different task queue - const worker = await createWorkerWithDefaults(t); - const options = { - memo: { a: 'b' }, - searchAttributes: { CustomIntField: [3] }, - workflowRunTimeout: '2s', - workflowExecutionTimeout: '3s', - workflowTaskTimeout: '1s', - taskQueue: 'diff-task-queue', - } as const; - const handle = await startWorkflow(workflows.sleeper, options); - async function fromPayload(payload: Payload) { - const payloadCodecs = env.client.options.dataConverter.payloadCodecs ?? []; - const [decodedPayload] = await payloadDecode(payloadCodecs, [payload]); - return defaultPayloadConverter.fromPayload(decodedPayload); - } - await t.throwsAsync(worker.runUntil(handle.result()), { - instanceOf: WorkflowFailedError, - message: 'Workflow execution timed out', - }); - const execution = await handle.describe(); - t.deepEqual( - execution.raw.workflowExecutionInfo?.type, - iface.temporal.api.common.v1.WorkflowType.create({ name: 'sleeper' }) - ); - t.deepEqual(await fromPayload(execution.raw.workflowExecutionInfo!.memo!.fields!.a!), 'b'); - t.deepEqual( - searchAttributePayloadConverter.fromPayload( - execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!.CustomIntField! - ), - [3] - ); - t.deepEqual(execution.searchAttributes!.CustomIntField, [3]); // eslint-disable-line deprecation/deprecation - t.is(execution.raw.executionConfig?.taskQueue?.name, 'diff-task-queue'); - t.is( - execution.raw.executionConfig?.taskQueue?.kind, - iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL - ); - - t.is(tsToMs(execution.raw.executionConfig!.workflowRunTimeout!), msToNumber(options.workflowRunTimeout)); - t.is(tsToMs(execution.raw.executionConfig!.workflowExecutionTimeout!), msToNumber(options.workflowExecutionTimeout)); - t.is(tsToMs(execution.raw.executionConfig!.defaultWorkflowTaskTimeout!), msToNumber(options.workflowTaskTimeout)); -}); - -test.serial('WorkflowHandle.result() throws if terminated', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.sleeper, { - args: [1000000], - }); - await t.throwsAsync( - worker.runUntil(async () => { - await handle.terminate('hasta la vista baby'); - await handle.result(); - }), - { - instanceOf: WorkflowFailedError, - message: 'hasta la vista baby', - } - ); -}); - -test.serial('WorkflowHandle.result() throws if continued as new', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil(async () => { - const originalWorkflowHandle = await startWorkflow(workflows.continueAsNewSameWorkflow, { - followRuns: false, - }); - let err = await t.throwsAsync(originalWorkflowHandle.result(), { instanceOf: WorkflowContinuedAsNewError }); - - if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion - const client = env.client; - let continueWorkflowHandle = client.workflow.getHandle( - originalWorkflowHandle.workflowId, - err.newExecutionRunId, - { - followRuns: false, - } - ); - - await continueWorkflowHandle.signal(workflows.continueAsNewSignal); - err = await t.throwsAsync(continueWorkflowHandle.result(), { - instanceOf: WorkflowContinuedAsNewError, - }); - if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion - - continueWorkflowHandle = client.workflow.getHandle( - continueWorkflowHandle.workflowId, - err.newExecutionRunId - ); - await continueWorkflowHandle.result(); - }); -}); - -test.serial('WorkflowHandle.result() follows chain of execution', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil( - executeWorkflow(workflows.continueAsNewSameWorkflow, { - args: ['execute', 'none'], - }) - ); - t.pass(); -}); - -test.serial('continue-as-new-to-different-workflow', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults, loadedDataConverter } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - await worker.runUntil(async () => { - const originalWorkflowHandle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { - followRuns: false, - }); - const err = await t.throwsAsync(originalWorkflowHandle.result(), { instanceOf: WorkflowContinuedAsNewError }); - if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion - const workflow = client.workflow.getHandle( - originalWorkflowHandle.workflowId, - err.newExecutionRunId, - { - followRuns: false, - } - ); - await workflow.result(); - const info = await workflow.describe(); - t.is(info.raw.workflowExecutionInfo?.type?.name, 'sleeper'); - const history = await workflow.fetchHistory(); - const timeSlept = await decodeFromPayloadsAtIndex( - loadedDataConverter, - 0, - history?.events?.[0].workflowExecutionStartedEventAttributes?.input?.payloads - ); - t.is(timeSlept, 1); - }); -}); - -test.serial('continue-as-new-to-same-workflow keeps memo and search attributes', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.continueAsNewSameWorkflow, { - memo: { - note: 'foo', - }, - searchAttributes: { - CustomKeywordField: ['test-value'], - CustomIntField: [1], - }, - followRuns: true, - }); - await worker.runUntil(async () => { - await handle.signal(workflows.continueAsNewSignal); - await handle.result(); - const execution = await handle.describe(); - t.not(execution.runId, handle.firstExecutionRunId); - t.deepEqual(execution.memo, { note: 'foo' }); - t.deepEqual(execution.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation - t.deepEqual(execution.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation - }); -}); - -test.serial( - 'continue-as-new-to-different-workflow keeps memo and search attributes by default', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { - followRuns: true, - memo: { - note: 'foo', - }, - searchAttributes: { - CustomKeywordField: ['test-value'], - CustomIntField: [1], - }, - }); - await worker.runUntil(async () => { - await handle.result(); - const info = await handle.describe(); - t.is(info.type, 'sleeper'); - t.not(info.runId, handle.firstExecutionRunId); - t.deepEqual(info.memo, { note: 'foo' }); - t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation - t.deepEqual(info.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation - }); - } -); - -test.serial( - 'continue-as-new-to-different-workflow can set memo and search attributes', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { - args: [ - 1, - { - memo: { - note: 'bar', - }, - searchAttributes: { - CustomKeywordField: ['test-value-2'], - CustomIntField: [3], - }, - }, - ], - followRuns: true, - memo: { - note: 'foo', - }, - searchAttributes: { - CustomKeywordField: ['test-value'], - CustomIntField: [1], - }, - }); - await worker.runUntil(async () => { - await handle.result(); - const info = await handle.describe(); - t.is(info.type, 'sleeper'); - t.not(info.runId, handle.firstExecutionRunId); - t.deepEqual(info.memo, { note: 'bar' }); - t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value-2']); // eslint-disable-line deprecation/deprecation - t.deepEqual(info.searchAttributes!.CustomIntField, [3]); // eslint-disable-line deprecation/deprecation - }); - } -); - -test.serial('signalWithStart works as intended and returns correct runId', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const originalWorkflowHandle = await client.workflow.signalWithStart(workflows.interruptableWorkflow, { - taskQueue, - workflowId: uuid4(), - signal: workflows.interruptSignal, - signalArgs: ['interrupted from signalWithStart'], - }); - await worker.runUntil(async () => { - let err: WorkflowFailedError | undefined = await t.throwsAsync(originalWorkflowHandle.result(), { - instanceOf: WorkflowFailedError, - }); - if (!(err?.cause instanceof ApplicationFailure)) { - return t.fail('Expected err.cause to be an instance of ApplicationFailure'); - } - t.is(err.cause.message, 'interrupted from signalWithStart'); - - // Test returned runId - const handle = client.workflow.getHandle( - originalWorkflowHandle.workflowId, - originalWorkflowHandle.signaledRunId - ); - err = await t.throwsAsync(handle.result(), { - instanceOf: WorkflowFailedError, - }); - if (!(err?.cause instanceof ApplicationFailure)) { - return t.fail('Expected err.cause to be an instance of ApplicationFailure'); - } - t.is(err.cause.message, 'interrupted from signalWithStart'); - }); -}); - -test.serial('activity-failures', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - await worker.runUntil(executeWorkflow(workflows.activityFailures)); - t.pass(); -}); - -export async function sleepInvalidDuration(): Promise { - await sleep(0); - await new Promise((resolve) => setTimeout(resolve, -1)); -} - -test.serial('sleepInvalidDuration is caught in Workflow runtime', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil(executeWorkflow(sleepInvalidDuration)); - t.pass(); -}); - -test.serial('unhandledRejection causes WFT to fail', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.throwUnhandledRejection, { - // throw an exception that our worker can associate with a running workflow - args: [{ crashWorker: false }], - }); - await worker.runUntil( - asyncRetry( - async () => { - const history = await handle.fetchHistory(); - const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); - if (wftFailedEvent === undefined) { - throw new Error('No WFT failed event'); - } - const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; - if (!failure) { - t.fail(); - return; - } - t.is(failure.message, 'Unhandled Promise rejection: Error: unhandled rejection'); - t.true(failure.stackTrace?.includes(`Error: unhandled rejection`)); - t.is(failure.cause?.cause?.message, 'root failure'); - }, - { minTimeout: 300, factor: 1, retries: 100 } - ) - ); - await handle.terminate(); -}); - -export async function throwObject(): Promise { - throw { plainObject: true }; -} - -test.serial('throwObject includes message with our recommendation', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(throwObject); - await worker.runUntil( - asyncRetry( - async () => { - const history = await handle.fetchHistory(); - const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); - if (wftFailedEvent === undefined) { - throw new Error('No WFT failed event'); - } - const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; - if (!failure) { - t.fail(); - return; - } - t.is( - failure.message, - '{"plainObject":true} [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]' - ); - }, - { minTimeout: 300, factor: 1, retries: 100 } - ) - ); - await handle.terminate(); -}); - -export async function throwBigInt(): Promise { - throw 42n; -} - -test.serial('throwBigInt includes message with our recommendation', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(throwBigInt); - await worker.runUntil( - asyncRetry( - async () => { - const history = await handle.fetchHistory(); - const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); - if (wftFailedEvent === undefined) { - throw new Error('No WFT failed event'); - } - const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; - if (!failure) { - t.fail(); - return; - } - t.is( - failure.message, - '42 [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]' - ); - }, - { minTimeout: 300, factor: 1, retries: 100 } - ) - ); - await handle.terminate(); -}); - -test.serial('Workflow RetryPolicy kicks in with retryable failure', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.throwAsync, { - args: ['retryable'], - retry: { - initialInterval: 1, - maximumInterval: 1, - maximumAttempts: 2, - }, - }); - await worker.runUntil(async () => { - await t.throwsAsync(handle.result()); - // Verify retry happened - const { runId } = await handle.describe(); - t.not(runId, handle.firstExecutionRunId); - }); -}); - -test.serial('Workflow RetryPolicy ignored with nonRetryable failure', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.throwAsync, { - args: ['nonRetryable'], - retry: { - initialInterval: 1, - maximumInterval: 1, - maximumAttempts: 2, - }, - }); - await worker.runUntil(async () => { - await t.throwsAsync(handle.result()); - const res = await handle.describe(); - t.is( - res.raw.workflowExecutionInfo?.status, - iface.temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED - ); - // Verify retry did not happen - const { runId } = await handle.describe(); - t.is(runId, handle.firstExecutionRunId); - }); -}); - -test.serial('WorkflowClient.start fails with WorkflowExecutionAlreadyStartedError', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handle = await startWorkflow(workflows.sleeper, { - args: [10000000], - }); - try { - await worker.runUntil( - t.throwsAsync( - client.workflow.start(workflows.sleeper, { - taskQueue, - workflowId: handle.workflowId, - }), - { - instanceOf: WorkflowExecutionAlreadyStartedError, - message: 'Workflow execution already started', - } - ) - ); - } finally { - await handle.terminate(); - } -}); - -test.serial( - 'WorkflowClient.signalWithStart fails with WorkflowExecutionAlreadyStartedError', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handle = await startWorkflow(workflows.sleeper); - await worker.runUntil(async () => { - await handle.result(); - await t.throwsAsync( - client.workflow.signalWithStart(workflows.sleeper, { - taskQueue: 'test', - workflowId: handle.workflowId, - signal: workflows.interruptSignal, - signalArgs: ['interrupted from signalWithStart'], - workflowIdReusePolicy: 'REJECT_DUPLICATE', - }), - { - instanceOf: WorkflowExecutionAlreadyStartedError, - message: 'Workflow execution already started', - } - ); - }); - } -); - -test.serial('Handle from WorkflowClient.start follows only own execution chain', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handleFromThrowerStart = await startWorkflow(workflows.throwAsync); - const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId); - await worker.runUntil(async () => { - await t.throwsAsync(handleFromGet.result(), { message: /.*/ }); - const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { - taskQueue: 'test', - workflowId: handleFromThrowerStart.workflowId, - args: [1_000_000], - }); - try { - await t.throwsAsync(handleFromThrowerStart.result(), { message: 'Workflow execution failed' }); - } finally { - await handleFromSleeperStart.terminate(); - } - }); -}); - -test.serial( - 'Handle from WorkflowClient.signalWithStart follows only own execution chain', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handleFromThrowerStart = await client.workflow.signalWithStart(workflows.throwAsync, { - taskQueue, - workflowId: uuid4(), - signal: 'unblock', - }); - const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId); - await worker.runUntil(async () => { - await t.throwsAsync(handleFromGet.result(), { message: /.*/ }); - const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { - taskQueue, - workflowId: handleFromThrowerStart.workflowId, - args: [1_000_000], - }); - try { - await t.throwsAsync(handleFromThrowerStart.result(), { message: 'Workflow execution failed' }); - } finally { - await handleFromSleeperStart.terminate(); - } - }); - } -); - -test.serial('Handle from WorkflowClient.getHandle follows only own execution chain', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handleFromThrowerStart = await startWorkflow(workflows.throwAsync); - const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId, undefined, { - firstExecutionRunId: handleFromThrowerStart.firstExecutionRunId, - }); - await worker.runUntil(async () => { - await t.throwsAsync(handleFromThrowerStart.result(), { message: /.*/ }); - const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { - taskQueue, - workflowId: handleFromThrowerStart.workflowId, - args: [1_000_000], - }); - try { - await t.throwsAsync(handleFromGet.result(), { message: 'Workflow execution failed' }); - } finally { - await handleFromSleeperStart.terminate(); - } - }); -}); - -test.serial('Handle from WorkflowClient.start terminates run after continue as new', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handleFromStart = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { - args: [1_000_000], - }); - const handleFromGet = client.workflow.getHandle(handleFromStart.workflowId, handleFromStart.firstExecutionRunId, { - followRuns: false, - }); - await worker.runUntil(async () => { - await t.throwsAsync(handleFromGet.result(), { instanceOf: WorkflowContinuedAsNewError }); - await handleFromStart.terminate('Expect workflow to terminate due to CAN'); - await t.throwsAsync(handleFromStart.result(), { message: 'Expect workflow to terminate due to CAN' }); - }); -}); - -test.serial( - 'Handle from WorkflowClient.getHandle does not terminate run after continue as new if given runId', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handleFromStart = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { - args: [1_000_000], - followRuns: false, - }); - const handleFromGet = client.workflow.getHandle(handleFromStart.workflowId, handleFromStart.firstExecutionRunId); - await worker.runUntil(async () => { - await t.throwsAsync(handleFromStart.result(), { instanceOf: WorkflowContinuedAsNewError }); - try { - await t.throwsAsync(handleFromGet.terminate(), { - instanceOf: WorkflowNotFoundError, - message: 'workflow execution already completed', - }); - } finally { - await client.workflow.getHandle(handleFromStart.workflowId).terminate(); - } - }); - } -); - -test.serial( - 'Runtime does not issue cancellations for activities and timers that throw during validation', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil(executeWorkflow(workflows.cancelScopeOnFailedValidation)); - t.pass(); - } -); - -const mutateWorkflowStateQuery = defineQuery('mutateWorkflowState'); -export async function queryAndCondition(): Promise { - let mutated = false; - // Not a valid query, used to verify that condition isn't triggered for query jobs - setHandler(mutateWorkflowStateQuery, () => void (mutated = true)); - await condition(() => mutated); -} - -test.serial('Query does not cause condition to be triggered', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(queryAndCondition); - await worker.runUntil(handle.query(mutateWorkflowStateQuery)); - await handle.terminate(); - // Worker did not crash - t.pass(); -}); - -const completeSignal = defineSignal('complete'); -const definedQuery = defineQuery('query-handler-type'); - -interface QueryNameAndArgs { - name: string; - queryName?: string; - args: any[]; -} - -export async function workflowWithMaybeDefinedQuery(useDefinedQuery: boolean): Promise { - let complete = false; - setHandler(completeSignal, () => { - complete = true; - }); - setDefaultQueryHandler((queryName: string, ...args: any[]) => { - return { name: 'default', queryName, args }; - }); - if (useDefinedQuery) { - setHandler(definedQuery, (...args: any[]) => { - return { name: definedQuery.name, args }; - }); - } - - await condition(() => complete); -} - -test.serial('default query handler is used if requested query does not exist', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const handle = await startWorkflow(workflowWithMaybeDefinedQuery, { - args: [false], - }); - await worker.runUntil(async () => { - const args = ['test', 'args']; - const result = await handle.query(definedQuery, ...args); - t.deepEqual(result, { name: 'default', queryName: definedQuery.name, args }); - }); -}); - -test.serial('default query handler is not used if requested query exists', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const handle = await startWorkflow(workflowWithMaybeDefinedQuery, { - args: [true], - }); - await worker.runUntil(async () => { - const args = ['test', 'args']; - const result = await handle.query('query-handler-type', ...args); - t.deepEqual(result, { name: definedQuery.name, args }); - }); -}); - -export async function completableWorkflow(completes: boolean): Promise { - await condition(() => completes); -} - -export async function userMetadataWorkflow(): Promise<{ - currentDetails: string; - childWorkflowId: string; - childRunId: string; -}> { - let done = false; - const signalDef = defineSignal('done'); - setHandler( - signalDef, - () => { - done = true; - }, - { description: 'signal-desc' } - ); - - // That workflow should call an activity (with summary) - const { activityWithSummary } = proxyActivities({ - scheduleToCloseTimeout: '10s', - scheduleToStartTimeout: '10s', - }); - await activityWithSummary.executeWithOptions( - { - summary: 'activity summary', - retry: { - initialInterval: '1s', - maximumAttempts: 5, - maximumInterval: '10s', - }, - scheduleToStartTimeout: '5s', - }, - [] - ); - const { localActivityWithSummary } = proxyLocalActivities({ scheduleToCloseTimeout: '10s' }); - await localActivityWithSummary.executeWithOptions( - { - summary: 'local activity summary', - retry: { - maximumAttempts: 2, - nonRetryableErrorTypes: ['CustomError'], - }, - scheduleToStartTimeout: '5s', - }, - [] - ); - // Timer (with summary) - await sleep(5, { summary: 'timer summary' }); - // Set current details - setCurrentDetails('current wf details'); - // Start child workflow - const childHandle = await startChild(completableWorkflow, { - args: [false], - staticDetails: 'child details', - staticSummary: 'child summary', - }); - - await condition(() => done); - return { - currentDetails: getCurrentDetails(), - childWorkflowId: childHandle.workflowId, - childRunId: childHandle.firstExecutionRunId, - }; -} - -test.serial('User metadata on workflow, timer, activity, child', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - - const worker = await createWorkerWithDefaults(t, { - activities: { - async activityWithSummary() {}, - async localActivityWithSummary() {}, - }, - }); - - await worker.runUntil(async () => { - // Start a workflow with static details - const handle = await startWorkflow(userMetadataWorkflow, { - staticSummary: 'wf static summary', - staticDetails: 'wf static details', - }); - // Describe workflow -> static summary, static details - const desc = await handle.describe(); - t.is(await desc.staticSummary(), 'wf static summary'); - t.is(await desc.staticDetails(), 'wf static details'); - - await handle.signal('done'); - const res = await handle.result(); - t.is(res.currentDetails, 'current wf details'); - - // Get child workflow handle and verify metadata - const childHandle = env.client.workflow.getHandle(res.childWorkflowId, res.childRunId); - const childDesc = await childHandle.describe(); - t.is(await childDesc.staticSummary(), 'child summary'); - t.is(await childDesc.staticDetails(), 'child details'); - - // Get history events for main workflow. - const resp = await env.client.workflowService.getWorkflowExecutionHistory({ - namespace: env.client.options.namespace, - execution: { - workflowId: handle.workflowId, - runId: handle.firstExecutionRunId, - }, - }); - for (const event of resp.history?.events ?? []) { - if (event.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED) { - t.deepEqual( - await decodeOptionalSinglePayload(env.client.options.loadedDataConverter, event.userMetadata?.summary), - 'wf static summary' - ); - t.deepEqual( - await decodeOptionalSinglePayload(env.client.options.loadedDataConverter, event.userMetadata?.details), - 'wf static details' - ); - } else if (event.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED) { - t.deepEqual( - await decodeOptionalSinglePayload(env.client.options.loadedDataConverter, event.userMetadata?.summary), - 'activity summary' - ); - // Assert that the overriden activity options are what we expect. - const attrs = event.activityTaskScheduledEventAttributes; - t.is(tsToMs(attrs?.scheduleToCloseTimeout), 10000); - t.is(tsToMs(attrs?.scheduleToStartTimeout), 5000); - const retryPolicy = attrs?.retryPolicy; - t.is(retryPolicy?.maximumAttempts, 5); - t.is(tsToMs(retryPolicy?.initialInterval), 1000); - t.is(tsToMs(retryPolicy?.maximumInterval), 10000); - } else if (event.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_TIMER_STARTED) { - t.deepEqual( - await decodeOptionalSinglePayload(env.client.options.loadedDataConverter, event.userMetadata?.summary), - 'timer summary' - ); - } - } - // Get history events for child workflow. - const childResp = await env.client.workflowService.getWorkflowExecutionHistory({ - namespace: env.client.options.namespace, - execution: { - workflowId: res.childWorkflowId, - runId: res.childRunId, - }, - }); - - for (const event of childResp.history?.events ?? []) { - if (event.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED) { - t.is( - await decodeOptionalSinglePayload(env.client.options.loadedDataConverter, event.userMetadata?.summary), - 'child summary' - ); - t.is( - await decodeOptionalSinglePayload(env.client.options.loadedDataConverter, event.userMetadata?.details), - 'child details' - ); - } - } - // Run metadata query -> get current details - const wfMetadata = (await handle.query('__temporal_workflow_metadata')) as temporal.api.sdk.v1.IWorkflowMetadata; - t.is(wfMetadata.definition?.signalDefinitions?.length, 1); - t.is(wfMetadata.definition?.signalDefinitions?.[0].name, 'done'); - t.is(wfMetadata.definition?.signalDefinitions?.[0].description, 'signal-desc'); - t.is(wfMetadata.definition?.queryDefinitions?.length, 3); // default queries - t.is(wfMetadata.currentDetails, 'current wf details'); - }); -}); - -export async function activityContextExposesClientConnectionParentWorkflow(): Promise { - return await proxyActivities({ - startToCloseTimeout: '10s', - })['foo'](); -} - -export async function activityContextExposesClientConnectionChildWorkflow(comment: string): Promise { - return `child(${comment})`; -} - -test('Activity Context exposes Client connection', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { - activities: { - foo: async () => { - const { client } = Context.current(); - return await client.workflow.execute(activityContextExposesClientConnectionChildWorkflow, { - workflowId: uuid4(), - taskQueue, - args: ['not intercepted'], - }); - }, - }, - interceptors: { - client: { - workflow: [ - { - async start(input, next) { - input.options.args = ['native client intercepted']; - return await next(input); - }, - }, - ], - }, - }, - }); - const res = await worker.runUntil(async () => { - const handle = await startWorkflow(activityContextExposesClientConnectionParentWorkflow); - return await handle.result(); - }); - t.is(res, 'child(native client intercepted)'); -}); diff --git a/packages/test/src/test-integration-update-interceptors.ts b/packages/test/src/test-integration-update-interceptors.ts deleted file mode 100644 index 4e2600dbc..000000000 --- a/packages/test/src/test-integration-update-interceptors.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { randomUUID } from 'crypto'; -import { - WithStartWorkflowOperation, - WorkflowStartUpdateInput, - WorkflowStartUpdateOutput, - WorkflowStartUpdateWithStartInput, - WorkflowStartUpdateWithStartOutput, - WorkflowUpdateStage, -} from '@temporalio/client'; -import * as wf from '@temporalio/workflow'; -import { Next, UpdateInput, WorkflowInboundCallsInterceptor, WorkflowInterceptors } from '@temporalio/workflow'; -import { helpers, makeTestFunction } from './helpers-integration'; - -const test = makeTestFunction({ - workflowsPath: __filename, - workflowInterceptorModules: [__filename], - workflowEnvironmentOpts: { - client: { - interceptors: { - workflow: [ - { - async startUpdate(input: WorkflowStartUpdateInput, next): Promise { - return next({ ...input, args: [input.args[0] + '-clientIntercepted', ...input.args.slice(1)] }); - }, - async startUpdateWithStart( - input: WorkflowStartUpdateWithStartInput, - next - ): Promise { - return next({ - ...input, - workflowStartOptions: { - ...input.workflowStartOptions, - args: [ - input.workflowStartOptions.args[0] + '-clientIntercepted', - ...input.workflowStartOptions.args.slice(1), - ], - }, - updateArgs: [input.updateArgs[0] + '-clientIntercepted', ...input.updateArgs.slice(1)], - }); - }, - }, - ], - }, - }, - }, -}); - -const update = wf.defineUpdate('update'); - -export async function workflowWithUpdate(wfArg: string): Promise { - let receivedUpdate = false; - const updateHandler = async (arg: string): Promise => { - receivedUpdate = true; - return arg; - }; - const validator = (arg: string): void => { - if (arg === 'bad-arg') { - throw new Error('Validation failed'); - } - }; - wf.setHandler(update, updateHandler, { validator }); - await wf.condition(() => receivedUpdate); - return wfArg; -} - -class MyWorkflowInboundCallsInterceptor implements WorkflowInboundCallsInterceptor { - async handleUpdate( - input: UpdateInput, - next: Next - ): Promise { - return await next({ ...input, args: [input.args[0] + '-workflowIntercepted', ...input.args.slice(1)] }); - } - validateUpdate(input: UpdateInput, next: Next): void { - const [arg] = input.args as string[]; - const args = arg.startsWith('validation-interceptor-will-make-me-invalid') ? ['bad-arg'] : [arg]; - next({ ...input, args }); - } -} - -export const interceptors = (): WorkflowInterceptors => ({ - inbound: [new MyWorkflowInboundCallsInterceptor()], -}); - -test('Update client and workflow interceptors work for executeUpdate', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdate, { args: ['wfArg'] }); - - const updateResult = await wfHandle.executeUpdate(update, { args: ['1'] }); - t.deepEqual(updateResult, '1-clientIntercepted-workflowIntercepted'); - }); -}); - -test('Update client and workflow interceptors work for startUpdate', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdate, { args: ['wfArg'] }); - - const updateHandle = await wfHandle.startUpdate(update, { - args: ['1'], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - const updateResult = await updateHandle.result(); - t.deepEqual(updateResult, '1-clientIntercepted-workflowIntercepted'); - }); -}); - -test('UpdateWithStart client and workflow interceptors work for executeUpdateWithStart', async (t) => { - const { createWorker, taskQueue } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const startWorkflowOperation = new WithStartWorkflowOperation(workflowWithUpdate, { - workflowId: randomUUID(), - taskQueue, - workflowIdConflictPolicy: 'FAIL', - args: ['wfArg'], - }); - const updateResult = await t.context.env.client.workflow.executeUpdateWithStart(update, { - args: ['updArg'], - startWorkflowOperation, - }); - t.deepEqual(updateResult, 'updArg-clientIntercepted-workflowIntercepted'); - const wfHandle = await startWorkflowOperation.workflowHandle(); - const wfResult = await wfHandle.result(); - t.deepEqual(wfResult, 'wfArg-clientIntercepted'); - }); -}); - -test('Update validation interceptor works', async (t) => { - const { createWorker, startWorkflow, assertWorkflowUpdateFailed } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdate, { args: ['wfArg'] }); - await assertWorkflowUpdateFailed( - wfHandle.executeUpdate(update, { args: ['validation-interceptor-will-make-me-invalid'] }), - wf.ApplicationFailure, - 'Validation failed' - ); - t.pass(); - }); -}); - -export async function workflowWithUpdateWithoutValidator(): Promise { - const updateHandler = async (arg: string): Promise => arg; - wf.setHandler(update, updateHandler); - await wf.condition(() => false); // Ensure the update is handled if it is dispatched in a second WFT. -} - -test('Update validation interceptors are not run when no validator', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdateWithoutValidator); - const arg = 'validation-interceptor-will-make-me-invalid'; - const result = await wfHandle.executeUpdate(update, { args: [arg] }); - t.true(result.startsWith(arg)); - }); -}); diff --git a/packages/test/src/test-integration-update.ts b/packages/test/src/test-integration-update.ts deleted file mode 100644 index cf03f7672..000000000 --- a/packages/test/src/test-integration-update.ts +++ /dev/null @@ -1,1054 +0,0 @@ -import { randomUUID } from 'crypto'; -import { status as grpcStatus } from '@grpc/grpc-js'; -import { - isGrpcServiceError, - WorkflowUpdateStage, - WorkflowUpdateRPCTimeoutOrCancelledError, - WorkflowFailedError, - WithStartWorkflowOperation, - WorkflowExecutionAlreadyStartedError, -} from '@temporalio/client'; -import * as wf from '@temporalio/workflow'; -import { temporal } from '@temporalio/proto'; -import { LogEntry } from '@temporalio/worker'; -import { helpers, makeTestFunction } from './helpers-integration'; -import { signalUpdateOrderingWorkflow } from './workflows/signal-update-ordering'; -import { signalsActivitiesTimersPromiseOrdering } from './workflows/signals-timers-activities-order'; -import { loadHistory, waitUntil } from './helpers'; - -// Use a reduced server long-poll expiration timeout, in order to confirm that client -// polling/retry strategies result in the expected behavior -const LONG_POLL_EXPIRATION_INTERVAL_SECONDS = 5.0; - -const recordedLogs: { [workflowId: string]: LogEntry[] } = {}; - -const test = makeTestFunction({ - workflowsPath: __filename, - workflowEnvironmentOpts: { - server: { - extraArgs: [ - '--dynamic-config-value', - `history.longPollExpirationInterval="${LONG_POLL_EXPIRATION_INTERVAL_SECONDS}s"`, - ], - }, - }, - recordedLogs, -}); - -export const update = wf.defineUpdate('update'); -export const doneUpdate = wf.defineUpdate('done-update'); - -export async function workflowWithUpdates(): Promise { - const state: string[] = []; - const updateHandler = async (arg: string): Promise => { - if (arg === 'wait-for-longer-than-server-long-poll-timeout') { - await wf.sleep(LONG_POLL_EXPIRATION_INTERVAL_SECONDS * 1500); - } - state.push(arg); - return state; - }; - // handlers can be sync - const doneUpdateHandler = (): void => { - state.push('done'); - }; - wf.setHandler(update, updateHandler); - wf.setHandler(doneUpdate, doneUpdateHandler); - await wf.condition(() => state.includes('done')); - state.push('$'); - return state; -} - -test('updateWithStart happy path', async (t) => { - const { createWorker, taskQueue } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const startOp = new WithStartWorkflowOperation(workflowWithUpdates, { - workflowId: randomUUID(), - taskQueue, - workflowIdConflictPolicy: 'USE_EXISTING', - }); - // Can send Update-With-Start MultiOperation request - const updHandle = await t.context.env.client.workflow.startUpdateWithStart(update, { - args: ['1'], - waitForStage: 'ACCEPTED', - startWorkflowOperation: startOp, - }); - // Can use returned upate handle to wait for update result - const updResult1 = await updHandle.result(); - t.deepEqual(updResult1, ['1']); - - // startOp has been mutated such that workflow handle is now available and - // can be used to interact with the workflow. - const wfHandle = await startOp.workflowHandle(); - const updResult2 = await wfHandle.executeUpdate(update, { args: ['2'] }); - t.deepEqual(updResult2, ['1', '2']); - - // startOp cannot be re-used in a second Update-With-Start call - const err = await t.throwsAsync( - t.context.env.client.workflow.executeUpdateWithStart(update, { - args: ['3'], - startWorkflowOperation: startOp, - }) - ); - t.true(err?.message.includes('WithStartWorkflowOperation instance has already been executed')); - }); -}); - -export async function workflowWithArgAndUpdateArg(wfArg: string): Promise { - const updateHandler = async (updateArg: string): Promise => { - return [wfArg, updateArg]; - }; - wf.setHandler(update, updateHandler); - await wf.condition(() => false); -} - -test('updateWithStart can send workflow arg and update arg', async (t) => { - const { createWorker, taskQueue } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const startOp = new WithStartWorkflowOperation(workflowWithArgAndUpdateArg, { - workflowId: randomUUID(), - args: ['wf-arg'], - taskQueue, - workflowIdConflictPolicy: 'USE_EXISTING', - }); - const updResult = await t.context.env.client.workflow.executeUpdateWithStart(update, { - args: ['upd-arg'], - startWorkflowOperation: startOp, - }); - t.deepEqual(updResult, ['wf-arg', 'upd-arg']); - t.deepEqual((await startOp.workflowHandle()).workflowId, startOp.options.workflowId); - }); -}); - -test('updateWithStart handles can be obtained concurrently', async (t) => { - const { createWorker, taskQueue } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const startOp = new WithStartWorkflowOperation(workflowWithUpdates, { - workflowId: randomUUID(), - taskQueue, - workflowIdConflictPolicy: 'USE_EXISTING', - }); - const [wfHandle, updHandle] = await Promise.all([ - startOp.workflowHandle(), - t.context.env.client.workflow.startUpdateWithStart(update, { - args: ['1'], - waitForStage: 'ACCEPTED', - startWorkflowOperation: startOp, - }), - ]); - - // Can use returned handles - t.deepEqual(await updHandle.result(), ['1']); - t.deepEqual(await wfHandle.executeUpdate(update, { args: ['2'] }), ['1', '2']); - }); -}); - -test('updateWithStart failure: invalid argument', async (t) => { - const startOp = new WithStartWorkflowOperation(workflowWithUpdates, { - workflowId: randomUUID().repeat(77), - taskQueue: 'does-not-exist', - workflowIdConflictPolicy: 'FAIL', - }); - - for (const promise of [ - t.context.env.client.workflow.startUpdateWithStart(update, { - args: ['1'], - waitForStage: 'ACCEPTED', - startWorkflowOperation: startOp, - }), - startOp.workflowHandle(), - ]) { - const err = await t.throwsAsync(promise); - t.true(isGrpcServiceError(err) && err.code === grpcStatus.INVALID_ARGUMENT); - t.true(err?.message.startsWith('WorkflowId length exceeds limit.')); - } -}); - -test('updateWithStart failure: workflow already exists', async (t) => { - const { createWorker, taskQueue } = helpers(t); - const workflowId = randomUUID(); - const worker = await createWorker(); - const makeStartOp = () => - new WithStartWorkflowOperation(workflowWithUpdates, { - workflowId, - taskQueue, - workflowIdConflictPolicy: 'FAIL', - }); - - const startUpdateWithStart = async (startOp: WithStartWorkflowOperation) => { - return [ - await t.context.env.client.workflow.startUpdateWithStart(update, { - args: ['1'], - waitForStage: 'ACCEPTED', - startWorkflowOperation: startOp, - }), - startOp, - ]; - }; - // The second call should fail with an ALREADY_EXISTS error. We assert that - // the resulting gRPC error has been correctly constructed from the two errors - // (start error, and aborted update) returned in the MultiOperationExecutionFailure. - await worker.runUntil(async () => { - const startOp1 = makeStartOp(); - await startUpdateWithStart(startOp1); - - const startOp2 = makeStartOp(); - for (const promise of [startUpdateWithStart(startOp2), startOp2.workflowHandle()]) { - await t.throwsAsync(promise, { - instanceOf: WorkflowExecutionAlreadyStartedError, - message: 'Workflow execution already started', - }); - } - }); -}); - -export const neverReturningUpdate = wf.defineUpdate('never-returning-update'); - -export async function workflowWithNeverReturningUpdate(): Promise { - const updateHandler = async (): Promise => { - await new Promise(() => {}); - throw new Error('unreachable'); - }; - wf.setHandler(neverReturningUpdate, updateHandler); - await new Promise(() => {}); - throw new Error('unreachable'); -} - -test('updateWithStart failure: update fails early due to limit on number of updates', async (t) => { - const workflowId = randomUUID(); - const { createWorker, taskQueue } = helpers(t); - const worker = await createWorker(); - const makeStartOp = () => - new WithStartWorkflowOperation(workflowWithNeverReturningUpdate, { - workflowId, - taskQueue, - workflowIdConflictPolicy: 'USE_EXISTING', - }); - const startUpdateWithStart = async (startOp: WithStartWorkflowOperation) => { - await t.context.env.client.workflow.startUpdateWithStart(neverReturningUpdate, { - waitForStage: 'ACCEPTED', - startWorkflowOperation: startOp, - }); - }; - - await worker.runUntil(async () => { - // The server permits 10 updates per workflow execution. - for (let i = 0; i < 10; i++) { - const startOp = makeStartOp(); - await startUpdateWithStart(startOp); - await startOp.workflowHandle; - } - // The 11th call should fail with a gRPC error - - // TODO: set gRPC retries to 1. This generates a RESOURCE_EXHAUSTED error, - // and by default these are retried 10 times. - const startOp = makeStartOp(); - for (const promise of [startUpdateWithStart(startOp), startOp.workflowHandle()]) { - const err = await t.throwsAsync(promise); - t.true(isGrpcServiceError(err) && err.code === grpcStatus.RESOURCE_EXHAUSTED); - } - }); -}); - -test('Update can be executed via executeUpdate()', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdates); - - const updateResult = await wfHandle.executeUpdate(update, { args: ['1'] }); - t.deepEqual(updateResult, ['1']); - - const doneUpdateResult = await wfHandle.executeUpdate(doneUpdate); - t.is(doneUpdateResult, undefined); - - const wfResult = await wfHandle.result(); - t.deepEqual(wfResult, ['1', 'done', '$']); - }); -}); - -test('Update can be executed via startUpdate() and handle.result()', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdates); - - const updateHandle = await wfHandle.startUpdate(update, { - args: ['1'], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - const updateResult = await updateHandle.result(); - t.deepEqual(updateResult, ['1']); - - const doneUpdateHandle = await wfHandle.startUpdate(doneUpdate, { - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - const doneUpdateResult = await doneUpdateHandle.result(); - t.is(doneUpdateResult, undefined); - - const wfResult = await wfHandle.result(); - t.deepEqual(wfResult, ['1', 'done', '$']); - }); -}); - -test('Update handle can be created from identifiers and used to obtain result', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const updateId = 'my-update-id'; - const wfHandle = await startWorkflow(workflowWithUpdates); - const updateHandleFromStartUpdate = await wfHandle.startUpdate(update, { - args: ['1'], - updateId, - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - - // Obtain update handle on workflow handle from start update. - const updateHandle = wfHandle.getUpdateHandle(updateId); - t.deepEqual(await updateHandle.result(), ['1']); - - // Obtain update handle on manually-created workflow handle with no run id. - t.truthy(updateHandleFromStartUpdate.workflowRunId); - const freshWorkflowHandleWithoutRunId = t.context.env.client.workflow.getHandle(wfHandle.workflowId); - const updateHandle2 = freshWorkflowHandleWithoutRunId.getUpdateHandle(updateId); - t.deepEqual(await updateHandle2.result(), ['1']); - - // Obtain update handle on manually-created workflow handle with run id. - const freshWorkflowHandleWithRunId = t.context.env.client.workflow.getHandle( - wfHandle.workflowId, - updateHandleFromStartUpdate.workflowRunId - ); - const updateHandle3 = freshWorkflowHandleWithRunId.getUpdateHandle(updateId); - t.deepEqual(await updateHandle3.result(), ['1']); - - // Obtain update handle on manually-created workflow handle with incorrect run id. - const workflowHandleWithIncorrectRunId = t.context.env.client.workflow.getHandle(wfHandle.workflowId, wf.uuid4()); - const updateHandle4 = workflowHandleWithIncorrectRunId.getUpdateHandle(updateId); - const err = await t.throwsAsync(updateHandle4.result()); - t.true(err instanceof wf.WorkflowNotFoundError); - }); -}); - -const activities = { - async myActivity(): Promise { - return 3; - }, -}; - -const proxyActivities = wf.proxyActivities({ - startToCloseTimeout: '5s', -}); - -const updateThatExecutesActivity = wf.defineUpdate('updateThatExecutesActivity'); - -export async function workflowWithMultiTaskUpdate(): Promise { - wf.setHandler(updateThatExecutesActivity, async (arg: number) => { - return arg + (await proxyActivities.myActivity()); - }); - await wf.condition(() => false); -} - -test('Update handler can execute activity', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker({ activities }); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithMultiTaskUpdate); - const result = await wfHandle.executeUpdate(updateThatExecutesActivity, { args: [4] }); - t.is(result, 7); - }); -}); - -const stringToStringUpdate = wf.defineUpdate('stringToStringUpdate'); - -export async function workflowWithUpdateValidator(): Promise { - const updateHandler = async (_: string): Promise => { - return 'update-result'; - }; - const validator = (arg: string): void => { - if (arg === 'bad-arg') { - throw new Error('Validation failed'); - } - }; - wf.setHandler(stringToStringUpdate, updateHandler, { validator }); - await wf.condition(() => false); -} - -test('Update validator can reject when using executeUpdate()', async (t) => { - const { createWorker, startWorkflow, assertWorkflowUpdateFailed } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdateValidator); - const result = await wfHandle.executeUpdate(stringToStringUpdate, { args: ['arg'] }); - t.is(result, 'update-result'); - await assertWorkflowUpdateFailed( - wfHandle.executeUpdate(stringToStringUpdate, { args: ['bad-arg'] }), - wf.ApplicationFailure, - 'Validation failed' - ); - }); -}); - -test('Update validator can reject when using handle.result() but handle can be obtained without error', async (t) => { - const { createWorker, startWorkflow, assertWorkflowUpdateFailed } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdateValidator); - let updateHandle = await wfHandle.startUpdate(stringToStringUpdate, { - args: ['arg'], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - t.is(await updateHandle.result(), 'update-result'); - updateHandle = await wfHandle.startUpdate(stringToStringUpdate, { - args: ['bad-arg'], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - await assertWorkflowUpdateFailed(updateHandle.result(), wf.ApplicationFailure, 'Validation failed'); - }); -}); - -const syncUpdate = wf.defineUpdate('sync'); -const asyncUpdate = wf.defineUpdate('async'); - -export async function handlerRaisesException(): Promise { - wf.setHandler(syncUpdate, (): void => { - throw new wf.ApplicationFailure(`Deliberate ApplicationFailure in handler`); - }); - wf.setHandler(asyncUpdate, async (): Promise => { - throw new wf.ApplicationFailure(`Deliberate ApplicationFailure in handler`); - }); - await wf.condition(() => false); -} - -test('Update: ApplicationFailure in handler rejects the update', async (t) => { - const { createWorker, startWorkflow, assertWorkflowUpdateFailed } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(handlerRaisesException); - for (const upd of [syncUpdate, asyncUpdate]) { - await assertWorkflowUpdateFailed( - wfHandle.executeUpdate(upd), - wf.ApplicationFailure, - 'Deliberate ApplicationFailure in handler' - ); - } - t.pass(); - }); -}); - -test('Update is rejected if there is no handler', async (t) => { - const { createWorker, startWorkflow, assertWorkflowUpdateFailed } = helpers(t); - const worker = await createWorker(); - const updateWithoutHandler = wf.defineUpdate('updateWithoutHandler'); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdates); - await assertWorkflowUpdateFailed( - wfHandle.executeUpdate(updateWithoutHandler, { args: [''] }), - wf.ApplicationFailure, - 'No registered handler for update: updateWithoutHandler' - ); - }); -}); - -test('Update sent after workflow completed', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdates); - await wfHandle.executeUpdate(doneUpdate); - await wfHandle.result(); - try { - await wfHandle.executeUpdate(update, { args: ['1'] }); - } catch (err) { - t.true(err instanceof wf.WorkflowNotFoundError); - t.is((err as wf.WorkflowNotFoundError).message, 'workflow execution already completed'); - } - }); -}); - -test('Update id can be assigned and is present on returned handle', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdates); - const updateHandle = await wfHandle.startUpdate(doneUpdate, { - updateId: 'my-update-id', - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - t.is(updateHandle.updateId, 'my-update-id'); - }); -}); - -export const updateWithMutableArg = wf.defineUpdate('updateWithMutableArg'); - -export async function workflowWithMutatingValidator(): Promise { - const updateHandler = async (arg: [string]): Promise => { - return arg; - }; - const validator = (arg: [string]): void => { - arg[0] = 'mutated!'; - }; - wf.setHandler(updateWithMutableArg, updateHandler, { validator }); - await wf.condition(() => false); -} - -test('Update handler does not see mutations to arguments made by validator', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithMutatingValidator); - const updateResult = await wfHandle.executeUpdate(updateWithMutableArg, { args: [['1']] }); - t.deepEqual(updateResult, ['1']); - }); -}); - -// The following tests test dispatch of buffered updates. An update is pushed to -// the buffer if its handler is not available when attempting to handle the -// update. If the handler is subsequently set by a setHandler call during -// processing of the same activation, then the handler is invoked on the -// buffered update . Otherwise, the buffered update is rejected. Hence in order -// to test dispatch of buffered updates, we need to cause the update job to be -// packaged together with another job that will cause the handler to be set -// (e.g. startWorkflow, or completeActivity). This scenario is typically -// encountered in the first WFT, and that is what these tests recreate. They -// start the workflow with startDelay, and then send an update (without waiting -// for the server's response) to ensure that doUpdate and startWorkflow are -// packaged in the same WFT (despite the large startDelay value, the server will -// dispatch a WFT when the update is received). - -const stateMutatingUpdate = wf.defineUpdate('stateMutatingUpdate'); - -export async function setUpdateHandlerAndExit(): Promise { - let state = 'initial'; - const mutateState = () => void (state = 'mutated-by-update'); - wf.setHandler(stateMutatingUpdate, mutateState); - // If an Update is present in the first WFT, then the handler should be called - // before the workflow exits and the workflow return value should reflect its - // side effects. - return state; -} - -test('Update is always delivered', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const wfHandle = await startWorkflow(setUpdateHandlerAndExit, { startDelay: '10000 days' }); - - wfHandle.executeUpdate(stateMutatingUpdate).catch(() => { - /* ignore */ - }); - const worker = await createWorker(); - await worker.runUntil(async () => { - // Worker receives activation: [doUpdate, startWorkflow] - const wfResult = await wfHandle.result(); - t.deepEqual(wfResult, 'mutated-by-update'); - }); -}); - -test('Two Updates in first WFT', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const wfHandle = await startWorkflow(workflowWithUpdates, { startDelay: '10000 days' }); - - wfHandle.executeUpdate(update, { args: ['1'] }).catch(() => { - /* ignore */ - }); - wfHandle.executeUpdate(doneUpdate).catch(() => { - /* ignore */ - }); - // Race condition: we want the second update to be in the WFT together with - // the first, so allow some time to ensure that happens. - await new Promise((res) => setTimeout(res, 500)); - - const worker = await createWorker(); - await worker.runUntil(async () => { - // Worker receives activation: [doUpdate, doUpdate, startWorkflow]. The - // updates initially lack a handler, are pushed to a buffer, and are - // executed when their handler is available. - const wfResult = await wfHandle.result(); - t.deepEqual(wfResult, ['1', 'done', '$']); - }); -}); - -// The following test would fail if the point at which the Update handler is -// executed differed between first execution and replay (in that case, the -// Update implementation would be violating workflow determinism). -const earlyExecutedUpdate = wf.defineUpdate('earlyExecutedUpdate'); -const handlerHasBeenExecutedQuery = wf.defineQuery('handlerHasBeenExecutedQuery'); -const openGateSignal = wf.defineSignal('openGateSignal'); - -export async function updateReplayTestWorkflow(): Promise { - let handlerHasBeenExecuted = false; - wf.setHandler(earlyExecutedUpdate, () => void (handlerHasBeenExecuted = true)); - const handlerWasExecutedEarly = handlerHasBeenExecuted; - - wf.setHandler(handlerHasBeenExecutedQuery, () => handlerHasBeenExecuted); - - let gateOpen = false; - wf.setHandler(openGateSignal, () => void (gateOpen = true)); - await wf.condition(() => gateOpen); - - return handlerWasExecutedEarly; -} - -test('Update handler is called at same point during first execution and replay', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - - // Start a Workflow and an Update of that Workflow. - const wfHandle = await startWorkflow(updateReplayTestWorkflow, { startDelay: '10000 days' }); - wfHandle.executeUpdate(earlyExecutedUpdate).catch(() => { - /* ignore */ - }); - await new Promise((res) => setTimeout(res, 1000)); - - // Avoid waiting for sticky execution timeout on worker transition - const worker1 = await createWorker({ maxCachedWorkflows: 0 }); - // Worker1 advances the workflow beyond the point where the update handler is - // invoked. - await worker1.runUntil(async () => { - // Use a query to wait until the update handler has been executed (the query - // handler is not set until after the desired point). - t.true(await wfHandle.query(handlerHasBeenExecutedQuery)); - // The workflow is now waiting for the gate to open. - }); - // Worker2 does not have the workflow in cache so will replay. - const worker2 = await createWorker(); - await worker2.runUntil(async () => { - await wfHandle.signal(openGateSignal); - const handlerWasExecutedEarly = await wfHandle.result(); - // If the Update handler is invoked at the same point during replay as it - // was on first execution then this will pass. But if, for example, the - // handler was invoked during replay _after_ advancing workflow code (which - // would violate workflow determinism), then this would not pass. - t.is(handlerWasExecutedEarly, true); - }); -}); - -/* Example from WorkflowHandle docstring */ - -// @@@SNIPSTART typescript-workflow-update-signal-query-example -export const incrementSignal = wf.defineSignal<[number]>('increment'); -export const getValueQuery = wf.defineQuery('getValue'); -export const incrementAndGetValueUpdate = wf.defineUpdate('incrementAndGetValue'); - -export async function counterWorkflow(initialValue: number): Promise { - let count = initialValue; - wf.setHandler(incrementSignal, (arg: number) => { - count += arg; - }); - wf.setHandler(getValueQuery, () => count); - wf.setHandler(incrementAndGetValueUpdate, (arg: number): number => { - count += arg; - return count; - }); - await wf.condition(() => false); -} -// @@@SNIPEND - -/* Example from WorkflowHandle docstring */ -test('Update/Signal/Query example in WorkflowHandle docstrings works', async (t) => { - const { createWorker, startWorkflow, assertWorkflowFailedError } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(counterWorkflow, { args: [2] }); - await wfHandle.signal(incrementSignal, 2); - const queryResult = await wfHandle.query(getValueQuery); - t.is(queryResult, 4); - const updateResult = await wfHandle.executeUpdate(incrementAndGetValueUpdate, { args: [2] }); - t.is(updateResult, 6); - const secondUpdateHandle = await wfHandle.startUpdate(incrementAndGetValueUpdate, { - args: [2], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - const secondUpdateResult = await secondUpdateHandle.result(); - t.is(secondUpdateResult, 8); - await wfHandle.cancel(); - await assertWorkflowFailedError(wfHandle.result(), wf.CancelledFailure); - }); -}); - -test('startUpdate does not return handle before update has reached requested stage', async (t) => { - const { startWorkflow } = helpers(t); - const wfHandle = await startWorkflow(workflowWithUpdates); - const updatePromise = wfHandle - .startUpdate(update, { - args: ['1'], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }) - .then(() => 'update'); - const timeoutPromise = new Promise((f) => - setTimeout(() => f('timeout'), LONG_POLL_EXPIRATION_INTERVAL_SECONDS * 1500) - ); - t.is( - await Promise.race([updatePromise, timeoutPromise]), - 'timeout', - 'The update call should never return, since it should be waiting until Accepted, yet there is no worker.' - ); -}); - -test('Interruption of update by server long-poll timeout is invisible to client', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdates); - const arg = 'wait-for-longer-than-server-long-poll-timeout'; - const updateResult = await wfHandle.executeUpdate(update, { args: [arg] }); - t.deepEqual(updateResult, [arg]); - await wfHandle.executeUpdate(doneUpdate); - const wfResult = await wfHandle.result(); - t.deepEqual(wfResult, [arg, 'done', '$']); - }); -}); - -export const currentInfoUpdate = wf.defineUpdate('current-info-update'); - -export async function workflowWithCurrentUpdateInfo(): Promise { - const state: Promise[] = []; - const getUpdateId = async (): Promise => { - await wf.sleep(10); - const info = wf.currentUpdateInfo(); - if (info === undefined) { - throw new Error('No current update info'); - } - return info.id; - }; - const updateHandler = async (): Promise => { - const info = wf.currentUpdateInfo(); - if (info === undefined || info.name !== 'current-info-update') { - throw new Error(`Invalid current update info in updateHandler: info ${info?.name}`); - } - const id = await getUpdateId(); - if (info.id !== id) { - throw new Error(`Update id changed: before ${info.id} after ${id}`); - } - - state.push(getUpdateId()); - // Re-fetch and return - const infoAfter = wf.currentUpdateInfo(); - if (infoAfter === undefined) { - throw new Error('Invalid current update info in updateHandler - after'); - } - return infoAfter.id; - }; - - const validator = (): void => { - const info = wf.currentUpdateInfo(); - if (info === undefined || info.name !== 'current-info-update') { - throw new Error(`Invalid current update info in validator: info ${info?.name}`); - } - }; - - wf.setHandler(currentInfoUpdate, updateHandler, { validator }); - - if (wf.currentUpdateInfo() !== undefined) { - throw new Error('Current update info not undefined outside handler'); - } - - await wf.condition(() => state.length === 5); - - if (wf.currentUpdateInfo() !== undefined) { - throw new Error('Current update info not undefined outside handler - after'); - } - - return await Promise.all(state); -} - -test('currentUpdateInfo returns the update id', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithCurrentUpdateInfo); - const updateIds = await Promise.all([ - wfHandle.executeUpdate(currentInfoUpdate, { updateId: 'update1' }), - wfHandle.executeUpdate(currentInfoUpdate, { updateId: 'update2' }), - wfHandle.executeUpdate(currentInfoUpdate, { updateId: 'update3' }), - wfHandle.executeUpdate(currentInfoUpdate, { updateId: 'update4' }), - wfHandle.executeUpdate(currentInfoUpdate, { updateId: 'update5' }), - ]); - t.deepEqual(updateIds, ['update1', 'update2', 'update3', 'update4', 'update5']); - const wfResults = await wfHandle.result(); - t.deepEqual(wfResults.sort(), ['update1', 'update2', 'update3', 'update4', 'update5']); - }); -}); - -test('startUpdate throws WorkflowUpdateRPCTimeoutOrCancelledError with no worker', async (t) => { - const { startWorkflow } = helpers(t); - const wfHandle = await startWorkflow(workflowWithUpdates); - await t.context.env.client.withDeadline(Date.now() + 100, async () => { - const err = await t.throwsAsync( - wfHandle.startUpdate(update, { args: ['1'], waitForStage: WorkflowUpdateStage.ACCEPTED }) - ); - t.true(err instanceof WorkflowUpdateRPCTimeoutOrCancelledError); - }); - - const ctrl = new AbortController(); - setTimeout(() => ctrl.abort(), 10); - await t.context.env.client.withAbortSignal(ctrl.signal, async () => { - const err = await t.throwsAsync( - wfHandle.startUpdate(update, { args: ['1'], waitForStage: WorkflowUpdateStage.ACCEPTED }) - ); - t.true(err instanceof WorkflowUpdateRPCTimeoutOrCancelledError); - }); -}); - -test('update result poll throws WorkflowUpdateRPCTimeoutOrCancelledError', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithUpdates); - const arg = 'wait-for-longer-than-server-long-poll-timeout'; - await t.context.env.client.withDeadline(Date.now() + LONG_POLL_EXPIRATION_INTERVAL_SECONDS * 1000, async () => { - const err = await t.throwsAsync(wfHandle.executeUpdate(update, { args: [arg] })); - t.true(err instanceof WorkflowUpdateRPCTimeoutOrCancelledError); - }); - - const ctrl = new AbortController(); - setTimeout(() => ctrl.abort(), LONG_POLL_EXPIRATION_INTERVAL_SECONDS * 1000); - await t.context.env.client.withAbortSignal(ctrl.signal, async () => { - const err = await t.throwsAsync(wfHandle.executeUpdate(update, { args: [arg] })); - t.true(err instanceof WorkflowUpdateRPCTimeoutOrCancelledError); - }); - }); -}); - -const updateThatShouldFail = wf.defineUpdate('updateThatShouldFail'); - -export async function workflowThatWillBeCanceled(): Promise { - wf.setHandler(updateThatShouldFail, async () => { - await wf.condition(() => false); - }); - await wf.condition(() => false); -} - -test('update caller gets update failed error on workflow cancellation', async (t) => { - const { createWorker, startWorkflow, assertWorkflowUpdateFailed } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const w = await startWorkflow(workflowThatWillBeCanceled); - const u = await w.startUpdate(updateThatShouldFail, { - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - await w.cancel(); - await assertWorkflowUpdateFailed(u.result(), wf.CancelledFailure, 'Workflow cancelled'); - }); -}); - -export { signalUpdateOrderingWorkflow }; - -// Validate that issue #1474 is fixed in 1.11.0+ -test("Pending promises can't unblock between signals and updates", async (t) => { - const { createWorker, startWorkflow, updateHasBeenAdmitted } = helpers(t); - - const handle = await startWorkflow(signalUpdateOrderingWorkflow); - const worker1 = await createWorker({ maxCachedWorkflows: 0 }); - await worker1.runUntil(async () => { - // Wait for the workflow to reach the first condition - await handle.executeUpdate('fooUpdate'); - }); - - const updateId = 'update-id'; - await handle.signal('fooSignal'); - const updateResult = handle.executeUpdate('fooUpdate', { updateId }); - await waitUntil(() => updateHasBeenAdmitted(handle, updateId), 5000); - - const worker2 = await createWorker(); - await worker2.runUntil(async () => { - t.is(await handle.result(), 3); - t.is(await updateResult, 3); - }); -}); - -export { signalsActivitiesTimersPromiseOrdering }; - -// A broader check covering issue #1474, but also other subtle ordering issues caused by the fact -// that signals used to be processed in a distinct phase from other types of jobs. -test('Signals/Updates/Activities/Timers have coherent promise completion ordering', async (t) => { - const { createWorker, startWorkflow, taskQueue, updateHasBeenAdmitted } = helpers(t); - - // We need signal+update+timer completion+activity completion to all happen in the same workflow task. - // To get there, as soon as the activity gets scheduled, we shutdown the workflow worker, then send - // the signal and update while there is no worker alive. When it eventually comes back up, all events - // will be queued up for the next WFT. - - const worker1 = await createWorker({ maxCachedWorkflows: 0 }); - const worker1Promise = worker1.run(); - const killWorker1 = async () => { - try { - worker1.shutdown(); - } catch { - // We may attempt to shutdown the worker multiple times. Ignore errors. - } - await worker1Promise; - }; - - try { - const activityWorker = await createWorker({ - taskQueue: `${taskQueue}-activity`, - activities: { myActivity: killWorker1 }, - workflowBundle: undefined, - workflowsPath: undefined, - }); - await activityWorker.runUntil(async () => { - const handle = await startWorkflow(signalsActivitiesTimersPromiseOrdering); - - // The workflow will schedule the activity, which will shutdown the worker. - // Then this promise will resolves. - await worker1Promise; - - await handle.signal('aaSignal'); - const updateId = 'update-id'; - const updatePromise = handle.executeUpdate('aaUpdate', { updateId }); - - // Timing is important here. Make sure that everything is ready before creating the new worker. - await waitUntil(async () => { - const updateAdmitted = await updateHasBeenAdmitted(handle, updateId); - if (!updateAdmitted) return false; - - const { events } = await handle.fetchHistory(); - return ( - events != null && - events.some((e) => e.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_ACTIVITY_TASK_COMPLETED) && - events.some((e) => e.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_TIMER_FIRED) && - events.some((e) => e.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED) - ); - }, 5000); - - await ( - await createWorker({}) - ).runUntil(async () => { - t.deepEqual(await handle.result(), [true, true, true, true]); - }); - - await updatePromise; - }); - } finally { - await killWorker1(); - } -}); - -export async function canCompleteUpdateAfterWorkflowReturns(fail: boolean = false): Promise { - let gotUpdate = false; - let mainReturned = false; - - wf.setHandler(wf.defineUpdate('doneUpdate'), async () => { - gotUpdate = true; - await wf.condition(() => mainReturned); - return 'completed'; - }); - - await wf.condition(() => gotUpdate); - mainReturned = true; - if (fail) throw wf.ApplicationFailure.nonRetryable('Intentional failure'); -} - -test('Can complete update after workflow returns', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - - const worker = await createWorker(); - await worker.runUntil(async () => { - const handle = await startWorkflow(canCompleteUpdateAfterWorkflowReturns); - const updateHandler = await handle.executeUpdate(wf.defineUpdate('doneUpdate')); - await handle.result(); - - await t.is(updateHandler, 'completed'); - }); -}); - -test('Can complete update after Workflow fails', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - - const worker = await createWorker(); - await worker.runUntil(async () => { - const handle = await startWorkflow(canCompleteUpdateAfterWorkflowReturns, { args: [true] }); - const updateHandler = await handle.executeUpdate(wf.defineUpdate('doneUpdate')); - await t.throwsAsync(handle.result(), { instanceOf: WorkflowFailedError }); - await t.is(updateHandler, 'completed'); - }); -}); - -/** - * The {@link canCompleteUpdateAfterWorkflowReturns} workflow above features an update handler that - * return safter the main workflow functions has returned. It will (assuming an update is sent in - * the first WFT) generate a raw command sequence (before sending to core) of: - * - * [UpdateAccepted, CompleteWorkflowExecution, UpdateCompleted]. - * - * Prior to https://github.com/temporalio/sdk-typescript/pull/1488, TS SDK ignored any command - * produced after a completion command, therefore truncating this command sequence to: - * - * [UpdateAccepted, CompleteWorkflowExecution]. - * - * Starting with #1488, TS SDK now performs no truncation, and Core reorders the sequence to: - * - * [UpdateAccepted, UpdateCompleted, CompleteWorkflowExecution]. - * - * This test takes a history generated using pre-#1488 SDK code, and replays it. That history - * contains the following events: - * - * 1 WorkflowExecutionStarted - * 2 WorkflowTaskScheduled - * 3 WorkflowTaskStarted - * 4 WorkflowTaskCompleted - * 5 WorkflowExecutionUpdateAccepted - * 6 WorkflowExecutionCompleted - * - * Note that the history lacks a `WorkflowExecutionUpdateCompleted` event. - * - * If Core's logic (which involves a flag) incorrectly allowed this history to be replayed using - * Core's post-#1488 implementation, then a non-determinism error would result. Specifically, Core - * would, at some point during replay, do the following: - * - * - Receive [UpdateAccepted, CompleteWorkflowExecution, UpdateCompleted] from lang; - * - Change that to `[UpdateAccepted, UpdateCompleted, CompleteWorkflowExecution]`; - * - Create an `UpdateMachine` instance (the `WorkflowTaskMachine` instance already exists). - * - Continue to consume history events. - * - * Event 5, `WorkflowExecutionUpdateAccepted`, would apply to the `UpdateMachine` associated with - * the `UpdateAccepted` command, but event 6, `WorkflowExecutionCompleted` would not, since Core is - * expecting an event that can be applied to the `UpdateMachine` corresponding to `UpdateCompleted`. - * If we modify Core to incorrectly apply its new logic then we do see that: - * - * [TMPRL1100] Nondeterminism error: Update machine does not handle this event: HistoryEvent(id: 6, WorkflowExecutionCompleted) - * - * The test passes because Core in fact (because the history lacks the flag) uses its old logic and - * changes the command sequence from `[UpdateAccepted, CompleteWorkflowExecution, UpdateCompleted]` - * to `[UpdateAccepted, CompleteWorkflowExecution]`, i.e. truncating commands emitted after the - * first completion command like TS SDK used to do, so that events 5 and 6 can be applied to the - * corresponding state machines. - */ -test('Can complete update after workflow returns - pre-1.11.0 compatibility', async (t) => { - const { runReplayHistory } = helpers(t); - const hist = await loadHistory('complete_update_after_workflow_returns_pre1488.json'); - await runReplayHistory({}, hist); - t.pass(); -}); - -const logUpdate = wf.defineUpdate<[string, string], [string]>('log-update'); -export async function workflowWithLogInUpdate(): Promise { - const updateHandler = (msg: string): [string, string] => { - const updateInfo = wf.currentUpdateInfo(); - if (!updateInfo) { - throw new Error('expected updateInfo to be defined'); - } - wf.log.info(msg); - return [updateInfo.id, updateInfo.name]; - }; - wf.setHandler(logUpdate, updateHandler); - await wf.condition(() => false); -} - -test('Workflow Worker logs update info when logging within update handler', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wfHandle = await startWorkflow(workflowWithLogInUpdate); - const logMsg = 'log msg'; - const [updateId, updateName] = await wfHandle.executeUpdate(logUpdate, { args: [logMsg] }); - t.true( - recordedLogs[wfHandle.workflowId].some( - (logEntry) => - logEntry.meta?.updateName === updateName && - logEntry.meta?.updateId === updateId && - logEntry.message === logMsg - ) - ); - }); -}); diff --git a/packages/test/src/test-integration-workflows-with-recorded-logs.ts b/packages/test/src/test-integration-workflows-with-recorded-logs.ts deleted file mode 100644 index 859598b0a..000000000 --- a/packages/test/src/test-integration-workflows-with-recorded-logs.ts +++ /dev/null @@ -1,512 +0,0 @@ -import { ExecutionContext } from 'ava'; -import * as workflow from '@temporalio/workflow'; -import { ApplicationFailureCategory, HandlerUnfinishedPolicy } from '@temporalio/common'; -import { LogEntry } from '@temporalio/worker'; -import { WorkflowFailedError, WorkflowUpdateFailedError } from '@temporalio/client'; -import { Context, helpers, makeTestFunction } from './helpers-integration'; -import { waitUntil } from './helpers'; - -const recordedLogs: { [workflowId: string]: LogEntry[] } = {}; -const test = makeTestFunction({ - workflowsPath: __filename, - recordedLogs, -}); - -export const unfinishedHandlersUpdate = workflow.defineUpdate('unfinished-handlers-update'); -export const unfinishedHandlersUpdate_ABANDON = workflow.defineUpdate('unfinished-handlers-update-ABANDON'); -export const unfinishedHandlersUpdate_WARN_AND_ABANDON = workflow.defineUpdate( - 'unfinished-handlers-update-WARN_AND_ABANDON' -); -export const unfinishedHandlersSignal = workflow.defineSignal('unfinished-handlers-signal'); -export const unfinishedHandlersSignal_ABANDON = workflow.defineSignal('unfinished-handlers-signal-ABANDON'); -export const unfinishedHandlersSignal_WARN_AND_ABANDON = workflow.defineSignal( - 'unfinished-handlers-signal-WARN_AND_ABANDON' -); - -/** - * A workflow for testing `workflow.allHandlersFinished()` and control of - * warnings by HandlerUnfinishedPolicy. - */ -export async function unfinishedHandlersWorkflow(waitAllHandlersFinished: boolean): Promise { - let startedHandler = false; - let handlerMayReturn = false; - let handlerFinished = false; - - const doUpdateOrSignal = async (): Promise => { - startedHandler = true; - await workflow.condition(() => handlerMayReturn); - handlerFinished = true; - }; - - workflow.setHandler(unfinishedHandlersUpdate, doUpdateOrSignal); - workflow.setHandler(unfinishedHandlersUpdate_ABANDON, doUpdateOrSignal, { - unfinishedPolicy: HandlerUnfinishedPolicy.ABANDON, - }); - workflow.setHandler(unfinishedHandlersUpdate_WARN_AND_ABANDON, doUpdateOrSignal, { - unfinishedPolicy: HandlerUnfinishedPolicy.WARN_AND_ABANDON, - }); - workflow.setHandler(unfinishedHandlersSignal, doUpdateOrSignal); - workflow.setHandler(unfinishedHandlersSignal_ABANDON, doUpdateOrSignal, { - unfinishedPolicy: HandlerUnfinishedPolicy.ABANDON, - }); - workflow.setHandler(unfinishedHandlersSignal_WARN_AND_ABANDON, doUpdateOrSignal, { - unfinishedPolicy: HandlerUnfinishedPolicy.WARN_AND_ABANDON, - }); - workflow.setDefaultSignalHandler(doUpdateOrSignal); - - await workflow.condition(() => startedHandler); - if (waitAllHandlersFinished) { - handlerMayReturn = true; - await workflow.condition(workflow.allHandlersFinished); - } - return handlerFinished; -} - -// These tests confirms that the unfinished-handler warning is issued, and respects the policy, and -// can be avoided by waiting for the `allHandlersFinished` condition. -test('unfinished update handler', async (t) => { - await new UnfinishedHandlersTest(t, 'update').testWaitAllHandlersFinishedAndUnfinishedHandlersWarning(); -}); - -test('unfinished signal handler', async (t) => { - await new UnfinishedHandlersTest(t, 'signal').testWaitAllHandlersFinishedAndUnfinishedHandlersWarning(); -}); - -class UnfinishedHandlersTest { - constructor( - private readonly t: ExecutionContext, - private readonly handlerType: 'update' | 'signal' - ) {} - - async testWaitAllHandlersFinishedAndUnfinishedHandlersWarning() { - // The unfinished handler warning is issued by default, - let [handlerFinished, warning] = await this.getWorkflowResultAndWarning(false); - this.t.false(handlerFinished); - this.t.true(warning); - - // and when the workflow sets the unfinished_policy to WARN_AND_ABANDON, - [handlerFinished, warning] = await this.getWorkflowResultAndWarning( - false, - HandlerUnfinishedPolicy.WARN_AND_ABANDON - ); - this.t.false(handlerFinished); - this.t.true(warning); - - // and when a default (aka dynamic) handler is used - if (this.handlerType === 'signal') { - [handlerFinished, warning] = await this.getWorkflowResultAndWarning(false, undefined, true); - this.t.false(handlerFinished); - this.t.true(warning); - } else { - // default handlers not supported yet for update - // https://github.com/temporalio/sdk-typescript/issues/1460 - } - - // but not when the workflow waits for handlers to complete, - [handlerFinished, warning] = await this.getWorkflowResultAndWarning(true); - this.t.true(handlerFinished); - this.t.false(warning); - - // TODO: make default handlers honor HandlerUnfinishedPolicy - // [handlerFinished, warning] = await this.getWorkflowResultAndWarning(true, undefined, true); - // this.t.true(handlerFinished); - // this.t.false(warning); - - // nor when the silence-warnings policy is set on the handler. - [handlerFinished, warning] = await this.getWorkflowResultAndWarning(false, HandlerUnfinishedPolicy.ABANDON); - this.t.false(handlerFinished); - this.t.false(warning); - } - - /** - * Run workflow and send signal/update. Return two booleans: - * - did the handler complete? (i.e. the workflow return value) - * - was an unfinished handler warning emitted? - */ - async getWorkflowResultAndWarning( - waitAllHandlersFinished: boolean, - unfinishedPolicy?: HandlerUnfinishedPolicy, - useDefaultHandler?: boolean - ): Promise<[boolean, boolean]> { - const { createWorker, startWorkflow } = helpers(this.t); - const worker = await createWorker(); - return await worker.runUntil(async () => { - const handle = await startWorkflow(unfinishedHandlersWorkflow, { args: [waitAllHandlersFinished] }); - let messageType: string; - if (useDefaultHandler) { - messageType = '__no_registered_handler__'; - this.t.falsy(unfinishedPolicy); // default handlers do not support setting the unfinished policy - } else { - messageType = `unfinished-handlers-${this.handlerType}`; - if (unfinishedPolicy) { - messageType += '-' + HandlerUnfinishedPolicy[unfinishedPolicy]; - } - } - switch (this.handlerType) { - case 'signal': - await handle.signal(messageType); - break; - case 'update': { - const executeUpdate = handle.executeUpdate(messageType, { updateId: 'my-update-id' }); - if (!waitAllHandlersFinished) { - await assertWorkflowUpdateFailedBecauseWorkflowCompleted(this.t, executeUpdate); - } else { - await executeUpdate; - } - break; - } - } - const handlerFinished = await handle.result(); - const unfinishedHandlerWarningEmitted = - recordedLogs[handle.workflowId] && - recordedLogs[handle.workflowId].findIndex((e) => this.isUnfinishedHandlerWarning(e)) >= 0; - return [handlerFinished, unfinishedHandlerWarningEmitted]; - }); - } - - isUnfinishedHandlerWarning(logEntry: LogEntry): boolean { - return ( - logEntry.level === 'WARN' && - new RegExp(`^\\[TMPRL1102\\] Workflow finished while an? ${this.handlerType} handler was still running\\.`).test( - logEntry.message - ) - ); - } -} - -export const unfinishedHandlersWorkflowTerminationTypeUpdate = workflow.defineUpdate( - 'unfinishedHandlersWorkflowTerminationTypeUpdate' -); -export const unfinishedHandlersWorkflowTerminationTypeSignal = workflow.defineSignal( - 'unfinishedHandlersWorkflowTerminationTypeSignal' -); - -export async function runUnfinishedHandlersWorkflowTerminationTypeWorkflow( - workflowTerminationType: - | 'cancellation' - | 'cancellation-with-shielded-handler' - | 'continue-as-new' - | 'failure' - | 'return', - waitAllHandlersFinished?: 'wait-all-handlers-finished' -): Promise { - let handlerMayReturn = false; - - const waitHandlerMayReturn = async () => { - if (workflowTerminationType === 'cancellation-with-shielded-handler') { - await workflow.CancellationScope.nonCancellable(async () => { - await workflow.condition(() => handlerMayReturn); - }); - } else { - await workflow.condition(() => handlerMayReturn); - } - }; - - workflow.setHandler(unfinishedHandlersWorkflowTerminationTypeUpdate, async () => { - await waitHandlerMayReturn(); - return 'update-result'; - }); - - workflow.setHandler(unfinishedHandlersWorkflowTerminationTypeSignal, async () => { - await waitHandlerMayReturn(); - }); - - switch (workflowTerminationType) { - case 'cancellation': - case 'cancellation-with-shielded-handler': - await workflow.condition(() => false); - throw new Error('unreachable'); - case 'continue-as-new': - if (waitAllHandlersFinished) { - handlerMayReturn = true; - await workflow.condition(workflow.allHandlersFinished); - } - // If we do not pass waitAllHandlersFinished here then the test occasionally fails. Recall - // that this test causes the worker to send a WFT response containing commands - // [completeUpdate, CAN]. Usually, that results in the update completing, the caller getting - // the update result, and the workflow CANing. However occasionally (~1/30) there is a server - // level=ERROR msg="service failures" operation=UpdateWorkflowExecution wf-namespace=default error="unable to locate current workflow execution" - // the update caller does not get a response, and the update is included again in the first - // WFT sent to the post-CAN workflow run. (This causes the current test to fail unless the - // post-CAN run waits for handlers to finish). - await workflow.continueAsNew('return', waitAllHandlersFinished); - throw new Error('unreachable'); - case 'failure': - throw new workflow.ApplicationFailure('Deliberately failing workflow with an unfinished handler'); - case 'return': - if (waitAllHandlersFinished) { - handlerMayReturn = true; - await workflow.condition(workflow.allHandlersFinished); - } - break; - } -} - -// These tests confirm that the warning is issued / not issued as appropriate for workflow -// termination via cancellation, continue-as-new, failure, and return, and that there is no warning -// when waiting on allHandlersFinished before return or continue-as-new. - -// We issue the warning if the workflow exited due to workflow return and there were unfinished -// handlers. Reason: the workflow author could/should have avoided this. - -test('unfinished update handler with workflow return', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest(t, 'update', 'return').testWarningIsIssued(true); -}); - -test('unfinished update handler with workflow return waiting for all handlers to finish', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest( - t, - 'update', - 'return', - 'wait-all-handlers-finished' - ).testWarningIsIssued(false); -}); - -test('unfinished signal handler with workflow return', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest(t, 'signal', 'return').testWarningIsIssued(true); -}); - -test('unfinished signal handler with workflow return waiting for all handlers to finish', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest( - t, - 'signal', - 'return', - 'wait-all-handlers-finished' - ).testWarningIsIssued(false); -}); - -// We issue the warning if the workflow exited due to workflow cancellation and there were -// unfinished handlers. Reason: workflow cancellation causes handler cancellation, so a workflow -// author is able to write a workflow that handles cancellation without leaving unfinished handlers. - -test('workflow cancellation does not cause unfinished update handler warnings because handler is cancelled', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest(t, 'update', 'cancellation').testWarningIsIssued(false); -}); - -test('workflow cancellation does not cause unfinished signal handler warnings because handler is cancelled', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest(t, 'signal', 'cancellation').testWarningIsIssued(false); -}); - -test('workflow cancellation causes unfinished update handler warnings when handler is not cancelled', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest( - t, - 'update', - 'cancellation-with-shielded-handler' - ).testWarningIsIssued(true); -}); - -test('workflow cancellation causes unfinished signal handler warnings when handler is not cancelled', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest( - t, - 'signal', - 'cancellation-with-shielded-handler' - ).testWarningIsIssued(true); -}); - -// We issue the warning if the workflow exited due to Continue-as-New and there were unfinished -// handlers. Reason: as with workflow return, the workflow author could/should have avoided this, -// and in this case the workflow is probably acting as some sort of task processor that must not -// drop work. - -test('unfinished update handler with continue-as-new', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest(t, 'update', 'continue-as-new').testWarningIsIssued(true); -}); - -test('unfinished update handler with continue-as-new waiting for all handlers to finish', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest( - t, - 'update', - 'continue-as-new', - 'wait-all-handlers-finished' - ).testWarningIsIssued(false); -}); - -test('unfinished signal handler with continue-as-new', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest(t, 'signal', 'continue-as-new').testWarningIsIssued(true); -}); - -test('unfinished signal handler with continue-as-new waiting for all handlers to finish', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest( - t, - 'signal', - 'continue-as-new', - 'wait-all-handlers-finished' - ).testWarningIsIssued(false); -}); - -// We do not issue the warning if the workflow finished due to failure and there were unfinished -// handlers. Reason: the workflow author cannot guarantee to avoid workflow failure: e.g. it might -// happen by an error thrown from executeActivity. - -test('unfinished update handler with workflow failure', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest(t, 'update', 'failure').testWarningIsIssued(false); -}); - -test('unfinished signal handler with workflow failure', async (t) => { - await new UnfinishedHandlersWorkflowTerminationTypeTest(t, 'signal', 'failure').testWarningIsIssued(false); -}); - -class UnfinishedHandlersWorkflowTerminationTypeTest { - constructor( - private readonly t: ExecutionContext, - private readonly handlerType: 'update' | 'signal', - private readonly workflowTerminationType: - | 'cancellation' - | 'cancellation-with-shielded-handler' - | 'continue-as-new' - | 'failure' - | 'return', - private readonly waitAllHandlersFinished?: 'wait-all-handlers-finished' - ) {} - - async testWarningIsIssued(expectWarning: boolean) { - this.t.is(await this.runWorkflowAndGetWarning(), expectWarning); - } - - async runWorkflowAndGetWarning(): Promise { - const { createWorker, startWorkflow, updateHasBeenAdmitted: workflowUpdateExists } = helpers(this.t); - const updateId = 'update-id'; - - // We require a startWorkflow, an update/signal, and maybe a cancellation request, to be - // delivered in the same WFT, so we ensure they've all been accepted by the server before - // starting the worker. - const w = await startWorkflow(runUnfinishedHandlersWorkflowTerminationTypeWorkflow, { - args: [this.workflowTerminationType, this.waitAllHandlersFinished], - }); - let executeUpdate: Promise; - switch (this.handlerType) { - case 'update': - executeUpdate = w.executeUpdate(unfinishedHandlersWorkflowTerminationTypeUpdate, { updateId }); - await waitUntil(() => workflowUpdateExists(w, updateId), 5000); - break; - case 'signal': - await w.signal(unfinishedHandlersWorkflowTerminationTypeSignal); - break; - } - if ( - this.workflowTerminationType === 'cancellation' || - this.workflowTerminationType === 'cancellation-with-shielded-handler' - ) { - await w.cancel(); - } - - const worker = await createWorker(); - - return await worker.runUntil(async () => { - if (this.handlerType === 'update') { - if (this.waitAllHandlersFinished) { - // The workflow author waited for allHandlersFinished and so the update caller gets a - // successful result. - this.t.is(await executeUpdate, 'update-result'); - } else if (this.workflowTerminationType === 'cancellation') { - // Workflow cancellation caused a CancellationFailure exception to be thrown in the - // handler. The update caller gets an error saying the update failed due to workflow - // cancellation. - await this.assertWorkflowUpdateFailedError(executeUpdate); - } else { - // (Including 'cancellation-with-shielded-handler'). The workflow finished while the - // handler was in-progress. The update caller gets an WorkflowUpdateFailedError error, - // with an ApplicationFailure cause whose type is AcceptedUpdateCompletedWorkflow - await assertWorkflowUpdateFailedBecauseWorkflowCompleted(this.t, executeUpdate); - } - } - switch (this.workflowTerminationType) { - case 'cancellation': - case 'cancellation-with-shielded-handler': - case 'failure': - await this.assertWorkflowFailedError(w.result(), this.workflowTerminationType); - break; - case 'return': - case 'continue-as-new': - await w.result(); - } - return ( - w.workflowId in recordedLogs && - recordedLogs[w.workflowId].findIndex((e) => this.isUnfinishedHandlerWarning(e)) >= 0 - ); - }); - } - - async assertWorkflowUpdateFailedError(p: Promise) { - const err: WorkflowUpdateFailedError = (await this.t.throwsAsync(p, { - instanceOf: WorkflowUpdateFailedError, - })) as WorkflowUpdateFailedError; - this.t.is(err.message, 'Workflow Update failed'); - } - - async assertWorkflowFailedError( - p: Promise, - workflowTerminationType: 'cancellation' | 'cancellation-with-shielded-handler' | 'failure' - ) { - const err = (await this.t.throwsAsync(p, { - instanceOf: WorkflowFailedError, - })) as WorkflowFailedError; - const howFailed = { - cancellation: 'cancelled', - 'cancellation-with-shielded-handler': 'cancelled', - failure: 'failed', - }[workflowTerminationType]; - this.t.is(err.message, 'Workflow execution ' + howFailed); - } - - isUnfinishedHandlerWarning(logEntry: LogEntry): boolean { - return ( - logEntry.level === 'WARN' && - new RegExp(`^\\[TMPRL1102\\] Workflow finished while an? ${this.handlerType} handler was still running\\.`).test( - logEntry.message - ) - ); - } -} - -async function assertWorkflowUpdateFailedBecauseWorkflowCompleted(t: ExecutionContext, p: Promise) { - const err: WorkflowUpdateFailedError = (await t.throwsAsync(p, { - instanceOf: WorkflowUpdateFailedError, - })) as WorkflowUpdateFailedError; - - const cause = err.cause; - t.true(cause instanceof workflow.ApplicationFailure); - t.true((cause as workflow.ApplicationFailure).type === 'AcceptedUpdateCompletedWorkflow'); - t.regex((cause as workflow.ApplicationFailure).message, /Workflow completed before the Update completed/); -} - -export async function raiseErrorWorkflow(useBenign: boolean): Promise { - await workflow - .proxyActivities({ startToCloseTimeout: '10s', retry: { maximumAttempts: 1 } }) - .throwApplicationFailureActivity(useBenign); -} - -test('Application failure category controls log level', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - async throwApplicationFailureActivity(useBenign: boolean) { - throw workflow.ApplicationFailure.create({ - category: useBenign ? ApplicationFailureCategory.BENIGN : undefined, - }); - }, - }, - }); - - await worker.runUntil(async () => { - // Run with BENIGN - let handle = await startWorkflow(raiseErrorWorkflow, { args: [true] }); - try { - await handle.result(); - } catch (_) { - const logs = recordedLogs[handle.workflowId]; - const activityFailureLog = logs.find((log) => log.message.includes('Activity failed')); - t.true(activityFailureLog !== undefined && activityFailureLog.level === 'DEBUG'); - } - - // Run without BENIGN - handle = await startWorkflow(raiseErrorWorkflow, { args: [false] }); - try { - await handle.result(); - } catch (_) { - const logs = recordedLogs[handle.workflowId]; - const activityFailureLog = logs.find((log) => log.message.includes('Activity failed')); - t.true(activityFailureLog !== undefined && activityFailureLog.level === 'WARN'); - } - }); -}); diff --git a/packages/test/src/test-integration-workflows.ts b/packages/test/src/test-integration-workflows.ts deleted file mode 100644 index 3b0604d17..000000000 --- a/packages/test/src/test-integration-workflows.ts +++ /dev/null @@ -1,1823 +0,0 @@ -import { setTimeout as setTimeoutPromise } from 'timers/promises'; -import { randomUUID } from 'crypto'; -import asyncRetry from 'async-retry'; -import { ExecutionContext } from 'ava'; -import { firstValueFrom, Subject } from 'rxjs'; -import { Client, WorkflowClient, WorkflowFailedError, WorkflowHandle } from '@temporalio/client'; -import * as activity from '@temporalio/activity'; -import { msToNumber, tsToMs } from '@temporalio/common/lib/time'; -import { TestWorkflowEnvironment } from '@temporalio/testing'; -import { CancelReason } from '@temporalio/worker/lib/activity'; -import * as workflow from '@temporalio/workflow'; -import { - condition, - defineQuery, - defineSignal, - defineUpdate, - setDefaultQueryHandler, - setDefaultSignalHandler, - setDefaultUpdateHandler, - setHandler, -} from '@temporalio/workflow'; -import { SdkFlags } from '@temporalio/workflow/lib/flags'; -import { - ActivityCancellationDetails, - ActivityCancellationType, - ApplicationFailure, - defineSearchAttributeKey, - encodingKeys, - METADATA_ENCODING_KEY, - RawValue, - SearchAttributePair, - SearchAttributeType, - TypedSearchAttributes, - WorkflowExecutionAlreadyStartedError, -} from '@temporalio/common'; -import { - TEMPORAL_RESERVED_PREFIX, - STACK_TRACE_QUERY_NAME, - ENHANCED_STACK_TRACE_QUERY_NAME, -} from '@temporalio/common/lib/reserved'; -import { encode } from '@temporalio/common/lib/encoding'; -import { signalSchedulingWorkflow } from './activities/helpers'; -import { activityStartedSignal } from './workflows/definitions'; -import * as workflows from './workflows'; -import { Context, createLocalTestEnvironment, helpers, makeTestFunction } from './helpers-integration'; -import { overrideSdkInternalFlag } from './mock-internal-flags'; -import { ActivityState, heartbeatCancellationDetailsActivity } from './activities/heartbeat-cancellation-details'; -import { loadHistory, RUN_TIME_SKIPPING_TESTS, waitUntil } from './helpers'; - -const test = makeTestFunction({ - workflowsPath: __filename, - workflowInterceptorModules: [__filename], -}); - -export async function parent(): Promise { - await workflow.startChild(child, { workflowId: 'child' }); - await workflow.startChild(child, { workflowId: 'child' }); -} - -export async function child(): Promise { - await workflow.CancellationScope.current().cancelRequested; -} - -test('Workflow fails if it tries to start a child with an existing workflow ID', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const err = await t.throwsAsync(executeWorkflow(parent), { - instanceOf: WorkflowFailedError, - }); - t.true( - err instanceof WorkflowFailedError && - err.cause?.name === 'TemporalFailure' && - err.cause?.message === 'Workflow execution already started' - ); - }); -}); - -export async function runTestActivity(activityOptions?: workflow.ActivityOptions): Promise { - await workflow.proxyActivities({ startToCloseTimeout: '1m', ...activityOptions }).testActivity(); -} - -test('Worker cancels activities after shutdown has been requested', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - let cancelReason = null as CancelReason | null; - const worker = await createWorker({ - activities: { - async testActivity() { - const ctx = activity.Context.current(); - worker.shutdown(); - try { - await ctx.cancelled; - } catch (err) { - if (err instanceof activity.CancelledFailure) { - cancelReason = err.message as CancelReason; - } - throw err; - } - }, - }, - }); - await startWorkflow(runTestActivity); - // If worker completes within graceful shutdown period, the activity has successfully been cancelled - await worker.run(); - t.is(cancelReason, 'WORKER_SHUTDOWN'); -}); - -export async function cancelFakeProgress(): Promise { - const { fakeProgress, shutdownWorker } = workflow.proxyActivities({ - startToCloseTimeout: '200s', - cancellationType: workflow.ActivityCancellationType.WAIT_CANCELLATION_COMPLETED, - }); - - await workflow.CancellationScope.cancellable(async () => { - const promise = fakeProgress(); - await new Promise((resolve) => workflow.setHandler(activityStartedSignal, resolve)); - workflow.CancellationScope.current().cancel(); - await workflow.CancellationScope.nonCancellable(shutdownWorker); - await promise; - }); -} - -test('Worker allows heartbeating activities after shutdown has been requested', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - - const workerWasShutdownSubject = new Subject(); - let cancelReason = null as CancelReason | null; - - const worker = await createWorker({ - shutdownGraceTime: '5m', - activities: { - async fakeProgress() { - await signalSchedulingWorkflow(activityStartedSignal.name); - const ctx = activity.Context.current(); - await firstValueFrom(workerWasShutdownSubject); - try { - for (;;) { - await ctx.sleep('100ms'); - ctx.heartbeat(); - } - } catch (err) { - if (err instanceof activity.CancelledFailure) { - cancelReason = err.message as CancelReason; - } - throw err; - } - }, - async shutdownWorker() { - worker.shutdown(); - workerWasShutdownSubject.next(); - }, - }, - }); - await startWorkflow(cancelFakeProgress); - await worker.run(); - t.is(cancelReason, 'CANCELLED'); -}); - -export async function conditionTimeout0(): Promise { - return await workflow.condition(() => false, 0); -} - -test('Condition 0 patch sets a timer', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker(); - t.false(await worker.runUntil(executeWorkflow(conditionTimeout0))); -}); - -export async function historySizeGrows(): Promise<[number, number]> { - const before = workflow.workflowInfo().historySize; - await workflow.sleep(1); - const after = workflow.workflowInfo().historySize; - return [before, after]; -} - -test('HistorySize grows with new WFT', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker(); - const [before, after] = await worker.runUntil(executeWorkflow(historySizeGrows)); - t.true(after > before && before > 100); -}); - -test('HistorySize is visible in WorkflowExecutionInfo', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - const handle = await startWorkflow(historySizeGrows); - - await worker.runUntil(handle.result()); - const historySize = (await handle.describe()).historySize; - t.true(historySize && historySize > 100); -}); - -export async function suggestedCAN(): Promise { - const maxEvents = 40_000; - const batchSize = 1000; - if (workflow.workflowInfo().continueAsNewSuggested) { - return false; - } - while (workflow.workflowInfo().historyLength < maxEvents) { - await Promise.all(Array.from({ length: batchSize }, (_) => workflow.sleep(1))); - if (workflow.workflowInfo().continueAsNewSuggested) { - return true; - } - } - return false; -} - -test('ContinueAsNew is suggested', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker(); - const flaggedCAN = await worker.runUntil(executeWorkflow(suggestedCAN)); - t.true(flaggedCAN); -}); - -test('Activity initialInterval is not getting rounded', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - testActivity: () => undefined, - }, - }); - const handle = await startWorkflow(runTestActivity, { - args: [ - { - startToCloseTimeout: '5s', - retry: { initialInterval: '50ms', maximumAttempts: 1 }, - }, - ], - }); - await worker.runUntil(handle.result()); - const { events } = await handle.fetchHistory(); - const activityTaskScheduledEvents = events?.find((ev) => ev.activityTaskScheduledEventAttributes); - const retryPolicy = activityTaskScheduledEvents?.activityTaskScheduledEventAttributes?.retryPolicy; - t.is(tsToMs(retryPolicy?.initialInterval), 50); -}); - -test('Start of workflow is delayed', async (t) => { - const { startWorkflow } = helpers(t); - // This workflow never runs - const handle = await startWorkflow(runTestActivity, { - startDelay: '5678s', - }); - const { events } = await handle.fetchHistory(); - const workflowExecutionStartedEvent = events?.find((ev) => ev.workflowExecutionStartedEventAttributes); - const startDelay = workflowExecutionStartedEvent?.workflowExecutionStartedEventAttributes?.firstWorkflowTaskBackoff; - t.is(tsToMs(startDelay), 5678000); -}); - -export async function conflictId(): Promise { - await workflow.condition(() => false); -} - -test('Start of workflow respects workflow id conflict policy', async (t) => { - const { createWorker, taskQueue } = helpers(t); - const wfid = `${taskQueue}-` + randomUUID(); - const client = t.context.env.client; - - const worker = await createWorker(); - await worker.runUntil(async () => { - const handle = await client.workflow.start(conflictId, { - taskQueue, - workflowId: wfid, - }); - const handleWithRunId = client.workflow.getHandle(handle.workflowId, handle.firstExecutionRunId); - - // Confirm another fails by default - const err = await t.throwsAsync( - client.workflow.start(conflictId, { - taskQueue, - workflowId: wfid, - }), - { - instanceOf: WorkflowExecutionAlreadyStartedError, - } - ); - - t.true(err instanceof WorkflowExecutionAlreadyStartedError); - - // Confirm fails with explicit option - const err1 = await t.throwsAsync( - client.workflow.start(conflictId, { - taskQueue, - workflowId: wfid, - workflowIdConflictPolicy: 'FAIL', - }), - { - instanceOf: WorkflowExecutionAlreadyStartedError, - } - ); - - t.true(err1 instanceof WorkflowExecutionAlreadyStartedError); - - // Confirm gives back same handle - const handle2 = await client.workflow.start(conflictId, { - taskQueue, - workflowId: wfid, - workflowIdConflictPolicy: 'USE_EXISTING', - }); - - const desc = await handleWithRunId.describe(); - const desc2 = await handle2.describe(); - - t.is(desc.runId, desc2.runId); - t.is(desc.status.name, 'RUNNING'); - t.is(desc2.status.name, 'RUNNING'); - - // Confirm terminates and starts new - const handle3 = await client.workflow.start(conflictId, { - taskQueue, - workflowId: wfid, - workflowIdConflictPolicy: 'TERMINATE_EXISTING', - }); - - const descWithRunId = await handleWithRunId.describe(); - const desc3 = await handle3.describe(); - t.not(descWithRunId.runId, desc3.runId); - t.is(descWithRunId.status.name, 'TERMINATED'); - t.is(desc3.status.name, 'RUNNING'); - }); -}); - -// FIXME: This test is passing, but spitting out "signalTarget not exported by -// the workflow bundle" errors. To be revisited at a later time. -test('Start of workflow with signal respects conflict id policy', async (t) => { - const { createWorker, taskQueue } = helpers(t); - const wfid = `${taskQueue}-` + randomUUID(); - const client = t.context.env.client; - const worker = await createWorker(); - await worker.runUntil(async () => { - const handle = await client.workflow.start(workflows.signalTarget, { - taskQueue, - workflowId: wfid, - }); - const handleWithRunId = client.workflow.getHandle(handle.workflowId, handle.firstExecutionRunId); - - // Confirm gives back same handle is the default policy - const handle2 = await t.context.env.client.workflow.signalWithStart(workflows.signalTarget, { - taskQueue, - workflowId: wfid, - signal: workflows.argsTestSignal, - signalArgs: [123, 'kid'], - }); - const desc = await handleWithRunId.describe(); - const desc2 = await handle2.describe(); - - t.deepEqual(desc.runId, desc2.runId); - t.deepEqual(desc.status.name, 'RUNNING'); - t.deepEqual(desc2.status.name, 'RUNNING'); - - // Confirm terminates and starts new - const handle3 = await t.context.env.client.workflow.signalWithStart(workflows.signalTarget, { - taskQueue, - workflowId: wfid, - signal: workflows.argsTestSignal, - signalArgs: [123, 'kid'], - workflowIdConflictPolicy: 'TERMINATE_EXISTING', - }); - - const descWithRunId = await handleWithRunId.describe(); - const desc3 = await handle3.describe(); - t.true(descWithRunId.runId !== desc3.runId); - t.deepEqual(descWithRunId.status.name, 'TERMINATED'); - t.deepEqual(desc3.status.name, 'RUNNING'); - }); -}); - -test('Start of workflow with signal is delayed', async (t) => { - const { taskQueue } = helpers(t); - // This workflow never runs - const handle = await t.context.env.client.workflow.signalWithStart(workflows.interruptableWorkflow, { - workflowId: randomUUID(), - taskQueue, - startDelay: '4678s', - signal: workflows.interruptSignal, - signalArgs: ['Never called'], - }); - - const { events } = await handle.fetchHistory(); - const workflowExecutionStartedEvent = events?.find((ev) => ev.workflowExecutionStartedEventAttributes); - const startDelay = workflowExecutionStartedEvent?.workflowExecutionStartedEventAttributes?.firstWorkflowTaskBackoff; - t.is(tsToMs(startDelay), 4678000); -}); - -export async function queryWorkflowMetadata(): Promise { - const dummyQuery1 = workflow.defineQuery('dummyQuery1'); - const dummyQuery2 = workflow.defineQuery('dummyQuery2'); - const dummyQuery3 = workflow.defineQuery('dummyQuery3'); - const dummySignal1 = workflow.defineSignal('dummySignal1'); - const dummyUpdate1 = workflow.defineUpdate('dummyUpdate1'); - - workflow.setHandler(dummyQuery1, () => void {}, { description: 'ignore' }); - // Override description - workflow.setHandler(dummyQuery1, () => void {}, { description: 'query1' }); - workflow.setHandler(dummyQuery2, () => void {}, { description: 'query2' }); - workflow.setHandler(dummyQuery3, () => void {}, { description: 'query3' }); - // Remove handler - workflow.setHandler(dummyQuery3, undefined); - workflow.setHandler(dummySignal1, () => void {}, { description: 'signal1' }); - workflow.setHandler(dummyUpdate1, () => void {}, { description: 'update1' }); - await workflow.condition(() => false); -} - -test('Query workflow metadata returns handler descriptions', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - - const worker = await createWorker(); - - await worker.runUntil(async () => { - const handle = await startWorkflow(queryWorkflowMetadata); - const meta = await handle.query(workflow.workflowMetadataQuery); - t.is(meta.definition?.type, 'queryWorkflowMetadata'); - const queryDefinitions = meta.definition?.queryDefinitions; - // Three built-in ones plus dummyQuery1 and dummyQuery2 - t.is(queryDefinitions?.length, 5); - t.deepEqual(queryDefinitions?.[3], { name: 'dummyQuery1', description: 'query1' }); - t.deepEqual(queryDefinitions?.[4], { name: 'dummyQuery2', description: 'query2' }); - const signalDefinitions = meta.definition?.signalDefinitions; - t.deepEqual(signalDefinitions, [{ name: 'dummySignal1', description: 'signal1' }]); - const updateDefinitions = meta.definition?.updateDefinitions; - t.deepEqual(updateDefinitions, [{ name: 'dummyUpdate1', description: 'update1' }]); - }); -}); - -export async function executeEagerActivity(): Promise { - const scheduleActivity = () => - workflow - .proxyActivities({ - scheduleToCloseTimeout: '5s', - allowEagerDispatch: true, - }) - .testActivity() - .then((res) => { - if (res !== 'workflow-and-activity-worker') - throw workflow.ApplicationFailure.nonRetryable('Activity was not eagerly dispatched'); - }); - - for (let i = 0; i < 10; i++) { - // Schedule 3 activities at a time (`MAX_EAGER_ACTIVITY_RESERVATIONS_PER_WORKFLOW_TASK`) - await Promise.all([scheduleActivity(), scheduleActivity(), scheduleActivity()]); - } -} - -test('Worker requests Eager Activity Dispatch if possible', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - - // If eager activity dispatch is working, then the task will always be dispatched to the workflow - // worker. Otherwise, chances are 50%-50% for either workers. The test workflow schedule the - // activity 30 times to make sure that the workflow worker is really getting the task thanks to - // eager activity dispatch, and not out of pure luck. - - const activityWorker = await createWorker({ - activities: { - testActivity: () => 'activity-only-worker', - }, - // Override the default workflow bundle, to make this an activity-only worker - workflowBundle: undefined, - }); - const workflowWorker = await createWorker({ - activities: { - testActivity: () => 'workflow-and-activity-worker', - }, - }); - const handle = await startWorkflow(executeEagerActivity); - await activityWorker.runUntil(workflowWorker.runUntil(handle.result())); - const { events } = await handle.fetchHistory(); - - t.false(events?.some?.((ev) => ev.activityTaskTimedOutEventAttributes)); - const activityTaskStarted = events?.filter?.((ev) => ev.activityTaskStartedEventAttributes); - t.is(activityTaskStarted?.length, 30); - t.true(activityTaskStarted?.every((ev) => ev.activityTaskStartedEventAttributes?.attempt === 1)); -}); - -export async function dontExecuteEagerActivity(): Promise { - return (await workflow - .proxyActivities({ scheduleToCloseTimeout: '5s', allowEagerDispatch: true }) - .testActivity() - .catch(() => 'failed')) as string; -} - -test("Worker doesn't request Eager Activity Dispatch if no activities are registered", async (t) => { - const { createWorker, startWorkflow } = helpers(t); - - // If the activity was eagerly dispatched to the Workflow worker even though it is a Workflow-only - // worker, then the activity execution will timeout (because tasks are not being polled) or - // otherwise fail (because no activity is registered under that name). Therefore, if the history - // shows only one attempt for that activity and no timeout, that can only mean that the activity - // was not eagerly dispatched. - - const activityWorker = await createWorker({ - activities: { - testActivity: () => 'success', - }, - // Override the default workflow bundle, to make this an activity-only worker - workflowBundle: undefined, - }); - const workflowWorker = await createWorker({ - activities: {}, - }); - const handle = await startWorkflow(dontExecuteEagerActivity); - const result = await activityWorker.runUntil(workflowWorker.runUntil(handle.result())); - const { events } = await handle.fetchHistory(); - - t.is(result, 'success'); - t.false(events?.some?.((ev) => ev.activityTaskTimedOutEventAttributes)); - const activityTaskStarted = events?.filter?.((ev) => ev.activityTaskStartedEventAttributes); - t.is(activityTaskStarted?.length, 1); - t.is(activityTaskStarted?.[0]?.activityTaskStartedEventAttributes?.attempt, 1); -}); - -const unblockSignal = defineSignal('unblock'); -const getBuildIdQuery = defineQuery('getBuildId'); - -export async function buildIdTester(): Promise { - let blocked = true; - workflow.setHandler(unblockSignal, () => { - blocked = false; - }); - - workflow.setHandler(getBuildIdQuery, () => { - return workflow.workflowInfo().currentBuildId ?? ''; // eslint-disable-line deprecation/deprecation - }); - - // The unblock signal will only be sent once we are in Worker 1.1. - // Therefore, up to this point, we are runing in Worker 1.0 - await workflow.condition(() => !blocked); - // From this point on, we are runing in Worker 1.1 - - // Prevent workflow completion - await workflow.condition(() => false); -} - -test('Build Id appropriately set in workflow info', async (t) => { - const { taskQueue, createWorker } = helpers(t); - const wfid = `${taskQueue}-` + randomUUID(); - const client = t.context.env.client; - - const worker1 = await createWorker({ buildId: '1.0' }); - await worker1.runUntil(async () => { - const handle = await client.workflow.start(buildIdTester, { - taskQueue, - workflowId: wfid, - }); - t.is(await handle.query(getBuildIdQuery), '1.0'); - }); - - await client.workflowService.resetStickyTaskQueue({ - namespace: worker1.options.namespace, - execution: { workflowId: wfid }, - }); - - const worker2 = await createWorker({ buildId: '1.1' }); - await worker2.runUntil(async () => { - const handle = await client.workflow.getHandle(wfid); - t.is(await handle.query(getBuildIdQuery), '1.0'); - await handle.signal(unblockSignal); - t.is(await handle.query(getBuildIdQuery), '1.1'); - }); -}); - -export async function runDelayedRetryActivities(): Promise { - const startTime = Date.now(); - const localActs = workflow.proxyLocalActivities({ - startToCloseTimeout: '20s', - retry: { initialInterval: '1ms', maximumInterval: '1ms', maximumAttempts: 2 }, - }); - const normalActs = workflow.proxyActivities({ - startToCloseTimeout: '20s', - retry: { initialInterval: '1ms', maximumInterval: '1ms', maximumAttempts: 2 }, - }); - await Promise.all([localActs.testActivity(), normalActs.testActivity()]); - const endTime = Date.now(); - if (endTime - startTime < 2000) { - throw ApplicationFailure.nonRetryable('Expected workflow to take at least 2 seconds to complete'); - } -} - -test('nextRetryDelay for activities', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - async testActivity() { - // Need to fail on first try - if (activity.activityInfo().attempt === 1) { - throw ApplicationFailure.create({ message: 'ahh', nextRetryDelay: '2s' }); - } - }, - }, - }); - const handle = await startWorkflow(runDelayedRetryActivities); - await worker.runUntil(handle.result()); - t.pass(); -}); - -// Repro for https://github.com/temporalio/sdk-typescript/issues/1423 -export async function issue1423Workflow(legacyCompatibility: boolean): Promise<'threw' | 'didnt-throw'> { - overrideSdkInternalFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation, !legacyCompatibility); - try { - workflow.CancellationScope.current().cancel(); - // We expect this to throw a CancellationException - await workflow.sleep(1); - throw workflow.ApplicationFailure.nonRetryable("sleep in cancelled scope didn't throw"); - } catch (_err) { - return await workflow.CancellationScope.nonCancellable(async () => { - try { - await workflow.condition(() => false, 1); - return 'didnt-throw'; // that's the correct behavior - } catch (error) { - if (workflow.isCancellation(error)) { - return 'threw'; // that's what would happen until 1.10.2 - } - throw error; // Shouldn't happen - } - }); - } -} - -// Validate that issue #1423 is fixed in 1.10.3+ -test('issue-1423 - 1.10.3+', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker({}); - const conditionResult = await worker.runUntil(async () => { - return await executeWorkflow(issue1423Workflow, { args: [false] }); - }); - t.is('didnt-throw', conditionResult); -}); - -// Validate that issue #1423 behavior is maintained in 1.10.2 in replay mode -test('issue-1423 - legacy', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker(); - const conditionResult = await worker.runUntil(async () => { - return await executeWorkflow(issue1423Workflow, { args: [true] }); - }); - t.is('threw', conditionResult); -}); - -export async function nonCancellableScopesBeforeAndAfterWorkflow(): Promise<[boolean, boolean]> { - // Start in legacy mode, similar to replaying an execution from a pre-1.10.3 workflow - overrideSdkInternalFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation, false); - - const parentScope1 = new workflow.CancellationScope({ cancellable: false }); - const childScope1 = new workflow.CancellationScope({ cancellable: true, parent: parentScope1 }); - const parentScope2 = new workflow.CancellationScope({ cancellable: false }); - const childScope2 = new workflow.CancellationScope({ cancellable: true, parent: parentScope2 }); - - parentScope1.cancel(); - await Promise.resolve(); - const childScope1Cancelled = childScope1.consideredCancelled; - - // Now enable the fix - overrideSdkInternalFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation, true); - parentScope2.cancel(); - await Promise.resolve(); - const childScope2Cancelled = childScope2.consideredCancelled; - - return [childScope1Cancelled, childScope2Cancelled]; -} - -test('Propagation of cancellation from non-cancellable scopes - before vs after', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker(); - const [childScope1Cancelled, childScope2Cancelled] = await worker.runUntil( - executeWorkflow(nonCancellableScopesBeforeAndAfterWorkflow) - ); - t.true(childScope1Cancelled); - t.false(childScope2Cancelled); -}); - -// The following workflow is used to extensively test CancellationScopes cancellation propagation in various scenarios -export async function cancellableScopesExtensiveChecksWorkflow( - parentCancellable: boolean, - childCancellable: boolean, - legacyCompatibility: boolean -): Promise { - overrideSdkInternalFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation, !legacyCompatibility); - - function expectCancellation(p: Promise): () => boolean { - let cancelled = false; - let exception: Error | undefined = undefined; - p.catch((e) => { - if (workflow.isCancellation(e)) { - cancelled = true; - } else { - exception = e; - } - }); - return () => { - if (exception) throw exception; - return cancelled; - }; - } - - // A non-existant child workflow that we'll use to send (and cancel) signals - const signalTargetWorkflow = await workflow.startChild('not-existant', { - taskQueue: 'not-existant', - workflowRunTimeout: '60s', - }); - - const { someActivity } = workflow.proxyActivities({ - scheduleToCloseTimeout: 5000, - taskQueue: 'non-existant', - }); - const { sleepLA } = workflow.proxyLocalActivities({ scheduleToCloseTimeout: 5000 }); - - const checks: { [k in keyof CancellableScopesExtensiveChecks]?: ReturnType } = {}; - - // This will not block/throw, as the run function itself doesn't actually await on promises created inside - const parentScope = new workflow.CancellationScope({ cancellable: parentCancellable }); - await parentScope.run(async () => { - checks.parentScope_timerCancelled = expectCancellation(workflow.sleep(2000)); - checks.parentScope_activityCancelled = expectCancellation(someActivity()); - checks.parentScope_localActivityCancelled = expectCancellation(sleepLA(2000)); - checks.parentScope_signalExtWorkflowCancelled = expectCancellation(signalTargetWorkflow.signal('signal')); - }); - checks.parentScope_cancelRequestedCancelled = expectCancellation(parentScope.cancelRequested); - - // This will not block/throw, as the run function itself doesn't actually await on promises created inside - const childScope = new workflow.CancellationScope({ - cancellable: childCancellable, - parent: parentScope, - }); - await childScope.run(async () => { - checks.childScope_timerCancelled = expectCancellation(workflow.sleep(2000)); - checks.childScope_activityCancelled = expectCancellation(someActivity()); - checks.childScope_localActivityCancelled = expectCancellation(sleepLA(2000)); - checks.childScope_signalExtWorkflowCancelled = expectCancellation(signalTargetWorkflow.signal('signal')); - }); - checks.childScope_cancelRequestedCancelled = expectCancellation(childScope.cancelRequested); - - parentScope.cancel(); - - // Flush all commands to Core, so that cancellations get a chance to be processed - await sleepLA(1); - - return { - ...Object.fromEntries(Object.entries(checks).map(([k, v]) => [k, v()] as const)), - parentScope_consideredCancelled: parentScope.consideredCancelled, - childScope_consideredCancelled: childScope.consideredCancelled, - } as unknown as CancellableScopesExtensiveChecks; -} - -interface CancellableScopesExtensiveChecks { - parentScope_cancelRequestedCancelled: boolean; - parentScope_timerCancelled: boolean; - parentScope_activityCancelled: boolean; - parentScope_localActivityCancelled: boolean; - parentScope_signalExtWorkflowCancelled: boolean; - parentScope_consideredCancelled: boolean; - - childScope_cancelRequestedCancelled: boolean; - childScope_timerCancelled: boolean; - childScope_activityCancelled: boolean; - childScope_localActivityCancelled: boolean; - childScope_signalExtWorkflowCancelled: boolean; - childScope_consideredCancelled: boolean; -} - -async function cancellableScopesExtensiveChecksHelper( - t: ExecutionContext, - parentCancellable: boolean, - childCancellable: boolean, - legacyCompatibility: boolean, - expected: CancellableScopesExtensiveChecks -) { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - sleepLA: activity.sleep, - }, - }); - - await worker.runUntil(async () => { - // cancellable/cancellable - t.deepEqual( - await executeWorkflow(cancellableScopesExtensiveChecksWorkflow, { - args: [parentCancellable, childCancellable, legacyCompatibility], - }), - expected - ); - }); -} - -test('CancellableScopes extensive checks - cancelleable/cancellable - 1.10.3+', async (t) => { - await cancellableScopesExtensiveChecksHelper(t, true, true, false, { - parentScope_cancelRequestedCancelled: true, - parentScope_timerCancelled: true, - parentScope_activityCancelled: true, - parentScope_localActivityCancelled: true, - parentScope_signalExtWorkflowCancelled: true, - parentScope_consideredCancelled: true, - - childScope_cancelRequestedCancelled: true, - childScope_timerCancelled: true, - childScope_activityCancelled: true, - childScope_localActivityCancelled: true, - childScope_signalExtWorkflowCancelled: true, - childScope_consideredCancelled: true, - }); -}); - -test('CancellableScopes extensive checks - cancelleable/cancellable - legacy', async (t) => { - await cancellableScopesExtensiveChecksHelper(t, true, true, true, { - parentScope_cancelRequestedCancelled: true, - parentScope_timerCancelled: true, - parentScope_activityCancelled: true, - parentScope_localActivityCancelled: true, - parentScope_signalExtWorkflowCancelled: true, - parentScope_consideredCancelled: true, - - childScope_cancelRequestedCancelled: true, - childScope_timerCancelled: true, - childScope_activityCancelled: true, - childScope_localActivityCancelled: true, - childScope_signalExtWorkflowCancelled: true, - childScope_consideredCancelled: true, - }); -}); - -test('CancellableScopes extensive checks - cancelleable/non-cancellable - 1.10.3+', async (t) => { - await cancellableScopesExtensiveChecksHelper(t, true, false, false, { - parentScope_cancelRequestedCancelled: true, - parentScope_timerCancelled: true, - parentScope_activityCancelled: true, - parentScope_localActivityCancelled: true, - parentScope_signalExtWorkflowCancelled: true, - parentScope_consideredCancelled: true, - - childScope_cancelRequestedCancelled: true, - childScope_timerCancelled: false, - childScope_activityCancelled: false, - childScope_localActivityCancelled: false, - childScope_signalExtWorkflowCancelled: false, - childScope_consideredCancelled: false, - }); -}); - -test('CancellableScopes extensive checks - cancelleable/non-cancellable - legacy', async (t) => { - await cancellableScopesExtensiveChecksHelper(t, true, false, true, { - parentScope_cancelRequestedCancelled: true, - parentScope_timerCancelled: true, - parentScope_activityCancelled: true, - parentScope_localActivityCancelled: true, - parentScope_signalExtWorkflowCancelled: true, - parentScope_consideredCancelled: true, - - childScope_cancelRequestedCancelled: true, - childScope_timerCancelled: false, - childScope_activityCancelled: false, - childScope_localActivityCancelled: false, - childScope_signalExtWorkflowCancelled: false, - childScope_consideredCancelled: false, - }); -}); - -test('CancellableScopes extensive checks - non-cancelleable/cancellable - 1.10.3+', async (t) => { - await cancellableScopesExtensiveChecksHelper(t, false, true, false, { - parentScope_cancelRequestedCancelled: true, - parentScope_timerCancelled: false, - parentScope_activityCancelled: false, - parentScope_localActivityCancelled: false, - parentScope_signalExtWorkflowCancelled: false, - parentScope_consideredCancelled: false, - - childScope_cancelRequestedCancelled: false, - childScope_timerCancelled: false, - childScope_activityCancelled: false, - childScope_localActivityCancelled: false, - childScope_signalExtWorkflowCancelled: false, - childScope_consideredCancelled: false, - }); -}); - -test('CancellableScopes extensive checks - non-cancelleable/cancellable - legacy', async (t) => { - await cancellableScopesExtensiveChecksHelper(t, false, true, true, { - parentScope_cancelRequestedCancelled: true, - parentScope_timerCancelled: false, - parentScope_activityCancelled: false, - parentScope_localActivityCancelled: false, - parentScope_signalExtWorkflowCancelled: false, - parentScope_consideredCancelled: false, - - childScope_cancelRequestedCancelled: true, // These were incorrect before 1.10.3 - childScope_timerCancelled: true, - childScope_activityCancelled: true, - childScope_localActivityCancelled: true, - childScope_signalExtWorkflowCancelled: true, - childScope_consideredCancelled: true, - }); -}); - -test('CancellableScopes extensive checks - non-cancelleable/non-cancellable - 1.10.3+', async (t) => { - await cancellableScopesExtensiveChecksHelper(t, false, false, false, { - parentScope_cancelRequestedCancelled: true, - parentScope_timerCancelled: false, - parentScope_activityCancelled: false, - parentScope_localActivityCancelled: false, - parentScope_signalExtWorkflowCancelled: false, - parentScope_consideredCancelled: false, - - childScope_cancelRequestedCancelled: false, - childScope_timerCancelled: false, - childScope_activityCancelled: false, - childScope_localActivityCancelled: false, - childScope_signalExtWorkflowCancelled: false, - childScope_consideredCancelled: false, - }); -}); - -test('CancellableScopes extensive checks - non-cancelleable/non-cancellable - legacy', async (t) => { - await cancellableScopesExtensiveChecksHelper(t, false, false, true, { - parentScope_cancelRequestedCancelled: true, - parentScope_timerCancelled: false, - parentScope_activityCancelled: false, - parentScope_localActivityCancelled: false, - parentScope_signalExtWorkflowCancelled: false, - parentScope_consideredCancelled: false, - - childScope_cancelRequestedCancelled: true, - childScope_timerCancelled: false, - childScope_activityCancelled: false, - childScope_localActivityCancelled: false, - childScope_signalExtWorkflowCancelled: false, - childScope_consideredCancelled: false, - }); -}); - -export async function cancellationScopeWithTimeoutTimerGetsCancelled(): Promise<[boolean, boolean]> { - const { activitySleep } = workflow.proxyActivities({ scheduleToCloseTimeout: '7s' }); - - // Start in legacy mode, similar to replaying an execution from a pre-1.10.3 workflow - overrideSdkInternalFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation, false); - - let scope1: workflow.CancellationScope; - await workflow.CancellationScope.withTimeout('11s', async () => { - scope1 = workflow.CancellationScope.current(); - await activitySleep(1); - // Legacy mode: this timer will not be cancelled - }); - - let scope2: workflow.CancellationScope; - await workflow.CancellationScope.withTimeout('12s', async () => { - scope2 = workflow.CancellationScope.current(); - await activitySleep(1); - overrideSdkInternalFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation, true); - // Fix enabled: this timer will get cancelled - }); - - // Timer cancelation won't appear in history if it sent in the same WFT as workflow complete - await activitySleep(1); - - //@ts-expect-error TSC can't see that scope variables will be initialized synchronously - return [scope1.consideredCancelled, scope2.consideredCancelled]; -} - -test('CancellationScope.withTimeout() - timer gets cancelled', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - activitySleep: activity.sleep, - }, - }); - const handle = await startWorkflow(cancellationScopeWithTimeoutTimerGetsCancelled); - const [scope1Cancelled, scope2Cancelled] = await worker.runUntil(handle.result()); - - t.false(scope1Cancelled); - t.false(scope2Cancelled); - - const { events } = await handle.fetchHistory(); - - const timerCanceledEvents = events?.filter((ev) => ev.timerCanceledEventAttributes) ?? []; - t.is(timerCanceledEvents?.length, 1); - - const timerStartedEventId = timerCanceledEvents[0].timerCanceledEventAttributes?.startedEventId; - const timerStartedEvent = events?.find((ev) => ev.eventId?.toNumber() === timerStartedEventId?.toNumber()); - t.is(tsToMs(timerStartedEvent?.timerStartedEventAttributes?.startToFireTimeout), msToNumber('12s')); -}); - -export async function cancellationScopeWithTimeoutScopeGetCancelledOnTimeout(): Promise<[boolean, boolean]> { - const { activitySleep } = workflow.proxyActivities({ scheduleToCloseTimeout: '10s' }); - - // Start in legacy mode, similar to replaying an execution from a pre-1.10.3 workflow - overrideSdkInternalFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation, false); - let scope1: workflow.CancellationScope; - await workflow.CancellationScope.withTimeout(1, async () => { - scope1 = workflow.CancellationScope.current(); - await activitySleep(7000); - }).catch(() => undefined); - - let scope2: workflow.CancellationScope; - await workflow.CancellationScope.withTimeout(1, async () => { - scope2 = workflow.CancellationScope.current(); - // Turn on CancellationScopeMultipleFixes to confirm that behavior didn't change - overrideSdkInternalFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation, true); - await activitySleep(7000); - }).catch(() => undefined); - - // Activity cancelation won't appear in history if it sent in the same WFT as workflow complete - await activitySleep(1); - - //@ts-expect-error TSC can't see that scope variables will be initialized synchronously - return [scope1.consideredCancelled, scope2.consideredCancelled]; -} - -test('CancellationScope.withTimeout() - scope gets cancelled on timeout', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - activitySleep: activity.sleep, - }, - }); - const handle = await startWorkflow(cancellationScopeWithTimeoutScopeGetCancelledOnTimeout); - const [scope1Cancelled, scope2Cancelled] = await worker.runUntil(handle.result()); - - t.true(scope1Cancelled); - t.true(scope2Cancelled); - - const { events } = await handle.fetchHistory(); - - const activityCancelledEvents = events?.filter((ev) => ev.activityTaskCancelRequestedEventAttributes) ?? []; - t.is(activityCancelledEvents?.length, 2); -}); - -export async function setAndClearTimeout(): Promise { - const { activitySleep } = workflow.proxyActivities({ scheduleToCloseTimeout: '10m' }); - - // Start in legacy mode, similar to replaying an execution from a pre-1.10.3 workflow - overrideSdkInternalFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation, false); - - const timerFired: boolean[] = [false, false, false, false, false]; - - // This timer will get cleared immediately; it should never fires - const timer0Handle = setTimeout(() => (timerFired[0] = true), 20_000); - await activitySleep(1); - clearTimeout(timer0Handle); - - // This timer will never get cancelled; it should fire - setTimeout(() => (timerFired[1] = true), 21_000); - await activitySleep(1); - - // This timer will get cleared after enabling the fix; it should never fire - const timer2Handle = setTimeout(() => (timerFired[2] = true), 22_000); - await activitySleep(1); - overrideSdkInternalFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation, true); - clearTimeout(timer2Handle); - - // This timer will get cancelled immediately; it should never fire - const timer3Handle = setTimeout(() => (timerFired[3] = true), 23_000); - await activitySleep(1); - clearTimeout(timer3Handle); - - // This timer will never get cancelled; it should fire - setTimeout(() => (timerFired[4] = true), 24_000); - - // Give time for timers to fire - await activitySleep('2m'); - - return timerFired; -} - -export function setAndClearTimeoutInterceptors(): workflow.WorkflowInterceptors { - return { - outbound: [ - { - async startTimer(input, next): Promise { - // Add 500ms to the duration of the timer; we'll look for that - return next({ ...input, durationMs: input.durationMs + 500 }); - }, - }, - ], - }; -} - -if (RUN_TIME_SKIPPING_TESTS) { - 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 { - const worker = await createWorker({ - activities: { - activitySleep: env.sleep, - }, - }); - const handle = await startWorkflow(setAndClearTimeout); - const timerFired: boolean[] = await worker.runUntil(handle.result()); - - t.false(timerFired[0]); - t.true(timerFired[1]); - t.false(timerFired[2]); - t.false(timerFired[3]); - t.true(timerFired[4]); - - const { events } = await handle.fetchHistory(); - const timerStartedEvents = events?.filter((ev) => ev.timerStartedEventAttributes); - t.is(timerStartedEvents?.length, 5); - // Durations that ends with 500ms are the ones that were intercepted - t.is(tsToMs(timerStartedEvents?.[0].timerStartedEventAttributes?.startToFireTimeout), 20_000); - t.is(tsToMs(timerStartedEvents?.[1].timerStartedEventAttributes?.startToFireTimeout), 21_000); - t.is(tsToMs(timerStartedEvents?.[2].timerStartedEventAttributes?.startToFireTimeout), 22_000); - t.is(tsToMs(timerStartedEvents?.[3].timerStartedEventAttributes?.startToFireTimeout), 23_500); - t.is(tsToMs(timerStartedEvents?.[4].timerStartedEventAttributes?.startToFireTimeout), 24_500); - } finally { - await env.teardown(); - } - }); -} - -export async function upsertAndReadMemo(memo: Record): Promise | undefined> { - workflow.upsertMemo(memo); - return workflow.workflowInfo().memo; -} - -test('Workflow can upsert memo', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const handle = await startWorkflow(upsertAndReadMemo, { - memo: { - alpha: 'bar1', - bravo: 'bar3', - charlie: { delta: 'bar2', echo: 12 }, - foxtrot: 'bar4', - }, - args: [ - { - alpha: 'bar11', - bravo: null, - charlie: { echo: 34, golf: 'bar5' }, - hotel: 'bar6', - }, - ], - }); - const result = await handle.result(); - t.deepEqual(result, { - alpha: 'bar11', - charlie: { echo: 34, golf: 'bar5' }, - foxtrot: 'bar4', - hotel: 'bar6', - }); - const { memo } = await handle.describe(); - t.deepEqual(memo, { - alpha: 'bar11', - charlie: { echo: 34, golf: 'bar5' }, - foxtrot: 'bar4', - hotel: 'bar6', - }); - }); -}); - -export async function langFlagsReplayCorrectly(): Promise { - const { noopActivity } = workflow.proxyActivities({ scheduleToCloseTimeout: '10s' }); - await workflow.CancellationScope.withTimeout('10s', async () => { - await noopActivity(); - }); -} - -test("Lang's SDK flags replay correctly", async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - noopActivity: () => {}, - }, - }); - - const handle = await startWorkflow(langFlagsReplayCorrectly); - await worker.runUntil(() => handle.result()); - - const worker2 = await createWorker(); - await worker2.runUntil(() => handle.query('__stack_trace')); - - // Query would have thrown if the workflow couldn't be replayed correctly - t.pass(); -}); - -test("Lang's SDK flags - History from before 1.11.0 replays correctly", async (t) => { - const { runReplayHistory } = helpers(t); - const hist = await loadHistory('lang_flags_replay_correctly_1_9_3.json'); - await runReplayHistory({}, hist); - t.pass(); -}); - -// Context: Due to a bug in 1.11.0 and 1.11.1, SDK flags that were set in those versions were not -// persisted to history. To avoid NDEs on histories produced by those releases, we check the Build -// ID for the SDK version number, and retroactively set some flags on these histories. -test("Lang's SDK flags - Flags from 1.11.[01] are retroactively applied on replay", async (t) => { - const { runReplayHistory } = helpers(t); - const hist = await loadHistory('lang_flags_replay_correctly_1_11_1.json'); - await runReplayHistory({}, hist); - t.pass(); -}); - -test("Lang's SDK flags from 1.11.2 are retroactively applied on replay", async (t) => { - const { runReplayHistory } = helpers(t); - const hist = await loadHistory('lang_flags_replay_correctly_1_11_2.json'); - await runReplayHistory({}, hist); - t.pass(); -}); - -export async function cancelAbandonActivityBeforeStarted(): Promise { - const { activitySleep } = workflow.proxyActivities({ - scheduleToCloseTimeout: '1m', - cancellationType: ActivityCancellationType.ABANDON, - }); - const cancelScope = new workflow.CancellationScope({ cancellable: true }); - const prom = cancelScope.run(async () => { - await activitySleep(1000); - }); - cancelScope.cancel(); - try { - await prom; - } catch { - // do nothing - } -} - -test('Abandon activity cancel before started works', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - activitySleep: activity.sleep, - }, - }); - const handle = await startWorkflow(cancelAbandonActivityBeforeStarted); - await worker.runUntil(handle.result()); - - t.pass(); -}); - -export async function WorkflowWillFail(): Promise { - if (workflow.workflowInfo().attempt > 1) { - return workflow.workflowInfo().lastFailure?.message; - } - throw ApplicationFailure.retryable('WorkflowWillFail', 'WorkflowWillFail'); -} - -test("WorkflowInfo().lastFailure contains last run's failure on Workflow Failure", async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - const handle = await startWorkflow(WorkflowWillFail, { retry: { maximumAttempts: 2 } }); - await worker.runUntil(async () => { - const lastFailure = await handle.result(); - t.is(lastFailure, 'WorkflowWillFail'); - }); -}); - -export const interceptors: workflow.WorkflowInterceptorsFactory = () => { - const interceptorsFactoryFunc = module.exports[`${workflow.workflowInfo().workflowType}Interceptors`]; - if (typeof interceptorsFactoryFunc === 'function') { - return interceptorsFactoryFunc(); - } - return {}; -}; - -export async function completableWorkflow(completes: boolean): Promise { - await workflow.condition(() => completes); -} - -test('Count workflow executions', async (t) => { - const { taskQueue, createWorker, executeWorkflow, startWorkflow } = helpers(t); - const worker = await createWorker(); - const client = t.context.env.client; - - await worker.runUntil(async () => { - await Promise.all([ - // Run 2 workflows that will never complete... - startWorkflow(completableWorkflow, { args: [false] }), - startWorkflow(completableWorkflow, { args: [false] }), - - // ... and 3 workflows that will complete - executeWorkflow(completableWorkflow, { args: [true] }), - executeWorkflow(completableWorkflow, { args: [true] }), - executeWorkflow(completableWorkflow, { args: [true] }), - ]); - }); - - // FIXME: Find a better way to wait for visibility to stabilize - await setTimeoutPromise(1000); - - const actualTotal = await client.workflow.count(`TaskQueue = '${taskQueue}'`); - t.deepEqual(actualTotal, { count: 5, groups: [] }); - - const actualByExecutionStatus = await client.workflow.count(`TaskQueue = '${taskQueue}' GROUP BY ExecutionStatus`); - t.deepEqual(actualByExecutionStatus, { - count: 5, - groups: [ - { count: 2, groupValues: [['Running']] }, - { count: 3, groupValues: [['Completed']] }, - ], - }); -}); - -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 }; - - // Create new test environment with search attribute registered. - const env = await createLocalTestEnvironment({ - server: { - searchAttributes: [key], - }, - }); - - const newClient = env.client; - // Expect workflow with search attribute to start without error. - const handle = await newClient.workflow.start(completableWorkflow, { - args: [true], - workflowId: randomUUID(), - taskQueue: 'new-env-tq', - typedSearchAttributes: [newSearchAttribute], - }); - // Expect workflow description to have search attribute. - const desc = await handle.describe(); - t.deepEqual(desc.typedSearchAttributes, new TypedSearchAttributes([newSearchAttribute])); - t.deepEqual(desc.searchAttributes, { 'new-search-attr': [12] }); // eslint-disable-line deprecation/deprecation - await env.teardown(); -}); - -export async function rawValueWorkflow(value: unknown, isPayload: boolean = false): Promise { - const { rawValueActivity } = workflow.proxyActivities({ startToCloseTimeout: '10s' }); - const rv = isPayload - ? RawValue.fromPayload({ - metadata: { [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_RAW }, - data: value as Uint8Array, - }) - : new RawValue(value); - return await rawValueActivity(rv, isPayload); -} - -test('workflow and activity can receive/return RawValue', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async rawValueActivity(value: unknown, isPayload: boolean = false): Promise { - const rv = isPayload - ? RawValue.fromPayload({ - metadata: { [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_RAW }, - data: value as Uint8Array, - }) - : new RawValue(value); - return rv; - }, - }, - }); - - await worker.runUntil(async () => { - const testValue = 'test'; - const rawValue = new RawValue(testValue); - const rawValuePayload = RawValue.fromPayload({ - metadata: { [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_RAW }, - data: encode(testValue), - }); - const res = await executeWorkflow(rawValueWorkflow, { - args: [rawValue], - }); - t.deepEqual(res, testValue); - const res2 = await executeWorkflow(rawValueWorkflow, { - args: [rawValuePayload, true], - }); - t.deepEqual(res2, encode(testValue)); - }); -}); - -export async function ChildWorkflowInfo(): Promise { - let blocked = true; - workflow.setHandler(unblockSignal, () => { - blocked = false; - }); - await workflow.condition(() => !blocked); - return workflow.workflowInfo().root; -} - -export async function WithChildWorkflow(childWfId: string): Promise { - return await workflow.executeChild(ChildWorkflowInfo, { - workflowId: childWfId, - }); -} - -test('root execution is exposed', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - - await worker.runUntil(async () => { - const childWfId = 'child-wf-id'; - const handle = await startWorkflow(WithChildWorkflow, { - args: [childWfId], - }); - - const childHandle = t.context.env.client.workflow.getHandle(childWfId); - const childStarted = async (): Promise => { - try { - await childHandle.describe(); - return true; - } catch (e) { - if (e instanceof workflow.WorkflowNotFoundError) { - return false; - } else { - throw e; - } - } - }; - await waitUntil(childStarted, 5000); - const childDesc = await childHandle.describe(); - const parentDesc = await handle.describe(); - - t.true(childDesc.rootExecution?.workflowId === parentDesc.workflowId); - t.true(childDesc.rootExecution?.runId === parentDesc.runId); - - await childHandle.signal(unblockSignal); - const childWfInfoRoot = await handle.result(); - t.true(childWfInfoRoot?.workflowId === parentDesc.workflowId); - t.true(childWfInfoRoot?.runId === parentDesc.runId); - }); -}); - -export async function rootWorkflow(): Promise { - let result = ''; - if (!workflow.workflowInfo().root) { - result += 'empty'; - } else { - result += workflow.workflowInfo().root!.workflowId; - } - if (!workflow.workflowInfo().parent) { - result += ' '; - result += await workflow.executeChild(rootWorkflow); - } - return result; -} - -test('Workflow can return root workflow', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const result = await executeWorkflow(rootWorkflow, { workflowId: 'test-root-workflow-length' }); - t.deepEqual(result, 'empty test-root-workflow-length'); - }); -}); - -export async function heartbeatCancellationWorkflow( - state: ActivityState -): Promise { - const { heartbeatCancellationDetailsActivity } = workflow.proxyActivities({ - startToCloseTimeout: '5s', - retry: { - maximumAttempts: 2, - }, - heartbeatTimeout: '1s', - }); - - return await heartbeatCancellationDetailsActivity(state); -} - -test('Activity pause returns expected cancellation details', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - heartbeatCancellationDetailsActivity, - }, - }); - - await worker.runUntil(async () => { - const result = await executeWorkflow(heartbeatCancellationWorkflow, { - args: [{ pause: true }], - }); - - t.deepEqual(result, { - cancelRequested: false, - notFound: false, - paused: true, - timedOut: false, - workerShutdown: false, - reset: false, - }); - }); -}); - -test('Activity can be cancelled via pause and retry after unpause', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - - const worker = await createWorker({ - activities: { - heartbeatCancellationDetailsActivity, - }, - }); - - await worker.runUntil(async () => { - const result = await executeWorkflow(heartbeatCancellationWorkflow, { - args: [{ pause: true, unpause: true, shouldRetry: true }], - }); - // Note that we expect the result to be null because unpausing an activity - // resets the activity context (akin to starting the activity anew) - t.true(result == null); - }); -}); - -test('Activity reset without retry returns expected cancellation details', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - heartbeatCancellationDetailsActivity, - }, - }); - - await worker.runUntil(async () => { - const result = await executeWorkflow(heartbeatCancellationWorkflow, { args: [{ reset: true }] }); - t.deepEqual(result, { - cancelRequested: false, - notFound: false, - paused: false, - timedOut: false, - workerShutdown: false, - reset: true, - }); - }); -}); - -test('Activity reset with retry returns expected cancellation details', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - heartbeatCancellationDetailsActivity, - }, - }); - - await worker.runUntil(async () => { - const result = await executeWorkflow(heartbeatCancellationWorkflow, { args: [{ reset: true, shouldRetry: true }] }); - t.true(result == null); - }); -}); - -test('Activity paused and reset returns expected cancellation details', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker({ - activities: { - heartbeatCancellationDetailsActivity, - }, - }); - - await worker.runUntil(async () => { - const result = await executeWorkflow(heartbeatCancellationWorkflow, { args: [{ pause: true, reset: true }] }); - t.deepEqual(result, { - cancelRequested: false, - notFound: false, - paused: true, - timedOut: false, - workerShutdown: false, - reset: true, - }); - }); -}); - -const reservedNames = [TEMPORAL_RESERVED_PREFIX, STACK_TRACE_QUERY_NAME, ENHANCED_STACK_TRACE_QUERY_NAME]; - -test('Cannot register activities using reserved prefixes', async (t) => { - const { createWorker } = helpers(t); - - for (const name of reservedNames) { - const activityName = name === TEMPORAL_RESERVED_PREFIX ? name + '_test' : name; - await t.throwsAsync( - createWorker({ - activities: { [activityName]: () => {} }, - }), - { - name: 'TypeError', - message: - name === TEMPORAL_RESERVED_PREFIX - ? `Cannot use activity name: '${activityName}', with reserved prefix: '${name}'` - : `Cannot use activity name: '${activityName}', which is a reserved name`, - } - ); - } -}); - -test('Cannot register task queues using reserved prefixes', async (t) => { - const { createWorker } = helpers(t); - - for (const name of reservedNames) { - const taskQueue = name === TEMPORAL_RESERVED_PREFIX ? name + '_test' : name; - - await t.throwsAsync( - createWorker({ - taskQueue, - }), - { - name: 'TypeError', - message: - name === TEMPORAL_RESERVED_PREFIX - ? `Cannot use task queue name: '${taskQueue}', with reserved prefix: '${name}'` - : `Cannot use task queue name: '${taskQueue}', which is a reserved name`, - } - ); - } -}); - -test('Cannot register sinks using reserved prefixes', async (t) => { - const { createWorker } = helpers(t); - - for (const name of reservedNames) { - const sinkName = name === TEMPORAL_RESERVED_PREFIX ? name + '_test' : name; - await t.throwsAsync( - createWorker({ - sinks: { - [sinkName]: { - test: { - fn: () => {}, - }, - }, - }, - }), - { - name: 'TypeError', - message: - name === TEMPORAL_RESERVED_PREFIX - ? `Cannot use sink name: '${sinkName}', with reserved prefix: '${name}'` - : `Cannot use sink name: '${sinkName}', which is a reserved name`, - } - ); - } -}); - -interface HandlerError { - name: string; - message: string; -} - -export async function workflowReservedNameHandler(name: string): Promise { - // Re-package errors, default payload converter has trouble converting native errors (no 'data' field). - const expectedErrors: HandlerError[] = []; - try { - setHandler(defineSignal(name === TEMPORAL_RESERVED_PREFIX ? name + '_signal' : name), () => {}); - } catch (e) { - if (e instanceof Error) { - expectedErrors.push({ name: e.name, message: e.message }); - } - } - try { - setHandler(defineUpdate(name === TEMPORAL_RESERVED_PREFIX ? name + '_update' : name), () => {}); - } catch (e) { - if (e instanceof Error) { - expectedErrors.push({ name: e.name, message: e.message }); - } - } - try { - setHandler(defineQuery(name === TEMPORAL_RESERVED_PREFIX ? name + '_query' : name), () => {}); - } catch (e) { - if (e instanceof Error) { - expectedErrors.push({ name: e.name, message: e.message }); - } - } - return expectedErrors; -} - -test('Workflow failure if define signals/updates/queries with reserved prefixes', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - for (const name of reservedNames) { - const result = await executeWorkflow(workflowReservedNameHandler, { - args: [name], - }); - t.deepEqual(result, [ - { - name: 'TypeError', - message: - name === TEMPORAL_RESERVED_PREFIX - ? `Cannot use signal name: '${name}_signal', with reserved prefix: '${name}'` - : `Cannot use signal name: '${name}', which is a reserved name`, - }, - { - name: 'TypeError', - message: - name === TEMPORAL_RESERVED_PREFIX - ? `Cannot use update name: '${name}_update', with reserved prefix: '${name}'` - : `Cannot use update name: '${name}', which is a reserved name`, - }, - { - name: 'TypeError', - message: - name === TEMPORAL_RESERVED_PREFIX - ? `Cannot use query name: '${name}_query', with reserved prefix: '${name}'` - : `Cannot use query name: '${name}', which is a reserved name`, - }, - ]); - } - }); -}); - -export const wfReadyQuery = defineQuery('wf-ready'); -export async function workflowWithDefaultHandlers(): Promise { - let unblocked = false; - setHandler(defineSignal('unblock'), () => { - unblocked = true; - }); - - setDefaultQueryHandler(() => {}); - setDefaultSignalHandler(() => {}); - setDefaultUpdateHandler(() => {}); - setHandler(wfReadyQuery, () => true); - - await condition(() => unblocked); -} - -test('Default handlers fail given reserved prefix', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - - const assertWftFailure = async (handle: WorkflowHandle, errMsg: string) => { - await asyncRetry( - async () => { - const history = await handle.fetchHistory(); - const wftFailedEvent = history.events?.findLast((ev) => ev.workflowTaskFailedEventAttributes); - if (wftFailedEvent === undefined) { - throw new Error('No WFT failed event found'); - } - const { failure } = wftFailedEvent.workflowTaskFailedEventAttributes ?? {}; - if (!failure) { - return t.fail('Expected failure in workflowTaskFailedEventAttributes'); - } - t.is(failure.message, errMsg); - }, - { minTimeout: 300, factor: 1, retries: 10 } - ); - }; - - await worker.runUntil(async () => { - // Reserved query - let handle = await startWorkflow(workflowWithDefaultHandlers); - await asyncRetry(async () => { - if (!(await handle.query(wfReadyQuery))) { - throw new Error('Workflow not ready yet'); - } - }); - const queryName = `${TEMPORAL_RESERVED_PREFIX}_query`; - await t.throwsAsync( - handle.query(queryName), - { - // TypeError transforms to a QueryNotRegisteredError on the way back from server - name: 'QueryNotRegisteredError', - message: `Cannot use query name: '${queryName}', with reserved prefix: '${TEMPORAL_RESERVED_PREFIX}'`, - }, - `Query ${queryName} should fail` - ); - await handle.terminate(); - - // Reserved signal - handle = await startWorkflow(workflowWithDefaultHandlers); - await asyncRetry(async () => { - if (!(await handle.query(wfReadyQuery))) { - throw new Error('Workflow not ready yet'); - } - }); - const signalName = `${TEMPORAL_RESERVED_PREFIX}_signal`; - await handle.signal(signalName); - await assertWftFailure( - handle, - `Cannot use signal name: '${signalName}', with reserved prefix: '${TEMPORAL_RESERVED_PREFIX}'` - ); - await handle.terminate(); - - // Reserved update - handle = await startWorkflow(workflowWithDefaultHandlers); - await asyncRetry(async () => { - if (!(await handle.query(wfReadyQuery))) { - throw new Error('Workflow not ready yet'); - } - }); - const updateName = `${TEMPORAL_RESERVED_PREFIX}_update`; - handle.executeUpdate(updateName).catch(() => { - // Expect failure. The error caught here is a WorkflowNotFound because - // the workflow will have already failed, so the update cannot go through. - // We assert on the expected failure below. - }); - await assertWftFailure( - handle, - `Cannot use update name: '${updateName}', with reserved prefix: '${TEMPORAL_RESERVED_PREFIX}'` - ); - await handle.terminate(); - }); -}); - -export async function helloWorkflow(name: string): Promise { - return `Hello, ${name}!`; -} - -test('Workflow can be started eagerly with shared NativeConnection', async (t) => { - const { createWorker, taskQueue } = helpers(t); - const client = new Client({ - connection: t.context.env.nativeConnection, - namespace: t.context.env.client.options.namespace, - }); - - const worker = await createWorker(); - await worker.runUntil(async () => { - const handle = await client.workflow.start(helloWorkflow, { - args: ['Temporal'], - workflowId: `eager-workflow-${randomUUID()}`, - taskQueue, - requestEagerStart: true, - workflowTaskTimeout: '1h', // hang if retry needed - }); - - t.true(handle.eagerlyStarted); - - const result = await handle.result(); - t.is(result, 'Hello, Temporal!'); - }); -}); - -test('Error thrown when requestEagerStart is used with regular Connection', async (t) => { - const { taskQueue } = helpers(t); - - const client = new WorkflowClient({ connection: t.context.env.connection }); - - await t.throwsAsync( - client.start(helloWorkflow, { - args: ['Temporal'], - workflowId: `eager-workflow-error-${randomUUID()}`, - taskQueue, - requestEagerStart: true, - }), - { - message: /Eager workflow start requires a NativeConnection/, - } - ); -}); diff --git a/packages/test/src/test-interceptors.ts b/packages/test/src/test-interceptors.ts deleted file mode 100644 index 5449ff585..000000000 --- a/packages/test/src/test-interceptors.ts +++ /dev/null @@ -1,288 +0,0 @@ -/* eslint @typescript-eslint/no-non-null-assertion: 0 */ -/** - * E2E Tests for the various SDK interceptors. - * Tests run serially to improve CI reliability.. - * @module - */ - -import test from 'ava'; -import dedent from 'dedent'; -import { v4 as uuid4 } from 'uuid'; -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, compareStackTrace, RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { defaultOptions } from './mock-native-worker'; -import { - continueAsNewToDifferentWorkflow, - interceptorExample, - internalsInterceptorExample, - unblockOrCancel, -} from './workflows'; -import { getSecretQuery, unblockWithSecretSignal } from './workflows/interceptor-example'; - -if (RUN_INTEGRATION_TESTS) { - test.before(() => { - Runtime.install({ logger: new DefaultLogger('DEBUG') }); - }); - - test.serial('Tracing can be implemented using interceptors', async (t) => { - const taskQueue = 'test-interceptors'; - const message = uuid4(); - - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - interceptors: { - activity: [ - () => ({ - inbound: { - async execute(input, next) { - const encoded = input.headers.message; - const receivedMessage = encoded ? defaultPayloadConverter.fromPayload(encoded) : ''; - return next({ ...input, args: [receivedMessage] }); - }, - }, - }), - ], - workflowModules: [require.resolve('./workflows/interceptor-example')], - }, - }); - const client = new WorkflowClient({ - interceptors: [ - { - async start(input, next) { - return next({ - ...input, - headers: { - ...input.headers, - message: defaultPayloadConverter.toPayload(message), - }, - }); - }, - async signalWithStart(input, next) { - const [decoded] = input.signalArgs; - const encoded = [...(decoded as any as string)].reverse().join(''); - return next({ - ...input, - signalArgs: [encoded], - headers: { - ...input.headers, - message: defaultPayloadConverter.toPayload(message), - marker: defaultPayloadConverter.toPayload(true), - }, - }); - }, - async signal(input, next) { - const [decoded] = input.args; - const encoded = [...(decoded as any as string)].reverse().join(''); - return next({ - ...input, - args: [encoded], - headers: { - ...input.headers, - marker: defaultPayloadConverter.toPayload(true), - }, - }); - }, - async query(input, next) { - const result: string = (await next({ - ...input, - headers: { - ...input.headers, - marker: defaultPayloadConverter.toPayload(true), - }, - })) as any; - return [...result].reverse().join(''); - }, - }, - ], - }); - await worker.runUntil(async () => { - { - const wf = await client.start(interceptorExample, { - taskQueue, - workflowId: uuid4(), - }); - // Send both signal and query to more consistently repro https://github.com/temporalio/sdk-node/issues/299 - await Promise.all([ - wf.signal(unblockWithSecretSignal, '12345'), - wf.query(getSecretQuery).then((result) => t.is(result, '12345')), - ]); - const result = await wf.result(); - t.is(result, message); - } - { - const wf = await client.signalWithStart(interceptorExample, { - taskQueue, - workflowId: uuid4(), - signal: unblockWithSecretSignal, - signalArgs: ['12345'], - }); - const result = await wf.result(); - t.is(result, message); - } - }); - }); - - test.serial('(Legacy) WorkflowClientCallsInterceptor intercepts terminate and cancel', async (t) => { - const taskQueue = 'test-interceptor-term-and-cancel'; - const message = uuid4(); - // Use these to coordinate with workflow activation to complete only after termination - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - }); - const client = new WorkflowClient({ - interceptors: { - calls: [ - () => ({ - async terminate(input, next) { - return next({ ...input, reason: message }); - }, - async cancel(_input, _next) { - throw new Error('nope'); - }, - }), - ], - }, - }); - - await worker.runUntil(async () => { - const wf = await client.start(unblockOrCancel, { - taskQueue, - workflowId: uuid4(), - }); - await t.throwsAsync(wf.cancel(), { - instanceOf: Error, - message: 'nope', - }); - await wf.terminate(); - const error = await t.throwsAsync(wf.result(), { - instanceOf: WorkflowFailedError, - message, - }); - if (!(error instanceof WorkflowFailedError)) { - throw new Error('Unreachable'); - } - t.true(error.cause instanceof TerminatedFailure); - }); - }); - - test.serial('WorkflowClientInterceptor intercepts terminate and cancel', async (t) => { - const taskQueue = 'test-interceptor-term-and-cancel'; - const message = uuid4(); - // Use these to coordinate with workflow activation to complete only after termination - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - }); - const client = new WorkflowClient({ - interceptors: [ - { - async terminate(input, next) { - return next({ ...input, reason: message }); - }, - async cancel(_input, _next) { - throw new Error('nope'); - }, - }, - ], - }); - - await worker.runUntil(async () => { - const wf = await client.start(unblockOrCancel, { - taskQueue, - workflowId: uuid4(), - }); - await t.throwsAsync(wf.cancel(), { - instanceOf: Error, - message: 'nope', - }); - await wf.terminate(); - const error = await t.throwsAsync(wf.result(), { - instanceOf: WorkflowFailedError, - message, - }); - if (!(error instanceof WorkflowFailedError)) { - throw new Error('Unreachable'); - } - t.true(error.cause instanceof TerminatedFailure); - }); - }); - - test.serial('Workflow continueAsNew can be intercepted', async (t) => { - const taskQueue = 'test-continue-as-new-interceptor'; - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - interceptors: { - // Includes an interceptor for ContinueAsNew that will throw an error when used with the workflow below - workflowModules: [require.resolve('./workflows/interceptor-example')], - }, - }); - const client = new WorkflowClient(); - const err = await worker.runUntil(async () => { - return (await t.throwsAsync( - client.execute(continueAsNewToDifferentWorkflow, { - taskQueue, - workflowId: uuid4(), - }), - { - instanceOf: WorkflowFailedError, - message: 'Workflow execution failed', - } - )) as WorkflowFailedError; - }); - - if (!(err.cause instanceof ApplicationFailure)) { - t.fail(`Expected err.cause to be an ApplicationFailure, got ${err.cause}`); - return; - } - t.deepEqual(err.cause.message, 'Expected anything other than 1'); - compareStackTrace( - t, - cleanOptionalStackTrace(err.cause.stack)!, - dedent` - ApplicationFailure: Expected anything other than 1 - 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) - ` - ); - t.is(err.cause.cause, undefined); - }); - - test.serial('Internals can be intercepted for observing Workflow state changes', async (t) => { - const taskQueue = 'test-internals-interceptor'; - - const events = Array(); - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - interceptors: { - // Co-exists with the Workflow - workflowModules: [require.resolve('./workflows/internals-interceptor-example')], - }, - sinks: { - logger: { - log: { - fn: (_: WorkflowInfo, event: string): void => { - events.push(event); - }, - }, - }, - }, - }); - const client = new WorkflowClient(); - await worker.runUntil( - client.execute(internalsInterceptorExample, { - taskQueue, - workflowId: uuid4(), - }) - ); - t.deepEqual(events, ['activate: 0', 'concludeActivation: 1', 'activate: 0', 'concludeActivation: 1']); - }); -} diff --git a/packages/test/src/test-interface-type-safety.ts b/packages/test/src/test-interface-type-safety.ts deleted file mode 100644 index a4d0c4fe6..000000000 --- a/packages/test/src/test-interface-type-safety.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -import test from 'ava'; -import { - defineSignal, - defineQuery, - ExternalWorkflowHandle, - ChildWorkflowHandle, - Workflow, - defineUpdate, - ChildWorkflowOptions, -} from '@temporalio/workflow'; -import { WorkflowHandle, WorkflowUpdateStage } from '@temporalio/client'; - -test('SignalDefinition Name type safety', (t) => { - // @ts-expect-error Assert expect a type error when generic and concrete names do not match - defineSignal<[string], 'mismatch'>('illegal value'); - - const signalA = defineSignal<[string], 'a'>('a'); - const signalB = defineSignal<[string], 'b'>('b'); - - type TypeAssertion = typeof signalB extends typeof signalA ? 'intermixable' : 'not-intermixable'; - - const _assertion: TypeAssertion = 'not-intermixable'; - t.pass(); -}); - -test('SignalDefinition Args type safety', (t) => { - const signalString = defineSignal<[string]>('a'); - const signalNumber = defineSignal<[number]>('b'); - - type TypeAssertion = typeof signalNumber extends typeof signalString ? 'intermixable' : 'not-intermixable'; - - const _assertion: TypeAssertion = 'not-intermixable'; - t.pass(); -}); - -test('QueryDefinition Name type safety', (t) => { - // @ts-expect-error Assert expect a type error when generic and concrete names do not match - defineQuery('illegal value'); - - const queryA = defineQuery('a'); - const queryB = defineQuery('b'); - - type TypeAssertion = typeof queryB extends typeof queryA ? 'intermixable' : 'not-intermixable'; - - const _assertion: TypeAssertion = 'not-intermixable'; - t.pass(); -}); - -test('QueryDefinition Args and Ret type safety', (t) => { - const retVariantA = defineQuery('a'); - const retVariantB = defineQuery('b'); - - type RetTypeAssertion = typeof retVariantB extends typeof retVariantA ? 'intermixable' : 'not-intermixable'; - - const _retAssertion: RetTypeAssertion = 'not-intermixable'; - - const argVariantA = defineQuery('a'); - const argVariantB = defineQuery('b'); - - type ArgTypeAssertion = typeof argVariantB extends typeof argVariantA ? 'intermixable' : 'not-intermixable'; - - const _argAssertion: ArgTypeAssertion = 'not-intermixable'; - t.pass(); -}); - -test('UpdateDefinition Name type safety', (t) => { - // @ts-expect-error Assert expect a type error when generic and concrete names do not match - defineUpdate('illegal value'); - - const updateA = defineUpdate('a'); - const updateB = defineUpdate('b'); - - type TypeAssertion = typeof updateB extends typeof updateA ? 'intermixable' : 'not-intermixable'; - - const _assertion: TypeAssertion = 'not-intermixable'; - t.pass(); -}); - -test('UpdateDefinition Args and Ret type safety', (t) => { - const retVariantA = defineUpdate('a'); - const retVariantB = defineUpdate('b'); - - type RetTypeAssertion = typeof retVariantB extends typeof retVariantA ? 'intermixable' : 'not-intermixable'; - - const _retAssertion: RetTypeAssertion = 'not-intermixable'; - - const argVariantA = defineUpdate('a'); - const argVariantB = defineUpdate('b'); - - type ArgTypeAssertion = typeof argVariantB extends typeof argVariantA ? 'intermixable' : 'not-intermixable'; - - const _argAssertion: ArgTypeAssertion = 'not-intermixable'; - t.pass(); -}); - -test('Can call signal on any WorkflowHandle', async (t) => { - // This function definition is an assertion by itself. TSC will throw a compile time error if - // the signature of the signal function is not compatible across all WorkflowHandle variants. - async function _assertion( - handle: WorkflowHandle | ChildWorkflowHandle | ExternalWorkflowHandle - ) { - await handle.signal(defineSignal('signal')); - } - - t.pass(); -}); - -test('startUpdate and executeUpdate call signatures', async (t) => { - // startUpdate and executeUpdate call signatures both require `args` iff update takes args. - // startUpdate requires `waitForStage=Accepted`. - // executeUpdate does not accept `waitForStage`. - const nullaryUpdate = defineUpdate('my-nullary-update'); - const unaryUpdate = defineUpdate('my-unary-update'); - - async function _assertion(handle: WorkflowHandle) { - // @ts-expect-error: waitForStage required - await handle.startUpdate(nullaryUpdate); - // @ts-expect-error: waitForStage required - await handle.startUpdate(nullaryUpdate, {}); - // @ts-expect-error: waitForStage required - await handle.startUpdate(nullaryUpdate, { args: [] }); - // @ts-expect-error: waitForStage must be ACCEPTED - await handle.startUpdate(nullaryUpdate, { - waitForStage: WorkflowUpdateStage.ADMITTED, - }); - // @ts-expect-error: waitForStage must be ACCEPTED - await handle.startUpdate(nullaryUpdate, { - waitForStage: WorkflowUpdateStage.COMPLETED, - }); - // @ts-expect-error: waitForStage must be ACCEPTED - await handle.startUpdate(nullaryUpdate, { - waitForStage: WorkflowUpdateStage.UNSPECIFIED, // eslint-disable-line deprecation/deprecation - }); - // @ts-expect-error: args must be empty if present - await handle.startUpdate(nullaryUpdate, { - args: [1], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - // valid - await handle.startUpdate(nullaryUpdate, { - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - await handle.startUpdate(nullaryUpdate, { - args: [], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - // @ts-expect-error:executeUpdate doesn't accept waitForStage - await handle.executeUpdate(nullaryUpdate, { - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - // @ts-expect-error:executeUpdate doesn't accept waitForStage - await handle.executeUpdate(nullaryUpdate, { - waitForStage: WorkflowUpdateStage.COMPLETED, - }); - // valid - await handle.executeUpdate(nullaryUpdate, {}); - await handle.executeUpdate(nullaryUpdate, { args: [] }); - - // @ts-expect-error: args required - await handle.startUpdate(unaryUpdate, { - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - // @ts-expect-error: args required - await handle.startUpdate(unaryUpdate, { - args: [], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - // valid - await handle.startUpdate(unaryUpdate, { - args: [1], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - // @ts-expect-error:executeUpdate doesn't accept waitForStage - await handle.executeUpdate(unaryUpdate, { - args: [1], - waitForStage: WorkflowUpdateStage.ACCEPTED, - }); - // valid - await handle.executeUpdate(unaryUpdate, { args: [1] }); - } - t.pass(); -}); - -test('ChildWorkflowOptions workflowIdConflictPolicy', (t) => { - const options: ChildWorkflowOptions = { - // @ts-expect-error: workflowIdConflictPolicy is not a valid option - workflowIdConflictPolicy: 'USE_EXISTING', - }; - t.pass(); -}); diff --git a/packages/test/src/test-isolation.ts b/packages/test/src/test-isolation.ts deleted file mode 100644 index 0d784361b..000000000 --- a/packages/test/src/test-isolation.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { ExecutionContext, ImplementationFn } from 'ava'; -import { ApplicationFailure, arrayFromPayloads } from '@temporalio/common'; -import * as wf from '@temporalio/workflow'; -import { WorkflowFailedError } from '@temporalio/client'; -import { makeTestFunction, Context, helpers } from './helpers-integration'; -import { REUSE_V8_CONTEXT } from './helpers'; - -const test = makeTestFunction({ - workflowsPath: __filename, - workflowInterceptorModules: [__filename], -}); - -const withReusableContext = test.macro<[ImplementationFn<[], Context>]>(async (t, fn) => { - if (!REUSE_V8_CONTEXT) { - t.pass('Skipped since REUSE_V8_CONTEXT is set to false'); - return; - } - await fn(t); -}); - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -test('globalThis can be safely mutated - misc string property', async (t) => { - await assertObjectSafelyMutable(t, globalThisMutatorWorkflow, 'myProperty'); -}); - -test('globalThis can be safely mutated - numeric index property', async (t) => { - await assertObjectSafelyMutable(t, globalThisMutatorWorkflow, 0); -}); - -test('globalThis can be safely mutated - symbol property', async (t) => { - await assertObjectSafelyMutable(t, globalThisMutatorWorkflow, Symbol.for('mySymbol')); -}); - -export async function globalThisMutatorWorkflow(prop: string): Promise<(number | null)[]> { - return basePropertyMutatorWorkflow(() => globalThis as any, decodeProperty(prop)); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -test("V8's built-in global objects are frozen", withReusableContext, async (t) => { - await assertObjectImmutable(t, v8BuiltinGlobalObjectMutatorWorkflow); -}); - -export async function v8BuiltinGlobalObjectMutatorWorkflow(): Promise<(number | null)[]> { - return basePropertyMutatorWorkflow(() => globalThis.Math); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -test("V8's built-in global objects can be safely reassigned", withReusableContext, async (t) => { - await assertObjectSafelyMutable(t, v8BuiltinGlobalObjectReassignWorkflow); -}); - -export async function v8BuiltinGlobalObjectReassignWorkflow(): Promise<(number | null)[]> { - globalThis.Math = Object.create(globalThis.Math); - return basePropertyMutatorWorkflow(() => globalThis.Math); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -test("V8's built-in global functions are frozen", withReusableContext, async (t) => { - await assertObjectImmutable(t, v8BuiltinGlobalFunctionMutatorWorkflow); -}); - -export async function v8BuiltinGlobalFunctionMutatorWorkflow(): Promise<(number | null)[]> { - return basePropertyMutatorWorkflow(() => globalThis.Array as any); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -test("V8's built-in global functions can be safely reassigned", withReusableContext, async (t) => { - await assertObjectSafelyMutable(t, v8BuiltinGlobalFunctionReassignWorkflow); -}); - -export async function v8BuiltinGlobalFunctionReassignWorkflow(): Promise<(number | null)[]> { - const originalArray = globalThis.Array; - globalThis.Array = ((...args: any[]) => originalArray(...args)) as any; - globalThis.Array.from = ((...args: any[]) => (originalArray as any).from(...args)) as any; - return basePropertyMutatorWorkflow(() => globalThis.Array); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -test( - "V8's built-in global function's prototypes are mutable, without safety guarantees", - withReusableContext, - async (t) => { - await assertObjectUnsafelyMutable(t, v8BuiltinGlobalFunctionPrototypeReassignWorkflow); - } -); - -export async function v8BuiltinGlobalFunctionPrototypeReassignWorkflow(): Promise<(number | null)[]> { - return basePropertyMutatorWorkflow(() => globalThis.Array.prototype); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -test("SDK's global functions can be reassigned", async (t) => { - await assertObjectSafelyMutable(t, sdkGlobalsReassignment); -}); - -export async function sdkGlobalsReassignment(): Promise<(number | null)[]> { - // The SDK's provided `console` object is frozen. - // Replace that global with a clone that is not frozen. - globalThis.console = { ...globalThis.console }; - return basePropertyMutatorWorkflow(() => globalThis.console); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -test("SDK's modules are frozen", withReusableContext, async (t) => { - await assertObjectSafelyMutable(t, sdkModuleMutatorWorkflow); -}); - -export async function sdkModuleMutatorWorkflow(): Promise<(number | null)[]> { - return basePropertyMutatorWorkflow(() => wf as any); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -test("SDK's API functions are frozen 1", withReusableContext, async (t) => { - await assertObjectImmutable(t, sdkPropertyMutatorWorkflow1); -}); - -export async function sdkPropertyMutatorWorkflow1(): Promise<(number | null)[]> { - return basePropertyMutatorWorkflow(() => arrayFromPayloads as any); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -test('Module state is isolated and maintained between activations', async (t) => { - await assertObjectSafelyMutable(t, modulePropertyMutator); -}); - -const moduleScopedObject: any = {}; -export async function modulePropertyMutator(): Promise<(number | null)[]> { - return basePropertyMutatorWorkflow(() => moduleScopedObject); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// Utils -//////////////////////////////////////////////////////////////////////////////////////////////////// - -async function assertObjectSafelyMutable( - t: ExecutionContext, - workflow: (prop: string) => Promise<(number | null)[]>, - property: string | symbol | number = 'a' -): Promise { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const [wf1Result, wf2Result] = await Promise.all([ - executeWorkflow(workflow, { args: [encodeProperty(property)] }), - executeWorkflow(workflow, { args: [encodeProperty(property)] }), - ]); - const wf1Step = wf1Result.shift() ?? 1; - const wf2Step = wf2Result.shift() ?? 1; - t.deepEqual( - wf1Result, - [null, 1, 1, 2, 2, null, null, 1].map((x) => x && x * wf1Step) - ); - t.deepEqual( - wf2Result, - [null, 1, 1, 2, 2, null, null, 1].map((x) => x && x * wf2Step) - ); - }); -} - -async function assertObjectImmutable( - t: ExecutionContext, - workflow: () => Promise<(number | null)[]> -): Promise { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - const wf1 = await startWorkflow(workflow); - const err = await t.throwsAsync(wf1.result(), { instanceOf: WorkflowFailedError }); - t.is(err?.cause?.message, 'Cannot add property a, object is not extensible'); - t.deepEqual((err?.cause as ApplicationFailure)?.details, [[null]]); - }); -} - -async function assertObjectUnsafelyMutable( - t: ExecutionContext, - workflow: (prop: string) => Promise<(number | null)[]>, - property: string | symbol | number = 'a' -): Promise { - const { createWorker, executeWorkflow } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(async () => { - await executeWorkflow(workflow, { args: [encodeProperty(property)] }); - await executeWorkflow(workflow, { args: [encodeProperty(property)] }); - }); - // That's it; if the test didn't throw, it passed. - t.pass(); -} - -// Given the object returned by `getObject()`, this function can be used to -// assert any of these three possible scenarios: -// 1. The object can't be mutated from Workflows (i.e. the object is frozen); -// - or - -// 2. The object can be safetly mutated from Workflows, meaning that: -// 2.1. Can add new properties to the object (i.e. the object is not frozen); -// 2.2. Properties added on the object from one workflow execution don't leak to other workflows; -// 2.3. Properties added on the object from one workflow are maintained between activations of that workflow; -// 2.4. Properties added then deleted from the object don't reappear on subsequent activations. -// - or - -// 3. The object can be mutated from Workflows, without isolation guarantees. -// This last case is notably desirable -async function basePropertyMutatorWorkflow( - getObject: () => any, - prop: string | symbol | number = 'a' -): Promise<(number | null)[]> { - // Randomly choose some step to add to the property; there's a 10% chance that two workflows in - // a same test run will get the same step, and that's really not a problem (the test is still valid). - // But getting different steps at least once in a while confirms that our test methodology isn't - // prone to false positives due to the two racing workflows turn out to be producing the very same - // sequence of values at exactly the same time. - const step = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000][ - Math.floor(Math.random() * 10) - ]; - - const checkpoints: (number | null)[] = [step]; - - // Very important: do not cache the result of getObject() to a local variable; - // in some scenarios, caching would defeat the purpose of this test. - try { - checkpoints.push(getObject()[prop]); // Expect null - getObject()[prop] = (getObject()[prop] || 0) + step; - checkpoints.push(getObject()[prop]); // Expect 1*step - - await wf.sleep(1); - - checkpoints.push(getObject()[prop]); // Expect 1*step - getObject()[prop] = (getObject()[prop] || 0) + step; - checkpoints.push(getObject()[prop]); // Expect 2*step - - await wf.sleep(1); - - checkpoints.push(getObject()[prop]); // Expect 2*step - delete getObject()[prop]; - checkpoints.push(getObject()[prop]); // Expect null - - await wf.sleep(1); - - checkpoints.push(getObject()[prop]); // Expect null - getObject()[prop] = (getObject()[prop] || 0) + step; - checkpoints.push(getObject()[prop]); // Expect 1*step - - return checkpoints; - } catch (e) { - throw ApplicationFailure.fromError(e, { details: [checkpoints.slice(1)] }); - } -} - -function encodeProperty(prop: string | symbol | number): string { - if (typeof prop === 'symbol') return `symbol:${String(prop)}`; - if (typeof prop === 'number') return `number:${prop}`; - return prop; -} - -function decodeProperty(prop: string): string | symbol | number { - if (prop.startsWith('symbol:')) return Symbol.for(prop.slice(7)); - if (prop.startsWith('number:')) return Number(prop.slice(7)); - return prop; -} diff --git a/packages/test/src/test-iterators-utils.ts b/packages/test/src/test-iterators-utils.ts deleted file mode 100644 index eb16f6f4b..000000000 --- a/packages/test/src/test-iterators-utils.ts +++ /dev/null @@ -1,199 +0,0 @@ -import test from 'ava'; -import { mapAsyncIterable } from '@temporalio/client/lib/iterators-utils'; - -test(`mapAsyncIterable (with no concurrency) returns mapped values`, async (t) => { - async function* source(): AsyncIterable { - yield 1; - yield new Promise((resolve) => setTimeout(resolve, 50)).then(() => 2); - yield Promise.resolve(3); - } - const iterable = mapAsyncIterable(source(), multBy10); - - const results: number[] = []; - for await (const res of iterable) { - results.push(res); - } - t.deepEqual(results, [10, 20, 30]); -}); - -test(`mapAsyncIterable's (with no concurrency) source function not executed until the mapped iterator actually get invoked`, async (t) => { - let invoked = false; - - async function* name(): AsyncIterable { - invoked = true; - yield 1; - } - - const iterable = mapAsyncIterable(name(), multBy10); - const iterator = iterable[Symbol.asyncIterator](); - - await Promise.resolve(); - - t.false(invoked); - t.is(await (await iterator.next()).value, 10); - t.true(invoked); -}); - -test(`mapAsyncIterable (with no concurrency) doesn't consume more input that required`, async (t) => { - let counter = 0; - - async function* name(): AsyncIterable { - for (;;) { - yield counter++; - } - } - - const iterable = mapAsyncIterable(name(), multBy10); - const iterator = iterable[Symbol.asyncIterator](); - - t.is(await (await iterator.next()).value, 0); - t.is(await (await iterator.next()).value, 10); - await Promise.resolve(); - t.is(counter, 2); -}); - -test(`mapAsyncIterable (with concurrency) run tasks concurrently`, async (t) => { - async function* name(): AsyncIterable { - yield 200; - yield 1; - yield 1; - yield 200; - yield 1; - yield 1; - yield 200; - yield 1; - yield 1; - } - - const iterable = mapAsyncIterable(name(), sleepThatTime, { concurrency: 4 }); - - const startTime = Date.now(); - const values: number[] = []; - for await (const val of iterable) { - values.push(val); - } - const endTime = Date.now(); - - t.deepEqual(values, [1, 1, 1, 1, 1, 1, 200, 200, 200]); - t.truthy(endTime - startTime < 400); -}); - -test(`mapAsyncIterable (with concurrency) source function not executed until the mapped iterator actually get invoked`, async (t) => { - let invoked = false; - - async function* name(): AsyncIterable { - invoked = true; - yield 1; - } - - const iterable = mapAsyncIterable(name(), multBy10, { concurrency: 4 }); - const iterator = iterable[Symbol.asyncIterator](); - - await Promise.resolve(); - - t.false(invoked); - t.is(await (await iterator.next()).value, 10); - t.true(invoked); -}); - -test(`mapAsyncIterable (with concurrency) doesn't consume more input than required`, async (t) => { - let counter = 0; - - async function* name(): AsyncIterable { - for (;;) { - yield ++counter; - } - } - - const iterable = mapAsyncIterable(name(), sleepThatTime, { concurrency: 5, bufferLimit: 8 }); - const iterator = iterable[Symbol.asyncIterator](); - - t.is(counter, 0); - await iterator.next(); - - // One already read + 5 pending - t.is(counter, 6); - await iterator.next(); - t.is(counter, 7); - - // Give time for buffer to get filled - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Two already read + 8 buffered results + 5 concurrent results - t.is(counter, 15); -}); - -test(`mapAsyncIterable (with concurrency) doesn't hang on source exceptions`, async (t) => { - async function* name(): AsyncIterable { - for (;;) { - yield 1; - yield 2; - yield 3; - yield 4; - throw new Error('Test Exception'); - } - } - - const iterable = mapAsyncIterable(name(), sleepThatTime, { concurrency: 2, bufferLimit: 8 }); - const iterator = iterable[Symbol.asyncIterator](); - - // Get the iterator started - await iterator.next(); - - // Give time for buffer to get filled - await new Promise((resolve) => setTimeout(resolve, 100)); - - await iterator.next(); - await iterator.next(); - - await t.throwsAsync(iterator.next(), { - instanceOf: Error, - message: 'Test Exception', - }); -}); - -// FIXME: This test is producing rare flakes -test(`mapAsyncIterable (with concurrency) doesn't hang mapFn exceptions`, async (t) => { - async function* name(): AsyncIterable { - for (let i = 0; i < 1000; i++) { - yield i; - } - } - - const iterable = mapAsyncIterable( - name(), - async (x: number) => { - await sleepThatTime(x * 10); - if (x === 4) throw new Error('Test Exception'); - return x; - }, - { concurrency: 2, bufferLimit: 8 } - ); - const iterator = iterable[Symbol.asyncIterator](); - - // Start the iterator - await iterator.next(); - - // Give time for buffer to get filled - await new Promise((resolve) => setTimeout(resolve, 100)); - - const values: (number | string | boolean)[] = []; - for (let i = 0; i < 6; i++) { - try { - const res = await iterator.next(); - values.push(res.value ?? res.done); - } catch (_error) { - values.push('error'); - } - } - - t.deepEqual(values.sort(), [1, 2, 3, 'error', true, true]); -}); - -async function multBy10(x: number): Promise { - return Promise.resolve(x * 10); -} - -async function sleepThatTime(x: number): Promise { - return new Promise((resolve) => setTimeout(() => resolve(x), x)); -} diff --git a/packages/test/src/test-local-activities.ts b/packages/test/src/test-local-activities.ts deleted file mode 100644 index ae73518b9..000000000 --- a/packages/test/src/test-local-activities.ts +++ /dev/null @@ -1,659 +0,0 @@ -import { randomUUID } from 'crypto'; -import { firstValueFrom, Subject } from 'rxjs'; -import { ExecutionContext, TestFn } from 'ava'; -import { Context as ActivityContext } from '@temporalio/activity'; -import { - ApplicationFailure, - defaultPayloadConverter, - WorkflowFailedError, - WorkflowHandle, - WorkflowStartOptions, -} from '@temporalio/client'; -import { LocalActivityOptions, RetryPolicy } from '@temporalio/common'; -import { msToNumber } from '@temporalio/common/lib/time'; -import { temporal } from '@temporalio/proto'; -import { workflowInterceptorModules } from '@temporalio/testing'; -import { - bundleWorkflowCode, - DefaultLogger, - LogLevel, - Runtime, - WorkflowBundle, - WorkerOptions, -} from '@temporalio/worker'; -import * as workflow from '@temporalio/workflow'; -import { test as anyTest, bundlerOptions, Worker, TestWorkflowEnvironment } from './helpers'; - -// FIXME MOVE THIS SECTION SOMEWHERE IT CAN BE SHARED // - -interface Context { - env: TestWorkflowEnvironment; - workflowBundle: WorkflowBundle; -} - -const test = anyTest as TestFn; - -interface Helpers { - taskQueue: string; - createWorker(opts?: Partial): Promise; - executeWorkflow Promise>(workflowType: T): Promise>; - executeWorkflow( - fn: T, - opts: Omit, 'taskQueue' | 'workflowId'> - ): Promise>; - startWorkflow Promise>(workflowType: T): Promise>; - startWorkflow( - fn: T, - opts: Omit, 'taskQueue' | 'workflowId'> - ): Promise>; -} - -function helpers(t: ExecutionContext): Helpers { - const taskQueue = t.title.replace(/ /g, '_'); - - return { - taskQueue, - async createWorker(opts?: Partial): Promise { - const { interceptors, ...rest } = opts ?? {}; - return await Worker.create({ - connection: t.context.env.nativeConnection, - workflowBundle: t.context.workflowBundle, - taskQueue, - interceptors: { - activity: interceptors?.activity ?? [], - }, - showStackTraceSources: true, - ...rest, - }); - }, - async executeWorkflow( - fn: workflow.Workflow, - opts?: Omit - ): Promise { - return await t.context.env.client.workflow.execute(fn, { - taskQueue, - workflowId: randomUUID(), - ...opts, - }); - }, - async startWorkflow( - fn: workflow.Workflow, - opts?: Omit - ): Promise> { - return await t.context.env.client.workflow.start(fn, { - taskQueue, - workflowId: randomUUID(), - ...opts, - }); - }, - }; -} - -test.before(async (t) => { - // Ignore invalid log levels - Runtime.install({ logger: new DefaultLogger((process.env.TEST_LOG_LEVEL || 'DEBUG').toUpperCase() as LogLevel) }); - const env = await TestWorkflowEnvironment.createLocal(); - const workflowBundle = await bundleWorkflowCode({ - ...bundlerOptions, - workflowInterceptorModules: [...workflowInterceptorModules, __filename], - workflowsPath: __filename, - }); - t.context = { - env, - workflowBundle, - }; -}); - -test.after.always(async (t) => { - await t.context.env.teardown(); -}); - -// END OF TO BE MOVED SECTION // - -export async function runOneLocalActivity(s: string): Promise { - return await workflow.proxyLocalActivities({ startToCloseTimeout: '1m' }).echo(s); -} - -test.serial('Simple local activity works end to end', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async echo(message: string): Promise { - return message; - }, - }, - }); - await worker.runUntil(async () => { - const res = await executeWorkflow(runOneLocalActivity, { - args: ['hello'], - }); - t.is(res, 'hello'); - }); -}); - -export async function runMyLocalActivityWithOption( - opts: LocalActivityOptions -): Promise> { - return await workflow.proxyLocalActivities(opts).myLocalActivity(); -} - -test.serial('Local activity with various timeouts', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async myLocalActivity(): Promise< - Pick - > { - return { - startToCloseTimeoutMs: ActivityContext.current().info.startToCloseTimeoutMs, - scheduleToCloseTimeoutMs: ActivityContext.current().info.scheduleToCloseTimeoutMs, - }; - }, - }, - }); - await worker.runUntil(async () => { - t.deepEqual(await executeWorkflow(runMyLocalActivityWithOption, { args: [{ startToCloseTimeout: '5s' }] }), { - startToCloseTimeoutMs: msToNumber('5s'), - scheduleToCloseTimeoutMs: 0, // FIXME - }); - t.deepEqual(await executeWorkflow(runMyLocalActivityWithOption, { args: [{ scheduleToCloseTimeout: '5s' }] }), { - startToCloseTimeoutMs: msToNumber('5s'), - scheduleToCloseTimeoutMs: msToNumber('5s'), - }); - t.deepEqual( - await executeWorkflow(runMyLocalActivityWithOption, { - args: [{ scheduleToStartTimeout: '2s', startToCloseTimeout: '5s' }], - }), - { - startToCloseTimeoutMs: msToNumber('5s'), - scheduleToCloseTimeoutMs: 0, - } - ); - t.deepEqual( - await executeWorkflow(runMyLocalActivityWithOption, { - args: [{ scheduleToCloseTimeout: '5s', startToCloseTimeout: '2s' }], - }), - { - startToCloseTimeoutMs: msToNumber('2s'), - scheduleToCloseTimeoutMs: msToNumber('5s'), - } - ); - t.deepEqual( - await executeWorkflow(runMyLocalActivityWithOption, { - args: [{ scheduleToCloseTimeout: '2s', startToCloseTimeout: '5s' }], - }), - { - startToCloseTimeoutMs: msToNumber('2s'), - scheduleToCloseTimeoutMs: msToNumber('2s'), - } - ); - }); -}); - -export async function getIsLocal(fromInsideLocal: boolean): Promise { - return await (fromInsideLocal - ? workflow.proxyLocalActivities({ startToCloseTimeout: '1m' }).isLocal() - : workflow.proxyActivities({ startToCloseTimeout: '1m' }).isLocal()); -} - -test.serial('isLocal is set correctly', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async isLocal(): Promise { - return ActivityContext.current().info.isLocal; - }, - }, - }); - await worker.runUntil(async () => { - t.is(await executeWorkflow(getIsLocal, { args: [true] }), true); - t.is(await executeWorkflow(getIsLocal, { args: [false] }), false); - }); -}); - -export async function runParallelLocalActivities(...ss: string[]): Promise { - return await Promise.all(ss.map(workflow.proxyLocalActivities({ startToCloseTimeout: '1m' }).echo)); -} - -test.serial('Parallel local activities work end to end', async (t) => { - const { startWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async echo(message: string): Promise { - return message; - }, - }, - }); - await worker.runUntil(async () => { - const args = ['hey', 'ho', 'lets', 'go']; - const handle = await startWorkflow(runParallelLocalActivities, { - args, - }); - const res = await handle.result(); - t.deepEqual(res, args); - - // Double check we have all local activity markers in history - const history = await handle.fetchHistory(); - const markers = history?.events?.filter( - (ev) => ev.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_MARKER_RECORDED - ); - t.is(markers?.length, 4); - }); -}); - -export async function throwAnErrorFromLocalActivity(message: string): Promise { - await workflow.proxyLocalActivities({ startToCloseTimeout: '1m' }).throwAnError(message); -} - -test.serial('Local activity error is propagated properly to the Workflow', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async throwAnError(message: string): Promise { - throw ApplicationFailure.nonRetryable(message, 'Error', 'details', 123, false); - }, - }, - }); - await worker.runUntil(async () => { - const err: WorkflowFailedError | undefined = await t.throwsAsync( - executeWorkflow(throwAnErrorFromLocalActivity, { - args: ['tesssst'], - }), - { instanceOf: WorkflowFailedError } - ); - t.is(err?.cause?.message, 'tesssst'); - }); -}); - -export async function cancelALocalActivity(): Promise { - await workflow.CancellationScope.cancellable(async () => { - const p = workflow.proxyLocalActivities({ startToCloseTimeout: '1m' }).myActivity(); - await workflow.sleep(1); - workflow.CancellationScope.current().cancel(); - await p; - }); -} - -test.serial('Local activity cancellation is propagated properly to the Workflow', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async myActivity(): Promise { - await ActivityContext.current().cancelled; - }, - }, - }); - await worker.runUntil(async () => { - const err: WorkflowFailedError | undefined = await t.throwsAsync( - executeWorkflow(cancelALocalActivity, { workflowTaskTimeout: '3s' }), - { instanceOf: WorkflowFailedError } - ); - t.true(workflow.isCancellation(err?.cause)); - t.is(err?.cause?.message, 'Local Activity cancelled'); - }); -}); - -test.serial('Worker shutdown while running a local activity completes after completion', async (t) => { - const { startWorkflow, createWorker } = helpers(t); - const subj = new Subject(); - const worker = await createWorker({ - activities: { - async myActivity(): Promise { - await ActivityContext.current().cancelled; - }, - }, - sinks: { - test: { - timerFired: { - fn() { - subj.next(); - }, - }, - }, - }, - // Just in case - shutdownGraceTime: '10s', - }); - const handle = await startWorkflow(cancelALocalActivity, { workflowTaskTimeout: '3s' }); - const p = worker.run(); - await firstValueFrom(subj); - worker.shutdown(); - - const err: WorkflowFailedError | undefined = await t.throwsAsync(handle.result(), { - instanceOf: WorkflowFailedError, - }); - t.true(workflow.isCancellation(err?.cause)); - t.is(err?.cause?.message, 'Local Activity cancelled'); - console.log('Local Waiting for worker to complete shutdown'); - await p; -}); - -test.serial('Failing local activity can be cancelled', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async myActivity(): Promise { - throw new Error('retry me'); - }, - }, - }); - await worker.runUntil(async () => { - const err: WorkflowFailedError | undefined = await t.throwsAsync( - executeWorkflow(cancelALocalActivity, { workflowTaskTimeout: '3s' }), - { instanceOf: WorkflowFailedError } - ); - t.true(workflow.isCancellation(err?.cause)); - t.is(err?.cause?.message, 'Local Activity cancelled'); - }); -}); - -export async function runSerialLocalActivities(): Promise { - const { echo } = workflow.proxyLocalActivities({ startToCloseTimeout: '1m' }); - await echo('1'); - await echo('2'); - await echo('3'); -} - -test.serial('Serial local activities (in the same task) work end to end', async (t) => { - const { startWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async echo(message: string): Promise { - return message; - }, - }, - }); - await worker.runUntil(async () => { - const handle = await startWorkflow(runSerialLocalActivities, {}); - await handle.result(); - const history = await handle.fetchHistory(); - if (history?.events == null) { - throw new Error('Expected non null events'); - } - // Last 3 events before completing the workflow should be MarkerRecorded - t.truthy(history.events[history.events.length - 2].markerRecordedEventAttributes); - t.truthy(history.events[history.events.length - 3].markerRecordedEventAttributes); - t.truthy(history.events[history.events.length - 4].markerRecordedEventAttributes); - }); -}); - -export async function throwAnExplicitNonRetryableErrorFromLocalActivity(message: string): Promise { - const { throwAnError } = workflow.proxyLocalActivities({ - startToCloseTimeout: '1m', - retry: { nonRetryableErrorTypes: ['Error'] }, - }); - - await throwAnError(false, message); -} - -test.serial('Local activity does not retry if error is in nonRetryableErrorTypes', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async throwAnError(useApplicationFailure: boolean, message: string): Promise { - if (useApplicationFailure) { - throw ApplicationFailure.nonRetryable(message, 'Error', 'details', 123, false); - } else { - throw new Error(message); - } - }, - }, - }); - await worker.runUntil(async () => { - const err: WorkflowFailedError | undefined = await t.throwsAsync( - executeWorkflow(throwAnExplicitNonRetryableErrorFromLocalActivity, { - args: ['tesssst'], - }), - { instanceOf: WorkflowFailedError } - ); - t.is(err?.cause?.message, 'tesssst'); - }); -}); - -export async function throwARetryableErrorWithASingleRetry(message: string): Promise { - const { throwAnError } = workflow.proxyLocalActivities({ - startToCloseTimeout: '1m', - retry: { maximumAttempts: 2 }, - }); - - await throwAnError(false, message); -} - -test.serial('Local activity can retry once', async (t) => { - let attempts = 0; - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - // Reimplement here to track number of attempts - async throwAnError(_: unknown, message: string) { - attempts++; - throw new Error(message); - }, - }, - }); - - await worker.runUntil(async () => { - const err: WorkflowFailedError | undefined = await t.throwsAsync( - executeWorkflow(throwARetryableErrorWithASingleRetry, { - args: ['tesssst'], - }), - { instanceOf: WorkflowFailedError } - ); - t.is(err?.cause?.message, 'tesssst'); - }); - // Might be more than 2 if workflow task times out (CI I'm looking at you) - t.true(attempts >= 2); -}); - -export async function throwAnErrorWithBackoff(): Promise { - const { succeedAfterFirstAttempt } = workflow.proxyLocalActivities({ - startToCloseTimeout: '1m', - localRetryThreshold: '1s', - retry: { maximumAttempts: 2, initialInterval: '2s' }, - }); - - await succeedAfterFirstAttempt(); -} - -test.serial('Local activity backs off with timer', async (t) => { - let attempts = 0; - const { startWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - // Reimplement here to track number of attempts - async succeedAfterFirstAttempt() { - attempts++; - if (attempts === 1) { - throw new Error('Retry me please'); - } - }, - }, - }); - - await worker.runUntil(async () => { - const handle = await startWorkflow(throwAnErrorWithBackoff, { - workflowTaskTimeout: '3s', - }); - await handle.result(); - const history = await handle.fetchHistory(); - const timers = history?.events?.filter( - (ev) => ev.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_TIMER_FIRED - ); - t.is(timers?.length, 1); - - const markers = history?.events?.filter( - (ev) => ev.eventType === temporal.api.enums.v1.EventType.EVENT_TYPE_MARKER_RECORDED - ); - t.is(markers?.length, 2); - }); -}); - -export async function runOneLocalActivityWithInterceptor(s: string): Promise { - return await workflow.proxyLocalActivities({ startToCloseTimeout: '1m' }).interceptMe(s); -} - -test.serial('Local activity can be intercepted', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async interceptMe(message: string): Promise { - return message; - }, - }, - interceptors: { - activity: [ - () => ({ - inbound: { - async execute(input, next) { - t.is(defaultPayloadConverter.fromPayload(input.headers.secret), 'shhh'); - return await next(input); - }, - }, - }), - ], - }, - }); - await worker.runUntil(async () => { - const res = await executeWorkflow(runOneLocalActivityWithInterceptor, { - args: ['message'], - }); - t.is(res, 'messagemessage'); - }); -}); - -export async function runNonExisitingLocalActivity(): Promise { - const { activityNotFound } = workflow.proxyLocalActivities({ - startToCloseTimeout: '1m', - }); - - try { - await activityNotFound(); - } catch (err) { - if (err instanceof ReferenceError) { - return; - } - throw err; - } - throw ApplicationFailure.nonRetryable('Unreachable'); -} - -test.serial('Local activity not registered on Worker throws ReferenceError in workflow context', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker(); - await worker.runUntil(executeWorkflow(runNonExisitingLocalActivity)); - t.pass(); -}); - -test.serial('Local activity not registered on replay Worker does not throw', async (t) => { - const { startWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async echo(input: string) { - return input; - }, - }, - }); - const handle = await startWorkflow(runOneLocalActivity, { args: ['hello'] }); - await worker.runUntil(() => handle.result()); - const history = await handle.fetchHistory(); - await Worker.runReplayHistory({ workflowBundle: t.context.workflowBundle }, history, handle.workflowId); - t.pass(); -}); - -/** - * Reproduces https://github.com/temporalio/sdk-typescript/issues/731 - */ -export async function issue731(): Promise { - await workflow.CancellationScope.cancellable(async () => { - const localActivityPromise = workflow.proxyLocalActivities({ startToCloseTimeout: '1m' }).echo('activity'); - const sleepPromise = workflow.sleep('30s').then(() => 'timer'); - const result = await Promise.race([localActivityPromise, sleepPromise]); - if (result === 'timer') { - throw workflow.ApplicationFailure.nonRetryable('Timer unexpectedly beat local activity'); - } - workflow.CancellationScope.current().cancel(); - }); - - await workflow.sleep(100); -} - -test.serial('issue-731', async (t) => { - const { startWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async echo(message: string): Promise { - return message; - }, - }, - }); - await worker.runUntil(async () => { - const handle = await startWorkflow(issue731, { - workflowTaskTimeout: '1m', // Give our local activities enough time to run in CI - }); - await handle.result(); - - const history = await handle.fetchHistory(); - if (history?.events == null) { - throw new Error('Expected non null events'); - } - // Verify only one timer was scheduled - t.is(history.events.filter(({ timerStartedEventAttributes }) => timerStartedEventAttributes != null).length, 1); - }); -}); - -export const interceptors: workflow.WorkflowInterceptorsFactory = () => { - return { - outbound: [ - { - async startTimer(input, next) { - const { test } = workflow.proxySinks(); - await next(input); - test.timerFired(); - }, - async scheduleLocalActivity(input, next) { - if (input.activityType !== 'interceptMe') return next(input); - - const secret = workflow.defaultPayloadConverter.toPayload('shhh'); - if (secret === undefined) { - throw new Error('Unexpected'); - } - const output: any = await next({ ...input, headers: { secret } }); - return output + output; - }, - }, - ], - }; -}; - -export async function getRetryPolicyFromActivityInfo( - retryPolicy: RetryPolicy, - fromInsideLocal: boolean -): Promise { - return await (fromInsideLocal - ? workflow.proxyLocalActivities({ startToCloseTimeout: '1m', retry: retryPolicy }).retryPolicy() - : workflow.proxyActivities({ startToCloseTimeout: '1m', retry: retryPolicy }).retryPolicy()); -} - -test.serial('retryPolicy is set correctly', async (t) => { - const { executeWorkflow, createWorker } = helpers(t); - const worker = await createWorker({ - activities: { - async retryPolicy(): Promise { - return ActivityContext.current().info.retryPolicy; - }, - }, - }); - - const retryPolicy: RetryPolicy = { - backoffCoefficient: 1.5, - initialInterval: 2.0, - maximumAttempts: 3, - maximumInterval: 10.0, - nonRetryableErrorTypes: ['nonRetryableError'], - }; - - await worker.runUntil(async () => { - t.deepEqual(await executeWorkflow(getRetryPolicyFromActivityInfo, { args: [retryPolicy, true] }), retryPolicy); - t.deepEqual(await executeWorkflow(getRetryPolicyFromActivityInfo, { args: [retryPolicy, false] }), retryPolicy); - }); -}); diff --git a/packages/test/src/test-logger.ts b/packages/test/src/test-logger.ts deleted file mode 100644 index 1c03e429d..000000000 --- a/packages/test/src/test-logger.ts +++ /dev/null @@ -1,15 +0,0 @@ -import test from 'ava'; -import { DefaultLogger, LogEntry } from '@temporalio/worker'; - -test('DefaultLogger logs messages according to configured level', (t) => { - const logs: Array> = []; - const log = new DefaultLogger('WARN', ({ level, message, meta }) => logs.push({ level, message, meta })); - log.debug('hey', { a: 1 }); - log.info('ho'); - log.warn('lets', { a: 1 }); - log.error('go'); - t.deepEqual(logs, [ - { level: 'WARN', message: 'lets', meta: { a: 1 } }, - { level: 'ERROR', message: 'go', meta: undefined }, - ]); -}); diff --git a/packages/test/src/test-metrics-custom.ts b/packages/test/src/test-metrics-custom.ts deleted file mode 100644 index 5e73505a2..000000000 --- a/packages/test/src/test-metrics-custom.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { ExecutionContext } from 'ava'; -import { ActivityInboundCallsInterceptor, ActivityOutboundCallsInterceptor, Runtime } from '@temporalio/worker'; -import * as workflow from '@temporalio/workflow'; -import { MetricTags } from '@temporalio/common'; -import { Context as ActivityContext, metricMeter as activityMetricMeter } from '@temporalio/activity'; -import { Context as BaseContext, helpers, makeTestFunction } from './helpers-integration'; -import { getRandomPort } from './helpers'; - -interface Context extends BaseContext { - port: number; -} - -const test = makeTestFunction({ - workflowsPath: __filename, - workflowInterceptorModules: [__filename], - runtimeOpts: async () => { - const port = await getRandomPort(); - return [ - { - telemetryOptions: { - metrics: { - metricPrefix: 'foo_', - prometheus: { - bindAddress: `127.0.0.1:${port}`, - histogramBucketOverrides: { - 'my-float-histogram': [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1], - 'workflow-float-histogram': [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1], - }, - }, - }, - }, - }, - { port }, - ]; - }, -}); - -async function assertMetricReported(t: ExecutionContext, regexp: RegExp) { - const resp = await fetch(`http://127.0.0.1:${t.context.port}/metrics`); - const text = await resp.text(); - const matched = t.regex(text, regexp); - if (!matched) { - t.log(text); - } -} - -/** - * Asserts custom metrics works properly at the bridge level. - */ -test('Custom Metrics - Bridge supports works properly (no tags)', async (t) => { - const meter = Runtime.instance().metricMeter; - - // Counter - const counter = meter.createCounter('my-counter', 'my-counter-unit', 'my-counter-description'); - counter.add(1); - counter.add(1); - counter.add(40); // 1+1+40 => 42 - await assertMetricReported(t, /my_counter 42/); - - // Int Gauge - const gaugeInt = meter.createGauge('my-int-gauge', 'int', 'my-int-gauge-description'); - gaugeInt.set(1); - gaugeInt.set(40); - await assertMetricReported(t, /my_int_gauge 40/); - - // Float Gauge - const gaugeFloat = meter.createGauge('my-float-gauge', 'float', 'my-float-gauge-description'); - gaugeFloat.set(1.1); - gaugeFloat.set(1.1); - gaugeFloat.set(40.1); - await assertMetricReported(t, /my_float_gauge 40.1/); - - // Int Histogram - const histogramInt = meter.createHistogram('my-int-histogram', 'int', 'my-int-histogram-description'); - histogramInt.record(20); - histogramInt.record(200); - histogramInt.record(2000); - await assertMetricReported(t, /my_int_histogram_bucket{le="50"} 1/); - await assertMetricReported(t, /my_int_histogram_bucket{le="500"} 2/); - await assertMetricReported(t, /my_int_histogram_bucket{le="10000"} 3/); - - // Float Histogram - const histogramFloat = meter.createHistogram('my-float-histogram', 'float', 'my-float-histogram-description'); - histogramFloat.record(0.02); - histogramFloat.record(0.07); - histogramFloat.record(0.99); - await assertMetricReported(t, /my_float_histogram_bucket{le="0.05"} 1/); - await assertMetricReported(t, /my_float_histogram_bucket{le="0.1"} 2/); - await assertMetricReported(t, /my_float_histogram_bucket{le="1"} 3/); -}); - -/** - * Asserts custom metrics tags composition works properly - */ -test('Custom Metrics - Tags composition works properly', async (t) => { - const meter = Runtime.instance().metricMeter; - - // Counter - const counter = meter.createCounter('my-counter', 'my-counter-unit', 'my-counter-description'); - counter.add(1, { labelA: 'value-a', labelB: true, labelC: 123, labelD: 123.456 }); - await assertMetricReported(t, /my_counter{labelA="value-a",labelB="true",labelC="123",labelD="123.456"} 1/); - - // Int Gauge - const gaugeInt = meter.createGauge('my-int-gauge', 'int', 'my-int-gauge-description'); - gaugeInt.set(1, { labelA: 'value-a', labelB: true, labelC: 123, labelD: 123.456 }); - await assertMetricReported(t, /my_int_gauge{labelA="value-a",labelB="true",labelC="123",labelD="123.456"} 1/); - - // Float Gauge - const gaugeFloat = meter.createGauge('my-float-gauge', 'float', 'my-float-gauge-description'); - gaugeFloat.set(1.2, { labelA: 'value-a', labelB: true, labelC: 123, labelD: 123.456 }); - await assertMetricReported(t, /my_float_gauge{labelA="value-a",labelB="true",labelC="123",labelD="123.456"} 1.2/); - - // Int Histogram - const histogramInt = meter.createHistogram('my-int-histogram', 'int', 'my-int-histogram-description'); - histogramInt.record(1, { labelA: 'value-a', labelB: true, labelC: 123, labelD: 123.456 }); - await assertMetricReported( - t, - /my_int_histogram_bucket{labelA="value-a",labelB="true",labelC="123",labelD="123.456",le="50"} 1/ - ); - - // Float Histogram - const histogramFloat = meter.createHistogram('my-float-histogram', 'float', 'my-float-histogram-description'); - histogramFloat.record(0.4, { labelA: 'value-a', labelB: true, labelC: 123, labelD: 123.456 }); - await assertMetricReported( - t, - /my_float_histogram_bucket{labelA="value-a",labelB="true",labelC="123",labelD="123.456",le="0.5"} 1/ - ); -}); - -export async function metricWorksWorkflow(): Promise { - const metricMeter = workflow.metricMeter; - - const myCounterMetric = metricMeter.createCounter( - 'workflow-counter', - 'workflow-counter-unit', - 'workflow-counter-description' - ); - const myHistogramMetric = metricMeter.createHistogram( - 'workflow-histogram', - 'int', - 'workflow-histogram-unit', - 'workflow-histogram-description' - ); - const myFloatHistogramMetric = metricMeter.createHistogram( - 'workflow-float-histogram', - 'float', - 'workflow-float-histogram-unit', - 'workflow-float-histogram-description' - ); - const myGaugeMetric = metricMeter.createGauge( - 'workflow-gauge', - 'int', - 'workflow-gauge-unit', - 'workflow-gauge-description' - ); - const myFloatGaugeMetric = metricMeter.createGauge( - 'workflow-float-gauge', - 'float', - 'workflow-float-gauge-unit', - 'workflow-float-gauge-description' - ); - - myCounterMetric.add(1); - myHistogramMetric.record(1); - myFloatHistogramMetric.record(0.01); - myGaugeMetric.set(1); - myFloatGaugeMetric.set(0.1); - - // Pause here, so that we can force replay to a distinct worker - let signalReceived = false; - workflow.setHandler(workflow.defineUpdate('checkpoint'), () => {}); - workflow.setHandler(workflow.defineSignal('unblock'), () => { - signalReceived = true; - }); - await workflow.condition(() => signalReceived); - - myCounterMetric.add(3); - myHistogramMetric.record(3); - myFloatHistogramMetric.record(0.03); - myGaugeMetric.set(3); - myFloatGaugeMetric.set(0.3); -} - -test('Metric in Workflow works and are not replayed', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker({ - // Avoid delay when transitioning to the second worker - stickyQueueScheduleToStartTimeout: '100ms', - }); - - const [handle1, handle2] = await worker.runUntil(async () => { - // Start two workflows, and wait for both to reach the checkpoint. - // Why two workflows? To confirm that there's no problem in having multiple - // workflows and sink reinstantiating a same metric. - return await Promise.all([ - // FIXME: Add support for Update with Start to our internal test helpers - startWorkflow(metricWorksWorkflow).then(async (handle) => { - await handle.executeUpdate('checkpoint'); - return handle; - }), - startWorkflow(metricWorksWorkflow).then(async (handle) => { - await handle.executeUpdate('checkpoint'); - return handle; - }), - ]); - }); - - await assertMetricReported(t, /workflow_counter{[^}]+} 2/); // 1 + 1 - await assertMetricReported(t, /workflow_histogram_bucket{[^}]+?,le="50"} 2/); - await assertMetricReported(t, /workflow_float_histogram_bucket{[^}]+?,le="0.05"} 2/); - await assertMetricReported(t, /workflow_gauge{[^}]+} 1/); - await assertMetricReported(t, /workflow_float_gauge{[^}]+} 0.1/); - - const worker2 = await createWorker(); - await worker2.runUntil(async () => { - await Promise.all([handle1.signal('unblock'), handle2.signal('unblock')]); - await Promise.all([handle1.result(), handle2.result()]); - }); - - await assertMetricReported(t, /workflow_counter{[^}]+} 8/); // 1 + 1 + 3 + 3 - await assertMetricReported(t, /workflow_histogram_bucket{[^}]+?,le="50"} 4/); - await assertMetricReported(t, /workflow_float_histogram_bucket{[^}]+?,le="0.05"} 4/); - await assertMetricReported(t, /workflow_gauge{[^}]+} 3/); - await assertMetricReported(t, /workflow_float_gauge{[^}]+} 0.3/); -}); - -export async function MetricTagsWorkflow(): Promise { - const metricMeter = workflow.metricMeter.withTags({ labelX: 'value-x', labelY: 'value-y' }); - - const myCounterMetric = metricMeter.createCounter( - 'workflow2-counter', - 'workflow2-counter-unit', - 'workflow2-counter-description' - ); - const myHistogramMetric = metricMeter.createHistogram( - 'workflow2-histogram', - 'int', - 'workflow2-histogram-unit', - 'workflow2-histogram-description' - ); - const myGaugeMetric = metricMeter.createGauge( - 'workflow2-gauge', - 'int', - 'workflow2-gauge-unit', - 'workflow2-gauge-description' - ); - - myCounterMetric - .withTags({ labelA: 'value-a', labelB: 'value-b' }) - .withTags({ labelC: 'value-c', labelB: 'value-b2' }) - .add(2, { labelD: 'value-d' }); - - myHistogramMetric - .withTags({ labelA: 'value-a', labelB: 'value-b' }) - .withTags({ labelC: 'value-c', labelB: 'value-b2' }) - .record(2, { labelD: 'value-d' }); - - myGaugeMetric - .withTags({ labelA: 'value-a', labelB: 'value-b' }) - .withTags({ labelC: 'value-c', labelB: 'value-b2' }) - .set(2, { labelD: 'value-d' }); -} - -test('Metric tags in Workflow works', async (t) => { - const { createWorker, executeWorkflow, taskQueue } = helpers(t); - const tags = `labelA="value-a",labelB="value-b2",labelC="value-c",labelD="value-d",labelX="value-x",labelY="value-y",namespace="default",taskQueue="${taskQueue}",workflowType="MetricTagsWorkflow"`; - - const worker = await createWorker(); - await worker.runUntil(executeWorkflow(MetricTagsWorkflow)); - - await assertMetricReported(t, new RegExp(`workflow2_counter{${tags}} 2`)); - await assertMetricReported(t, new RegExp(`workflow2_histogram_bucket{${tags},le="50"} 1`)); - await assertMetricReported(t, new RegExp(`workflow2_gauge{${tags}} 2`)); -}); - -// Define workflow interceptor for metrics -export const interceptors = (): workflow.WorkflowInterceptors => ({ - outbound: [ - { - getMetricTags(tags: MetricTags): MetricTags { - if (!workflow.workflowInfo().workflowType.includes('Interceptor')) return tags; - return { - ...tags, - intercepted: 'workflow-interceptor', - }; - }, - }, - ], -}); - -// Define activity interceptor for metrics -export function activityInterceptorFactory(_ctx: ActivityContext): { - inbound?: ActivityInboundCallsInterceptor; - outbound?: ActivityOutboundCallsInterceptor; -} { - return { - outbound: { - getMetricTags(tags: MetricTags): MetricTags { - return { - ...tags, - intercepted: 'activity-interceptor', - }; - }, - }, - }; -} - -// Activity that uses metrics -export async function metricActivity(): Promise { - const { metricMeter } = ActivityContext.current(); - - const counter = metricMeter.createCounter('activity-counter'); - counter.add(5); - - const histogram = metricMeter.createHistogram('activity-histogram'); - histogram.record(10); - - // Use the `metricMeter` exported from the top level of the activity module rather than the one in the context - const gauge = activityMetricMeter.createGauge('activity-gauge'); - gauge.set(15); -} - -// Workflow that uses metrics and calls the activity -export async function metricsInterceptorWorkflow(): Promise { - const metricMeter = workflow.metricMeter; - - // Use workflow metrics - const counter = metricMeter.createCounter('intercepted-workflow-counter'); - counter.add(3); - - const histogram = metricMeter.createHistogram('intercepted-workflow-histogram'); - histogram.record(6); - - const gauge = metricMeter.createGauge('intercepted-workflow-gauge'); - gauge.set(9); - - // Call activity with metrics - await workflow - .proxyActivities({ - startToCloseTimeout: '1 minute', - }) - .metricActivity(); -} - -// Test for workflow metrics interceptor -test('Workflow and Activity Context metrics interceptors add tags', async (t) => { - const { createWorker, executeWorkflow, taskQueue } = helpers(t); - - const worker = await createWorker({ - taskQueue, - workflowsPath: __filename, - activities: { - metricActivity, - }, - interceptors: { - activity: [activityInterceptorFactory], - }, - }); - - await worker.runUntil(executeWorkflow(metricsInterceptorWorkflow)); - - // Verify workflow metrics have interceptor tag - await assertMetricReported(t, /intercepted_workflow_counter{[^}]*intercepted="workflow-interceptor"[^}]*} 3/); - await assertMetricReported( - t, - /intercepted_workflow_histogram_bucket{[^}]*intercepted="workflow-interceptor"[^}]*} \d+/ - ); - await assertMetricReported(t, /intercepted_workflow_gauge{[^}]*intercepted="workflow-interceptor"[^}]*} 9/); - - // Verify activity metrics have interceptor tag - await assertMetricReported(t, /activity_counter{[^}]*intercepted="activity-interceptor"[^}]*} 5/); - await assertMetricReported(t, /activity_histogram_bucket{[^}]*intercepted="activity-interceptor"[^}]*} \d+/); - await assertMetricReported(t, /activity_gauge{[^}]*intercepted="activity-interceptor"[^}]*} 15/); -}); diff --git a/packages/test/src/test-mockactivityenv.ts b/packages/test/src/test-mockactivityenv.ts deleted file mode 100644 index dd8916b25..000000000 --- a/packages/test/src/test-mockactivityenv.ts +++ /dev/null @@ -1,49 +0,0 @@ -import test from 'ava'; -import { MockActivityEnvironment } from '@temporalio/testing'; -import * as activity from '@temporalio/activity'; -import { Runtime } from '@temporalio/worker'; - -test("MockActivityEnvironment doesn't implicitly instantiate Runtime", async (t) => { - t.is(Runtime._instance, undefined); - const env = new MockActivityEnvironment(); - await env.run(async (): Promise => { - activity.log.info('log message from activity'); - }); - t.is(Runtime._instance, undefined); -}); - -test('MockActivityEnvironment can run a single activity', async (t) => { - const env = new MockActivityEnvironment(); - const res = await env.run(async (x: number): Promise => { - return x + 1; - }, 3); - t.is(res, 4); -}); - -test('MockActivityEnvironment emits heartbeat events and can be cancelled', async (t) => { - const env = new MockActivityEnvironment(); - env.on('heartbeat', (d: unknown) => { - if (d === 6) { - env.cancel('CANCELLED'); - } - }); - await t.throwsAsync( - env.run(async (x: number): Promise => { - activity.heartbeat(6); - await activity.sleep(100); - return x + 1; - }, 3), - { - instanceOf: activity.CancelledFailure, - message: 'CANCELLED', - } - ); -}); - -test('MockActivityEnvironment injects provided info', async (t) => { - const env = new MockActivityEnvironment({ attempt: 3 }); - const res = await env.run(async (x: number): Promise => { - return x + activity.activityInfo().attempt; - }, 1); - t.is(res, 4); -}); diff --git a/packages/test/src/test-native-connection-headers.ts b/packages/test/src/test-native-connection-headers.ts deleted file mode 100644 index db466930d..000000000 --- a/packages/test/src/test-native-connection-headers.ts +++ /dev/null @@ -1,146 +0,0 @@ -import util from 'node:util'; -import path from 'node:path'; -import assert from 'node:assert'; -import test from 'ava'; -import { Subject, firstValueFrom, skip } from 'rxjs'; -import * as grpc from '@grpc/grpc-js'; -import * as protoLoader from '@grpc/proto-loader'; -import { NativeConnection } from '@temporalio/worker'; -import { temporal } from '@temporalio/proto'; -import { Worker } from './helpers'; - -const workflowServicePackageDefinition = protoLoader.loadSync( - path.resolve( - __dirname, - '../../core-bridge/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/service.proto' - ), - { includeDirs: [path.resolve(__dirname, '../../core-bridge/sdk-core/sdk-core-protos/protos/api_upstream')] } -); -const workflowServiceProtoDescriptor = grpc.loadPackageDefinition(workflowServicePackageDefinition) as any; - -async function bindLocalhost(server: grpc.Server): Promise { - return await util.promisify(server.bindAsync.bind(server))('127.0.0.1:0', grpc.ServerCredentials.createInsecure()); -} - -test('NativeConnection passes headers provided in options', async (t) => { - const packageDefinition = protoLoader.loadSync( - path.resolve( - __dirname, - '../../core-bridge/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/service.proto' - ), - { includeDirs: [path.resolve(__dirname, '../../core-bridge/sdk-core/sdk-core-protos/protos/api_upstream')] } - ); - const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any; - - const server = new grpc.Server(); - let gotInitialHeader = false; - let gotApiKey = false; - const newValuesSubject = new Subject(); - - // Create a mock server to verify headers are actually sent - server.addService(protoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - // called on NativeConnection.connect() - getSystemInfo( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IGetSystemInfoRequest, - temporal.api.workflowservice.v1.IGetSystemInfoResponse - >, - callback: grpc.sendUnaryData - ) { - const [value] = call.metadata.get('initial'); - if (value === 'true') { - gotInitialHeader = true; - } - const [apiVal] = call.metadata.get('authorization'); - if (apiVal === 'Bearer enchi_cat') { - gotApiKey = true; - } - callback(null, {}); - }, - // called when worker starts polling for tasks - pollActivityTaskQueue( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IPollActivityTaskQueueRequest, - temporal.api.workflowservice.v1.PollActivityTaskQueueResponse - >, - callback: grpc.sendUnaryData - ) { - const [value] = call.metadata.get('update'); - if (value === 'true') { - newValuesSubject.next(); - } - const [apiVal] = call.metadata.get('authorization'); - if (apiVal === 'Bearer cute_kitty') { - newValuesSubject.next(); - } - callback(new Error()); - }, - }); - const port = await util.promisify(server.bindAsync.bind(server))( - '127.0.0.1:0', - grpc.ServerCredentials.createInsecure() - ); - const connection = await NativeConnection.connect({ - address: `127.0.0.1:${port}`, - metadata: { initial: 'true' }, - apiKey: 'enchi_cat', - }); - t.true(gotInitialHeader); - t.true(gotApiKey); - - await connection.setMetadata({ update: 'true' }); - await connection.setApiKey('cute_kitty'); - // Create a worker so it starts polling for activities so we can check our mock server got the "update" header & - // new api key - const worker = await Worker.create({ - connection, - taskQueue: 'tq', - activities: { - async noop() { - /* yes eslint this is meant to be empty */ - }, - }, - }); - await Promise.all([firstValueFrom(newValuesSubject.pipe(skip(1))).then(() => worker.shutdown()), worker.run()]); -}); - -test('apiKey sets temporal-namespace header appropriately', async (t) => { - let getSystemInfoHeaders: grpc.Metadata = new grpc.Metadata(); - let startWorkflowExecutionHeaders: grpc.Metadata = new grpc.Metadata(); - - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - getSystemInfo( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IGetSystemInfoRequest, - temporal.api.workflowservice.v1.IGetSystemInfoResponse - >, - callback: grpc.sendUnaryData - ) { - getSystemInfoHeaders = call.metadata.clone(); - callback(null, {}); - }, - startWorkflowExecution(call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) { - startWorkflowExecutionHeaders = call.metadata.clone(); - callback(null, {}); - }, - }); - const port = await bindLocalhost(server); - const conn = await NativeConnection.connect({ - address: `127.0.0.1:${port}`, - metadata: { staticKey: 'set' }, - apiKey: 'test-token', - }); - - await conn.workflowService.startWorkflowExecution({ namespace: 'test-namespace' }); - - assert(getSystemInfoHeaders !== undefined); - t.deepEqual(getSystemInfoHeaders.get('temporal-namespace'), []); - t.deepEqual(getSystemInfoHeaders.get('authorization'), ['Bearer test-token']); - t.deepEqual(getSystemInfoHeaders.get('staticKey'), ['set']); - - assert(startWorkflowExecutionHeaders); - t.deepEqual(startWorkflowExecutionHeaders.get('temporal-namespace'), ['test-namespace']); - t.deepEqual(startWorkflowExecutionHeaders.get('authorization'), ['Bearer test-token']); - t.deepEqual(startWorkflowExecutionHeaders.get('staticKey'), ['set']); -}); diff --git a/packages/test/src/test-native-connection.ts b/packages/test/src/test-native-connection.ts deleted file mode 100644 index 4eabf99ed..000000000 --- a/packages/test/src/test-native-connection.ts +++ /dev/null @@ -1,477 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import util from 'node:util'; -import path from 'node:path'; -import fs from 'node:fs/promises'; -import test from 'ava'; -import * as grpc from '@grpc/grpc-js'; -import * as protoLoader from '@grpc/proto-loader'; -import { Client, NamespaceNotFoundError, WorkflowNotFoundError } from '@temporalio/client'; -import { InternalConnectionOptions, InternalConnectionOptionsSymbol } from '@temporalio/client/lib/connection'; -import { IllegalStateError, NativeConnection, NativeConnectionOptions, TransportError } from '@temporalio/worker'; -import { temporal } from '@temporalio/proto'; -import { TestWorkflowEnvironment } from '@temporalio/testing'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; - -const workflowServicePackageDefinition = protoLoader.loadSync( - path.resolve( - __dirname, - '../../core-bridge/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/service.proto' - ), - { includeDirs: [path.resolve(__dirname, '../../core-bridge/sdk-core/sdk-core-protos/protos/api_upstream')] } -); -const workflowServiceProtoDescriptor = grpc.loadPackageDefinition(workflowServicePackageDefinition) as any; - -const operatorServicePackageDefinition = protoLoader.loadSync( - path.resolve( - __dirname, - '../../core-bridge/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/operatorservice/v1/service.proto' - ), - { includeDirs: [path.resolve(__dirname, '../../core-bridge/sdk-core/sdk-core-protos/protos/api_upstream')] } -); -const operatorServiceProtoDescriptor = grpc.loadPackageDefinition(operatorServicePackageDefinition) as any; - -const healthServicePackageDefinition = protoLoader.loadSync( - path.resolve(__dirname, '../../core-bridge/sdk-core/sdk-core-protos/protos/grpc/health/v1/health.proto'), - { includeDirs: [] } -); -const healthServiceProtoDescriptor = grpc.loadPackageDefinition(healthServicePackageDefinition) as any; - -const testServicePackageDefinition = protoLoader.loadSync( - path.resolve( - __dirname, - '../../core-bridge/sdk-core/sdk-core-protos/protos/testsrv_upstream/temporal/api/testservice/v1/service.proto' - ), - { includeDirs: [path.resolve(__dirname, '../../core-bridge/sdk-core/sdk-core-protos/protos/testsrv_upstream')] } -); -const testServiceProtoDescriptor = grpc.loadPackageDefinition(testServicePackageDefinition) as any; - -async function bindLocalhostIpv6(server: grpc.Server): Promise { - return await util.promisify(server.bindAsync.bind(server))('[::1]:0', grpc.ServerCredentials.createInsecure()); -} - -async function bindLocalhostTls(server: grpc.Server): Promise { - const caCert = await fs.readFile(path.resolve(__dirname, `../tls_certs/test-ca.crt`)); - const serverChainCert = await fs.readFile(path.resolve(__dirname, `../tls_certs/test-server-chain.crt`)); - const serverKey = await fs.readFile(path.resolve(__dirname, `../tls_certs/test-server.key`)); - const credentials = grpc.ServerCredentials.createSsl( - caCert, - [ - { - cert_chain: serverChainCert, - private_key: serverKey, - }, - ], - false - ); - return await util.promisify(server.bindAsync.bind(server))('127.0.0.1:0', credentials); -} - -test('NativeConnection.connect() throws meaningful error when passed invalid address', async (t) => { - await t.throwsAsync(NativeConnection.connect({ address: ':invalid' }), { - instanceOf: TypeError, - message: /Invalid address for Temporal gRPC endpoint.*/, - }); -}); - -test('NativeConnection.connect() throws meaningful error when passed invalid clientCertPair', async (t) => { - await t.throwsAsync(NativeConnection.connect({ tls: { clientCertPair: {} as any } }), { - instanceOf: TypeError, - message: /tls\.clientTlsConfig\.clientCert: Missing property 'clientCert'/, - }); -}); - -if (RUN_INTEGRATION_TESTS) { - test('NativeConnection errors have detail', async (t) => { - await t.throwsAsync(() => NativeConnection.connect({ address: '127.0.0.1:1' }), { - instanceOf: TransportError, - message: /.*Connection[ ]?refused.*/i, - }); - }); - - test('NativeConnection.close() throws when called a second time', async (t) => { - const conn = await NativeConnection.connect(); - await conn.close(); - await t.throwsAsync(() => conn.close(), { - instanceOf: IllegalStateError, - message: 'Client already closed', - }); - }); - - test('NativeConnection.close() throws if being used by a Worker and succeeds if it has been shutdown', async (t) => { - const connection = await NativeConnection.connect(); - const worker = await Worker.create({ - connection, - taskQueue: 'default', - activities: { - async noop() { - // empty placeholder - }, - }, - }); - try { - await t.throwsAsync(() => connection.close(), { - instanceOf: IllegalStateError, - message: 'Cannot close connection while Workers hold a reference to it', - }); - } finally { - const p = worker.run(); - worker.shutdown(); - await p; - await connection.close(); - } - }); -} - -test('NativeConnection can connect using "[ipv6]:port" address', async (t) => { - let gotRequest = false; - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - getSystemInfo( - _call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IGetSystemInfoRequest, - temporal.api.workflowservice.v1.IGetSystemInfoResponse - >, - callback: grpc.sendUnaryData - ) { - gotRequest = true; - callback(null, {}); - }, - }); - const port = await bindLocalhostIpv6(server); - const connection = await NativeConnection.connect({ - address: `[::1]:${port}`, - }); - t.true(gotRequest); - await connection.close(); - server.forceShutdown(); -}); - -test('Can configure TLS + call credentials', async (t) => { - let gotRequest = false; - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - getSystemInfo( - _call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IGetSystemInfoRequest, - temporal.api.workflowservice.v1.IGetSystemInfoResponse - >, - callback: grpc.sendUnaryData - ) { - gotRequest = true; - callback(null, {}); - }, - }); - - const port = await bindLocalhostTls(server); - const connection = await NativeConnection.connect({ - address: `127.0.0.1:${port}`, - tls: { - serverRootCACertificate: await fs.readFile(path.resolve(__dirname, `../tls_certs/test-ca.crt`)), - clientCertPair: { - crt: await fs.readFile(path.resolve(__dirname, `../tls_certs/test-client-chain.crt`)), - key: await fs.readFile(path.resolve(__dirname, `../tls_certs/test-client.key`)), - }, - serverNameOverride: 'server', - }, - }); - - t.true(gotRequest); - await connection.close(); - server.forceShutdown(); -}); - -test('withMetadata and withDeadline propagate metadata and deadline', async (t) => { - const requests = new Array<{ metadata: grpc.Metadata; deadline: grpc.Deadline }>(); - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - getSystemInfo( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IGetSystemInfoRequest, - temporal.api.workflowservice.v1.IGetSystemInfoResponse - >, - callback: grpc.sendUnaryData - ) { - requests.push({ metadata: call.metadata, deadline: call.getDeadline() }); - call.getDeadline(); - callback(null, {}); - }, - }); - - const port = await util.promisify(server.bindAsync.bind(server))( - 'localhost:0', - grpc.ServerCredentials.createInsecure() - ); - const connection = await NativeConnection.connect({ - address: `127.0.0.1:${port}`, - metadata: { 'default-bin': Buffer.from([0x00]) }, - }); - - await connection.withDeadline(Date.now() + 10_000, () => - connection.withMetadata({ test: 'true', 'other-bin': Buffer.from([0x01]) }, () => - connection.workflowService.getSystemInfo({}) - ) - ); - t.is(requests.length, 2); - t.is(requests[1].metadata.get('test').toString(), 'true'); - t.deepEqual(requests[1].metadata.get('default-bin'), [Buffer.from([0x00])]); - t.deepEqual(requests[1].metadata.get('other-bin'), [Buffer.from([0x01])]); - t.true(typeof requests[1].deadline === 'number' && requests[1].deadline > 5_000); - await connection.close(); - server.forceShutdown(); -}); - -test('all WorkflowService methods are implemented', async (t) => { - const server = new grpc.Server(); - const calledMethods = new Set(); - server.addService( - workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, - new Proxy( - {}, - { - get() { - return ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData - ) => { - const parts = call.getPath().split('/'); - const method = parts[parts.length - 1]; - calledMethods.add(method[0].toLowerCase() + method.slice(1)); - callback(null, {}); - }; - }, - } - ) - ); - - const port = await util.promisify(server.bindAsync.bind(server))( - 'localhost:0', - grpc.ServerCredentials.createInsecure() - ); - const connection = await NativeConnection.connect({ - address: `127.0.0.1:${port}`, - }); - - // Transform all methods from pascal case to lower case. - const methods = Object.keys( - workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service - ).map((k) => k[0].toLowerCase() + k.slice(1)); - methods.sort(); - for (const method of methods) { - await (connection.workflowService as any)[method]({}); - t.true(calledMethods.has(method), `method ${method} not called`); - } - - await connection.close(); - server.forceShutdown(); -}); - -test('all OperatorService methods are implemented', async (t) => { - const server = new grpc.Server(); - const calledMethods = new Set(); - server.addService( - operatorServiceProtoDescriptor.temporal.api.operatorservice.v1.OperatorService.service, - new Proxy( - {}, - { - get() { - return ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData - ) => { - const parts = call.getPath().split('/'); - const method = parts[parts.length - 1]; - calledMethods.add(method[0].toLowerCase() + method.slice(1)); - callback(null, {}); - }; - }, - } - ) - ); - - const port = await util.promisify(server.bindAsync.bind(server))( - 'localhost:0', - grpc.ServerCredentials.createInsecure() - ); - const connection = await NativeConnection.connect({ - address: `127.0.0.1:${port}`, - }); - - // Transform all methods from pascal case to lower case. - const methods = Object.keys( - operatorServiceProtoDescriptor.temporal.api.operatorservice.v1.OperatorService.service - ).map((k) => k[0].toLowerCase() + k.slice(1)); - methods.sort(); - for (const method of methods) { - await (connection.operatorService as any)[method]({}); - t.true(calledMethods.has(method), `method ${method} not called`); - } - - await connection.close(); - server.forceShutdown(); -}); - -test('all HealthService methods are implemented', async (t) => { - const server = new grpc.Server(); - const calledMethods = new Set(); - server.addService( - healthServiceProtoDescriptor.grpc.health.v1.Health.service, - new Proxy( - {}, - { - get() { - return ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData - ) => { - const parts = call.getPath().split('/'); - const method = parts[parts.length - 1]; - calledMethods.add(method[0].toLowerCase() + method.slice(1)); - callback(null, {}); - }; - }, - } - ) - ); - - const port = await util.promisify(server.bindAsync.bind(server))( - 'localhost:0', - grpc.ServerCredentials.createInsecure() - ); - const connection = await NativeConnection.connect({ - address: `127.0.0.1:${port}`, - }); - - // Transform all methods from pascal case to lower case. - const methods = Object.keys(healthServiceProtoDescriptor.grpc.health.v1.Health.service).map( - (k) => k[0].toLowerCase() + k.slice(1) - ); - methods.sort(); - for (const method of methods) { - // Intentionally ignore 'watch' because it's a streaming method. - if (method === 'watch') { - continue; - } - await (connection.healthService as any)[method]({}); - t.true(calledMethods.has(method), `method ${method} not called`); - } - - await connection.close(); - server.forceShutdown(); -}); - -test('all TestService methods are implemented', async (t) => { - const server = new grpc.Server(); - const calledMethods = new Set(); - server.addService( - testServiceProtoDescriptor.temporal.api.testservice.v1.TestService.service, - new Proxy( - {}, - { - get() { - return ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData - ) => { - const parts = call.getPath().split('/'); - const method = parts[parts.length - 1]; - calledMethods.add(method[0].toLowerCase() + method.slice(1)); - callback(null, {}); - }; - }, - } - ) - ); - - const port = await util.promisify(server.bindAsync.bind(server))( - 'localhost:0', - grpc.ServerCredentials.createInsecure() - ); - const connection = await NativeConnection.connect({ - address: `127.0.0.1:${port}`, - [InternalConnectionOptionsSymbol]: { supportsTestService: true }, - }); - - // Transform all methods from pascal case to lower case. - const methods = Object.keys(testServiceProtoDescriptor.temporal.api.testservice.v1.TestService.service).map( - (k) => k[0].toLowerCase() + k.slice(1) - ); - methods.sort(); - for (const method of methods) { - await (connection.testService as any)[method]({}); - t.true(calledMethods.has(method), `method ${method} not called`); - } - - await connection.close(); - server.forceShutdown(); -}); - -test('can power workflow client calls', async (t) => { - const env = await TestWorkflowEnvironment.createLocal(); - try { - { - const client = new Client({ connection: env.nativeConnection }); - const handle = await client.workflow.start('dont-care', { - workflowId: t.title + '-' + randomUUID(), - taskQueue: 'dont-care', - }); - await handle.terminate(); - const err = await t.throwsAsync(() => handle.terminate(), { - instanceOf: WorkflowNotFoundError, - }); - t.is(err?.workflowId, handle.workflowId); - } - { - const client = new Client({ connection: env.nativeConnection, namespace: 'non-existing' }); - const err = await t.throwsAsync( - () => - client.workflow.start('dont-care', { - workflowId: 'dont-care', - taskQueue: 'dont-care', - }), - { - instanceOf: NamespaceNotFoundError, - message: "Namespace not found: 'non-existing'", - } - ); - t.is(err?.namespace, 'non-existing'); - } - } finally { - await env.teardown(); - } -}); - -test('setMetadata accepts binary headers', async (t) => { - const requests = new Array<{ metadata: grpc.Metadata; deadline: grpc.Deadline }>(); - const server = new grpc.Server(); - server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, { - getSystemInfo( - call: grpc.ServerUnaryCall< - temporal.api.workflowservice.v1.IGetSystemInfoRequest, - temporal.api.workflowservice.v1.IGetSystemInfoResponse - >, - callback: grpc.sendUnaryData - ) { - requests.push({ metadata: call.metadata, deadline: call.getDeadline() }); - callback(null, {}); - }, - }); - - const port = await util.promisify(server.bindAsync.bind(server))( - 'localhost:0', - grpc.ServerCredentials.createInsecure() - ); - const connection = await NativeConnection.connect({ - address: `127.0.0.1:${port}`, - metadata: { 'start-ascii': 'a', 'start-bin': Buffer.from([0x00]) }, - }); - - await connection.setMetadata({ 'end-bin': Buffer.from([0x01]) }); - - await connection.workflowService.getSystemInfo({}); - t.is(requests.length, 2); - t.deepEqual(requests[1].metadata.get('start-bin'), []); - t.deepEqual(requests[1].metadata.get('start-ascii'), []); - t.deepEqual(requests[1].metadata.get('end-bin'), [Buffer.from([0x01])]); - await connection.close(); - server.forceShutdown(); -}); diff --git a/packages/test/src/test-nexus-handler.ts b/packages/test/src/test-nexus-handler.ts deleted file mode 100644 index f13f2b427..000000000 --- a/packages/test/src/test-nexus-handler.ts +++ /dev/null @@ -1,716 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import anyTest, { TestFn } from 'ava'; -import Long from 'long'; -import * as nexus from 'nexus-rpc'; -import * as protoJsonSerializer from 'proto3-json-serializer'; -import * as temporalnexus from '@temporalio/nexus'; -import * as temporalclient from '@temporalio/client'; -import * as root from '@temporalio/proto'; -import * as testing from '@temporalio/testing'; -import { DefaultLogger, LogEntry, Runtime, Worker } from '@temporalio/worker'; -import { - ApplicationFailure, - CancelledFailure, - defaultFailureConverter, - defaultPayloadConverter, - SdkComponent, -} from '@temporalio/common'; -import { - convertWorkflowEventLinkToNexusLink, - convertNexusLinkToWorkflowEventLink, -} from '@temporalio/nexus/lib/link-converter'; -import { cleanStackTrace, compareStackTrace, getRandomPort } from './helpers'; - -export interface Context { - httpPort: number; - taskQueue: string; - endpointId: string; - env: testing.TestWorkflowEnvironment; - logEntries: LogEntry[]; -} - -const test = anyTest as TestFn; - -test.before(async (t) => { - const logEntries = new Array(); - - const logger = new DefaultLogger('INFO', (entry) => { - logEntries.push(entry); - }); - - Runtime.install({ logger }); - t.context.httpPort = await getRandomPort(); - t.context.env = await testing.TestWorkflowEnvironment.createLocal({ - server: { - extraArgs: [ - '--http-port', - `${t.context.httpPort}`, - // SDK tests use arbitrary callback URLs, permit that on the server. - '--dynamic-config-value', - 'component.callbacks.allowedAddresses=[{"Pattern":"*","AllowInsecure":true}]', - // TODO: remove this config when it becomes the default on the server. - '--dynamic-config-value', - 'history.enableRequestIdRefLinks=true', - ], - }, - }); - t.context.logEntries = logEntries; -}); - -test.after.always(async (t) => { - await t.context.env.teardown(); -}); - -test.beforeEach(async (t) => { - const taskQueue = t.title + randomUUID(); - const { env } = t.context; - const response = await env.connection.operatorService.createNexusEndpoint({ - spec: { - name: t.title.replaceAll(/[\s,.]/g, '-'), - target: { - worker: { - namespace: 'default', - taskQueue, - }, - }, - }, - }); - t.context.taskQueue = taskQueue; - t.context.endpointId = response.endpoint!.id!; - t.truthy(t.context.endpointId); -}); - -test('sync Operation Handler happy path', async (t) => { - const { env, taskQueue, httpPort, endpointId } = t.context; - - const testServiceHandler = nexus.serviceHandler( - nexus.service('testService', { - testSyncOp: nexus.operation(), - }), - { - async testSyncOp(ctx, input) { - // Testing headers normalization to lower case. - if (ctx.headers.Test !== 'true') { - throw new nexus.HandlerError('BAD_REQUEST', 'expected test header to be set to true'); - } - // Echo links back to the caller. - ctx.outboundLinks.push(...ctx.inboundLinks); - return input; - }, - } - ); - - const w = await Worker.create({ - connection: env.nativeConnection, - namespace: env.namespace, - taskQueue, - nexusServices: [testServiceHandler], - }); - - await w.runUntil(async () => { - const res = await fetch( - `http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/testSyncOp`, - { - method: 'POST', - body: JSON.stringify('hello'), - headers: { - 'Content-Type': 'application/json', - Test: 'true', - 'Nexus-Link': '; type="test"', - }, - } - ); - t.true(res.ok); - const output = await res.json(); - t.is(output, 'hello'); - t.is(res.headers.get('Nexus-Link'), '; type="test"'); - }); -}); - -test('Operation Handler cancelation', async (t) => { - const { env, taskQueue, httpPort, endpointId } = t.context; - let p: Promise | undefined; - - const w = await Worker.create({ - connection: env.nativeConnection, - namespace: env.namespace, - taskQueue, - nexusServices: [ - nexus.serviceHandler( - nexus.service('testService', { - testSyncOp: nexus.operation(), - }), - { - async testSyncOp(ctx) { - p = new Promise((_, reject) => { - ctx.abortSignal.onabort = () => { - reject(ctx.abortSignal.reason); - }; - // never resolve this promise. - }); - return await p; - }, - } - ), - ], - }); - - await w.runUntil(async () => { - const res = await fetch( - `http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/testSyncOp`, - { - method: 'POST', - headers: { - 'Request-Timeout': '2s', - }, - } - ); - t.is(res.status, 520 /* UPSTREAM_TIMEOUT */); - // Give time for the worker to actually process the timeout; otherwise the Operation - // may end up being cancelled because of the worker shutdown rather than the timeout. - await Promise.race([ - p?.catch(() => undefined), - new Promise((resolve) => { - setTimeout(resolve, 2000).unref(); - }), - ]); - }); - t.truthy(p); - await t.throwsAsync(p!, { instanceOf: CancelledFailure, message: 'TIMED_OUT' }); -}); - -test('async Operation Handler happy path', async (t) => { - const { env, taskQueue, httpPort, endpointId } = t.context; - const requestId = 'test-' + randomUUID(); - - const w = await Worker.create({ - connection: env.nativeConnection, - namespace: env.namespace, - taskQueue, - nexusServices: [ - nexus.serviceHandler( - nexus.service('testService', { - // Also test custom Operation name. - testAsyncOp: nexus.operation({ name: 'async-op' }), - }), - { - testAsyncOp: { - async start(ctx, input): Promise> { - if (input !== 'hello') { - throw new nexus.HandlerError('BAD_REQUEST', 'expected input to equal "hello"'); - } - if (ctx.headers.test !== 'true') { - throw new nexus.HandlerError('BAD_REQUEST', 'expected test header to be set to true'); - } - if (!ctx.requestId) { - throw new nexus.HandlerError('BAD_REQUEST', 'expected requestId to be set'); - } - return nexus.HandlerStartOperationResult.async(ctx.requestId); - }, - async cancel(ctx, token) { - if (ctx.headers.test !== 'true') { - throw new nexus.HandlerError('BAD_REQUEST', 'expected test header to be set to true'); - } - if (token !== requestId) { - throw new nexus.HandlerError('BAD_REQUEST', 'expected token to equal original requestId'); - } - }, - async getInfo() { - throw new Error('not implemented'); - }, - async getResult() { - throw new Error('not implemented'); - }, - }, - } - ), - ], - }); - - await w.runUntil(async () => { - let res = await fetch(`http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/async-op`, { - method: 'POST', - body: JSON.stringify('hello'), - headers: { - 'Content-Type': 'application/json', - 'Nexus-Request-Id': requestId, - Test: 'true', - }, - }); - t.true(res.ok); - const output = (await res.json()) as { token: string; state: nexus.OperationState }; - - t.is(output.token, requestId); - t.is(output.state, 'running'); - - res = await fetch( - `http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/async-op/cancel`, - { - method: 'POST', - headers: { - 'Nexus-Operation-Token': output.token, - Test: 'true', - }, - } - ); - t.true(res.ok); - }); -}); - -test('start Operation Handler errors', async (t) => { - const { env, taskQueue, httpPort, endpointId } = t.context; - - const w = await Worker.create({ - connection: env.nativeConnection, - namespace: env.namespace, - taskQueue, - nexusServices: [ - nexus.serviceHandler( - nexus.service('testService', { - op: nexus.operation(), - }), - { - async op(_ctx, outcome) { - switch (outcome) { - case 'NonRetryableApplicationFailure': - throw ApplicationFailure.create({ - nonRetryable: true, - message: 'deliberate failure', - details: ['details'], - }); - case 'NonRetryableInternalHandlerError': - throw new nexus.HandlerError('INTERNAL', 'deliberate error', { retryableOverride: false }); - case 'OperationError': - throw new nexus.OperationError('failed', 'deliberate error'); - } - throw new nexus.HandlerError('BAD_REQUEST', 'invalid outcome requested'); - }, - } - ), - ], - }); - - await w.runUntil(async () => { - { - const res = await fetch(`http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/op`, { - method: 'POST', - body: JSON.stringify('NonRetryableApplicationFailure'), - headers: { - 'Content-Type': 'application/json', - }, - }); - t.is(res.status, 500); - const failure = (await res.json()) as any; - const failureType = (root as any).lookupType('temporal.api.failure.v1.Failure'); - const temporalFailure = protoJsonSerializer.fromProto3JSON(failureType, failure.details); - const err = defaultFailureConverter.failureToError(temporalFailure as any, defaultPayloadConverter); - delete failure.details; - - t.deepEqual(failure, { - message: 'deliberate failure', - metadata: { - type: 'temporal.api.failure.v1.Failure', - }, - }); - t.true(err instanceof ApplicationFailure); - t.is(err.message, ''); - compareStackTrace( - t, - cleanStackTrace(err.stack!), - `ApplicationFailure: deliberate failure - 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)` - ); - t.deepEqual((err as ApplicationFailure).details, ['details']); - t.is((err as ApplicationFailure).failure?.source, 'TypeScriptSDK'); - } - { - const res = await fetch(`http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/op`, { - method: 'POST', - body: JSON.stringify('NonRetryableInternalHandlerError'), - headers: { - 'Content-Type': 'application/json', - }, - }); - t.is(res.status, 500); - t.is(res.headers.get('Nexus-Request-Retryable'), 'false'); - const failure = (await res.json()) as any; - const failureType = (root as any).lookupType('temporal.api.failure.v1.Failure'); - const temporalFailure = protoJsonSerializer.fromProto3JSON(failureType, failure.details); - const err = defaultFailureConverter.failureToError(temporalFailure as any, defaultPayloadConverter); - delete failure.details; - t.true(err instanceof Error); - t.is(err.message, ''); - t.deepEqual( - cleanStackTrace(err.stack!), - `HandlerError: deliberate error - 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)` - ); - } - { - const res = await fetch(`http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/op`, { - method: 'POST', - body: JSON.stringify('OperationError'), - headers: { - 'Content-Type': 'application/json', - }, - }); - t.is(res.status, 424 /* As defined in the nexus HTTP spec */); - const failure = (await res.json()) as any; - const failureType = (root as any).lookupType('temporal.api.failure.v1.Failure'); - const temporalFailure = protoJsonSerializer.fromProto3JSON(failureType, failure.details); - const err = defaultFailureConverter.failureToError(temporalFailure as any, defaultPayloadConverter); - delete failure.details; - t.true(err instanceof Error); - t.is(err.message, ''); - t.is(res.headers.get('nexus-operation-state'), 'failed'); - t.deepEqual( - cleanStackTrace(err.stack!), - `OperationError: deliberate error - 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)` - ); - } - { - const res = await fetch(`http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/op`, { - method: 'POST', - body: 'invalid', - headers: { - 'Content-Type': 'application/json', - }, - }); - t.is(res.status, 400); - const { message } = (await res.json()) as { message: string }; - // Exact error message varies between Node versions. - t.regex(message, /Failed to deserialize input: SyntaxError: Unexpected token .* JSON/); - } - }); -}); - -test('cancel Operation Handler errors', async (t) => { - const { env, taskQueue, httpPort, endpointId } = t.context; - - const w = await Worker.create({ - connection: env.nativeConnection, - namespace: env.namespace, - taskQueue, - nexusServices: [ - nexus.serviceHandler( - nexus.service('testService', { - op: nexus.operation(), - }), - { - op: { - async start() { - throw new Error('not implemented'); - }, - async cancel(ctx, _token) { - switch (ctx.headers.outcome) { - case 'NonRetryableApplicationFailure': - throw ApplicationFailure.create({ - nonRetryable: true, - message: 'deliberate failure', - details: ['details'], - }); - case 'NonRetryableInternalHandlerError': - throw new nexus.HandlerError('INTERNAL', 'deliberate error', { retryableOverride: false }); - } - throw new nexus.HandlerError('BAD_REQUEST', 'invalid outcome requested'); - }, - async getInfo() { - throw new Error('not implemented'); - }, - async getResult() { - throw new Error('not implemented'); - }, - }, - } - ), - ], - }); - - await w.runUntil(async () => { - { - const res = await fetch( - `http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/op/cancel`, - { - method: 'POST', - headers: { - 'Nexus-Operation-Token': 'token', - Outcome: 'NonRetryableApplicationFailure', - }, - } - ); - t.is(res.status, 500); - const failure = (await res.json()) as any; - const failureType = (root as any).lookupType('temporal.api.failure.v1.Failure'); - const temporalFailure = protoJsonSerializer.fromProto3JSON(failureType, failure.details); - const err = defaultFailureConverter.failureToError(temporalFailure as any, defaultPayloadConverter); - delete failure.details; - - t.deepEqual(failure, { - message: 'deliberate failure', - metadata: { - type: 'temporal.api.failure.v1.Failure', - }, - }); - t.true(err instanceof ApplicationFailure); - t.is(err.message, ''); - compareStackTrace( - t, - cleanStackTrace(err.stack!), - `ApplicationFailure: deliberate failure - 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)` - ); - t.deepEqual((err as ApplicationFailure).details, ['details']); - t.is((err as ApplicationFailure).failure?.source, 'TypeScriptSDK'); - } - { - const res = await fetch( - `http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/op/cancel`, - { - method: 'POST', - headers: { - 'Nexus-Operation-Token': 'token', - Outcome: 'NonRetryableInternalHandlerError', - }, - } - ); - t.is(res.status, 500); - t.is(res.headers.get('Nexus-Request-Retryable'), 'false'); - const failure = (await res.json()) as any; - const failureType = (root as any).lookupType('temporal.api.failure.v1.Failure'); - const temporalFailure = protoJsonSerializer.fromProto3JSON(failureType, failure.details); - const err = defaultFailureConverter.failureToError(temporalFailure as any, defaultPayloadConverter); - delete failure.details; - t.true(err instanceof Error); - t.is(err.message, ''); - t.deepEqual( - cleanStackTrace(err.stack!), - `HandlerError: deliberate error - at Object.cancel (test/src/test-nexus-handler.ts) - at ServiceRegistry.cancel (nexus-rpc/src/handler/service-registry.ts)` - ); - } - }); -}); - -test('logger is available in handler context', async (t) => { - const { env, taskQueue, httpPort, endpointId, logEntries } = t.context; - - const w = await Worker.create({ - connection: env.nativeConnection, - namespace: env.namespace, - taskQueue, - nexusServices: [ - nexus.serviceHandler( - nexus.service('testService', { - testSyncOp: nexus.operation(), - }), - { - async testSyncOp(_ctx, input) { - temporalnexus.log.info('handler ran', { input }); - return input; - }, - } - ), - ], - }); - - await w.runUntil(async () => { - const res = await fetch( - `http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/testSyncOp`, - { - method: 'POST', - body: JSON.stringify('hello'), - headers: { 'Content-Type': 'application/json' }, - } - ); - t.true(res.ok); - const output = await res.json(); - t.is(output, 'hello'); - }); - - const entries = logEntries.filter(({ meta }) => meta?.sdkComponent === SdkComponent.nexus); - t.is(entries.length, 1); - t.is(entries[0].message, 'handler ran'); - t.deepEqual(entries[0].meta, { - sdkComponent: SdkComponent.nexus, - namespace: env.namespace ?? 'default', - service: 'testService', - operation: 'testSyncOp', - taskQueue, - input: 'hello', - }); -}); - -test('getClient is available in handler context', async (t) => { - const { env, taskQueue, httpPort, endpointId } = t.context; - - const w = await Worker.create({ - connection: env.nativeConnection, - namespace: env.namespace, - taskQueue, - nexusServices: [ - nexus.serviceHandler( - nexus.service('testService', { - testSyncOp: nexus.operation(), - }), - { - async testSyncOp() { - const systemInfo = await temporalnexus - .getClient() - .connection.workflowService.getSystemInfo({ namespace: 'default' }); - return systemInfo.capabilities?.nexus ?? false; - }, - } - ), - ], - }); - - await w.runUntil(async () => { - const res = await fetch( - `http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/testSyncOp`, - { - method: 'POST', - } - ); - t.true(res.ok); - const output = await res.json(); - t.is(output, true); - }); -}); - -test('WorkflowRunOperationHandler attaches callback, link, and request ID', async (t) => { - const { env, taskQueue, httpPort, endpointId } = t.context; - const requestId1 = randomUUID(); - const requestId2 = randomUUID(); - const workflowId = t.title; - - const w = await Worker.create({ - connection: env.nativeConnection, - namespace: env.namespace, - taskQueue, - nexusServices: [ - nexus.serviceHandler( - nexus.service('testService', { - testOp: nexus.operation(), - }), - { - testOp: new temporalnexus.WorkflowRunOperationHandler(async (ctx) => { - return await temporalnexus.startWorkflow(ctx, 'some-workflow', { - workflowId, - // To test attaching multiple callers to the same Operation. - workflowIdConflictPolicy: 'USE_EXISTING', - }); - }), - } - ), - ], - }); - - const callbackURL = 'http://not-found'; - const workflowLink = { - namespace: 'default', - workflowId: 'wid', - runId: 'runId', - eventRef: { - eventId: Long.fromNumber(5), - eventType: root.temporal.api.enums.v1.EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, - }, - }; - const nexusLink = convertWorkflowEventLinkToNexusLink(workflowLink); - - await w.runUntil(async () => { - const backlinks = []; - for (const requestId of [requestId1, requestId2]) { - const endpointUrl = new URL( - `http://127.0.0.1:${httpPort}/nexus/endpoints/${endpointId}/services/testService/testOp` - ); - endpointUrl.searchParams.set('callback', callbackURL); - const res = await fetch(endpointUrl.toString(), { - method: 'POST', - body: JSON.stringify('hello'), - headers: { - 'Content-Type': 'application/json', - 'Nexus-Request-Id': requestId, - 'Nexus-Callback-Token': 'token', - 'Nexus-Link': `<${nexusLink.url}>; type="${nexusLink.type}"`, - }, - }); - t.true(res.ok); - const output = (await res.json()) as { token: string; state: nexus.OperationState }; - t.is(output.state, 'running'); - console.log(res.headers.get('Nexus-Link')); - const m = /<([^>]+)>; type="([^"]+)"/.exec(res.headers.get('Nexus-Link') ?? ''); - t.truthy(m); - const [_, url, type] = m!; - const backlink = convertNexusLinkToWorkflowEventLink({ url: new URL(url), type }); - backlinks.push(backlink); - } - - t.is(backlinks[0].eventRef?.eventType, root.temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); - t.deepEqual(backlinks[0].eventRef?.eventId, Long.fromNumber(1)); - t.is(backlinks[0].workflowId, workflowId); - - console.log(backlinks[1]); - t.is( - backlinks[1].requestIdRef?.eventType, - root.temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_OPTIONS_UPDATED - ); - t.deepEqual(backlinks[1].requestIdRef?.requestId, requestId2); - t.is(backlinks[1].workflowId, workflowId); - }); - - const description = await env.client.workflow.getHandle(workflowId).describe(); - // Ensure that request IDs are propagated. - t.truthy(description.raw.workflowExtendedInfo?.requestIdInfos?.[requestId1]); - t.truthy(description.raw.workflowExtendedInfo?.requestIdInfos?.[requestId2]); - - // Ensure that callbacks are attached. - t.is(description.raw.callbacks?.length, 2); - // Don't bother verifying the second callback. - const callback = description.raw.callbacks?.[0].callback; - t.is(callback?.nexus?.url, callbackURL); - t.deepEqual(callback?.nexus?.header, { token: 'token' }); - t.is(callback?.links?.length, 1); - const actualLink = callback!.links![0]!.workflowEvent; - - t.deepEqual(actualLink?.namespace, workflowLink.namespace); - t.deepEqual(actualLink?.workflowId, workflowLink.workflowId); - t.deepEqual(actualLink?.runId, workflowLink.runId); - t.deepEqual(actualLink?.eventRef?.eventType, workflowLink.eventRef.eventType); - t.deepEqual(actualLink?.eventRef?.eventId, workflowLink.eventRef.eventId); -}); - -test('WorkflowRunOperationHandler does not accept WorkflowHandle from WorkflowClient.start', async (t) => { - // Dummy client, it won't actually be called. - const client: temporalclient.WorkflowClient = undefined as any; - - nexus.serviceHandler( - nexus.service('testService', { - syncOperation: nexus.operation(), - }), - { - syncOperation: new temporalnexus.WorkflowRunOperationHandler( - // @ts-expect-error - Missing property [isNexusWorkflowHandle] - async (_ctx) => { - return await client.start('some-workflow', { - workflowId: 'some-workflow', - taskQueue: 'some-task-queue', - }); - } - ), - } - ); - - // This test only checks for compile-time error. - t.pass(); -}); diff --git a/packages/test/src/test-nexus-link-converter.ts b/packages/test/src/test-nexus-link-converter.ts deleted file mode 100644 index 6763af1cf..000000000 --- a/packages/test/src/test-nexus-link-converter.ts +++ /dev/null @@ -1,163 +0,0 @@ -import test from 'ava'; -import Long from 'long'; -import { temporal } from '@temporalio/proto'; -import { - convertWorkflowEventLinkToNexusLink, - convertNexusLinkToWorkflowEventLink, -} from '@temporalio/nexus/lib/link-converter'; - -const { EventType } = temporal.api.enums.v1; -const WORKFLOW_EVENT_TYPE = (temporal.api.common.v1.Link.WorkflowEvent as any).fullName.slice(1); - -function makeEventRef(eventId: number, eventType: keyof typeof EventType) { - return { - eventId: Long.fromNumber(eventId), - eventType: EventType[eventType], - }; -} - -function makeRequestIdRef(requestId: string, eventType: keyof typeof EventType) { - return { - requestId, - eventType: EventType[eventType], - }; -} - -test('convertWorkflowEventLinkToNexusLink and back with eventRef', (t) => { - const we = { - namespace: 'ns', - workflowId: 'wid', - runId: 'rid', - eventRef: makeEventRef(42, 'EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED'), - }; - const nexusLink = convertWorkflowEventLinkToNexusLink(we); - t.is(nexusLink.type, WORKFLOW_EVENT_TYPE); - t.regex(nexusLink.url.toString(), /^temporal:\/\/\/namespaces\/ns\/workflows\/wid\/rid\/history\?/); - - const roundTrip = convertNexusLinkToWorkflowEventLink(nexusLink); - t.deepEqual(roundTrip, we); -}); - -test('convertWorkflowEventLinkToNexusLink and back with requestIdRef', (t) => { - const we = { - namespace: 'ns2', - workflowId: 'wid2', - runId: 'rid2', - requestIdRef: makeRequestIdRef('req-123', 'EVENT_TYPE_WORKFLOW_TASK_COMPLETED'), - }; - const nexusLink = convertWorkflowEventLinkToNexusLink(we); - t.is(nexusLink.type, WORKFLOW_EVENT_TYPE); - t.regex(nexusLink.url.toString(), /^temporal:\/\/\/namespaces\/ns2\/workflows\/wid2\/rid2\/history\?/); - - const roundTrip = convertNexusLinkToWorkflowEventLink(nexusLink); - t.deepEqual(roundTrip, we); -}); - -test('convertNexusLinkToLinkWorkflowEvent with an event type in PascalCase', (t) => { - const nexusLink = { - url: new URL( - 'temporal:///namespaces/ns2/workflows/wid2/rid2/history?referenceType=RequestIdReference&requestID=req-123&eventType=WorkflowTaskCompleted' - ), - type: WORKFLOW_EVENT_TYPE, - }; - - const workflowEventLink = convertNexusLinkToWorkflowEventLink(nexusLink); - t.is(workflowEventLink.requestIdRef?.eventType, EventType.EVENT_TYPE_WORKFLOW_TASK_COMPLETED); -}); - -test('throws on missing required fields', (t) => { - t.throws( - () => - convertWorkflowEventLinkToNexusLink({ - namespace: '', - workflowId: 'wid', - runId: 'rid', - }), - { instanceOf: TypeError } - ); - t.throws( - () => - convertWorkflowEventLinkToNexusLink({ - namespace: 'ns', - workflowId: '', - runId: 'rid', - }), - { instanceOf: TypeError } - ); - t.throws( - () => - convertWorkflowEventLinkToNexusLink({ - namespace: 'ns', - workflowId: 'wid', - runId: '', - }), - { instanceOf: TypeError } - ); -}); - -test('throws on invalid URL scheme', (t) => { - const fakeLink = { - url: new URL('http://example.com'), - type: WORKFLOW_EVENT_TYPE, - }; - t.throws(() => convertNexusLinkToWorkflowEventLink(fakeLink as any), { instanceOf: TypeError }); -}); - -test('throws on invalid URL path', (t) => { - const fakeLink = { - url: new URL('temporal:///badpath'), - type: WORKFLOW_EVENT_TYPE, - }; - t.throws(() => convertNexusLinkToWorkflowEventLink(fakeLink as any), { instanceOf: TypeError }); -}); - -test('throws on unknown reference type', (t) => { - const url = new URL('temporal:///namespaces/ns/workflows/wid/rid/history?referenceType=UnknownType'); - const fakeLink = { - url, - type: WORKFLOW_EVENT_TYPE, - }; - t.throws(() => convertNexusLinkToWorkflowEventLink(fakeLink as any), { instanceOf: TypeError }); -}); - -test('throws on missing eventType in eventRef', (t) => { - const url = new URL('temporal:///namespaces/ns/workflows/wid/rid/history?referenceType=EventReference&eventID=1'); - const fakeLink = { - url, - type: WORKFLOW_EVENT_TYPE, - }; - t.throws(() => convertNexusLinkToWorkflowEventLink(fakeLink as any), { message: /Missing eventType parameter/ }); -}); - -test('throws on unknown eventType in eventRef', (t) => { - const url = new URL( - 'temporal:///namespaces/ns/workflows/wid/rid/history?referenceType=EventReference&eventID=1&eventType=NotAType' - ); - const fakeLink = { - url, - type: WORKFLOW_EVENT_TYPE, - }; - t.throws(() => convertNexusLinkToWorkflowEventLink(fakeLink as any), { message: /Unknown eventType parameter/ }); -}); - -test('throws on missing eventType in requestIdRef', (t) => { - const url = new URL( - 'temporal:///namespaces/ns/workflows/wid/rid/history?referenceType=RequestIdReference&requestID=req' - ); - const fakeLink = { - url, - type: WORKFLOW_EVENT_TYPE, - }; - t.throws(() => convertNexusLinkToWorkflowEventLink(fakeLink as any), { message: /Missing eventType parameter/ }); -}); - -test('throws on unknown eventType in requestIdRef', (t) => { - const url = new URL( - 'temporal:///namespaces/ns/workflows/wid/rid/history?referenceType=RequestIdReference&requestID=req&eventType=NotAType' - ); - const fakeLink = { - url, - type: WORKFLOW_EVENT_TYPE, - }; - t.throws(() => convertNexusLinkToWorkflowEventLink(fakeLink as any), { message: /Unknown eventType parameter/ }); -}); diff --git a/packages/test/src/test-nexus-token-helpers.ts b/packages/test/src/test-nexus-token-helpers.ts deleted file mode 100644 index 57b9eca35..000000000 --- a/packages/test/src/test-nexus-token-helpers.ts +++ /dev/null @@ -1,43 +0,0 @@ -import test from 'ava'; -import { - base64URLEncodeNoPadding, - generateWorkflowRunOperationToken, - loadWorkflowRunOperationToken, -} from '@temporalio/nexus/lib/token'; - -test('encode and decode workflow run Operation token', (t) => { - const expected = { - t: 1, - ns: 'ns', - wid: 'w', - }; - const token = generateWorkflowRunOperationToken('ns', 'w'); - const decoded = loadWorkflowRunOperationToken(token); - t.deepEqual(decoded, expected); -}); - -test('decode workflow run Operation token errors', (t) => { - t.throws(() => loadWorkflowRunOperationToken(''), { message: /invalid workflow run token: token is empty/ }); - - t.throws(() => loadWorkflowRunOperationToken('not-base64!@#$'), { message: /failed to decode token/ }); - - const invalidJSONToken = base64URLEncodeNoPadding('invalid json'); - t.throws(() => loadWorkflowRunOperationToken(invalidJSONToken), { - message: /failed to unmarshal workflow run Operation token/, - }); - - const invalidTypeToken = base64URLEncodeNoPadding('{"t":2}'); - t.throws(() => loadWorkflowRunOperationToken(invalidTypeToken), { - message: /invalid workflow token type: 2, expected: 1/, - }); - - const missingWIDToken = base64URLEncodeNoPadding('{"t":1}'); - t.throws(() => loadWorkflowRunOperationToken(missingWIDToken), { - message: /invalid workflow run token: missing workflow ID \(wid\)/, - }); - - const versionedToken = base64URLEncodeNoPadding('{"v":1, "t":1,"wid": "workflow-id"}'); - t.throws(() => loadWorkflowRunOperationToken(versionedToken), { - message: /invalid workflow run token: "v" field should not be present/, - }); -}); diff --git a/packages/test/src/test-nexus-workflow-caller.ts b/packages/test/src/test-nexus-workflow-caller.ts deleted file mode 100644 index 2df87e139..000000000 --- a/packages/test/src/test-nexus-workflow-caller.ts +++ /dev/null @@ -1,313 +0,0 @@ -import assert from 'assert'; -import { randomUUID } from 'crypto'; -import * as nexus from 'nexus-rpc'; -import { ApplicationFailure, CancelledFailure, NexusOperationFailure } from '@temporalio/common'; -import { WorkflowFailedError } from '@temporalio/client'; -import * as temporalnexus from '@temporalio/nexus'; -import * as workflow from '@temporalio/workflow'; -import { helpers, makeTestFunction } from './helpers-integration'; - -const service = nexus.service('test', { - syncOp: nexus.operation({ name: 'my-sync-op' }), - asyncOp: nexus.operation(), -}); - -const test = makeTestFunction({ - workflowsPath: __filename, - workflowInterceptorModules: [__filename], -}); - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -export async function caller(endpoint: string, op: keyof typeof service.operations, action: string): Promise { - const client = workflow.createNexusClient({ - endpoint, - service, - }); - return await workflow.CancellationScope.cancellable(async () => { - const handle = await client.startOperation(op, action); - if (action === 'waitForCancel') { - workflow.CancellationScope.current().cancel(); - } - return await handle.result(); - }); -} - -export async function handler(action: string): Promise { - if (action === 'failWorkflow') { - throw ApplicationFailure.create({ - nonRetryable: true, - message: 'test asked to fail', - type: 'IntentionalError', - details: ['a detail'], - }); - } - if (action === 'waitForCancel') { - await workflow.CancellationScope.current().cancelRequested; - } - return action; -} - -test('Nexus Operation from a Workflow', async (t) => { - const { createWorker, executeWorkflow, taskQueue } = helpers(t); - const endpoint = t.title.replaceAll(/[\s,]/g, '-') + '-' + randomUUID(); - await t.context.env.connection.operatorService.createNexusEndpoint({ - spec: { - name: endpoint, - target: { - worker: { - namespace: 'default', - taskQueue, - }, - }, - }, - }); - const worker = await createWorker({ - nexusServices: [ - nexus.serviceHandler(service, { - async syncOp(_ctx, action) { - if (action === 'pass') { - return action; - } - if (action === 'throwHandlerError') { - throw new nexus.HandlerError('INTERNAL', 'test asked to fail', { retryableOverride: false }); - } - throw new nexus.HandlerError('BAD_REQUEST', 'invalid action'); - }, - asyncOp: new temporalnexus.WorkflowRunOperationHandler(async (ctx, action) => { - if (action === 'throwOperationError') { - throw new nexus.OperationError('failed', 'some message'); - } - if (action === 'throwApplicationFailure') { - throw ApplicationFailure.create({ - nonRetryable: true, - message: 'test asked to fail', - type: 'IntentionalError', - details: ['a detail'], - }); - } - return await temporalnexus.startWorkflow(ctx, handler, { - workflowId: randomUUID(), - args: [action], - }); - }), - }), - ], - }); - await worker.runUntil(async () => { - let res = await executeWorkflow(caller, { - args: [endpoint, 'syncOp', 'pass'], - }); - t.is(res, 'pass'); - let err = await t.throwsAsync( - () => - executeWorkflow(caller, { - args: [endpoint, 'syncOp', 'throwHandlerError'], - }), - { - instanceOf: WorkflowFailedError, - } - ); - t.true( - err instanceof WorkflowFailedError && - err.cause instanceof NexusOperationFailure && - err.cause.cause instanceof nexus.HandlerError && - err.cause.cause.type === 'INTERNAL' - ); - - res = await executeWorkflow(caller, { - args: [endpoint, 'asyncOp', 'pass'], - }); - t.is(res, 'pass'); - err = await t.throwsAsync( - () => - executeWorkflow(caller, { - args: [endpoint, 'asyncOp', 'waitForCancel'], - }), - { - instanceOf: WorkflowFailedError, - } - ); - t.true( - err instanceof WorkflowFailedError && - err.cause instanceof NexusOperationFailure && - err.cause.cause instanceof CancelledFailure - ); - - err = await t.throwsAsync( - () => - executeWorkflow(caller, { - args: [endpoint, 'asyncOp', 'throwOperationError'], - }), - { - instanceOf: WorkflowFailedError, - } - ); - t.true( - err instanceof WorkflowFailedError && - err.cause instanceof NexusOperationFailure && - err.cause.cause instanceof ApplicationFailure - ); - - err = await t.throwsAsync( - () => - executeWorkflow(caller, { - args: [endpoint, 'asyncOp', 'throwApplicationFailure'], - }), - { - instanceOf: WorkflowFailedError, - } - ); - t.true( - err instanceof WorkflowFailedError && - err.cause instanceof NexusOperationFailure && - err.cause.cause instanceof nexus.HandlerError && - err.cause.cause.cause instanceof ApplicationFailure && - err.cause.cause.cause.message === 'test asked to fail' && - err.cause.cause.cause.details?.length === 1 && - err.cause.cause.cause.details[0] === 'a detail' - ); - - err = await t.throwsAsync( - () => - executeWorkflow(caller, { - args: [endpoint, 'asyncOp', 'failWorkflow'], - }), - { - instanceOf: WorkflowFailedError, - } - ); - t.true( - err instanceof WorkflowFailedError && - err.cause instanceof NexusOperationFailure && - err.cause.cause instanceof ApplicationFailure && - err.cause.cause.message === 'test asked to fail' && - err.cause.cause.details?.length === 1 && - err.cause.cause.details[0] === 'a detail' - ); - }); -}); - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -type InputA = { a: string }; -type InputB = { b: string }; - -const clientOperationTypeSafetyCheckerService = nexus.service('test', { - implicit: nexus.operation(), - explicit: nexus.operation({ name: 'my-custom-operation-name' }), -}); - -export async function clientOperationTypeSafetyCheckerWorkflow(endpoint: string): Promise { - const Service = clientOperationTypeSafetyCheckerService; - const operations = Service.operations; - const client = workflow.createNexusClient({ - endpoint, - service: Service, - }); - - // That's quite exhaustive, but we can't generalize these without risking compromising - // the validity of the compiler's type safety checks that we specifically want to test here. - - // Case 1: Provide resolved OperationDefinition object, with correct input type - // There should be no compilation error, and it should execute successfully. - { - const wha = await client.startOperation(operations.implicit, { a: 'a' }); - assert(((await wha.result()) satisfies InputA).a === 'a'); - - const whb = await client.startOperation(operations.explicit, { b: 'b' }); - assert(((await whb.result()) satisfies InputB).b === 'b'); - } - - // Case 2: Provide the operation _property name_, with correct input type - // - // The _property name_ is the name of the property used to specify the operation in the - // `ServiceDefinition` object, which may differ from the value of the `name` property - // if one was explicitly specified on the OperationDefinition object). - // There should be no compilation error, and it should execute successfully. - { - const wha = await client.startOperation('implicit', { a: 'a' }); - assert(((await wha.result()) satisfies InputA).a === 'a'); - - const whb = await client.startOperation('explicit', { b: 'b' }); - assert(((await whb.result()) satisfies InputB).b === 'b'); - } - - // Case 3: Provide resolved OperationDefinition object, with _incorrect_ input type. - // Compiler should complain, and if forced to execute anyway, should result in a HandlerError. - { - // @ts-expect-error - Incompatible input type - const wha = await client.startOperation(operations.implicit, { x: 'x' }); - assert(((await wha.result()) satisfies InputA as any).x === 'x'); - - // @ts-expect-error - Incompatible input type - const whb = await client.startOperation(operations.explicit, { x: 'x' }); - assert(((await whb.result()) satisfies InputB as any).x === 'x'); - } - - // Case 4: Provide the operation _property name_, with _incorrect_ input type. - // Compiler should complain, and if forced to execute anyway, should result in a HandlerError. - { - // @ts-expect-error - Incompatible input type - const wha = await client.startOperation(operations.implicit, { x: 'x' }); - assert(((await wha.result()) satisfies InputA as any).x === 'x'); - - // @ts-expect-error - Incompatible input type - const whb = await client.startOperation(operations.explicit, { x: 'x' }); - assert(((await whb.result()) satisfies InputB as any).x === 'x'); - } - - // Case 5: Non-existent operation name - // Compiler should complain, and if forced to execute anyway, handler will throw a HandlerError. - { - try { - // @ts-expect-error - Incorrect operation name - await client.startOperation('non-existent', { x: 'x' }); - } catch (err) { - assert(err instanceof NexusOperationFailure, `Expected a NexusOperationFailure, got ${err}`); - assert(err.cause instanceof nexus.HandlerError, `Expected casue to be a HandlerError, got ${err.cause}`); - assert(err.cause.type === 'NOT_FOUND', `Expected a NOT_FOUND error, got ${err.cause.type}`); - } - } -} - -test('NexusClient is type-safe in regard to Operation Definitions', async (t) => { - const { createWorker, executeWorkflow, taskQueue } = helpers(t); - const endpoint = t.title.replaceAll(/[\s,]/g, '-') + '-' + randomUUID(); - await t.context.env.connection.operatorService.createNexusEndpoint({ - spec: { - name: endpoint, - target: { - worker: { - namespace: 'default', - taskQueue, - }, - }, - }, - }); - - // We intentionally use different property names here, to assert that the client side sent the - // correct op name to the server (i.e. the operation's name, not the operation property name). - const clientOperationTypeSafetyCheckerService = nexus.service('test', { - implicitImpl: nexus.operation({ name: 'implicit' }), - explicitImpl: nexus.operation({ name: 'my-custom-operation-name' }), - }); - - const worker = await createWorker({ - nexusServices: [ - nexus.serviceHandler(clientOperationTypeSafetyCheckerService, { - implicitImpl: async (_ctx, input) => input, - explicitImpl: async (_ctx, input) => input, - }), - ], - }); - await worker.runUntil(async () => { - await executeWorkflow(clientOperationTypeSafetyCheckerWorkflow, { - args: [endpoint], - }); - }); - - // Most of the checks here would be exposed as compile-time errors, - // and `executeWorkflow` will have thrown if any runtime error. - t.pass(); -}); diff --git a/packages/test/src/test-nyc-coverage.ts b/packages/test/src/test-nyc-coverage.ts deleted file mode 100644 index 27fbb145d..000000000 --- a/packages/test/src/test-nyc-coverage.ts +++ /dev/null @@ -1,97 +0,0 @@ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import * as libCoverage from 'istanbul-lib-coverage'; -import { bundleWorkflowCode, Worker } from '@temporalio/worker'; -import { Client, WorkflowClient } from '@temporalio/client'; -import { WorkflowCoverage } from '@temporalio/nyc-test-coverage'; -import { RUN_INTEGRATION_TESTS } from './helpers'; -import { successString } from './workflows'; - -declare global { - // eslint-disable-next-line no-var - var __coverage__: libCoverage.CoverageMapData; -} - -if (RUN_INTEGRATION_TESTS) { - test('Istanbul injector execute correctly in Worker', async (t) => { - // Make it believe that NYC has been loaded - (global as any).__coverage__ = {}; - - const workflowCoverage = new WorkflowCoverage(); - - const taskQueue = `${t.title}-${uuid4()}`; - const worker = await Worker.create( - workflowCoverage.augmentWorkerOptions({ - taskQueue, - workflowsPath: require.resolve('./workflows'), - }) - ); - const client = new Client(); - await worker.runUntil(client.workflow.execute(successString, { taskQueue, workflowId: uuid4() })); - - workflowCoverage.mergeIntoGlobalCoverage(); - const coverageMap = libCoverage.createCoverageMap(global.__coverage__); - - const successStringFileName = coverageMap.files().find((x) => x.match(/[/\\]success-string\.js/)); - if (successStringFileName) { - t.is(coverageMap.fileCoverageFor(successStringFileName).toSummary().lines.pct, 100); - } else t.fail(); - }); - - test('Istanbul injector execute correctly in Bundler', async (t) => { - const workflowCoverageBundler = new WorkflowCoverage(); - const { code } = await bundleWorkflowCode( - workflowCoverageBundler.augmentBundleOptions({ - workflowsPath: require.resolve('./workflows'), - }) - ); - - // Make it believe that NYC has been loaded - (global as any).__coverage__ = {}; - - const workflowCoverageWorker = new WorkflowCoverage(); - const taskQueue = `${t.title}-${uuid4()}`; - const worker = await Worker.create( - workflowCoverageWorker.augmentWorkerOptionsWithBundle({ - taskQueue, - workflowBundle: { code }, - }) - ); - const client = new Client(); - await worker.runUntil(client.workflow.execute(successString, { taskQueue, workflowId: uuid4() })); - - workflowCoverageBundler.mergeIntoGlobalCoverage(); - workflowCoverageWorker.mergeIntoGlobalCoverage(); - const coverageMap = libCoverage.createCoverageMap(global.__coverage__); - console.log(coverageMap.files()); - - const successStringFileName = coverageMap.files().find((x) => x.match(/[/\\]success-string\.js/)); - if (successStringFileName) { - t.is(coverageMap.fileCoverageFor(successStringFileName).toSummary().lines.pct, 100); - } else t.fail(); - }); - - test('Istanbul injector exclude non-user code', async (t) => { - // Make it believe that NYC has been loaded - (global as any).__coverage__ = {}; - - const workflowCoverage = new WorkflowCoverage(); - - const taskQueue = `${t.title}-${uuid4()}`; - const worker = await Worker.create( - workflowCoverage.augmentWorkerOptions({ - taskQueue, - workflowsPath: require.resolve('./workflows'), - }) - ); - const client = new WorkflowClient(); - await worker.runUntil(client.execute(successString, { taskQueue, workflowId: uuid4() })); - - workflowCoverage.mergeIntoGlobalCoverage(); - const coverageMap = libCoverage.createCoverageMap(global.__coverage__); - - // Only user code should be included in coverage - t.is(coverageMap.files().filter((x) => x.match(/[/\\]worker-interface.js/)).length, 0); - t.is(coverageMap.files().filter((x) => x.match(/[/\\]ms[/\\]/)).length, 0); - }); -} diff --git a/packages/test/src/test-otel.ts b/packages/test/src/test-otel.ts deleted file mode 100644 index a45a8951a..000000000 --- a/packages/test/src/test-otel.ts +++ /dev/null @@ -1,512 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -/** - * Manual tests to inspect tracing output - */ -import * as http from 'http'; -import * as http2 from 'http2'; -import { SpanStatusCode } from '@opentelemetry/api'; -import { ExportResultCode } from '@opentelemetry/core'; -import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; -import * as opentelemetry from '@opentelemetry/sdk-node'; -import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { WorkflowClient } from '@temporalio/client'; -import { OpenTelemetryWorkflowClientInterceptor } from '@temporalio/interceptors-opentelemetry/lib/client'; -import { OpenTelemetryWorkflowClientCallsInterceptor } from '@temporalio/interceptors-opentelemetry'; -import { instrument } from '@temporalio/interceptors-opentelemetry/lib/instrumentation'; -import { - makeWorkflowExporter, - OpenTelemetryActivityInboundInterceptor, - OpenTelemetryActivityOutboundInterceptor, -} from '@temporalio/interceptors-opentelemetry/lib/worker'; -import { OpenTelemetrySinks, SpanName, SPAN_DELIMITER } from '@temporalio/interceptors-opentelemetry/lib/workflow'; -import { DefaultLogger, InjectedSinks, Runtime } from '@temporalio/worker'; -import * as activities from './activities'; -import { RUN_INTEGRATION_TESTS, TestWorkflowEnvironment, Worker } from './helpers'; -import * as workflows from './workflows'; - -async function withFakeGrpcServer( - fn: (port: number) => Promise, - requestListener?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void -): Promise { - return new Promise((resolve, reject) => { - const srv = http2.createServer(); - srv.listen({ port: 0, host: '127.0.0.1' }, () => { - const addr = srv.address(); - if (typeof addr === 'string' || addr === null) { - throw new Error('Unexpected server address type'); - } - srv.on('request', async (req, res) => { - if (requestListener) await requestListener(req, res); - res.statusCode = 200; - res.addTrailers({ - 'grpc-status': '0', - 'grpc-message': 'OK', - }); - res.write( - // This is a raw gRPC response, of length 0 - Buffer.from([ - // Frame Type: Data; Not Compressed - 0, - // Message Length: 0 - 0, 0, 0, 0, - ]) - ); - res.end(); - }); - fn(addr.port) - .catch((e) => reject(e)) - .finally(() => { - resolve(); - - // The OTel exporter will try to flush metrics on drop, which may result in tons of ERROR - // messages on the console if the server has had time to complete shutdown before then. - // Delaying closing the server by 1 second is enough to avoid that situation, and doesn't - // need to be awaited, no that doesn't slow down tests. - setTimeout(() => { - srv.close(); - }, 1000).unref(); - }); - }); - }); -} - -async function withHttpServer( - fn: (port: number) => Promise, - requestListener?: (request: http.IncomingMessage) => void -): Promise { - return new Promise((resolve, reject) => { - const srv = http.createServer(); - srv.listen({ port: 0, host: '127.0.0.1' }, () => { - const addr = srv.address(); - if (typeof addr === 'string' || addr === null) { - throw new Error('Unexpected server address type'); - } - srv.on('request', async (req, res) => { - if (requestListener) await requestListener(req); - res.statusCode = 200; - res.end(); - }); - fn(addr.port) - .catch((e) => reject(e)) - .finally(() => { - resolve(); - - // The OTel exporter will try to flush metrics on drop, which may result in tons of ERROR - // messages on the console if the server has had time to complete shutdown before then. - // Delaying closing the server by 1 second is enough to avoid that situation, and doesn't - // need to be awaited, no that doesn't slow down tests. - setTimeout(() => { - srv.close(); - }, 1000).unref(); - }); - }); - }); -} - -test.serial('Runtime.install() throws meaningful error when passed invalid metrics.otel.url', async (t) => { - t.throws(() => Runtime.install({ telemetryOptions: { metrics: { otel: { url: ':invalid' } } } }), { - instanceOf: TypeError, - message: /metricsExporter.otel.url/, - }); -}); - -test.serial('Runtime.install() accepts metrics.otel.url without headers', async (t) => { - try { - Runtime.install({ telemetryOptions: { metrics: { otel: { url: 'http://127.0.0.1:1234' } } } }); - t.pass(); - } finally { - // Cleanup the runtime so that it doesn't interfere with other tests - await Runtime._instance?.shutdown(); - } -}); - -test.serial('Exporting OTEL metrics from Core works', async (t) => { - let resolveCapturedRequest = (_req: http2.Http2ServerRequest) => undefined as void; - const capturedRequest = new Promise((r) => (resolveCapturedRequest = r)); - try { - await withFakeGrpcServer(async (port: number) => { - Runtime.install({ - telemetryOptions: { - metrics: { - otel: { - url: `http://127.0.0.1:${port}`, - headers: { - 'x-test-header': 'test-value', - }, - metricsExportInterval: 10, - }, - }, - }, - }); - - const localEnv = await TestWorkflowEnvironment.createLocal(); - try { - const worker = await Worker.create({ - connection: localEnv.nativeConnection, - workflowsPath: require.resolve('./workflows'), - taskQueue: 'test-otel', - }); - const client = new WorkflowClient({ - connection: localEnv.connection, - }); - await worker.runUntil(async () => { - await client.execute(workflows.successString, { - taskQueue: 'test-otel', - workflowId: uuid4(), - }); - const req = await Promise.race([ - capturedRequest, - await new Promise((resolve) => setTimeout(() => resolve(undefined), 2000)), - ]); - t.truthy(req); - t.is(req?.url, '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export'); - t.is(req?.headers['x-test-header'], 'test-value'); - }); - } finally { - await localEnv.teardown(); - } - }, resolveCapturedRequest); - } finally { - // Cleanup the runtime so that it doesn't interfere with other tests - await Runtime._instance?.shutdown(); - } -}); - -test.serial('Exporting OTEL metrics using OTLP/HTTP from Core works', async (t) => { - let resolveCapturedRequest = (_req: http.IncomingMessage) => undefined as void; - const capturedRequest = new Promise((r) => (resolveCapturedRequest = r)); - try { - await withHttpServer(async (port: number) => { - Runtime.install({ - telemetryOptions: { - metrics: { - otel: { - url: `http://127.0.0.1:${port}/v1/metrics`, - http: true, - headers: { - 'x-test-header': 'test-value', - }, - metricsExportInterval: 10, - }, - }, - }, - }); - - const localEnv = await TestWorkflowEnvironment.createLocal(); - try { - const worker = await Worker.create({ - connection: localEnv.nativeConnection, - workflowsPath: require.resolve('./workflows'), - taskQueue: 'test-otel', - }); - const client = new WorkflowClient({ - connection: localEnv.connection, - }); - await worker.runUntil(async () => { - await client.execute(workflows.successString, { - taskQueue: 'test-otel', - workflowId: uuid4(), - }); - const req = await Promise.race([ - capturedRequest, - await new Promise((resolve) => setTimeout(() => resolve(undefined), 2000)), - ]); - t.truthy(req); - t.is(req?.url, '/v1/metrics'); - t.is(req?.headers['x-test-header'], 'test-value'); - }); - } finally { - await localEnv.teardown(); - } - }, resolveCapturedRequest); - } finally { - // Cleanup the runtime so that it doesn't interfere with other tests - await Runtime._instance?.shutdown(); - } -}); - -if (RUN_INTEGRATION_TESTS) { - test.serial('Otel interceptor spans are connected and complete', async (t) => { - Runtime.install({}); - try { - const spans = Array(); - - const staticResource = new opentelemetry.resources.Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', - }); - const traceExporter: opentelemetry.tracing.SpanExporter = { - export(spans_, resultCallback) { - spans.push(...spans_); - resultCallback({ code: ExportResultCode.SUCCESS }); - }, - async shutdown() { - // Nothing to shutdown - }, - }; - const otel = new opentelemetry.NodeSDK({ - resource: staticResource, - traceExporter, - }); - await otel.start(); - - const sinks: InjectedSinks = { - exporter: makeWorkflowExporter(traceExporter, staticResource), - }; - - const worker = await Worker.create({ - workflowsPath: require.resolve('./workflows'), - activities, - taskQueue: 'test-otel', - interceptors: { - client: { - workflow: [new OpenTelemetryWorkflowClientCallsInterceptor()], - }, - workflowModules: [require.resolve('./workflows/otel-interceptors')], - activity: [ - (ctx) => ({ - inbound: new OpenTelemetryActivityInboundInterceptor(ctx), - outbound: new OpenTelemetryActivityOutboundInterceptor(ctx), - }), - ], - }, - sinks, - }); - - const client = new WorkflowClient({ - interceptors: [new OpenTelemetryWorkflowClientInterceptor()], - }); - await worker.runUntil(client.execute(workflows.smorgasbord, { taskQueue: 'test-otel', workflowId: uuid4() })); - await otel.shutdown(); - const originalSpan = spans.find(({ name }) => name === `${SpanName.WORKFLOW_START}${SPAN_DELIMITER}smorgasbord`); - t.true(originalSpan !== undefined); - t.log( - spans.map((span) => ({ name: span.name, parentSpanId: span.parentSpanId, spanId: span.spanContext().spanId })) - ); - - const firstExecuteSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}smorgasbord` && - parentSpanId === originalSpan?.spanContext().spanId - ); - t.true(firstExecuteSpan !== undefined); - t.true(firstExecuteSpan!.status.code === SpanStatusCode.OK); - - const continueAsNewSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.CONTINUE_AS_NEW}${SPAN_DELIMITER}smorgasbord` && - parentSpanId === firstExecuteSpan?.spanContext().spanId - ); - t.true(continueAsNewSpan !== undefined); - t.true(continueAsNewSpan!.status.code === SpanStatusCode.OK); - - const parentExecuteSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}smorgasbord` && - parentSpanId === continueAsNewSpan?.spanContext().spanId - ); - t.true(parentExecuteSpan !== undefined); - const firstActivityStartSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}fakeProgress` && - parentSpanId === parentExecuteSpan?.spanContext().spanId - ); - t.true(firstActivityStartSpan !== undefined); - - const firstActivityExecuteSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}fakeProgress` && - parentSpanId === firstActivityStartSpan?.spanContext().spanId - ); - t.true(firstActivityExecuteSpan !== undefined); - - const secondActivityStartSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}queryOwnWf` && - parentSpanId === parentExecuteSpan?.spanContext().spanId - ); - t.true(secondActivityStartSpan !== undefined); - - const secondActivityExecuteSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}queryOwnWf` && - parentSpanId === secondActivityStartSpan?.spanContext().spanId - ); - t.true(secondActivityExecuteSpan !== undefined); - - const childWorkflowStartSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.CHILD_WORKFLOW_START}${SPAN_DELIMITER}signalTarget` && - parentSpanId === parentExecuteSpan?.spanContext().spanId - ); - t.true(childWorkflowStartSpan !== undefined); - - const childWorkflowExecuteSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}signalTarget` && - parentSpanId === childWorkflowStartSpan?.spanContext().spanId - ); - t.true(childWorkflowExecuteSpan !== undefined); - - const signalChildWithUnblockSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}unblock` && - parentSpanId === parentExecuteSpan?.spanContext().spanId - ); - t.true(signalChildWithUnblockSpan !== undefined); - - const localActivityStartSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}echo` && - parentSpanId === parentExecuteSpan?.spanContext().spanId - ); - t.true(localActivityStartSpan !== undefined); - - const localActivityExecuteSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}echo` && - parentSpanId === localActivityStartSpan?.spanContext().spanId - ); - t.true(localActivityExecuteSpan !== undefined); - - const activityStartedSignalSpan = spans.find( - ({ name, parentSpanId }) => - name === `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}activityStarted` && - parentSpanId === firstActivityExecuteSpan?.spanContext().spanId - ); - t.true(activityStartedSignalSpan !== undefined); - - t.deepEqual(new Set(spans.map((span) => span.spanContext().traceId)).size, 1); - } finally { - // Cleanup the runtime so that it doesn't interfere with other tests - await Runtime._instance?.shutdown(); - } - }); - - // FIXME: This tests take ~9 seconds to complete on my local machine, even - // more in CI, and yet, it doesn't really do any assertion by itself. - // To be revisited at a later time. - test.skip('Otel spans connected', async (t) => { - const logger = new DefaultLogger('DEBUG'); - Runtime.install({ - logger, - }); - try { - const oTelUrl = 'http://127.0.0.1:4317'; - const exporter = new OTLPTraceExporter({ url: oTelUrl }); - const staticResource = new opentelemetry.resources.Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: 'ts-test-otel-worker', - }); - const otel = new opentelemetry.NodeSDK({ - resource: staticResource, - traceExporter: exporter, - }); - await otel.start(); - - const sinks: InjectedSinks = { - exporter: makeWorkflowExporter(exporter, staticResource), - }; - const worker = await Worker.create({ - workflowsPath: require.resolve('./workflows'), - activities, - enableSDKTracing: true, - taskQueue: 'test-otel', - interceptors: { - workflowModules: [require.resolve('./workflows/otel-interceptors')], - activity: [(ctx) => ({ inbound: new OpenTelemetryActivityInboundInterceptor(ctx) })], - }, - sinks, - }); - - const client = new WorkflowClient({ - interceptors: [new OpenTelemetryWorkflowClientInterceptor()], - }); - await worker.runUntil(client.execute(workflows.smorgasbord, { taskQueue: 'test-otel', workflowId: uuid4() })); - // Allow some time to ensure spans are flushed out to collector - await new Promise((resolve) => setTimeout(resolve, 5000)); - t.pass(); - } finally { - // Cleanup the runtime so that it doesn't interfere with other tests - await Runtime._instance?.shutdown(); - } - }); - - test('Otel workflow module does not patch node window object', (t) => { - // Importing the otel workflow modules above should patch globalThis - t.falsy((globalThis as any).window); - }); - - test('instrumentation: Error status includes message and records exception', async (t) => { - const memoryExporter = new InMemorySpanExporter(); - const provider = new BasicTracerProvider(); - provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); - provider.register(); - const tracer = provider.getTracer('test-error-tracer'); - - const errorMessage = 'Test error message'; - - await t.throwsAsync( - instrument({ - tracer, - spanName: 'test-error-span', - fn: async () => { - throw new Error(errorMessage); - }, - }), - { message: errorMessage } - ); - - const spans = memoryExporter.getFinishedSpans(); - t.is(spans.length, 1); - - const span = spans[0]; - - t.is(span.status.code, SpanStatusCode.ERROR); - - t.is(span.status.message, errorMessage); - - const exceptionEvents = span.events.filter((event) => event.name === 'exception'); - t.is(exceptionEvents.length, 1); - }); - - test('Otel workflow omits ApplicationError with BENIGN category', async (t) => { - const memoryExporter = new InMemorySpanExporter(); - const provider = new BasicTracerProvider(); - provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); - provider.register(); - const tracer = provider.getTracer('test-error-tracer'); - - const worker = await Worker.create({ - workflowsPath: require.resolve('./workflows'), - activities, - taskQueue: 'test-otel-benign-err', - interceptors: { - activity: [ - (ctx) => { - return { inbound: new OpenTelemetryActivityInboundInterceptor(ctx, { tracer }) }; - }, - ], - }, - }); - - const client = new WorkflowClient(); - - await worker.runUntil( - client.execute(workflows.throwMaybeBenignErr, { - taskQueue: 'test-otel-benign-err', - workflowId: uuid4(), - retry: { maximumAttempts: 3 }, - }) - ); - - const spans = memoryExporter.getFinishedSpans(); - t.is(spans.length, 3); - t.is(spans[0].status.code, SpanStatusCode.ERROR); - t.is(spans[0].status.message, 'not benign'); - t.is(spans[1].status.code, SpanStatusCode.UNSET); - t.is(spans[1].status.message, 'benign'); - t.is(spans[2].status.code, SpanStatusCode.OK); - }); -} diff --git a/packages/test/src/test-parse-uri.ts b/packages/test/src/test-parse-uri.ts deleted file mode 100644 index 56d7da861..000000000 --- a/packages/test/src/test-parse-uri.ts +++ /dev/null @@ -1,30 +0,0 @@ -import test from 'ava'; -import { splitProtoHostPort, normalizeGrpcEndpointAddress } from '@temporalio/common/lib/internal-non-workflow'; - -test('splitProtoHostPort', (t) => { - t.deepEqual(splitProtoHostPort('127.0.0.1'), { scheme: undefined, hostname: '127.0.0.1', port: undefined }); - t.deepEqual(splitProtoHostPort('[::1]'), { scheme: undefined, hostname: '::1', port: undefined }); - t.deepEqual(splitProtoHostPort('myserver.com'), { scheme: undefined, hostname: 'myserver.com', port: undefined }); - - t.deepEqual(splitProtoHostPort('127.0.0.1:7890'), { scheme: undefined, hostname: '127.0.0.1', port: 7890 }); - t.deepEqual(splitProtoHostPort('[::1]:7890'), { scheme: undefined, hostname: '::1', port: 7890 }); - t.deepEqual(splitProtoHostPort('myserver.com:7890'), { scheme: undefined, hostname: 'myserver.com', port: 7890 }); - - t.deepEqual(splitProtoHostPort('http://127.0.0.1:8080'), { scheme: 'http', hostname: '127.0.0.1', port: 8080 }); - t.deepEqual(splitProtoHostPort('http://[::1]:1234'), { scheme: 'http', hostname: '::1', port: 1234 }); - t.deepEqual(splitProtoHostPort('http://myserver.com:5678'), { scheme: 'http', hostname: 'myserver.com', port: 5678 }); -}); - -test('normalizeTemporalGrpcEndpointAddress', (t) => { - const normalize = (s: string) => normalizeGrpcEndpointAddress(s, 7233); - - t.is(normalize('127.0.0.1'), '127.0.0.1:7233'); - t.is(normalize('127.0.0.1:7890'), '127.0.0.1:7890'); - t.is(normalize('[::1]'), '[::1]:7233'); - t.is(normalize('[::1]:7890'), '[::1]:7890'); - t.is(normalize('myserver.com'), 'myserver.com:7233'); - t.is(normalize('myserver.com:7890'), 'myserver.com:7890'); - - t.throws(() => normalize('http://127.0.0.1'), { message: /Invalid/ }); - t.throws(() => normalize('::1'), { message: /Invalid/ }); -}); diff --git a/packages/test/src/test-patch-and-condition.ts b/packages/test/src/test-patch-and-condition.ts deleted file mode 100644 index 246a2342f..000000000 --- a/packages/test/src/test-patch-and-condition.ts +++ /dev/null @@ -1,60 +0,0 @@ -import crypto from 'node:crypto'; -import test from 'ava'; -import { WorkflowClient } from '@temporalio/client'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import * as workflows from './workflows/patch-and-condition-pre-patch'; - -if (RUN_INTEGRATION_TESTS) { - test('Patch in condition does not cause non-determinism error on replay', async (t) => { - const client = new WorkflowClient(); - const workflowId = crypto.randomUUID(); - - // Create the first worker with pre-patched version of the workflow - const worker1 = await Worker.create({ - taskQueue: 'patch-in-condition', - workflowsPath: require.resolve('./workflows/patch-and-condition-pre-patch'), - // Avoid waiting for sticky execution timeout on each worker transition - maxCachedWorkflows: 0, - }); - - // Start the workflow and wait for the first task to be processed - const handle = await worker1.runUntil(async () => { - const handle = await client.start(workflows.patchInCondition, { - taskQueue: 'patch-in-condition', - workflowId, - }); - await handle.query('__stack_trace'); - return handle; - }); - - // Create the second worker with post-patched version of the workflow - const worker2 = await Worker.create({ - taskQueue: 'patch-in-condition', - workflowsPath: require.resolve('./workflows/patch-and-condition-post-patch'), - maxCachedWorkflows: 0, - }); - - // Trigger a signal and wait for it to be processed - await worker2.runUntil(async () => { - await handle.signal(workflows.generateCommandSignal); - await handle.query('__stack_trace'); - }); - - // Create the third worker that is identical to the second one - const worker3 = await Worker.create({ - taskQueue: 'patch-in-condition', - workflowsPath: require.resolve('./workflows/patch-and-condition-post-patch'), - maxCachedWorkflows: 0, - }); - - // Trigger a workflow task that will cause replay. - await worker3.runUntil(async () => { - await handle.signal(workflows.generateCommandSignal); - await handle.result(); - }); - - // If the workflow completes, commands are generated in the right order and it is safe to use a patched statement - // inside a condition. - t.pass(); - }); -} diff --git a/packages/test/src/test-patch-root.ts b/packages/test/src/test-patch-root.ts deleted file mode 100644 index 94d66de99..000000000 --- a/packages/test/src/test-patch-root.ts +++ /dev/null @@ -1,37 +0,0 @@ -import test from 'ava'; -import { patchProtobufRoot } from '@temporalio/common/lib/protobufs'; - -test('patchRoot', (t) => { - const type = new Type(); - t.deepEqual((patchProtobufRoot({ nested: { type } }) as any).type, type); - - const bar = new Namespace({ BarMsg: new Type() }); - const root = { - nested: { - foo: new Namespace({ - Msg: new Type(), - nested: { - bar, - }, - }), - }, - }; - t.like(patchProtobufRoot(root), { - ...root, - foo: { Msg: new Type(), bar: { BarMsg: new Type() }, nested: { bar } }, - } as any); -}); - -class Namespace { - public static className = 'Namespace'; - - constructor(props: Record) { - for (const key in props) { - (this as any)[key] = props[key]; - } - } -} - -class Type { - public static className = 'Type'; -} diff --git a/packages/test/src/test-payload-converter.ts b/packages/test/src/test-payload-converter.ts deleted file mode 100644 index 495b5f542..000000000 --- a/packages/test/src/test-payload-converter.ts +++ /dev/null @@ -1,325 +0,0 @@ -/* eslint @typescript-eslint/no-non-null-assertion: 0 */ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { WorkflowClient } from '@temporalio/client'; -import { - BinaryPayloadConverter, - defaultPayloadConverter, - encodingKeys, - JsonPayloadConverter, - METADATA_ENCODING_KEY, - METADATA_MESSAGE_TYPE_KEY, - PayloadConverterError, - UndefinedPayloadConverter, - ValueError, -} from '@temporalio/common'; -import { SearchAttributePayloadConverter } from '@temporalio/common/lib/converter/payload-search-attributes'; -import { encode } from '@temporalio/common/lib/encoding'; -import { - DefaultPayloadConverterWithProtobufs, - ProtobufBinaryPayloadConverter, - ProtobufJsonPayloadConverter, -} from '@temporalio/common/lib/protobufs'; -import { DefaultLogger, Runtime } from '@temporalio/worker'; -import root from '../protos/root'; // eslint-disable-line import/default -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { defaultOptions } from './mock-native-worker'; -import { messageInstance } from './payload-converters/proto-payload-converter'; -import { protobufWorkflow } from './workflows/protobufs'; -import { echoBinaryProtobuf } from './workflows/echo-binary-protobuf'; - -test('UndefinedPayloadConverter converts from undefined only', (t) => { - const converter = new UndefinedPayloadConverter(); - t.is(converter.toPayload(null), undefined); - t.is(converter.toPayload({}), undefined); - t.is(converter.toPayload(1), undefined); - t.is(converter.toPayload(0), undefined); - t.is(converter.toPayload('abc'), undefined); - - t.deepEqual(converter.toPayload(undefined), { - metadata: { [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_NULL }, - }); -}); - -test('UndefinedPayloadConverter converts to undefined', (t) => { - const converter = new UndefinedPayloadConverter(); - t.is(converter.fromPayload(converter.toPayload(undefined)!), undefined); -}); - -test('BinaryPayloadConverter converts from Uint8Array', (t) => { - const converter = new BinaryPayloadConverter(); - t.is(converter.toPayload(null), undefined); - t.is(converter.toPayload({}), undefined); - t.is(converter.toPayload(1), undefined); - t.is(converter.toPayload(0), undefined); - t.is(converter.toPayload('abc'), undefined); - - t.deepEqual(converter.toPayload(encode('abc')), { - metadata: { [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_RAW }, - data: encode('abc'), - }); -}); - -test('BinaryPayloadConverter converts to Uint8Array', (t) => { - const converter = new BinaryPayloadConverter(); - t.deepEqual(converter.fromPayload(converter.toPayload(encode('abc'))!), encode('abc')); -}); - -test('JsonPayloadConverter converts from non undefined', (t) => { - const payload = (val: any) => ({ - metadata: { [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_JSON }, - data: encode(JSON.stringify(val)), - }); - const converter = new JsonPayloadConverter(); - t.deepEqual(converter.toPayload(null), payload(null)); - t.deepEqual(converter.toPayload({ a: 1 }), payload({ a: 1 })); - t.deepEqual(converter.toPayload(1), payload(1)); - t.deepEqual(converter.toPayload(0), payload(0)); - t.deepEqual(converter.toPayload('abc'), payload('abc')); - - t.is(converter.toPayload(undefined), undefined); - t.is(converter.toPayload(0n), undefined); -}); - -test('JsonPayloadConverter converts to object', (t) => { - const converter = new JsonPayloadConverter(); - t.deepEqual(converter.fromPayload(converter.toPayload({ a: 1 })!), { a: 1 }); -}); - -test('ProtobufBinaryPayloadConverter converts from an instance', (t) => { - const instance = root.ProtoActivityInput.create({ name: 'Proto', age: 1 }); - const converter = new ProtobufBinaryPayloadConverter(root); - t.deepEqual(converter.toPayload(instance), { - metadata: { - [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_PROTOBUF, - [METADATA_MESSAGE_TYPE_KEY]: encode('ProtoActivityInput'), - }, - data: root.ProtoActivityInput.encode(instance).finish(), - }); -}); - -test('ProtobufBinaryPayloadConverter converts to an instance', (t) => { - const instance = root.ProtoActivityInput.create({ name: 'Proto', age: 1 }); - const converter = new ProtobufBinaryPayloadConverter(root); - const testInstance = converter.fromPayload(converter.toPayload(instance)!); - // tests that both are instances of the same class with the same properties - t.deepEqual(testInstance, instance); -}); - -test('ProtobufBinaryPayloadConverter throws detailed errors', (t) => { - const instance = root.ProtoActivityInput.create({ name: 'Proto', age: 1 }); - const converter = new ProtobufBinaryPayloadConverter(root); - - t.throws( - () => - converter.fromPayload({ - metadata: { [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_PROTOBUF }, - data: root.ProtoActivityInput.encode(instance).finish(), - }), - { instanceOf: ValueError, message: 'Got protobuf payload without metadata.messageType' } - ); - t.throws( - () => - converter.fromPayload({ - metadata: { - [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_PROTOBUF, - [METADATA_MESSAGE_TYPE_KEY]: encode('NonExistentMessageClass'), - }, - data: root.ProtoActivityInput.encode(instance).finish(), - }), - { - instanceOf: PayloadConverterError, - message: 'Got a `NonExistentMessageClass` protobuf message but cannot find corresponding message class in `root`', - } - ); -}); - -test('ProtobufJSONPayloadConverter converts from an instance to JSON', (t) => { - const instance = root.foo.bar.ProtoActivityInput.create({ name: 'Proto', age: 1 }); - const converter = new ProtobufJsonPayloadConverter(root); - t.deepEqual(converter.toPayload(instance), { - metadata: { - [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_PROTOBUF_JSON, - [METADATA_MESSAGE_TYPE_KEY]: encode('foo.bar.ProtoActivityInput'), - }, - data: encode(JSON.stringify(instance)), - }); -}); - -test('ProtobufJSONPayloadConverter converts to an instance from JSON', (t) => { - const instance = root.foo.bar.ProtoActivityInput.create({ name: 'Proto', age: 1 }); - const converter = new ProtobufJsonPayloadConverter(root); - const testInstance = converter.fromPayload(converter.toPayload(instance)!); - // tests that both are instances of the same class with the same properties - t.deepEqual(testInstance, instance); -}); - -test('ProtobufJSONPayloadConverter converts binary', (t) => { - // binary should be base64-encoded: - // https://developers.google.com/protocol-buffers/docs/proto3#json - const instance = root.BinaryMessage.create({ data: encode('abc') }); - const converter = new ProtobufJsonPayloadConverter(root); - const encoded = converter.toPayload(instance); - t.deepEqual(encoded, { - metadata: { - [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_PROTOBUF_JSON, - [METADATA_MESSAGE_TYPE_KEY]: encode('BinaryMessage'), - }, - data: encode(JSON.stringify({ data: Buffer.from('abc').toString('base64') })), - }); - - const testInstance = converter.fromPayload(encoded!); - t.deepEqual(testInstance.data, instance.data); -}); - -test(`SearchAttributePayloadConverter doesn't fail if Array.prototype contains enumerable properties`, (t) => { - try { - const converter = new SearchAttributePayloadConverter(); - Object.defineProperty(Array.prototype, 'foo', { - configurable: true, - value: 123, - enumerable: true, - }); - converter.toPayload(['test']); - t.pass(); - } finally { - delete (Array.prototype as any).foo; - } -}); - -if (RUN_INTEGRATION_TESTS) { - test('Worker throws decoding proto JSON without WorkerOptions.dataConverter', async (t) => { - let markErrorThrown: () => void; - const expectedErrorWasThrown = new Promise((resolve) => { - markErrorThrown = resolve; - }); - - Runtime.install({ - logger: new DefaultLogger('WARN', (entry) => { - if ( - entry.message.includes('Failing workflow task') && - entry.meta?.failure?.includes('Unknown encoding: json/protobuf') - ) { - markErrorThrown(); - } - }), - telemetryOptions: { logging: { forward: {}, filter: 'WARN' } }, - }); - - const taskQueue = `${__filename}/${t.title}`; - const worker = await Worker.create({ - ...defaultOptions, - workflowsPath: require.resolve('./workflows/protobufs'), - taskQueue, - }); - const client = new WorkflowClient({ - dataConverter: { payloadConverterPath: require.resolve('./payload-converters/proto-payload-converter') }, - }); - - const handle = await client.start(protobufWorkflow, { - args: [messageInstance], - workflowId: uuid4(), - taskQueue, - }); - - await worker.runUntil(async () => { - try { - await expectedErrorWasThrown; - } finally { - await handle.terminate(); - } - }); - t.pass(); - }); - - test('Worker encodes/decodes a protobuf containing a binary array', async (t) => { - const binaryInstance = root.BinaryMessage.create({ data: encode('abc') }); - const dataConverter = { payloadConverterPath: require.resolve('./payload-converters/proto-payload-converter') }; - const taskQueue = `${__filename}/${t.title}`; - - const worker = await Worker.create({ - ...defaultOptions, - workflowsPath: require.resolve('./workflows/echo-binary-protobuf'), - taskQueue, - dataConverter, - }); - - const client = new WorkflowClient({ dataConverter }); - - await worker.runUntil(async () => { - const result = await client.execute(echoBinaryProtobuf, { - args: [binaryInstance], - workflowId: uuid4(), - taskQueue, - }); - t.deepEqual(result, binaryInstance); - }); - }); -} - -test('DefaultPayloadConverterWithProtobufs converts protobufs', (t) => { - const instance = root.ProtoActivityInput.create({ name: 'Proto', age: 1 }); - const defaultPayloadConverterWithProtos = new DefaultPayloadConverterWithProtobufs({ protobufRoot: root }); - t.deepEqual( - defaultPayloadConverterWithProtos.toPayload(instance), - // It will always use JSON because it appears before binary in the list - new ProtobufJsonPayloadConverter(root).toPayload(instance) - ); -}); - -test('DefaultPayloadConverterWithProtobufs converts to payload by trying each converter in order', (t) => { - const defaultPayloadConverterWithProtos = new DefaultPayloadConverterWithProtobufs({ protobufRoot: root }); - const instance = root.ProtoActivityInput.create({ name: 'Proto', age: 1 }); - t.deepEqual( - defaultPayloadConverterWithProtos.toPayload(instance), - new ProtobufJsonPayloadConverter().toPayload(instance) - ); - - t.deepEqual(defaultPayloadConverterWithProtos.toPayload('abc'), new JsonPayloadConverter().toPayload('abc')); - t.deepEqual( - defaultPayloadConverterWithProtos.toPayload(undefined), - new UndefinedPayloadConverter().toPayload(undefined) - ); - t.deepEqual( - defaultPayloadConverterWithProtos.toPayload(encode('abc')), - new BinaryPayloadConverter().toPayload(encode('abc')) - ); - t.throws(() => defaultPayloadConverterWithProtos.toPayload(0n), { instanceOf: ValueError }); -}); - -test('defaultPayloadConverter converts from payload by payload type', (t) => { - t.deepEqual(defaultPayloadConverter.fromPayload(new JsonPayloadConverter().toPayload('abc')!), 'abc'); - t.deepEqual(defaultPayloadConverter.fromPayload(new UndefinedPayloadConverter().toPayload(undefined)!), undefined); - t.deepEqual( - defaultPayloadConverter.fromPayload(new BinaryPayloadConverter().toPayload(encode('abc'))!), - encode('abc') - ); - t.throws( - () => defaultPayloadConverter.fromPayload({ metadata: { [METADATA_ENCODING_KEY]: encode('not-supported') } }), - { - instanceOf: ValueError, - message: 'Unknown encoding: not-supported', - } - ); - t.throws( - () => - defaultPayloadConverter.fromPayload({ - metadata: { [METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_JSON }, - }), - { instanceOf: ValueError, message: 'Got payload with no data' } - ); - - const instance = root.ProtoActivityInput.create({ name: 'Proto', age: 1 }); - const protoError = { - instanceOf: ValueError, - message: /Unknown encoding: .*protobuf/, - }; - t.throws( - () => defaultPayloadConverter.fromPayload(new ProtobufBinaryPayloadConverter(root).toPayload(instance)!), - protoError - ); - t.throws( - () => defaultPayloadConverter.fromPayload(new ProtobufJsonPayloadConverter(root).toPayload(instance)!), - protoError - ); -}); diff --git a/packages/test/src/test-prometheus.ts b/packages/test/src/test-prometheus.ts deleted file mode 100644 index 264547402..000000000 --- a/packages/test/src/test-prometheus.ts +++ /dev/null @@ -1,125 +0,0 @@ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import fetch from 'node-fetch'; -import { WorkflowClient } from '@temporalio/client'; -import { Runtime } from '@temporalio/worker'; -import { Worker, getRandomPort, TestWorkflowEnvironment } from './helpers'; -import * as workflows from './workflows'; - -test.serial('Runtime.install() throws meaningful error when passed invalid metrics.prometheus.bindAddress', (t) => { - t.throws(() => Runtime.install({ telemetryOptions: { metrics: { prometheus: { bindAddress: ':invalid' } } } }), { - instanceOf: TypeError, - message: /metricsExporter.prometheus.socketAddr/, - }); -}); - -test.serial( - 'Runtime.install() throws meaningful error when metrics.prometheus.bindAddress port is already taken', - async (t) => { - await getRandomPort(async (port: number) => { - t.throws( - () => Runtime.install({ telemetryOptions: { metrics: { prometheus: { bindAddress: `127.0.0.1:${port}` } } } }), - { - instanceOf: Error, - message: /(Address already in use|socket address)/, - } - ); - }); - } -); - -test.serial('Exporting Prometheus metrics from Core works', async (t) => { - const port = await getRandomPort(); - Runtime.install({ - telemetryOptions: { - metrics: { - metricPrefix: 'myprefix_', - prometheus: { - bindAddress: `127.0.0.1:${port}`, - }, - }, - }, - }); - const localEnv = await TestWorkflowEnvironment.createLocal(); - try { - const worker = await Worker.create({ - connection: localEnv.nativeConnection, - workflowsPath: require.resolve('./workflows'), - taskQueue: 'test-prometheus', - }); - const client = new WorkflowClient({ - connection: localEnv.connection, - }); - await worker.runUntil(async () => { - await client.execute(workflows.successString, { - taskQueue: 'test-prometheus', - workflowId: uuid4(), - }); - const resp = await fetch(`http://127.0.0.1:${port}/metrics`); - // We're not concerned about exact details here, just that the metrics are present - const text = await resp.text(); - t.assert(text.includes('myprefix_worker_task_slots_available')); - }); - } finally { - await localEnv.teardown(); - } -}); - -test.serial('Exporting Prometheus metrics from Core works with lots of options', async (t) => { - const port = await getRandomPort(); - Runtime.install({ - telemetryOptions: { - metrics: { - globalTags: { - my_tag: 'my_value', - }, - attachServiceName: true, - prometheus: { - bindAddress: `127.0.0.1:${port}`, - countersTotalSuffix: true, - unitSuffix: true, - useSecondsForDurations: true, - histogramBucketOverrides: { - request_latency: [3, 31, 314, 3141, 31415], - workflow_task_execution_latency: [3, 31, 314, 3141, 31415, 314159], - }, - }, - }, - }, - }); - const localEnv = await TestWorkflowEnvironment.createLocal(); - try { - const worker = await Worker.create({ - connection: localEnv.nativeConnection, - workflowsPath: require.resolve('./workflows'), - taskQueue: 'test-prometheus', - }); - await worker.runUntil(async () => { - await localEnv.client.workflow.execute(workflows.successString, { - taskQueue: 'test-prometheus', - workflowId: uuid4(), - }); - - const resp = await fetch(`http://127.0.0.1:${port}/metrics`); - const text = await resp.text(); - - // Verify use seconds & unit suffix - t.assert( - text.includes( - 'temporal_workflow_task_replay_latency_seconds_bucket{namespace="default",' + - 'service_name="temporal-core-sdk",task_queue="test-prometheus",' + - 'workflow_type="successString",my_tag="my_value",le="0.001"}' - ) - ); - - // Verify histogram overrides - t.assert(text.match(/temporal_request_latency_seconds_bucket\{.*,le="31415"/)); - t.assert(text.match(/workflow_task_execution_latency_seconds_bucket\{.*,le="31415"/)); - - // Verify prefix exists on client request metrics - t.assert(text.includes('temporal_long_request{')); - }); - } finally { - await localEnv.teardown(); - } -}); diff --git a/packages/test/src/test-proto-utils.ts b/packages/test/src/test-proto-utils.ts deleted file mode 100644 index 0145d10be..000000000 --- a/packages/test/src/test-proto-utils.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as fs from 'node:fs'; -import path from 'node:path'; -import test from 'ava'; -import Long from 'long'; // eslint-disable-line import/no-named-as-default -import { historyFromJSON } from '@temporalio/common/lib/proto-utils'; -import proto from '@temporalio/proto'; // eslint-disable-line import/default - -const EventType = proto.temporal.api.enums.v1.EventType; -const ContinueAsNewInitiator = proto.temporal.api.enums.v1.ContinueAsNewInitiator; -const TaskQueueKind = proto.temporal.api.enums.v1.TaskQueueKind; - -test('cancel-fake-progress-replay', async (t) => { - const histJSON = JSON.parse( - await fs.promises.readFile(path.resolve(__dirname, '../history_files/cancel_fake_progress_history.json'), 'utf8') - ); - const hist = historyFromJSON(histJSON); - t.deepEqual(hist.events?.[15].eventId, new Long(16)); - t.is(hist.events?.[15].eventType, EventType.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED); - t.is( - hist.events?.[0].workflowExecutionStartedEventAttributes?.initiator, - ContinueAsNewInitiator.CONTINUE_AS_NEW_INITIATOR_UNSPECIFIED - ); - t.is(hist.events?.[0].workflowExecutionStartedEventAttributes?.taskQueue?.kind, TaskQueueKind.TASK_QUEUE_KIND_NORMAL); -}); - -test('null payload data doesnt crash', async (t) => { - const historyJson = { - events: [ - { - eventId: '16', - eventTime: '2022-07-06T00:33:18.000Z', - eventType: 'WorkflowExecutionCompleted', - version: '0', - taskId: '1057236', - workflowExecutionCompletedEventAttributes: { - result: { payloads: [{ metadata: { encoding: 'YmluYXJ5L251bGw=' }, data: null }] }, - workflowTaskCompletedEventId: '15', - newExecutionRunId: '', - }, - }, - ], - }; - - // This would throw an error if payload data was still null - const history = historyFromJSON(historyJson); - - // Make sure that other history properties were not corrupted - t.is( - Buffer.from( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - history.events?.[0].workflowExecutionCompletedEventAttributes?.result?.payloads?.[0].metadata! - .encoding as Uint8Array - ).toString(), - 'binary/null' - ); -}); diff --git a/packages/test/src/test-replay.ts b/packages/test/src/test-replay.ts deleted file mode 100644 index 0a98d0f81..000000000 --- a/packages/test/src/test-replay.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -import anyTest, { TestFn } from 'ava'; -import { temporal } from '@temporalio/proto'; -import { bundleWorkflowCode, ReplayError, WorkflowBundle } from '@temporalio/worker'; -import { DeterminismViolationError } from '@temporalio/workflow'; -import { loadHistory, Worker } from './helpers'; - -async function gen2array(gen: AsyncIterable): Promise { - const out: T[] = []; - for await (const x of gen) { - out.push(x); - } - return out; -} - -export interface Context { - bundle: WorkflowBundle; -} - -function historator(histories: Array) { - return (async function* () { - for (const history of histories) { - yield { workflowId: 'fake', history }; - } - })(); -} - -const test = anyTest as TestFn; - -test.before(async (t) => { - // We don't want AVA to whine about unhandled rejections thrown by workflows - process.removeAllListeners('unhandledRejection'); - const bundle = await bundleWorkflowCode({ workflowsPath: require.resolve('./workflows') }); - - t.context = { - bundle, - }; -}); - -test('cancel-fake-progress-replay', async (t) => { - const hist = await loadHistory('cancel_fake_progress_history.bin'); - await Worker.runReplayHistory( - { - workflowBundle: t.context.bundle, - }, - hist - ); - t.pass(); -}); - -test('cancel-fake-progress-replay from JSON', async (t) => { - const hist = await loadHistory('cancel_fake_progress_history.json'); - await Worker.runReplayHistory( - { - workflowBundle: t.context.bundle, - }, - hist - ); - t.pass(); -}); - -test('cancel-fake-progress-replay-nondeterministic', async (t) => { - const hist = await loadHistory('cancel_fake_progress_history.bin'); - // Manually alter the workflow type to point to different workflow code - hist.events[0].workflowExecutionStartedEventAttributes!.workflowType!.name = 'http'; - - await t.throwsAsync( - Worker.runReplayHistory( - { - workflowBundle: t.context.bundle, - }, - hist - ), - { - instanceOf: DeterminismViolationError, - } - ); -}); - -test('workflow-task-failure-fails-replay', async (t) => { - const hist = await loadHistory('cancel_fake_progress_history.bin'); - // Manually alter the workflow type to point to our workflow which will fail workflow tasks - hist.events[0].workflowExecutionStartedEventAttributes!.workflowType!.name = 'failsWorkflowTask'; - - await t.throwsAsync( - Worker.runReplayHistory( - { - workflowBundle: t.context.bundle, - replayName: t.title, - }, - hist - ), - { instanceOf: ReplayError } - ); -}); - -test('multiple-histories-replay', async (t) => { - const hist1 = await loadHistory('cancel_fake_progress_history.bin'); - const hist2 = await loadHistory('cancel_fake_progress_history.json'); - const histories = historator([hist1, hist2]); - - const res = await gen2array( - Worker.runReplayHistories( - { - workflowBundle: t.context.bundle, - replayName: t.title, - }, - histories - ) - ); - t.deepEqual( - res.map(({ error }) => error), - [undefined, undefined] - ); -}); - -test('multiple-histories-replay-returns-errors', async (t) => { - const hist1 = await loadHistory('cancel_fake_progress_history.bin'); - const hist2 = await loadHistory('cancel_fake_progress_history.json'); - // change workflow type to break determinism - hist1.events[0].workflowExecutionStartedEventAttributes!.workflowType!.name = 'http'; - hist2.events[0].workflowExecutionStartedEventAttributes!.workflowType!.name = 'http'; - const histories = historator([hist1, hist2]); - - const results = await gen2array( - Worker.runReplayHistories( - { - workflowBundle: t.context.bundle, - replayName: t.title, - }, - histories - ) - ); - - t.is(results.filter(({ error }) => error instanceof DeterminismViolationError).length, 2); -}); - -test('empty-histories-replay-returns-empty-result', async (t) => { - const histories = historator([]); - - const res = await gen2array( - Worker.runReplayHistories( - { - workflowBundle: t.context.bundle, - }, - histories - ) - ); - t.is(res.length, 0); -}); diff --git a/packages/test/src/test-retry-policy.ts b/packages/test/src/test-retry-policy.ts deleted file mode 100644 index b597d346b..000000000 --- a/packages/test/src/test-retry-policy.ts +++ /dev/null @@ -1,83 +0,0 @@ -import test from 'ava'; -import { compileRetryPolicy, ValueError } from '@temporalio/common'; -import { msToTs } from '@temporalio/common/lib/time'; - -test('compileRetryPolicy validates intervals are not 0', (t) => { - t.throws(() => compileRetryPolicy({ initialInterval: 0 }), { - instanceOf: ValueError, - message: 'RetryPolicy.initialInterval cannot be 0', - }); - t.throws(() => compileRetryPolicy({ initialInterval: '0 ms' }), { - instanceOf: ValueError, - message: 'RetryPolicy.initialInterval cannot be 0', - }); - t.throws(() => compileRetryPolicy({ maximumInterval: 0 }), { - instanceOf: ValueError, - message: 'RetryPolicy.maximumInterval cannot be 0', - }); - t.throws(() => compileRetryPolicy({ maximumInterval: '0 ms' }), { - instanceOf: ValueError, - message: 'RetryPolicy.maximumInterval cannot be 0', - }); -}); - -test('compileRetryPolicy validates maximumInterval is not less than initialInterval', (t) => { - t.throws(() => compileRetryPolicy({ maximumInterval: '1 ms', initialInterval: '3 ms' }), { - instanceOf: ValueError, - message: 'RetryPolicy.maximumInterval cannot be less than its initialInterval', - }); -}); - -test('compileRetryPolicy validates backoffCoefficient is greater than 0', (t) => { - t.throws(() => compileRetryPolicy({ backoffCoefficient: 0 }), { - instanceOf: ValueError, - message: 'RetryPolicy.backoffCoefficient must be greater than 0', - }); -}); - -test('compileRetryPolicy validates maximumAttempts is positive', (t) => { - t.throws(() => compileRetryPolicy({ maximumAttempts: -1 }), { - instanceOf: ValueError, - message: 'RetryPolicy.maximumAttempts must be a positive integer', - }); -}); - -test('compileRetryPolicy validates maximumAttempts is an integer', (t) => { - t.throws(() => compileRetryPolicy({ maximumAttempts: 3.1415 }), { - instanceOf: ValueError, - message: 'RetryPolicy.maximumAttempts must be an integer', - }); -}); - -test('compileRetryPolicy drops maximumAttempts when POSITIVE_INFINITY', (t) => { - t.deepEqual(compileRetryPolicy({ maximumAttempts: Number.POSITIVE_INFINITY }), compileRetryPolicy({})); -}); - -test('compileRetryPolicy defaults initialInterval to 1 second', (t) => { - t.deepEqual(compileRetryPolicy({}), { - initialInterval: msToTs('1 second'), - maximumInterval: undefined, - backoffCoefficient: undefined, - maximumAttempts: undefined, - nonRetryableErrorTypes: undefined, - }); -}); - -test('compileRetryPolicy compiles a valid policy', (t) => { - t.deepEqual( - compileRetryPolicy({ - maximumInterval: '4 ms', - initialInterval: '3 ms', - backoffCoefficient: 2, - maximumAttempts: 3, - nonRetryableErrorTypes: ['Error'], - }), - { - initialInterval: msToTs('3 ms'), - maximumInterval: msToTs('4 ms'), - backoffCoefficient: 2, - maximumAttempts: 3, - nonRetryableErrorTypes: ['Error'], - } - ); -}); diff --git a/packages/test/src/test-runtime.ts b/packages/test/src/test-runtime.ts index 405e0a90b..4df717cff 100644 --- a/packages/test/src/test-runtime.ts +++ b/packages/test/src/test-runtime.ts @@ -13,65 +13,74 @@ import { RUN_INTEGRATION_TESTS, Worker, test } from './helpers'; import { createTestWorkflowBundle } from './helpers-integration'; if (RUN_INTEGRATION_TESTS) { - test.serial('Runtime can be created and disposed', async (t) => { - await Runtime.instance().shutdown(); - t.pass(); - }); - - test.serial('Runtime tracks registered workers, shuts down and restarts as expected', async (t) => { - // Create 2 Workers and verify Runtime keeps running after first Worker deregisteration - const worker1 = await Worker.create({ - ...defaultOptions, - taskQueue: 'q1', - }); - const worker2 = await Worker.create({ - ...defaultOptions, - taskQueue: 'q2', - }); - const worker1Drained = worker1.run(); - const worker2Drained = worker2.run(); - worker1.shutdown(); - await worker1Drained; - const client = new WorkflowClient(); - // Run a simple workflow - await client.execute(workflows.sleeper, { taskQueue: 'q2', workflowId: uuid4(), args: [1] }); - worker2.shutdown(); - await worker2Drained; - - const worker3 = await Worker.create({ - ...defaultOptions, - taskQueue: 'q1', // Same as the first Worker created - }); - const worker3Drained = worker3.run(); - // Run a simple workflow - await client.execute('sleeper', { taskQueue: 'q1', workflowId: uuid4(), args: [1] }); - worker3.shutdown(); - await worker3Drained; - // No exceptions, test passes, Runtime is implicitly shut down - t.pass(); - }); + // test.serial('Runtime can be created and disposed', async (t) => { + // await Runtime.instance().shutdown(); + // t.pass(); + // }); + // + // test.serial('Runtime tracks registered workers, shuts down and restarts as expected', async (t) => { + // // Create 2 Workers and verify Runtime keeps running after first Worker deregisteration + // const worker1 = await Worker.create({ + // ...defaultOptions, + // taskQueue: 'q1', + // }); + // const worker2 = await Worker.create({ + // ...defaultOptions, + // taskQueue: 'q2', + // }); + // const worker1Drained = worker1.run(); + // const worker2Drained = worker2.run(); + // worker1.shutdown(); + // await worker1Drained; + // const client = new WorkflowClient(); + // // Run a simple workflow + // await client.execute(workflows.sleeper, { taskQueue: 'q2', workflowId: uuid4(), args: [1] }); + // worker2.shutdown(); + // await worker2Drained; + // + // const worker3 = await Worker.create({ + // ...defaultOptions, + // taskQueue: 'q1', // Same as the first Worker created + // }); + // const worker3Drained = worker3.run(); + // // Run a simple workflow + // await client.execute('sleeper', { taskQueue: 'q1', workflowId: uuid4(), args: [1] }); + // worker3.shutdown(); + // await worker3Drained; + // // No exceptions, test passes, Runtime is implicitly shut down + // t.pass(); + // }); // Stopping and starting Workers is probably not a common pattern but if we don't remember what // Runtime configuration was installed, creating a new Worker after Runtime shutdown we would fallback // to the default configuration (127.0.0.1) which is surprising behavior. test.serial('Runtime.install() remembers installed options after it has been shut down', async (t) => { const logger = new DefaultLogger('DEBUG'); - Runtime.install({ logger }); + Runtime.install({ logger, telemetryOptions: { logging: { filter: { core: 'DEBUG' } } } }); { + console.log('Runtime.instance().options.logger', Runtime.instance().options.logger); const runtime = Runtime.instance(); t.is(runtime.options.logger, logger); + console.log("1.0"); } const worker = await Worker.create({ ...defaultOptions, taskQueue: 'q1', // Same as the first Worker created }); + console.log("1.1"); const workerDrained = worker.run(); - worker.shutdown(); + console.log("1.2"); + worker.shutdown(); // TODO: Because we don't await before this, we won't have polled yet. + console.log("1.3"); await workerDrained; { + console.log("1.4"); const runtime = Runtime.instance(); + console.log("1.5"); t.is(runtime.options.logger, logger); + console.log("1.6"); await runtime.shutdown(); + console.log("1.7"); } }); diff --git a/packages/test/src/test-schedules.ts b/packages/test/src/test-schedules.ts deleted file mode 100644 index 200f67dfe..000000000 --- a/packages/test/src/test-schedules.ts +++ /dev/null @@ -1,911 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import anyTest, { TestFn } from 'ava'; -import asyncRetry from 'async-retry'; -import { - defaultPayloadConverter, - CalendarSpec, - CalendarSpecDescription, - Client, - Connection, - ScheduleHandle, - ScheduleSummary, - ScheduleUpdateOptions, - ScheduleDescription, -} from '@temporalio/client'; -import { msToNumber } from '@temporalio/common/lib/time'; -import { - SearchAttributeType, - SearchAttributes, - TypedSearchAttributes, - defineSearchAttributeKey, -} from '@temporalio/common'; -import { registerDefaultCustomSearchAttributes, RUN_INTEGRATION_TESTS, waitUntil } from './helpers'; -import { defaultSAKeys } from './helpers-integration'; - -export interface Context { - client: Client; -} - -const taskQueue = 'async-activity-completion'; -const test = anyTest as TestFn; - -const dummyWorkflow = async () => undefined; -const dummyWorkflowWith1Arg = async (_s: string) => undefined; -const dummyWorkflowWith2Args = async (_x: number, _y: number) => undefined; - -const calendarSpecDescriptionDefaults: CalendarSpecDescription = { - second: [{ start: 0, end: 0, step: 1 }], - minute: [{ start: 0, end: 0, step: 1 }], - hour: [{ start: 0, end: 0, step: 1 }], - dayOfMonth: [{ start: 1, end: 31, step: 1 }], - month: [{ start: 'JANUARY', end: 'DECEMBER', step: 1 }], - dayOfWeek: [{ start: 'SUNDAY', end: 'SATURDAY', step: 1 }], - year: [], - comment: '', -}; - -if (RUN_INTEGRATION_TESTS) { - test.before(async (t) => { - const connection = await Connection.connect(); - await registerDefaultCustomSearchAttributes(connection); - t.context = { - client: new Client({ connection }), - }; - }); - - test.serial('Can create schedule with calendar', async (t) => { - const { client } = t.context; - const scheduleId = `can-create-schedule-with-calendar-${randomUUID()}`; - const action = { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - } as const; - const handle = await client.schedule.create({ - scheduleId, - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action, - }); - - try { - const describedSchedule = await handle.describe(); - t.deepEqual(describedSchedule.spec.calendars, [ - { ...calendarSpecDescriptionDefaults, hour: [{ start: 2, end: 7, step: 1 }] }, - ]); - } finally { - await handle.delete(); - } - }); - - test.serial('Can create schedule with intervals', async (t) => { - const { client } = t.context; - const scheduleId = `can-create-schedule-with-inteval-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: { - intervals: [{ every: '1h', offset: '5m' }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - }); - - try { - const describedSchedule = await handle.describe(); - t.deepEqual(describedSchedule.spec.intervals, [{ every: msToNumber('1h'), offset: msToNumber('5m') }]); - } finally { - await handle.delete(); - } - }); - - test.serial('Can create schedule with cron syntax', async (t) => { - const { client } = t.context; - const scheduleId = `can-create-schedule-with-cron-syntax-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: { - cronExpressions: ['0 12 * * MON-WED,FRI'], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - }); - - try { - const describedSchedule = await handle.describe(); - t.deepEqual(describedSchedule.spec.calendars, [ - { - ...calendarSpecDescriptionDefaults, - hour: [{ start: 12, end: 12, step: 1 }], - dayOfWeek: [ - { start: 'MONDAY', end: 'WEDNESDAY', step: 1 }, - { start: 'FRIDAY', end: 'FRIDAY', step: 1 }, - ], - }, - ]); - } finally { - await handle.delete(); - } - }); - - test.serial('Can create schedule without any spec', async (t) => { - const { client } = t.context; - const scheduleId = `can-create-schedule-without-any-spec-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: {}, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - }); - - try { - const describedSchedule = await handle.describe(); - t.deepEqual(describedSchedule.spec.calendars, []); - t.deepEqual(describedSchedule.spec.intervals, []); - t.deepEqual(describedSchedule.spec.skip, []); - } finally { - await handle.delete(); - } - }); - - test.serial('Can create schedule with startWorkflow action (no arg)', async (t) => { - const { client } = t.context; - const scheduleId = `can-create-schedule-with-startWorkflow-action-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - memo: { - 'my-memo': 'foo', - }, - searchAttributes: { - CustomKeywordField: ['test-value2'], - }, - typedSearchAttributes: new TypedSearchAttributes([{ key: defaultSAKeys.CustomIntField, value: 42 }]), - }, - }); - - try { - const describedSchedule = await handle.describe(); - - t.is(describedSchedule.action.type, 'startWorkflow'); - t.is(describedSchedule.action.workflowType, 'dummyWorkflow'); - t.deepEqual(describedSchedule.action.memo, { 'my-memo': 'foo' }); - // eslint-disable-next-line deprecation/deprecation - t.deepEqual(describedSchedule.action.searchAttributes, { - CustomKeywordField: ['test-value2'], - CustomIntField: [42], - }); - t.deepEqual( - describedSchedule.action.typedSearchAttributes, - new TypedSearchAttributes([ - { key: defaultSAKeys.CustomIntField, value: 42 }, - { key: defaultSAKeys.CustomKeywordField, value: 'test-value2' }, - ]) - ); - } finally { - await handle.delete(); - } - }); - - test.serial('Can create schedule with startWorkflow action (with args)', async (t) => { - const { client } = t.context; - const scheduleId = `can-create-schedule-with-startWorkflow-action-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflowWith2Args, - args: [3, 4], - taskQueue, - memo: { - 'my-memo': 'foo', - }, - searchAttributes: { - CustomKeywordField: ['test-value2'], - }, - typedSearchAttributes: new TypedSearchAttributes([{ key: defaultSAKeys.CustomIntField, value: 42 }]), - }, - }); - - try { - const describedSchedule = await handle.describe(); - - t.is(describedSchedule.action.type, 'startWorkflow'); - t.is(describedSchedule.action.workflowType, 'dummyWorkflowWith2Args'); - t.deepEqual(describedSchedule.action.args, [3, 4]); - t.deepEqual(describedSchedule.action.memo, { 'my-memo': 'foo' }); - // eslint-disable-next-line deprecation/deprecation - t.deepEqual(describedSchedule.action.searchAttributes, { - CustomKeywordField: ['test-value2'], - CustomIntField: [42], - }); - t.deepEqual( - describedSchedule.action.typedSearchAttributes, - new TypedSearchAttributes([ - { key: defaultSAKeys.CustomIntField, value: 42 }, - { key: defaultSAKeys.CustomKeywordField, value: 'test-value2' }, - ]) - ); - } finally { - await handle.delete(); - } - }); - - test.serial('Interceptor is called on create schedule', async (t) => { - const clientWithInterceptor = new Client({ - connection: t.context.client.connection, - interceptors: { - schedule: [ - { - async create(input, next) { - return next({ - ...input, - headers: { - intercepted: defaultPayloadConverter.toPayload('intercepted'), - }, - }); - }, - }, - ], - }, - }); - - const scheduleId = `interceptor-called-on-create-schedule-${randomUUID()}`; - const handle = await clientWithInterceptor.schedule.create({ - scheduleId, - spec: { - intervals: [{ every: '1h', offset: '5m' }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - }); - - try { - const describedSchedule = await handle.describe(); - const outHeaders = describedSchedule.raw.schedule?.action?.startWorkflow?.header; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - t.is(defaultPayloadConverter.fromPayload(outHeaders!.fields!.intercepted!), 'intercepted'); - } finally { - await handle.delete(); - } - }); - - test.serial('startWorkflow headers are kept on update', async (t) => { - const clientWithInterceptor = new Client({ - connection: t.context.client.connection, - interceptors: { - schedule: [ - { - async create(input, next) { - return next({ - ...input, - headers: { - intercepted: defaultPayloadConverter.toPayload('intercepted'), - }, - }); - }, - }, - ], - }, - }); - - const scheduleId = `startWorkflow-headerskept-on-update-${randomUUID()}`; - const handle = await clientWithInterceptor.schedule.create({ - scheduleId, - spec: { - intervals: [{ every: '1h', offset: '5m' }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - }); - - try { - // Actually perform no change to the schedule - await handle.update((x) => x); - - const describedSchedule = await handle.describe(); - const outHeaders = describedSchedule.raw.schedule?.action?.startWorkflow?.header; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - t.is(defaultPayloadConverter.fromPayload(outHeaders!.fields!.intercepted!), 'intercepted'); - } finally { - await handle.delete(); - } - }); - - test.serial('Can pause and unpause schedule', async (t) => { - const { client } = t.context; - const scheduleId = `can-pause-and-unpause-schedule-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - memo: { - 'my-memo': 'foo', - }, - searchAttributes: { - CustomKeywordField: ['test-value2'], - }, - typedSearchAttributes: new TypedSearchAttributes([{ key: defaultSAKeys.CustomIntField, value: 42 }]), - }, - }); - - try { - let describedSchedule = await handle.describe(); - t.false(describedSchedule.state.paused); - - await handle.pause(); - describedSchedule = await handle.describe(); - t.true(describedSchedule.state.paused); - - await handle.unpause(); - describedSchedule = await handle.describe(); - t.false(describedSchedule.state.paused); - } finally { - await handle.delete(); - } - }); - - test.serial('Can update schedule calendar', async (t) => { - const { client } = t.context; - const scheduleId = `can-update-schedule-calendar-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - }); - - try { - await handle.update((x) => ({ - ...x, - spec: { - calendars: [{ hour: { start: 6, end: 9, step: 1 } }], - }, - })); - - const describedSchedule = await handle.describe(); - t.deepEqual(describedSchedule.spec.calendars, [ - { ...calendarSpecDescriptionDefaults, hour: [{ start: 6, end: 9, step: 1 }] }, - ]); - } finally { - await handle.delete(); - } - }); - - test.serial('Can update schedule action', async (t) => { - const { client } = t.context; - const scheduleId = `can-update-schedule-action-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action: { - type: 'startWorkflow', - workflowId: `${scheduleId}-workflow`, - workflowType: dummyWorkflowWith1Arg, - args: ['foo'], - taskQueue, - }, - }); - - try { - await handle.update((x) => ({ - ...x, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflowWith2Args, - args: [3, 4], - taskQueue, - }, - })); - - const describedSchedule = await handle.describe(); - t.is(describedSchedule.action.type, 'startWorkflow'); - t.is(describedSchedule.action.workflowType, 'dummyWorkflowWith2Args'); - t.deepEqual(describedSchedule.action.args, [3, 4]); - } finally { - await handle.delete(); - } - }); - - test.serial('Can update schedule intervals', async (t) => { - const { client } = t.context; - const scheduleId = `can-update-schedule-intervals-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: { - intervals: [{ every: '5h' }], - }, - action: { - type: 'startWorkflow', - workflowId: `${scheduleId}-workflow`, - workflowType: dummyWorkflowWith1Arg, - args: ['foo'], - taskQueue, - }, - }); - - try { - await handle.update((x: ScheduleUpdateOptions) => { - x.spec.intervals = [{ every: '3h' }]; - return x; - }); - - const describedSchedule = await handle.describe(); - t.is(describedSchedule.action.type, 'startWorkflow'); - t.is(describedSchedule.action.workflowType, 'dummyWorkflowWith1Arg'); - t.deepEqual(describedSchedule.action.args, ['foo']); - } finally { - await handle.delete(); - } - }); - - test.serial('Schedule updates throws without retry on validation error', async (t) => { - const { client } = t.context; - const scheduleId = `schedule-update-throws-without-retry-on-validation-error-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - }); - - try { - let retryCount = 0; - - await t.throwsAsync( - async (): Promise => { - retryCount++; - return handle.update((previous) => ({ - ...previous, - spec: { - calendars: [{ hour: 42 }], - }, - })); - }, - { - instanceOf: TypeError, - } - ); - - t.is(retryCount, 1); - } finally { - await handle.delete(); - } - }); - - test.serial('Can list Schedules', async (t) => { - const { client } = t.context; - - const groupId = randomUUID(); - - const createdScheduleHandlesPromises = []; - for (let i = 10; i < 30; i++) { - const scheduleId = `can-list-schedule-${groupId}-${i}`; - createdScheduleHandlesPromises.push( - client.schedule.create({ - scheduleId, - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - }) - ); - } - const createdScheduleHandles: { [k: string]: ScheduleHandle } = Object.fromEntries( - (await Promise.all(createdScheduleHandlesPromises)).map((x) => [x.scheduleId, x]) - ); - - try { - // Wait for visibility to stabilize - await asyncRetry( - async () => { - const listedScheduleHandles: ScheduleSummary[] = []; - // Page size is intentionnally low to guarantee multiple pages - for await (const schedule of client.schedule.list({ pageSize: 6 })) { - listedScheduleHandles.push(schedule); - } - - const listedScheduleIds = listedScheduleHandles - .map((x) => x.scheduleId) - .filter((x) => x.startsWith(`can-list-schedule-${groupId}-`)) - .sort(); - - const createdSchedulesIds = Object.values(createdScheduleHandles).map((x) => x.scheduleId); - if (createdSchedulesIds.length !== listedScheduleIds.length) throw new Error('Missing list entries'); - - t.deepEqual(listedScheduleIds, createdSchedulesIds); - }, - { - retries: 60, - maxTimeout: 1000, - } - ); - - t.pass(); - } finally { - for (const handle of Object.values(createdScheduleHandles)) { - await handle.delete(); - } - } - }); - - test.serial('Can list Schedules with a query string', async (t) => { - const { client } = t.context; - - const groupId = randomUUID(); - const createdScheduleHandlesPromises = []; - const expectedIds: string[] = []; - for (let i = 0; i < 4; i++) { - const scheduleId = `test-query-${groupId}-${i + 1}`; - const searchAttributes: SearchAttributes = {}; // eslint-disable-line deprecation/deprecation - if (i < 2) { - searchAttributes['CustomKeywordField'] = ['some-value']; - expectedIds.push(scheduleId); - } - createdScheduleHandlesPromises.push( - client.schedule.create({ - scheduleId, - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - searchAttributes, - typedSearchAttributes: new TypedSearchAttributes([{ key: defaultSAKeys.CustomIntField, value: 42 }]), - }) - ); - } - - const createdScheduleHandles: { [k: string]: ScheduleHandle } = Object.fromEntries( - (await Promise.all(createdScheduleHandlesPromises)).map((x) => [x.scheduleId, x]) - ); - - try { - // Wait for visibility to stabilize - await asyncRetry( - async () => { - const listedScheduleHandlesFromQuery: ScheduleSummary[] = []; // to list all the schedule handles from the query string provided - const query = `CustomKeywordField="some-value"`; - - for await (const schedule of client.schedule.list({ query })) { - listedScheduleHandlesFromQuery.push(schedule); - } - - const listedScheduleIdsFromQuery = listedScheduleHandlesFromQuery.map((x) => x.scheduleId).sort(); - - if (listedScheduleIdsFromQuery.length !== expectedIds.length) throw new Error('Entries are missing'); - - t.deepEqual(listedScheduleIdsFromQuery, expectedIds); - }, - { - retries: 60, - maxTimeout: 1000, - } - ); - - t.pass(); - } finally { - for (const handle of Object.values(createdScheduleHandles)) { - await handle.delete(); - } - } - }); - - test.serial('Structured calendar specs are encoded and decoded properly', async (t) => { - const checks: { input: CalendarSpec; expected: CalendarSpecDescription; comment?: string }[] = [ - { - comment: 'a single value X encode to a range in the form { X, X, 1 }', - input: { - hour: 4, - dayOfWeek: 'MONDAY', - month: 'APRIL', - }, - expected: { - ...calendarSpecDescriptionDefaults, - hour: [{ start: 4, end: 4, step: 1 }], - dayOfWeek: [{ start: 'MONDAY', end: 'MONDAY', step: 1 }], - month: [{ start: 'APRIL', end: 'APRIL', step: 1 }], - }, - }, - { - comment: 'match all ranges are exact', - input: { - second: '*', - minute: '*', - hour: '*', - dayOfMonth: '*', - month: '*', - year: '*', - dayOfWeek: '*', - }, - expected: { - ...calendarSpecDescriptionDefaults, - second: [{ start: 0, end: 59, step: 1 }], - minute: [{ start: 0, end: 59, step: 1 }], - hour: [{ start: 0, end: 23, step: 1 }], - dayOfMonth: [{ start: 1, end: 31, step: 1 }], - month: [{ start: 'JANUARY', end: 'DECEMBER', step: 1 }], - year: [], - dayOfWeek: [{ start: 'SUNDAY', end: 'SATURDAY', step: 1 }], - }, - }, - { - comment: 'a mixed array of values and ranges encode properly', - input: { - hour: [4, 7, 9, { start: 15, end: 20, step: 2 }], - dayOfWeek: ['FRIDAY', 'SATURDAY', { start: 'TUESDAY', end: 'FRIDAY', step: 1 }], - month: ['DECEMBER', 'JANUARY', { start: 'APRIL', end: 'JULY', step: 3 }], - }, - expected: { - ...calendarSpecDescriptionDefaults, - hour: [ - { start: 4, end: 4, step: 1 }, - { start: 7, end: 7, step: 1 }, - { start: 9, end: 9, step: 1 }, - { start: 15, end: 20, step: 2 }, - ], - dayOfWeek: [ - { start: 'FRIDAY', end: 'FRIDAY', step: 1 }, - { start: 'SATURDAY', end: 'SATURDAY', step: 1 }, - { start: 'TUESDAY', end: 'FRIDAY', step: 1 }, - ], - month: [ - { start: 'DECEMBER', end: 'DECEMBER', step: 1 }, - { start: 'JANUARY', end: 'JANUARY', step: 1 }, - { start: 'APRIL', end: 'JULY', step: 3 }, - ], - }, - }, - { - input: { - hour: [ - { start: 2, end: 7 }, - { start: 2, end: 7, step: 1 }, - { start: 2, end: 7, step: 1 }, - { start: 2, end: 7, step: 2 }, - { start: 4, end: 0, step: 2 }, - ], - }, - expected: { - ...calendarSpecDescriptionDefaults, - hour: [ - { start: 2, end: 7, step: 1 }, - { start: 2, end: 7, step: 1 }, - { start: 2, end: 7, step: 1 }, - { start: 2, end: 7, step: 2 }, - { start: 4, end: 4, step: 2 }, - ], - }, - }, - { - input: { hour: 4 }, - expected: { ...calendarSpecDescriptionDefaults, hour: [{ start: 4, end: 4, step: 1 }] }, - }, - ]; - - const { client } = t.context; - const scheduleId = `structured-schedule-specs-encoding-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: { - calendars: checks.map(({ input }) => input), - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - }); - - try { - const describedSchedule = await handle.describe(); - const describedCalendars = describedSchedule.spec.calendars ?? []; - - t.is(describedCalendars.length, checks.length); - for (let i = 0; i < checks.length; i++) { - t.deepEqual(describedCalendars[i], checks[i].expected, checks[i].comment); - } - } finally { - await handle.delete(); - } - }); - - test.serial('Can update search attributes of a schedule', async (t) => { - const { client } = t.context; - const scheduleId = `can-update-search-attributes-of-schedule-${randomUUID()}`; - - // Helper to wait for search attribute changes to propagate. - const waitForAttributeChange = async ( - handle: ScheduleHandle, - attributeName: string, - shouldExist: boolean - ): Promise => { - await waitUntil(async () => { - const desc = await handle.describe(); - const exists = - desc.typedSearchAttributes.getAll().find((pair) => pair.key.name === attributeName) !== undefined; - return exists === shouldExist; - }, 5000); - return await handle.describe(); - }; - - // Create a schedule with search attributes. - const handle = await client.schedule.create({ - scheduleId, - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }, - searchAttributes: { - CustomKeywordField: ['keyword-one'], - }, - typedSearchAttributes: [{ key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 1 }], - }); - - // Check the search attributes are part of the schedule description. - const desc = await handle.describe(); - // eslint-disable-next-line deprecation/deprecation - t.deepEqual(desc.searchAttributes, { - CustomKeywordField: ['keyword-one'], - CustomIntField: [1], - }); - t.deepEqual( - desc.typedSearchAttributes, - new TypedSearchAttributes([ - { key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 1 }, - { key: defineSearchAttributeKey('CustomKeywordField', SearchAttributeType.KEYWORD), value: 'keyword-one' }, - ]) - ); - - // Perform a series of updates to schedule's search attributes. - try { - // Update existing search attributes, add new ones. - await handle.update((desc) => ({ - ...desc, - searchAttributes: { - CustomKeywordField: ['keyword-two'], - // Add a new search attribute. - CustomDoubleField: [1.5], - }, - typedSearchAttributes: [ - { key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 2 }, - // Add a new typed search attribute. - { key: defineSearchAttributeKey('CustomTextField', SearchAttributeType.TEXT), value: 'new-text' }, - ], - })); - - let desc = await waitForAttributeChange(handle, 'CustomTextField', true); - // eslint-disable-next-line deprecation/deprecation - t.deepEqual(desc.searchAttributes, { - CustomKeywordField: ['keyword-two'], - CustomIntField: [2], - CustomDoubleField: [1.5], - CustomTextField: ['new-text'], - }); - t.deepEqual( - desc.typedSearchAttributes, - new TypedSearchAttributes([ - { key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 2 }, - { key: defineSearchAttributeKey('CustomKeywordField', SearchAttributeType.KEYWORD), value: 'keyword-two' }, - { key: defineSearchAttributeKey('CustomTextField', SearchAttributeType.TEXT), value: 'new-text' }, - { key: defineSearchAttributeKey('CustomDoubleField', SearchAttributeType.DOUBLE), value: 1.5 }, - ]) - ); - - // Update and remove some search attributes. We remove a search attribute by omitting an existing key from the update. - await handle.update((desc) => ({ - ...desc, - searchAttributes: { - CustomKeywordField: ['keyword-three'], - }, - typedSearchAttributes: [{ key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 3 }], - })); - - desc = await waitForAttributeChange(handle, 'CustomTextField', false); - // eslint-disable-next-line deprecation/deprecation - t.deepEqual(desc.searchAttributes, { - CustomKeywordField: ['keyword-three'], - CustomIntField: [3], - }); - t.deepEqual( - desc.typedSearchAttributes, - new TypedSearchAttributes([ - { key: defineSearchAttributeKey('CustomIntField', SearchAttributeType.INT), value: 3 }, - { key: defineSearchAttributeKey('CustomKeywordField', SearchAttributeType.KEYWORD), value: 'keyword-three' }, - ]) - ); - - // Remove all search attributes. - await handle.update((desc) => ({ - ...desc, - searchAttributes: {}, - typedSearchAttributes: [], - })); - - desc = await waitForAttributeChange(handle, 'CustomIntField', false); - t.deepEqual(desc.searchAttributes, {}); // eslint-disable-line deprecation/deprecation - t.deepEqual(desc.typedSearchAttributes, new TypedSearchAttributes([])); - } finally { - await handle.delete(); - } - }); - - test.serial('User metadata on schedule', async (t) => { - const { client } = t.context; - const scheduleId = `schedule-with-user-metadata-${randomUUID()}`; - const handle = await client.schedule.create({ - scheduleId, - spec: {}, - action: { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - staticSummary: 'schedule static summary', - staticDetails: 'schedule static details', - }, - }); - - try { - const describedSchedule = await handle.describe(); - t.deepEqual(describedSchedule.spec.calendars, []); - t.deepEqual(describedSchedule.spec.intervals, []); - t.deepEqual(describedSchedule.spec.skip, []); - t.deepEqual(describedSchedule.action.staticSummary, 'schedule static summary'); - t.deepEqual(describedSchedule.action.staticDetails, 'schedule static details'); - } finally { - await handle.delete(); - } - }); -} diff --git a/packages/test/src/test-server-options.ts b/packages/test/src/test-server-options.ts deleted file mode 100644 index c45cde1ef..000000000 --- a/packages/test/src/test-server-options.ts +++ /dev/null @@ -1,31 +0,0 @@ -import test from 'ava'; -import { normalizeTlsConfig } from '@temporalio/common/lib/internal-non-workflow'; - -test('normalizeTlsConfig turns null to undefined', (t) => { - t.is(normalizeTlsConfig(null), undefined); -}); - -test('normalizeTlsConfig turns false to undefined', (t) => { - t.is(normalizeTlsConfig(false), undefined); -}); - -test('normalizeTlsConfig turns 0 to undefined', (t) => { - t.is(normalizeTlsConfig(0 as any), undefined); -}); - -test('normalizeTlsConfig turns true to object', (t) => { - t.deepEqual(normalizeTlsConfig(true), {}); -}); - -test('normalizeTlsConfig turns 1 to object', (t) => { - t.deepEqual(normalizeTlsConfig(1 as any), {}); -}); - -test('normalizeTlsConfig passes through undefined', (t) => { - t.is(normalizeTlsConfig(undefined), undefined); -}); - -test('normalizeTlsConfig passes through object', (t) => { - const cfg = { serverNameOverride: 'temporal' }; - t.is(normalizeTlsConfig(cfg), cfg); -}); diff --git a/packages/test/src/test-signal-query-patch.ts b/packages/test/src/test-signal-query-patch.ts deleted file mode 100644 index b768abe06..000000000 --- a/packages/test/src/test-signal-query-patch.ts +++ /dev/null @@ -1,54 +0,0 @@ -import crypto from 'node:crypto'; -import test from 'ava'; -import * as wf from '@temporalio/workflow'; -import { Worker, TestWorkflowEnvironment } from './helpers'; -import * as workflows from './workflows/signal-query-patch-pre-patch'; - -test('Signal+Query+Patch does not cause non-determinism error on replay', async (t) => { - const env = await TestWorkflowEnvironment.createLocal(); - try { - const workflowId = crypto.randomUUID(); - - // Create the first worker with pre-patched version of the workflow - const worker1 = await Worker.create({ - connection: env.nativeConnection, - taskQueue: 'signal-query-patch', - workflowsPath: require.resolve('./workflows/signal-query-patch-pre-patch'), - - // Avoid waiting for sticky execution timeout on worker transition - stickyQueueScheduleToStartTimeout: '1s', - }); - - // Start the workflow, wait for the first task to be processed, then send it a signal and wait for it to be completed - const handle = await worker1.runUntil(async () => { - const handle = await env.client.workflow.start(workflows.patchQuerySignal, { - taskQueue: 'signal-query-patch', - workflowId, - }); - await handle.signal(wf.defineSignal('signal')); - - // Make sure the signal got processed before we shutdown the worker - await handle.query('query'); - return handle; - }); - - // Create the second worker with post-patched version of the workflow - const worker2 = await Worker.create({ - connection: env.nativeConnection, - taskQueue: 'signal-query-patch', - workflowsPath: require.resolve('./workflows/signal-query-patch-post-patch'), - }); - - // Trigger a query and wait for it to be processed - const enteredPatchBlock = await worker2.runUntil(async () => { - await handle.query('query'); - await handle.signal('unblock'); - - return await handle.result(); - }); - - t.false(enteredPatchBlock); - } finally { - await env.teardown(); - } -}); diff --git a/packages/test/src/test-signals-are-always-delivered.ts b/packages/test/src/test-signals-are-always-delivered.ts deleted file mode 100644 index 9251713f2..000000000 --- a/packages/test/src/test-signals-are-always-delivered.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Tests that if a signal is delivered while the Worker is processing a Workflow - * Task, the Worker picks up a new Workflow Task (including the signal) and - * the Workflow library delivers the signal to user code before it starts the - * Workflow execution. - * - * @module - */ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { WorkflowClient } from '@temporalio/client'; -import { DefaultLogger, Runtime, InjectedSinks } from '@temporalio/worker'; -import { defaultOptions } from './mock-native-worker'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import * as workflows from './workflows'; - -if (RUN_INTEGRATION_TESTS) { - test.before(async () => { - Runtime.install({ logger: new DefaultLogger('DEBUG') }); - }); - - test('Signals are always delivered', async (t) => { - const taskQueue = 'test-signal-delivery'; - const conn = new WorkflowClient(); - const wf = await conn.start(workflows.signalsAreAlwaysProcessed, { taskQueue, workflowId: uuid4() }); - - const sinks: InjectedSinks = { - controller: { - sendSignal: { - async fn() { - // Send a signal to the Workflow which will cause the WFT to fail - await wf.signal(workflows.incrementSignal); - }, - }, - }, - }; - - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - }); - - await worker.runUntil(wf.result()); - - // Workflow completes if it got the signal - t.pass(); - }); -} diff --git a/packages/test/src/test-sinks.ts b/packages/test/src/test-sinks.ts deleted file mode 100644 index a7ed18b88..000000000 --- a/packages/test/src/test-sinks.ts +++ /dev/null @@ -1,557 +0,0 @@ -/* eslint @typescript-eslint/no-non-null-assertion: 0 */ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { Connection, WorkflowClient } from '@temporalio/client'; -import { DefaultLogger, InjectedSinks, Runtime, WorkerOptions, LogEntry, NativeConnection } from '@temporalio/worker'; -import { SearchAttributes, WorkflowInfo } from '@temporalio/workflow'; -import { UnsafeWorkflowInfo } from '@temporalio/workflow/lib/interfaces'; -import { SdkComponent, TypedSearchAttributes } from '@temporalio/common'; -import { RUN_INTEGRATION_TESTS, Worker, registerDefaultCustomSearchAttributes } from './helpers'; -import { defaultOptions } from './mock-native-worker'; -import * as workflows from './workflows'; - -class DependencyError extends Error { - constructor( - public readonly ifaceName: string, - public readonly fnName: string - ) { - super(`${ifaceName}.${fnName}`); - } -} - -if (RUN_INTEGRATION_TESTS) { - const recordedLogs: { [workflowId: string]: LogEntry[] } = {}; - let nativeConnection: NativeConnection; - - test.before(async (_) => { - await registerDefaultCustomSearchAttributes(await Connection.connect({})); - Runtime.install({ - logger: new DefaultLogger('DEBUG', (entry: LogEntry) => { - const workflowId = (entry.meta as any)?.workflowInfo?.workflowId; - recordedLogs[workflowId] ??= []; - recordedLogs[workflowId].push(entry); - }), - }); - - // FIXME(JWH): At some point, tests in this file ends up creating a situation where we no longer have any - // native resource tracked by the lang side Runtime object, so the lang Runtime tries to shutdown itself, - // but in the mean time, another test tries to create another resource. which results in a rust side - // finalization error. Holding on to a nativeConnection object avoids that situation. That's a dirty hack. - // Proper fix will be implemented in a distinct PR. - nativeConnection = await NativeConnection.connect({}); - }); - - test.after.always(async () => { - await nativeConnection.close(); - }); - - test('Worker injects sinks', async (t) => { - interface RecordedCall { - info: WorkflowInfo; - counter: number; - fn: string; - } - - function fixWorkflowInfoDates(input: WorkflowInfo): WorkflowInfo { - delete (input.unsafe as any).now; - return input; - } - - const recordedCalls: RecordedCall[] = []; - const taskQueue = `${__filename}-${t.title}`; - const thrownErrors = Array(); - const sinks: InjectedSinks = { - success: { - runAsync: { - async fn(info, counter) { - recordedCalls.push({ info: fixWorkflowInfoDates(info), counter, fn: 'success.runAsync' }); - }, - }, - runSync: { - fn(info, counter) { - recordedCalls.push({ info: fixWorkflowInfoDates(info), counter, fn: 'success.runSync' }); - }, - }, - }, - error: { - throwAsync: { - async fn(info, counter) { - recordedCalls.push({ info: fixWorkflowInfoDates(info), counter, fn: 'error.throwAsync' }); - const error = new DependencyError('error', 'throwAsync'); - thrownErrors.push(error); - throw error; - }, - }, - throwSync: { - fn(info, counter) { - recordedCalls.push({ info: fixWorkflowInfoDates(info), counter, fn: 'error.throwSync' }); - const error = new DependencyError('error', 'throwSync'); - thrownErrors.push(error); - throw error; - }, - }, - }, - }; - - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - }); - const client = new WorkflowClient(); - const wf = await worker.runUntil(async () => { - const wf = await client.start(workflows.sinksWorkflow, { taskQueue, workflowId: uuid4() }); - await wf.result(); - return wf; - }); - - // Capture volatile values that are hard to predict - const { historySize, startTime, runStartTime, currentBuildId, currentDeploymentVersion } = recordedCalls[0].info; // eslint-disable-line deprecation/deprecation - t.true(historySize > 300); - - const info: WorkflowInfo = { - namespace: 'default', - firstExecutionRunId: wf.firstExecutionRunId, - attempt: 1, - taskTimeoutMs: 10_000, - continuedFromExecutionRunId: undefined, - cronSchedule: undefined, - cronScheduleToScheduleInterval: undefined, - executionExpirationTime: undefined, - executionTimeoutMs: undefined, - retryPolicy: undefined, - runTimeoutMs: undefined, - taskQueue, - workflowId: wf.workflowId, - runId: wf.firstExecutionRunId, - workflowType: 'sinksWorkflow', - lastFailure: undefined, - lastResult: undefined, - memo: {}, - parent: undefined, - root: undefined, - searchAttributes: {}, - // FIXME: consider rehydrating the class before passing to sink functions or - // create a variant of WorkflowInfo that corresponds to what we actually get in sinks. - // See issue #1635. - typedSearchAttributes: { searchAttributes: {} } as unknown as TypedSearchAttributes, - historyLength: 3, - continueAsNewSuggested: false, - // values ignored for the purpose of comparison - historySize, - startTime, - runStartTime, - currentBuildId, - currentDeploymentVersion, - // unsafe.now() doesn't make it through serialization, but .now is required, so we need to cast - unsafe: { - isReplaying: false, - } as UnsafeWorkflowInfo, - priority: { - fairnessKey: undefined, - fairnessWeight: undefined, - priorityKey: undefined, - }, - }; - - t.deepEqual(recordedCalls, [ - { info, fn: 'success.runSync', counter: 0 }, - { info, fn: 'success.runAsync', counter: 1 }, - { info, fn: 'error.throwSync', counter: 2 }, - { info, fn: 'error.throwAsync', counter: 3 }, - ]); - - t.deepEqual( - recordedLogs[info.workflowId].map((x: LogEntry) => ({ - ...x, - meta: { - ...x.meta, - workflowInfo: fixWorkflowInfoDates(x.meta?.workflowInfo), - namespace: info.namespace, - runId: info.runId, - workflowId: info.workflowId, - workflowType: info.workflowType, - }, - timestampNanos: undefined, - })), - thrownErrors.map((error) => ({ - level: 'ERROR', - message: 'External sink function threw an error', - meta: { - error, - ifaceName: error.ifaceName, - fnName: error.fnName, - workflowInfo: info, - sdkComponent: SdkComponent.worker, - taskQueue, - namespace: info.namespace, - runId: info.runId, - workflowId: info.workflowId, - workflowType: info.workflowType, - }, - timestampNanos: undefined, - })) - ); - }); - - test('Sink functions are not called during replay if callDuringReplay is unset', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - - const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - async fn(info, message) { - recordedMessages.push({ - message, - historyLength: info.historyLength, - isReplaying: info.unsafe.isReplaying, - }); - }, - }, - }, - }; - - const client = new WorkflowClient(); - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - maxCachedWorkflows: 0, - maxConcurrentWorkflowTaskExecutions: 2, - }); - await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId: uuid4() })); - - t.deepEqual(recordedMessages, [ - { - message: 'Workflow execution started, replaying: false, hl: 3', - historyLength: 3, - isReplaying: false, - }, - { - message: 'Workflow execution completed, replaying: false, hl: 8', - historyLength: 8, - isReplaying: false, - }, - ]); - }); - - test('Sink functions are called during replay if callDuringReplay is set', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - - const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - fn: async (info, message) => { - recordedMessages.push({ - message, - historyLength: info.historyLength, - isReplaying: info.unsafe.isReplaying, - }); - }, - callDuringReplay: true, - }, - }, - }; - - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - maxCachedWorkflows: 0, - maxConcurrentWorkflowTaskExecutions: 2, - }); - const client = new WorkflowClient(); - await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId: uuid4() })); - - // Note that task may be replayed more than once and record the first messages multiple times. - t.deepEqual(recordedMessages.slice(0, 2), [ - { - message: 'Workflow execution started, replaying: false, hl: 3', - historyLength: 3, - isReplaying: false, - }, - { - message: 'Workflow execution started, replaying: true, hl: 3', - historyLength: 3, - isReplaying: true, - }, - ]); - t.deepEqual(recordedMessages[recordedMessages.length - 1], { - message: 'Workflow execution completed, replaying: false, hl: 8', - historyLength: 8, - isReplaying: false, - }); - }); - - test('Sink functions are not called in runReplayHistories if callDuringReplay is unset', async (t) => { - const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - fn: async (info, message) => { - recordedMessages.push({ - message, - historyLength: info.historyLength, - isReplaying: info.unsafe.isReplaying, - }); - }, - }, - }, - }; - - const client = new WorkflowClient(); - const taskQueue = `${__filename}-${t.title}`; - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - }); - const workflowId = uuid4(); - await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId })); - const history = await client.getHandle(workflowId).fetchHistory(); - - // Last 3 events are WorkflowTaskStarted, WorkflowTaskCompleted and WorkflowExecutionCompleted - history.events = history!.events!.slice(0, -3); - - recordedMessages.length = 0; - await Worker.runReplayHistory( - { - ...defaultOptions, - sinks, - }, - history, - workflowId - ); - - t.deepEqual(recordedMessages, []); - }); - - test('Sink functions are called in runReplayHistories if callDuringReplay is set', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - - const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - fn: async (info, message) => { - recordedMessages.push({ - message, - historyLength: info.historyLength, - isReplaying: info.unsafe.isReplaying, - }); - }, - callDuringReplay: true, - }, - }, - }; - - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - }); - const client = new WorkflowClient(); - const workflowId = uuid4(); - await worker.runUntil(async () => { - await client.execute(workflows.logSinkTester, { taskQueue, workflowId }); - }); - const history = await client.getHandle(workflowId).fetchHistory(); - - // Last 3 events are WorkflowExecutionStarted, WorkflowTaskCompleted and WorkflowExecutionCompleted - history.events = history!.events!.slice(0, -3); - - recordedMessages.length = 0; - await Worker.runReplayHistory( - { - ...defaultOptions, - sinks, - }, - history, - workflowId - ); - - t.deepEqual(recordedMessages.slice(0, 2), [ - { - message: 'Workflow execution started, replaying: true, hl: 3', - isReplaying: true, - historyLength: 3, - }, - { - message: 'Workflow execution completed, replaying: false, hl: 7', - isReplaying: false, - historyLength: 7, - }, - ]); - }); - - test('Sink functions contains upserted search attributes', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - - const recordedMessages = Array<{ message: string; searchAttributes: SearchAttributes }>(); // eslint-disable-line deprecation/deprecation - const sinks: InjectedSinks = { - customLogger: { - info: { - fn: async (info, message) => { - recordedMessages.push({ - message, - searchAttributes: info.searchAttributes, // eslint-disable-line deprecation/deprecation - }); - }, - callDuringReplay: false, - }, - }, - }; - - const client = new WorkflowClient(); - const date = new Date(); - - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - }); - await worker.runUntil( - client.execute(workflows.upsertAndReadSearchAttributes, { - taskQueue, - workflowId: uuid4(), - args: [date.getTime()], - }) - ); - - t.deepEqual(recordedMessages, [ - { - message: 'Before upsert', - searchAttributes: {}, - }, - { - message: 'After upsert', - searchAttributes: { - CustomBoolField: [true], - CustomKeywordField: ['durable code'], - CustomTextField: ['is useful'], - CustomDatetimeField: [date], - CustomDoubleField: [3.14], - }, - }, - ]); - }); - - test('Sink functions contains upserted memo', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - const client = new WorkflowClient(); - - const recordedMessages = Array<{ message: string; memo: Record | undefined }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - fn: async (info, message) => { - recordedMessages.push({ - message, - memo: info.memo, - }); - }, - callDuringReplay: false, - }, - }, - }; - - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - }); - - await worker.runUntil( - client.execute(workflows.upsertAndReadMemo, { - taskQueue, - workflowId: uuid4(), - memo: { - note1: 'aaa', - note2: 'bbb', - note4: 'eee', - }, - args: [ - { - note2: 'ccc', - note3: 'ddd', - note4: null, - }, - ], - }) - ); - - t.deepEqual(recordedMessages, [ - { - message: 'Before upsert memo', - memo: { - note1: 'aaa', - note2: 'bbb', - note4: 'eee', - }, - }, - { - message: 'After upsert memo', - memo: { - note1: 'aaa', - note2: 'ccc', - note3: 'ddd', - }, - }, - ]); - }); - - test('Core issue 589', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - - const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - fn: async (info, message) => { - recordedMessages.push({ - message, - historyLength: info.historyLength, - isReplaying: info.unsafe.isReplaying, - }); - }, - callDuringReplay: true, - }, - }, - }; - - const client = new WorkflowClient(); - const handle = await client.start(workflows.coreIssue589, { taskQueue, workflowId: uuid4() }); - - const workerOptions: WorkerOptions = { - ...defaultOptions, - taskQueue, - sinks, - - // Cut down on execution time - stickyQueueScheduleToStartTimeout: 1, - }; - - // Start the first worker and wait for the first task to complete before shutdown that worker - await (await Worker.create(workerOptions)).runUntil(handle.query('q')); - - // Start the second worker - await ( - await Worker.create(workerOptions) - ).runUntil(async () => { - await handle.query('q'); - await handle.signal(workflows.unblockSignal); - await handle.result(); - }); - - const checkpointEntries = recordedMessages.filter((m) => m.message.startsWith('Checkpoint')); - t.deepEqual(checkpointEntries, [ - { - message: 'Checkpoint, replaying: false, hl: 8', - historyLength: 8, - isReplaying: false, - }, - ]); - }); -} diff --git a/packages/test/src/test-temporal-cloud.ts b/packages/test/src/test-temporal-cloud.ts deleted file mode 100644 index dde679bd5..000000000 --- a/packages/test/src/test-temporal-cloud.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import test from 'ava'; -import { Client, Connection, Metadata } from '@temporalio/client'; -import { CloudOperationsClient, CloudOperationsConnection } from '@temporalio/cloud'; -import { NativeConnection, Worker } from '@temporalio/worker'; -import * as workflows from './workflows'; - -test('Can connect to Temporal Cloud using mTLS', async (t) => { - const address = process.env.TEMPORAL_CLOUD_MTLS_TEST_TARGET_HOST; - const namespace = process.env.TEMPORAL_CLOUD_MTLS_TEST_NAMESPACE; - const clientCert = process.env.TEMPORAL_CLOUD_MTLS_TEST_CLIENT_CERT; - const clientKey = process.env.TEMPORAL_CLOUD_MTLS_TEST_CLIENT_KEY; - - if (!address || !namespace || !clientCert || !clientKey) { - t.pass('Skipping: No Temporal Cloud mTLS connection details provided'); - return; - } - - const connection = await Connection.connect({ - address, - tls: { - clientCertPair: { - crt: Buffer.from(clientCert), - key: Buffer.from(clientKey), - }, - }, - }); - const client = new Client({ connection, namespace }); - - const nativeConnection = await NativeConnection.connect({ - address, - tls: { - clientCertPair: { - crt: Buffer.from(clientCert), - key: Buffer.from(clientKey), - }, - }, - }); - const nativeClient = new Client({ connection: nativeConnection, namespace }); - - const taskQueue = `test-temporal-cloud-mtls-${randomUUID()}`; - const worker = await Worker.create({ - namespace, - workflowsPath: require.resolve('./workflows'), - connection: nativeConnection, - taskQueue, - }); - - const [res1, res2] = await worker.runUntil(async () => { - return Promise.all([ - client.workflow.execute(workflows.successString, { - workflowId: randomUUID(), - taskQueue, - }), - nativeClient.workflow.execute(workflows.successString, { - workflowId: randomUUID(), - taskQueue, - }), - ]); - }); - - t.is(res1, 'success'); - t.is(res2, 'success'); -}); - -test('Can connect to Temporal Cloud using API Keys', async (t) => { - const address = process.env.TEMPORAL_CLOUD_API_KEY_TEST_TARGET_HOST; - const namespace = process.env.TEMPORAL_CLOUD_API_KEY_TEST_NAMESPACE; - const apiKey = process.env.TEMPORAL_CLOUD_API_KEY_TEST_API_KEY; - - if (!address || !namespace || !apiKey) { - t.pass('Skipping: No Temporal Cloud API Key connection details provided'); - return; - } - - const connection = await Connection.connect({ - address, - apiKey, - tls: true, - }); - const client = new Client({ connection, namespace }); - - const nativeConnection = await NativeConnection.connect({ - address, - apiKey, - tls: true, - }); - const nativeClient = new Client({ connection: nativeConnection, namespace }); - - const taskQueue = `test-temporal-cloud-api-key-${randomUUID()}`; - const worker = await Worker.create({ - namespace, - workflowsPath: require.resolve('./workflows'), - connection: nativeConnection, - taskQueue, - }); - - const [res1, res2] = await worker.runUntil(async () => { - return Promise.all([ - client.workflow.execute(workflows.successString, { - workflowId: randomUUID(), - taskQueue, - }), - nativeClient.workflow.execute(workflows.successString, { - workflowId: randomUUID(), - taskQueue, - }), - ]); - }); - - t.is(res1, 'success'); - t.is(res2, 'success'); -}); - -test('Can create connection to Temporal Cloud Operation service', async (t) => { - const address = process.env.TEMPORAL_CLOUD_OPS_TEST_TARGET_HOST; - const namespace = process.env.TEMPORAL_CLOUD_OPS_TEST_NAMESPACE; - const apiKey = process.env.TEMPORAL_CLOUD_OPS_TEST_API_KEY; - const apiVersion = process.env.TEMPORAL_CLOUD_OPS_TEST_API_VERSION; - - if (!address || !namespace || !apiKey || !apiVersion) { - t.pass('Skipping: No Cloud Operations connection details provided'); - return; - } - - const connection = await CloudOperationsConnection.connect({ - address, - apiKey, - }); - const client = new CloudOperationsClient({ connection, apiVersion }); - - const metadata: Metadata = {}; - if (apiVersion) { - metadata['temporal-cloud-api-version'] = apiVersion; - } - - // Note that the Cloud Operations client does not automatically inject the namespace header. - // This is intentional, as the Cloud Operations Client is a temporary API and will be moved - // to a different owner package in the near future. - const response = await client.withMetadata(metadata, async () => { - return client.cloudService.getNamespace({ namespace }); - }); - t.is(response?.namespace?.namespace, namespace); -}); diff --git a/packages/test/src/test-testenvironment.ts b/packages/test/src/test-testenvironment.ts deleted file mode 100644 index f05f73608..000000000 --- a/packages/test/src/test-testenvironment.ts +++ /dev/null @@ -1,176 +0,0 @@ -import * as process from 'process'; -import { TestFn } from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { WorkflowFailedError } from '@temporalio/client'; -import { workflowInterceptorModules } from '@temporalio/testing'; -import { bundleWorkflowCode, WorkflowBundleWithSourceMap } from '@temporalio/worker'; -import { - assertFromWorkflow, - asyncChildStarter, - raceActivityAndTimer, - sleep, - unblockSignal, - waitOnSignalWithTimeout, -} from './workflows/testenv-test-workflows'; -import { Worker, TestWorkflowEnvironment, testTimeSkipping as anyTestTimeSkipping } from './helpers'; - -interface Context { - testEnv: TestWorkflowEnvironment; - bundle: WorkflowBundleWithSourceMap; -} - -const testTimeSkipping = anyTestTimeSkipping as TestFn; - -testTimeSkipping.before(async (t) => { - t.context = { - testEnv: await TestWorkflowEnvironment.createTimeSkipping(), - bundle: await bundleWorkflowCode({ - workflowsPath: require.resolve('./workflows/testenv-test-workflows'), - workflowInterceptorModules, - }), - }; -}); - -testTimeSkipping.after.always(async (t) => { - await t.context.testEnv?.teardown(); -}); - -testTimeSkipping.serial( - 'TestEnvironment sets up test server and is able to run a Workflow with time skipping', - async (t) => { - const { client, nativeConnection } = t.context.testEnv; - const worker = await Worker.create({ - connection: nativeConnection, - taskQueue: 'test', - workflowBundle: t.context.bundle, - }); - await worker.runUntil( - client.workflow.execute(sleep, { - workflowId: uuid4(), - taskQueue: 'test', - args: [1_000_000], - }) - ); - t.pass(); - } -); - -testTimeSkipping.serial('TestEnvironment can toggle between normal and skipped time', async (t) => { - const { client, nativeConnection } = t.context.testEnv; - - const worker = await Worker.create({ - connection: nativeConnection, - taskQueue: 'test', - workflowBundle: t.context.bundle, - }); - - await worker.runUntil(async () => { - const wfSleepDuration = 1_000_000; - - const t0 = process.hrtime.bigint(); - await client.workflow.execute(sleep, { - workflowId: uuid4(), - taskQueue: 'test', - args: [wfSleepDuration], - }); - const realDuration = Number((process.hrtime.bigint() - t0) / 1_000_000n); - if (wfSleepDuration < realDuration) { - t.fail(`Workflow execution took ${realDuration}, sleep duration was: ${wfSleepDuration}`); - } - }); - t.pass(); -}); - -testTimeSkipping.serial('TestEnvironment sleep can be used to delay activity completion', async (t) => { - const { client, nativeConnection, sleep } = t.context.testEnv; - - const worker = await Worker.create({ - connection: nativeConnection, - taskQueue: 'test', - activities: { - async sleep(duration: number) { - await sleep(duration); - }, - }, - workflowBundle: t.context.bundle, - }); - - const run = async (expectedWinner: 'timer' | 'activity') => { - const winner = await client.workflow.execute(raceActivityAndTimer, { - workflowId: uuid4(), - taskQueue: 'test', - args: [expectedWinner], - }); - t.is(winner, expectedWinner); - }; - await worker.runUntil(async () => { - await run('activity'); - await run('timer'); - }); - t.pass(); -}); - -testTimeSkipping.serial('TestEnvironment sleep can be used to delay sending a signal', async (t) => { - const { client, nativeConnection, sleep } = t.context.testEnv; - - const worker = await Worker.create({ - connection: nativeConnection, - taskQueue: 'test', - workflowBundle: t.context.bundle, - }); - - await worker.runUntil(async () => { - const handle = await client.workflow.start(waitOnSignalWithTimeout, { - workflowId: uuid4(), - taskQueue: 'test', - }); - await sleep(1_000_000); // Time is skipped - await handle.signal(unblockSignal); - await handle.result(); // Time is skipped - }); - t.pass(); -}); - -testTimeSkipping.serial('Workflow code can run assertions', async (t) => { - const { client, nativeConnection } = t.context.testEnv; - - const worker = await Worker.create({ - connection: nativeConnection, - taskQueue: 'test', - workflowBundle: t.context.bundle, - }); - - const err: WorkflowFailedError | undefined = await t.throwsAsync( - worker.runUntil( - client.workflow.execute(assertFromWorkflow, { - workflowId: uuid4(), - taskQueue: 'test', - args: [6], - }) - ), - { instanceOf: WorkflowFailedError } - ); - t.is(err?.cause?.message, 'Expected values to be strictly equal:\n\n6 !== 7\n'); -}); - -testTimeSkipping.serial('ABNADONED child timer can be fast-forwarded', async (t) => { - const { client, nativeConnection } = t.context.testEnv; - - const worker = await Worker.create({ - connection: nativeConnection, - taskQueue: 'test', - workflowBundle: t.context.bundle, - }); - - const childWorkflowId = uuid4(); - await worker.runUntil(async () => { - await client.workflow.execute(asyncChildStarter, { - workflowId: uuid4(), - taskQueue: 'test', - args: [childWorkflowId], - }); - await client.workflow.getHandle(childWorkflowId).result(); - }); - - t.pass(); -}); diff --git a/packages/test/src/test-time.ts b/packages/test/src/test-time.ts deleted file mode 100644 index 569f78047..000000000 --- a/packages/test/src/test-time.ts +++ /dev/null @@ -1,12 +0,0 @@ -import test from 'ava'; -// eslint-disable-next-line import/no-named-as-default -import Long from 'long'; -import { msToTs } from '@temporalio/common/lib/time'; - -test('msToTs converts to Timestamp', (t) => { - t.deepEqual({ seconds: Long.fromInt(600), nanos: 0 }, msToTs('10 minutes')); -}); - -test('msToTs converts number to Timestamp', (t) => { - t.deepEqual({ seconds: Long.fromInt(42), nanos: 0 }, msToTs(42000)); -}); diff --git a/packages/test/src/test-type-helpers.ts b/packages/test/src/test-type-helpers.ts deleted file mode 100644 index 5dbae6a02..000000000 --- a/packages/test/src/test-type-helpers.ts +++ /dev/null @@ -1,164 +0,0 @@ -import vm from 'vm'; -import anyTest, { TestFn } from 'ava'; -import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers'; - -interface Context { - cx1: (script: string) => any; - cx2: (script: string) => any; -} -const test = anyTest as TestFn; - -const script = new vm.Script(` - class ClassA extends Error {}; - class ClassB extends ClassA {} - class ClassC extends ClassB {} -`); - -test.beforeEach((t) => { - const cx1 = vm.createContext(); - cx1.SymbolBasedInstanceOfError = SymbolBasedInstanceOfError; - script.runInContext(cx1); - - const cx2 = vm.createContext(); - cx2.SymbolBasedInstanceOfError = SymbolBasedInstanceOfError; - script.runInContext(cx2); - - t.context = { - cx1: (script: string) => vm.runInContext(script, cx1), - cx2: (script: string) => vm.runInContext(script, cx2), - }; -}); - -// This test is trivial and obvious. It is only meant to clearly establish a baseline for other tests. -test.serial('BASELINE - instanceof works as expected in single realm, without SymbolBasedInstanceOfError', (t) => { - const { cx1 } = t.context; - - t.true(cx1('new ClassA()') instanceof cx1('ClassA')); - t.true(cx1('new ClassB()') instanceof cx1('ClassA')); - t.true(cx1('new ClassC()') instanceof cx1('ClassA')); - - t.false(cx1('new ClassA()') instanceof cx1('ClassB')); - t.true(cx1('new ClassB()') instanceof cx1('ClassB')); - t.true(cx1('new ClassC()') instanceof cx1('ClassB')); - - t.false(cx1('new ClassA()') instanceof cx1('ClassC')); - t.false(cx1('new ClassB()') instanceof cx1('ClassC')); - t.true(cx1('new ClassC()') instanceof cx1('ClassC')); - - t.true(cx1('new ClassA()') instanceof cx1('Object')); - t.true(cx1('new ClassB()') instanceof cx1('Object')); - t.true(cx1('new ClassC()') instanceof cx1('Object')); -}); - -// This test demonstrates that cross-realm instanceof is indeed broken by default. -test.serial('BASELINE - instanceof is broken in cross realms, without SymbolBasedInstanceOfError', (t) => { - const { cx1, cx2 } = t.context; - - t.false(cx1('new ClassA()') instanceof cx2('ClassA')); - t.false(cx1('new ClassB()') instanceof cx2('ClassA')); - t.false(cx1('new ClassC()') instanceof cx2('ClassA')); - - t.false(cx1('new ClassA()') instanceof cx2('ClassB')); - t.false(cx1('new ClassB()') instanceof cx2('ClassB')); - t.false(cx1('new ClassC()') instanceof cx2('ClassB')); - - t.false(cx1('new ClassA()') instanceof cx2('ClassC')); - t.false(cx1('new ClassB()') instanceof cx2('ClassC')); - t.false(cx1('new ClassC()') instanceof cx2('ClassC')); - - t.false(cx1('new ClassA()') instanceof cx2('Object')); - t.false(cx1('new ClassB()') instanceof cx2('Object')); - t.false(cx1('new ClassC()') instanceof cx2('Object')); -}); - -test.serial(`SymbolBasedInstanceOfError doesn't break any default behaviour of instanceof in single realm`, (t) => { - const { cx1 } = t.context; - - cx1(`SymbolBasedInstanceOfError('ClassA')(ClassA)`); - cx1(`SymbolBasedInstanceOfError('ClassB')(ClassB)`); - - t.true(cx1('new ClassA()') instanceof cx1('ClassA')); - t.true(cx1('new ClassB()') instanceof cx1('ClassA')); - t.true(cx1('new ClassC()') instanceof cx1('ClassA')); - - t.false(cx1('new ClassA()') instanceof cx1('ClassB')); - t.true(cx1('new ClassB()') instanceof cx1('ClassB')); - t.true(cx1('new ClassC()') instanceof cx1('ClassB')); - - t.false(cx1('new ClassA()') instanceof cx1('ClassC')); - t.false(cx1('new ClassB()') instanceof cx1('ClassC')); - t.true(cx1('new ClassC()') instanceof cx1('ClassC')); - - t.true(cx1('new ClassA()') instanceof cx1('Object')); - t.true(cx1('new ClassB()') instanceof cx1('Object')); - t.true(cx1('new ClassC()') instanceof cx1('Object')); -}); - -test.serial(`instanceof is working as expected across realms with SymbolBasedInstanceOfError`, (t) => { - const { cx1, cx2 } = t.context; - - cx1(`SymbolBasedInstanceOfError('ClassA')(ClassA)`); - cx1(`SymbolBasedInstanceOfError('ClassB')(ClassB)`); - - cx2(`SymbolBasedInstanceOfError('ClassA')(ClassA)`); - cx2(`SymbolBasedInstanceOfError('ClassB')(ClassB)`); - - t.true(cx1('new ClassA()') instanceof cx2('ClassA')); - t.true(cx1('new ClassB()') instanceof cx2('ClassA')); - t.true(cx1('new ClassC()') instanceof cx2('ClassA')); - - t.false(cx1('new ClassA()') instanceof cx2('ClassB')); - t.true(cx1('new ClassB()') instanceof cx2('ClassB')); - t.true(cx1('new ClassC()') instanceof cx2('ClassB')); - - t.false(cx1('new ClassA()') instanceof cx2('ClassC')); - t.false(cx1('new ClassB()') instanceof cx2('ClassC')); - - // This one is surprising but expected, as SymbolBasedInstanceOfError was never called on ClassC; - // it therefore reverts to the default behavior of instanceof, which is not cross-realm safe. - t.false(cx1('new ClassC()') instanceof cx2('ClassC')); - - // The followings are surprising, but expected, as 'Object' differs between realms. - // SymbolBasedInstanceOfError doesn't help with that. - t.false(cx1('new ClassA()') instanceof cx2('Object')); - t.false(cx1('new ClassB()') instanceof cx2('Object')); - t.false(cx1('new ClassC()') instanceof cx2('Object')); -}); - -test.serial('SymbolBasedInstanceOfError doesnt break on non-object values', (t) => { - const { cx1 } = t.context; - - cx1(`SymbolBasedInstanceOfError('ClassA')(ClassA)`); - - t.false((true as any) instanceof cx1('ClassA')); - t.false((12 as any) instanceof cx1('ClassA')); - t.false((NaN as any) instanceof cx1('ClassA')); - t.false(('string' as any) instanceof cx1('ClassA')); - t.false(([] as any) instanceof cx1('ClassA')); - t.false((undefined as any) instanceof cx1('ClassA')); - t.false((null as any) instanceof cx1('ClassA')); - t.false(((() => null) as any) instanceof cx1('ClassA')); - t.false((Symbol() as any) instanceof cx1('ClassA')); -}); - -test.serial('Same context with same SymbolBasedInstanceOfError calls also works', (t) => { - class ClassA extends Error {} - class ClassB extends Error {} - - t.false(new ClassA() instanceof ClassB); - t.false(new ClassB() instanceof ClassA); - - SymbolBasedInstanceOfError('Foo')(ClassA); - SymbolBasedInstanceOfError('Foo')(ClassB); - - t.true(new ClassA() instanceof ClassB); - t.true(new ClassB() instanceof ClassA); -}); - -test.serial('SymbolBasedInstanceOfError correctly sets the name property', (t) => { - @SymbolBasedInstanceOfError('CustomName') - class ClassA extends Error {} - - t.true(new ClassA() instanceof ClassA); - t.is(new ClassA().name, 'CustomName'); -}); diff --git a/packages/test/src/test-typed-search-attributes.ts b/packages/test/src/test-typed-search-attributes.ts deleted file mode 100644 index 9d4324aaf..000000000 --- a/packages/test/src/test-typed-search-attributes.ts +++ /dev/null @@ -1,466 +0,0 @@ -import { randomUUID } from 'crypto'; -import { ExecutionContext } from 'ava'; -import { ScheduleOptionsAction, WorkflowExecutionDescription } from '@temporalio/client'; -import { - TypedSearchAttributes, - SearchAttributes, - SearchAttributePair, - SearchAttributeType, - SearchAttributeUpdatePair, - defineSearchAttributeKey, -} from '@temporalio/common'; -import { temporal } from '@temporalio/proto'; -import { - condition, - defineQuery, - defineSignal, - setHandler, - upsertSearchAttributes, - WorkflowInfo, - workflowInfo, -} from '@temporalio/workflow'; -import { encodeSearchAttributeIndexedValueType } from '@temporalio/common/lib/search-attributes'; -import { waitUntil } from './helpers'; -import { Context, helpers, makeTestFunction } from './helpers-integration'; - -const test = makeTestFunction({ - workflowsPath: __filename, - workflowEnvironmentOpts: { - server: { - namespace: 'test-typed-search-attributes', - searchAttributes: [], - }, - }, -}); - -const date = new Date(); -const secondDate = new Date(date.getTime() + 1000); - -// eslint-disable-next-line deprecation/deprecation -const untypedAttrsInput: SearchAttributes = { - untyped_single_string: ['one'], - untyped_single_int: [1], - untyped_single_double: [1.23], - untyped_single_bool: [true], - untyped_single_date: [date], - untyped_multi_string: ['one', 'two'], -}; - -// The corresponding typed search attributes from untypedSearchAttributes. -const typedFromUntypedInput: SearchAttributePair[] = [ - { key: defineSearchAttributeKey('untyped_single_string', SearchAttributeType.TEXT), value: 'one' }, - { key: defineSearchAttributeKey('untyped_single_int', SearchAttributeType.INT), value: 1 }, - { key: defineSearchAttributeKey('untyped_single_double', SearchAttributeType.DOUBLE), value: 1.23 }, - { key: defineSearchAttributeKey('untyped_single_bool', SearchAttributeType.BOOL), value: true }, - { key: defineSearchAttributeKey('untyped_single_date', SearchAttributeType.DATETIME), value: date }, - { key: defineSearchAttributeKey('untyped_multi_string', SearchAttributeType.KEYWORD_LIST), value: ['one', 'two'] }, -]; - -const typedAttrsListInput: SearchAttributePair[] = [ - { key: defineSearchAttributeKey('typed_text', SearchAttributeType.TEXT), value: 'typed_text' }, - { key: defineSearchAttributeKey('typed_keyword', SearchAttributeType.KEYWORD), value: 'typed_keyword' }, - { key: defineSearchAttributeKey('typed_int', SearchAttributeType.INT), value: 123 }, - { key: defineSearchAttributeKey('typed_double', SearchAttributeType.DOUBLE), value: 123.45 }, - { key: defineSearchAttributeKey('typed_bool', SearchAttributeType.BOOL), value: true }, - { key: defineSearchAttributeKey('typed_datetime', SearchAttributeType.DATETIME), value: date }, - { - key: defineSearchAttributeKey('typed_keyword_list', SearchAttributeType.KEYWORD_LIST), - value: ['typed', 'keywords'], - }, -]; - -const typedAttrsObjInput = new TypedSearchAttributes(typedAttrsListInput); - -// The corresponding untyped search attributes from typedSearchAttributesList. -// eslint-disable-next-line deprecation/deprecation -const untypedFromTypedInput: SearchAttributes = { - typed_text: ['typed_text'], - typed_keyword: ['typed_keyword'], - typed_int: [123], - typed_double: [123.45], - typed_bool: [true], - typed_datetime: [date], - typed_keyword_list: ['typed', 'keywords'], -}; - -const erroneousTypedKeys = { - erroneous_typed_int: temporal.api.enums.v1.IndexedValueType.INDEXED_VALUE_TYPE_INT, -}; - -const dummyWorkflow = async () => undefined; - -// Note: this is needed, the test fails due to -// test.serial.before not being defined when running workflows. -if (test?.serial?.before) { - // Register all search attribute keys. - test.serial.before(async (t) => { - // Transform untyped keys into 'untypedKey: IndexValueType' pairs. - const untypedKeys = Object.entries(untypedAttrsInput).reduce( - (acc, [key, value]) => { - const typedKey = TypedSearchAttributes.getKeyFromUntyped(key, value); - const encodedKey = encodeSearchAttributeIndexedValueType(typedKey?.type); - if (encodedKey) { - acc[key] = encodedKey; - } - return acc; - }, - {} as { [key: string]: temporal.api.enums.v1.IndexedValueType } - ); - - const typedKeys = typedAttrsListInput.reduce( - (acc, pair) => { - const encodedKey = encodeSearchAttributeIndexedValueType(pair.key.type); - if (encodedKey) { - acc[pair.key.name] = encodedKey; - } - return acc; - }, - {} as { [key: string]: temporal.api.enums.v1.IndexedValueType } - ); - - await t.context.env.connection.operatorService.addSearchAttributes({ - namespace: t.context.env.namespace, - searchAttributes: { - ...untypedKeys, - ...typedKeys, - ...erroneousTypedKeys, - }, - }); - - await waitUntil(async () => { - const resp = await t.context.env.connection.operatorService.listSearchAttributes({ - namespace: t.context.env.namespace, - }); - return ( - Object.keys(untypedKeys).every((key) => key in resp.customAttributes) && - Object.keys(typedKeys).every((key) => key in resp.customAttributes) - ); - }, 5000); - }); -} - -test('does not allow non-integer values for integer search attributes', async (t) => { - try { - const { taskQueue } = helpers(t); - const client = t.context.env.client; - const action: ScheduleOptionsAction = { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }; - const erroneousKeyName = Object.keys(erroneousTypedKeys)[0]; - await client.schedule.create({ - scheduleId: randomUUID(), - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action, - typedSearchAttributes: [ - // Use a double value for an integer search attribute. - // This is legal at compile-time, but should error at runtime when converting to payload. - { key: defineSearchAttributeKey(erroneousKeyName, SearchAttributeType.INT), value: 123.4 }, - ], - }); - } catch (err) { - if (err instanceof Error) { - t.is(err.message, 'Invalid search attribute value 123.4 for given type INT'); - } else { - t.fail('Unexpected error type'); - } - } -}); - -interface TestInputSearchAttributes { - name: string; - input: { - searchAttributes?: SearchAttributes; // eslint-disable-line deprecation/deprecation - typedSearchAttributes?: TypedSearchAttributes | SearchAttributePair[]; - }; - expected: { - searchAttributes?: SearchAttributes; // eslint-disable-line deprecation/deprecation - typedSearchAttributes?: TypedSearchAttributes; - }; -} - -// inputTestCases contains permutations of search attribute inputs -const inputTestCases: TestInputSearchAttributes[] = [ - // Input only untyped search attributes - { - name: 'only-untyped-search-attributes', - input: { - searchAttributes: untypedAttrsInput, - }, - expected: { - searchAttributes: untypedAttrsInput, - typedSearchAttributes: new TypedSearchAttributes(typedFromUntypedInput), - }, - }, - // Input only typed search attributes as a list - { - name: 'only-typed-search-attributes-list', - input: { - typedSearchAttributes: typedAttrsListInput, - }, - expected: { - searchAttributes: untypedFromTypedInput, - typedSearchAttributes: typedAttrsObjInput, - }, - }, - // Input only typed search attributes as an object - { - name: 'only-typed-search-attributes-obj', - input: { - typedSearchAttributes: typedAttrsObjInput, - }, - expected: { - searchAttributes: untypedFromTypedInput, - typedSearchAttributes: typedAttrsObjInput, - }, - }, - // Input both untyped and typed search attributes - { - name: 'both-untyped-and-typed-sa', - input: { - searchAttributes: { - ...untypedAttrsInput, - // Expect to be overwritten by the corresponding typed search attribute. Overwritten value to be "typed_text". - typed_text: ['different_value_from_untyped'], - }, - typedSearchAttributes: typedAttrsListInput, - }, - expected: { - searchAttributes: { - ...untypedFromTypedInput, - ...untypedAttrsInput, - }, - typedSearchAttributes: typedAttrsObjInput.updateCopy(typedFromUntypedInput), - }, - }, -]; - -test('creating schedules with various input search attributes', async (t) => { - await Promise.all( - inputTestCases.map(async ({ input, expected, name }) => { - const { taskQueue } = helpers(t); - const client = t.context.env.client; - const action: ScheduleOptionsAction = { - type: 'startWorkflow', - workflowType: dummyWorkflow, - taskQueue, - }; - const handle = await client.schedule.create({ - scheduleId: randomUUID(), - spec: { - calendars: [{ hour: { start: 2, end: 7, step: 1 } }], - }, - action, - ...input, - }); - const desc = await handle.describe(); - t.deepEqual(desc.searchAttributes, expected.searchAttributes, name); // eslint-disable-line deprecation/deprecation - t.deepEqual(desc.typedSearchAttributes, expected.typedSearchAttributes, name); - }) - ); -}); - -export const getWorkflowInfo = defineQuery('getWorkflowInfo'); -export const mutateSearchAttributes = - defineSignal<[SearchAttributes | SearchAttributeUpdatePair[]]>('mutateSearchAttributes'); // eslint-disable-line deprecation/deprecation -export const complete = defineSignal('complete'); - -export async function changeSearchAttributes(): Promise { - let isComplete = false; - setHandler(getWorkflowInfo, () => { - return workflowInfo(); - }); - setHandler(complete, () => { - isComplete = true; - }); - setHandler(mutateSearchAttributes, (attrs) => { - upsertSearchAttributes(attrs); - }); - await condition(() => isComplete); -} - -test('upsert works with various search attribute mutations', async (t) => { - const { createWorker, startWorkflow } = helpers(t); - const worker = await createWorker({ namespace: t.context.env.namespace }); - await worker.runUntil(async () => { - // Start workflow with some initial search attributes. - const handle = await startWorkflow(changeSearchAttributes, { - typedSearchAttributes: typedAttrsListInput, - }); - let res = await handle.query(getWorkflowInfo); - let desc = await handle.describe(); - assertWorkflowInfoSearchAttributes(t, res, untypedFromTypedInput, typedAttrsListInput); - assertWorkflowDescSearchAttributes(t, desc, untypedFromTypedInput, typedAttrsListInput); - - // Update search attributes with untyped input. - // eslint-disable-next-line deprecation/deprecation - const untypedUpdateAttrs: SearchAttributes = { - typed_text: ['new_value'], - typed_keyword: ['new_keyword'], - typed_int: [2], - typed_double: [2.34], - typed_datetime: [secondDate], - typed_keyword_list: ['three', 'four', 'five'], - // Delete key - empty value. - typed_bool: [], - }; - - // Update search attributes with untyped input. - await handle.signal(mutateSearchAttributes, untypedUpdateAttrs); - res = await handle.query(getWorkflowInfo); - desc = await handle.describe(); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { typed_bool, ...untypedUpdateExpected } = untypedUpdateAttrs; - - assertWorkflowInfoSearchAttributes(t, res, untypedUpdateExpected, [ - { key: defineSearchAttributeKey('typed_text', SearchAttributeType.TEXT), value: 'new_value' }, - { key: defineSearchAttributeKey('typed_keyword', SearchAttributeType.KEYWORD), value: 'new_keyword' }, - { key: defineSearchAttributeKey('typed_int', SearchAttributeType.INT), value: 2 }, - { key: defineSearchAttributeKey('typed_double', SearchAttributeType.DOUBLE), value: 2.34 }, - { - key: defineSearchAttributeKey('typed_keyword_list', SearchAttributeType.KEYWORD_LIST), - value: ['three', 'four', 'five'], - }, - { key: defineSearchAttributeKey('typed_datetime', SearchAttributeType.DATETIME), value: secondDate }, - ]); - - assertWorkflowDescSearchAttributes(t, desc, untypedUpdateExpected, [ - { key: defineSearchAttributeKey('typed_text', SearchAttributeType.TEXT), value: 'new_value' }, - { key: defineSearchAttributeKey('typed_keyword', SearchAttributeType.KEYWORD), value: 'new_keyword' }, - { key: defineSearchAttributeKey('typed_int', SearchAttributeType.INT), value: 2 }, - { key: defineSearchAttributeKey('typed_double', SearchAttributeType.DOUBLE), value: 2.34 }, - { - key: defineSearchAttributeKey('typed_keyword_list', SearchAttributeType.KEYWORD_LIST), - value: ['three', 'four', 'five'], - }, - { key: defineSearchAttributeKey('typed_datetime', SearchAttributeType.DATETIME), value: secondDate }, - ]); - - // Update search attributes with typed input. - const typedUpdateAttrs: SearchAttributeUpdatePair[] = [ - // Delete key. - { key: defineSearchAttributeKey('typed_text', SearchAttributeType.TEXT), value: null }, - { key: defineSearchAttributeKey('typed_int', SearchAttributeType.INT), value: 3 }, - { key: defineSearchAttributeKey('typed_double', SearchAttributeType.DOUBLE), value: 3.45 }, - { - key: defineSearchAttributeKey('typed_keyword_list', SearchAttributeType.KEYWORD_LIST), - value: ['six', 'seven'], - }, - // Add key. - { key: defineSearchAttributeKey('typed_bool', SearchAttributeType.BOOL), value: false }, - ]; - - // Update search attributes with typed input. - await handle.signal(mutateSearchAttributes, typedUpdateAttrs); - res = await handle.query(getWorkflowInfo); - desc = await handle.describe(); - - // Note that we expect the empty array in the untyped search attributes. - const expectedUntyped = { - typed_int: [3], - typed_double: [3.45], - typed_keyword_list: ['six', 'seven'], - typed_bool: [false], - typed_keyword: ['new_keyword'], - typed_datetime: [secondDate], - }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { typed_keyword, typed_datetime, ...newDescExpected } = expectedUntyped; - const expectedTyped = [ - { key: defineSearchAttributeKey('typed_int', SearchAttributeType.INT), value: 3 }, - { key: defineSearchAttributeKey('typed_double', SearchAttributeType.DOUBLE), value: 3.45 }, - { - key: defineSearchAttributeKey('typed_keyword_list', SearchAttributeType.KEYWORD_LIST), - value: ['six', 'seven'], - }, - { key: defineSearchAttributeKey('typed_bool', SearchAttributeType.BOOL), value: false }, - { key: defineSearchAttributeKey('typed_keyword', SearchAttributeType.KEYWORD), value: 'new_keyword' }, - { key: defineSearchAttributeKey('typed_datetime', SearchAttributeType.DATETIME), value: secondDate }, - ]; - - const expectedDescTyped = [ - { key: defineSearchAttributeKey('typed_int', SearchAttributeType.INT), value: 3 }, - { key: defineSearchAttributeKey('typed_double', SearchAttributeType.DOUBLE), value: 3.45 }, - { - key: defineSearchAttributeKey('typed_keyword_list', SearchAttributeType.KEYWORD_LIST), - value: ['six', 'seven'], - }, - { key: defineSearchAttributeKey('typed_bool', SearchAttributeType.BOOL), value: false }, - { key: defineSearchAttributeKey('typed_keyword', SearchAttributeType.KEYWORD), value: 'new_keyword' }, - { key: defineSearchAttributeKey('typed_datetime', SearchAttributeType.DATETIME), value: secondDate }, - ]; - - assertWorkflowInfoSearchAttributes(t, res, expectedUntyped, expectedTyped); - assertWorkflowDescSearchAttributes(t, desc, newDescExpected, expectedDescTyped); - - await handle.signal(complete); - }); -}); - -function assertWorkflowInfoSearchAttributes( - t: ExecutionContext, - res: WorkflowInfo, - searchAttributes: SearchAttributes, // eslint-disable-line deprecation/deprecation - searchAttrPairs: SearchAttributePair[] -) { - // Check initial search attributes are present. - // Response from query serializes datetime attributes to strings so we serialize our expected responses. - t.deepEqual(res.searchAttributes, normalizeSearchAttrs(searchAttributes)); // eslint-disable-line deprecation/deprecation - // This casting is necessary because res.typedSearchAttributes has actually been serialized by its toJSON method - // (returning an array of SearchAttributePair), but is not reflected in its type definition. - assertMatchingSearchAttributePairs(t, res.typedSearchAttributes as unknown as SearchAttributePair[], searchAttrPairs); -} - -function assertWorkflowDescSearchAttributes( - t: ExecutionContext, - desc: WorkflowExecutionDescription, - searchAttributes: SearchAttributes, // eslint-disable-line deprecation/deprecation - searchAttrPairs: SearchAttributePair[] -) { - // Check that all search attributes are present in the workflow description's search attributes. - t.like(desc.searchAttributes, searchAttributes); // eslint-disable-line deprecation/deprecation - const descOmittingBuildIds = desc.typedSearchAttributes - .updateCopy([{ key: defineSearchAttributeKey('BuildIds', SearchAttributeType.KEYWORD_LIST), value: null }]) - .getAll(); - assertMatchingSearchAttributePairs(t, descOmittingBuildIds, searchAttrPairs); -} - -// eslint-disable-next-line deprecation/deprecation -function normalizeSearchAttrs(attrs: SearchAttributes): SearchAttributes { - const res: SearchAttributes = {}; // eslint-disable-line deprecation/deprecation - for (const [key, value] of Object.entries(attrs)) { - if (Array.isArray(value) && value.length === 1 && value[0] instanceof Date) { - res[key] = [value[0].toISOString()]; - continue; - } - res[key] = value; - } - return res; -} - -function normalizeSearchAttrPairs(attrs: SearchAttributePair[]): SearchAttributePair[] { - const res: SearchAttributePair[] = []; - for (const { key, value } of attrs) { - if (value instanceof Date) { - res.push({ key, value: value.toISOString() } as SearchAttributePair); - continue; - } - res.push({ key, value } as SearchAttributePair); - } - return res; -} - -function assertMatchingSearchAttributePairs( - t: ExecutionContext, - actual: SearchAttributePair[], - expected: SearchAttributePair[] -) { - t.deepEqual( - normalizeSearchAttrPairs(actual).sort((a, b) => a.key.name.localeCompare(b.key.name)), - normalizeSearchAttrPairs(expected).sort((a, b) => a.key.name.localeCompare(b.key.name)) - ); -} diff --git a/packages/test/src/test-worker-activities.ts b/packages/test/src/test-worker-activities.ts deleted file mode 100644 index 10508bd5e..000000000 --- a/packages/test/src/test-worker-activities.ts +++ /dev/null @@ -1,348 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import anyTest, { ExecutionContext, TestFn } from 'ava'; -import dedent from 'dedent'; -import { v4 as uuid4 } from 'uuid'; -import { TemporalFailure, defaultPayloadConverter, toPayloads, ApplicationFailure } from '@temporalio/common'; -import { coresdk, google } from '@temporalio/proto'; -import { msToTs } from '@temporalio/common/lib/time'; -import { httpGet } from './activities'; -import { cleanOptionalStackTrace } from './helpers'; -import { defaultOptions, isolateFreeWorker, Worker } from './mock-native-worker'; -import { withZeroesHTTPServer } from './zeroes-http-server'; -import Duration = google.protobuf.Duration; - -export interface Context { - worker: Worker; -} - -export const test = anyTest as TestFn; - -export async function runWorker(t: ExecutionContext, fn: () => Promise): Promise { - const { worker } = t.context; - const promise = worker.run(); - try { - return await fn(); - } finally { - worker.shutdown(); - await promise; - } -} - -test.beforeEach(async (t) => { - const worker = isolateFreeWorker(defaultOptions); - - t.context = { - worker, - }; -}); - -function compareCompletion( - t: ExecutionContext, - actual: coresdk.activity_result.IActivityExecutionResult | null | undefined, - expected: coresdk.activity_result.IActivityExecutionResult -) { - if (actual?.failed?.failure) { - const { stackTrace, ...rest } = actual.failed.failure; - actual = { failed: { failure: { stackTrace: cleanOptionalStackTrace(stackTrace), ...rest } } }; - } - t.deepEqual( - coresdk.activity_result.ActivityExecutionResult.create(actual ?? undefined).toJSON(), - coresdk.activity_result.ActivityExecutionResult.create(expected).toJSON() - ); -} - -test('Worker runs an activity and reports completion', async (t) => { - const { worker } = t.context; - await runWorker(t, async () => { - const taskToken = Buffer.from(uuid4()); - const url = 'https://temporal.io'; - const completion = await worker.native.runActivityTask({ - taskToken, - start: { - activityType: 'httpGet', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter, url), - }, - }); - compareCompletion(t, completion.result, { - completed: { result: defaultPayloadConverter.toPayload(await httpGet(url)) }, - }); - }); -}); - -test('Worker runs an activity and reports failure', async (t) => { - const { worker } = t.context; - await runWorker(t, async () => { - const taskToken = Buffer.from(uuid4()); - const message = ':('; - const completion = await worker.native.runActivityTask({ - taskToken, - start: { - activityType: 'throwAnError', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter, false, message), - }, - }); - compareCompletion(t, completion.result, { - failed: { - failure: { - message, - source: 'TypeScriptSDK', - stackTrace: dedent` - Error: :( - at throwAnError (test/src/activities/index.ts) - `, - applicationFailureInfo: { type: 'Error', nonRetryable: false }, - }, - }, - }); - }); -}); - -const workerCancelsActivityMacro = test.macro(async (t, throwIfAborted?: boolean) => { - const { worker } = t.context; - await runWorker(t, async () => { - const taskToken = Buffer.from(uuid4()); - worker.native.emit({ - activity: { - taskToken, - start: { - activityType: 'waitForCancellation', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter, throwIfAborted), - }, - }, - }); - const completion = await worker.native.runActivityTask({ - taskToken, - cancel: { - reason: coresdk.activity_task.ActivityCancelReason.CANCELLED, - }, - }); - compareCompletion(t, completion.result, { - cancelled: { - failure: { - source: 'TypeScriptSDK', - message: 'CANCELLED', - canceledFailureInfo: {}, - }, - }, - }); - }); -}); - -test('Worker cancels activity and reports cancellation', workerCancelsActivityMacro); - -test('Worker cancels activity and reports cancellation when using throwIfAborted', workerCancelsActivityMacro, true); - -test('Activity Context AbortSignal cancels a fetch request', async (t) => { - const { worker } = t.context; - await runWorker(t, async () => { - await withZeroesHTTPServer(async (port) => { - const taskToken = Buffer.from(uuid4()); - worker.native.emit({ - activity: { - taskToken, - start: { - activityType: 'cancellableFetch', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter, `http://127.0.0.1:${port}`, false), - }, - }, - }); - const completion = await worker.native.runActivityTask({ - taskToken, - cancel: { - reason: coresdk.activity_task.ActivityCancelReason.CANCELLED, - }, - }); - compareCompletion(t, completion.result, { - cancelled: { failure: { source: 'TypeScriptSDK', canceledFailureInfo: {} } }, - }); - }); - }); -}); - -test('Activity cancel with reason "NOT_FOUND" is valid', async (t) => { - const { worker } = t.context; - await runWorker(t, async () => { - await withZeroesHTTPServer(async (port) => { - const taskToken = Buffer.from(uuid4()); - worker.native.emit({ - activity: { - taskToken, - start: { - activityType: 'cancellableFetch', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter, `http://127.0.0.1:${port}`, false), - }, - }, - }); - const completion = await worker.native.runActivityTask({ - taskToken, - cancel: { - reason: coresdk.activity_task.ActivityCancelReason.NOT_FOUND, - }, - }); - compareCompletion(t, completion.result, { - cancelled: { failure: { source: 'TypeScriptSDK', canceledFailureInfo: {} } }, - }); - }); - }); -}); - -test('Activity Context heartbeat is sent to core', async (t) => { - const { worker } = t.context; - await runWorker(t, async () => { - const taskToken = Buffer.from(uuid4()); - const completionPromise = worker.native.runActivityTask({ - taskToken, - start: { - activityType: 'progressiveSleep', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter), - }, - }); - console.log('waiting heartbeat 1'); - t.is(await worker.native.untilHeartbeat(taskToken), 1); - console.log('waiting heartbeat 2'); - t.is(await worker.native.untilHeartbeat(taskToken), 2); - t.is(await worker.native.untilHeartbeat(taskToken), 3); - console.log('waiting completion'); - compareCompletion(t, (await completionPromise).result, { - completed: { result: defaultPayloadConverter.toPayload(undefined) }, - }); - }); -}); - -test('Worker fails activity with proper message when it is not registered', async (t) => { - const { worker } = t.context; - await runWorker(t, async () => { - const taskToken = Buffer.from(uuid4()); - const { result } = await worker.native.runActivityTask({ - taskToken, - start: { - activityType: 'notFound', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter), - }, - }); - t.regex( - result?.failed?.failure?.message ?? '', - /^Activity function notFound is not registered on this Worker, available activities: \[.*"progressiveSleep".*\]/ - ); - }); -}); - -test('Worker fails activity with proper message if activity info contains null ScheduledTime', async (t) => { - const worker = isolateFreeWorker({ - ...defaultOptions, - activities: { - async dummy(): Promise {}, - }, - }); - t.context.worker = worker; - - await runWorker(t, async () => { - const taskToken = Buffer.from(uuid4()); - const { result } = await worker.native.runActivityTask({ - taskToken, - start: { - activityType: 'dummy', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter), - scheduledTime: null, - }, - }); - t.is(worker.getState(), 'RUNNING'); - t.is(result?.failed?.failure?.applicationFailureInfo?.type, 'TypeError'); - t.is(result?.failed?.failure?.message, 'Expected scheduledTime to be a timestamp, got null'); - t.true(/worker\.[jt]s/.test(result?.failed?.failure?.stackTrace ?? '')); - }); -}); - -test('Worker fails activity task if interceptor factory throws', async (t) => { - const worker = isolateFreeWorker({ - ...defaultOptions, - activities: { - async dummy(): Promise {}, - }, - interceptors: { - activity: [ - () => { - throw new Error('I am a bad interceptor'); - }, - ], - }, - }); - t.context.worker = worker; - - await runWorker(t, async () => { - const taskToken = Buffer.from(uuid4()); - const { result } = await worker.native.runActivityTask({ - taskToken, - start: { - activityType: 'dummy', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter), - }, - }); - t.is(worker.getState(), 'RUNNING'); - t.is(result?.failed?.failure?.applicationFailureInfo?.type, 'Error'); - t.is(result?.failed?.failure?.message, 'I am a bad interceptor'); - t.true(/test-worker-activities\.[tj]s/.test(result?.failed?.failure?.stackTrace ?? '')); - }); -}); - -test('Non ApplicationFailure TemporalFailures thrown from Activity are wrapped with ApplicationFailure', async (t) => { - const worker = isolateFreeWorker({ - ...defaultOptions, - activities: { - async throwTemporalFailure() { - throw new TemporalFailure('I should be valid'); - }, - }, - }); - t.context.worker = worker; - - await runWorker(t, async () => { - const taskToken = Buffer.from(uuid4()); - const { result } = await worker.native.runActivityTask({ - taskToken, - start: { - activityType: 'throwTemporalFailure', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter), - }, - }); - t.is(result?.failed?.failure?.applicationFailureInfo?.type, 'TemporalFailure'); - }); -}); - -test('nextRetryDelay in activity failures is propagated to Core', async (t) => { - const worker = isolateFreeWorker({ - ...defaultOptions, - activities: { - async throwNextDelayFail() { - throw ApplicationFailure.create({ - message: 'Enchi cat', - nextRetryDelay: '1s', - }); - }, - }, - }); - t.context.worker = worker; - - await runWorker(t, async () => { - const taskToken = Buffer.from(uuid4()); - const { result } = await worker.native.runActivityTask({ - taskToken, - start: { - activityType: 'throwNextDelayFail', - workflowExecution: { workflowId: 'wfid', runId: 'runId' }, - input: toPayloads(defaultPayloadConverter), - }, - }); - t.deepEqual(result?.failed?.failure?.applicationFailureInfo?.nextRetryDelay, Duration.create(msToTs('1s'))); - }); -}); diff --git a/packages/test/src/test-worker-debug-mode.ts b/packages/test/src/test-worker-debug-mode.ts deleted file mode 100644 index 637b45cef..000000000 --- a/packages/test/src/test-worker-debug-mode.ts +++ /dev/null @@ -1,22 +0,0 @@ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { WorkflowClient } from '@temporalio/client'; -import { defaultOptions } from './mock-native-worker'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { successString } from './workflows'; - -if (RUN_INTEGRATION_TESTS) { - test('Worker works in debugMode', async (t) => { - // To debug Workflows with this worker run the test with `ava debug` and add breakpoints to your Workflows - const taskQueue = 'debug-mode'; - const worker = await Worker.create({ ...defaultOptions, taskQueue, debugMode: true }); - const client = new WorkflowClient(); - const result = await worker.runUntil( - client.execute(successString, { - workflowId: uuid4(), - taskQueue, - }) - ); - t.is(result, 'success'); - }); -} diff --git a/packages/test/src/test-worker-deployment-versioning.ts b/packages/test/src/test-worker-deployment-versioning.ts deleted file mode 100644 index fdac331cc..000000000 --- a/packages/test/src/test-worker-deployment-versioning.ts +++ /dev/null @@ -1,468 +0,0 @@ -/** - * Tests worker-deployment-based versioning - * - * @module - */ -import assert from 'assert'; -import { randomUUID } from 'crypto'; -import asyncRetry from 'async-retry'; -import { ExecutionContext } from 'ava'; -import { Client } from '@temporalio/client'; -import { toCanonicalString, WorkerDeploymentVersion } from '@temporalio/common'; -import { temporal } from '@temporalio/proto'; -import { Worker } from './helpers'; -import { Context, makeTestFunction } from './helpers-integration'; -import { unblockSignal, versionQuery } from './workflows'; - -const test = makeTestFunction({ workflowsPath: __filename }); - -test('Worker deployment based versioning', async (t) => { - const taskQueue = 'worker-deployment-based-versioning-' + randomUUID(); - const deploymentName = 'deployment-' + randomUUID(); - const { client, nativeConnection } = t.context.env; - - const w1DeploymentVersion = { - buildId: '1.0', - deploymentName, - }; - const w2DeploymentVersion = { - buildId: '2.0', - deploymentName, - }; - const w3DeploymentVersion = { - buildId: '3.0', - deploymentName, - }; - - const worker1 = await Worker.create({ - workflowsPath: require.resolve('./deployment-versioning-v1'), - taskQueue, - workerDeploymentOptions: { - useWorkerVersioning: true, - version: w1DeploymentVersion, - defaultVersioningBehavior: 'PINNED', - }, - connection: nativeConnection, - }); - const worker1Promise = worker1.run(); - worker1Promise.catch((err) => { - t.fail('Worker 1.0 run error: ' + err); - }); - - const worker2 = await Worker.create({ - workflowsPath: require.resolve('./deployment-versioning-v2'), - taskQueue, - workerDeploymentOptions: { - useWorkerVersioning: true, - version: w2DeploymentVersion, - defaultVersioningBehavior: 'PINNED', - }, - connection: nativeConnection, - }); - const worker2Promise = worker2.run(); - worker2Promise.catch((err) => { - t.fail('Worker 2.0 run error: ' + err); - }); - - const worker3 = await Worker.create({ - workflowsPath: require.resolve('./deployment-versioning-v3'), - taskQueue, - workerDeploymentOptions: { - useWorkerVersioning: true, - version: w3DeploymentVersion, - defaultVersioningBehavior: 'PINNED', - }, - connection: nativeConnection, - }); - const worker3Promise = worker3.run(); - worker3Promise.catch((err) => { - t.fail('Worker 3.0 run error: ' + err); - }); - - // Wait for worker 1 to be visible and set as current version - const describeResp1 = await waitUntilWorkerDeploymentVisible(client, w1DeploymentVersion); - await setCurrentDeploymentVersion(client, describeResp1.conflictToken, w1DeploymentVersion); - - // Start workflow 1 which will use the 1.0 worker on auto-upgrade - const wf1 = await client.workflow.start('deploymentVersioning', { - taskQueue, - workflowId: 'deployment-versioning-v1-' + randomUUID(), - }); - const state1 = await wf1.query(versionQuery); - assert.equal(state1, 'v1'); - - // Wait for worker 2 to be visible and set as current version - const describeResp2 = await waitUntilWorkerDeploymentVisible(client, w2DeploymentVersion); - await setCurrentDeploymentVersion(client, describeResp2.conflictToken, w2DeploymentVersion); - - // Start workflow 2 which will use the 2.0 worker pinned - const wf2 = await client.workflow.start('deploymentVersioning', { - taskQueue, - workflowId: 'deployment-versioning-v2-' + randomUUID(), - }); - const state2 = await wf2.query(versionQuery); - assert.equal(state2, 'v2'); - - // Wait for worker 3 to be visible and set as current version - const describeResp3 = await waitUntilWorkerDeploymentVisible(client, w3DeploymentVersion); - await setCurrentDeploymentVersion(client, describeResp3.conflictToken, w3DeploymentVersion); - - // Start workflow 3 which will use the 3.0 worker on auto-upgrade - const wf3 = await client.workflow.start('deploymentVersioning', { - taskQueue, - workflowId: 'deployment-versioning-v3-' + randomUUID(), - }); - const state3 = await wf3.query(versionQuery); - assert.equal(state3, 'v3'); - - // Signal all workflows to finish - await wf1.signal(unblockSignal); - await wf2.signal(unblockSignal); - await wf3.signal(unblockSignal); - - const res1 = await wf1.result(); - const res2 = await wf2.result(); - const res3 = await wf3.result(); - - assert.equal(res1, 'version-v3'); - assert.equal(res2, 'version-v2'); - assert.equal(res3, 'version-v3'); - - worker1.shutdown(); - worker2.shutdown(); - worker3.shutdown(); - await worker1Promise; - await worker2Promise; - await worker3Promise; - t.pass(); -}); - -test('Worker deployment based versioning with ramping', async (t) => { - const taskQueue = 'worker-deployment-based-ramping-' + randomUUID(); - const deploymentName = 'deployment-ramping-' + randomUUID(); - const { client, nativeConnection } = t.context.env; - - const v1 = { - buildId: '1.0', - deploymentName, - }; - const v2 = { - buildId: '2.0', - deploymentName, - }; - - const worker1 = await Worker.create({ - workflowsPath: require.resolve('./deployment-versioning-v1'), - taskQueue, - workerDeploymentOptions: { - useWorkerVersioning: true, - version: v1, - defaultVersioningBehavior: 'PINNED', - }, - connection: nativeConnection, - }); - const worker1Promise = worker1.run(); - worker1Promise.catch((err) => { - t.fail('Worker 1.0 run error: ' + err); - }); - - const worker2 = await Worker.create({ - workflowsPath: require.resolve('./deployment-versioning-v2'), - taskQueue, - workerDeploymentOptions: { - useWorkerVersioning: true, - version: v2, - defaultVersioningBehavior: 'PINNED', - }, - connection: nativeConnection, - }); - const worker2Promise = worker2.run(); - worker2Promise.catch((err) => { - t.fail('Worker 2.0 run error: ' + err); - }); - - // Wait for worker deployments to be visible - await waitUntilWorkerDeploymentVisible(client, v1); - const describeResp = await waitUntilWorkerDeploymentVisible(client, v2); - - // Set current version to v1 and ramp v2 to 100% - let conflictToken = (await setCurrentDeploymentVersion(client, describeResp.conflictToken, v1)).conflictToken; - conflictToken = (await setRampingVersion(client, conflictToken, v2, 100)).conflictToken; - - // Run workflows and verify they run on v2 - for (let i = 0; i < 3; i++) { - const wf = await client.workflow.start('deploymentVersioning', { - taskQueue, - workflowId: `versioning-ramp-100-${i}-${randomUUID()}`, - }); - await wf.signal(unblockSignal); - const res = await wf.result(); - assert.equal(res, 'version-v2'); - } - - // Set ramp to 0, expecting workflows to run on v1 - conflictToken = (await setRampingVersion(client, conflictToken, v2, 0)).conflictToken; - for (let i = 0; i < 3; i++) { - const wf = await client.workflow.start('deploymentVersioning', { - taskQueue, - workflowId: `versioning-ramp-0-${i}-${randomUUID()}`, - }); - await wf.signal(unblockSignal); - const res = await wf.result(); - assert.equal(res, 'version-v1'); - } - - // Set ramp to 50 and eventually verify workflows run on both versions - await setRampingVersion(client, conflictToken, v2, 50); - const seenResults = new Set(); - - const runAndRecord = async () => { - const wf = await client.workflow.start('deploymentVersioning', { - taskQueue, - workflowId: `versioning-ramp-50-${randomUUID()}`, - }); - await wf.signal(unblockSignal); - return await wf.result(); - }; - - await asyncRetry( - async () => { - const res = await runAndRecord(); - seenResults.add(res); - if (!seenResults.has('version-v1') || !seenResults.has('version-v2')) { - throw new Error('Not all versions seen yet'); - } - }, - { maxTimeout: 1000, retries: 20 } - ); - - worker1.shutdown(); - worker2.shutdown(); - await worker1Promise; - await worker2Promise; - t.pass(); -}); - -async function testWorkerDeploymentWithDynamicBehavior( - t: ExecutionContext, - workflowName: string, - expectedResult: string -) { - if (t.context.env.supportsTimeSkipping) { - t.pass("Test Server doesn't support worker deployments"); - return; - } - - const taskQueue = 'worker-deployment-dynamic-' + randomUUID(); - const deploymentName = 'deployment-dynamic-' + randomUUID(); - const { client, nativeConnection } = t.context.env; - - const version = { - buildId: '1.0', - deploymentName, - }; - - const worker = await Worker.create({ - workflowsPath: require.resolve('./deployment-versioning-v1'), - taskQueue, - workerDeploymentOptions: { - useWorkerVersioning: true, - version, - defaultVersioningBehavior: 'AUTO_UPGRADE', - }, - connection: nativeConnection, - }); - - const workerPromise = worker.run(); - workerPromise.catch((err) => { - t.fail('Worker run error: ' + err); - }); - - const describeResp = await waitUntilWorkerDeploymentVisible(client, version); - await setCurrentDeploymentVersion(client, describeResp.conflictToken, version); - - const wf = await client.workflow.start(workflowName, { - taskQueue, - workflowId: 'dynamic-workflow-versioning-' + randomUUID(), - }); - - const result = await wf.result(); - assert.equal(result, expectedResult); - - const history = await wf.fetchHistory(); - const hasPinnedVersioningBehavior = history.events!.some( - (event) => - event.workflowTaskCompletedEventAttributes && - event.workflowTaskCompletedEventAttributes.versioningBehavior === - temporal.api.enums.v1.VersioningBehavior.VERSIONING_BEHAVIOR_PINNED - ); - assert.ok(hasPinnedVersioningBehavior, 'Expected workflow to use pinned versioning behavior'); - - worker.shutdown(); - await workerPromise; - t.pass(); -} - -test('Worker deployment with dynamic workflow static behavior', async (t) => { - await testWorkerDeploymentWithDynamicBehavior(t, 'cooldynamicworkflow', 'dynamic'); -}); - -test('Worker deployment with behavior in getter', async (t) => { - await testWorkerDeploymentWithDynamicBehavior(t, 'usesGetter', 'usesGetter'); -}); - -test('Workflows can use default versioning behavior', async (t) => { - const taskQueue = 'task-queue-default-versioning-' + randomUUID(); - const deploymentName = 'deployment-default-versioning-' + randomUUID(); - const { client, nativeConnection } = t.context.env; - - const workerV1 = { - buildId: '1.0', - deploymentName, - }; - - const worker = await Worker.create({ - workflowsPath: require.resolve('./deployment-versioning-no-annotations'), - taskQueue, - workerDeploymentOptions: { - useWorkerVersioning: true, - version: workerV1, - defaultVersioningBehavior: 'PINNED', - }, - connection: nativeConnection, - }); - - const workerPromise = worker.run(); - workerPromise.catch((err) => { - t.fail('Worker run error: ' + err); - }); - - const describeResp = await waitUntilWorkerDeploymentVisible(client, workerV1); - await setCurrentDeploymentVersion(client, describeResp.conflictToken, workerV1); - - const wf = await client.workflow.start('noVersioningAnnotationWorkflow', { - taskQueue, - workflowId: 'default-versioning-behavior-' + randomUUID(), - }); - - await wf.result(); - - const history = await wf.fetchHistory(); - const hasPinnedVersioningBehavior = history.events!.some( - (event) => - event.workflowTaskCompletedEventAttributes && - event.workflowTaskCompletedEventAttributes.versioningBehavior === - temporal.api.enums.v1.VersioningBehavior.VERSIONING_BEHAVIOR_PINNED - ); - assert.ok(hasPinnedVersioningBehavior, 'Expected workflow to use pinned versioning behavior'); - - worker.shutdown(); - await workerPromise; - t.pass(); -}); - -test('Workflow versioningOverride overrides default versioning behavior', async (t) => { - const taskQueue = 'task-queue-versioning-override-' + randomUUID(); - const { client, nativeConnection } = t.context.env; - - const workerV1 = { - buildId: '1.0', - deploymentName: 'deployment-versioning-override-' + randomUUID(), - }; - - const worker1 = await Worker.create({ - workflowsPath: require.resolve('./deployment-versioning-v1'), - taskQueue, - workerDeploymentOptions: { - useWorkerVersioning: true, - version: workerV1, - defaultVersioningBehavior: 'AUTO_UPGRADE', - }, - connection: nativeConnection, - }); - const worker1Promise = worker1.run(); - worker1Promise.catch((err) => { - t.fail('Worker 1.0 run error: ' + err); - }); - - // Wait for workers to be visible and set current version to v1 - const describeResp = await waitUntilWorkerDeploymentVisible(client, workerV1); - await setCurrentDeploymentVersion(client, describeResp.conflictToken, workerV1); - - // Start workflow with PINNED to v1 versioningOverride - should use v1 despite AUTO_UPGRADE default - const wfPinned = await client.workflow.start('deploymentVersioning', { - taskQueue, - workflowId: 'versioning-override-pinned-v1-' + randomUUID(), - versioningOverride: { - pinnedTo: workerV1, - }, - }); - const statePinned = await wfPinned.query(versionQuery); - assert.equal(statePinned, 'v1'); - - await wfPinned.signal(unblockSignal); - - // Get results and check versioning behavior - const historyPinned = await wfPinned.fetchHistory(); - const hasPinnedVersioningBehavior = historyPinned.events!.some( - (event) => - event.workflowExecutionStartedEventAttributes?.versioningOverride?.behavior === - temporal.api.enums.v1.VersioningBehavior.VERSIONING_BEHAVIOR_PINNED || - event.workflowExecutionStartedEventAttributes?.versioningOverride?.pinned != null - ); - assert.ok(hasPinnedVersioningBehavior, 'Expected workflow to use pinned versioning behavior'); - - const resPinned = await wfPinned.result(); - assert.equal(resPinned, 'version-v1'); - - worker1.shutdown(); - await worker1Promise; - t.pass(); -}); - -async function setRampingVersion( - client: Client, - conflictToken: Uint8Array, - version: WorkerDeploymentVersion, - percentage: number -) { - return await client.workflowService.setWorkerDeploymentRampingVersion({ - namespace: client.options.namespace, - deploymentName: version.deploymentName, - version: toCanonicalString(version), - conflictToken, - percentage, - }); -} - -async function waitUntilWorkerDeploymentVisible(client: Client, version: WorkerDeploymentVersion) { - return await asyncRetry( - async () => { - const resp = await client.workflowService.describeWorkerDeployment({ - namespace: client.options.namespace, - deploymentName: version.deploymentName, - }); - const isVersionVisible = resp.workerDeploymentInfo!.versionSummaries!.some( - (vs) => vs.version === toCanonicalString(version) - ); - if (!isVersionVisible) { - throw new Error('Version not visible yet'); - } - return resp; - }, - { maxTimeout: 1000, retries: 10 } - ); -} - -async function setCurrentDeploymentVersion( - client: Client, - conflictToken: Uint8Array, - version: WorkerDeploymentVersion -) { - return await client.workflowService.setWorkerDeploymentCurrentVersion({ - namespace: client.options.namespace, - deploymentName: version.deploymentName, - version: toCanonicalString(version), - conflictToken, - }); -} diff --git a/packages/test/src/test-worker-exposes-abortcontroller.ts b/packages/test/src/test-worker-exposes-abortcontroller.ts deleted file mode 100644 index dfb0e4ea5..000000000 --- a/packages/test/src/test-worker-exposes-abortcontroller.ts +++ /dev/null @@ -1,22 +0,0 @@ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { Client } from '@temporalio/client'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { defaultOptions } from './mock-native-worker'; -import { abortController } from './workflows'; - -if (RUN_INTEGRATION_TESTS) { - test(`Worker runtime exposes AbortController as a global`, async (t) => { - const worker = await Worker.create({ ...defaultOptions, taskQueue: 'test-worker-exposes-abortcontroller' }); - const client = new Client(); - const result = await worker.runUntil( - client.workflow.execute(abortController, { - args: [], - taskQueue: 'test-worker-exposes-abortcontroller', - workflowId: uuid4(), - workflowExecutionTimeout: '5s', - }) - ); - t.is(result, 'abort successful'); - }); -} diff --git a/packages/test/src/test-worker-exposes-textencoderdecoder.ts b/packages/test/src/test-worker-exposes-textencoderdecoder.ts deleted file mode 100644 index e3363ea6b..000000000 --- a/packages/test/src/test-worker-exposes-textencoderdecoder.ts +++ /dev/null @@ -1,36 +0,0 @@ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { Client } from '@temporalio/client'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { defaultOptions } from './mock-native-worker'; -import { textEncoderDecoder, textEncoderDecoderFromImport } from './workflows'; - -if (RUN_INTEGRATION_TESTS) { - test('Worker runtime exposes TextEncoder and TextDecoder as globals', async (t) => { - const worker = await Worker.create({ ...defaultOptions, taskQueue: 'test-worker-exposes-textencoderdecoder' }); - const client = new Client(); - const result = await worker.runUntil( - client.workflow.execute(textEncoderDecoder, { - args: ['a string that will be encoded and decoded'], - taskQueue: 'test-worker-exposes-textencoderdecoder', - workflowId: uuid4(), - workflowExecutionTimeout: '5s', - }) - ); - t.is(result, 'a string that will be encoded and decoded'); - }); - - test('Worker runtime exposes TextEncoder and TextDecoder as overrided import of util', async (t) => { - const worker = await Worker.create({ ...defaultOptions, taskQueue: 'test-worker-exposes-textencoderdecoder' }); - const client = new Client(); - const result = await worker.runUntil( - client.workflow.execute(textEncoderDecoderFromImport, { - args: ['a string that will be encoded and decoded'], - taskQueue: 'test-worker-exposes-textencoderdecoder', - workflowId: uuid4(), - workflowExecutionTimeout: '5s', - }) - ); - t.is(result, 'a string that will be encoded and decoded'); - }); -} diff --git a/packages/test/src/test-worker-heartbeats.ts b/packages/test/src/test-worker-heartbeats.ts deleted file mode 100644 index df62a107b..000000000 --- a/packages/test/src/test-worker-heartbeats.ts +++ /dev/null @@ -1,174 +0,0 @@ -import test from 'ava'; -import { firstValueFrom, Subject } from 'rxjs'; -import { v4 as uuid4 } from 'uuid'; -import { coresdk } from '@temporalio/proto'; -import { Context } from '@temporalio/activity'; -import { isolateFreeWorker, Worker } from './mock-native-worker'; - -async function runActivity(worker: Worker, callback?: (completion: coresdk.ActivityTaskCompletion) => void) { - const taskToken = Buffer.from(uuid4()); - await worker.runUntil(async () => { - const completion = await worker.native.runActivityTask({ - taskToken, - start: { activityType: 'rapidHeartbeater', workflowExecution: { workflowId: 'wfid', runId: 'runid' } }, - }); - callback?.(completion); - }); -} - -test('Worker stores last heartbeat if flushing is in progress', async (t) => { - const subj = new Subject(); - - const worker = isolateFreeWorker({ - taskQueue: 'unused', - activities: { - async rapidHeartbeater() { - Context.current().heartbeat(1); - // These details should be overriden by `3` - Context.current().heartbeat(2); - // This details should be flushed - Context.current().heartbeat(3); - // Prevent activity from completing - await firstValueFrom(subj); - }, - }, - }); - - const heartbeatsSeen = Array(); - worker.native.activityHeartbeatCallback = (_tt, details) => { - heartbeatsSeen.push(details); - if (heartbeatsSeen.length === 2) { - subj.next(); - } - }; - await runActivity(worker); - t.deepEqual(heartbeatsSeen, [1, 3]); -}); - -test('Worker flushes last heartbeat if activity fails', async (t) => { - const worker = isolateFreeWorker({ - taskQueue: 'unused', - activities: { - async rapidHeartbeater() { - Context.current().heartbeat(1); - // These details should be overriden by `3` - Context.current().heartbeat(2); - // This details should be flushed - Context.current().heartbeat(3); - // Fail - throw new Error(); - }, - }, - }); - - const heartbeatsSeen = Array(); - worker.native.activityHeartbeatCallback = (_tt, details) => { - heartbeatsSeen.push(details); - }; - await runActivity(worker); - t.deepEqual(heartbeatsSeen, [1, 3]); -}); - -test('Worker ignores last heartbeat if activity succeeds', async (t) => { - const subj = new Subject(); - - const activityCompletePromise = firstValueFrom(subj); - const worker = isolateFreeWorker({ - taskQueue: 'unused', - dataConverter: { - payloadCodecs: [ - { - async encode(p) { - // Don't complete encoding heartbeat details until activity has completed. - // data will be undefined when this method gets the activity result for completion. - if (p[0].data !== undefined) { - await activityCompletePromise; - } - return p; - }, - async decode(p) { - return p; - }, - }, - ], - }, - activities: { - async rapidHeartbeater() { - Context.current().heartbeat(1); - Context.current().heartbeat(2); - Context.current().heartbeat(3); - }, - }, - }); - - const heartbeatsSeen = Array(); - worker.native.activityHeartbeatCallback = (_tt, details) => { - heartbeatsSeen.push(details); - }; - await runActivity(worker, () => subj.next()); - t.deepEqual(heartbeatsSeen, [1]); -}); - -test('Activity gets cancelled if heartbeat fails', async (t) => { - const worker = isolateFreeWorker({ - taskQueue: 'unused', - dataConverter: { - payloadCodecs: [ - { - async encode(p) { - // Fail to encode heartbeat details. - // data will be undefined when this method gets the activity result for completion. - if (p[0].data !== undefined) { - throw new Error('Refuse to encode data for test'); - } - return p; - }, - async decode(p) { - return p; - }, - }, - ], - }, - activities: { - async rapidHeartbeater() { - Context.current().heartbeat(1); - await Context.current().cancelled; - }, - }, - }); - - const heartbeatsSeen = Array(); - worker.native.activityHeartbeatCallback = (_tt, details) => { - heartbeatsSeen.push(details); - }; - await runActivity(worker, (completion) => { - t.is(completion.result?.failed?.failure?.message, 'HEARTBEAT_DETAILS_CONVERSION_FAILED'); - }); - t.deepEqual(heartbeatsSeen, []); -}); - -test('No heartbeat is emitted with rogue activity', async (t) => { - const subj = new Subject(); - let cx: Context | undefined = undefined; - - const worker = isolateFreeWorker({ - taskQueue: 'unused', - activities: { - async rapidHeartbeater() { - cx = Context.current(); - Context.current().heartbeat(1); - }, - }, - }); - - const heartbeatsSeen = Array(); - worker.native.activityHeartbeatCallback = (_tt, details) => { - heartbeatsSeen.push(details); - }; - await runActivity(worker, () => { - subj.next(); - t.truthy(cx); - cx?.heartbeat(2); - }); - t.deepEqual(heartbeatsSeen, [1]); -}); diff --git a/packages/test/src/test-worker-lifecycle.ts b/packages/test/src/test-worker-lifecycle.ts index e6e98f548..b85486113 100644 --- a/packages/test/src/test-worker-lifecycle.ts +++ b/packages/test/src/test-worker-lifecycle.ts @@ -7,7 +7,7 @@ import { setTimeout } from 'timers/promises'; import { randomUUID } from 'crypto'; import test from 'ava'; -import { Runtime, PromiseCompletionTimeoutError } from '@temporalio/worker'; +import { Runtime, PromiseCompletionTimeoutError, makeTelemetryFilterString, DefaultLogger } from '@temporalio/worker'; import { TransportError, UnexpectedError } from '@temporalio/worker/lib/errors'; import { Client } from '@temporalio/client'; import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; @@ -35,6 +35,12 @@ if (RUN_INTEGRATION_TESTS) { }); test.serial("Worker.runUntil doesn't hang if provided promise survives to Worker's shutdown", async (t) => { + const logger = new DefaultLogger('DEBUG'); + t.is(Runtime._instance, undefined); + Runtime.install({ + telemetryOptions: { tracingFilter: makeTelemetryFilterString({ core: 'DEBUG' }) }, + logger, + }); const worker = await Worker.create({ ...defaultOptions, taskQueue: t.title.replace(/ /g, '_'), diff --git a/packages/test/src/test-worker-no-activities.ts b/packages/test/src/test-worker-no-activities.ts deleted file mode 100644 index a51f2b5c8..000000000 --- a/packages/test/src/test-worker-no-activities.ts +++ /dev/null @@ -1,22 +0,0 @@ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { WorkflowClient } from '@temporalio/client'; -import { defaultOptions } from './mock-native-worker'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { successString } from './workflows'; - -if (RUN_INTEGRATION_TESTS) { - test('Worker functions when asked not to run Activities', async (t) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { activities, taskQueue, ...rest } = defaultOptions; - const worker = await Worker.create({ taskQueue: 'only-workflows', ...rest }); - const client = new WorkflowClient(); - const result = await worker.runUntil( - client.execute(successString, { - workflowId: uuid4(), - taskQueue: 'only-workflows', - }) - ); - t.is(result, 'success'); - }); -} diff --git a/packages/test/src/test-worker-no-workflows.ts b/packages/test/src/test-worker-no-workflows.ts deleted file mode 100644 index df11605df..000000000 --- a/packages/test/src/test-worker-no-workflows.ts +++ /dev/null @@ -1,25 +0,0 @@ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { WorkflowClient } from '@temporalio/client'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { defaultOptions } from './mock-native-worker'; -import { runActivityInDifferentTaskQueue } from './workflows'; - -if (RUN_INTEGRATION_TESTS) { - test('Worker functions when asked not to run Workflows', async (t) => { - const { activities } = defaultOptions; - const workflowlessWorker = await Worker.create({ taskQueue: 'only-activities', activities }); - const normalWorker = await Worker.create({ ...defaultOptions, taskQueue: 'also-workflows' }); - const client = new WorkflowClient(); - const result = await normalWorker.runUntil( - workflowlessWorker.runUntil( - client.execute(runActivityInDifferentTaskQueue, { - args: ['only-activities'], - taskQueue: 'also-workflows', - workflowId: uuid4(), - }) - ) - ); - t.is(result, 'hi'); - }); -} diff --git a/packages/test/src/test-worker-poller-autoscale.ts b/packages/test/src/test-worker-poller-autoscale.ts index edd96c8b9..669255066 100644 --- a/packages/test/src/test-worker-poller-autoscale.ts +++ b/packages/test/src/test-worker-poller-autoscale.ts @@ -48,7 +48,7 @@ test.serial('Can run autoscaling polling worker', async (t) => { const activity_pollers = matches.filter((l) => l.includes('activity_task')); t.is(activity_pollers.length, 1, 'Should have exactly one activity poller metric'); t.true(activity_pollers[0].endsWith('2'), 'Activity poller count should be 2'); - const workflow_pollers = matches.filter((l) => l.includes('workflow_task')); + const workflow_pollers = matches.filter((l) => l.includes('workflow_task') && l.includes(taskQueue)); t.is(workflow_pollers.length, 2, 'Should have exactly two workflow poller metrics (sticky and non-sticky)'); // There's sticky & non-sticky pollers, and they may have a count of 1 or 2 depending on diff --git a/packages/test/src/test-worker-tuner.ts b/packages/test/src/test-worker-tuner.ts deleted file mode 100644 index bec5f6532..000000000 --- a/packages/test/src/test-worker-tuner.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { ExecutionContext } from 'ava'; -import { - CustomSlotSupplier, - ResourceBasedTunerOptions, - SlotInfo, - SlotMarkUsedContext, - SlotPermit, - SlotReleaseContext, - SlotReserveContext, -} from '@temporalio/worker'; -import * as wf from '@temporalio/workflow'; -import { helpers, makeTestFunction } from './helpers-integration'; - -const test = makeTestFunction({ workflowsPath: __filename }); - -const activities = { - async hiActivity(): Promise { - return 'hi'; - }, -}; - -const proxyActivities = wf.proxyActivities({ - startToCloseTimeout: '5s', -}); - -export async function successString(): Promise { - return 'success'; -} - -export async function doesActivity(): Promise { - await proxyActivities.hiActivity(); - return 'success'; -} - -test('Worker can run with resource based tuner', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const resourceBasedTunerOptions: ResourceBasedTunerOptions = { - targetCpuUsage: 0.6, - targetMemoryUsage: 0.6, - }; - const worker = await createWorker({ - tuner: { - tunerOptions: resourceBasedTunerOptions, - activityTaskSlotOptions: { - minimumSlots: 2, - maximumSlots: 10, - rampThrottle: 20, - }, - }, - }); - const result = await worker.runUntil(executeWorkflow(successString)); - t.is(result, 'success'); -}); - -test('Worker can run with mixed slot suppliers in tuner', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const resourceBasedTunerOptions: ResourceBasedTunerOptions = { - targetCpuUsage: 0.5, - targetMemoryUsage: 0.5, - }; - const worker = await createWorker({ - tuner: { - activityTaskSlotSupplier: { - type: 'resource-based', - minimumSlots: 2, - maximumSlots: 10, - rampThrottle: 20, - tunerOptions: resourceBasedTunerOptions, - }, - workflowTaskSlotSupplier: { - type: 'fixed-size', - numSlots: 10, - }, - localActivityTaskSlotSupplier: { - type: 'fixed-size', - numSlots: 10, - }, - nexusTaskSlotSupplier: { - type: 'fixed-size', - numSlots: 10, - }, - }, - }); - const result = await worker.runUntil(executeWorkflow(successString)); - t.is(result, 'success'); -}); - -test('Can assume defaults for resource based options', async (t) => { - const { createWorker } = helpers(t); - const resourceBasedTunerOptions: ResourceBasedTunerOptions = { - targetCpuUsage: 0.6, - targetMemoryUsage: 0.6, - }; - - // With explicit tuner type - const worker1 = await createWorker({ - tuner: { - tunerOptions: resourceBasedTunerOptions, - activityTaskSlotOptions: { - minimumSlots: 1, - }, - localActivityTaskSlotOptions: { - maximumSlots: 10, - }, - workflowTaskSlotOptions: { - rampThrottle: 20, - }, - }, - }); - await worker1.runUntil(Promise.resolve()); - - // With mixed slot suppliers - const worker2 = await createWorker({ - tuner: { - activityTaskSlotSupplier: { - type: 'resource-based', - tunerOptions: resourceBasedTunerOptions, - minimumSlots: 3, - }, - workflowTaskSlotSupplier: { - type: 'fixed-size', - numSlots: 40, - }, - localActivityTaskSlotSupplier: { - type: 'resource-based', - tunerOptions: resourceBasedTunerOptions, - maximumSlots: 50, - }, - nexusTaskSlotSupplier: { - type: 'resource-based', - tunerOptions: resourceBasedTunerOptions, - maximumSlots: 50, - }, - }, - }); - await worker2.runUntil(Promise.resolve()); - - t.pass(); -}); - -test('Cannot construct worker tuner with multiple different tuner options', async (t) => { - const { createWorker } = helpers(t); - const tunerOptions1: ResourceBasedTunerOptions = { - targetCpuUsage: 0.5, - targetMemoryUsage: 0.5, - }; - const tunerOptions2: ResourceBasedTunerOptions = { - targetCpuUsage: 0.9, - targetMemoryUsage: 0.9, - }; - const error = await t.throwsAsync(() => - createWorker({ - tuner: { - activityTaskSlotSupplier: { - type: 'resource-based', - tunerOptions: tunerOptions1, - }, - workflowTaskSlotSupplier: { - type: 'resource-based', - tunerOptions: tunerOptions2, - }, - localActivityTaskSlotSupplier: { - type: 'fixed-size', - numSlots: 10, - }, - nexusTaskSlotSupplier: { - type: 'fixed-size', - numSlots: 10, - }, - }, - }) - ); - t.is(error?.message, 'Cannot construct worker tuner with multiple different tuner options'); -}); - -class MySS implements CustomSlotSupplier { - readonly type = 'custom'; - reserved = 0; - released = 0; - markedUsed = 0; - releasedWithInfo = 0; - seenStickyFlags = new Set(); - seenSlotTypes = new Set(); - t: ExecutionContext; - - constructor(testCtx: ExecutionContext) { - this.t = testCtx; - } - - async reserveSlot(ctx: SlotReserveContext, _: AbortSignal): Promise { - // Ensure all fields are present - this.reserveAsserts(ctx); - return { isTry: false }; - } - - tryReserveSlot(ctx: SlotReserveContext): SlotPermit | null { - this.reserveAsserts(ctx); - return { isTry: true }; - } - - markSlotUsed(ctx: SlotMarkUsedContext): void { - this.t.truthy(ctx.slotInfo); - this.t.truthy(ctx.permit); - this.t.true((ctx.permit as any).isTry !== undefined); - this.markedUsed++; - } - - releaseSlot(ctx: SlotReleaseContext): void { - this.t.truthy(ctx.permit); - // Info may not be present for un-used slots - if (ctx.slotInfo !== undefined) { - this.releasedWithInfo++; - } - this.released++; - } - - private reserveAsserts(ctx: SlotReserveContext) { - // Ensure all fields are present - this.t.truthy(ctx.slotType); - this.seenSlotTypes.add(ctx.slotType); - this.t.truthy(ctx.taskQueue); - this.t.truthy(ctx.workerIdentity); - this.t.truthy(ctx.workerBuildId); // eslint-disable-line deprecation/deprecation - this.t.not(ctx.isSticky, undefined); - this.seenStickyFlags.add(ctx.isSticky); - this.reserved++; - } -} - -// FIXME: This test is flaky. To be reviewed at a later time. -test('Custom slot supplier works', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const slotSupplier = new MySS(t); - - const worker = await createWorker({ - activities, - tuner: { - workflowTaskSlotSupplier: slotSupplier, - activityTaskSlotSupplier: slotSupplier, - localActivityTaskSlotSupplier: slotSupplier, - nexusTaskSlotSupplier: slotSupplier, - }, - }); - const result = await worker.runUntil(executeWorkflow(doesActivity)); - t.is(result, 'success'); - - // All reserved slots will be released - make sure all calls made it through. - // t.is(slotSupplier.reserved, slotSupplier.released); - // FIXME: This assertion is flaky due to a possible race condition that happens during Core's shutdown process. - // For now, we just accept the fact that we may sometime terminate with one unreleased slot. - t.true(slotSupplier.reserved === slotSupplier.released || slotSupplier.reserved === slotSupplier.released + 1); - - t.is(slotSupplier.markedUsed, slotSupplier.releasedWithInfo); - // TODO: See if it makes sense to change core to lazily do LA reservation - t.like([...slotSupplier.seenSlotTypes].sort(), ['local-activity', 'activity', 'workflow'].sort()); - t.like([...slotSupplier.seenStickyFlags].sort(), [false, true].sort()); -}); - -class BlockingSlotSupplier implements CustomSlotSupplier { - readonly type = 'custom'; - - aborts = 0; - - async reserveSlot(_: SlotReserveContext, abortSignal: AbortSignal): Promise { - abortSignal.throwIfAborted(); - const abortPromise = new Promise((_, reject) => { - abortSignal.addEventListener('abort', () => { - this.aborts++; - reject(abortSignal.reason); - }); - }); - await abortPromise; - throw new Error('Should not reach here'); - } - - tryReserveSlot(_: SlotReserveContext): SlotPermit | null { - return null; - } - - markSlotUsed(_: SlotMarkUsedContext): void {} - - releaseSlot(_: SlotReleaseContext): void {} -} - -test('Custom slot supplier sees aborts', async (t) => { - const { createWorker } = helpers(t); - const slotSupplier = new BlockingSlotSupplier(); - - const worker = await createWorker({ - activities, - tuner: { - workflowTaskSlotSupplier: slotSupplier, - activityTaskSlotSupplier: slotSupplier, - localActivityTaskSlotSupplier: slotSupplier, - nexusTaskSlotSupplier: slotSupplier, - }, - }); - const runprom = worker.run(); - await new Promise((resolve) => setTimeout(resolve, 1000)); - worker.shutdown(); - await runprom; - t.true(slotSupplier.aborts > 0); -}); - -class ThrowingSlotSupplier implements CustomSlotSupplier { - readonly type = 'custom'; - - markedUsed = false; - - async reserveSlot(ctx: SlotReserveContext, _: AbortSignal): Promise { - // Give out one workflow tasks until one gets used - if (ctx.slotType === 'workflow' && !this.markedUsed) { - return {}; - } - throw new Error('I always throw'); - } - - tryReserveSlot(_: SlotReserveContext): SlotPermit | null { - throw new Error('I always throw'); - } - - markSlotUsed(_: SlotMarkUsedContext): void { - this.markedUsed = true; - throw new Error('I always throw'); - } - - releaseSlot(_: SlotReleaseContext): void { - throw new Error('I always throw'); - } -} - -test('Throwing slot supplier avoids blowing everything up', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const slotSupplier = new ThrowingSlotSupplier(); - - const worker = await createWorker({ - activities, - tuner: { - workflowTaskSlotSupplier: slotSupplier, - activityTaskSlotSupplier: slotSupplier, - localActivityTaskSlotSupplier: slotSupplier, - nexusTaskSlotSupplier: slotSupplier, - }, - }); - const result = await worker.runUntil(executeWorkflow(successString)); - t.is(result, 'success'); -}); - -class UndefinedSlotSupplier implements CustomSlotSupplier { - readonly type = 'custom'; - - async reserveSlot(_: SlotReserveContext, __: AbortSignal): Promise { - // I'm a bad cheater - return undefined as any; - } - - tryReserveSlot(_: SlotReserveContext): SlotPermit | null { - return undefined as any; - } - - markSlotUsed(_: SlotMarkUsedContext): void {} - - releaseSlot(_: SlotReleaseContext): void {} -} - -test('Undefined slot supplier avoids blowing everything up', async (t) => { - const { createWorker, executeWorkflow } = helpers(t); - const slotSupplier = new UndefinedSlotSupplier(); - - const worker = await createWorker({ - activities, - tuner: { - workflowTaskSlotSupplier: slotSupplier, - activityTaskSlotSupplier: slotSupplier, - localActivityTaskSlotSupplier: slotSupplier, - nexusTaskSlotSupplier: slotSupplier, - }, - }); - const result = await worker.runUntil(executeWorkflow(successString)); - t.is(result, 'success'); -}); diff --git a/packages/test/src/test-worker-versioning-unit.ts b/packages/test/src/test-worker-versioning-unit.ts deleted file mode 100644 index 84a9a98ed..000000000 --- a/packages/test/src/test-worker-versioning-unit.ts +++ /dev/null @@ -1,104 +0,0 @@ -import test from 'ava'; -import { reachabilityResponseFromProto, UnversionedBuildId } from '@temporalio/client/lib/task-queue-client'; -import { temporal } from '@temporalio/proto'; -import { Worker } from '@temporalio/worker'; - -const TaskReachability = temporal.api.enums.v1.TaskReachability; -const GetWorkerTaskReachabilityResponse = temporal.api.workflowservice.v1.GetWorkerTaskReachabilityResponse; - -test('Worker.create fails if useVersioning is true and not provided a buildId', async (t) => { - const err = await t.throwsAsync(() => - Worker.create({ - taskQueue: 'foo', - useVersioning: true, - }) - ); - t.true(err instanceof TypeError); - t.is(err?.message, 'Must provide a buildId if useVersioning is true'); -}); - -test('Worker versioning workers get appropriate tasks', async (t) => { - const res = reachabilityResponseFromProto( - GetWorkerTaskReachabilityResponse.create({ - buildIdReachability: [ - { - buildId: '2.0', - taskQueueReachability: [ - { - taskQueue: 'foo', - reachability: [TaskReachability.TASK_REACHABILITY_NEW_WORKFLOWS], - }, - ], - }, - { - buildId: '1.0', - taskQueueReachability: [ - { - taskQueue: 'foo', - reachability: [TaskReachability.TASK_REACHABILITY_OPEN_WORKFLOWS], - }, - ], - }, - { - buildId: '1.1', - taskQueueReachability: [ - { - taskQueue: 'foo', - reachability: [ - TaskReachability.TASK_REACHABILITY_EXISTING_WORKFLOWS, - TaskReachability.TASK_REACHABILITY_NEW_WORKFLOWS, - ], - }, - ], - }, - { - buildId: '0.1', - taskQueueReachability: [ - { - taskQueue: 'foo', - reachability: [TaskReachability.TASK_REACHABILITY_CLOSED_WORKFLOWS], - }, - ], - }, - { - buildId: 'unreachable', - taskQueueReachability: [ - { - taskQueue: 'foo', - reachability: [], - }, - ], - }, - { - buildId: 'badboi', - taskQueueReachability: [ - { - taskQueue: 'foo', - reachability: [TaskReachability.TASK_REACHABILITY_UNSPECIFIED], - }, - ], - }, - { - buildId: '', // Unversioned - taskQueueReachability: [ - { - taskQueue: 'foo', - reachability: [], - }, - ], - }, - ], - }) - ); - - console.warn(res.buildIdReachability); - t.deepEqual(res.buildIdReachability['2.0'].taskQueueReachability.foo, ['NEW_WORKFLOWS']); - t.deepEqual(res.buildIdReachability['1.0'].taskQueueReachability.foo, ['OPEN_WORKFLOWS']); - t.deepEqual(res.buildIdReachability['1.1'].taskQueueReachability.foo, ['EXISTING_WORKFLOWS', 'NEW_WORKFLOWS']); - t.deepEqual(res.buildIdReachability['0.1'].taskQueueReachability.foo, ['CLOSED_WORKFLOWS']); - t.deepEqual(res.buildIdReachability['unreachable'].taskQueueReachability.foo, []); - t.deepEqual(res.buildIdReachability['badboi'].taskQueueReachability.foo, ['NOT_FETCHED']); - t.deepEqual(res.buildIdReachability[UnversionedBuildId].taskQueueReachability.foo, []); - - t.pass(); -}); diff --git a/packages/test/src/test-worker-versioning.ts b/packages/test/src/test-worker-versioning.ts deleted file mode 100644 index 8c396a061..000000000 --- a/packages/test/src/test-worker-versioning.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Tests the client and worker functionality for the worker versioning feature. - * - * @module - */ -import assert from 'assert'; -import { randomUUID } from 'crypto'; -import anyTest, { ImplementationFn, TestFn } from 'ava'; -import { status } from '@grpc/grpc-js'; -import asyncRetry from 'async-retry'; -import { BuildIdNotFoundError, Client, UnversionedBuildId } from '@temporalio/client'; -import { DefaultLogger, Runtime } from '@temporalio/worker'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import * as activities from './activities'; -import { unblockSignal } from './workflows'; - -export interface Context { - client: Client; - doSkip: boolean; -} - -const test = anyTest as TestFn; -const withSkipper = test.macro<[ImplementationFn<[], Context>]>(async (t, fn) => { - if (t.context.doSkip) { - t.log('Skipped since this server does not support worker versioning'); - t.pass(); - return; - } - await fn(t); -}); - -if (RUN_INTEGRATION_TESTS) { - test.before(async (t) => { - Runtime.install({ logger: new DefaultLogger('DEBUG') }); - const client = new Client(); - // Test if this server supports worker versioning - let doSkip = false; - const taskQueue = 'test-worker-versioning' + randomUUID(); - try { - await client.taskQueue.updateBuildIdCompatibility(taskQueue, { - operation: 'addNewIdInNewDefaultSet', - buildId: '1.0', - }); - } catch (e: any) { - const cause = e.cause; - if (cause && (cause.code === status.PERMISSION_DENIED || cause.code === status.UNIMPLEMENTED)) { - doSkip = true; - } else { - throw e; - } - } - t.context = { - client, - doSkip, - }; - }); - - test('Worker versioning workers get appropriate tasks', withSkipper, async (t) => { - const taskQueue = 'worker-versioning-tasks-' + randomUUID(); - const wf1Id = 'worker-versioning-1-' + randomUUID(); - const wf2Id = 'worker-versioning-2-' + randomUUID(); - const client = t.context.client; - await client.taskQueue.updateBuildIdCompatibility(taskQueue, { - operation: 'addNewIdInNewDefaultSet', - buildId: '1.0', - }); - - const worker1 = await Worker.create({ - workflowsPath: require.resolve('./workflows'), - activities, - taskQueue, - buildId: '1.0', - useVersioning: true, - }); - const worker1Prom = worker1.run(); - worker1Prom.catch((err) => { - t.fail('Worker 1.0 run error: ' + JSON.stringify(err)); - }); - - const wf1 = await client.workflow.start('unblockOrCancel', { - taskQueue, - workflowId: wf1Id, - }); - await client.taskQueue.updateBuildIdCompatibility(taskQueue, { - operation: 'addNewIdInNewDefaultSet', - buildId: '2.0', - }); - const wf2 = await client.workflow.start('unblockOrCancel', { - taskQueue, - workflowId: wf2Id, - }); - await wf1.signal(unblockSignal); - - const worker2 = await Worker.create({ - workflowsPath: require.resolve('./workflows'), - activities, - taskQueue, - buildId: '2.0', - useVersioning: true, - }); - const worker2Prom = worker2.run(); - worker2Prom.catch((err) => { - t.fail('Worker 2.0 run error: ' + JSON.stringify(err)); - }); - - await wf2.signal(unblockSignal); - - await wf1.result(); - await wf2.result(); - - worker1.shutdown(); - worker2.shutdown(); - await worker1Prom; - await worker2Prom; - t.pass(); - }); - - test('Worker versioning client updates', withSkipper, async (t) => { - const taskQueue = 'worker-versioning-client-updates-' + randomUUID(); - const conn = t.context.client; - - await conn.taskQueue.updateBuildIdCompatibility(taskQueue, { - operation: 'addNewIdInNewDefaultSet', - buildId: '1.0', - }); - let resp = await conn.taskQueue.getBuildIdCompatability(taskQueue); - assert.equal(resp?.defaultBuildId, '1.0'); - - await conn.taskQueue.updateBuildIdCompatibility(taskQueue, { - operation: 'addNewCompatibleVersion', - buildId: '1.1', - existingCompatibleBuildId: '1.0', - }); - resp = await conn.taskQueue.getBuildIdCompatability(taskQueue); - assert.equal(resp?.defaultBuildId, '1.1'); - - // Target nonexistent build ID - await t.throwsAsync( - conn.taskQueue.updateBuildIdCompatibility(taskQueue, { - operation: 'addNewCompatibleVersion', - buildId: '1.2', - existingCompatibleBuildId: 'amnotreal', - }), - { message: /amnotreal not found/, instanceOf: BuildIdNotFoundError } - ); - - await conn.taskQueue.updateBuildIdCompatibility(taskQueue, { - operation: 'promoteBuildIdWithinSet', - buildId: '1.0', - }); - resp = await conn.taskQueue.getBuildIdCompatability(taskQueue); - assert.equal(resp?.defaultBuildId, '1.0'); - - await conn.taskQueue.updateBuildIdCompatibility(taskQueue, { - operation: 'addNewIdInNewDefaultSet', - buildId: '2.0', - }); - resp = await conn.taskQueue.getBuildIdCompatability(taskQueue); - assert.equal(resp?.defaultBuildId, '2.0'); - - await conn.taskQueue.updateBuildIdCompatibility(taskQueue, { - operation: 'promoteSetByBuildId', - buildId: '1.0', - }); - resp = await conn.taskQueue.getBuildIdCompatability(taskQueue); - assert.equal(resp?.defaultBuildId, '1.0'); - - await conn.taskQueue.updateBuildIdCompatibility(taskQueue, { - operation: 'mergeSets', - primaryBuildId: '2.0', - secondaryBuildId: '1.0', - }); - resp = await conn.taskQueue.getBuildIdCompatability(taskQueue); - assert.equal(resp?.defaultBuildId, '2.0'); - - await asyncRetry( - async () => { - const reachResp = await conn.taskQueue.getReachability({ buildIds: ['2.0', '1.0', '1.1'] }); - assert.deepEqual(reachResp.buildIdReachability['2.0']?.taskQueueReachability[taskQueue], ['NEW_WORKFLOWS']); - assert.deepEqual(reachResp.buildIdReachability['1.1']?.taskQueueReachability[taskQueue], []); - assert.deepEqual(reachResp.buildIdReachability['1.0']?.taskQueueReachability[taskQueue], []); - }, - { maxTimeout: 1000 } - ); - await asyncRetry( - async () => { - const reachResp = await conn.taskQueue.getReachability({ - buildIds: [UnversionedBuildId], - taskQueues: [taskQueue], - }); - assert.deepEqual(reachResp.buildIdReachability[UnversionedBuildId]?.taskQueueReachability[taskQueue], [ - 'NEW_WORKFLOWS', - ]); - }, - { maxTimeout: 1000 } - ); - - t.pass(); - }); -} diff --git a/packages/test/src/test-workflow-cancellation.ts b/packages/test/src/test-workflow-cancellation.ts deleted file mode 100644 index 8faba97bf..000000000 --- a/packages/test/src/test-workflow-cancellation.ts +++ /dev/null @@ -1,76 +0,0 @@ -import anyTest, { Macro, TestFn, ErrorConstructor } from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { WorkflowClient, WorkflowFailedError } from '@temporalio/client'; -import { ApplicationFailure, CancelledFailure } from '@temporalio/common'; -import * as activities from './activities'; -import { RUN_INTEGRATION_TESTS, Worker } from './helpers'; -import { - WorkflowCancellationScenarioOutcome, - workflowCancellationScenarios, - WorkflowCancellationScenarioTiming, -} from './workflows'; - -export interface Context { - worker: Worker; - runPromise: Promise; -} - -const test = anyTest as TestFn; -const taskQueue = 'test-cancellation'; - -const testWorkflowCancellation: Macro< - [WorkflowCancellationScenarioOutcome, WorkflowCancellationScenarioTiming, ErrorConstructor | undefined], - Context -> = { - exec: async (t, outcome, timing, expected) => { - const client = new WorkflowClient(); - const workflow = await client.start(workflowCancellationScenarios, { - args: [outcome, timing], - taskQueue, - workflowId: uuid4(), - }); - await workflow.cancel(); - if (expected === undefined) { - await workflow.result(); - t.pass(); - } else { - const err = await t.throwsAsync(workflow.result(), { - instanceOf: WorkflowFailedError, - }); - if (!(err instanceof WorkflowFailedError)) { - throw new Error('Unreachable'); - } - t.true(err.cause instanceof expected); - } - }, - title: (_providedTitle = '', outcome, timing) => `workflow cancellation scenario ${outcome} ${timing}`, -}; - -if (RUN_INTEGRATION_TESTS) { - test.before(async (t) => { - const worker = await Worker.create({ - workflowsPath: require.resolve('./workflows'), - activities, - taskQueue, - }); - - const runPromise = worker.run(); - // Catch the error here to avoid unhandled rejection - runPromise.catch((err) => { - console.error('Caught error while worker was running', err); - }); - t.context = { worker, runPromise }; - }); - - test.after.always(async (t) => { - t.context.worker.shutdown(); - await t.context.runPromise; - }); - - test(testWorkflowCancellation, 'complete', 'immediately', undefined); - test(testWorkflowCancellation, 'complete', 'after-cleanup', undefined); - test(testWorkflowCancellation, 'cancel', 'immediately', CancelledFailure); - test(testWorkflowCancellation, 'cancel', 'after-cleanup', CancelledFailure); - test(testWorkflowCancellation, 'fail', 'immediately', ApplicationFailure); - test(testWorkflowCancellation, 'fail', 'after-cleanup', ApplicationFailure); -} diff --git a/packages/test/src/test-workflow-log-interceptor.ts b/packages/test/src/test-workflow-log-interceptor.ts deleted file mode 100644 index ac8123f80..000000000 --- a/packages/test/src/test-workflow-log-interceptor.ts +++ /dev/null @@ -1,200 +0,0 @@ -import anyTest, { TestFn, ExecutionContext } from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { - DefaultLogger, - InjectedSinks, - LogEntry, - LogLevel, - LoggerSinks, - Runtime, - defaultSinks, -} from '@temporalio/worker'; -import { WorkflowInfo } from '@temporalio/workflow'; -import * as workflows from './workflows'; -import { Worker, TestWorkflowEnvironment } from './helpers'; - -interface Context { - testEnv: TestWorkflowEnvironment; - taskQueue: string; -} -const test = anyTest as TestFn; - -const recordedLogs: { [workflowId: string]: LogEntry[] } = {}; - -test.before(async (t) => { - Runtime.install({ - logger: new DefaultLogger('DEBUG', (entry) => { - const workflowId = (entry.meta as any)?.workflowInfo?.workflowId ?? (entry.meta as any)?.workflowId; - recordedLogs[workflowId] ??= []; - recordedLogs[workflowId].push(entry); - }), - }); - - t.context = { - testEnv: await TestWorkflowEnvironment.createLocal(), - taskQueue: '', // Will be set in beforeEach - }; -}); - -test.beforeEach(async (t) => { - t.context.taskQueue = uuid4(); -}); - -test.after.always(async (t) => { - await t.context.testEnv?.teardown(); -}); - -async function withWorker( - t: ExecutionContext, - p: Promise, - workflowId: string -): Promise<[LogEntry, LogEntry]> { - const { nativeConnection } = t.context.testEnv; - const worker = await Worker.create({ - connection: nativeConnection, - taskQueue: t.context.taskQueue, - workflowsPath: require.resolve('./workflows'), - }); - await worker.runUntil(p); - const logs = recordedLogs[workflowId]; - t.true(logs.length >= 2); - return logs as [LogEntry, LogEntry]; -} - -test('Workflow Worker logs when workflow completes', async (t) => { - const { client } = t.context.testEnv; - const workflowId = uuid4(); - const [startLog, endLog] = await withWorker( - t, - client.workflow.execute(workflows.successString, { workflowId, taskQueue: t.context.taskQueue }), - workflowId - ); - t.is(startLog.level, 'DEBUG'); - t.is(startLog.message, 'Workflow started'); - t.is(startLog.meta?.workflowId, workflowId); - t.true(typeof startLog.meta?.runId === 'string'); - t.is(startLog.meta?.taskQueue, t.context.taskQueue); - t.is(startLog.meta?.namespace, 'default'); - t.is(startLog.meta?.workflowType, 'successString'); - t.is(endLog.level, 'DEBUG'); - t.is(endLog.message, 'Workflow completed'); -}); - -test('Workflow Worker logs when workflow continues as new', async (t) => { - const { client } = t.context.testEnv; - const workflowId = uuid4(); - const [_, endLog] = await withWorker( - t, - t.throwsAsync( - client.workflow.execute(workflows.continueAsNewSameWorkflow, { - args: ['execute', 'execute'], - workflowId, - taskQueue: t.context.taskQueue, - followRuns: false, - }) - ), - workflowId - ); - t.is(endLog.level, 'DEBUG'); - t.is(endLog.message, 'Workflow continued as new'); -}); - -test('Workflow Worker logs warning when workflow fails', async (t) => { - const { client } = t.context.testEnv; - const workflowId = uuid4(); - const [_, endLog] = await withWorker( - t, - t.throwsAsync( - client.workflow.execute(workflows.throwAsync, { - workflowId, - taskQueue: t.context.taskQueue, - followRuns: false, - }) - ), - workflowId - ); - t.is(endLog.level, 'WARN'); - t.is(endLog.message, 'Workflow failed'); -}); - -test('(Legacy) defaultSinks(logger) can be used to customize where logs are sent', async (t) => { - const { client } = t.context.testEnv; - const workflowId = uuid4(); - const { nativeConnection } = t.context.testEnv; - const logs = Array(); - const logger = new DefaultLogger('DEBUG', (entry) => logs.push(entry)); - const worker = await Worker.create({ - connection: nativeConnection, - taskQueue: t.context.taskQueue, - workflowsPath: require.resolve('./workflows'), - // eslint-disable-next-line deprecation/deprecation - sinks: defaultSinks(logger), - }); - await worker.runUntil( - client.workflow.execute(workflows.successString, { workflowId, taskQueue: t.context.taskQueue }) - ); - t.false(workflowId in recordedLogs); - t.true(logs.length >= 2); - const [startLog, endLog] = logs; - t.is(startLog.level, 'DEBUG'); - t.is(startLog.message, 'Workflow started'); - t.is(startLog.meta?.workflowId, workflowId); - t.true(typeof startLog.meta?.runId === 'string'); - t.is(startLog.meta?.taskQueue, t.context.taskQueue); - t.is(startLog.meta?.namespace, 'default'); - t.is(startLog.meta?.workflowType, 'successString'); - t.is(endLog.level, 'DEBUG'); - t.is(endLog.message, 'Workflow completed'); -}); - -test('(Legacy) Can register defaultWorkerLogger sink to customize where logs are sent', async (t) => { - const { client } = t.context.testEnv; - const workflowId = uuid4(); - const { nativeConnection } = t.context.testEnv; - const logs = Array(); - const fn = (level: LogLevel, _info: WorkflowInfo, message: string, attrs?: Record) => { - logs.push({ level, message, meta: attrs, timestampNanos: 0n }); - }; - const worker = await Worker.create({ - connection: nativeConnection, - taskQueue: t.context.taskQueue, - workflowsPath: require.resolve('./workflows'), - // eslint-disable-next-line deprecation/deprecation - sinks: >{ - defaultWorkerLogger: { - trace: { fn: fn.bind(undefined, 'TRACE') }, - debug: { fn: fn.bind(undefined, 'DEBUG') }, - info: { fn: fn.bind(undefined, 'INFO') }, - warn: { fn: fn.bind(undefined, 'WARN') }, - error: { fn: fn.bind(undefined, 'ERROR') }, - }, - }, - }); - await worker.runUntil( - client.workflow.execute(workflows.successString, { workflowId, taskQueue: t.context.taskQueue }) - ); - t.false(workflowId in recordedLogs); - t.true(logs.length >= 2); - const [startLog, endLog] = logs; - t.is(startLog.level, 'DEBUG'); - t.is(startLog.message, 'Workflow started'); - t.is(startLog.meta?.workflowId, workflowId); - t.true(typeof startLog.meta?.runId === 'string'); - t.is(startLog.meta?.taskQueue, t.context.taskQueue); - t.is(startLog.meta?.namespace, 'default'); - t.is(startLog.meta?.workflowType, 'successString'); - t.is(endLog.level, 'DEBUG'); - t.is(endLog.message, 'Workflow completed'); -}); - -test('(Legacy) Can explicitly call defaultWorkerLogger sink to emit logs', async (t) => { - const { client } = t.context.testEnv; - const workflowId = uuid4(); - const [_, midLog] = await withWorker( - t, - client.workflow.execute(workflows.useDepreatedLoggerSinkWorkflow, { workflowId, taskQueue: t.context.taskQueue }), - workflowId - ); - t.is(midLog.level, 'INFO'); - t.is(midLog.message, 'Log message from workflow'); -}); diff --git a/packages/test/src/test-workflow-unhandled-rejection-crash.ts b/packages/test/src/test-workflow-unhandled-rejection-crash.ts deleted file mode 100644 index af1f266d0..000000000 --- a/packages/test/src/test-workflow-unhandled-rejection-crash.ts +++ /dev/null @@ -1,33 +0,0 @@ -import test from 'ava'; -import { v4 as uuid4 } from 'uuid'; -import { UnexpectedError, Worker } from '@temporalio/worker'; -import { WorkflowClient } from '@temporalio/client'; -import { defaultOptions } from './mock-native-worker'; -import { RUN_INTEGRATION_TESTS } from './helpers'; -import { throwUnhandledRejection } from './workflows'; - -if (RUN_INTEGRATION_TESTS) { - test('Worker crashes if workflow throws unhandled rejection that cannot be associated with a workflow run', async (t) => { - // To debug Workflows with this worker run the test with `ava debug` and add breakpoints to your Workflows - const taskQueue = `unhandled-rejection-crash-${uuid4()}`; - const worker = await Worker.create({ ...defaultOptions, taskQueue }); - const client = new WorkflowClient(); - const handle = await client.start(throwUnhandledRejection, { - workflowId: uuid4(), - taskQueue, - args: [{ crashWorker: true }], - }); - try { - await t.throwsAsync(worker.run(), { - instanceOf: UnexpectedError, - message: - 'Workflow Worker Thread exited prematurely: UnhandledRejectionError: ' + - "Unhandled Promise rejection for unknown Workflow Run id='undefined': " + - 'Error: error to crash the worker', - }); - t.is(worker.getState(), 'FAILED'); - } finally { - await handle.terminate(); - } - }); -} diff --git a/packages/test/src/test-workflows.ts b/packages/test/src/test-workflows.ts deleted file mode 100644 index 7330e0bf9..000000000 --- a/packages/test/src/test-workflows.ts +++ /dev/null @@ -1,2711 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import path from 'node:path'; -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 { - ApplicationFailure, - defaultFailureConverter, - defaultPayloadConverter, - SdkComponent, - Payload, - toPayloads, - TypedSearchAttributes, -} from '@temporalio/common'; -import { msToTs } from '@temporalio/common/lib/time'; -import { coresdk, temporal } from '@temporalio/proto'; -import { LogTimestamp } from '@temporalio/worker'; -import { WorkflowCodeBundler } from '@temporalio/worker/lib/workflow/bundler'; -import { VMWorkflow, VMWorkflowCreator } from '@temporalio/worker/lib/workflow/vm'; -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, compareStackTrace, REUSE_V8_CONTEXT, u8 } from './helpers'; -import { ProcessedSignal } from './workflows'; - -export interface Context { - workflow: VMWorkflow | ReusableVMWorkflow; - logs: unknown[][]; - workflowType: string; - startTime: number; - runId: string; - workflowCreator: TestVMWorkflowCreator | TestReusableVMWorkflowCreator; -} - -const test = anyTest as TestFn; - -function injectCustomConsole(logsGetter: (runId: string) => unknown[][], context: vm.Context) { - context.console = { - log(...args: unknown[]) { - const { runId } = context.__TEMPORAL_ACTIVATOR__.info; - logsGetter(runId).push(args); - }, - }; -} - -class TestVMWorkflowCreator extends VMWorkflowCreator { - public logs: Record = {}; - - override injectGlobals(context: vm.Context) { - super.injectGlobals(context); - injectCustomConsole((runId) => this.logs[runId], context); - } -} - -class TestReusableVMWorkflowCreator extends ReusableVMWorkflowCreator { - public logs: Record = {}; - - override injectGlobals(context: vm.Context) { - super.injectGlobals(context); - injectCustomConsole((runId) => this.logs[runId], context); - } -} - -test.before(async (t) => { - const workflowsPath = path.join(__dirname, 'workflows'); - const bundler = new WorkflowCodeBundler({ workflowsPath }); - const workflowBundle = parseWorkflowCode((await bundler.createBundle()).code); - // FIXME: isolateExecutionTimeoutMs used to be 200 ms, but that's causing - // lot of flakes on CI. Revert this after investigation / resolution. - t.context.workflowCreator = REUSE_V8_CONTEXT - ? await TestReusableVMWorkflowCreator.create(workflowBundle, 400, new Set()) - : await TestVMWorkflowCreator.create(workflowBundle, 400, new Set()); -}); - -test.after.always(async (t) => { - await t.context.workflowCreator.destroy(); -}); - -test.beforeEach(async (t) => { - const { workflowCreator } = t.context; - const workflowType = t.title.match(/\S+$/)![0]; - const runId = t.title; - const logs = new Array(); - workflowCreator.logs[runId] = logs; - const startTime = Date.now(); - const workflow = await createWorkflow(workflowType, runId, startTime, workflowCreator); - - t.context = { - logs, - runId, - workflowType, - workflowCreator, - startTime, - workflow, - }; -}); - -async function createWorkflow( - workflowType: string, - runId: string, - startTime: number, - workflowCreator: VMWorkflowCreator | ReusableVMWorkflowCreator -) { - const workflow = (await workflowCreator.createWorkflow({ - info: { - workflowType, - runId, - workflowId: 'test-workflowId', - namespace: 'default', - firstExecutionRunId: runId, - attempt: 1, - taskTimeoutMs: 1000, - taskQueue: 'test', - searchAttributes: {}, - typedSearchAttributes: new TypedSearchAttributes(), - historyLength: 3, - historySize: 300, - continueAsNewSuggested: false, - unsafe: { isReplaying: false, now: Date.now }, - startTime: new Date(), - runStartTime: new Date(), - }, - randomnessSeed: Long.fromInt(1337).toBytes(), - now: startTime, - showStackTraceSources: true, - })) as VMWorkflow; - return workflow; -} - -async function activate(t: ExecutionContext, activation: coresdk.workflow_activation.IWorkflowActivation) { - const { workflow, runId } = t.context; - - // Core guarantees the following jobs ordering: - // initWf -> patches -> update random seed -> signals+update -> others -> Resolve LA - // reference: github.com/temporalio/sdk-core/blob/a8150d5c7c3fc1bfd5a941fd315abff1556cd9dc/core/src/worker/workflow/mod.rs#L1363-L1378 - // Tests are likely to fail if we artifically make an activation that does not follow that order - const jobs: coresdk.workflow_activation.IWorkflowActivationJob[] = activation.jobs ?? []; - function getPriority(job: coresdk.workflow_activation.IWorkflowActivationJob) { - if (job.initializeWorkflow) return 0; - if (job.notifyHasPatch) return 1; - if (job.updateRandomSeed) return 2; - if (job.signalWorkflow || job.doUpdate) return 3; - if (job.resolveActivity && job.resolveActivity.isLocal) return 5; - return 4; - } - jobs.reduce((prevPriority: number, currJob) => { - const currPriority = getPriority(currJob); - if (prevPriority > currPriority) { - throw new Error('Jobs are not correctly sorted'); - } - return currPriority; - }, 0); - - const completion = await workflow.activate(coresdk.workflow_activation.WorkflowActivation.fromObject(activation)); - t.deepEqual(completion.runId, runId); - return completion; -} - -function compareCompletion( - t: ExecutionContext, - 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({ - ...expected, - 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( - commands: coresdk.workflow_commands.IWorkflowCommand[] = [makeCompleteWorkflowExecution()], - usedInternalFlags: SdkFlag[] = [] -): coresdk.workflow_completion.IWorkflowActivationCompletion { - return { - successful: { - commands, - usedInternalFlags: usedInternalFlags.map((x) => x.id), - }, - }; -} - -function makeStartWorkflow( - workflowType: string, - args?: Payload[], - timestamp: number = Date.now() -): coresdk.workflow_activation.IWorkflowActivation { - return makeActivation(timestamp, makeInitializeWorkflowJob(workflowType, args)); -} - -function makeInitializeWorkflowJob( - workflowType: string, - args?: Payload[] -): { initializeWorkflow: coresdk.workflow_activation.IInitializeWorkflow } { - return { - initializeWorkflow: { workflowId: 'test-workflowId', workflowType, arguments: args }, - }; -} - -/** - * Creates a Failure object for a cancelled activity - */ -function makeActivityCancelledFailure(activityId: string, activityType: string) { - return { - cause: { - canceledFailureInfo: {}, - }, - activityFailureInfo: { - activityId, - identity: 'test', - activityType: { name: activityType }, - retryState: temporal.api.enums.v1.RetryState.RETRY_STATE_CANCEL_REQUESTED, - }, - }; -} -function makeActivation( - timestamp: number = Date.now(), - ...jobs: coresdk.workflow_activation.IWorkflowActivationJob[] -): coresdk.workflow_activation.IWorkflowActivation { - return { - runId: 'test-runId', - timestamp: msToTs(timestamp), - jobs, - }; -} - -function makeFireTimer(seq: number, timestamp: number = Date.now()): coresdk.workflow_activation.IWorkflowActivation { - return makeActivation(timestamp, makeFireTimerJob(seq)); -} - -function makeFireTimerJob(seq: number): coresdk.workflow_activation.IWorkflowActivationJob { - return { - fireTimer: { seq }, - }; -} - -function makeResolveActivityJob( - seq: number, - result: coresdk.activity_result.IActivityExecutionResult -): coresdk.workflow_activation.IWorkflowActivationJob { - return { - resolveActivity: { seq, result }, - }; -} - -function makeResolveActivity( - seq: number, - result: coresdk.activity_result.IActivityExecutionResult, - timestamp: number = Date.now() -): coresdk.workflow_activation.IWorkflowActivation { - return makeActivation(timestamp, makeResolveActivityJob(seq, result)); -} - -function makeNotifyHasPatchJob(patchId: string): coresdk.workflow_activation.IWorkflowActivationJob { - return { - notifyHasPatch: { patchId }, - }; -} - -function makeQueryWorkflow( - queryId: string, - queryType: string, - queryArgs: any[], - timestamp: number = Date.now() -): coresdk.workflow_activation.IWorkflowActivation { - return makeActivation(timestamp, makeQueryWorkflowJob(queryId, queryType, ...queryArgs)); -} - -function makeQueryWorkflowJob( - queryId: string, - queryType: string, - ...queryArgs: any[] -): coresdk.workflow_activation.IWorkflowActivationJob { - return { - queryWorkflow: { - queryId, - queryType, - arguments: toPayloads(defaultPayloadConverter, ...queryArgs), - }, - }; -} - -async function makeSignalWorkflow( - signalName: string, - args: any[], - timestamp: number = Date.now() -): Promise { - return makeActivation(timestamp, makeSignalWorkflowJob(signalName, args)); -} - -function makeSignalWorkflowJob(signalName: string, args: any[]): coresdk.workflow_activation.IWorkflowActivationJob { - return { - signalWorkflow: { signalName, input: toPayloads(defaultPayloadConverter, ...args) }, - }; -} - -function makeCompleteWorkflowExecution(result?: Payload): coresdk.workflow_commands.IWorkflowCommand { - result ??= { metadata: { encoding: u8('binary/null') } }; - return { - completeWorkflowExecution: { result }, - }; -} - -function makeFailWorkflowExecution( - message: string, - stackTrace: string, - type = 'Error', - nonRetryable = true -): coresdk.workflow_commands.IWorkflowCommand { - return { - failWorkflowExecution: { - failure: { message, stackTrace, applicationFailureInfo: { type, nonRetryable }, source: 'TypeScriptSDK' }, - }, - }; -} - -function makeScheduleActivityCommand( - attrs: coresdk.workflow_commands.IScheduleActivity -): coresdk.workflow_commands.IWorkflowCommand { - return { - scheduleActivity: attrs, - }; -} - -function makeCancelActivityCommand(seq: number, _reason?: string): coresdk.workflow_commands.IWorkflowCommand { - return { - requestCancelActivity: { seq }, - }; -} - -function makeStartTimerCommand( - attrs: coresdk.workflow_commands.IStartTimer -): coresdk.workflow_commands.IWorkflowCommand { - return { - startTimer: attrs, - }; -} - -function makeCancelTimerCommand( - attrs: coresdk.workflow_commands.ICancelTimer -): coresdk.workflow_commands.IWorkflowCommand { - return { - cancelTimer: attrs, - }; -} - -function makeRespondToQueryCommand( - respondToQuery: coresdk.workflow_commands.IQueryResult -): coresdk.workflow_commands.IWorkflowCommand { - return { - respondToQuery, - }; -} - -function makeSetPatchMarker(myPatchId: string, deprecated: boolean): coresdk.workflow_commands.IWorkflowCommand { - return { - setPatchMarker: { - patchId: myPatchId, - deprecated, - }, - }; -} - -function makeUpdateActivationJob( - id: string, - protocolInstanceId: string, - name: string, - input: unknown[] -): coresdk.workflow_activation.IWorkflowActivationJob { - return { - doUpdate: { - id, - protocolInstanceId, - name, - input: toPayloads(defaultPayloadConverter, ...input), - }, - }; -} - -function makeUpdateAcceptedResponse(id: string): coresdk.workflow_commands.IWorkflowCommand { - return { - updateResponse: { - protocolInstanceId: id, - accepted: {}, - }, - }; -} - -function makeUpdateCompleteResponse(id: string, result: unknown): coresdk.workflow_commands.IWorkflowCommand { - return { - updateResponse: { - protocolInstanceId: id, - completed: defaultPayloadConverter.toPayload(result), - }, - }; -} - -test('random', async (t) => { - const { logs, workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1) })])); - } - { - const req = await activate( - t, - makeActivation( - undefined, - { updateRandomSeed: { randomnessSeed: Long.fromNumber(7331) } }, - { fireTimer: { seq: 1 } } - ) - ); - compareCompletion(t, req, makeSuccess()); - } - t.deepEqual(logs, [[0.8380154962651432], ['a50eca73-ff3e-4445-a512-2330c2f4f86e'], [0.18803317612037063]]); -}); - -test('successString', async (t) => { - const { workflowType } = t.context; - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess([makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload('success'))])); -}); - -test('continueAsNewSuggested', async (t) => { - const { workflowType } = t.context; - const activation = makeStartWorkflow(workflowType); - activation.continueAsNewSuggested = true; - const req = await activate(t, activation); - compareCompletion(t, req, makeSuccess([makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload(true))])); -}); - -function cleanWorkflowFailureStackTrace( - req: coresdk.workflow_completion.IWorkflowActivationCompletion, - commandIndex = 0 -) { - req.successful!.commands![commandIndex].failWorkflowExecution!.failure!.stackTrace = cleanStackTrace( - req.successful!.commands![commandIndex].failWorkflowExecution!.failure!.stackTrace! - ); - return req; -} - -function cleanWorkflowQueryFailureStackTrace( - req: coresdk.workflow_completion.IWorkflowActivationCompletion, - commandIndex = 0 -) { - req.successful!.commands![commandIndex].respondToQuery!.failed!.stackTrace = cleanStackTrace( - req.successful!.commands![commandIndex].respondToQuery!.failed!.stackTrace! - ); - return req; -} - -test('throwAsync', async (t) => { - const { workflowType } = t.context; - const req = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType))); - compareCompletion( - t, - req, - makeSuccess([ - makeFailWorkflowExecution( - 'failure', - dedent` - ApplicationFailure: failure - at $CLASS.nonRetryable (common/src/failure.ts) - at throwAsync (test/src/workflows/throw-async.ts) - ` - ), - ]) - ); -}); - -test('date', async (t) => { - const { startTime, logs, workflowType } = t.context; - const req = await activate(t, makeStartWorkflow(workflowType, undefined, startTime)); - compareCompletion(t, req, makeSuccess()); - t.deepEqual(logs, [[startTime], [startTime], [true], [true], [true], [true], [true]]); -}); - -test('asyncWorkflow', async (t) => { - const { workflowType } = t.context; - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess([makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload('async'))])); -}); - -test('deferredResolve', async (t) => { - const { logs, workflowType } = t.context; - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess()); - t.deepEqual(logs, [[1], [2]]); -}); - -test('sleeper', async (t) => { - const { logs, workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100) })])); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion(t, req, makeSuccess()); - } - t.deepEqual(logs, [['slept']]); -}); - -test('with ms string - sleeper', async (t) => { - const { logs, workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType, [defaultPayloadConverter.toPayload('10s')])); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs('10s') })])); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion(t, req, makeSuccess()); - } - t.deepEqual(logs, [['slept']]); -}); - -test('setTimeoutAfterMicroTasks', async (t) => { - const { logs, workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100) })])); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion(t, req, makeSuccess()); - } - t.deepEqual(logs, [['slept']]); -}); - -test('promiseThenPromise', async (t) => { - const { logs, workflowType } = t.context; - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess()); - t.deepEqual(logs, [[2]]); -}); - -test('rejectPromise', async (t) => { - const { logs, workflowType } = t.context; - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess()); - t.deepEqual(logs, [[true], [true]]); -}); - -test('promiseAll', async (t) => { - const { logs, workflowType } = t.context; - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess()); - t.deepEqual(logs, [[1, 2, 3], [1, 2, 3], [1, 2, 3], ['wow']]); -}); - -test('tasksAndMicrotasks', async (t) => { - const { logs, workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - req, - makeSuccess( - [makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1) })], - [SdkFlags.NonCancellableScopesAreShieldedFromPropagation] - ) - ); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion( - t, - req, - makeSuccess([makeCompleteWorkflowExecution()], [SdkFlags.NonCancellableScopesAreShieldedFromPropagation]) - ); - } - t.deepEqual(logs, [['script start'], ['script end'], ['promise1'], ['promise2'], ['setTimeout']]); -}); - -test('trailingTimer', async (t) => { - const { logs, workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1) }), - makeStartTimerCommand({ seq: 2, startToFireTimeout: msToTs(1) }), - ]) - ); - } - { - const completion = await activate(t, makeActivation(undefined, makeFireTimerJob(1), makeFireTimerJob(2))); - // Note that the trailing timer does not get scheduled since the workflow completes - // after the first timer is triggered causing the second one to be dropped. - compareCompletion( - t, - completion, - makeSuccess([ - makeStartTimerCommand({ seq: 3, startToFireTimeout: msToTs(1) }), - makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload('first')), - ]) - ); - } - t.deepEqual(logs, []); -}); - -test('promiseRace', async (t) => { - const { logs, workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - req, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(20) }), - makeStartTimerCommand({ seq: 2, startToFireTimeout: msToTs(30) }), - ]) - ); - } - { - const req = await activate(t, makeActivation(undefined, makeFireTimerJob(1), makeFireTimerJob(2))); - compareCompletion(t, req, makeSuccess()); - } - t.deepEqual(logs, [[1], [1], [1], [1], [20], ['wow']]); -}); - -test('race', async (t) => { - const { logs, workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - req, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(10) }), - makeStartTimerCommand({ seq: 2, startToFireTimeout: msToTs(11) }), - ]) - ); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion(t, req, makeSuccess([])); - } - { - const req = await activate(t, makeFireTimer(2)); - compareCompletion(t, req, makeSuccess()); - } - t.deepEqual(logs, [[1], [2], [3]]); -}); - -test('importer', async (t) => { - const { logs, workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - req, - makeSuccess( - [makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(10) })], - [SdkFlags.NonCancellableScopesAreShieldedFromPropagation] - ) - ); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion( - t, - req, - makeSuccess([makeCompleteWorkflowExecution()], [SdkFlags.NonCancellableScopesAreShieldedFromPropagation]) - ); - } - t.deepEqual(logs, [['slept']]); -}); - -test('argsAndReturn', async (t) => { - const { workflowType } = t.context; - const req = await activate( - t, - makeStartWorkflow(workflowType, [ - { - metadata: { encoding: u8('json/plain') }, - data: u8(JSON.stringify('Hello')), - }, - { - metadata: { encoding: u8('binary/null') }, - }, - { - metadata: { encoding: u8('binary/plain') }, - data: u8('world'), - }, - ]) - ); - compareCompletion( - t, - req, - makeSuccess([ - makeCompleteWorkflowExecution({ - metadata: { encoding: u8('json/plain') }, - data: u8(JSON.stringify('Hello, world')), - }), - ]) - ); -}); - -test('invalidOrFailedQueries', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, completion, makeSuccess()); - } - { - const completion = cleanWorkflowQueryFailureStackTrace( - await activate(t, makeQueryWorkflow('3', 'invalidAsyncMethod', [])) - ); - compareCompletion( - t, - completion, - makeSuccess([ - makeRespondToQueryCommand({ - queryId: '3', - failed: { - message: 'Query handlers should not return a Promise', - source: 'TypeScriptSDK', - stackTrace: 'DeterminismViolationError: Query handlers should not return a Promise', - applicationFailureInfo: { - type: 'DeterminismViolationError', - nonRetryable: false, - }, - }, - }), - ]) - ); - } - { - const completion = cleanWorkflowQueryFailureStackTrace(await activate(t, makeQueryWorkflow('3', 'fail', []))); - compareCompletion( - t, - completion, - makeSuccess([ - makeRespondToQueryCommand({ - queryId: '3', - failed: { - source: 'TypeScriptSDK', - message: 'fail', - stackTrace: dedent` - Error: fail - at test/src/workflows/invalid-or-failed-queries.ts - `, - applicationFailureInfo: { - type: 'Error', - nonRetryable: false, - }, - }, - }), - ]) - ); - } -}); - -test('interruptableWorkflow', async (t) => { - const { workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess([])); - } - { - const req = cleanWorkflowFailureStackTrace( - await activate(t, await makeSignalWorkflow('interrupt', ['just because'])) - ); - 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 $CLASS.retryable (common/src/failure.ts) - at test/src/workflows/interrupt-signal.ts - `, - 'Error', - false - ), - ], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -test('failSignalWorkflow', async (t) => { - const { workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100000) })])); - } - { - const req = cleanWorkflowFailureStackTrace(await activate(t, await makeSignalWorkflow('fail', []))); - compareCompletion( - t, - req, - makeSuccess( - [ - makeFailWorkflowExecution( - 'Signal failed', - dedent` - ApplicationFailure: Signal failed - at $CLASS.nonRetryable (common/src/failure.ts) - at test/src/workflows/fail-signal.ts - `, - 'Error' - ), - ], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -test('asyncFailSignalWorkflow', async (t) => { - const { workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100000) })])); - } - { - const req = await activate(t, await makeSignalWorkflow('fail', [])); - compareCompletion( - t, - req, - makeSuccess( - [makeStartTimerCommand({ seq: 2, startToFireTimeout: msToTs(100) })], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } - { - const req = cleanWorkflowFailureStackTrace(await activate(t, makeFireTimer(2))); - compareCompletion( - t, - req, - makeSuccess( - [ - makeFailWorkflowExecution( - 'Signal failed', - dedent` - ApplicationFailure: Signal failed - at $CLASS.nonRetryable (common/src/failure.ts) - at test/src/workflows/async-fail-signal.ts`, - 'Error' - ), - ], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -test('cancelWorkflow', async (t) => { - const url = 'https://temporal.io'; - const { workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType, toPayloads(defaultPayloadConverter, url))); - compareCompletion( - t, - req, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'httpGet', - arguments: toPayloads(defaultPayloadConverter, url), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const req = await activate(t, makeActivation(undefined, { cancelWorkflow: {} })); - compareCompletion(t, req, makeSuccess([makeCancelActivityCommand(1)])); - } - { - const req = await activate( - t, - makeActivation(undefined, { - resolveActivity: { - seq: 1, - result: { - cancelled: { - failure: makeActivityCancelledFailure('1', 'httpGet'), - }, - }, - }, - }) - ); - compareCompletion( - t, - req, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 3, - activityId: '3', - activityType: 'httpGet', - arguments: toPayloads(defaultPayloadConverter, url), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - const result = defaultPayloadConverter.toPayload(await activityFunctions.httpGet(url)); - { - const req = await activate( - t, - makeActivation(undefined, { - resolveActivity: { - seq: 3, - result: { completed: { result } }, - }, - }) - ); - compareCompletion(t, req, makeSuccess([makeCompleteWorkflowExecution(result)])); - } -}); - -test('cancel - unblockOrCancel', async (t) => { - const { workflowType, logs } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, completion, makeSuccess([])); - } - { - const completion = await activate(t, makeActivation(undefined, { cancelWorkflow: {} })); - compareCompletion(t, completion, makeSuccess()); - } - t.deepEqual(logs, [['Blocked'], ['Cancelled']]); -}); - -test('unblock - unblockOrCancel', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, completion, makeSuccess([])); - } - { - const completion = await activate(t, makeQueryWorkflow('1', 'isBlocked', [])); - compareCompletion( - t, - completion, - makeSuccess([ - makeRespondToQueryCommand({ - queryId: '1', - succeeded: { response: defaultPayloadConverter.toPayload(true) }, - }), - ]) - ); - } - { - const completion = await activate(t, await makeSignalWorkflow('unblock', [])); - compareCompletion(t, completion, makeSuccess(undefined, [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch])); - } - { - const completion = await activate(t, makeQueryWorkflow('2', 'isBlocked', [])); - compareCompletion( - t, - completion, - makeSuccess( - [ - makeRespondToQueryCommand({ - queryId: '2', - succeeded: { response: defaultPayloadConverter.toPayload(false) }, - }), - ], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -test('cancelTimer', async (t) => { - const { workflowType, logs } = t.context; - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - req, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1) }), - makeCancelTimerCommand({ seq: 1 }), - makeCompleteWorkflowExecution(), - ]) - ); - t.deepEqual(logs, [['Timer cancelled 👍']]); -}); - -test('cancelTimerAltImpl', async (t) => { - const { workflowType, logs } = t.context; - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - req, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1) }), - makeCancelTimerCommand({ seq: 1 }), - makeCompleteWorkflowExecution(), - ]) - ); - t.deepEqual(logs, [['Timer cancelled 👍']]); -}); - -test('nonCancellable', async (t) => { - const { workflowType } = t.context; - const url = 'https://temporal.io'; - const result = defaultPayloadConverter.toPayload({ test: true }); - { - const completion = await activate(t, makeStartWorkflow(workflowType, [defaultPayloadConverter.toPayload(url)])); - compareCompletion( - t, - completion, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'httpGetJSON', - arguments: toPayloads(defaultPayloadConverter, url), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate(t, makeResolveActivity(1, { completed: { result } })); - compareCompletion(t, completion, makeSuccess([makeCompleteWorkflowExecution(result)])); - } -}); - -test('resumeAfterCancellation', async (t) => { - const { workflowType } = t.context; - const url = 'https://temporal.io'; - const result = defaultPayloadConverter.toPayload({ test: true }); - { - const completion = await activate(t, makeStartWorkflow(workflowType, [defaultPayloadConverter.toPayload(url)])); - compareCompletion( - t, - completion, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'httpGetJSON', - arguments: toPayloads(defaultPayloadConverter, url), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate(t, makeActivation(undefined, { cancelWorkflow: {} })); - compareCompletion(t, completion, makeSuccess([])); - } - { - const completion = await activate(t, makeResolveActivity(1, { completed: { result } })); - compareCompletion(t, completion, makeSuccess([makeCompleteWorkflowExecution(result)])); - } -}); - -test('handleExternalWorkflowCancellationWhileActivityRunning', async (t) => { - const { workflowType } = t.context; - const url = 'https://temporal.io'; - const data = { content: 'new HTML content' }; - { - const completion = await activate( - t, - makeStartWorkflow(workflowType, toPayloads(defaultPayloadConverter, url, data) ?? []) - ); - - compareCompletion( - t, - completion, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'httpPostJSON', - arguments: toPayloads(defaultPayloadConverter, url, data), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate(t, makeActivation(undefined, { cancelWorkflow: {} })); - compareCompletion(t, completion, makeSuccess([makeCancelActivityCommand(1)])); - } - { - const completion = await activate( - t, - makeResolveActivity(1, { cancelled: { failure: { canceledFailureInfo: {} } } }) - ); - compareCompletion( - t, - completion, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 2, - activityId: '2', - activityType: 'cleanup', - arguments: toPayloads(defaultPayloadConverter, url), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate( - t, - makeResolveActivity(2, { completed: { result: defaultPayloadConverter.toPayload(undefined) } }) - ); - compareCompletion(t, completion, makeSuccess([{ cancelWorkflowExecution: {} }])); - } -}); - -test('nestedCancellation', async (t) => { - const { workflowType } = t.context; - const url = 'https://temporal.io'; - { - const completion = await activate(t, makeStartWorkflow(workflowType, [defaultPayloadConverter.toPayload(url)])); - - compareCompletion( - t, - completion, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'setup', - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate( - t, - makeResolveActivity(1, { completed: { result: defaultPayloadConverter.toPayload(undefined) } }) - ); - - compareCompletion( - t, - completion, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1000) }), - makeScheduleActivityCommand({ - seq: 2, - activityId: '2', - activityType: 'httpPostJSON', - arguments: toPayloads(defaultPayloadConverter, url, { some: 'data' }), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate(t, makeFireTimer(1)); - compareCompletion(t, completion, makeSuccess([makeCancelActivityCommand(2)])); - } - const failure = makeActivityCancelledFailure('2', 'httpPostJSON'); - { - const completion = await activate(t, makeResolveActivity(2, { cancelled: { failure } })); - compareCompletion( - t, - completion, - makeSuccess([ - makeScheduleActivityCommand({ - activityId: '3', - seq: 3, - activityType: 'cleanup', - arguments: toPayloads(defaultPayloadConverter, url), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate( - t, - makeResolveActivity(3, { completed: { result: defaultPayloadConverter.toPayload(undefined) } }) - ); - compareCompletion( - t, - completion, - makeSuccess([ - { - failWorkflowExecution: { failure }, - }, - ]) - ); - } -}); - -test('sharedCancellationScopes', async (t) => { - const { workflowType } = t.context; - const result = { some: 'data' }; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess( - await Promise.all( - [1, 2].map(async (idx) => - makeScheduleActivityCommand({ - seq: idx, - activityId: `${idx}`, - activityType: 'httpGetJSON', - arguments: toPayloads(defaultPayloadConverter, `http://url${idx}.ninja`), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }) - ) - ) - ) - ); - } - { - const completion = await activate( - t, - makeResolveActivity(2, { completed: { result: defaultPayloadConverter.toPayload(result) } }) - ); - compareCompletion( - t, - completion, - makeSuccess([makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload(result))]) - ); - } -}); - -test('nonCancellableAwaitedInRootScope', async (t) => { - const { workflowType } = t.context; - const result = { some: 'data' }; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'httpGetJSON', - arguments: toPayloads(defaultPayloadConverter, `http://example.com`), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - // Workflow ignores cancellation - const completion = await activate(t, makeActivation(undefined, { cancelWorkflow: {} })); - compareCompletion(t, completion, makeSuccess([])); - } - { - const completion = await activate( - t, - makeResolveActivity(1, { completed: { result: defaultPayloadConverter.toPayload(result) } }) - ); - compareCompletion( - t, - completion, - makeSuccess([makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload(result))]) - ); - } -}); - -test('cancellationScopesWithCallbacks', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess( - [makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(10) })], - [SdkFlags.NonCancellableScopesAreShieldedFromPropagation] - ) - ); - } - { - const completion = await activate(t, makeActivation(undefined, { cancelWorkflow: {} })); - compareCompletion( - t, - completion, - makeSuccess( - [{ cancelTimer: { seq: 1 } }, { cancelWorkflowExecution: {} }], - [SdkFlags.NonCancellableScopesAreShieldedFromPropagation] - ) - ); - } -}); - -test('cancellationScopes', async (t) => { - const { workflowType, logs } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(3) })])); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion( - t, - req, - makeSuccess([ - makeStartTimerCommand({ seq: 2, startToFireTimeout: msToTs(3) }), - makeStartTimerCommand({ seq: 3, startToFireTimeout: msToTs(3) }), - makeCancelTimerCommand({ seq: 2 }), - ]) - ); - } - { - const req = await activate(t, makeFireTimer(3)); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 4, startToFireTimeout: msToTs(3) })])); - } - { - const req = await activate(t, makeActivation(undefined, { cancelWorkflow: {} })); - compareCompletion(t, req, makeSuccess([makeCancelTimerCommand({ seq: 4 }), makeCompleteWorkflowExecution()])); - } - t.deepEqual(logs, [ - ['Scope cancelled 👍'], - ['Exception was propagated 👍'], - ['Scope 2 was not cancelled 👍'], - ['Scope cancelled 👍'], - ['Exception was propagated 👍'], - ]); -}); - -test('childAndNonCancellable', async (t) => { - const { workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(5) })])); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion(t, req, makeSuccess()); - } -}); - -test('partialNonCancellable', async (t) => { - const { workflowType, logs } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - req, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(5) }), - makeStartTimerCommand({ seq: 2, startToFireTimeout: msToTs(3) }), - ]) - ); - } - { - const req = await activate(t, makeFireTimer(2)); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 3, startToFireTimeout: msToTs(2) })])); - } - { - const req = await activate(t, makeActivation(undefined, { cancelWorkflow: {} })); - compareCompletion( - t, - req, - makeSuccess([ - makeCancelTimerCommand({ seq: 3 }), - makeStartTimerCommand({ seq: 4, startToFireTimeout: msToTs(10) }), - ]) - ); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion(t, req, makeSuccess([makeStartTimerCommand({ seq: 5, startToFireTimeout: msToTs(1) })])); - } - { - const req = await activate(t, makeFireTimer(4)); - compareCompletion(t, req, makeSuccess()); - } - t.deepEqual(logs, [['Workflow cancelled']]); -}); - -test('nonCancellableInNonCancellable', async (t) => { - const { workflowType, logs } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - req, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(2) }), - makeStartTimerCommand({ seq: 2, startToFireTimeout: msToTs(1) }), - ]) - ); - } - { - const req = await activate(t, makeFireTimer(2)); - compareCompletion(t, req, makeSuccess([])); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion(t, req, makeSuccess()); - } - t.deepEqual(logs, [['Timer 1 finished 👍'], ['Timer 0 finished 👍']]); -}); - -test('cancellationErrorIsPropagated', async (t) => { - const { workflowType, logs } = t.context; - const req = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType)), 2); - compareCompletion( - t, - req, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1) }), - makeCancelTimerCommand({ seq: 1 }), - { - 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 $CLASS.cancellable (workflow/src/cancellation-scope.ts) - at cancellationErrorIsPropagated (test/src/workflows/cancellation-error-is-propagated.ts) - `, - canceledFailureInfo: {}, - source: 'TypeScriptSDK', - }, - }, - }, - ]) - ); - t.deepEqual(logs, []); -}); - -test('cancelActivityAfterFirstCompletion', async (t) => { - const url = 'https://temporal.io'; - const { workflowType, logs } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType, toPayloads(defaultPayloadConverter, url))); - compareCompletion( - t, - req, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'httpGet', - arguments: toPayloads(defaultPayloadConverter, url), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const req = await activate( - t, - makeResolveActivity(1, { completed: { result: defaultPayloadConverter.toPayload('response1') } }) - ); - compareCompletion( - t, - req, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 2, - activityId: '2', - activityType: 'httpGet', - arguments: toPayloads(defaultPayloadConverter, url), - startToCloseTimeout: msToTs('10m'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const req = await activate(t, makeActivation(undefined, { cancelWorkflow: {} })); - compareCompletion(t, req, makeSuccess([])); - } - { - const req = await activate( - t, - makeResolveActivity(2, { completed: { result: defaultPayloadConverter.toPayload('response2') } }) - ); - compareCompletion( - t, - req, - makeSuccess([makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload(['response1', 'response2']))]) - ); - } - t.deepEqual(logs, [['Workflow cancelled while waiting on non cancellable scope']]); -}); - -test('multipleActivitiesSingleTimeout', async (t) => { - const urls = ['https://slow-site.com/', 'https://slow-site.org/']; - const { workflowType } = t.context; - { - const completion = await activate( - t, - makeStartWorkflow(workflowType, toPayloads(defaultPayloadConverter, urls, 1000)) - ); - compareCompletion( - t, - completion, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1000) }), - ...(await Promise.all( - urls.map(async (url, index) => - makeScheduleActivityCommand({ - seq: index + 1, - activityId: `${index + 1}`, - activityType: 'httpGetJSON', - arguments: toPayloads(defaultPayloadConverter, url), - startToCloseTimeout: msToTs('1s'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }) - ) - )), - ]) - ); - } - { - const completion = await activate(t, makeFireTimer(1)); - compareCompletion(t, completion, makeSuccess([makeCancelActivityCommand(1), makeCancelActivityCommand(2)])); - } - const failure1 = makeActivityCancelledFailure('1', 'httpGetJSON'); - const failure2 = makeActivityCancelledFailure('2', 'httpGetJSON'); - { - const completion = await activate( - t, - makeActivation( - undefined, - { resolveActivity: { seq: 1, result: { cancelled: { failure: failure1 } } } }, - { resolveActivity: { seq: 2, result: { cancelled: { failure: failure2 } } } } - ) - ); - compareCompletion(t, completion, makeSuccess([{ failWorkflowExecution: { failure: failure1 } }])); - } -}); - -test('resolve activity with result - http', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'httpGet', - arguments: toPayloads(defaultPayloadConverter, 'https://temporal.io'), - startToCloseTimeout: msToTs('1 minute'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - const result = 'hello from https://temporal.io'; - { - const completion = await activate( - t, - makeResolveActivity(1, { completed: { result: defaultPayloadConverter.toPayload(result) } }) - ); - - compareCompletion( - t, - completion, - makeSuccess([makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload(result))]) - ); - } -}); - -test('resolve activity with failure - http', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess([ - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'httpGet', - arguments: toPayloads(defaultPayloadConverter, 'https://temporal.io'), - startToCloseTimeout: msToTs('1 minute'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - - const failure = ApplicationFailure.nonRetryable('Connection timeout', 'MockError'); - failure.stack = failure.stack?.split('\n')[0]; - - { - const completion = await activate( - t, - makeResolveActivity(1, { - failed: { - failure: defaultFailureConverter.errorToFailure(failure, defaultPayloadConverter), - }, - }) - ); - compareCompletion( - t, - completion, - makeSuccess([ - makeFailWorkflowExecution('Connection timeout', 'ApplicationFailure: Connection timeout', 'MockError'), - ]) - ); - } -}); - -test('globalOverrides', async (t) => { - const { workflowType, logs } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, completion, makeSuccess()); - } - t.deepEqual( - logs, - ['WeakRef' /* First error happens on startup */, 'FinalizationRegistry', 'WeakRef'].map((type) => [ - `DeterminismViolationError: ${type} cannot be used in Workflows because v8 GC is non-deterministic`, - ]) - ); -}); - -test('logAndTimeout', async (t) => { - const { workflowType, workflow } = t.context; - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, completion, { - failed: { - failure: { - message: 'Script execution timed out after 400ms', - source: 'TypeScriptSDK', - stackTrace: 'Error: Script execution timed out after 400ms', - cause: undefined, - }, - }, - }); - const calls = await workflow.getAndResetSinkCalls(); - // Ignore LogTimestamp and workflowInfo for the purpose of this comparison - calls.forEach((call) => { - delete call.args[1]?.[LogTimestamp]; - delete (call as any).workflowInfo; - }); - t.deepEqual(calls, [ - { - ifaceName: '__temporal_logger', - fnName: 'debug', - args: [ - 'Workflow started', - { - namespace: 'default', - runId: 'beforeEach hook for logAndTimeout', - taskQueue: 'test', - workflowId: 'test-workflowId', - workflowType: 'logAndTimeout', - sdkComponent: SdkComponent.worker, - }, - ], - }, - { - ifaceName: '__temporal_logger', - fnName: 'info', - args: [ - 'logging before getting stuck', - { - namespace: 'default', - runId: 'beforeEach hook for logAndTimeout', - taskQueue: 'test', - workflowId: 'test-workflowId', - workflowType: 'logAndTimeout', - sdkComponent: SdkComponent.workflow, - }, - ], - }, - ]); -}); - -test('continueAsNewSameWorkflow', async (t) => { - const { workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - req, - makeSuccess([ - { - continueAsNewWorkflowExecution: { - workflowType, - taskQueue: 'test', - arguments: toPayloads(defaultPayloadConverter, 'signal'), - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }, - }, - ]) - ); - } -}); - -test('not-replay patchedWorkflow', async (t) => { - const { logs, workflowType } = t.context; - { - const req = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - req, - makeSuccess([ - makeSetPatchMarker('my-change-id', false), - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100) }), - ]) - ); - } - { - const req = await activate(t, makeFireTimer(1)); - compareCompletion(t, req, makeSuccess([makeCompleteWorkflowExecution()])); - } - t.deepEqual(logs, [['has change'], ['has change 2']]); -}); - -test('replay-no-marker patchedWorkflow', async (t) => { - const { logs, workflowType } = t.context; - { - const act: coresdk.workflow_activation.IWorkflowActivation = { - runId: 'test-runId', - timestamp: msToTs(Date.now()), - isReplaying: true, - jobs: [makeInitializeWorkflowJob(workflowType)], - }; - const completion = await activate(t, act); - compareCompletion(t, completion, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100) })])); - } - { - const act: coresdk.workflow_activation.IWorkflowActivation = { - runId: 'test-runId', - timestamp: msToTs(Date.now()), - isReplaying: true, - jobs: [makeFireTimerJob(1)], - }; - const completion = await activate(t, act); - compareCompletion(t, completion, makeSuccess([makeCompleteWorkflowExecution()])); - } - t.deepEqual(logs, [['no change'], ['no change 2']]); -}); - -test('replay-no-marker-then-not-replay patchedWorkflow', async (t) => { - const { logs, workflowType } = t.context; - { - const act: coresdk.workflow_activation.IWorkflowActivation = { - runId: 'test-runId', - timestamp: msToTs(Date.now()), - isReplaying: true, - jobs: [makeInitializeWorkflowJob(workflowType)], - }; - const completion = await activate(t, act); - compareCompletion(t, completion, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100) })])); - } - // For this second activation we are no longer replaying, let core know we have the marker - { - const completion = await activate(t, makeFireTimer(1)); - compareCompletion( - t, - completion, - makeSuccess([makeSetPatchMarker('my-change-id', false), makeCompleteWorkflowExecution()]) - ); - } - t.deepEqual(logs, [['no change'], ['has change 2']]); -}); - -test('replay-with-marker patchedWorkflow', async (t) => { - const { logs, workflowType } = t.context; - { - const act: coresdk.workflow_activation.IWorkflowActivation = { - runId: 'test-runId', - timestamp: msToTs(Date.now()), - isReplaying: true, - jobs: [makeInitializeWorkflowJob(workflowType), makeNotifyHasPatchJob('my-change-id')], - }; - const completion = await activate(t, act); - compareCompletion( - t, - completion, - makeSuccess([ - makeSetPatchMarker('my-change-id', false), - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100) }), - ]) - ); - } - { - const completion = await activate(t, makeFireTimer(1)); - compareCompletion(t, completion, makeSuccess([makeCompleteWorkflowExecution()])); - } - t.deepEqual(logs, [['has change'], ['has change 2']]); -}); - -test('deprecatePatchWorkflow', async (t) => { - const { logs, workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess([makeSetPatchMarker('my-change-id', true), makeCompleteWorkflowExecution()]) - ); - } - t.deepEqual(logs, [['has change']]); -}); - -test('patchedTopLevel', async (t) => { - const { workflowType, logs } = t.context; - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, completion, makeSuccess()); - t.deepEqual(logs, [[['Patches cannot be used before Workflow starts']]]); -}); - -test('tryToContinueAfterCompletion', async (t) => { - const { workflowType } = t.context; - { - const completion = cleanWorkflowFailureStackTrace(await activate(t, makeStartWorkflow(workflowType))); - compareCompletion( - t, - 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) - ` - ), - ]) - ); - } -}); - -test('failUnlessSignaledBeforeStart', async (t) => { - const { workflowType } = t.context; - const completion = await activate( - t, - makeActivation(undefined, makeInitializeWorkflowJob(workflowType), { - signalWorkflow: { signalName: 'someShallPass' }, - }) - ); - compareCompletion(t, completion, makeSuccess(undefined, [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch])); -}); - -test('conditionWaiter', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion(t, completion, makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1) })])); - } - { - const completion = await activate(t, makeFireTimer(1)); - compareCompletion( - t, - completion, - makeSuccess([makeStartTimerCommand({ seq: 2, startToFireTimeout: msToTs('1s') })]) - ); - } - { - const completion = await activate(t, makeFireTimer(2)); - compareCompletion(t, completion, makeSuccess([makeCompleteWorkflowExecution()])); - } -}); - -test('conditionRacer', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs('1s') })]) - ); - } - { - const completion = await activate( - t, - makeActivation( - Date.now(), - { - signalWorkflow: { signalName: 'unblock', input: [] }, - }, - makeFireTimerJob(1) - ) - ); - compareCompletion( - t, - completion, - makeSuccess( - [makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload(true))], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -test('signalHandlersCanBeCleared', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs('20ms') })]) - ); - } - { - const completion = await activate( - t, - makeActivation( - Date.now(), - { - signalWorkflow: { signalName: 'unblock', input: [] }, - }, - { - signalWorkflow: { signalName: 'unblock', input: [] }, - }, - { - signalWorkflow: { signalName: 'unblock', input: [] }, - } - ) - ); - compareCompletion(t, completion, makeSuccess([], [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch])); - } - { - const completion = await activate(t, makeFireTimer(1)); - compareCompletion( - t, - completion, - makeSuccess( - [makeStartTimerCommand({ seq: 2, startToFireTimeout: msToTs('1ms') })], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } - { - const completion = await activate(t, makeFireTimer(2)); - compareCompletion( - t, - completion, - makeSuccess( - [makeStartTimerCommand({ seq: 3, startToFireTimeout: msToTs('1ms') })], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } - { - const completion = await activate(t, makeFireTimer(3)); - compareCompletion( - t, - completion, - makeSuccess( - [makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload(111))], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -test('waitOnUser', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess([makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs('30 days') })]) - ); - } - { - const completion = await activate(t, await makeSignalWorkflow('completeUserInteraction', [])); - compareCompletion(t, completion, makeSuccess(undefined, [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch])); - } -}); - -test('scopeCancelledWhileWaitingOnExternalWorkflowCancellation', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeStartWorkflow(workflowType)); - compareCompletion( - t, - completion, - makeSuccess([ - { - requestCancelExternalWorkflowExecution: { - seq: 1, - workflowExecution: { namespace: 'default', workflowId: 'irrelevant' }, - }, - }, - { - setPatchMarker: { deprecated: false, patchId: '__temporal_internal_connect_external_handle_cancel_to_scope' }, - }, - { - completeWorkflowExecution: { result: defaultPayloadConverter.toPayload(undefined) }, - }, - ]) - ); - } -}); - -test('query not found - successString', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, makeActivation(undefined, makeInitializeWorkflowJob(workflowType))); - compareCompletion( - t, - completion, - makeSuccess([makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload('success'))]) - ); - } - { - const completion = await activate(t, makeActivation(undefined, makeQueryWorkflowJob('qid', 'not-found'))); - compareCompletion( - t, - completion, - makeSuccess([ - makeRespondToQueryCommand({ - queryId: 'qid', - failed: { - message: - 'Workflow did not register a handler for not-found. Registered queries: [__stack_trace __enhanced_stack_trace __temporal_workflow_metadata]', - source: 'TypeScriptSDK', - stackTrace: - 'ReferenceError: Workflow did not register a handler for not-found. Registered queries: [__stack_trace __enhanced_stack_trace __temporal_workflow_metadata]', - applicationFailureInfo: { - type: 'ReferenceError', - nonRetryable: false, - }, - }, - }), - ]) - ); - } -}); - -test('Buffered signals are dispatched to correct handler and in correct order - signalsOrdering', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate( - t, - makeActivation( - undefined, - makeInitializeWorkflowJob(workflowType), - { signalWorkflow: { signalName: 'non-existant', input: toPayloads(defaultPayloadConverter, 1) } }, - { signalWorkflow: { signalName: 'signalA', input: toPayloads(defaultPayloadConverter, 2) } }, - { signalWorkflow: { signalName: 'signalA', input: toPayloads(defaultPayloadConverter, 3) } }, - { signalWorkflow: { signalName: 'signalC', input: toPayloads(defaultPayloadConverter, 4) } }, - { signalWorkflow: { signalName: 'signalB', input: toPayloads(defaultPayloadConverter, 5) } }, - { signalWorkflow: { signalName: 'non-existant', input: toPayloads(defaultPayloadConverter, 6) } }, - { signalWorkflow: { signalName: 'signalB', input: toPayloads(defaultPayloadConverter, 7) } } - ) - ); - - // Signal handlers will be registered in the following order: - // - // Registration of handler A => Processing of signalA#2 - // Deregistration of handler A => No more processing of signalA - // Registration of handler B => Processing of signalB#5, signalB#7 - // Registration of default handler => Processing of the rest of signals, in numeric order - // Registration of handler C => No signal pending for handler C - - compareCompletion( - t, - completion, - makeSuccess( - [ - makeCompleteWorkflowExecution( - defaultPayloadConverter.toPayload([ - { handler: 'signalA', args: [2] }, - { handler: 'signalB', args: [5] }, - { handler: 'signalB', args: [7] }, - { handler: 'default', signalName: 'non-existant', args: [1] }, - { handler: 'default', signalName: 'signalA', args: [3] }, - { handler: 'default', signalName: 'signalC', args: [4] }, - { handler: 'default', signalName: 'non-existant', args: [6] }, - ] as ProcessedSignal[]) - ), - ], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -test('Buffered signals dispatch is reentrant - signalsOrdering2', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate( - t, - makeActivation( - undefined, - makeInitializeWorkflowJob(workflowType), - { signalWorkflow: { signalName: 'non-existant', input: toPayloads(defaultPayloadConverter, 1) } }, - { signalWorkflow: { signalName: 'signalA', input: toPayloads(defaultPayloadConverter, 2) } }, - { signalWorkflow: { signalName: 'signalA', input: toPayloads(defaultPayloadConverter, 3) } }, - { signalWorkflow: { signalName: 'signalB', input: toPayloads(defaultPayloadConverter, 4) } }, - { signalWorkflow: { signalName: 'signalB', input: toPayloads(defaultPayloadConverter, 5) } }, - { signalWorkflow: { signalName: 'signalC', input: toPayloads(defaultPayloadConverter, 6) } }, - { signalWorkflow: { signalName: 'signalC', input: toPayloads(defaultPayloadConverter, 7) } } - ) - ); - compareCompletion( - t, - completion, - makeSuccess( - [ - makeCompleteWorkflowExecution( - defaultPayloadConverter.toPayload([ - { handler: 'signalA', args: [2] }, - { handler: 'signalB', args: [4] }, - { handler: 'signalC', args: [6] }, - { handler: 'default', signalName: 'non-existant', args: [1] }, - { handler: 'signalA', args: [3] }, - { handler: 'signalB', args: [5] }, - { handler: 'signalC', args: [7] }, - ] as ProcessedSignal[]) - ), - ], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -// Validate that issue #1474 is fixed in 1.11.0+ -test("Pending promises can't unblock between signals and updates - 1.11.0+ - signalUpdateOrderingWorkflow", async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, { - ...makeActivation(undefined, makeInitializeWorkflowJob(workflowType), { - doUpdate: { name: 'fooUpdate', protocolInstanceId: '1', runValidator: false, id: 'first' }, - }), - isReplaying: false, - }); - compareCompletion( - t, - completion, - makeSuccess([ - { updateResponse: { protocolInstanceId: '1', accepted: {} } }, - { updateResponse: { protocolInstanceId: '1', completed: defaultPayloadConverter.toPayload(1) } }, - ]) - ); - } - - { - const completion = await activate(t, { - ...makeActivation( - undefined, - { signalWorkflow: { signalName: 'fooSignal', input: [] } }, - { doUpdate: { name: 'fooUpdate', protocolInstanceId: '2', id: 'second' } } - ), - isReplaying: false, - }); - compareCompletion( - t, - completion, - makeSuccess( - [ - { updateResponse: { protocolInstanceId: '2', accepted: {} } }, - { updateResponse: { protocolInstanceId: '2', completed: defaultPayloadConverter.toPayload(3) } }, - { completeWorkflowExecution: { result: defaultPayloadConverter.toPayload(3) } }, - ], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -// Validate that issue #1474 legacy behavior is maintained when replaying from pre-1.11.0 history -test("Pending promises can't unblock between signals and updates - pre-1.11.0 - signalUpdateOrderingWorkflow", async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, { - ...makeActivation(undefined, makeInitializeWorkflowJob(workflowType), { - doUpdate: { name: 'fooUpdate', protocolInstanceId: '1', runValidator: false, id: 'first' }, - }), - isReplaying: true, - }); - compareCompletion( - t, - completion, - makeSuccess([ - { updateResponse: { protocolInstanceId: '1', accepted: {} } }, - { updateResponse: { protocolInstanceId: '1', completed: defaultPayloadConverter.toPayload(1) } }, - ]) - ); - } - - { - const completion = await activate(t, { - ...makeActivation( - undefined, - { signalWorkflow: { signalName: 'fooSignal', input: [] } }, - { doUpdate: { name: 'fooUpdate', protocolInstanceId: '2', id: 'second' } } - ), - isReplaying: true, - }); - compareCompletion( - t, - completion, - makeSuccess([ - { completeWorkflowExecution: { result: defaultPayloadConverter.toPayload(2) } }, - { updateResponse: { protocolInstanceId: '2', accepted: {} } }, - { updateResponse: { protocolInstanceId: '2', completed: defaultPayloadConverter.toPayload(3) } }, - ]) - ); - } -}); - -test('Signals/Updates/Activities/Timers have coherent promise completion ordering (no signal) - pre-1.11.0 compatibility - signalsActivitiesTimersPromiseOrdering', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, { - ...makeActivation(undefined, makeInitializeWorkflowJob(workflowType)), - isReplaying: true, - }); - compareCompletion( - t, - completion, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100) }), - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'myActivity', - scheduleToCloseTimeout: msToTs('10s'), - taskQueue: 'test-activity', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate(t, { - ...makeActivation( - undefined, - { doUpdate: { id: 'first', name: 'aaUpdate', protocolInstanceId: '1' } }, - makeFireTimerJob(1), - makeResolveActivityJob(1, { completed: {} }) - ), - isReplaying: true, - }); - compareCompletion( - t, - completion, - makeSuccess([ - { updateResponse: { protocolInstanceId: '1', accepted: {} } }, - { updateResponse: { protocolInstanceId: '1', completed: defaultPayloadConverter.toPayload(undefined) } }, - makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload([false, true, true, true])), - ]) - ); - } -}); - -test('Signals/Updates/Activities/Timers have coherent promise completion ordering (w/ signals) - pre-1.11.0 compatibility - signalsActivitiesTimersPromiseOrdering', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, { - ...makeActivation(undefined, makeInitializeWorkflowJob(workflowType)), - isReplaying: true, - }); - compareCompletion( - t, - completion, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100) }), - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'myActivity', - scheduleToCloseTimeout: msToTs('10s'), - taskQueue: 'test-activity', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate(t, { - ...makeActivation( - undefined, - makeSignalWorkflowJob('aaSignal', []), - { doUpdate: { id: 'first', name: 'aaUpdate', protocolInstanceId: '1' } }, - makeFireTimerJob(1), - makeResolveActivityJob(1, { completed: {} }) - ), - isReplaying: true, - }); - compareCompletion( - t, - completion, - makeSuccess([ - // Note the missing update responses here; this is due to #1474. The fact that the activity - // and timer completions have not been observed before the workflow completed is a related but - // distinct issue. But are resolved by the ProcessWorkflowActivationJobsAsSingleBatch fix. - makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload([true, false, false, false])), - { updateResponse: { protocolInstanceId: '1', accepted: {} } }, - { updateResponse: { protocolInstanceId: '1', completed: defaultPayloadConverter.toPayload(undefined) } }, - ]) - ); - } -}); - -test('Signals/Updates/Activities/Timers have coherent promise completion ordering (w/ signals) - signalsActivitiesTimersPromiseOrdering', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, { - ...makeActivation(undefined, makeInitializeWorkflowJob(workflowType)), - }); - compareCompletion( - t, - completion, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(100) }), - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'myActivity', - scheduleToCloseTimeout: msToTs('10s'), - taskQueue: 'test-activity', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate(t, { - ...makeActivation( - undefined, - makeSignalWorkflowJob('aaSignal', []), - { doUpdate: { id: 'first', name: 'aaUpdate', protocolInstanceId: '1' } }, - makeFireTimerJob(1), - makeResolveActivityJob(1, { completed: {} }) - ), - }); - compareCompletion( - t, - completion, - makeSuccess( - [ - { updateResponse: { protocolInstanceId: '1', accepted: {} } }, - { updateResponse: { protocolInstanceId: '1', completed: defaultPayloadConverter.toPayload(undefined) } }, - makeCompleteWorkflowExecution(defaultPayloadConverter.toPayload([true, true, true, true])), - ], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -test('Signals/Updates/Activities/Timers - Trace promises completion order - pre-1.11.0 compatibility - signalsActivitiesTimersPromiseOrderingTracer', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, { - ...makeActivation(undefined, makeInitializeWorkflowJob(workflowType)), - isReplaying: true, - }); - compareCompletion( - t, - completion, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1) }), - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'myActivity', - scheduleToCloseTimeout: msToTs('1s'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate(t, { - ...makeActivation( - undefined, - makeSignalWorkflowJob('aaSignal', ['signal1']), - makeUpdateActivationJob('first', '1', 'aaUpdate', ['update1']), - makeSignalWorkflowJob('aaSignal', ['signal2']), - makeUpdateActivationJob('second', '2', 'aaUpdate', ['update2']), - makeFireTimerJob(1), - makeResolveActivityJob(1, { completed: {} }) - ), - isReplaying: true, - }); - compareCompletion( - t, - completion, - makeSuccess([ - { updateResponse: { protocolInstanceId: '1', accepted: {} } }, - { updateResponse: { protocolInstanceId: '2', accepted: {} } }, - { updateResponse: { protocolInstanceId: '1', completed: defaultPayloadConverter.toPayload(undefined) } }, - { updateResponse: { protocolInstanceId: '2', completed: defaultPayloadConverter.toPayload(undefined) } }, - makeCompleteWorkflowExecution( - defaultPayloadConverter.toPayload( - [ - // Signals first (sync part, then microtasks) - 'signal1.sync, signal2.sync', - 'signal1.1, signal2.1, signal1.2, signal2.2, signal1.3, signal2.3, signal1.4, signal2.4', - - // Then update (sync part first), then microtasks for update+timers+activities - 'update1.sync, update2.sync', - 'update1.1, update2.1, timer.1, activity.1', - 'update1.2, update2.2, timer.2, activity.2', - 'update1.3, update2.3, timer.3, activity.3', - 'update1.4, update2.4, timer.4, activity.4', - ].flatMap((x) => x.split(', ')) - ) - ), - ]) - ); - } -}); - -test('Signals/Updates/Activities/Timers - Trace promises completion order - 1.11.0+ - signalsActivitiesTimersPromiseOrderingTracer', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate(t, { - ...makeActivation(undefined, makeInitializeWorkflowJob(workflowType)), - }); - compareCompletion( - t, - completion, - makeSuccess([ - makeStartTimerCommand({ seq: 1, startToFireTimeout: msToTs(1) }), - makeScheduleActivityCommand({ - seq: 1, - activityId: '1', - activityType: 'myActivity', - scheduleToCloseTimeout: msToTs('1s'), - taskQueue: 'test', - doNotEagerlyExecute: false, - versioningIntent: coresdk.common.VersioningIntent.UNSPECIFIED, - }), - ]) - ); - } - { - const completion = await activate(t, { - ...makeActivation( - undefined, - makeSignalWorkflowJob('aaSignal', ['signal1']), - makeUpdateActivationJob('first', '1', 'aaUpdate', ['update1']), - makeSignalWorkflowJob('aaSignal', ['signal2']), - makeUpdateActivationJob('second', '2', 'aaUpdate', ['update2']), - makeFireTimerJob(1), - makeResolveActivityJob(1, { completed: {} }) - ), - }); - compareCompletion( - t, - completion, - makeSuccess( - [ - { updateResponse: { protocolInstanceId: '1', accepted: {} } }, - { updateResponse: { protocolInstanceId: '2', accepted: {} } }, - { updateResponse: { protocolInstanceId: '1', completed: defaultPayloadConverter.toPayload(undefined) } }, - { updateResponse: { protocolInstanceId: '2', completed: defaultPayloadConverter.toPayload(undefined) } }, - makeCompleteWorkflowExecution( - defaultPayloadConverter.toPayload( - [ - 'signal1.sync, update1.sync, signal2.sync, update2.sync', - 'signal1.1, update1.1, signal2.1, update2.1, timer.1, activity.1', - 'signal1.2, update1.2, signal2.2, update2.2, timer.2, activity.2', - 'signal1.3, update1.3, signal2.3, update2.3, timer.3, activity.3', - 'signal1.4, update1.4, signal2.4, update2.4, timer.4, activity.4', - ].flatMap((x) => x.split(', ')) - ) - ), - ], - [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -test('Buffered updates are dispatched in the correct order - updatesOrdering', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate( - t, - makeActivation( - undefined, - makeInitializeWorkflowJob(workflowType), - makeUpdateActivationJob('1', '1', 'non-existant', [1]), - makeUpdateActivationJob('2', '2', 'updateA', [2]), - makeUpdateActivationJob('3', '3', 'updateA', [3]), - makeUpdateActivationJob('4', '4', 'updateC', [4]), - makeUpdateActivationJob('5', '5', 'updateB', [5]), - makeUpdateActivationJob('6', '6', 'non-existant', [6]), - makeUpdateActivationJob('7', '7', 'updateB', [7]) - ) - ); - - // The activation above: - // - initializes the workflow - // - buffers all its updates (we attempt update jobs first, but since there are no handlers, they get buffered) - // - enters the workflow code - // - workflow code sets handler for updateA - // - handler is registered for updateA - // - we attempt to dispatch buffered updates - // - buffered updates for handler A are *accepted* but not executed - // (executing an update is a promise/async, so it instead goes on the node event queue) - // - we continue/re-enter the workflow code - // - ...and do the same pattern for updateB, the default update handler, the updateC - // - once updates have been accepted, node processes the waiting events in its queue (the waiting updates) - // - these are processesed in FIFO order, so we get execution for updateA, then updateB, the default handler, then updateC - - // As such, the expected order of these updates is the order that the handlers were registered. - // Note that because the default handler was registered *before* updateC, all remaining buffered updates were dispatched - // to it, including the update for updateC. - - compareCompletion( - t, - completion, - makeSuccess( - [ - // FIFO accepted order - makeUpdateAcceptedResponse('2'), - makeUpdateAcceptedResponse('3'), - makeUpdateAcceptedResponse('5'), - makeUpdateAcceptedResponse('7'), - makeUpdateAcceptedResponse('1'), - makeUpdateAcceptedResponse('4'), - makeUpdateAcceptedResponse('6'), - // FIFO executed order - makeUpdateCompleteResponse('2', { handler: 'updateA', args: [2] }), - makeUpdateCompleteResponse('3', { handler: 'updateA', args: [3] }), - makeUpdateCompleteResponse('5', { handler: 'updateB', args: [5] }), - makeUpdateCompleteResponse('7', { handler: 'updateB', args: [7] }), - makeUpdateCompleteResponse('1', { handler: 'default', updateName: 'non-existant', args: [1] }), - // updateC handled by default handler. - makeUpdateCompleteResponse('4', { handler: 'default', updateName: 'updateC', args: [4] }), - makeUpdateCompleteResponse('6', { handler: 'default', updateName: 'non-existant', args: [6] }), - // No expected update response from updateC handler - makeCompleteWorkflowExecution(), - ] - // [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); - -test('Buffered updates are reentrant - updatesAreReentrant', async (t) => { - const { workflowType } = t.context; - { - const completion = await activate( - t, - makeActivation( - undefined, - makeInitializeWorkflowJob(workflowType), - makeUpdateActivationJob('1', '1', 'non-existant', [1]), - makeUpdateActivationJob('2', '2', 'updateA', [2]), - makeUpdateActivationJob('3', '3', 'updateA', [3]), - makeUpdateActivationJob('4', '4', 'updateC', [4]), - makeUpdateActivationJob('5', '5', 'updateB', [5]), - makeUpdateActivationJob('6', '6', 'non-existant', [6]), - makeUpdateActivationJob('7', '7', 'updateB', [7]), - makeUpdateActivationJob('8', '8', 'updateC', [8]) - ) - ); - - // The activation above: - // - initializes the workflow - // - buffers all its updates (we attempt update jobs first, but since there are no handlers, they get buffered) - // - enters the workflow code - // - workflow code sets handler for updateA - // - handler is registered for updateA - // - we attempt to dispatch buffered updates - // - buffered updates for handler A are *accepted* but not executed - // (executing an update is a promise/async, so it instead goes on the node event queue) - // - however, there is no more workflow code, node dequues event queue and we immediately run the update handler - // (we begin executing the update which...) - // - deletes the current handler and registers the next one (updateB) - // - this pattern repeats (updateA -> updateB -> updateC -> default) until there are no more updates to handle - // - at this point, all updates have been accepted and are executing - // - due to the call order in the workflow, the completion order of the updates follows the call stack, LIFO - - // This workflow is interesting in that updates are accepted FIFO, but executed LIFO - - compareCompletion( - t, - completion, - makeSuccess( - [ - // FIFO accepted order - makeUpdateAcceptedResponse('2'), - makeUpdateAcceptedResponse('5'), - makeUpdateAcceptedResponse('4'), - makeUpdateAcceptedResponse('1'), - makeUpdateAcceptedResponse('3'), - makeUpdateAcceptedResponse('7'), - makeUpdateAcceptedResponse('8'), - makeUpdateAcceptedResponse('6'), - // LIFO executed order - makeUpdateCompleteResponse('6', { handler: 'default', updateName: 'non-existant', args: [6] }), - makeUpdateCompleteResponse('8', { handler: 'updateC', args: [8] }), - makeUpdateCompleteResponse('7', { handler: 'updateB', args: [7] }), - makeUpdateCompleteResponse('3', { handler: 'updateA', args: [3] }), - makeUpdateCompleteResponse('1', { handler: 'default', updateName: 'non-existant', args: [1] }), - makeUpdateCompleteResponse('4', { handler: 'updateC', args: [4] }), - makeUpdateCompleteResponse('5', { handler: 'updateB', args: [5] }), - makeUpdateCompleteResponse('2', { handler: 'updateA', args: [2] }), - makeCompleteWorkflowExecution(), - ] - // [SdkFlags.ProcessWorkflowActivationJobsAsSingleBatch] - ) - ); - } -}); diff --git a/packages/worker/src/runtime-options.ts b/packages/worker/src/runtime-options.ts index b92ad017e..d38db5a87 100644 --- a/packages/worker/src/runtime-options.ts +++ b/packages/worker/src/runtime-options.ts @@ -468,7 +468,7 @@ export type MakeTelemetryFilterStringOptions = CoreLogFilterOptions; */ export function makeTelemetryFilterString(options: CoreLogFilterOptions): string { const { core, other } = options; - return `${other ?? 'ERROR'},temporal_sdk_core=${core},temporal_client=${core},temporal_sdk=${core}`; + return `${other ?? 'ERROR'},temporalio_sdk_core=${core},temporalio_client=${core},temporalio_common=${core}`; } function isOtelCollectorExporter(metrics: MetricsExporterConfig): metrics is OtelCollectorExporter {