Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/lib/hooks/useActorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, 🤷
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 1 addition & 4 deletions src/lib/hooks/user-confirmations/_stdinCheckWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ type NewFunctionArgs<Fn extends (...args: any[]) => any> = [
...AllButFirst<Parameters<Fn>>,
];

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.
Expand All @@ -43,7 +40,7 @@ interface StdinCheckWrapperOptions {
export function stdinCheckWrapper<Fn extends (...args: any[]) => 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<Fn>) => Promise<Awaited<ReturnType<Fn>>> {
return async (input, ...rest) => {
Expand Down
56 changes: 56 additions & 0 deletions test/local/lib/stdinCheckWrapper.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import('ci-info')>();
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');
});
});
Loading