From 028c7ef0fd7e452f3e4d6db305ea696f9d859857 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Sat, 23 May 2026 12:57:10 +0300 Subject: [PATCH] fix(dev-env): restore update wizard prompts #### Purpose and Context The update wizard stopped prompting in interactive sessions because prompt suppression was triggered by broad option detection. Metadata and config-derived objects were treated like explicit CLI selections, which suppressed prompts globally. #### Key Changes - Changed suppression logic in vip dev-env update to suppress prompts only when stdin is non-TTY. - Removed broad option-count suppression that treated unrelated option metadata as explicit configuration. - Added a focused bin test suite for vip dev-env update prompt behavior in interactive and non-interactive modes. - Added coverage for partial interactive options to ensure provided values are preselected while remaining prompts are still shown. #### Impact and Considerations Interactive TTY flows now continue to show the wizard unless running in non-interactive mode. Partial options still prefill selected values, and non-TTY behavior remains fully non-interactive. #### Testing and Validation - Ran npm ci in the isolated worktree. - Ran focused Jest regression tests for vip dev-env update suppression. - Ran TypeScript type checking. Refs: PLTFRM-2426 --- __tests__/bin/vip-dev-env-update.js | 138 ++++++++++++++++++++++++++++ src/bin/vip-dev-env-update.js | 7 +- 2 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 __tests__/bin/vip-dev-env-update.js diff --git a/__tests__/bin/vip-dev-env-update.js b/__tests__/bin/vip-dev-env-update.js new file mode 100644 index 000000000..76c5720a6 --- /dev/null +++ b/__tests__/bin/vip-dev-env-update.js @@ -0,0 +1,138 @@ +jest.mock( '../../src/lib/cli/command', () => { + const command = jest.fn( () => commandMock ); + command.registeredHandler = undefined; + + const commandMock = { + option: () => commandMock, + examples: () => commandMock, + argv: ( _argv, handler ) => { + command.registeredHandler = handler; + return commandMock; + }, + }; + + return command; +} ); + +jest.mock( '../../src/lib/dev-environment/dev-environment-cli', () => ( { + addDevEnvConfigurationOptions: jest.fn(), + getEnvTrackingInfo: jest.fn( () => ( { slug: 'example-site' } ) ), + getEnvironmentName: jest.fn( async () => 'example-site' ), + handleCLIException: jest.fn(), + processSlug: jest.fn( value => value ), + promptForArguments: jest.fn( async () => ( { wpTitle: 'Site Title' } ) ), + validateDependencies: jest.fn(), + ensureValidPathsInOptions: jest.fn(), + getDevEnvLogFile: jest.fn( () => '/tmp/dev-env.log' ), +} ) ); + +jest.mock( '../../src/lib/dev-environment/dev-environment-configuration-file', () => ( { + getConfigurationFileOptions: jest.fn( async () => ( { + overrides: 'services:\n appserver:\n environment:\n FOO: bar', + } ) ), + mergeConfigurationFileOptions: jest.fn( ( cliOptions, configOptions ) => ( { + ...configOptions, + ...cliOptions, + } ) ), +} ) ); + +jest.mock( '../../src/lib/dev-environment/dev-environment-core', () => ( { + doesEnvironmentExist: jest.fn( async () => true ), + getEnvironmentPath: jest.fn( () => '/tmp/example-site' ), + readEnvironmentData: jest.fn( () => ( { + wpTitle: 'Current Title', + multisite: false, + appCode: { tag: 'demo' }, + muPlugins: { tag: 'demo' }, + wordpress: { tag: 'trunk' }, + elasticsearch: false, + php: '8.2', + mariadb: '10.6', + phpmyadmin: false, + xdebug: false, + mailpit: false, + photon: false, + mediaRedirectDomain: '', + cron: false, + adminPassword: '', + } ) ), + updateEnvironment: jest.fn( async () => {} ), +} ) ); + +jest.mock( '../../src/lib/dev-environment/dev-environment-lando', () => ( { + bootstrapLando: jest.fn( async () => ( {} ) ), +} ) ); + +jest.mock( '../../src/lib/tracker', () => ( { + trackEvent: jest.fn( async () => {} ), +} ) ); + +import command from '../../src/lib/cli/command'; +import { promptForArguments } from '../../src/lib/dev-environment/dev-environment-cli'; + +import '../../src/bin/vip-dev-env-update'; + +function setStdinTTY( value ) { + Object.defineProperty( process.stdin, 'isTTY', { + configurable: true, + value, + } ); +} + +describe( 'vip dev-env update prompt suppression', () => { + beforeEach( () => { + jest.clearAllMocks(); + process.exitCode = 0; + } ); + + afterEach( () => { + setStdinTTY( true ); + } ); + + it( 'keeps wizard interactive for TTY even with metadata/config options present', async () => { + setStdinTTY( true ); + + await command.registeredHandler( [], { + slug: 'example-site', + metadata: { source: 'test' }, + } ); + + expect( promptForArguments ).toHaveBeenCalledWith( + expect.any( Object ), + expect.any( Object ), + false, + false + ); + } ); + + it( 'uses partial options as preselected values while keeping wizard interactive in TTY', async () => { + setStdinTTY( true ); + + await command.registeredHandler( [], { + slug: 'example-site', + php: '8.3', + } ); + + expect( promptForArguments ).toHaveBeenCalledWith( + expect.objectContaining( { php: '8.3' } ), + expect.any( Object ), + false, + false + ); + } ); + + it( 'suppresses wizard prompts in non-TTY mode', async () => { + setStdinTTY( false ); + + await command.registeredHandler( [], { + slug: 'example-site', + } ); + + expect( promptForArguments ).toHaveBeenCalledWith( + expect.any( Object ), + expect.any( Object ), + true, + false + ); + } ); +} ); diff --git a/src/bin/vip-dev-env-update.js b/src/bin/vip-dev-env-update.js index 2c6085f83..d1a4e0bdb 100755 --- a/src/bin/vip-dev-env-update.js +++ b/src/bin/vip-dev-env-update.js @@ -95,7 +95,6 @@ cmd.argv( process.argv, async ( arg, opt ) => { }; const configurationFileOptions = await getConfigurationFileOptions(); - const thereAreOptionsFromConfigFile = Object.keys( configurationFileOptions ).length > 0; const finalPreselectedOptions = mergeConfigurationFileOptions( preselectedOptions, configurationFileOptions @@ -122,11 +121,7 @@ cmd.argv( process.argv, async ( arg, opt ) => { adminPassword: currentInstanceData.adminPassword, }; - const providedOptions = Object.keys( opt ) - .filter( option => option.length > 1 ) // Filter out single letter aliases - .filter( option => ! [ 'debug', 'help', 'slug' ].includes( option ) ); // Filter out options that are not related to instance configuration - - const suppressPrompts = providedOptions.length > 0 || thereAreOptionsFromConfigFile; + const suppressPrompts = ! process.stdin.isTTY; const instanceData = await promptForArguments( finalPreselectedOptions, defaultOptions,