From 3c7a418bbd46c72b387c173641e2e5e42259194f Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Fri, 22 May 2026 15:14:44 +0530 Subject: [PATCH 1/9] feat: Add `isAvailable()` method to AbstractCommand --- system/CLI/AbstractCommand.php | 25 +++++++-- .../CommandNotAvailableException.php | 23 ++++++++ system/Language/en/Commands.php | 1 + .../Modern/UnavailableFixtureCommand.php | 56 +++++++++++++++++++ tests/system/CLI/AbstractCommandTest.php | 42 ++++++++++++++ 5 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 system/CLI/Exceptions/CommandNotAvailableException.php create mode 100644 tests/_support/Commands/Modern/UnavailableFixtureCommand.php diff --git a/system/CLI/AbstractCommand.php b/system/CLI/AbstractCommand.php index 46a4d9982786..519465308d33 100644 --- a/system/CLI/AbstractCommand.php +++ b/system/CLI/AbstractCommand.php @@ -15,6 +15,7 @@ use CodeIgniter\CLI\Attributes\Command; use CodeIgniter\CLI\Exceptions\ArgumentCountMismatchException; +use CodeIgniter\CLI\Exceptions\CommandNotAvailableException; use CodeIgniter\CLI\Exceptions\InvalidArgumentDefinitionException; use CodeIgniter\CLI\Exceptions\InvalidOptionDefinitionException; use CodeIgniter\CLI\Exceptions\OptionValueMismatchException; @@ -390,19 +391,20 @@ public function setInteractive(bool $interactive): static * * The lifecycle is: * - * 1. `initialize()` and `interact()` are handed the raw parsed input by reference, in that order. + * 1. Run `isAvailable()` to check if the command can be run in the current state. + * 2. `initialize()` and `interact()` are handed the raw parsed input by reference, in that order. * Both can mutate the tokens before the framework interprets them against the declared definitions. * Note: the per-run interactive state is captured from `$options` before `initialize()` runs, so * mutating `--no-interaction` from within `initialize()` will not affect this invocation. Use * `setInteractive()` instead. - * 2. The post-hook input is snapshotted into `$unboundArguments` and `$unboundOptions` so the unbound + * 3. The post-hook input is snapshotted into `$unboundArguments` and `$unboundOptions` so the unbound * accessors can report the tokens carried into binding (as opposed to what defaults resolved to). * Any mutations performed in `initialize()` or `interact()` are therefore reflected in the snapshot. - * 3. `bind()` maps the raw tokens onto the declared arguments and options, applying defaults and + * 4. `bind()` maps the raw tokens onto the declared arguments and options, applying defaults and * coercing flag/negation values. - * 4. `validate()` rejects the bound result if it violates any of the declarations — missing required + * 5. `validate()` rejects the bound result if it violates any of the declarations — missing required * argument, unknown option, value/flag mismatches, and so on. - * 5. The bound-and-validated values are snapshotted into `$validatedArguments` / `$validatedOptions` + * 6. The bound-and-validated values are snapshotted into `$validatedArguments` / `$validatedOptions` * and then passed to `execute()`, whose integer return is the command's exit code. * * @param list $arguments Parsed arguments from command line. @@ -412,9 +414,14 @@ public function setInteractive(bool $interactive): static * @throws LogicException * @throws OptionValueMismatchException * @throws UnknownOptionException + * @throws CommandNotAvailableException */ final public function run(array $arguments, array $options): int { + if (! $this->isAvailable()) { + throw new CommandNotAvailableException(lang('Commands.notAvailable', [$this->name])); + } + // Reset per-run interactive state from the current options. $this->runtimeInteractive = $this->hasUnboundOption('no-interaction', $options) ? false : null; @@ -449,6 +456,14 @@ protected function configure(): void { } + /** + * Check whether this command is available to execute in the current system state + */ + protected function isAvailable(): bool + { + return true; + } + /** * Initializes a command before the arguments and options are bound to their definitions. * diff --git a/system/CLI/Exceptions/CommandNotAvailableException.php b/system/CLI/Exceptions/CommandNotAvailableException.php new file mode 100644 index 000000000000..0afc8899dd04 --- /dev/null +++ b/system/CLI/Exceptions/CommandNotAvailableException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\CLI\Exceptions; + +use CodeIgniter\Exceptions\RuntimeException; + +/** + * Exception thrown when a command is found but is not currently available for execution. + */ +final class CommandNotAvailableException extends RuntimeException +{ +} diff --git a/system/Language/en/Commands.php b/system/Language/en/Commands.php index 29e33a9a876f..93f3ab96e33c 100644 --- a/system/Language/en/Commands.php +++ b/system/Language/en/Commands.php @@ -59,4 +59,5 @@ 'reservedOptionName' => 'Option name "--extra_options" is reserved and cannot be used.', 'tooManyArguments' => '{1, plural, =1{One unexpected argument was} other{Multiple unexpected arguments were}} provided to "{0}" command: "{2}".', 'unknownOptions' => 'The following {0, plural, =1{option} other{options}} {0, plural, =1{is} other{are}} unknown in the "{1}" command: {2}.', + 'notAvailable' => 'Command "{0}" is not available in the current environment.', ]; diff --git a/tests/_support/Commands/Modern/UnavailableFixtureCommand.php b/tests/_support/Commands/Modern/UnavailableFixtureCommand.php new file mode 100644 index 000000000000..5ac64584f7cc --- /dev/null +++ b/tests/_support/Commands/Modern/UnavailableFixtureCommand.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Commands\Modern; + +use CodeIgniter\CLI\AbstractCommand; +use CodeIgniter\CLI\Attributes\Command; + +#[Command(name: 'test:unavailable', description: 'Fixture command to test runtime availability checks.', group: 'Tests')] +final class UnavailableFixtureCommand extends AbstractCommand +{ + public static bool $initializeCalled = false; + public static bool $interactCalled = false; + public static bool $executeCalled = false; + public static bool $available = true; + + public static function reset(): void + { + self::$initializeCalled = false; + self::$interactCalled = false; + self::$executeCalled = false; + self::$available = true; + } + + protected function isAvailable(): bool + { + return self::$available; + } + + protected function initialize(array &$arguments, array &$options): void + { + self::$initializeCalled = true; + } + + protected function interact(array &$arguments, array &$options): void + { + self::$interactCalled = true; + } + + protected function execute(array $arguments, array $options): int + { + self::$executeCalled = true; + + return EXIT_SUCCESS; + } +} diff --git a/tests/system/CLI/AbstractCommandTest.php b/tests/system/CLI/AbstractCommandTest.php index 97cc90311754..970fe8f0e4e1 100644 --- a/tests/system/CLI/AbstractCommandTest.php +++ b/tests/system/CLI/AbstractCommandTest.php @@ -15,6 +15,7 @@ use CodeIgniter\CLI\Attributes\Command; use CodeIgniter\CLI\Exceptions\ArgumentCountMismatchException; +use CodeIgniter\CLI\Exceptions\CommandNotAvailableException; use CodeIgniter\CLI\Exceptions\InvalidArgumentDefinitionException; use CodeIgniter\CLI\Exceptions\InvalidOptionDefinitionException; use CodeIgniter\CLI\Exceptions\OptionValueMismatchException; @@ -40,6 +41,7 @@ use Tests\Support\Commands\Modern\InteractiveStateProbeCommand; use Tests\Support\Commands\Modern\ParentCallsInteractFixtureCommand; use Tests\Support\Commands\Modern\TestFixtureCommand; +use Tests\Support\Commands\Modern\UnavailableFixtureCommand; use Throwable; /** @@ -60,6 +62,7 @@ protected function resetAll(): void CLI::reset(); InteractiveStateProbeCommand::reset(); + UnavailableFixtureCommand::reset(); } private function getUndecoratedBuffer(): string @@ -930,6 +933,45 @@ public function testCallPreservesCallerFlagWhenForcingNonInteractive(): void $this->assertFalse(InteractiveStateProbeCommand::$observedInteractive); } + public function testRunThrowsWhenCommandIsUnavailable(): void + { + $command = new UnavailableFixtureCommand(new Commands()); + UnavailableFixtureCommand::$available = false; + + $this->expectException(CommandNotAvailableException::class); + $this->expectExceptionMessage('Command "test:unavailable" is not available in the current environment.'); + + $command->run([], []); + } + + public function testRunExecutesWhenCommandIsAvailable(): void + { + $command = new UnavailableFixtureCommand(new Commands()); + UnavailableFixtureCommand::$available = true; + + $exitCode = $command->run([], []); + + $this->assertSame(EXIT_SUCCESS, $exitCode); + $this->assertTrue(UnavailableFixtureCommand::$initializeCalled); + $this->assertTrue(UnavailableFixtureCommand::$interactCalled); + $this->assertTrue(UnavailableFixtureCommand::$executeCalled); + } + + public function testRunChecksAvailabilityBeforeInitializeInteractAndExecute(): void + { + $command = new UnavailableFixtureCommand(new Commands()); + UnavailableFixtureCommand::$available = false; + + try { + $command->run([], []); + $this->fail('Expected CommandNotAvailableException was not thrown.'); + } catch (CommandNotAvailableException) { + $this->assertFalse(UnavailableFixtureCommand::$initializeCalled); + $this->assertFalse(UnavailableFixtureCommand::$interactCalled); + $this->assertFalse(UnavailableFixtureCommand::$executeCalled); + } + } + /** * @param array|string|null> $options */ From 3c1bd8a77ae4d6b78332eb01028ab9658467ba7a Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Fri, 22 May 2026 15:34:18 +0530 Subject: [PATCH 2/9] Add docs and changelog --- user_guide_src/source/changelogs/v4.8.0.rst | 2 +- .../source/cli/cli_modern_commands.rst | 30 ++++++++++++++++--- .../source/cli/cli_modern_commands/013.php | 24 +++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 user_guide_src/source/cli/cli_modern_commands/013.php diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 5ed77fd5b64f..db749ba744ed 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -183,7 +183,7 @@ Commands ======== - Added a new attribute-based command style built on :php:class:`AbstractCommand ` and the ``#[Command]`` attribute, - with ``configure()`` / ``initialize()`` / ``interact()`` / ``execute()`` hooks and typed ``Argument`` / ``Option`` definitions. + with ``configure()`` / ``isAvailable()`` / ``initialize()`` / ``interact()`` / ``execute()`` hooks and typed ``Argument`` / ``Option`` definitions. The legacy ``BaseCommand`` style continues to work. See :doc:`../cli/cli_modern_commands`. - Every modern command now ships with a ``--no-interaction`` / ``-N`` flag that skips the ``interact()`` hook, plus public ``isInteractive()`` / ``setInteractive()`` methods on ``AbstractCommand``. ``isInteractive()`` also auto-detects piped or diff --git a/user_guide_src/source/cli/cli_modern_commands.rst b/user_guide_src/source/cli/cli_modern_commands.rst index c9d805642149..93ee48afca38 100644 --- a/user_guide_src/source/cli/cli_modern_commands.rst +++ b/user_guide_src/source/cli/cli_modern_commands.rst @@ -68,19 +68,23 @@ this order: extra usage examples. A default ``--help``/ ``-h`` flag, ``--no-header`` flag, and ``--no-interaction``/ ``-N`` flag are added automatically afterwards. -2. ``initialize(array &$arguments, array &$options): void`` receives the raw +2. ``isAvailable(): bool`` is called to check whether the command should run. + By default it returns ``true``, but you can override it to prevent command execution + based on environment, configuration, or any other condition. When it returns ``false``, + the command execution is skipped entirely and ``CommandNotAvailableException`` is thrown. +3. ``initialize(array &$arguments, array &$options): void`` receives the raw arguments and options by reference. Useful when your command needs to massage input — for instance, to unfold an alias argument into the canonical form before anything else runs. -3. ``interact(array &$arguments, array &$options): void`` also receives the +4. ``interact(array &$arguments, array &$options): void`` also receives the raw arguments and options by reference. This is where you prompt the user for missing input, set values conditionally, or abort early. This hook is skipped when the command is non-interactive (see :ref:`non-interactive-mode`). -4. **Bind & validate.** The framework maps the raw input to the definitions +5. **Bind & validate.** The framework maps the raw input to the definitions you declared in ``configure()``, applies defaults, and rejects input that violates the definitions (missing required argument, unknown option, array option passed without a value, and so on). -5. ``execute(array $arguments, array $options): int`` receives the bound and +6. ``execute(array $arguments, array $options): int`` receives the bound and validated arguments and options, and returns an exit code. You only have to implement ``execute()``; the other hooks are optional. @@ -223,6 +227,24 @@ parameter of ``call()``: child ``$options`` so the sub-command resolves its own state. Note: TTY detection can still downgrade the sub-command if STDIN is not a TTY. +***************************** +Restricting Command Execution +***************************** + +Sometimes a command should not run in a specific runtime context. For example, +development-only commands may need to be blocked in the ``production`` environment. + +You can override ``isAvailable()`` to decide at runtime whether the command may execute. +By default, this method returns ``true``. + +.. literalinclude:: cli_modern_commands/013.php + +The availability check runs before ``initialize()``, ``interact()``, argument and +option binding, validation, and ``execute()``. If the command is not available, +a ``CommandNotAvailableException`` is thrown immediately. + +.. note:: This method prevents execution of the command, but does not remove it from help output or command discovery. + ****************** Inside execute() ****************** diff --git a/user_guide_src/source/cli/cli_modern_commands/013.php b/user_guide_src/source/cli/cli_modern_commands/013.php new file mode 100644 index 000000000000..9862c84c136c --- /dev/null +++ b/user_guide_src/source/cli/cli_modern_commands/013.php @@ -0,0 +1,24 @@ + Date: Fri, 22 May 2026 15:36:32 +0530 Subject: [PATCH 3/9] cs-fix --- system/CLI/AbstractCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/CLI/AbstractCommand.php b/system/CLI/AbstractCommand.php index 519465308d33..f07f76aa5b74 100644 --- a/system/CLI/AbstractCommand.php +++ b/system/CLI/AbstractCommand.php @@ -411,10 +411,10 @@ public function setInteractive(bool $interactive): static * @param array|string|null> $options Parsed options from command line. * * @throws ArgumentCountMismatchException + * @throws CommandNotAvailableException * @throws LogicException * @throws OptionValueMismatchException * @throws UnknownOptionException - * @throws CommandNotAvailableException */ final public function run(array $arguments, array $options): int { From 37d1d5b44a2c0b24566af25aed9d3798289aafb8 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Fri, 22 May 2026 15:45:01 +0530 Subject: [PATCH 4/9] fix some english and references --- system/CLI/AbstractCommand.php | 4 ++-- user_guide_src/source/cli/cli_modern_commands.rst | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/system/CLI/AbstractCommand.php b/system/CLI/AbstractCommand.php index f07f76aa5b74..1bc1222d3914 100644 --- a/system/CLI/AbstractCommand.php +++ b/system/CLI/AbstractCommand.php @@ -391,7 +391,7 @@ public function setInteractive(bool $interactive): static * * The lifecycle is: * - * 1. Run `isAvailable()` to check if the command can be run in the current state. + * 1. Run `isAvailable()` to check if the command can be run in the current environment. * 2. `initialize()` and `interact()` are handed the raw parsed input by reference, in that order. * Both can mutate the tokens before the framework interprets them against the declared definitions. * Note: the per-run interactive state is captured from `$options` before `initialize()` runs, so @@ -457,7 +457,7 @@ protected function configure(): void } /** - * Check whether this command is available to execute in the current system state + * Check whether this command is available to execute in the current environment */ protected function isAvailable(): bool { diff --git a/user_guide_src/source/cli/cli_modern_commands.rst b/user_guide_src/source/cli/cli_modern_commands.rst index 93ee48afca38..e3298d92f584 100644 --- a/user_guide_src/source/cli/cli_modern_commands.rst +++ b/user_guide_src/source/cli/cli_modern_commands.rst @@ -68,10 +68,8 @@ this order: extra usage examples. A default ``--help``/ ``-h`` flag, ``--no-header`` flag, and ``--no-interaction``/ ``-N`` flag are added automatically afterwards. -2. ``isAvailable(): bool`` is called to check whether the command should run. - By default it returns ``true``, but you can override it to prevent command execution - based on environment, configuration, or any other condition. When it returns ``false``, - the command execution is skipped entirely and ``CommandNotAvailableException`` is thrown. +2. ``isAvailable(): bool`` is called to check whether the command should execute + (see :ref:`restricting-execution`). 3. ``initialize(array &$arguments, array &$options): void`` receives the raw arguments and options by reference. Useful when your command needs to massage input — for instance, to unfold an alias argument into the canonical @@ -227,6 +225,8 @@ parameter of ``call()``: child ``$options`` so the sub-command resolves its own state. Note: TTY detection can still downgrade the sub-command if STDIN is not a TTY. +.. _restricting-execution: + ***************************** Restricting Command Execution ***************************** From 626aa098b91e08e01bee6d009a00725a19c50bac Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Fri, 22 May 2026 18:16:33 +0530 Subject: [PATCH 5/9] apply suggestions --- system/CLI/AbstractCommand.php | 2 +- user_guide_src/source/cli/cli_modern_commands/013.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/system/CLI/AbstractCommand.php b/system/CLI/AbstractCommand.php index 1bc1222d3914..58b25c6b5957 100644 --- a/system/CLI/AbstractCommand.php +++ b/system/CLI/AbstractCommand.php @@ -457,7 +457,7 @@ protected function configure(): void } /** - * Check whether this command is available to execute in the current environment + * Checks whether this command is available to execute in the current environment */ protected function isAvailable(): bool { diff --git a/user_guide_src/source/cli/cli_modern_commands/013.php b/user_guide_src/source/cli/cli_modern_commands/013.php index 9862c84c136c..a7935131e407 100644 --- a/user_guide_src/source/cli/cli_modern_commands/013.php +++ b/user_guide_src/source/cli/cli_modern_commands/013.php @@ -4,12 +4,11 @@ use CodeIgniter\CLI\AbstractCommand; use CodeIgniter\CLI\Attributes\Command; -use CodeIgniter\CLI\CLI; #[Command(name: 'dev:only', description: 'A dev only command', group: 'Dev')] class DevOnly extends AbstractCommand { - public function isAvailable(): bool + protected function isAvailable(): bool { // Only allow this command in the development environment return ENVIRONMENT === 'development'; From 0558c64e7f61cb514dea5f01c0a41fa9a159d1b0 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Fri, 22 May 2026 18:17:31 +0530 Subject: [PATCH 6/9] Add period --- system/CLI/AbstractCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/CLI/AbstractCommand.php b/system/CLI/AbstractCommand.php index 58b25c6b5957..26edc9218dc5 100644 --- a/system/CLI/AbstractCommand.php +++ b/system/CLI/AbstractCommand.php @@ -457,7 +457,7 @@ protected function configure(): void } /** - * Checks whether this command is available to execute in the current environment + * Checks whether this command is available to execute in the current environment. */ protected function isAvailable(): bool { From 2bd8c648a8f4de6322c7258665f1198f6d104560 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Fri, 22 May 2026 19:45:51 +0530 Subject: [PATCH 7/9] apply suggestions --- system/Language/en/Commands.php | 2 +- tests/system/CLI/AbstractCommandTest.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/system/Language/en/Commands.php b/system/Language/en/Commands.php index 93f3ab96e33c..46320b37d26a 100644 --- a/system/Language/en/Commands.php +++ b/system/Language/en/Commands.php @@ -47,6 +47,7 @@ 'noArgumentsExpected' => 'No arguments expected for "{0}" command. Received: "{1}".', 'nonArrayArgumentWithArrayDefault' => 'Argument "{0}" does not accept an array default value.', 'nonArrayOptionWithArrayValue' => 'Option "--{0}" does not accept an array value.', + 'notAvailable' => 'Command "{0}" is not available in the current environment.', 'optionClashesWithExistingNegation' => 'Option "--{0}" clashes with the negation of negatable option "--{1}".', 'optionNoValueAndNoDefault' => 'Option "--{0}" does not accept a value and cannot have a default value.', 'optionNotAcceptingValue' => 'Option "--{0}" does not accept a value.', @@ -59,5 +60,4 @@ 'reservedOptionName' => 'Option name "--extra_options" is reserved and cannot be used.', 'tooManyArguments' => '{1, plural, =1{One unexpected argument was} other{Multiple unexpected arguments were}} provided to "{0}" command: "{2}".', 'unknownOptions' => 'The following {0, plural, =1{option} other{options}} {0, plural, =1{is} other{are}} unknown in the "{1}" command: {2}.', - 'notAvailable' => 'Command "{0}" is not available in the current environment.', ]; diff --git a/tests/system/CLI/AbstractCommandTest.php b/tests/system/CLI/AbstractCommandTest.php index 970fe8f0e4e1..bc7b673d19e0 100644 --- a/tests/system/CLI/AbstractCommandTest.php +++ b/tests/system/CLI/AbstractCommandTest.php @@ -935,7 +935,8 @@ public function testCallPreservesCallerFlagWhenForcingNonInteractive(): void public function testRunThrowsWhenCommandIsUnavailable(): void { - $command = new UnavailableFixtureCommand(new Commands()); + $command = new UnavailableFixtureCommand(new Commands()); + UnavailableFixtureCommand::$available = false; $this->expectException(CommandNotAvailableException::class); @@ -946,7 +947,8 @@ public function testRunThrowsWhenCommandIsUnavailable(): void public function testRunExecutesWhenCommandIsAvailable(): void { - $command = new UnavailableFixtureCommand(new Commands()); + $command = new UnavailableFixtureCommand(new Commands()); + UnavailableFixtureCommand::$available = true; $exitCode = $command->run([], []); @@ -959,7 +961,8 @@ public function testRunExecutesWhenCommandIsAvailable(): void public function testRunChecksAvailabilityBeforeInitializeInteractAndExecute(): void { - $command = new UnavailableFixtureCommand(new Commands()); + $command = new UnavailableFixtureCommand(new Commands()); + UnavailableFixtureCommand::$available = false; try { From e05b978e18e980062c5bd308c03c8bd865125859 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Fri, 22 May 2026 19:45:58 +0530 Subject: [PATCH 8/9] Added more tests --- tests/system/Commands/HelpCommandTest.php | 47 ++++++++++++++++------ tests/system/Commands/ListCommandsTest.php | 8 ++++ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/tests/system/Commands/HelpCommandTest.php b/tests/system/Commands/HelpCommandTest.php index ab916b055739..07a1d53a8a94 100644 --- a/tests/system/Commands/HelpCommandTest.php +++ b/tests/system/Commands/HelpCommandTest.php @@ -29,18 +29,6 @@ final class HelpCommandTest extends CIUnitTestCase { use StreamFilterTrait; - #[After] - #[Before] - protected function resetCli(): void - { - CLI::reset(); - } - - private function getUndecoratedBuffer(): string - { - return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; - } - public function testNoArgumentDescribesItself(): void { command('help'); @@ -67,6 +55,11 @@ public function testNoArgumentDescribesItself(): void ); } + private function getUndecoratedBuffer(): string + { + return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; + } + public function testDescribeCommandNoArguments(): void { command('help app:about'); @@ -126,6 +119,29 @@ public function testDescribeSpecificCommand(): void ); } + public function testDescribeUnavailableCommand(): void + { + command('help test:unavailable'); + + $this->assertSame( + <<<'EOT' + + Usage: + test:unavailable [options] + + Description: + Fixture command to test runtime availability checks. + + Options: + -h, --help Display help for the given command. + --no-header Do not display the banner when running the command. + -N, --no-interaction Do not ask any interactive questions. + + EOT, + $this->getUndecoratedBuffer(), + ); + } + public function testDescribeLegacyCommandUsesLegacyShowHelp(): void { // `app:info` is a legacy BaseCommand fixture. Help must take the @@ -266,4 +282,11 @@ public function testHelpCommandWithDoubleHyphenStillRemovesBanner(): void ); $this->assertStringContainsString('Lists the available commands.', $this->getStreamFilterBuffer()); } + + #[After] + #[Before] + protected function resetCli(): void + { + CLI::reset(); + } } diff --git a/tests/system/Commands/ListCommandsTest.php b/tests/system/Commands/ListCommandsTest.php index 621f3feb165c..23c7551d5f4d 100644 --- a/tests/system/Commands/ListCommandsTest.php +++ b/tests/system/Commands/ListCommandsTest.php @@ -58,6 +58,14 @@ public function testRunCommandWithSimpleOption(): void $this->assertStringNotContainsString('Clears the current system caches.', $this->getStreamFilterBuffer()); } + public function testUnavailableCommandIsStillListed(): void + { + command('list'); + + $this->assertStringContainsString('test:unavailable', $this->getStreamFilterBuffer()); + $this->assertStringContainsString('Fixture command to test runtime availability checks.', $this->getStreamFilterBuffer()); + } + public function testDuplicateCommandNameListedOnceInSimpleOutput(): void { $list = new ListCommands($this->mockRunnerWithDuplicate()); From afd53b9f14bc16286e7eea32e50b813ed8e6072c Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Fri, 22 May 2026 19:51:23 +0530 Subject: [PATCH 9/9] Revert PHPStorm's auto styled code --- tests/system/Commands/HelpCommandTest.php | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/system/Commands/HelpCommandTest.php b/tests/system/Commands/HelpCommandTest.php index 07a1d53a8a94..a8308eca3f01 100644 --- a/tests/system/Commands/HelpCommandTest.php +++ b/tests/system/Commands/HelpCommandTest.php @@ -29,6 +29,18 @@ final class HelpCommandTest extends CIUnitTestCase { use StreamFilterTrait; + #[After] + #[Before] + protected function resetCli(): void + { + CLI::reset(); + } + + private function getUndecoratedBuffer(): string + { + return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; + } + public function testNoArgumentDescribesItself(): void { command('help'); @@ -55,11 +67,6 @@ public function testNoArgumentDescribesItself(): void ); } - private function getUndecoratedBuffer(): string - { - return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()) ?? ''; - } - public function testDescribeCommandNoArguments(): void { command('help app:about'); @@ -282,11 +289,4 @@ public function testHelpCommandWithDoubleHyphenStillRemovesBanner(): void ); $this->assertStringContainsString('Lists the available commands.', $this->getStreamFilterBuffer()); } - - #[After] - #[Before] - protected function resetCli(): void - { - CLI::reset(); - } }