From b4bf7062520d5a58690330db807c22365c666326 Mon Sep 17 00:00:00 2001 From: Josef Prochazka Date: Mon, 12 Oct 2026 11:49:15 +0000 Subject: [PATCH 1/2] fix: Send --timeout 0 (no timeout) to the API in call, actors start and task run The flag was forwarded only when truthy, so `--timeout 0` was dropped and the run got the Actor's default timeout instead of none. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015VTFswSQJstnQ6r8QeqfgK --- src/commands/actors/call.ts | 3 +- src/commands/actors/start.ts | 3 +- src/commands/task/run.ts | 3 +- test/local/commands/run-timeout-flag.test.ts | 117 +++++++++++++++++++ 4 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 test/local/commands/run-timeout-flag.test.ts diff --git a/src/commands/actors/call.ts b/src/commands/actors/call.ts index 1829c607c..5d6818282 100644 --- a/src/commands/actors/call.ts +++ b/src/commands/actors/call.ts @@ -136,7 +136,8 @@ export class ActorsCallCommand extends ApifyCommand { runOpts.build = this.flags.build; } - if (this.flags.timeout) { + // `0` means no timeout, so only an omitted flag falls back to the Actor's default. + if (this.flags.timeout !== undefined) { runOpts.timeout = this.flags.timeout; } diff --git a/src/commands/actors/start.ts b/src/commands/actors/start.ts index e04b91797..ea8292d74 100644 --- a/src/commands/actors/start.ts +++ b/src/commands/actors/start.ts @@ -95,7 +95,8 @@ export class ActorsStartCommand extends ApifyCommand runOpts.build = this.flags.build; } - if (this.flags.timeout) { + // `0` means no timeout, so only an omitted flag falls back to the Actor's default. + if (this.flags.timeout !== undefined) { runOpts.timeout = this.flags.timeout; } diff --git a/src/commands/task/run.ts b/src/commands/task/run.ts index 0f625e5d5..b525b96d5 100644 --- a/src/commands/task/run.ts +++ b/src/commands/task/run.ts @@ -54,7 +54,8 @@ export class TaskRunCommand extends ApifyCommand { runOpts.build = this.flags.build; } - if (this.flags.timeout) { + // `0` means no timeout, so only an omitted flag falls back to the Actor's default. + if (this.flags.timeout !== undefined) { runOpts.timeout = this.flags.timeout; } diff --git a/test/local/commands/run-timeout-flag.test.ts b/test/local/commands/run-timeout-flag.test.ts new file mode 100644 index 000000000..1fd68ff18 --- /dev/null +++ b/test/local/commands/run-timeout-flag.test.ts @@ -0,0 +1,117 @@ +import type { ApifyClient } from 'apify-client'; + +import { ActorsCallCommand } from '../../../src/commands/actors/call.js'; +import { ActorsStartCommand } from '../../../src/commands/actors/start.js'; +import { TaskRunCommand } from '../../../src/commands/task/run.js'; +import { testRunCommand } from '../../../src/lib/command-framework/apify-command.js'; +import type { RunOnCloudOptions } from '../../../src/lib/commands/run-on-cloud.js'; +import { useAuthSetup } from '../../__setup__/hooks/useAuthSetup.js'; +import { useConsoleSpy } from '../../__setup__/hooks/useConsoleSpy.js'; + +const { mockGetLoggedClientOrThrow, mockGetCurrentUserInfo, mockRunActorOrTaskOnCloud, mockFinalizeRun } = + vitest.hoisted(() => ({ + mockGetLoggedClientOrThrow: vitest.fn(), + mockGetCurrentUserInfo: vitest.fn(), + mockRunActorOrTaskOnCloud: vitest.fn(), + mockFinalizeRun: vitest.fn(), + })); + +vitest.mock('../../../src/lib/utils.js', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + getLoggedClientOrThrow: mockGetLoggedClientOrThrow, + getCurrentUserInfo: mockGetCurrentUserInfo, + }; +}); + +vitest.mock('../../../src/lib/commands/run-on-cloud.js', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + runActorOrTaskOnCloud: mockRunActorOrTaskOnCloud, + }; +}); + +vitest.mock('../../../src/lib/commands/run-result.js', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + finalizeRun: mockFinalizeRun, + }; +}); + +useAuthSetup(); +useConsoleSpy(); + +const actor = { + id: 'actor-id', + name: 'some-actor', + username: 'someone', + versions: [{ versionNumber: '0.0', buildTag: 'latest' }], + taggedBuilds: { latest: { buildId: 'build-id', buildNumber: '0.0.1' } }, +}; + +const task = { id: 'task-id', name: 'some-task', title: 'Some Task' }; + +const run = { + id: 'run-id', + status: 'SUCCEEDED', + startedAt: new Date('2024-01-01T00:00:00Z'), + buildNumber: '0.0.1', + options: { timeoutSecs: 0, memoryMbytes: 1024 }, + defaultDatasetId: 'dataset-id', +}; + +const fakeClient = () => + ({ + actor: () => ({ get: async () => actor }), + task: () => ({ get: async () => task }), + }) as unknown as ApifyClient; + +beforeEach(() => { + mockGetLoggedClientOrThrow.mockResolvedValue(fakeClient()); + mockGetCurrentUserInfo.mockResolvedValue({ id: 'user-id', username: 'someone' }); + mockRunActorOrTaskOnCloud.mockImplementation(async function* () { + yield run; + }); + mockFinalizeRun.mockResolvedValue(undefined); +}); + +afterEach(() => { + mockGetLoggedClientOrThrow.mockReset(); + mockGetCurrentUserInfo.mockReset(); + mockRunActorOrTaskOnCloud.mockReset(); + mockFinalizeRun.mockReset(); +}); + +function sentRunOptions(): RunOnCloudOptions['runOptions'] { + expect(mockRunActorOrTaskOnCloud).toHaveBeenCalledTimes(1); + return (mockRunActorOrTaskOnCloud.mock.calls[0][1] as RunOnCloudOptions).runOptions; +} + +const commands = [ + { name: 'apify call', command: ActorsCallCommand, args: { args_actorId: 'someone/some-actor' } }, + { name: 'apify actors start', command: ActorsStartCommand, args: { args_actorId: 'someone/some-actor' } }, + { name: 'apify task run', command: TaskRunCommand, args: { args_taskId: 'some-task' } }, +] as const; + +describe.each(commands)('$name --timeout', ({ command, args }) => { + it('sends --timeout 0 as a zero timeout (no timeout), not as an omitted option', async () => { + await testRunCommand(command as never, { ...args, flags_timeout: 0 } as never); + + expect(sentRunOptions()).toMatchObject({ timeout: 0 }); + }); + + it('sends a positive --timeout as is', async () => { + await testRunCommand(command as never, { ...args, flags_timeout: 120 } as never); + + expect(sentRunOptions()).toMatchObject({ timeout: 120 }); + }); + + it("leaves the timeout to the Actor's default when the flag is omitted", async () => { + await testRunCommand(command as never, { ...args } as never); + + expect(sentRunOptions()).not.toHaveProperty('timeout'); + }); +}); From b23edfcab4d1a749937725f0a31a8a8a5481fa1f Mon Sep 17 00:00:00 2001 From: Josef Prochazka Date: Mon, 12 Oct 2026 12:02:43 +0000 Subject: [PATCH 2/2] chore: Drop the comment on the timeout flag check --- src/commands/actors/call.ts | 1 - src/commands/actors/start.ts | 1 - src/commands/task/run.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/src/commands/actors/call.ts b/src/commands/actors/call.ts index 5d6818282..7aee4357e 100644 --- a/src/commands/actors/call.ts +++ b/src/commands/actors/call.ts @@ -136,7 +136,6 @@ export class ActorsCallCommand extends ApifyCommand { runOpts.build = this.flags.build; } - // `0` means no timeout, so only an omitted flag falls back to the Actor's default. if (this.flags.timeout !== undefined) { runOpts.timeout = this.flags.timeout; } diff --git a/src/commands/actors/start.ts b/src/commands/actors/start.ts index ea8292d74..a83582e35 100644 --- a/src/commands/actors/start.ts +++ b/src/commands/actors/start.ts @@ -95,7 +95,6 @@ export class ActorsStartCommand extends ApifyCommand runOpts.build = this.flags.build; } - // `0` means no timeout, so only an omitted flag falls back to the Actor's default. if (this.flags.timeout !== undefined) { runOpts.timeout = this.flags.timeout; } diff --git a/src/commands/task/run.ts b/src/commands/task/run.ts index b525b96d5..cd073701b 100644 --- a/src/commands/task/run.ts +++ b/src/commands/task/run.ts @@ -54,7 +54,6 @@ export class TaskRunCommand extends ApifyCommand { runOpts.build = this.flags.build; } - // `0` means no timeout, so only an omitted flag falls back to the Actor's default. if (this.flags.timeout !== undefined) { runOpts.timeout = this.flags.timeout; }