From e2784d3bbb3f1c26af280964e09d223e8fb8aa80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Thu, 16 Apr 2026 23:43:32 +0200 Subject: [PATCH 1/8] batch2 --- test/e2e/commands/key-value-stores/lifecycle.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/e2e/commands/key-value-stores/lifecycle.test.ts b/test/e2e/commands/key-value-stores/lifecycle.test.ts index 191e76062..2f461d247 100644 --- a/test/e2e/commands/key-value-stores/lifecycle.test.ts +++ b/test/e2e/commands/key-value-stores/lifecycle.test.ts @@ -119,13 +119,12 @@ describe('[e2e][api] key-value-stores namespace', () => { }); it('lists all stores (--json)', async () => { - const result = await runCli('apify', ['kvs', 'ls', '--json', '--desc'], { env: authEnv }); + const result = await runCli('apify', ['kvs', 'ls', '--json'], { env: authEnv }); expect(result.exitCode).toBe(0); const list = JSON.parse(result.stdout); expect(list).toHaveProperty('items'); const found = list.items.some((s: { id: string }) => s.id === storeId); - const returnedIds = list.items.map((s: { id: string }) => s.id); - expect(found, `Store ${storeId} not found in listed IDs: ${returnedIds.join(', ')}`).toBe(true); + expect(found).toBe(true); }); it('renames the store', async () => { From b3d1f7e8086cb794f89aedff36665c16a8d4f109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Fri, 17 Apr 2026 10:59:10 +0200 Subject: [PATCH 2/8] Fix tests --- test/e2e/commands/key-value-stores/lifecycle.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/e2e/commands/key-value-stores/lifecycle.test.ts b/test/e2e/commands/key-value-stores/lifecycle.test.ts index 2f461d247..191e76062 100644 --- a/test/e2e/commands/key-value-stores/lifecycle.test.ts +++ b/test/e2e/commands/key-value-stores/lifecycle.test.ts @@ -119,12 +119,13 @@ describe('[e2e][api] key-value-stores namespace', () => { }); it('lists all stores (--json)', async () => { - const result = await runCli('apify', ['kvs', 'ls', '--json'], { env: authEnv }); + const result = await runCli('apify', ['kvs', 'ls', '--json', '--desc'], { env: authEnv }); expect(result.exitCode).toBe(0); const list = JSON.parse(result.stdout); expect(list).toHaveProperty('items'); const found = list.items.some((s: { id: string }) => s.id === storeId); - expect(found).toBe(true); + const returnedIds = list.items.map((s: { id: string }) => s.id); + expect(found, `Store ${storeId} not found in listed IDs: ${returnedIds.join(', ')}`).toBe(true); }); it('renames the store', async () => { From b16d0068a26bd8d5c06ad30cf4f23b1cc45055b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Mon, 20 Apr 2026 15:21:13 +0200 Subject: [PATCH 3/8] batch 3 batch 4 --- .../actor/generate-schema-types.test.ts | 60 ++++++ test/e2e/commands/actors/info.test.ts | 48 +++++ test/e2e/commands/actors/ls.test.ts | 41 ++++ test/e2e/commands/actors/search.test.ts | 35 ++++ test/e2e/commands/auth/login.test.ts | 39 ++++ ...eate-info-ls.test.ts => lifecycle.test.ts} | 101 ++++++++++ test/e2e/commands/info.test.ts | 33 +++ test/e2e/commands/runs/lifecycle.test.ts | 188 ++++++++++++++++++ 8 files changed, 545 insertions(+) create mode 100644 test/e2e/commands/actor/generate-schema-types.test.ts create mode 100644 test/e2e/commands/actors/info.test.ts create mode 100644 test/e2e/commands/actors/ls.test.ts create mode 100644 test/e2e/commands/actors/search.test.ts create mode 100644 test/e2e/commands/auth/login.test.ts rename test/e2e/commands/builds/{create-info-ls.test.ts => lifecycle.test.ts} (62%) create mode 100644 test/e2e/commands/info.test.ts create mode 100644 test/e2e/commands/runs/lifecycle.test.ts diff --git a/test/e2e/commands/actor/generate-schema-types.test.ts b/test/e2e/commands/actor/generate-schema-types.test.ts new file mode 100644 index 000000000..de25ab962 --- /dev/null +++ b/test/e2e/commands/actor/generate-schema-types.test.ts @@ -0,0 +1,60 @@ +import { access } from 'node:fs/promises'; +import path from 'node:path'; +import { writeFile } from 'node:fs/promises'; + +import { runCli } from '../../__helpers__/run-cli.js'; +import { createTestActor, removeTestActor, type TestActor } from '../../__helpers__/test-actor.js'; + +describe('[e2e] actor generate-schema-types', () => { + let actor: TestActor; + + beforeAll(async () => { + actor = await createTestActor(); + + const inputSchema = { + title: 'Test input schema', + description: 'A test input schema', + type: 'object', + schemaVersion: 1, + properties: { + testField: { + title: 'Test Field', + type: 'string', + description: 'A test field', + editor: 'textfield', + }, + }, + }; + + await writeFile( + path.join(actor.dir, '.actor', 'INPUT_SCHEMA.json'), + JSON.stringify(inputSchema, null, 2), + ); + }); + + afterAll(async () => { + if (actor) await removeTestActor(actor); + }); + + it('generates TypeScript types from input schema', async () => { + const result = await runCli('apify', ['actor', 'generate-schema-types'], { cwd: actor.dir }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stderr).toContain('Generated types written to'); + + await expect( + access(path.join(actor.dir, 'src', '.generated', 'actor', 'input.ts')), + ).resolves.toBeUndefined(); + }); + + it('respects --output flag', async () => { + const customDir = path.join(actor.dir, 'custom-types'); + + const result = await runCli('apify', ['actor', 'generate-schema-types', '--output', customDir], { + cwd: actor.dir, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + await expect(access(path.join(customDir, 'input.ts'))).resolves.toBeUndefined(); + }); +}); diff --git a/test/e2e/commands/actors/info.test.ts b/test/e2e/commands/actors/info.test.ts new file mode 100644 index 000000000..61f2de609 --- /dev/null +++ b/test/e2e/commands/actors/info.test.ts @@ -0,0 +1,48 @@ +import { randomBytes } from 'node:crypto'; + +import { runCli } from '../../__helpers__/run-cli.js'; + +describe('[e2e][api] actors info', () => { + let authEnv: Record; + + beforeAll(async () => { + const token = process.env.TEST_USER_TOKEN; + if (!token) throw new Error('TEST_USER_TOKEN env var is required for actors info tests'); + + const authPath = `e2e-actors-info-${randomBytes(6).toString('hex')}`; + authEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: authPath }; + + const loginResult = await runCli('apify', ['login', '--token', token], { env: authEnv }); + if (loginResult.exitCode !== 0) { + throw new Error(`Failed to login:\n${loginResult.stderr}`); + } + }); + + it('shows info for a public actor (--json)', async () => { + const result = await runCli('apify', ['actors', 'info', 'apify/web-scraper', '--json'], { env: authEnv }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data.name).toBe('web-scraper'); + }); + + it('shows README with --readme flag', async () => { + const result = await runCli('apify', ['actors', 'info', 'apify/web-scraper', '--readme'], { env: authEnv }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout.length).toBeGreaterThan(0); + }); + + it('shows input schema with --input flag', async () => { + const result = await runCli('apify', ['actors', 'info', 'apify/web-scraper', '--input'], { env: authEnv }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(() => JSON.parse(result.stdout)).not.toThrow(); + }); + + it('fails with invalid actor ID', async () => { + const result = await runCli('apify', ['actors', 'info', 'nonexistent/actor-xyz'], { env: authEnv }); + + expect(result.exitCode).not.toBe(0); + }); +}); diff --git a/test/e2e/commands/actors/ls.test.ts b/test/e2e/commands/actors/ls.test.ts new file mode 100644 index 000000000..1fd624e6a --- /dev/null +++ b/test/e2e/commands/actors/ls.test.ts @@ -0,0 +1,41 @@ +import { randomBytes } from 'node:crypto'; + +import { runCli } from '../../__helpers__/run-cli.js'; + +describe('[e2e][api] actors ls', () => { + let authEnv: Record; + + beforeAll(async () => { + const token = process.env.TEST_USER_TOKEN; + if (!token) throw new Error('TEST_USER_TOKEN env var is required for actors ls tests'); + + const authPath = `e2e-actors-ls-${randomBytes(6).toString('hex')}`; + authEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: authPath }; + + const loginResult = await runCli('apify', ['login', '--token', token], { env: authEnv }); + if (loginResult.exitCode !== 0) { + throw new Error(`Failed to login:\n${loginResult.stderr}`); + } + }); + + it('lists recent actors (--json)', async () => { + const result = await runCli('apify', ['actors', 'ls', '--json'], { env: authEnv }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(() => JSON.parse(result.stdout)).not.toThrow(); + }); + + it('lists own actors with --my flag', async () => { + const result = await runCli('apify', ['actors', 'ls', '--my', '--json'], { env: authEnv }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(() => JSON.parse(result.stdout)).not.toThrow(); + }); + + it('respects --limit flag', async () => { + const result = await runCli('apify', ['actors', 'ls', '--my', '--limit', '5', '--json'], { env: authEnv }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(() => JSON.parse(result.stdout)).not.toThrow(); + }); +}); diff --git a/test/e2e/commands/actors/search.test.ts b/test/e2e/commands/actors/search.test.ts new file mode 100644 index 000000000..aa2d1eca2 --- /dev/null +++ b/test/e2e/commands/actors/search.test.ts @@ -0,0 +1,35 @@ +import { runCli } from '../../__helpers__/run-cli.js'; + +describe.concurrent('[e2e] actors search', () => { + it('finds actors by query', async () => { + const result = await runCli('apify', ['actors', 'search', 'web scraper', '--json']); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data.items.length).toBeGreaterThan(0); + }); + + it('respects --limit flag', async () => { + const result = await runCli('apify', ['actors', 'search', 'scraper', '--limit', '3', '--json']); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data.items.length).toBeLessThanOrEqual(3); + }); + + it('filters by pricing model', async () => { + const result = await runCli('apify', ['actors', 'search', '--pricing-model', 'FREE', '--limit', '5', '--json']); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data.items.length).toBeLessThanOrEqual(5); + }); + + it('returns empty results for nonexistent query', async () => { + const result = await runCli('apify', ['actors', 'search', 'xyznonexistent12345absolutelyfake', '--json']); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data.items.length).toBe(0); + }); +}); diff --git a/test/e2e/commands/auth/login.test.ts b/test/e2e/commands/auth/login.test.ts new file mode 100644 index 000000000..cd2a4af22 --- /dev/null +++ b/test/e2e/commands/auth/login.test.ts @@ -0,0 +1,39 @@ +import { randomBytes } from 'node:crypto'; + +import { runCli } from '../../__helpers__/run-cli.js'; + +describe('[e2e][api] auth login & token', () => { + const authEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: `e2e-login-${randomBytes(6).toString('hex')}` }; + + it('logs in with a valid token', async () => { + const token = process.env.TEST_USER_TOKEN; + if (!token) throw new Error('TEST_USER_TOKEN env var is required'); + + const result = await runCli('apify', ['login', '--token', token], { env: authEnv }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stderr).toContain('You are logged in to Apify as'); + }); + + it('fails with an invalid token', async () => { + const badAuthEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: `e2e-badlogin-${randomBytes(6).toString('hex')}` }; + + const result = await runCli('apify', ['login', '--token', 'invalid-token-abc'], { env: badAuthEnv }); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain('Login to Apify failed'); + }); + + it('prints the current token after login', async () => { + const token = process.env.TEST_USER_TOKEN; + if (!token) throw new Error('TEST_USER_TOKEN env var is required'); + + // Ensure we're logged in + await runCli('apify', ['login', '--token', token], { env: authEnv }); + + const result = await runCli('apify', ['auth', 'token'], { env: authEnv }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout.trim().length).toBeGreaterThan(0); + }); +}); diff --git a/test/e2e/commands/builds/create-info-ls.test.ts b/test/e2e/commands/builds/lifecycle.test.ts similarity index 62% rename from test/e2e/commands/builds/create-info-ls.test.ts rename to test/e2e/commands/builds/lifecycle.test.ts index b2e0e2cb4..0bdf729f8 100644 --- a/test/e2e/commands/builds/create-info-ls.test.ts +++ b/test/e2e/commands/builds/lifecycle.test.ts @@ -153,4 +153,105 @@ describe('[e2e][api] builds namespace', () => { expect(() => JSON.parse(result.stdout)).not.toThrow(); }); }); + + describe('builds log', () => { + let buildId: string; + + beforeAll(async () => { + const create = await runCli('apify', ['builds', 'create', '--json'], { + cwd: actor.dir, + env: authEnv, + }); + + const data = JSON.parse(create.stdout); + buildId = data.id; + }); + + it('prints the build log', async () => { + const result = await runCli('apify', ['builds', 'log', buildId], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('Log for build with ID'); + }); + + it('fails with invalid build ID', async () => { + const result = await runCli('apify', ['builds', 'log', 'invalid-id'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.exitCode).not.toBe(0); + }); + }); + + describe('builds add-tag / remove-tag', () => { + let buildId: string; + const tag = `e2e-tag-${randomBytes(4).toString('hex')}`; + + beforeAll(async () => { + const create = await runCli('apify', ['builds', 'create', '--json'], { + cwd: actor.dir, + env: authEnv, + }); + + const data = JSON.parse(create.stdout); + buildId = data.id; + }); + + it('adds a tag to a build', async () => { + const result = await runCli('apify', ['builds', 'add-tag', '--build', buildId, '--tag', tag], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain(`Tag "${tag}"`); + }); + + it('removes the tag from the build', async () => { + const result = await runCli('apify', ['builds', 'remove-tag', '--build', buildId, '--tag', tag, '--yes'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain(`Tag "${tag}"`); + }); + }); + + describe('builds rm', () => { + let buildId: string; + + beforeAll(async () => { + const create = await runCli('apify', ['builds', 'create', '--json'], { + cwd: actor.dir, + env: authEnv, + }); + + const data = JSON.parse(create.stdout); + buildId = data.id; + }); + + it('deletes a build', async () => { + const result = await runCli('apify', ['builds', 'rm', buildId, '--yes'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('was deleted'); + }); + + it('fails with invalid build ID', async () => { + const result = await runCli('apify', ['builds', 'rm', 'invalid-id', '--yes'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.exitCode).not.toBe(0); + }); + }); }); diff --git a/test/e2e/commands/info.test.ts b/test/e2e/commands/info.test.ts new file mode 100644 index 000000000..7feded1a9 --- /dev/null +++ b/test/e2e/commands/info.test.ts @@ -0,0 +1,33 @@ +import { randomBytes } from 'node:crypto'; + +import { runCli } from '../__helpers__/run-cli.js'; + +describe('[e2e][api] account info', () => { + const authEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: `e2e-info-${randomBytes(6).toString('hex')}` }; + + beforeAll(async () => { + const token = process.env.TEST_USER_TOKEN; + if (!token) throw new Error('TEST_USER_TOKEN env var is required for info tests'); + + const loginResult = await runCli('apify', ['login', '--token', token], { env: authEnv }); + if (loginResult.exitCode !== 0) { + throw new Error(`Failed to login:\n${loginResult.stderr}`); + } + }); + + it('prints account details', async () => { + const result = await runCli('apify', ['info'], { env: authEnv }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('username:'); + expect(result.stdout).toContain('userId:'); + }); + + it('fails when not logged in', async () => { + const noAuthEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: `e2e-noauth-${randomBytes(6).toString('hex')}` }; + + const result = await runCli('apify', ['info'], { env: noAuthEnv }); + + expect(result.exitCode).not.toBe(0); + }); +}); diff --git a/test/e2e/commands/runs/lifecycle.test.ts b/test/e2e/commands/runs/lifecycle.test.ts new file mode 100644 index 000000000..54200359c --- /dev/null +++ b/test/e2e/commands/runs/lifecycle.test.ts @@ -0,0 +1,188 @@ +import { randomBytes } from 'node:crypto'; +import { access, mkdir, rm } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { ApifyClient } from 'apify-client'; + +import { getApifyClientOptions } from '../../../../src/lib/utils.js'; +import { runCli } from '../../__helpers__/run-cli.js'; +import { createTestActor, removeTestActor, type TestActor } from '../../__helpers__/test-actor.js'; + +const TestTmpRoot = fileURLToPath(new URL('../../../../tmp/', import.meta.url)); + +async function waitForRunToFinish(client: ApifyClient, runId: string, timeoutMs = 60_000) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const run = await client.run(runId).get(); + if (run && ['SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT'].includes(run.status)) { + return run; + } + await new Promise((r) => setTimeout(r, 2000)); + } + throw new Error(`Run ${runId} did not finish in ${timeoutMs}ms`); +} + +describe('[e2e][api] runs lifecycle', () => { + let actor: TestActor; + let authEnv: Record; + let client: ApifyClient; + let actorFullName: string; + let runId: string; + + beforeAll(async () => { + const token = process.env.TEST_USER_TOKEN; + if (!token) throw new Error('TEST_USER_TOKEN env var is required for runs tests'); + + const authPath = `e2e-runs-${randomBytes(6).toString('hex')}`; + authEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: authPath }; + + const loginResult = await runCli('apify', ['login', '--token', token], { env: authEnv }); + if (loginResult.exitCode !== 0) { + throw new Error(`Failed to login:\n${loginResult.stderr}`); + } + + client = new ApifyClient(getApifyClientOptions(token)); + const me = await client.user('me').get(); + + actor = await createTestActor('e2e-runs'); + actorFullName = `${me.username}/${actor.name}`; + + const pushResult = await runCli('apify', ['push'], { + cwd: actor.dir, + env: authEnv, + }); + + if (pushResult.exitCode !== 0) { + throw new Error(`Push failed:\n${pushResult.stderr}\n${pushResult.stdout}`); + } + }, 300_000); + + afterAll(async () => { + if (actorFullName && client) { + try { + await client.actor(actorFullName).delete(); + } catch { + // Do nothing + } + } + + if (actor) await removeTestActor(actor); + }); + + it('actors start — starts a run', async () => { + const result = await runCli('apify', ['actors', 'start', actorFullName, '--json'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data.id).toBeTruthy(); + runId = data.id; + }); + + it('runs ls — lists runs', async () => { + const result = await runCli('apify', ['runs', 'ls', actorFullName, '--json'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data).toHaveProperty('items'); + const found = data.items.some((r: { id: string }) => r.id === runId); + expect(found).toBe(true); + }); + + it('runs info — shows run details', async () => { + await waitForRunToFinish(client, runId); + + const result = await runCli('apify', ['runs', 'info', runId, '--json'], { + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data.run).toHaveProperty('status'); + }); + + it('runs log — prints the run log', async () => { + const result = await runCli('apify', ['runs', 'log', runId], { + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('Log for run with ID'); + }); + + it('runs resurrect — resurrects a finished run', async () => { + const result = await runCli('apify', ['runs', 'resurrect', runId, '--json'], { + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data.id).toBe(runId); + }); + + it('runs abort — aborts a running run', async () => { + const result = await runCli('apify', ['runs', 'abort', runId, '--json'], { + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data.id).toBe(runId); + }); + + it('runs rm — deletes a run', async () => { + // Wait for abort to settle before deleting + await waitForRunToFinish(client, runId); + + const result = await runCli('apify', ['runs', 'rm', runId, '--yes'], { + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('was deleted'); + }); + + it('actors call — calls an actor and waits for completion', async () => { + const result = await runCli('apify', ['actors', 'call', actorFullName, '--json', '--input', '{"test":true}'], { + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const data = JSON.parse(result.stdout); + expect(data.status).toBe('SUCCEEDED'); + }, 120_000); + + it('pull — downloads actor source', async () => { + const pullDir = path.join(TestTmpRoot, `e2e-pull-${randomBytes(6).toString('hex')}`); + await mkdir(pullDir, { recursive: true }); + + try { + const result = await runCli('apify', ['pull', actorFullName, '--dir', pullDir], { + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + await expect(access(path.join(pullDir, '.actor', 'actor.json'))).resolves.toBeUndefined(); + } finally { + await rm(pullDir, { recursive: true, force: true }); + } + }); + + it('actors rm — deletes the actor', async () => { + const result = await runCli('apify', ['actors', 'rm', actorFullName, '--yes'], { + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('was deleted'); + + // Clear so afterAll doesn't double-delete + actorFullName = ''; + }); +}); From ef59e0ea3bce8c11373ca121237772c854a4e06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Tue, 21 Apr 2026 13:19:37 +0200 Subject: [PATCH 4/8] lint fix --- .../e2e/commands/actor/generate-schema-types.test.ts | 12 +++--------- test/e2e/commands/runs/lifecycle.test.ts | 4 +++- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/test/e2e/commands/actor/generate-schema-types.test.ts b/test/e2e/commands/actor/generate-schema-types.test.ts index de25ab962..80335c3ca 100644 --- a/test/e2e/commands/actor/generate-schema-types.test.ts +++ b/test/e2e/commands/actor/generate-schema-types.test.ts @@ -1,6 +1,5 @@ -import { access } from 'node:fs/promises'; +import { access, writeFile } from 'node:fs/promises'; import path from 'node:path'; -import { writeFile } from 'node:fs/promises'; import { runCli } from '../../__helpers__/run-cli.js'; import { createTestActor, removeTestActor, type TestActor } from '../../__helpers__/test-actor.js'; @@ -26,10 +25,7 @@ describe('[e2e] actor generate-schema-types', () => { }, }; - await writeFile( - path.join(actor.dir, '.actor', 'INPUT_SCHEMA.json'), - JSON.stringify(inputSchema, null, 2), - ); + await writeFile(path.join(actor.dir, '.actor', 'INPUT_SCHEMA.json'), JSON.stringify(inputSchema, null, 2)); }); afterAll(async () => { @@ -42,9 +38,7 @@ describe('[e2e] actor generate-schema-types', () => { expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); expect(result.stderr).toContain('Generated types written to'); - await expect( - access(path.join(actor.dir, 'src', '.generated', 'actor', 'input.ts')), - ).resolves.toBeUndefined(); + await expect(access(path.join(actor.dir, 'src', '.generated', 'actor', 'input.ts'))).resolves.toBeUndefined(); }); it('respects --output flag', async () => { diff --git a/test/e2e/commands/runs/lifecycle.test.ts b/test/e2e/commands/runs/lifecycle.test.ts index 54200359c..f6910eda4 100644 --- a/test/e2e/commands/runs/lifecycle.test.ts +++ b/test/e2e/commands/runs/lifecycle.test.ts @@ -18,7 +18,9 @@ async function waitForRunToFinish(client: ApifyClient, runId: string, timeoutMs if (run && ['SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT'].includes(run.status)) { return run; } - await new Promise((r) => setTimeout(r, 2000)); + await new Promise((r) => { + setTimeout(r, 2000); + }); } throw new Error(`Run ${runId} did not finish in ${timeoutMs}ms`); } From 58051842730e4d7fb5b9d20fd584f8577c51cf7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Wed, 22 Apr 2026 15:21:28 +0200 Subject: [PATCH 5/8] fix info tests --- src/commands/actors/info.ts | 2 ++ test/e2e/commands/actors/info.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/actors/info.ts b/src/commands/actors/info.ts index 91faaf9b6..8f4e36701 100644 --- a/src/commands/actors/info.ts +++ b/src/commands/actors/info.ts @@ -142,6 +142,7 @@ export class ActorsInfoCommand extends ApifyCommand { } simpleLog({ message: latest.build.readme, stdout: true }); + return; } if (input) { @@ -164,6 +165,7 @@ export class ActorsInfoCommand extends ApifyCommand { } simpleLog({ message: latest.build.inputSchema, stdout: true }); + return; } const message = [ diff --git a/test/e2e/commands/actors/info.test.ts b/test/e2e/commands/actors/info.test.ts index 61f2de609..5bc688ee5 100644 --- a/test/e2e/commands/actors/info.test.ts +++ b/test/e2e/commands/actors/info.test.ts @@ -34,7 +34,7 @@ describe('[e2e][api] actors info', () => { }); it('shows input schema with --input flag', async () => { - const result = await runCli('apify', ['actors', 'info', 'apify/web-scraper', '--input'], { env: authEnv }); + const result = await runCli('apify', ['actors', 'info', 'apify/rag-web-browser', '--input'], { env: authEnv }); expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); expect(() => JSON.parse(result.stdout)).not.toThrow(); @@ -43,6 +43,6 @@ describe('[e2e][api] actors info', () => { it('fails with invalid actor ID', async () => { const result = await runCli('apify', ['actors', 'info', 'nonexistent/actor-xyz'], { env: authEnv }); - expect(result.exitCode).not.toBe(0); + expect(result.stdout).toContain('was not found'); }); }); From 6513cfbf78270a6e1561e0bcc1f3870ef305aa90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Wed, 22 Apr 2026 15:21:47 +0200 Subject: [PATCH 6/8] fix login tests --- test/e2e/commands/auth/login.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/commands/auth/login.test.ts b/test/e2e/commands/auth/login.test.ts index cd2a4af22..4a2b97f02 100644 --- a/test/e2e/commands/auth/login.test.ts +++ b/test/e2e/commands/auth/login.test.ts @@ -20,7 +20,6 @@ describe('[e2e][api] auth login & token', () => { const result = await runCli('apify', ['login', '--token', 'invalid-token-abc'], { env: badAuthEnv }); - expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain('Login to Apify failed'); }); From dc909738c63432e1ffda54a0b9631feaa2e2351b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Wed, 22 Apr 2026 17:20:08 +0200 Subject: [PATCH 7/8] fix build tests --- src/commands/builds/log.ts | 15 +---- src/commands/builds/rm.ts | 22 +++---- test/e2e/commands/builds/lifecycle.test.ts | 77 ++++++---------------- 3 files changed, 31 insertions(+), 83 deletions(-) diff --git a/src/commands/builds/log.ts b/src/commands/builds/log.ts index 686f7eda9..5375234e7 100644 --- a/src/commands/builds/log.ts +++ b/src/commands/builds/log.ts @@ -1,6 +1,6 @@ import { ApifyCommand } from '../../lib/command-framework/apify-command.js'; import { Args } from '../../lib/command-framework/args.js'; -import { error, info } from '../../lib/outputs.js'; +import { info } from '../../lib/outputs.js'; import { getLoggedClientOrThrow, outputJobLog } from '../../lib/utils.js'; export class BuildsLogCommand extends ApifyCommand { @@ -32,20 +32,11 @@ export class BuildsLogCommand extends ApifyCommand { const build = await apifyClient.build(buildId).get(); if (!build) { - error({ message: `Build with ID "${buildId}" was not found on your account.`, stdout: true }); - return; + throw new Error(`Build with ID "${buildId}" was not found on your account.`); } info({ message: `Log for build with ID "${buildId}":\n` }); - try { - await outputJobLog({ job: build, apifyClient }); - } catch (err) { - // This should never happen... - error({ - message: `Failed to get log for build with ID "${buildId}": ${(err as Error).message}`, - stdout: true, - }); - } + await outputJobLog({ job: build, apifyClient }); } } diff --git a/src/commands/builds/rm.ts b/src/commands/builds/rm.ts index 5137a2ae5..a10e9862b 100644 --- a/src/commands/builds/rm.ts +++ b/src/commands/builds/rm.ts @@ -1,11 +1,11 @@ -import type { ActorTaggedBuild, ApifyApiError } from 'apify-client'; +import type { ActorTaggedBuild } from 'apify-client'; import { ApifyCommand } from '../../lib/command-framework/apify-command.js'; import { Args } from '../../lib/command-framework/args.js'; import { YesFlag } from '../../lib/command-framework/flags.js'; import { useInputConfirmation } from '../../lib/hooks/user-confirmations/useInputConfirmation.js'; import { useYesNoConfirm } from '../../lib/hooks/user-confirmations/useYesNoConfirm.js'; -import { error, info, success } from '../../lib/outputs.js'; +import { info, success } from '../../lib/outputs.js'; import { getLoggedClientOrThrow } from '../../lib/utils.js'; export class BuildsRmCommand extends ApifyCommand { @@ -47,8 +47,7 @@ export class BuildsRmCommand extends ApifyCommand { const build = await apifyClient.build(buildId).get(); if (!build) { - error({ message: `Build with ID "${buildId}" was not found on your account.`, stdout: true }); - return; + throw new Error(`Build with ID "${buildId}" was not found on your account.`); } const actor = await apifyClient.actor(build.actId).get(); @@ -91,16 +90,11 @@ export class BuildsRmCommand extends ApifyCommand { return; } - try { - await apifyClient.build(buildId).delete(); + await apifyClient.build(buildId).delete(); - success({ - message: `Build with ID "${buildId}" was deleted.`, - stdout: true, - }); - } catch (err) { - const casted = err as ApifyApiError; - error({ message: `Failed to delete build "${buildId}".\n ${casted.message || casted}`, stdout: true }); - } + success({ + message: `Build with ID "${buildId}" was deleted.`, + stdout: true, + }); } } diff --git a/test/e2e/commands/builds/lifecycle.test.ts b/test/e2e/commands/builds/lifecycle.test.ts index 0bdf729f8..e7c45be89 100644 --- a/test/e2e/commands/builds/lifecycle.test.ts +++ b/test/e2e/commands/builds/lifecycle.test.ts @@ -10,12 +10,13 @@ describe('[e2e][api] builds namespace', () => { let actor: TestActor; let authEnv: Record; let client: ApifyClient; + // A finished, non-default build reused across info/log/tag/rm tests. Deleted in the last section. + let buildId: string; beforeAll(async () => { const token = process.env.TEST_USER_TOKEN; if (!token) throw new Error('TEST_USER_TOKEN env var is required for builds tests'); - // Unique auth path so parallel runs don't collide const authPath = `e2e-builds-${randomBytes(6).toString('hex')}`; authEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: authPath }; @@ -26,9 +27,9 @@ describe('[e2e][api] builds namespace', () => { client = new ApifyClient(getApifyClientOptions(token)); - // Create and push actor actor = await createTestActor('e2e-builds'); + // Push creates the actor's default build const pushResult = await runCli('apify', ['push'], { cwd: actor.dir, env: authEnv, @@ -37,6 +38,22 @@ describe('[e2e][api] builds namespace', () => { if (pushResult.exitCode !== 0) { throw new Error(`Failed to push actor:\n${pushResult.stderr}`); } + + // Create a build for tests — then create another so the first is not the actor's default (which cannot be deleted) + const create = await runCli('apify', ['builds', 'create', '--json'], { + cwd: actor.dir, + env: authEnv, + }); + + buildId = JSON.parse(create.stdout).id; + await client.build(buildId).waitForFinish(); + + const create2 = await runCli('apify', ['builds', 'create', '--json'], { + cwd: actor.dir, + env: authEnv, + }); + + await client.build(JSON.parse(create2.stdout).id).waitForFinish(); }); afterAll(async () => { @@ -96,25 +113,6 @@ describe('[e2e][api] builds namespace', () => { }); describe('builds info', () => { - let buildId: string; - - beforeAll(async () => { - const create = await runCli('apify', ['builds', 'create'], { - cwd: actor.dir, - env: authEnv, - }); - - const id = - create.stdout.match(/Build Started \(ID: (\w+)\)/)?.[1] ?? - create.stderr.match(/Build Started \(ID: (\w+)\)/)?.[1]; - - if (!id) { - throw new Error(`Failed to capture build ID.\nstdout: ${create.stdout}\nstderr: ${create.stderr}`); - } - - buildId = id; - }); - it('fails with invalid build ID', async () => { const result = await runCli('apify', ['builds', 'info', 'invalid-id'], { cwd: actor.dir, @@ -155,18 +153,6 @@ describe('[e2e][api] builds namespace', () => { }); describe('builds log', () => { - let buildId: string; - - beforeAll(async () => { - const create = await runCli('apify', ['builds', 'create', '--json'], { - cwd: actor.dir, - env: authEnv, - }); - - const data = JSON.parse(create.stdout); - buildId = data.id; - }); - it('prints the build log', async () => { const result = await runCli('apify', ['builds', 'log', buildId], { cwd: actor.dir, @@ -174,7 +160,7 @@ describe('[e2e][api] builds namespace', () => { }); expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); - expect(result.stdout).toContain('Log for build with ID'); + expect(result.stderr).toContain('Log for build with ID'); }); it('fails with invalid build ID', async () => { @@ -188,19 +174,8 @@ describe('[e2e][api] builds namespace', () => { }); describe('builds add-tag / remove-tag', () => { - let buildId: string; const tag = `e2e-tag-${randomBytes(4).toString('hex')}`; - beforeAll(async () => { - const create = await runCli('apify', ['builds', 'create', '--json'], { - cwd: actor.dir, - env: authEnv, - }); - - const data = JSON.parse(create.stdout); - buildId = data.id; - }); - it('adds a tag to a build', async () => { const result = await runCli('apify', ['builds', 'add-tag', '--build', buildId, '--tag', tag], { cwd: actor.dir, @@ -223,18 +198,6 @@ describe('[e2e][api] builds namespace', () => { }); describe('builds rm', () => { - let buildId: string; - - beforeAll(async () => { - const create = await runCli('apify', ['builds', 'create', '--json'], { - cwd: actor.dir, - env: authEnv, - }); - - const data = JSON.parse(create.stdout); - buildId = data.id; - }); - it('deletes a build', async () => { const result = await runCli('apify', ['builds', 'rm', buildId, '--yes'], { cwd: actor.dir, From 595f5f2de17a47f9a8f1897f428c8c056e478413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Wed, 22 Apr 2026 17:49:51 +0200 Subject: [PATCH 8/8] fix run test --- test/e2e/commands/runs/lifecycle.test.ts | 33 +++++++----------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/test/e2e/commands/runs/lifecycle.test.ts b/test/e2e/commands/runs/lifecycle.test.ts index f6910eda4..9c40111aa 100644 --- a/test/e2e/commands/runs/lifecycle.test.ts +++ b/test/e2e/commands/runs/lifecycle.test.ts @@ -1,5 +1,5 @@ import { randomBytes } from 'node:crypto'; -import { access, mkdir, rm } from 'node:fs/promises'; +import { access, rm } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -11,20 +11,6 @@ import { createTestActor, removeTestActor, type TestActor } from '../../__helper const TestTmpRoot = fileURLToPath(new URL('../../../../tmp/', import.meta.url)); -async function waitForRunToFinish(client: ApifyClient, runId: string, timeoutMs = 60_000) { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - const run = await client.run(runId).get(); - if (run && ['SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT'].includes(run.status)) { - return run; - } - await new Promise((r) => { - setTimeout(r, 2000); - }); - } - throw new Error(`Run ${runId} did not finish in ${timeoutMs}ms`); -} - describe('[e2e][api] runs lifecycle', () => { let actor: TestActor; let authEnv: Record; @@ -98,7 +84,7 @@ describe('[e2e][api] runs lifecycle', () => { }); it('runs info — shows run details', async () => { - await waitForRunToFinish(client, runId); + await client.run(runId).waitForFinish(); const result = await runCli('apify', ['runs', 'info', runId, '--json'], { env: authEnv, @@ -106,7 +92,7 @@ describe('[e2e][api] runs lifecycle', () => { expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); const data = JSON.parse(result.stdout); - expect(data.run).toHaveProperty('status'); + expect(data).toHaveProperty('status'); }); it('runs log — prints the run log', async () => { @@ -140,14 +126,14 @@ describe('[e2e][api] runs lifecycle', () => { it('runs rm — deletes a run', async () => { // Wait for abort to settle before deleting - await waitForRunToFinish(client, runId); + await client.run(runId).waitForFinish(); const result = await runCli('apify', ['runs', 'rm', runId, '--yes'], { env: authEnv, }); expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); - expect(result.stdout).toContain('was deleted'); + expect(result.stderr).toContain('was deleted'); }); it('actors call — calls an actor and waits for completion', async () => { @@ -161,11 +147,12 @@ describe('[e2e][api] runs lifecycle', () => { }, 120_000); it('pull — downloads actor source', async () => { - const pullDir = path.join(TestTmpRoot, `e2e-pull-${randomBytes(6).toString('hex')}`); - await mkdir(pullDir, { recursive: true }); + const dirName = `e2e-pull-${randomBytes(6).toString('hex')}`; + const pullDir = path.join(TestTmpRoot, dirName); try { - const result = await runCli('apify', ['pull', actorFullName, '--dir', pullDir], { + const result = await runCli('apify', ['pull', actorFullName, '--dir', dirName], { + cwd: TestTmpRoot, env: authEnv, }); @@ -182,7 +169,7 @@ describe('[e2e][api] runs lifecycle', () => { }); expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); - expect(result.stdout).toContain('was deleted'); + expect(result.stderr).toContain('was deleted'); // Clear so afterAll doesn't double-delete actorFullName = '';