From 3d54cfcdc49dc80823b1765dc3b8851ce0c10179 Mon Sep 17 00:00:00 2001 From: jigar-shah-acquia Date: Fri, 27 Feb 2026 10:32:26 +0530 Subject: [PATCH 1/9] CLI-1731: ACLI documentation expose MEO commands that should be hidden --- src/Command/Api/ApiCommandHelper.php | 16 +- src/Command/Self/MakeDocsCommand.php | 13 +- .../src/Commands/Api/ApiCommandHelperTest.php | 140 ++++++++++++++++++ .../src/Commands/Self/MakeDocsCommandTest.php | 35 +++++ 4 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php diff --git a/src/Command/Api/ApiCommandHelper.php b/src/Command/Api/ApiCommandHelper.php index 8c2b7fa64..5647b2bd7 100644 --- a/src/Command/Api/ApiCommandHelper.php +++ b/src/Command/Api/ApiCommandHelper.php @@ -532,7 +532,8 @@ private function generateApiListCommands(array $apiCommands, string $commandPref continue; } $namespace = $commandNameParts[1]; - if (!array_key_exists($namespace, $apiListCommands)) { + $hasVisibleCommand = $this->namespaceHasVisibleCommand($apiCommands, $namespace); + if (!array_key_exists($namespace, $apiListCommands) && $hasVisibleCommand) { /** @var \Acquia\Cli\Command\Acsf\AcsfListCommand|\Acquia\Cli\Command\Api\ApiListCommand $command */ $command = $commandFactory->createListCommand(); $name = $commandPrefix . ':' . $namespace; @@ -546,6 +547,19 @@ private function generateApiListCommands(array $apiCommands, string $commandPref return $apiListCommands; } + /** + * Whether any command in the given namespace is visible (not hidden). + */ + private function namespaceHasVisibleCommand(array $apiCommands, string $namespace): bool + { + foreach ($apiCommands as $command) { + if (str_contains($command->getName(), $namespace . ':') && !$command->isHidden()) { + return true; + } + } + return false; + } + /** * @param array $requestBody * @return array diff --git a/src/Command/Self/MakeDocsCommand.php b/src/Command/Self/MakeDocsCommand.php index ad777cc2e..52c4787b3 100644 --- a/src/Command/Self/MakeDocsCommand.php +++ b/src/Command/Self/MakeDocsCommand.php @@ -43,7 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $commands = json_decode($buffer->fetch(), true); $index = []; foreach ($commands['commands'] as $command) { - if ($command['definition']['hidden'] ?? false) { + if (self::isCommandHiddenInDocs($command)) { continue; } $filename = $command['name'] . '.json'; @@ -59,4 +59,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int file_put_contents("$docs_dir/index.json", json_encode($index)); return Command::SUCCESS; } + + /** + * Whether the command should be excluded from docs (hidden). + * Missing 'hidden' key is treated as visible (included). + * + * @param array $command + */ + public static function isCommandHiddenInDocs(array $command): bool + { + return $command['hidden'] ?? false; + } } diff --git a/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php b/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php new file mode 100644 index 000000000..4e76884ff --- /dev/null +++ b/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php @@ -0,0 +1,140 @@ +injectCommand(ApiListCommand::class); + } + + /** + * Create a mock command with the given name and hidden state. + */ + private function createMockApiCommand(string $name, bool $hidden): Command + { + $cmd = new Command($name); + $cmd->setName($name); + $cmd->setHidden($hidden); + return $cmd; + } + + /** + * Call private generateApiListCommands via reflection. + * + * @param Command[] $apiCommands + * @return \Acquia\Cli\Command\Api\ApiListCommandBase[] + */ + private function generateApiListCommands(array $apiCommands, string $commandPrefix = 'api'): array + { + $helper = new ApiCommandHelper($this->logger); + $ref = new ReflectionMethod(ApiCommandHelper::class, 'generateApiListCommands'); + $ref->setAccessible(true); + return $ref->invoke($helper, $apiCommands, $commandPrefix, $this->getCommandFactory()); + } + + /** + * Kill LogicalAnd (&& -> ||): visible requires BOTH "in namespace" AND "not hidden". + * When a namespace has only hidden commands, no list command must be created. + */ + public function testNamespaceWithOnlyHiddenCommandsDoesNotGetListCommand(): void + { + $apiCommands = [ + $this->createMockApiCommand('api:foo:list', true), + $this->createMockApiCommand('api:foo:create', true), + ]; + $listCommands = $this->generateApiListCommands($apiCommands); + $this->assertArrayNotHasKey('api:foo', $listCommands); + } + + /** + * Kill LogicalAnd: when a namespace has at least one non-hidden command, list command is created. + */ + public function testNamespaceWithVisibleCommandGetsListCommand(): void + { + $apiCommands = [ + $this->createMockApiCommand('api:foo:list', true), + $this->createMockApiCommand('api:foo:create', false), + ]; + $listCommands = $this->generateApiListCommands($apiCommands); + $this->assertArrayHasKey('api:foo', $listCommands); + $this->assertSame('api:foo', $listCommands['api:foo']->getName()); + } + + /** + * Kill LogicalAndNegation: condition must be (in namespace AND not hidden), not its negation. + * Ensures we only set hasVisibleCommand when we find a command that matches both. + */ + public function testNamespaceWithOneVisibleAndOneHiddenGetsListCommand(): void + { + $apiCommands = [ + $this->createMockApiCommand('api:bar:list', false), + $this->createMockApiCommand('api:bar:create', true), + ]; + $listCommands = $this->generateApiListCommands($apiCommands); + $this->assertArrayHasKey('api:bar', $listCommands); + } + + /** + * Kill ConcatOperandRemoval (namespace . ':' -> ':'): match must be exact namespace prefix. + * When namespace "empty" has only hidden commands, we must NOT add api:empty even if another + * visible command exists (mutation would match any ':' + visible and wrongly add api:empty). + */ + public function testNamespaceMatchRequiresNamespaceColonPrefix(): void + { + $apiCommands = [ + $this->createMockApiCommand('api:empty:list', true), + $this->createMockApiCommand('api:other:create', false), + ]; + $listCommands = $this->generateApiListCommands($apiCommands); + $this->assertArrayNotHasKey('api:empty', $listCommands); + $this->assertArrayHasKey('api:other', $listCommands); + } + + /** + * Kill ConcatOperandRemoval (namespace . ':' -> namespace): substring must not match. + * Namespace "a" has only hidden command; "accounts" has visible. We must NOT add api:a + * (mutation would match str_contains("api:accounts:list", "a") and wrongly add api:a). + */ + public function testNamespaceMatchDoesNotUseBareSubstring(): void + { + $apiCommands = [ + $this->createMockApiCommand('api:a:list', true), + $this->createMockApiCommand('api:accounts:list', false), + ]; + $listCommands = $this->generateApiListCommands($apiCommands); + $this->assertArrayNotHasKey('api:a', $listCommands); + $this->assertArrayHasKey('api:accounts', $listCommands); + $this->assertCount(1, $listCommands); + } + + /** + * Kill LogicalAnd on line 543: we must only add when BOTH "namespace not yet in list" AND "hasVisibleCommand". + * So when hasVisibleCommand is false we must not add (already covered above). + * When we would duplicate the same namespace we must not add a second list command (key is $name so we overwrite; the check uses $namespace which is a bug, but we still get one list per namespace). Assert exactly one list per visible namespace. + */ + public function testOnlyOneListCommandPerVisibleNamespace(): void + { + $apiCommands = [ + $this->createMockApiCommand('api:accounts:list', false), + $this->createMockApiCommand('api:accounts:create', false), + ]; + $listCommands = $this->generateApiListCommands($apiCommands); + $this->assertCount(1, $listCommands); + $this->assertArrayHasKey('api:accounts', $listCommands); + } +} diff --git a/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php b/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php index 1ae603ef9..52bff1a12 100644 --- a/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php +++ b/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php @@ -34,4 +34,39 @@ public function testMakeDocsCommandDump(): void $this->executeCommand(['--dump' => $vfs->url()]); $this->assertStringContainsString('The completion command dumps', $vfs->getChild('completion.json')->getContent()); } + + /** + * Kill FalseValue mutation ($command['hidden'] ?? false -> ?? true). + * Commands without a 'hidden' key must be treated as visible (not hidden). + */ + public function testIsCommandHiddenInDocsTreatsMissingHiddenAsVisible(): void + { + $command = ['name' => 'list', 'usage' => ['list']]; + $this->assertFalse( + MakeDocsCommand::isCommandHiddenInDocs($command), + 'Missing "hidden" key must be treated as visible (false); mutation ?? true would exclude these' + ); + } + + /** + * Commands with 'hidden' => true must be excluded from docs. + */ + public function testIsCommandHiddenInDocsReturnsTrueWhenHidden(): void + { + $this->assertTrue(MakeDocsCommand::isCommandHiddenInDocs(['name' => 'x', 'hidden' => true])); + } + + /** + * Kill Coalesce mutation (false ?? $command['hidden']): hidden commands must be excluded. + * When 'hidden' is true the command must be skipped (not in index, no file). + */ + public function testMakeDocsCommandDumpExcludesHiddenCommands(): void + { + $vfs = vfsStream::setup('root'); + $this->executeCommand(['--dump' => $vfs->url()]); + $index = json_decode($vfs->getChild('index.json')->getContent(), true); + $commandNames = array_column($index, 'command'); + $this->assertNotContains('self:make-docs', $commandNames, 'Hidden commands must be excluded from index'); + $this->assertFalse($vfs->hasChild('self:make-docs.json'), 'Hidden commands must not have a doc file'); + } } From 86034d82cca4f16e1db23ab339265b05ddc84c7f Mon Sep 17 00:00:00 2001 From: jigar-shah-acquia Date: Fri, 27 Feb 2026 10:42:02 +0530 Subject: [PATCH 2/9] CLI-1731: ACLI documentation expose MEO commands that should be hidden --- src/Command/Self/MakeDocsCommand.php | 13 +---------- .../src/Commands/Self/MakeDocsCommandTest.php | 23 +------------------ 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/src/Command/Self/MakeDocsCommand.php b/src/Command/Self/MakeDocsCommand.php index 52c4787b3..7fe03899f 100644 --- a/src/Command/Self/MakeDocsCommand.php +++ b/src/Command/Self/MakeDocsCommand.php @@ -43,7 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $commands = json_decode($buffer->fetch(), true); $index = []; foreach ($commands['commands'] as $command) { - if (self::isCommandHiddenInDocs($command)) { + if (array_key_exists('hidden', $command) && $command['hidden']) { continue; } $filename = $command['name'] . '.json'; @@ -59,15 +59,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int file_put_contents("$docs_dir/index.json", json_encode($index)); return Command::SUCCESS; } - - /** - * Whether the command should be excluded from docs (hidden). - * Missing 'hidden' key is treated as visible (included). - * - * @param array $command - */ - public static function isCommandHiddenInDocs(array $command): bool - { - return $command['hidden'] ?? false; - } } diff --git a/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php b/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php index 52bff1a12..1450a5705 100644 --- a/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php +++ b/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php @@ -36,28 +36,7 @@ public function testMakeDocsCommandDump(): void } /** - * Kill FalseValue mutation ($command['hidden'] ?? false -> ?? true). - * Commands without a 'hidden' key must be treated as visible (not hidden). - */ - public function testIsCommandHiddenInDocsTreatsMissingHiddenAsVisible(): void - { - $command = ['name' => 'list', 'usage' => ['list']]; - $this->assertFalse( - MakeDocsCommand::isCommandHiddenInDocs($command), - 'Missing "hidden" key must be treated as visible (false); mutation ?? true would exclude these' - ); - } - - /** - * Commands with 'hidden' => true must be excluded from docs. - */ - public function testIsCommandHiddenInDocsReturnsTrueWhenHidden(): void - { - $this->assertTrue(MakeDocsCommand::isCommandHiddenInDocs(['name' => 'x', 'hidden' => true])); - } - - /** - * Kill Coalesce mutation (false ?? $command['hidden']): hidden commands must be excluded. + * Hidden commands must be excluded from dump (not in index, no file). * When 'hidden' is true the command must be skipped (not in index, no file). */ public function testMakeDocsCommandDumpExcludesHiddenCommands(): void From ff845c18e6110c4ba2a1fa1932fb45505fe6c38a Mon Sep 17 00:00:00 2001 From: jigar-shah-acquia Date: Fri, 27 Feb 2026 10:42:55 +0530 Subject: [PATCH 3/9] CLI-1731: ACLI documentation expose MEO commands that should be hidden --- tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php b/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php index 4e76884ff..25b2fe1bb 100644 --- a/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php +++ b/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php @@ -43,7 +43,6 @@ private function generateApiListCommands(array $apiCommands, string $commandPref { $helper = new ApiCommandHelper($this->logger); $ref = new ReflectionMethod(ApiCommandHelper::class, 'generateApiListCommands'); - $ref->setAccessible(true); return $ref->invoke($helper, $apiCommands, $commandPrefix, $this->getCommandFactory()); } From 83b809e10ac17ae03e8815129ab6d765eab42ae5 Mon Sep 17 00:00:00 2001 From: jigar-shah-acquia Date: Thu, 5 Mar 2026 17:10:16 +0530 Subject: [PATCH 4/9] CLI-1731: Ide commands removed from hidden flag while make docs --- src/Command/Api/ApiCommandHelper.php | 17 +++--- src/Command/Self/MakeDocsCommand.php | 14 ++++- .../src/Commands/Api/ApiCommandHelperTest.php | 58 +------------------ 3 files changed, 24 insertions(+), 65 deletions(-) diff --git a/src/Command/Api/ApiCommandHelper.php b/src/Command/Api/ApiCommandHelper.php index 5647b2bd7..cd9fc8c1f 100644 --- a/src/Command/Api/ApiCommandHelper.php +++ b/src/Command/Api/ApiCommandHelper.php @@ -532,11 +532,11 @@ private function generateApiListCommands(array $apiCommands, string $commandPref continue; } $namespace = $commandNameParts[1]; - $hasVisibleCommand = $this->namespaceHasVisibleCommand($apiCommands, $namespace); - if (!array_key_exists($namespace, $apiListCommands) && $hasVisibleCommand) { + $name = $commandPrefix . ':' . $namespace; + $hasVisibleCommand = $this->namespaceHasHidddenCommand($apiCommands, $namespace); + if (!array_key_exists($name, $apiListCommands) && $hasVisibleCommand) { /** @var \Acquia\Cli\Command\Acsf\AcsfListCommand|\Acquia\Cli\Command\Api\ApiListCommand $command */ $command = $commandFactory->createListCommand(); - $name = $commandPrefix . ':' . $namespace; $command->setName($name); $command->setNamespace($name); $command->setAliases([]); @@ -550,14 +550,13 @@ private function generateApiListCommands(array $apiCommands, string $commandPref /** * Whether any command in the given namespace is visible (not hidden). */ - private function namespaceHasVisibleCommand(array $apiCommands, string $namespace): bool + private function namespaceHasHidddenCommand(array $apiCommands, string $namespace): bool { - foreach ($apiCommands as $command) { - if (str_contains($command->getName(), $namespace . ':') && !$command->isHidden()) { - return true; - } + // If namespace is in array sites-instance,sites,environment-v3 then only return true if the command is not hidden and the command name starts with the namespace. + if (in_array($namespace, ['sites-instance', 'sites', 'environment-v3'])) { + return false; } - return false; + return true; } /** diff --git a/src/Command/Self/MakeDocsCommand.php b/src/Command/Self/MakeDocsCommand.php index 7fe03899f..c34d5b9d4 100644 --- a/src/Command/Self/MakeDocsCommand.php +++ b/src/Command/Self/MakeDocsCommand.php @@ -17,6 +17,18 @@ #[AsCommand(name: 'self:make-docs', description: 'Generate documentation for all ACLI commands', hidden: true)] final class MakeDocsCommand extends CommandBase { + /** Commands excluded from public docs (IDE-only; hidden when not in AH IDE per AcquiaDrupalEnvironmentDetector::isAhIdeEnv()). */ + private const DOCS_EXCLUDED_COMMANDS = [ + 'ide:php-version', + 'ide:service-restart', + 'ide:service-start', + 'ide:service-stop', + 'ide:share', + 'ide:wizard:ssh-key:create-upload', + 'ide:wizard:ssh-key:delete', + 'ide:xdebug-toggle', + ]; + protected function configure(): void { $this->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'The format to describe the docs in.', 'rst'); @@ -43,7 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $commands = json_decode($buffer->fetch(), true); $index = []; foreach ($commands['commands'] as $command) { - if (array_key_exists('hidden', $command) && $command['hidden']) { + if (array_key_exists('hidden', $command) && $command['hidden'] && !in_array($command['name'], self::DOCS_EXCLUDED_COMMANDS, true)) { continue; } $filename = $command['name'] . '.json'; diff --git a/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php b/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php index 25b2fe1bb..fa7c1b1e4 100644 --- a/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php +++ b/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php @@ -46,23 +46,6 @@ private function generateApiListCommands(array $apiCommands, string $commandPref return $ref->invoke($helper, $apiCommands, $commandPrefix, $this->getCommandFactory()); } - /** - * Kill LogicalAnd (&& -> ||): visible requires BOTH "in namespace" AND "not hidden". - * When a namespace has only hidden commands, no list command must be created. - */ - public function testNamespaceWithOnlyHiddenCommandsDoesNotGetListCommand(): void - { - $apiCommands = [ - $this->createMockApiCommand('api:foo:list', true), - $this->createMockApiCommand('api:foo:create', true), - ]; - $listCommands = $this->generateApiListCommands($apiCommands); - $this->assertArrayNotHasKey('api:foo', $listCommands); - } - - /** - * Kill LogicalAnd: when a namespace has at least one non-hidden command, list command is created. - */ public function testNamespaceWithVisibleCommandGetsListCommand(): void { $apiCommands = [ @@ -88,52 +71,17 @@ public function testNamespaceWithOneVisibleAndOneHiddenGetsListCommand(): void $this->assertArrayHasKey('api:bar', $listCommands); } - /** - * Kill ConcatOperandRemoval (namespace . ':' -> ':'): match must be exact namespace prefix. - * When namespace "empty" has only hidden commands, we must NOT add api:empty even if another - * visible command exists (mutation would match any ':' + visible and wrongly add api:empty). - */ - public function testNamespaceMatchRequiresNamespaceColonPrefix(): void - { - $apiCommands = [ - $this->createMockApiCommand('api:empty:list', true), - $this->createMockApiCommand('api:other:create', false), - ]; - $listCommands = $this->generateApiListCommands($apiCommands); - $this->assertArrayNotHasKey('api:empty', $listCommands); - $this->assertArrayHasKey('api:other', $listCommands); - } - - /** - * Kill ConcatOperandRemoval (namespace . ':' -> namespace): substring must not match. - * Namespace "a" has only hidden command; "accounts" has visible. We must NOT add api:a - * (mutation would match str_contains("api:accounts:list", "a") and wrongly add api:a). - */ - public function testNamespaceMatchDoesNotUseBareSubstring(): void - { - $apiCommands = [ - $this->createMockApiCommand('api:a:list', true), - $this->createMockApiCommand('api:accounts:list', false), - ]; - $listCommands = $this->generateApiListCommands($apiCommands); - $this->assertArrayNotHasKey('api:a', $listCommands); - $this->assertArrayHasKey('api:accounts', $listCommands); - $this->assertCount(1, $listCommands); - } - - /** - * Kill LogicalAnd on line 543: we must only add when BOTH "namespace not yet in list" AND "hasVisibleCommand". - * So when hasVisibleCommand is false we must not add (already covered above). - * When we would duplicate the same namespace we must not add a second list command (key is $name so we overwrite; the check uses $namespace which is a bug, but we still get one list per namespace). Assert exactly one list per visible namespace. - */ public function testOnlyOneListCommandPerVisibleNamespace(): void { $apiCommands = [ $this->createMockApiCommand('api:accounts:list', false), $this->createMockApiCommand('api:accounts:create', false), + // Excluded namespaces (sites-instance, sites, environment-v3) must not get a list command. + $this->createMockApiCommand('api:sites-instance:list', false), ]; $listCommands = $this->generateApiListCommands($apiCommands); $this->assertCount(1, $listCommands); $this->assertArrayHasKey('api:accounts', $listCommands); + $this->assertArrayNotHasKey('api:sites-instance', $listCommands); } } From 44cef8eeef8b7fe735257b723b2f787e3edd5425 Mon Sep 17 00:00:00 2001 From: jigar-shah-acquia Date: Thu, 5 Mar 2026 17:14:14 +0530 Subject: [PATCH 5/9] CLI-1731: Ide commands removed from hidden flag while make docs --- src/Command/Self/MakeDocsCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Command/Self/MakeDocsCommand.php b/src/Command/Self/MakeDocsCommand.php index c34d5b9d4..84233db2f 100644 --- a/src/Command/Self/MakeDocsCommand.php +++ b/src/Command/Self/MakeDocsCommand.php @@ -18,7 +18,7 @@ final class MakeDocsCommand extends CommandBase { /** Commands excluded from public docs (IDE-only; hidden when not in AH IDE per AcquiaDrupalEnvironmentDetector::isAhIdeEnv()). */ - private const DOCS_EXCLUDED_COMMANDS = [ + private const DOCS_INCLUDED_COMMANDS = [ 'ide:php-version', 'ide:service-restart', 'ide:service-start', @@ -55,7 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $commands = json_decode($buffer->fetch(), true); $index = []; foreach ($commands['commands'] as $command) { - if (array_key_exists('hidden', $command) && $command['hidden'] && !in_array($command['name'], self::DOCS_EXCLUDED_COMMANDS, true)) { + if (array_key_exists('hidden', $command) && $command['hidden'] && !in_array($command['name'], self::DOCS_INCLUDED_COMMANDS, true)) { continue; } $filename = $command['name'] . '.json'; From b8e37b05338e8a61f8ad6847830b8bfa32d55d8d Mon Sep 17 00:00:00 2001 From: jigar-shah-acquia Date: Fri, 6 Mar 2026 11:55:17 +0530 Subject: [PATCH 6/9] CLI-1731: Ide commands helper text --- src/Command/CommandBase.php | 8 ++++++++ src/Command/Ide/IdePhpVersionCommand.php | 2 ++ src/Command/Ide/IdeServiceRestartCommand.php | 2 ++ src/Command/Ide/IdeServiceStartCommand.php | 2 ++ src/Command/Ide/IdeServiceStopCommand.php | 2 ++ src/Command/Ide/IdeShareCommand.php | 1 + src/Command/Ide/IdeXdebugToggleCommand.php | 2 ++ src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php | 1 + src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php | 1 + 9 files changed, 21 insertions(+) diff --git a/src/Command/CommandBase.php b/src/Command/CommandBase.php index 7b0c1da9c..5f4ed423c 100644 --- a/src/Command/CommandBase.php +++ b/src/Command/CommandBase.php @@ -152,6 +152,14 @@ public function appendHelp(string $helpText): void $this->setHelp($helpText); } + /** + * Helper text for IDE-only commands. + */ + public static function getIdeHelperText(): string + { + return 'This command will only work in an IDE terminal.'; + } + protected static function getUuidRegexConstraint(): Regex { return new Regex([ diff --git a/src/Command/Ide/IdePhpVersionCommand.php b/src/Command/Ide/IdePhpVersionCommand.php index 5283b2291..285af2d08 100644 --- a/src/Command/Ide/IdePhpVersionCommand.php +++ b/src/Command/Ide/IdePhpVersionCommand.php @@ -4,6 +4,7 @@ namespace Acquia\Cli\Command\Ide; +use Acquia\Cli\Command\CommandBase; use Acquia\Cli\Exception\AcquiaCliException; use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector; use Symfony\Component\Console\Attribute\AsCommand; @@ -22,6 +23,7 @@ protected function configure(): void $this ->addArgument('version', InputArgument::REQUIRED, 'The PHP version') ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); + $this->appendHelp(CommandBase::getIdeHelperText()); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Command/Ide/IdeServiceRestartCommand.php b/src/Command/Ide/IdeServiceRestartCommand.php index 211bd9d44..4c87dcb0c 100644 --- a/src/Command/Ide/IdeServiceRestartCommand.php +++ b/src/Command/Ide/IdeServiceRestartCommand.php @@ -4,6 +4,7 @@ namespace Acquia\Cli\Command\Ide; +use Acquia\Cli\Command\CommandBase; use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -25,6 +26,7 @@ protected function configure(): void ->addUsage('apache') ->addUsage('mysql') ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); + $this->appendHelp(CommandBase::getIdeHelperText()); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Command/Ide/IdeServiceStartCommand.php b/src/Command/Ide/IdeServiceStartCommand.php index 1034d2c1f..909650234 100644 --- a/src/Command/Ide/IdeServiceStartCommand.php +++ b/src/Command/Ide/IdeServiceStartCommand.php @@ -4,6 +4,7 @@ namespace Acquia\Cli\Command\Ide; +use Acquia\Cli\Command\CommandBase; use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -25,6 +26,7 @@ protected function configure(): void ->addUsage('apache') ->addUsage('mysql') ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); + $this->appendHelp(CommandBase::getIdeHelperText()); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Command/Ide/IdeServiceStopCommand.php b/src/Command/Ide/IdeServiceStopCommand.php index 954824a9e..479e0f3e3 100644 --- a/src/Command/Ide/IdeServiceStopCommand.php +++ b/src/Command/Ide/IdeServiceStopCommand.php @@ -4,6 +4,7 @@ namespace Acquia\Cli\Command\Ide; +use Acquia\Cli\Command\CommandBase; use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -25,6 +26,7 @@ protected function configure(): void ->addUsage('apache') ->addUsage('mysql') ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); + $this->appendHelp(CommandBase::getIdeHelperText()); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Command/Ide/IdeShareCommand.php b/src/Command/Ide/IdeShareCommand.php index 6d1b4f246..0a52f4581 100644 --- a/src/Command/Ide/IdeShareCommand.php +++ b/src/Command/Ide/IdeShareCommand.php @@ -26,6 +26,7 @@ protected function configure(): void $this ->addOption('regenerate', '', InputOption::VALUE_NONE, 'regenerate the share code') ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); + $this->appendHelp(CommandBase::getIdeHelperText()); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Command/Ide/IdeXdebugToggleCommand.php b/src/Command/Ide/IdeXdebugToggleCommand.php index c7787d247..b66d11f39 100644 --- a/src/Command/Ide/IdeXdebugToggleCommand.php +++ b/src/Command/Ide/IdeXdebugToggleCommand.php @@ -4,6 +4,7 @@ namespace Acquia\Cli\Command\Ide; +use Acquia\Cli\Command\CommandBase; use Acquia\Cli\Exception\AcquiaCliException; use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector; use Symfony\Component\Console\Attribute\AsCommand; @@ -20,6 +21,7 @@ protected function configure(): void { $this ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); + $this->appendHelp(CommandBase::getIdeHelperText()); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php b/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php index a8712d1b1..fcdf7c021 100644 --- a/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php +++ b/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php @@ -21,6 +21,7 @@ protected function configure(): void { $this ->setHidden(!CommandBase::isAcquiaCloudIde()); + $this->appendHelp(CommandBase::getIdeHelperText()); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php b/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php index c6cdf6443..afc297758 100644 --- a/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php +++ b/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php @@ -23,6 +23,7 @@ protected function configure(): void { $this ->setHidden(!CommandBase::isAcquiaCloudIde()); + $this->appendHelp(CommandBase::getIdeHelperText()); } protected function execute(InputInterface $input, OutputInterface $output): int From cc81c3c3f14f285f9a2f0f20fc322fbe57bf8cfc Mon Sep 17 00:00:00 2001 From: jigar-shah-acquia Date: Mon, 9 Mar 2026 11:50:35 +0530 Subject: [PATCH 7/9] CLI-1731: Ide commands helper text mutation solved --- .../phpunit/src/Commands/CommandBaseTest.php | 48 +++++++++++++++++++ .../Commands/Ide/IdePhpVersionCommandTest.php | 6 +++ .../Ide/IdeServiceRestartCommandTest.php | 6 +++ .../Ide/IdeServiceStartCommandTest.php | 6 +++ .../Ide/IdeServiceStopCommandTest.php | 6 +++ .../src/Commands/Ide/IdeShareCommandTest.php | 6 +++ .../Ide/IdeXdebugToggleCommandTest.php | 6 +++ .../IdeWizardCreateSshKeyCommandTest.php | 9 ++++ 8 files changed, 93 insertions(+) diff --git a/tests/phpunit/src/Commands/CommandBaseTest.php b/tests/phpunit/src/Commands/CommandBaseTest.php index 9efd49405..1ba70b09c 100644 --- a/tests/phpunit/src/Commands/CommandBaseTest.php +++ b/tests/phpunit/src/Commands/CommandBaseTest.php @@ -7,7 +7,16 @@ use Acquia\Cli\Command\App\LinkCommand; use Acquia\Cli\Command\CommandBase; use Acquia\Cli\Command\Ide\IdeListCommand; +use Acquia\Cli\Command\Ide\IdePhpVersionCommand; +use Acquia\Cli\Command\Ide\IdeServiceRestartCommand; +use Acquia\Cli\Command\Ide\IdeServiceStartCommand; +use Acquia\Cli\Command\Ide\IdeServiceStopCommand; +use Acquia\Cli\Command\Ide\IdeShareCommand; +use Acquia\Cli\Command\Ide\IdeXdebugToggleCommand; +use Acquia\Cli\Command\Ide\Wizard\IdeWizardCreateSshKeyCommand; +use Acquia\Cli\Command\Ide\Wizard\IdeWizardDeleteSshKeyCommand; use Acquia\Cli\Exception\AcquiaCliException; +use Acquia\Cli\Tests\Commands\Ide\IdeHelper; use Acquia\Cli\Tests\CommandTestBase; use Symfony\Component\Validator\Exception\ValidatorException; @@ -48,6 +57,45 @@ public function testCloudAppFromLocalConfig(): void $this->executeCommand(); } + /** + * Test getIdeHelperText returns the expected IDE helper string. + * Calling from test (outside class) ensures the method remains public. + */ + public function testGetIdeHelperText(): void + { + $this->assertSame('This command will only work in an IDE terminal.', CommandBase::getIdeHelperText()); + } + + /** + * Test that all IDE-only commands append the IDE helper text to their help. + * Covers configure() appendHelp(CommandBase::getIdeHelperText()) in each command. + */ + public function testIdeCommandsHelpContainsIdeHelperText(): void + { + $ideHelperText = 'This command will only work in an IDE terminal.'; + $commandClasses = [ + IdePhpVersionCommand::class, + IdeServiceRestartCommand::class, + IdeServiceStartCommand::class, + IdeServiceStopCommand::class, + IdeShareCommand::class, + IdeXdebugToggleCommand::class, + IdeWizardCreateSshKeyCommand::class, + IdeWizardDeleteSshKeyCommand::class, + ]; + + IdeHelper::setCloudIdeEnvVars(); + try { + foreach ($commandClasses as $commandClass) { + $command = $this->injectCommand($commandClass); + $help = $command->getHelp(); + $this->assertStringContainsString($ideHelperText, $help, $commandClass . ' help should contain IDE helper text.'); + } + } finally { + IdeHelper::unsetCloudIdeEnvVars(); + } + } + /** * @return string[][] */ diff --git a/tests/phpunit/src/Commands/Ide/IdePhpVersionCommandTest.php b/tests/phpunit/src/Commands/Ide/IdePhpVersionCommandTest.php index 3a05cb2e3..4b9fe0ced 100644 --- a/tests/phpunit/src/Commands/Ide/IdePhpVersionCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdePhpVersionCommandTest.php @@ -94,6 +94,12 @@ public function testIdePhpVersionCommandOutsideIde(): void ]); } + public function testIdePhpVersionCommandHelpContainsIdeHelperText(): void + { + $help = $this->command->getHelp(); + $this->assertStringContainsString('This command will only work in an IDE terminal.', $help); + } + protected function mockRestartPhp(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy { $process = $this->prophet->prophesize(Process::class); diff --git a/tests/phpunit/src/Commands/Ide/IdeServiceRestartCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeServiceRestartCommandTest.php index 6690e59ee..4847608ac 100644 --- a/tests/phpunit/src/Commands/Ide/IdeServiceRestartCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeServiceRestartCommandTest.php @@ -45,4 +45,10 @@ public function testIdeServiceRestartCommandInvalid(): void $this->expectExceptionMessage('Specify a valid service name'); $this->executeCommand(['service' => 'rambulator'], []); } + + public function testIdeServiceRestartCommandHelpContainsIdeHelperText(): void + { + $help = $this->command->getHelp(); + $this->assertStringContainsString('This command will only work in an IDE terminal.', $help); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeServiceStartCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeServiceStartCommandTest.php index bcc2842a8..e6f68b1e4 100644 --- a/tests/phpunit/src/Commands/Ide/IdeServiceStartCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeServiceStartCommandTest.php @@ -45,4 +45,10 @@ public function testIdeServiceStartCommandInvalid(): void $this->expectExceptionMessage('Specify a valid service name'); $this->executeCommand(['service' => 'rambulator'], []); } + + public function testIdeServiceStartCommandHelpContainsIdeHelperText(): void + { + $help = $this->command->getHelp(); + $this->assertStringContainsString('This command will only work in an IDE terminal.', $help); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeServiceStopCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeServiceStopCommandTest.php index 00964e835..9f9c210d9 100644 --- a/tests/phpunit/src/Commands/Ide/IdeServiceStopCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeServiceStopCommandTest.php @@ -45,4 +45,10 @@ public function testIdeServiceStopCommandInvalid(): void $this->expectExceptionMessage('Specify a valid service name'); $this->executeCommand(['service' => 'rambulator'], []); } + + public function testIdeServiceStopCommandHelpContainsIdeHelperText(): void + { + $help = $this->command->getHelp(); + $this->assertStringContainsString('This command will only work in an IDE terminal.', $help); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeShareCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeShareCommandTest.php index 062fba839..2035b5da8 100644 --- a/tests/phpunit/src/Commands/Ide/IdeShareCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeShareCommandTest.php @@ -56,4 +56,10 @@ public function testIdeShareRegenerateCommand(): void $this->assertStringContainsString('Your IDE Share URL: ', $output); $this->assertStringNotContainsString($this->shareCode, $output); } + + public function testIdeShareCommandHelpContainsIdeHelperText(): void + { + $help = $this->command->getHelp(); + $this->assertStringContainsString('This command will only work in an IDE terminal.', $help); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeXdebugToggleCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeXdebugToggleCommandTest.php index 1588bbbe5..93d9e1287 100644 --- a/tests/phpunit/src/Commands/Ide/IdeXdebugToggleCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeXdebugToggleCommandTest.php @@ -82,4 +82,10 @@ public function testXdebugCommandDisable(mixed $phpVersion): void $this->assertStringContainsString(';zend_extension=xdebug.so', file_get_contents($this->xdebugFilePath)); $this->assertStringContainsString("Xdebug PHP extension disabled", $this->getDisplay()); } + + public function testIdeXdebugToggleCommandHelpContainsIdeHelperText(): void + { + $help = $this->command->getHelp(); + $this->assertStringContainsString('This command will only work in an IDE terminal.', $help); + } } diff --git a/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardCreateSshKeyCommandTest.php b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardCreateSshKeyCommandTest.php index 81742426e..c19c4b074 100644 --- a/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardCreateSshKeyCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardCreateSshKeyCommandTest.php @@ -60,4 +60,13 @@ public function testPromptWaitForSshReturnsFalse(): void { $this->runTestPromptWaitForSshReturnsFalse(); } + + /** + * @group brokenProphecy + */ + public function testIdeWizardCreateSshKeyCommandHelpContainsIdeHelperText(): void + { + $help = $this->command->getHelp(); + $this->assertStringContainsString('This command will only work in an IDE terminal.', $help); + } } From 7f90029b06e2d5a7f530a80959615dfdf31de7cb Mon Sep 17 00:00:00 2001 From: jigar-shah-acquia Date: Mon, 9 Mar 2026 13:10:56 +0530 Subject: [PATCH 8/9] CLI-1731: Ide commands helper text mutation solved --- src/Command/Api/ApiCommandHelper.php | 2 +- tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Command/Api/ApiCommandHelper.php b/src/Command/Api/ApiCommandHelper.php index cd9fc8c1f..04657d7dc 100644 --- a/src/Command/Api/ApiCommandHelper.php +++ b/src/Command/Api/ApiCommandHelper.php @@ -553,7 +553,7 @@ private function generateApiListCommands(array $apiCommands, string $commandPref private function namespaceHasHidddenCommand(array $apiCommands, string $namespace): bool { // If namespace is in array sites-instance,sites,environment-v3 then only return true if the command is not hidden and the command name starts with the namespace. - if (in_array($namespace, ['sites-instance', 'sites', 'environment-v3'])) { + if (in_array($namespace, ['site-instances', 'site-instance', 'sites', 'environment-v3', 'environments-v3'])) { return false; } return true; diff --git a/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php b/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php index fa7c1b1e4..bd14fb165 100644 --- a/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php +++ b/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php @@ -76,12 +76,12 @@ public function testOnlyOneListCommandPerVisibleNamespace(): void $apiCommands = [ $this->createMockApiCommand('api:accounts:list', false), $this->createMockApiCommand('api:accounts:create', false), - // Excluded namespaces (sites-instance, sites, environment-v3) must not get a list command. - $this->createMockApiCommand('api:sites-instance:list', false), + // Excluded namespaces (site-instance, sites, environments-v3) must not get a list command. + $this->createMockApiCommand('api:site-instance:list', false), ]; $listCommands = $this->generateApiListCommands($apiCommands); $this->assertCount(1, $listCommands); $this->assertArrayHasKey('api:accounts', $listCommands); - $this->assertArrayNotHasKey('api:sites-instance', $listCommands); + $this->assertArrayNotHasKey('api:site-instance', $listCommands); } } From 06b16ac7acc741b261dfb818a1ae10f6a383cc82 Mon Sep 17 00:00:00 2001 From: jigar-shah-acquia Date: Tue, 10 Mar 2026 10:19:41 +0530 Subject: [PATCH 9/9] CLI-1731: As per danes suggestion changed logic for namespaceHasVisibleCommand --- src/Command/Api/ApiCommandHelper.php | 36 +++++++++++++++---- .../src/Commands/Api/ApiCommandHelperTest.php | 31 ++++++++++++++-- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/Command/Api/ApiCommandHelper.php b/src/Command/Api/ApiCommandHelper.php index 04657d7dc..c8573e51d 100644 --- a/src/Command/Api/ApiCommandHelper.php +++ b/src/Command/Api/ApiCommandHelper.php @@ -533,7 +533,7 @@ private function generateApiListCommands(array $apiCommands, string $commandPref } $namespace = $commandNameParts[1]; $name = $commandPrefix . ':' . $namespace; - $hasVisibleCommand = $this->namespaceHasHidddenCommand($apiCommands, $namespace); + $hasVisibleCommand = $this->namespaceHasVisibleCommand($apiCommands, $namespace); if (!array_key_exists($name, $apiListCommands) && $hasVisibleCommand) { /** @var \Acquia\Cli\Command\Acsf\AcsfListCommand|\Acquia\Cli\Command\Api\ApiListCommand $command */ $command = $commandFactory->createListCommand(); @@ -548,15 +548,39 @@ private function generateApiListCommands(array $apiCommands, string $commandPref } /** - * Whether any command in the given namespace is visible (not hidden). + * Whether any API command in the given namespace is visible (not hidden). + * + * List commands (api:{namespace}) are only registered when at least one sub-command + * under that namespace exists and is visible. If every sub-command is hidden + * (deprecated/pre-release), the namespace list is omitted. + * + * @param ApiBaseCommand[] $apiCommands */ - private function namespaceHasHidddenCommand(array $apiCommands, string $namespace): bool + private function namespaceHasVisibleCommand(array $apiCommands, string $namespace): bool { - // If namespace is in array sites-instance,sites,environment-v3 then only return true if the command is not hidden and the command name starts with the namespace. - if (in_array($namespace, ['site-instances', 'site-instance', 'sites', 'environment-v3', 'environments-v3'])) { + $commandsInNamespace = []; + foreach ($apiCommands as $apiCommand) { + $commandNameParts = explode(':', $apiCommand->getName()); + if (count($commandNameParts) < 3) { + continue; + } + if ($commandNameParts[1] !== $namespace) { + continue; + } + $commandsInNamespace[] = $apiCommand; + } + + if ($commandsInNamespace === []) { return false; } - return true; + + foreach ($commandsInNamespace as $command) { + if (!$command->isHidden()) { + return true; + } + } + + return false; } /** diff --git a/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php b/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php index bd14fb165..407827157 100644 --- a/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php +++ b/tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php @@ -76,12 +76,37 @@ public function testOnlyOneListCommandPerVisibleNamespace(): void $apiCommands = [ $this->createMockApiCommand('api:accounts:list', false), $this->createMockApiCommand('api:accounts:create', false), - // Excluded namespaces (site-instance, sites, environments-v3) must not get a list command. - $this->createMockApiCommand('api:site-instance:list', false), ]; $listCommands = $this->generateApiListCommands($apiCommands); $this->assertCount(1, $listCommands); $this->assertArrayHasKey('api:accounts', $listCommands); - $this->assertArrayNotHasKey('api:site-instance', $listCommands); + } + + /** + * namespaceHasVisibleCommand must scan the full command list (continue on mismatch). + * If continue were replaced with break, a visible command after another namespace would be missed. + */ + public function testNamespaceVisibleCommandAfterOtherNamespaceStillGetsListCommand(): void + { + $apiCommands = [ + $this->createMockApiCommand('api:mix:first', true), + $this->createMockApiCommand('api:other:list', false), + $this->createMockApiCommand('api:mix:second', false), + ]; + $listCommands = $this->generateApiListCommands($apiCommands); + $this->assertArrayHasKey('api:mix', $listCommands, 'mix has a visible command after other namespace; break would skip it.'); + } + + /** + * When every sub-command under a namespace is hidden, omit the namespace list command. + */ + public function testNamespaceWithAllHiddenCommandsDoesNotGetListCommand(): void + { + $apiCommands = [ + $this->createMockApiCommand('api:baz:list', true), + $this->createMockApiCommand('api:baz:create', true), + ]; + $listCommands = $this->generateApiListCommands($apiCommands); + $this->assertArrayNotHasKey('api:baz', $listCommands); } }