diff --git a/src/lib/hooks/useActorConfig.ts b/src/lib/hooks/useActorConfig.ts index a8efe1985..7fc22b656 100644 --- a/src/lib/hooks/useActorConfig.ts +++ b/src/lib/hooks/useActorConfig.ts @@ -110,6 +110,8 @@ export async function useActorConfig( async function handleBothConfigVersionsFound(deprecatedConfigPath: string) { const confirmed = await useYesNoConfirm({ message: `The new version of Apify CLI uses the ".actor/actor.json" instead of the "apify.json" file. Since we have found both files in your Actor directory, "apify.json" will be renamed to "apify.json.deprecated". Going forward, all commands will use ".actor/actor.json". You can read about the differences between the old and the new config at https://github.com/apify/apify-cli/blob/master/MIGRATIONS.md. Do you want to continue?`, + errorMessageForStdin: + 'Config migration requires an interactive terminal. Run the command in an interactive terminal to confirm.', }); // If users refuse to migrate, 🤷 @@ -169,6 +171,8 @@ async function handleMigrationFlow( const confirmed = await useYesNoConfirm({ message: `The new version of Apify CLI uses the ".actor/actor.json" instead of the "apify.json" file. Your "apify.json" file will be automatically updated to the new format under ".actor/actor.json". The original file will be renamed by adding the ".deprecated" suffix. Do you want to continue?`, + errorMessageForStdin: + 'Config migration requires an interactive terminal. Run the command in an interactive terminal to confirm.', }); if (!confirmed) { diff --git a/src/lib/hooks/user-confirmations/_stdinCheckWrapper.ts b/src/lib/hooks/user-confirmations/_stdinCheckWrapper.ts index dd2db6223..fe29606ce 100644 --- a/src/lib/hooks/user-confirmations/_stdinCheckWrapper.ts +++ b/src/lib/hooks/user-confirmations/_stdinCheckWrapper.ts @@ -24,9 +24,6 @@ type NewFunctionArgs any> = [ ...AllButFirst>, ]; -const ConfirmFlag = 'confirm'; -const NoConfirmFlag = `no-${ConfirmFlag}`; - interface StdinCheckWrapperOptions { /** * When set, this value will be used in environments where stdin is not available to provide a custom error message. @@ -43,7 +40,7 @@ interface StdinCheckWrapperOptions { export function stdinCheckWrapper any>( fn: Fn, { - errorMessageForStdin = `Please use the --${ConfirmFlag}/--${NoConfirmFlag} flags to confirm the action.`, + errorMessageForStdin = 'This command requires interactive confirmation. Pass --yes to confirm non-interactively.', }: StdinCheckWrapperOptions = {}, ): (...args: NewFunctionArgs) => Promise>> { return async (input, ...rest) => { diff --git a/test/local/lib/stdinCheckWrapper.test.ts b/test/local/lib/stdinCheckWrapper.test.ts new file mode 100644 index 000000000..064d46db7 --- /dev/null +++ b/test/local/lib/stdinCheckWrapper.test.ts @@ -0,0 +1,56 @@ +// Force the non-interactive code path in stdinCheckWrapper by faking a CI environment. +// See src/lib/hooks/user-confirmations/_stdinCheckWrapper.ts. +vitest.mock('ci-info', async (importOriginal) => { + const original = await importOriginal(); + return { ...original, isCI: true }; +}); + +import { stdinCheckWrapper } from '../../../src/lib/hooks/user-confirmations/_stdinCheckWrapper.js'; + +describe('stdinCheckWrapper (non-interactive / CI mode)', () => { + it('throws when providedConfirmFromStdin is not given', async () => { + const wrapped = stdinCheckWrapper(async () => true); + await expect(wrapped({})).rejects.toThrow(); + }); + + it('does not mention --confirm or --no-confirm in the default error (regression for #1354)', async () => { + const wrapped = stdinCheckWrapper(async () => true); + await expect(wrapped({})).rejects.toThrow( + expect.objectContaining({ message: expect.not.stringMatching(/--confirm|--no-confirm/) }), + ); + }); + + it('mentions --yes in the default error message', async () => { + const wrapped = stdinCheckWrapper(async () => true); + await expect(wrapped({})).rejects.toThrow(/--yes/); + }); + + it('returns providedConfirmFromStdin without calling the inner function', async () => { + const inner = vitest.fn(async () => false); + const wrapped = stdinCheckWrapper(inner); + const result = await wrapped({ providedConfirmFromStdin: true }); + expect(result).toBe(true); + expect(inner).not.toHaveBeenCalled(); + }); + + it('uses caller-supplied errorMessageForStdin over the default', async () => { + const wrapped = stdinCheckWrapper(async () => true); + await expect(wrapped({ errorMessageForStdin: 'Custom non-interactive error' })).rejects.toThrow( + 'Custom non-interactive error', + ); + }); + + it('uses wrapper-level errorMessageForStdin when no per-call override is given', async () => { + const wrapped = stdinCheckWrapper(async () => true, { + errorMessageForStdin: 'Wrapper-level message', + }); + await expect(wrapped({})).rejects.toThrow('Wrapper-level message'); + }); + + it('per-call errorMessageForStdin takes precedence over wrapper-level', async () => { + const wrapped = stdinCheckWrapper(async () => true, { + errorMessageForStdin: 'Wrapper-level message', + }); + await expect(wrapped({ errorMessageForStdin: 'Per-call message' })).rejects.toThrow('Per-call message'); + }); +});