From f4d7f9f2e0100beb4ff1bf86b0bc8b16af7698da Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Wed, 13 May 2026 17:53:12 -0500 Subject: [PATCH 1/5] Add standalone activity startDelay support Wire ActivityOptions.startDelay into StartActivityExecutionRequest and validate that it is non-negative. Port the standalone activity delayed-start integration coverage from Python, including the server version/config needed to assert the scheduled-to-started delay. --- packages/client/src/activity-client.ts | 10 +++- .../test/src/test-standalone-activities.ts | 47 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/client/src/activity-client.ts b/packages/client/src/activity-client.ts index 89709f169..7bc83b26e 100644 --- a/packages/client/src/activity-client.ts +++ b/packages/client/src/activity-client.ts @@ -17,7 +17,7 @@ import { decompileRetryPolicy, } from '@temporalio/common'; import type { Duration } from '@temporalio/common/lib/time'; -import { msOptionalToTs, optionalTsToDate, optionalTsToMs } from '@temporalio/common/lib/time'; +import { msOptionalToTs, msToNumber, optionalTsToDate, optionalTsToMs } from '@temporalio/common/lib/time'; import { composeInterceptors } from '@temporalio/common/lib/interceptors'; import { decodeTypedSearchAttributes, @@ -309,6 +309,7 @@ export class ActivityClient extends AsyncCompletionClient implements TypedActivi header: { fields: input.headers }, userMetadata: await encodeUserMetadata(this.dataConverter, input.options.summary, undefined), priority: input.options.priority ? compilePriority(input.options.priority) : undefined, + startDelay: msOptionalToTs(input.options.startDelay), }; } @@ -546,6 +547,10 @@ export interface ActivityOptions { * Priority to use when starting this activity. */ priority?: Priority; + /** + * Time to wait before dispatching the first activity task. This delay is not applied to retry attempts. + */ + startDelay?: Duration; /** * Specifies behavior if there's a *closed* activity with the same ID. */ @@ -571,6 +576,9 @@ function validateActivityOptions(options: ActivityOptions): void { if (!options.scheduleToCloseTimeout && !options.startToCloseTimeout) { throw new TypeError('Either scheduleToCloseTimeout or startToCloseTimeout is required'); } + if (options.startDelay !== undefined && msToNumber(options.startDelay) < 0) { + throw new TypeError('startDelay must be non-negative'); + } } function buildActivityExecutionInfoCommonPart( diff --git a/packages/test/src/test-standalone-activities.ts b/packages/test/src/test-standalone-activities.ts index 39397bb11..c2f803c03 100644 --- a/packages/test/src/test-standalone-activities.ts +++ b/packages/test/src/test-standalone-activities.ts @@ -4,6 +4,7 @@ import anyTest from 'ava'; import * as rxjs from 'rxjs'; import type { ActivityHandle, TypedActivityClient, ActivityOptions } from '@temporalio/client'; import { + ActivityExecutionStatus, ActivityExecutionAlreadyStartedError, ActivityExecutionFailedError, ServiceError, @@ -64,6 +65,7 @@ interface ActivityInterface { } const taskQueue = 'standalone-activities'; +const startDelayTestCliVersion = 'v1.7.1-standalone-nexus-operations'; const defaultOptions: Omit = { taskQueue, scheduleToCloseTimeout: '1 minute', @@ -118,8 +120,8 @@ if (RUN_INTEGRATION_TESTS) { test.after.always(async (t) => { t.context.worker.shutdown(); - await t.context.env.teardown(); await t.context.runPromise; + await t.context.env.teardown(); }); test('Get activity result - success', async (t) => { @@ -175,6 +177,49 @@ if (RUN_INTEGRATION_TESTS) { t.is(err?.cause?.message, 'failure'); }); + test('Start activity with start delay', async (t) => { + const env = await createLocalTestEnvironment({ + server: { + executable: { + type: 'cached-download', + version: startDelayTestCliVersion, + }, + extraArgs: ['--dynamic-config-value', 'activity.startDelayEnabled=true'], + }, + }); + + try { + const startDelayTaskQueue = `${taskQueue}-${uuid4()}`; + const worker = await Worker.create({ + activities: { echo }, + taskQueue: startDelayTaskQueue, + connection: env.nativeConnection, + }); + + await worker.runUntil(async () => { + const activityId = uuid4(); + const startDelayMs = 2000; + const handle = await env.client.activity.start('echo', { + ...defaultOptions, + id: activityId, + taskQueue: startDelayTaskQueue, + args: ['hello'], + startDelay: startDelayMs, + }); + + t.is(await handle.result(), 'hello'); + + const description = await handle.describe(); + t.is(description.status, ActivityExecutionStatus.COMPLETED); + t.truthy(description.scheduleTime); + t.truthy(description.lastStartedTime); + t.true(description.lastStartedTime!.getTime() - description.scheduleTime!.getTime() >= startDelayMs - 500); + }); + } finally { + await env.teardown(); + } + }); + test('Describe activity from start handle', async (t) => { const client = t.context.env.client.activity; const activityId = randomUUID(); From 101757a335ce7a37c3148f67c579b0525cebec73 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Thu, 14 May 2026 12:24:46 -0500 Subject: [PATCH 2/5] SAA delay integration test uses server version in CI. --- .github/workflows/ci.yml | 6 ++ .../test/src/test-standalone-activities.ts | 63 +++++++------------ 2 files changed, 28 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35c24ed3d..d2a2c499c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,6 +188,8 @@ jobs: - name: Install Temporal CLI uses: temporalio/setup-temporal@1059a504f87e7fa2f385e3fa40d1aa7e62f1c6ca # v0 + with: + version: ${{ env.TESTS_CLI_VERSION }} - name: Run Temporal CLI working-directory: ${{ runner.temp }} @@ -201,6 +203,8 @@ jobs: --dynamic-config-value history.enableRequestIdRefLinks=true \ --dynamic-config-value frontend.activityAPIsEnabled=true \ --dynamic-config-value activity.enableStandalone=true \ + --dynamic-config-value activity.startDelayEnabled=true \ + --dynamic-config-value 'activity.longPollTimeout="5000ms"' \ --dynamic-config-value history.enableChasm=true \ --dynamic-config-value history.enableTransitionHistory=true & @@ -210,6 +214,7 @@ jobs: env: RUN_INTEGRATION_TESTS: true REUSE_V8_CONTEXT: ${{ matrix.reuse-v8-context }} + TEMPORAL_SERVICE_ADDRESS: localhost:7233 # For Temporal Cloud + mTLS tests TEMPORAL_CLOUD_MTLS_TEST_TARGET_HOST: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }}.tmprl.cloud:7233 @@ -234,6 +239,7 @@ jobs: env: RUN_INTEGRATION_TESTS: true REUSE_V8_CONTEXT: ${{ matrix.reuse-v8-context }} + TEMPORAL_SERVICE_ADDRESS: localhost:7233 # FIXME: Move samples tests to a custom activity # Sample 1: hello-world to local server diff --git a/packages/test/src/test-standalone-activities.ts b/packages/test/src/test-standalone-activities.ts index c2f803c03..55a1b51d0 100644 --- a/packages/test/src/test-standalone-activities.ts +++ b/packages/test/src/test-standalone-activities.ts @@ -17,7 +17,7 @@ import type { TestWorkflowEnvironment } from './helpers'; import { RUN_INTEGRATION_TESTS, waitUntil, Worker } from './helpers'; import { echo, throwAnError } from './activities'; import { heartbeatCancellationDetailsActivity } from './activities/heartbeat-cancellation-details'; -import { createLocalTestEnvironment } from './helpers-integration'; +import { createTestWorkflowEnvironment } from './helpers-integration'; // Use a reduced server long-poll expiration timeout, in order to confirm that client // polling/retry strategies result in the expected behavior @@ -65,7 +65,6 @@ interface ActivityInterface { } const taskQueue = 'standalone-activities'; -const startDelayTestCliVersion = 'v1.7.1-standalone-nexus-operations'; const defaultOptions: Omit = { taskQueue, scheduleToCloseTimeout: '1 minute', @@ -80,9 +79,14 @@ async function waitForValue(subject: rxjs.Subject, value: T) { if (RUN_INTEGRATION_TESTS) { test.before(async (t) => { - const env = await createLocalTestEnvironment({ + const env = await createTestWorkflowEnvironment({ server: { - extraArgs: ['--dynamic-config-value', `activity.longPollTimeout="${LONG_POLL_TIMEOUT_MS}ms"`], + extraArgs: [ + '--dynamic-config-value', + `activity.longPollTimeout="${LONG_POLL_TIMEOUT_MS}ms"`, + '--dynamic-config-value', + 'activity.startDelayEnabled=true', + ], }, }); @@ -178,46 +182,23 @@ if (RUN_INTEGRATION_TESTS) { }); test('Start activity with start delay', async (t) => { - const env = await createLocalTestEnvironment({ - server: { - executable: { - type: 'cached-download', - version: startDelayTestCliVersion, - }, - extraArgs: ['--dynamic-config-value', 'activity.startDelayEnabled=true'], - }, + const client = t.context.env.client.activity; + const activityId = uuid4(); + const startDelayMs = 2000; + const handle = await client.start('echo', { + ...defaultOptions, + id: activityId, + args: ['hello'], + startDelay: startDelayMs, }); - try { - const startDelayTaskQueue = `${taskQueue}-${uuid4()}`; - const worker = await Worker.create({ - activities: { echo }, - taskQueue: startDelayTaskQueue, - connection: env.nativeConnection, - }); - - await worker.runUntil(async () => { - const activityId = uuid4(); - const startDelayMs = 2000; - const handle = await env.client.activity.start('echo', { - ...defaultOptions, - id: activityId, - taskQueue: startDelayTaskQueue, - args: ['hello'], - startDelay: startDelayMs, - }); - - t.is(await handle.result(), 'hello'); + t.is(await handle.result(), 'hello'); - const description = await handle.describe(); - t.is(description.status, ActivityExecutionStatus.COMPLETED); - t.truthy(description.scheduleTime); - t.truthy(description.lastStartedTime); - t.true(description.lastStartedTime!.getTime() - description.scheduleTime!.getTime() >= startDelayMs - 500); - }); - } finally { - await env.teardown(); - } + const description = await handle.describe(); + t.is(description.status, ActivityExecutionStatus.COMPLETED); + t.truthy(description.scheduleTime); + t.truthy(description.lastStartedTime); + t.true(description.lastStartedTime!.getTime() - description.scheduleTime!.getTime() >= startDelayMs - 500); }); test('Describe activity from start handle', async (t) => { From 48f1f5539e8c8e1d9e2ca24fa0eb275ba18a8ed7 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Mon, 18 May 2026 18:30:26 -0500 Subject: [PATCH 3/5] Use patched setup-temporal action --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2a2c499c..81c4b5f1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -187,7 +187,7 @@ jobs: run: pnpm tsx scripts/publish-to-verdaccio.ts --registry-dir ${{ steps.tmp-dir.outputs.dir }}/npm-registry - name: Install Temporal CLI - uses: temporalio/setup-temporal@1059a504f87e7fa2f385e3fa40d1aa7e62f1c6ca # v0 + uses: temporalio/setup-temporal@8bde337644eaaa6644b8b527d9a8406c2207de5b # v0 with: version: ${{ env.TESTS_CLI_VERSION }} From a06df0f0cd52cb3402f25692a9a044fb21496b9d Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Thu, 21 May 2026 18:26:01 -0500 Subject: [PATCH 4/5] Remove TEMPORAL_SERVICE_ADDRESS --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81c4b5f1d..03db6842a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,7 +214,6 @@ jobs: env: RUN_INTEGRATION_TESTS: true REUSE_V8_CONTEXT: ${{ matrix.reuse-v8-context }} - TEMPORAL_SERVICE_ADDRESS: localhost:7233 # For Temporal Cloud + mTLS tests TEMPORAL_CLOUD_MTLS_TEST_TARGET_HOST: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }}.tmprl.cloud:7233 @@ -239,7 +238,6 @@ jobs: env: RUN_INTEGRATION_TESTS: true REUSE_V8_CONTEXT: ${{ matrix.reuse-v8-context }} - TEMPORAL_SERVICE_ADDRESS: localhost:7233 # FIXME: Move samples tests to a custom activity # Sample 1: hello-world to local server From a34b13c04ad80381a6feffb84dfd5a58ed8b4993 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Thu, 21 May 2026 18:37:38 -0500 Subject: [PATCH 5/5] Fix stale uuid4 after rebase --- packages/test/src/test-standalone-activities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test/src/test-standalone-activities.ts b/packages/test/src/test-standalone-activities.ts index 55a1b51d0..04ddb0769 100644 --- a/packages/test/src/test-standalone-activities.ts +++ b/packages/test/src/test-standalone-activities.ts @@ -183,7 +183,7 @@ if (RUN_INTEGRATION_TESTS) { test('Start activity with start delay', async (t) => { const client = t.context.env.client.activity; - const activityId = uuid4(); + const activityId = randomUUID(); const startDelayMs = 2000; const handle = await client.start('echo', { ...defaultOptions,