From 05d4614adc8cf4f0f4cad7002b1a499fda3ac62a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:09:25 +0000 Subject: [PATCH 1/4] Initial plan From 212838edf863e86e888b1e561f2ee561d5313bae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:22:17 +0000 Subject: [PATCH 2/4] Fix error handling to exit with code 1 on command errors Co-authored-by: B4nan <615580+B4nan@users.noreply.github.com> --- src/lib/command-framework/apify-command.ts | 17 +++++++++++-- test/local/commands/secrets/add.test.ts | 28 +++++++++++++++++----- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/lib/command-framework/apify-command.ts b/src/lib/command-framework/apify-command.ts index c1fe6a5ce..cab6900a3 100644 --- a/src/lib/command-framework/apify-command.ts +++ b/src/lib/command-framework/apify-command.ts @@ -301,15 +301,17 @@ export abstract class ApifyCommand>) { + private _printMissingRequiredArgs(missingRequiredArgs: Map>): never { const help = selectiveRenderHelpForCommand(this.ctor, { showUsageString: true, }); @@ -616,6 +627,8 @@ export abstract class ApifyCommand { } }); + afterEach(async () => { + // Clean up after each test + const secrets = getSecretsFile(); + if (secrets[SECRET_KEY]) { + await testRunCommand(SecretsRmCommand, { + args_name: SECRET_KEY, + }); + } + }); + it('should work', async () => { await testRunCommand(SecretsAddCommand, { args_name: SECRET_KEY, @@ -26,13 +36,19 @@ describe('apify secrets add', () => { expect(secrets[SECRET_KEY]).to.eql(SECRET_VALUE); }); - afterAll(async () => { - const secrets = getSecretsFile(); + it('should throw error when adding duplicate secret', async () => { + // First add a secret + await testRunCommand(SecretsAddCommand, { + args_name: SECRET_KEY, + args_value: SECRET_VALUE, + }); - if (secrets[SECRET_KEY]) { - await testRunCommand(SecretsRmCommand, { + // Try to add the same secret again and expect it to throw + await expect( + testRunCommand(SecretsAddCommand, { args_name: SECRET_KEY, - }); - } + args_value: SECRET_VALUE, + }), + ).rejects.toThrow(`Secret with name ${SECRET_KEY} already exists`); }); }); From e615fc9bc71e8457745caafcb2e975dc9bf1073a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:31:30 +0000 Subject: [PATCH 3/4] Update tests to expect errors to be thrown instead of just logged Co-authored-by: B4nan <615580+B4nan@users.noreply.github.com> --- ...-on-project-with-no-detected-start.test.ts | 4 +--- test/local/commands/crawlee/run.test.ts | 4 +--- test/local/commands/create.test.ts | 6 ++--- test/local/commands/run.test.ts | 22 ++++++------------- test/local/commands/validate-schema.test.ts | 22 +++++++++---------- 5 files changed, 22 insertions(+), 36 deletions(-) diff --git a/test/local/__fixtures__/commands/run/python/prints-error-message-on-project-with-no-detected-start.test.ts b/test/local/__fixtures__/commands/run/python/prints-error-message-on-project-with-no-detected-start.test.ts index 4c53b8b3d..fa9064448 100644 --- a/test/local/__fixtures__/commands/run/python/prints-error-message-on-project-with-no-detected-start.test.ts +++ b/test/local/__fixtures__/commands/run/python/prints-error-message-on-project-with-no-detected-start.test.ts @@ -37,8 +37,6 @@ describe('[python] prints error message on project with no detected start', () = }); it('should print error message', async () => { - await testRunCommand(RunCommand, {}); - - expect(lastErrorMessage()).toMatch(/Actor is of an unknown format./i); + await expect(testRunCommand(RunCommand, {})).rejects.toThrow(/Actor is of an unknown format./i); }); }); diff --git a/test/local/commands/crawlee/run.test.ts b/test/local/commands/crawlee/run.test.ts index 6d06d891b..5a2d36e08 100644 --- a/test/local/commands/crawlee/run.test.ts +++ b/test/local/commands/crawlee/run.test.ts @@ -63,9 +63,7 @@ describe('apify run', () => { it('throws when required field is not provided', async () => { await writeFile(inputPath, '{}'); - await testRunCommand(RunCommand, {}); - - expect(lastErrorMessage()).toMatch(/Field awesome is required/i); + await expect(testRunCommand(RunCommand, {})).rejects.toThrow(/Field awesome is required/i); }); it('prefills input with defaults', async () => { diff --git a/test/local/commands/create.test.ts b/test/local/commands/create.test.ts index 0e0fce83f..d23a5a54f 100644 --- a/test/local/commands/create.test.ts +++ b/test/local/commands/create.test.ts @@ -33,9 +33,9 @@ describe('apify create', () => { ['a'.repeat(151), 'sh', 'bad_escaped'].forEach((badActorName) => { it(`returns error with bad Actor name ${badActorName}`, async () => { - await testRunCommand(CreateCommand, { args_actorName: badActorName }); - - expect(lastErrorMessage()).toMatch(/the actor name/i); + await expect(testRunCommand(CreateCommand, { args_actorName: badActorName })).rejects.toThrow( + /the actor name/i, + ); }); }); diff --git a/test/local/commands/run.test.ts b/test/local/commands/run.test.ts index dbbf6fc9b..1aff578dd 100644 --- a/test/local/commands/run.test.ts +++ b/test/local/commands/run.test.ts @@ -292,45 +292,37 @@ writeFileSync(String.raw\`${joinPath('result.txt')}\`, 'hello world'); writeFileSync(inputPath, '{}', { flag: 'w' }); copyFileSync(missingRequiredPropertyInputSchemaPath, inputSchemaPath); - await testRunCommand(RunCommand, {}); - - expect(lastErrorMessage()).toMatch(/Field awesome is required/i); + await expect(testRunCommand(RunCommand, {})).rejects.toThrow(/Field awesome is required/i); }); it('throws when required field has wrong type', async () => { writeFileSync(inputPath, '{"awesome": 42}', { flag: 'w' }); copyFileSync(defaultsInputSchemaPath, inputSchemaPath); - await testRunCommand(RunCommand, {}); - - expect(lastErrorMessage()).toMatch(/Field awesome must be boolean/i); + await expect(testRunCommand(RunCommand, {})).rejects.toThrow(/Field awesome must be boolean/i); }); it('throws when passing manual input, but local file has correct input', async () => { writeFileSync(inputPath, '{"awesome": true}', { flag: 'w' }); copyFileSync(defaultsInputSchemaPath, inputSchemaPath); - await testRunCommand(RunCommand, { flags_input: handPassedInput }); - - expect(lastErrorMessage()).toMatch(/Field awesome must be boolean/i); + await expect(testRunCommand(RunCommand, { flags_input: handPassedInput })).rejects.toThrow( + /Field awesome must be boolean/i, + ); }); it('throws when input has default field of wrong type', async () => { writeFileSync(inputPath, '{"awesome": true, "help": 123}', { flag: 'w' }); copyFileSync(defaultsInputSchemaPath, inputSchemaPath); - await testRunCommand(RunCommand, {}); - - expect(lastErrorMessage()).toMatch(/Field help must be string/i); + await expect(testRunCommand(RunCommand, {})).rejects.toThrow(/Field help must be string/i); }); it('throws when input has prefilled field of wrong type', async () => { writeFileSync(inputPath, '{"awesome": true, "help": 123}', { flag: 'w' }); copyFileSync(prefillsInputSchemaPath, inputSchemaPath); - await testRunCommand(RunCommand, {}); - - expect(lastErrorMessage()).toMatch(/Field help must be string/i); + await expect(testRunCommand(RunCommand, {})).rejects.toThrow(/Field help must be string/i); }); it('automatically inserts missing defaulted fields', async () => { diff --git a/test/local/commands/validate-schema.test.ts b/test/local/commands/validate-schema.test.ts index d9fa9eeab..b9f19d8c8 100644 --- a/test/local/commands/validate-schema.test.ts +++ b/test/local/commands/validate-schema.test.ts @@ -19,20 +19,18 @@ describe('apify validate-schema', () => { }); it('should correctly validate schema 2', async () => { - await testRunCommand(ValidateInputSchemaCommand, { - args_path: invalidInputSchemaPath, - }); - - expect(lastErrorMessage()).to.contain( - 'Field schema.properties.queries.editor must be equal to one of the allowed values', - ); + await expect( + testRunCommand(ValidateInputSchemaCommand, { + args_path: invalidInputSchemaPath, + }), + ).rejects.toThrow(/Field schema.properties.queries.editor must be equal to one of the allowed values/); }); it('should correctly validate schema 3', async () => { - await testRunCommand(ValidateInputSchemaCommand, { - args_path: unparsableInputSchemaPath, - }); - - expect(lastErrorMessage()).to.contain.oneOf(['Unexpected token }', "Expected ',' or ']' after array element"]); + await expect( + testRunCommand(ValidateInputSchemaCommand, { + args_path: unparsableInputSchemaPath, + }), + ).rejects.toThrow(); }); }); From 38681dc232a6292bae020f7834373bc3eb542f77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:47:26 +0000 Subject: [PATCH 4/4] Fix linting errors - remove throw from finally block and unused variables Co-authored-by: vladfrangu <17960496+vladfrangu@users.noreply.github.com> --- src/lib/command-framework/apify-command.ts | 62 +++++++++---------- ...-on-project-with-no-detected-start.test.ts | 3 - test/local/commands/crawlee/run.test.ts | 3 - test/local/commands/create.test.ts | 3 - test/local/commands/run.test.ts | 3 - 5 files changed, 31 insertions(+), 43 deletions(-) diff --git a/src/lib/command-framework/apify-command.ts b/src/lib/command-framework/apify-command.ts index cab6900a3..0749bc8e8 100644 --- a/src/lib/command-framework/apify-command.ts +++ b/src/lib/command-framework/apify-command.ts @@ -312,41 +312,41 @@ export abstract class ApifyCommand { - if (project.type === ProjectLanguage.JavaScript) { - this.telemetryData.actorLanguage = 'javascript'; - this.telemetryData.actorRuntime = project.runtime!.runtimeShorthand || 'node'; - this.telemetryData.actorRuntimeVersion = project.runtime!.version; - } else if (project.type === ProjectLanguage.Python || project.type === ProjectLanguage.Scrapy) { - this.telemetryData.actorLanguage = 'python'; - this.telemetryData.actorRuntime = 'python'; - this.telemetryData.actorRuntimeVersion = project.runtime!.version; - } - }); - } + } + + // analytics + if (!this.telemetryData.actorLanguage && COMMANDS_WITHIN_ACTOR.includes(this.commandString)) { + const cwdProject = await useCwdProject(); + + cwdProject.inspect((project) => { + if (project.type === ProjectLanguage.JavaScript) { + this.telemetryData.actorLanguage = 'javascript'; + this.telemetryData.actorRuntime = project.runtime!.runtimeShorthand || 'node'; + this.telemetryData.actorRuntimeVersion = project.runtime!.version; + } else if (project.type === ProjectLanguage.Python || project.type === ProjectLanguage.Scrapy) { + this.telemetryData.actorLanguage = 'python'; + this.telemetryData.actorRuntime = 'python'; + this.telemetryData.actorRuntimeVersion = project.runtime!.version; + } + }); + } - this.telemetryData.flagsUsed = Object.keys(this.flags); + this.telemetryData.flagsUsed = Object.keys(this.flags); - if (!this.skipTelemetry) { - await trackEvent( - `cli_command_${this.commandString.replaceAll(' ', '_').toLowerCase()}` as const, - this.telemetryData, - ); - } + if (!this.skipTelemetry) { + await trackEvent( + `cli_command_${this.commandString.replaceAll(' ', '_').toLowerCase()}` as const, + this.telemetryData, + ); + } - if (hadError) { - // In test mode (skipTelemetry=true), throw the error so tests can catch it - // In normal mode, exit with code 1 - if (this.skipTelemetry) { - throw hadError; - } - process.exit(1); + if (hadError) { + // In test mode (skipTelemetry=true), throw the error so tests can catch it + // In normal mode, exit with code 1 + if (this.skipTelemetry) { + throw hadError; } + process.exit(1); } } diff --git a/test/local/__fixtures__/commands/run/python/prints-error-message-on-project-with-no-detected-start.test.ts b/test/local/__fixtures__/commands/run/python/prints-error-message-on-project-with-no-detected-start.test.ts index fa9064448..d907fc5d5 100644 --- a/test/local/__fixtures__/commands/run/python/prints-error-message-on-project-with-no-detected-start.test.ts +++ b/test/local/__fixtures__/commands/run/python/prints-error-message-on-project-with-no-detected-start.test.ts @@ -1,7 +1,6 @@ import { rename } from 'node:fs/promises'; import { testRunCommand } from '../../../../../../src/lib/command-framework/apify-command.js'; -import { useConsoleSpy } from '../../../../../__setup__/hooks/useConsoleSpy.js'; import { useTempPath } from '../../../../../__setup__/hooks/useTempPath.js'; import { resetCwdCaches } from '../../../../../__setup__/reset-cwd-caches.js'; @@ -14,8 +13,6 @@ const { beforeAllCalls, afterAllCalls, joinPath, toggleCwdBetweenFullAndParentPa cwdParent: true, }); -const { lastErrorMessage } = useConsoleSpy(); - const { CreateCommand } = await import('../../../../../../src/commands/create.js'); const { RunCommand } = await import('../../../../../../src/commands/run.js'); diff --git a/test/local/commands/crawlee/run.test.ts b/test/local/commands/crawlee/run.test.ts index 5a2d36e08..468ed226c 100644 --- a/test/local/commands/crawlee/run.test.ts +++ b/test/local/commands/crawlee/run.test.ts @@ -3,7 +3,6 @@ import { readFile, writeFile } from 'node:fs/promises'; import { testRunCommand } from '../../../../src/lib/command-framework/apify-command.js'; import { getLocalKeyValueStorePath } from '../../../../src/lib/utils.js'; import { TEST_TIMEOUT } from '../../../__setup__/consts.js'; -import { useConsoleSpy } from '../../../__setup__/hooks/useConsoleSpy.js'; import { useTempPath } from '../../../__setup__/hooks/useTempPath.js'; import { defaultsInputSchemaPath } from '../../../__setup__/input-schemas/paths.js'; @@ -30,8 +29,6 @@ const { beforeAllCalls, afterAllCalls, joinPath, toggleCwdBetweenFullAndParentPa cwdParent: true, }); -const { lastErrorMessage } = useConsoleSpy(); - const { CreateCommand } = await import('../../../../src/commands/create.js'); const { RunCommand } = await import('../../../../src/commands/run.js'); diff --git a/test/local/commands/create.test.ts b/test/local/commands/create.test.ts index d23a5a54f..0c3dbf0a9 100644 --- a/test/local/commands/create.test.ts +++ b/test/local/commands/create.test.ts @@ -6,7 +6,6 @@ import { KEY_VALUE_STORE_KEYS } from '@apify/consts'; import { testRunCommand } from '../../../src/lib/command-framework/apify-command.js'; import { LOCAL_CONFIG_PATH } from '../../../src/lib/consts.js'; import { getLocalKeyValueStorePath } from '../../../src/lib/utils.js'; -import { useConsoleSpy } from '../../__setup__/hooks/useConsoleSpy.js'; import { useTempPath } from '../../__setup__/hooks/useTempPath.js'; const actName = 'create-my-actor'; @@ -18,8 +17,6 @@ const { beforeAllCalls, afterAllCalls, joinPath, joinCwdPath, toggleCwdBetweenFu cwdParent: true, }); -const { lastErrorMessage } = useConsoleSpy(); - const { CreateCommand } = await import('../../../src/commands/create.js'); describe('apify create', () => { diff --git a/test/local/commands/run.test.ts b/test/local/commands/run.test.ts index 1aff578dd..02f031f2e 100644 --- a/test/local/commands/run.test.ts +++ b/test/local/commands/run.test.ts @@ -14,7 +14,6 @@ import { } from '../../../src/lib/utils.js'; import { TEST_TIMEOUT } from '../../__setup__/consts.js'; import { safeLogin, useAuthSetup } from '../../__setup__/hooks/useAuthSetup.js'; -import { useConsoleSpy } from '../../__setup__/hooks/useConsoleSpy.js'; import { useTempPath } from '../../__setup__/hooks/useTempPath.js'; import { defaultsInputSchemaPath, @@ -46,8 +45,6 @@ const { beforeAllCalls, afterAllCalls, joinPath, toggleCwdBetweenFullAndParentPa cwdParent: true, }); -const { lastErrorMessage } = useConsoleSpy(); - const { CreateCommand } = await import('../../../src/commands/create.js'); const { RunCommand } = await import('../../../src/commands/run.js');