diff --git a/package.json b/package.json index 0a65dbe18c..9f38d8095a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "build:watch": "pnpm run build:protos && tsc --build --watch packages/*/tsconfig.json", "build:protos": "tsx ./packages/proto/scripts/compile-proto.ts", "test": "pnpm --recursive --parallel --workspace-concurrency=1 --stream run test", + "test:external": "pnpm --dir packages/test run test:external", "test:watch": "pnpm --recursive --stream run test:watch", "ci-stress": "node ./packages/test/lib/load/run-all-stress-ci-scenarios.js", "ci-nightly": "node ./packages/test/lib/load/run-all-nightly-scenarios.js", diff --git a/packages/test-helpers/package.json b/packages/test-helpers/package.json index ca51a79dc9..588bd421b6 100644 --- a/packages/test-helpers/package.json +++ b/packages/test-helpers/package.json @@ -18,6 +18,7 @@ "license": "MIT", "dependencies": { "@temporalio/common": "workspace:*", + "@temporalio/envconfig": "workspace:*", "@temporalio/proto": "workspace:*", "@temporalio/testing": "workspace:*", "@temporalio/worker": "workspace:*", diff --git a/packages/test-helpers/src/environment.ts b/packages/test-helpers/src/environment.ts index 2658a01b0b..1c1826c531 100644 --- a/packages/test-helpers/src/environment.ts +++ b/packages/test-helpers/src/environment.ts @@ -1,10 +1,12 @@ import type { LocalTestWorkflowEnvironmentOptions } from '@temporalio/testing'; import { workflowInterceptorModules as defaultWorkflowInterceptorModules } from '@temporalio/testing'; +import { loadClientConnectConfig } from '@temporalio/envconfig'; import type { BundlerPlugin, WorkflowBundleWithSourceMap, BundleOptions } from '@temporalio/worker'; import { bundleWorkflowCode, DefaultLogger } from '@temporalio/worker'; import { defineSearchAttributeKey, SearchAttributeType } from '@temporalio/common/lib/search-attributes'; import { TestWorkflowEnvironment } from './wrappers'; import { baseBundlerIgnoreModules } from './bundler'; +import { isSet } from './flags'; export const defaultDynamicConfigOptions = [ 'system.enableActivityEagerExecution=true', @@ -76,14 +78,26 @@ export async function createLocalTestEnvironment( } /** - * Create a test workflow environment, using an existing server if TEMPORAL_SERVICE_ADDRESS is set, - * otherwise creating a local one. + * Create a test workflow environment. + * + * Uses envconfig for the test server connection when TEMPORAL_TEST_ENV_CONFIG_SERVER is truthy, uses + * TEMPORAL_SERVICE_ADDRESS as a legacy existing-server shortcut when set, otherwise creates a local environment. */ export async function createTestWorkflowEnvironment( opts?: LocalTestWorkflowEnvironmentOptions ): Promise { let env: TestWorkflowEnvironment; - if (process.env.TEMPORAL_SERVICE_ADDRESS) { + if (isSet(process.env.TEMPORAL_TEST_ENV_CONFIG_SERVER, false)) { + const { namespace, connectionOptions } = loadClientConnectConfig(); + const { address, apiKey, metadata, tls } = connectionOptions; + env = await TestWorkflowEnvironment.createFromExistingServer({ + address, + namespace, + connectionOptions: { apiKey, metadata, tls }, + client: opts?.client, + plugins: opts?.plugins, + }); + } else if (isSet(process.env.TEMPORAL_SERVICE_ADDRESS, false)) { env = await TestWorkflowEnvironment.createFromExistingServer({ address: process.env.TEMPORAL_SERVICE_ADDRESS, plugins: opts?.plugins, diff --git a/packages/test-helpers/src/helpers.ts b/packages/test-helpers/src/helpers.ts index c704764944..d0dd80a104 100644 --- a/packages/test-helpers/src/helpers.ts +++ b/packages/test-helpers/src/helpers.ts @@ -73,6 +73,7 @@ export function helpers): Promise { return await Worker.create({ connection: env.nativeConnection, + namespace: env.namespace, workflowBundle, taskQueue, showStackTraceSources: true, diff --git a/packages/test-helpers/tsconfig.json b/packages/test-helpers/tsconfig.json index 9cb59c3d55..3f9d6e5a66 100644 --- a/packages/test-helpers/tsconfig.json +++ b/packages/test-helpers/tsconfig.json @@ -7,6 +7,7 @@ "references": [ { "path": "../client" }, { "path": "../common" }, + { "path": "../envconfig" }, { "path": "../proto" }, { "path": "../testing" }, { "path": "../worker" }, diff --git a/packages/test/EXTERNAL_SERVER_TESTS.md b/packages/test/EXTERNAL_SERVER_TESTS.md new file mode 100644 index 0000000000..9f723efcbd --- /dev/null +++ b/packages/test/EXTERNAL_SERVER_TESTS.md @@ -0,0 +1,131 @@ +# `@temporalio/test` + +## External Server Test Coverage + +`pnpm run test:external` runs the subset of SDK integration tests that currently use the shared configurable test +harness and are expected to work against an existing Temporal server. + +When `TEMPORAL_TEST_ENV_CONFIG_SERVER` is truthy (`1`, `t`, or `true`), the shared harness uses envconfig to +configure the test server connection. Envconfig resolves settings from environment variables, `TEMPORAL_CONFIG_FILE`, +`TEMPORAL_PROFILE`, and default TOML discovery. + +- `TEMPORAL_TEST_ENV_CONFIG_SERVER` +- `TEMPORAL_ADDRESS` +- `TEMPORAL_NAMESPACE` +- `TEMPORAL_API_KEY` +- `TEMPORAL_CONFIG_FILE` +- `TEMPORAL_PROFILE` +- `TEMPORAL_TLS_CLIENT_CERT_PATH` +- `TEMPORAL_TLS_CLIENT_KEY_PATH` +- `TEMPORAL_TLS_CLIENT_CERT_DATA` +- `TEMPORAL_TLS_CLIENT_KEY_DATA` +- `TEMPORAL_TLS_SERVER_CA_CERT_PATH` +- `TEMPORAL_TLS_SERVER_CA_CERT_DATA` +- `TEMPORAL_TLS_SERVER_NAME` +- `TEMPORAL_GRPC_META_*` + +`TEMPORAL_SERVICE_ADDRESS` remains supported as a legacy shortcut for connecting to an existing server without +envconfig. Use `TEMPORAL_TEST_ENV_CONFIG_SERVER` for namespace, auth, TLS, metadata, and TOML configuration. + +Example: + +```sh +pnpm run build +TEMPORAL_TEST_ENV_CONFIG_SERVER=true \ +TEMPORAL_ADDRESS=namespace.account.tmprl.cloud:7233 \ +TEMPORAL_NAMESPACE=namespace.account \ +TEMPORAL_API_KEY=... \ +pnpm run test:external +``` + +TOML example: + +```sh +pnpm run build +TEMPORAL_TEST_ENV_CONFIG_SERVER=true \ +TEMPORAL_CONFIG_FILE=/path/to/temporal.toml \ +pnpm run test:external +``` + +`workflowEnvironmentOpts.server.extraArgs` only apply when the harness starts a local server. In external mode, the +target server must already have equivalent dynamic config and feature flags enabled. + +## Current External Runner + +| Test file | Notes | +| -------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `test-default-activity.ts` | Shared harness. | +| `test-integration-activities.ts` | Shared harness. | +| `test-integration-cancellation-scopes.ts` | Shared harness. | +| `test-integration-replay-and-flags.ts` | Shared harness; time-skipping-only block is local-only. | +| `test-integration-reserved-prefixes.ts` | Shared harness. | +| `test-integration-update.ts` | Shared harness; assumes external server long-poll behavior is compatible with the assertions. | +| `test-integration-update-interceptors.ts` | Shared harness. | +| `test-integration-workflows-with-recorded-logs.ts` | Shared harness. | +| `test-isolation.ts` | Shared harness. | +| `test-nexus-operation-timeouts.ts` | Requires Nexus endpoint creation/operator permission. | +| `test-nexus-signal-linking.ts` | Requires Nexus and CHASM signal backlink server support. | +| `test-nexus-standalone.ts` | Requires standalone Nexus operation server support. | +| `test-nexus-temporal-operation.ts` | Requires standalone Nexus operation and CHASM callback server support. | +| `test-random-stream-reset.ts` | Shared harness. | +| `test-worker-tuner.ts` | Shared harness. | +| `test-workflow-async-local-storage.ts` | Shared harness. | +| `test-workflow-cancellation.ts` | Shared harness. | +| `test-workflow-fail-on-errors-policy.ts` | Shared harness. | +| `test-workflow-nexus-cancellation.ts` | Requires Nexus endpoint creation/operator permission. | + +## Follow-Up Coverage Inventory + +| Test file | Status / notes | +| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `test-async-completion.ts` | Uses default `Connection.connect()` in integration setup; needs shared harness connection. | +| `test-bundler.ts` | Uses default client/worker connection in integration block; needs shared harness connection. | +| `test-client-connection.ts` | Tests client behavior against local fake gRPC servers; not a cloud server test. | +| `test-client-errors.ts` | Direct `TestWorkflowEnvironment.createLocal()`. | +| `test-custom-payload-codec.ts` | Uses default client/worker connection in integration block. | +| `test-custom-payload-converter.ts` | Uses default client/worker connection in integration block. | +| `test-default-workflow.ts` | Direct `TestWorkflowEnvironment.createLocal()`. | +| `test-ephemeral-server.ts` | Intentionally tests local ephemeral/time-skipping servers. | +| `test-failure-converter.ts` | Direct `TestWorkflowEnvironment.createLocal()`. | +| `test-integration-split-one.ts` | Shared harness, but multi-codec environments share task queues; also has custom Search Attribute and hard-coded `default` namespace assertions. | +| `test-integration-split-two.ts` | Shared harness, but multi-codec environments share task queues and require custom Search Attributes. | +| `test-integration-split-three.ts` | Shared harness, but multi-codec environments share task queues; otherwise likely addable after validation. | +| `test-integration-workflow-info.ts` | Mostly shared harness; one test uses `createLocalTestEnvironment()` for custom Search Attribute setup. | +| `test-integration-workflow-start.ts` | Shared harness; has extra native worker connections and eager activity/start-delay behavior that need external validation. | +| `test-interceptors.ts` | Uses default client/worker connection in integration block. | +| `test-local-activities.ts` | Direct local environment setup; local activities do not require a local server but this file needs shared harness conversion. | +| `test-metrics-custom.ts` | Shared harness with local Prometheus scrape endpoint; likely addable after external validation. | +| `test-native-connection.ts` | Primarily local fake gRPC/default-connection behavior; not a cloud release test. | +| `test-native-connection-headers.ts` | Local fake gRPC server tests; not a cloud release test. | +| `test-nexus-codec-converter-errors.ts` | Shared harness; requires Nexus endpoint creation/operator permission and external validation. | +| `test-nexus-handler.ts` | Direct local environment with Nexus dynamic config; needs shared harness conversion or local-only classification. | +| `test-nexus-workflow-caller.ts` | Shared harness; has a hard-coded `default` namespace request and requires Nexus endpoint creation/operator permission. | +| `test-nyc-coverage.ts` | Uses default client/worker connection in integration block. | +| `test-patch-and-condition.ts` | Uses default client/worker connection in integration block. | +| `test-payload-converter.ts` | Uses default client/worker connection in integration block. | +| `test-plugins.ts` | Direct local environments and plugin-specific local connection assertions. | +| `test-runtime.ts` | Mixes shared harness and default native connections; runtime-focused tests need local-only review. | +| `test-runtime-otel.ts` | Direct local environment with local telemetry endpoint. | +| `test-runtime-prometheus.ts` | Direct local environment with local Prometheus endpoint. | +| `test-schedules.ts` | Uses default connection and operator Search Attribute registration; needs envconfig connection and pre-provisioned/managed Search Attributes. | +| `test-serialization-context.ts` | Shared harness but has hard-coded `workflow.default.*` expectations. | +| `test-serialization-context-codec.ts` | Shared harness but has hard-coded `workflow.default.*` and `activity.default.*` expectations. | +| `test-signal-query-patch.ts` | Direct `TestWorkflowEnvironment.createLocal()`. | +| `test-signals-are-always-delivered.ts` | Uses default client/worker connection in integration block. | +| `test-sinks.ts` | Uses default connection, operator Search Attribute registration, and hard-coded `default` namespace expectations. | +| `test-standalone-activities.ts` | Shared harness but has fixed task queue and local-only server dynamic config assumptions. | +| `test-temporal-cloud.ts` | Separate cloud smoke tests with bespoke cloud env vars; not part of envconfig external runner. | +| `test-testenvironment.ts` | Intentionally tests time-skipping test environment. | +| `test-typed-search-attributes.ts` | Uses local namespace/server Search Attribute setup through operator service. | +| `test-worker-connection-replacement.ts` | Intentionally creates a second local environment. | +| `test-worker-debug-mode.ts` | Uses default client/worker connection in integration block. | +| `test-worker-deployment-versioning.ts` | Shared harness; extra native connections now use env auth/TLS, but server feature support needs validation. | +| `test-worker-exposes-abortcontroller.ts` | Uses default client/worker connection in integration block. | +| `test-worker-exposes-textencoderdecoder.ts` | Uses default client/worker connection in integration block. | +| `test-worker-lifecycle.ts` | Uses default client/worker connection in integration block and has runtime/lifecycle-local assertions. | +| `test-worker-no-activities.ts` | Uses default client/worker connection in integration block. | +| `test-worker-no-workflows.ts` | Uses default client/worker connection in integration block. | +| `test-worker-poller-autoscale.ts` | Direct local environment with local Prometheus endpoint. | +| `test-worker-versioning.ts` | Uses default client/worker connection in integration block. | +| `test-workflow-log-interceptor.ts` | Direct `TestWorkflowEnvironment.createLocal()`. | +| `test-workflow-unhandled-rejection-crash.ts` | Uses default client/worker connection in integration block. | diff --git a/packages/test/package.json b/packages/test/package.json index 45d8c594fe..8f9381f37f 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -8,6 +8,7 @@ "build:ts": "tsc --build", "build:protos": "tsx ./scripts/compile-proto.ts", "test": "ava ./lib/test-*.js", + "test:external": "ava ./lib/test-default-activity.js ./lib/test-integration-activities.js ./lib/test-integration-cancellation-scopes.js ./lib/test-integration-replay-and-flags.js ./lib/test-integration-reserved-prefixes.js ./lib/test-integration-update.js ./lib/test-integration-update-interceptors.js ./lib/test-integration-workflows-with-recorded-logs.js ./lib/test-isolation.js ./lib/test-nexus-operation-timeouts.js ./lib/test-nexus-signal-linking.js ./lib/test-nexus-standalone.js ./lib/test-nexus-temporal-operation.js ./lib/test-random-stream-reset.js ./lib/test-worker-tuner.js ./lib/test-workflow-async-local-storage.js ./lib/test-workflow-cancellation.js ./lib/test-workflow-fail-on-errors-policy.js ./lib/test-workflow-nexus-cancellation.js", "test:watch": "ava --watch ./lib/test-*.js", "test:bun": "bun run -b ava ./lib/test-*.js" }, diff --git a/packages/test/src/helpers-integration.ts b/packages/test/src/helpers-integration.ts index cac93b103e..d4180121a0 100644 --- a/packages/test/src/helpers-integration.ts +++ b/packages/test/src/helpers-integration.ts @@ -171,7 +171,7 @@ export function helpers(t: ExecutionContext, env?: TestWorkflowEnvironm return { ...base, async createNativeConnection(opts?: Partial): Promise { - return await NativeConnection.connect({ address: testEnv.address, ...opts }); + return await NativeConnection.connect({ ...testEnv.connectionOptions, address: testEnv.address, ...opts }); }, async runReplayHistory( opts: Partial, diff --git a/packages/testing/src/testing-workflow-environment.ts b/packages/testing/src/testing-workflow-environment.ts index 14563422be..c7909e8111 100644 --- a/packages/testing/src/testing-workflow-environment.ts +++ b/packages/testing/src/testing-workflow-environment.ts @@ -33,14 +33,19 @@ export type TimeSkippingTestWorkflowEnvironmentOptions = { plugins?: (ClientPlugin | ConnectionPlugin | NativeConnectionPlugin)[]; }; +type ExistingServerConnectionOptions = Pick; + /** * Options for {@link TestWorkflowEnvironment.createExistingServer} + * + * Accepts connection options that can be used for both the client and worker connections. */ export type ExistingServerTestWorkflowEnvironmentOptions = { /** If not set, defaults to localhost:7233 */ address?: string; /** If not set, defaults to default */ namespace?: string; + connectionOptions?: ExistingServerConnectionOptions; client?: ClientOptionsForTestEnv; plugins?: (ClientPlugin | ConnectionPlugin | NativeConnectionPlugin)[]; }; @@ -99,7 +104,11 @@ export class TestWorkflowEnvironment { /** * Address used when constructing `connection` and `nativeConnection` */ - public readonly address: string + public readonly address: string, + /** + * Connection options used when constructing `connection` and `nativeConnection`. + */ + public readonly connectionOptions: ExistingServerConnectionOptions ) { this.connection = connection; this.nativeConnection = nativeConnection; @@ -198,6 +207,7 @@ export class TestWorkflowEnvironment { static async createFromExistingServer( opts?: ExistingServerTestWorkflowEnvironmentOptions ): Promise { + const { apiKey, metadata, tls } = opts?.connectionOptions ?? {}; return await this.create({ server: { type: 'existing' }, client: opts?.client, @@ -205,6 +215,7 @@ export class TestWorkflowEnvironment { namespace: opts?.namespace ?? 'default', supportsTimeSkipping: false, address: opts?.address, + connectionOptions: { apiKey, metadata, tls }, }); } @@ -216,9 +227,10 @@ export class TestWorkflowEnvironment { supportsTimeSkipping: boolean; namespace?: string; address?: string; + connectionOptions?: ExistingServerConnectionOptions; } ): Promise { - const { supportsTimeSkipping, namespace, ...rest } = opts; + const { supportsTimeSkipping, namespace, connectionOptions, ...rest } = opts; const optsWithDefaults = addDefaults(filterNullAndUndefined(rest)); let address: string; @@ -243,12 +255,15 @@ export class TestWorkflowEnvironment { server = 'existing'; } + const connectionOptionsWithDefaults = { ...(connectionOptions ?? {}), address }; const nativeConnection = await NativeConnection.connect({ + ...connectionOptionsWithDefaults, address, plugins: opts.plugins, [InternalConnectionOptionsSymbol]: { supportsTestService: supportsTimeSkipping }, }); const connection = await Connection.connect({ + ...connectionOptionsWithDefaults, address, plugins: opts.plugins, [InternalConnectionOptionsSymbol]: { supportsTestService: supportsTimeSkipping }, @@ -262,7 +277,8 @@ export class TestWorkflowEnvironment { connection, nativeConnection, namespace, - address + address, + connectionOptionsWithDefaults ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ce2aa8e1b..c2f44ca121 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -810,6 +810,9 @@ importers: '@temporalio/common': specifier: workspace:* version: link:../common + '@temporalio/envconfig': + specifier: workspace:* + version: link:../envconfig '@temporalio/proto': specifier: workspace:* version: link:../proto