From 9b0101717b1d7fedc50f042338d1b4fdfd351ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Wed, 1 Oct 2025 18:17:11 +0300 Subject: [PATCH 1/4] chore(deps): add laravel/prompts for modern CLI interactions Add laravel/prompts package to enable beautiful, interactive CLI prompts with validation, auto-complete, and loading spinners for enhanced UX. --- composer.json | 1 + composer.lock | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f7b468c5..e0a91310 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "require": { "php": "^8.2", "guzzlehttp/guzzle": "^7.10", + "laravel/prompts": "^0.3.7", "phpseclib/phpseclib": "^3.0", "symfony/console": "^7.3", "symfony/dotenv": "^7.3", diff --git a/composer.lock b/composer.lock index b4dc454a..d7023b2c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e86b7f3511958e642dac0e17d1f5ed23", + "content-hash": "3181e5843386ae2bf1e36e28c2a5cfeb", "packages": [ { "name": "guzzlehttp/guzzle", @@ -331,6 +331,65 @@ ], "time": "2025-08-23T21:21:41+00:00" }, + { + "name": "laravel/prompts", + "version": "v0.3.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.7" + }, + "time": "2025-09-19T13:47:56+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v3.1.3", From 37123380ebbd539d145ae79d9a3096b09a2c880a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Wed, 1 Oct 2025 18:17:25 +0300 Subject: [PATCH 2/4] refactor(console)!: remove quiet mode and simplify output handling Remove quiet mode suppression from BaseCommand output methods and introduce custom input definition that exposes only essential options. Changes: - Remove $isQuiet property and checks from BaseCommand - Remove quiet mode logic from writeln(), text(), and hr() methods - Add custom getDefaultInputDefinition() in SymfonyApp - Expose only --help, --version, and --ansi options - Add early exit when --version flag is used - Banner now always displays (no quiet mode suppression) BREAKING CHANGE: --quiet, --verbose, and --no-interaction flags are no longer available. Commands always produce output. This aligns with the new philosophy of modern TUI styling over verbosity management. --- app/Contracts/BaseCommand.php | 15 --------------- app/SymfonyApp.php | 23 +++++++++++++++++++++-- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/Contracts/BaseCommand.php b/app/Contracts/BaseCommand.php index fc949f50..d2108927 100644 --- a/app/Contracts/BaseCommand.php +++ b/app/Contracts/BaseCommand.php @@ -17,8 +17,6 @@ abstract class BaseCommand extends Command { protected SymfonyStyle $io; - protected bool $isQuiet = false; - public function __construct( protected readonly Container $container, protected readonly EnvService $env, @@ -61,7 +59,6 @@ protected function initialize(InputInterface $input, OutputInterface $output): v parent::initialize($input, $output); $this->io = new SymfonyStyle($input, $output); - $this->isQuiet = $output->isQuiet(); // // Initialize env service @@ -114,10 +111,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ protected function writeln(string|array $lines): void { - if ($this->isQuiet) { - return; - } - $writeLines = is_array($lines) ? $lines : [$lines]; foreach ($writeLines as $line) { $this->io->writeln(' ' . $line); @@ -131,10 +124,6 @@ protected function writeln(string|array $lines): void */ protected function text(string|array $lines): void { - if ($this->isQuiet) { - return; - } - $writeLines = is_array($lines) ? $lines : [$lines]; foreach ($writeLines as $line) { $this->io->text(' ' . $line); @@ -146,10 +135,6 @@ protected function text(string|array $lines): void */ protected function hr(): void { - if ($this->isQuiet) { - return; - } - $this->writeln([ '╭──────────────────────────────────────────', '', diff --git a/app/SymfonyApp.php b/app/SymfonyApp.php index ca28e53b..1e09988d 100644 --- a/app/SymfonyApp.php +++ b/app/SymfonyApp.php @@ -8,7 +8,10 @@ use Bigpixelrocket\DeployerPHP\Services\VersionService; use Symfony\Component\Console\Application as SymfonyApplication; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -36,6 +39,19 @@ public function __construct( // Public // ------------------------------------------------------------------------------- + /** + * Override default input definition to remove unwanted options. + */ + protected function getDefaultInputDefinition(): InputDefinition + { + return new InputDefinition([ + new InputArgument('command', InputArgument::OPTIONAL, 'The command to execute'), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the list command'), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'), + new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', null), + ]); + } + /** * Override to hide default Symfony application name/version display. */ @@ -51,8 +67,11 @@ public function doRun(InputInterface $input, OutputInterface $output): int { $this->io = new SymfonyStyle($input, $output); - if (!$output->isQuiet()) { - $this->displayBanner(); + $this->displayBanner(); + + // If --version is requested, skip the rest (banner includes version) + if ($input->hasParameterOption(['--version', '-V'], true)) { + return Command::SUCCESS; } return parent::doRun($input, $output); From 20597a580b8e52e13012efc824aeaca8e7546f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Wed, 1 Oct 2025 18:17:37 +0300 Subject: [PATCH 3/4] docs(console): update rules for modern TUI-focused approach Replace quiet/verbosity management documentation with modern TUI philosophy emphasizing BaseCommand custom methods and Laravel Prompts. Changes: - Remove quiet mode and verbosity level documentation - Add output method philosophy section - Document mandatory use of BaseCommand custom methods - Add Laravel Prompts integration examples - Remove performance and verbosity pattern guidelines - Simplify to focus on consistent, beautiful output --- .cursor/rules/03-commands.mdc | 266 +++++++++------------------------- 1 file changed, 65 insertions(+), 201 deletions(-) diff --git a/.cursor/rules/03-commands.mdc b/.cursor/rules/03-commands.mdc index 4fffe725..b28e138a 100644 --- a/.cursor/rules/03-commands.mdc +++ b/.cursor/rules/03-commands.mdc @@ -6,71 +6,89 @@ alwaysApply: true **🚨 Console rules are MANDATORY and IMMUTABLE - fix violating code, not the console rules** -### 🔇 Quiet Mode Philosophy +### 📤 Output Method Philosophy -**Philosophy:** All console output must respect global options `--quiet` (-q) and `--silent` to ensure minimal noise in automated/CI runs while keeping errors visible in quiet mode. +**🚨 NEVER use Symfony IO methods directly - use [BaseCommand.php](mdc:app/Contracts/BaseCommand.php) methods exclusively** -**Core Principle:** Commands should be silent when requested, but never hide critical errors. +**Core Principle:** All console output flows through custom methods in BaseCommand that: -### 📤 Output Method Hierarchy +- Provide modern, beautiful TUI styling +- Maintain consistent user experience +- Enable centralized output control -Use `SymfonyStyle` methods in this order of preference: - -**🟢 High-Level Methods (MANDATORY - Auto-Honor Quiet/Silent)** - -These automatically respect verbosity settings: +**Custom IO Methods (MANDATORY):** ```php -// ✅ CORRECT - Auto-suppress in quiet mode -$this->io->success('Task completed!'); // Green success block -$this->io->info('Processing...'); // Blue info block -$this->io->warning('Heads up'); // Yellow warning -$this->io->error('Failed'); // Red error (shown in quiet) -$this->io->note('Tip'); // Note block -$this->io->caution('Careful'); // Caution block -$this->io->table($headers, $rows); // Formatted table -$this->io->progressStart(); // Progress indicators +// ✅ CORRECT - BaseCommand custom methods +$this->writeln(['Multiple', 'lines']); // Multi-line output +$this->text('Single line message'); // Simple text output +$this->hr(); // Beautiful section separator ``` -**🟡 BaseCommand Wrapper Methods (REQUIRED for Low-Level)** - -For simple text output, use our custom wrappers that honor quiet mode: +**❌ FORBIDDEN - Direct Symfony IO Usage:** ```php -// ✅ CORRECT - Project wrappers respect quiet mode -$this->writeln(['Multiple', 'lines']); // Multi-line output -$this->text('Single line message'); // Simple text -$this->hr(); // Section separator +// ❌ NEVER use Symfony methods directly +$this->io->writeln('Direct output'); // Bypasses our custom styling +$this->io->text('Raw text'); // Inconsistent with our TUI +$this->io->success('Task done'); // Basic, outdated styling ``` -**🔴 Raw Symfony Methods (FORBIDDEN)** +**🔧 Missing a Method? Create It!** -Never use these directly - they bypass quiet mode: +If you need output functionality not in [BaseCommand.php](mdc:app/Contracts/BaseCommand.php): + +1. Add a new method to BaseCommand +2. Use modern styling and formatting +3. Keep it reusable and minimal +4. Document with example usage ```php -// ❌ FORBIDDEN - Ignores quiet mode -$this->io->writeln('Direct output'); // Always shows -$this->io->text('Bypasses quiet'); // Always shows +// ✅ CORRECT - Extend BaseCommand for new needs +protected function success(string $message): void { + // Custom success styling here +} ``` -### 🎨 Custom Formatting Rules +**Integration Points:** -**When to Use Raw Methods:** +- Base implementation: [BaseCommand.php](mdc:app/Contracts/BaseCommand.php) +- Current methods: `writeln()`, `text()`, `hr()` -Only use raw `$this->io->writeln()` for complex styling where high-level methods don't suffice: +### 🎯 User Input with Laravel Prompts + +**Use `laravel/prompts` for ALL user interactions to create rich CLI experiences:** ```php -// ✅ CORRECT - Custom formatting with quiet check -if (!$this->isQuiet) { - $this->io->writeln(' ╭─ Custom Header ─╮'); +use function Laravel\Prompts\text; +use function Laravel\Prompts\password; +use function Laravel\Prompts\confirm; +use function Laravel\Prompts\select; +use function Laravel\Prompts\multiselect; +use function Laravel\Prompts\suggest; +use function Laravel\Prompts\search; +use function Laravel\Prompts\spin; + +// ✅ CORRECT - Modern prompts with interaction check +if ($this->io->isInteractive()) { + $name = text('What is your name?', required: true); + $password = password('Enter password:', required: true); + $confirmed = confirm('Deploy to production?', default: false); + $environment = select('Select environment:', ['dev', 'staging', 'prod']); + $features = multiselect('Enable features:', ['cache', 'queue', 'logs']); } + +// Spinners work in all modes +$result = spin(fn() => $this->service->heavyOperation(), 'Processing...'); ``` -**Integration Points:** +**Key Benefits:** -- Status display: [BaseCommand.php](mdc:app/Contracts/BaseCommand.php) → `initialize()` method -- Wrapper methods: `writeln()`, `text()`, `hr()` → All check `$this->isQuiet` flag -- Quiet detection: Set once in `initialize()` for performance +- Beautiful, interactive prompts with validation +- Auto-complete and search functionality +- Loading spinners for long operations +- Consistent, modern user experience +- Zero Symfony IO boilerplate ### 📋 Command Layer Patterns @@ -86,17 +104,17 @@ if (!$this->isQuiet) { ```php class MyCommand extends BaseCommand { protected function execute(InputInterface $input, OutputInterface $output): int { - // ✅ CORRECT - Use high-level methods first - $this->io->info('Starting process...'); + // ✅ CORRECT - Custom BaseCommand methods + $this->text('Starting process...'); - // ✅ CORRECT - Use wrappers for simple text - $this->text('Processing item: ' . $item); + // Prompt user with Laravel Prompts + $confirmed = confirm('Continue with deployment?', default: true); - // ✅ CORRECT - Services return data, not output + // Services return data, not output $result = $this->service->performWork(); - // ✅ CORRECT - Commands format the output - $this->io->success('Completed: ' . $result); + // Custom output formatting + $this->writeln('✓ Completed: ' . $result); return Command::SUCCESS; } @@ -105,162 +123,8 @@ class MyCommand extends BaseCommand { **Key Benefits:** -- Zero console noise in automated environments - Consistent user experience across all commands - Easy testing with mockable I/O patterns - Clean separation between business logic and presentation **Rule:** Commands orchestrate Services and format output. Services never touch console I/O. - -### 🔊 Verbosity Level Management - -**Philosophy:** Provide progressively more detail as users request higher verbosity, from essential information to debug traces. - -**Symfony Verbosity Levels:** - -| Level | Flag | Constant | Usage | -| ------------ | ------ | ------------------------ | ----------------------------- | -| Normal | (none) | `VERBOSITY_NORMAL` | Essential output only | -| Verbose | `-v` | `VERBOSITY_VERBOSE` | Additional context & progress | -| Very Verbose | `-vv` | `VERBOSITY_VERY_VERBOSE` | Detailed operation info | -| Debug | `-vvv` | `VERBOSITY_DEBUG` | Full debugging traces | - -**🟢 High-Level Methods (Auto-Verbosity Support)** - -These methods automatically show at appropriate verbosity levels: - -```php -// ✅ CORRECT - Auto-verbosity management -$this->io->success('Task completed!'); // Normal+ (always shown) -$this->io->info('Processing items...'); // Normal+ (always shown) -$this->io->note('Using cached data'); // Verbose+ (-v and above) -$this->io->section('Deployment Phase'); // Normal+ (section headers) - -// Progress indicators respect verbosity automatically -$progress = $this->io->createProgressBar(100); // Normal+ (essential feedback) -``` - -**🎯 Content Guidelines by Verbosity** - -**Normal (Default) - Essential Only:** - -```php -$this->io->success('Deployment completed successfully'); -$this->io->error('Failed to connect to server'); -$this->io->warning('Configuration file not found, using defaults'); -``` - -**Verbose (-v) - Progress & Context:** - -```php -$this->io->note('Found 25 files to process'); -$this->io->text('Connecting to server: example.com'); -$this->io->section('Installing Dependencies'); -``` - -**Very Verbose (-vv) - Detailed Operations:** - -```php -$this->io->text('Reading configuration from: /path/to/config.yml'); -$this->io->text('Executing: composer install --no-dev'); -$this->io->table(['File', 'Status'], $detailedResults); -``` - -**Debug (-vvv) - Full Traces:** - -```php -// Use raw output with verbosity checks for debug traces -if ($this->io->isVeryVerbose()) { - $this->text('DEBUG: Raw API response: ' . json_encode($response)); -} - -if ($this->io->isDebug()) { - $this->text('TRACE: Method call stack: ' . implode(' → ', $trace)); -} -``` - -**🔧 Custom Verbosity Checks** - -For fine-grained control, use verbosity methods: - -```php -// ✅ CORRECT - Custom verbosity logic -if ($this->io->isVerbose()) { - $this->text('Scanning directory: ' . $directory); -} - -if ($this->io->isVeryVerbose()) { - $this->io->table(['Property', 'Value'], $configDetails); -} - -if ($this->io->isDebug()) { - $this->text('Memory usage: ' . memory_get_peak_usage(true)); -} - -// Available verbosity checks: -// $this->io->isQuiet() // -q flag -// $this->io->isVerbose() // -v flag -// $this->io->isVeryVerbose() // -vv flag -// $this->io->isDebug() // -vvv flag -``` - -**⚡ Performance Considerations** - -Avoid expensive operations unless verbosity justifies them: - -```php -// ✅ CORRECT - Only collect debug data when needed -if ($this->io->isDebug()) { - $debugInfo = $this->service->getExpensiveDebugData(); - $this->text('Debug info: ' . json_encode($debugInfo)); -} - -// ❌ WRONG - Always collecting expensive data -$debugInfo = $this->service->getExpensiveDebugData(); -if ($this->io->isDebug()) { - $this->text('Debug info: ' . json_encode($debugInfo)); -} -``` - -**🎨 Output Patterns for Commands** - -Structure your command output progressively: - -```php -protected function execute(InputInterface $input, OutputInterface $output): int { - // Always show: Critical start message - $this->io->info('Starting deployment process...'); - - // Verbose: Show configuration summary - if ($this->io->isVerbose()) { - $this->text('Target: ' . $this->config->getServer()); - $this->text('Environment: ' . $this->config->getEnvironment()); - } - - // Process with automatic progress (normal verbosity) - $progress = $this->io->createProgressBar(count($tasks)); - foreach ($tasks as $task) { - $this->processTask($task); - - // Very verbose: Show each task detail - if ($this->io->isVeryVerbose()) { - $this->text('Completed: ' . $task->getName()); - } - - $progress->advance(); - } - $progress->finish(); - - // Always show: Final status - $this->io->success('Deployment completed successfully'); - - return Command::SUCCESS; -} -``` - -**Key Benefits:** - -- Users control information density with standard Symfony flags -- Essential information always visible, details available on demand -- Performance optimized - expensive debug data only when requested -- Consistent verbosity behavior across all commands From b42e33d25749c4c481ab235364c87d1b6300b437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Wed, 1 Oct 2025 18:17:49 +0300 Subject: [PATCH 4/4] test(console): update tests for simplified output handling Remove obsolete quiet mode tests and add new tests for custom input definition and version flag handling. Changes: - Remove quiet mode suppression tests from BaseCommandTest - Remove quiet mode test from HelloCommandTest - Add test for custom input definition (only essential options) - Add test for early exit on --version flag - Add test for banner always displaying - All tests passing with new architecture (14 integration tests) --- .../Integration/Console/HelloCommandTest.php | 13 ---- tests/Integration/SymfonyAppTest.php | 60 +++++++++++++++++ tests/Unit/Contracts/BaseCommandTest.php | 64 ------------------- 3 files changed, 60 insertions(+), 77 deletions(-) diff --git a/tests/Integration/Console/HelloCommandTest.php b/tests/Integration/Console/HelloCommandTest.php index 8cdb0379..fde5996e 100644 --- a/tests/Integration/Console/HelloCommandTest.php +++ b/tests/Integration/Console/HelloCommandTest.php @@ -59,19 +59,6 @@ function createCommandTester(): CommandTester 'defaults when empty' => [[], 'Hello there!'], ]); - it('suppresses all output in quiet mode', function () { - // ARRANGE - setEnv('USER', 'testuser'); - $tester = createCommandTester(); - - // ACT - $exitCode = $tester->execute([], ['verbosity' => \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_QUIET]); - - // ASSERT - expect($exitCode)->toBe(Command::SUCCESS) - ->and($tester->getDisplay())->toBe(''); - }); - afterEach(function () { foreach ($this->originals as $key => $value) { setEnv($key, $value); diff --git a/tests/Integration/SymfonyAppTest.php b/tests/Integration/SymfonyAppTest.php index f8cbd904..ec3484f6 100644 --- a/tests/Integration/SymfonyAppTest.php +++ b/tests/Integration/SymfonyAppTest.php @@ -147,4 +147,64 @@ ->and($output2)->toContain('┌┬┐┌─┐┌─┐'); }); + // + // Custom Input Definition + + it('exposes only custom-defined input options', function () { + // ARRANGE + $container = new Container(); + $app = $container->build(SymfonyApp::class); + + // ACT + $definition = $app->getDefinition(); + $availableOptions = array_keys($definition->getOptions()); + + // ASSERT + expect($availableOptions)->toContain('help') + ->and($availableOptions)->toContain('version') + ->and($availableOptions)->toContain('ansi') + ->and($availableOptions)->not->toContain('quiet') + ->and($availableOptions)->not->toContain('verbose') + ->and($availableOptions)->not->toContain('no-interaction'); + }); + + it('exits early when --version flag is provided', function () { + // ARRANGE + $container = new Container(); + $app = $container->build(SymfonyApp::class); + + $input = new ArrayInput(['--version' => true]); + + $output = new BufferedOutput(); + $output->setDecorated(false); + + // ACT + $exitCode = $app->doRun($input, $output); + $outputContent = $output->fetch(); + + // ASSERT + expect($exitCode)->toBe(Command::SUCCESS) + ->and($outputContent)->toContain('┌┬┐┌─┐┌─┐') + ->and($outputContent)->not->toContain('Available commands'); + }); + + it('always displays banner regardless of output settings', function () { + // ARRANGE + $container = new Container(); + $app = $container->build(SymfonyApp::class); + + $input = new ArrayInput(['command' => 'list']); + + $output = new BufferedOutput(); + $output->setDecorated(false); + + // ACT + $app->doRun($input, $output); + $outputContent = $output->fetch(); + + // ASSERT + expect($outputContent)->toContain('┌┬┐┌─┐┌─┐') + ->and($outputContent)->toContain('The Server & Site Deployment Tool for PHP'); + }); + }); diff --git a/tests/Unit/Contracts/BaseCommandTest.php b/tests/Unit/Contracts/BaseCommandTest.php index 9f7d8a4f..3e4168a2 100644 --- a/tests/Unit/Contracts/BaseCommandTest.php +++ b/tests/Unit/Contracts/BaseCommandTest.php @@ -113,70 +113,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'no env file' => [false, '/No \\.env file found/'], ]); - it('suppresses all output in quiet mode', function () { - // ARRANGE - $container = new Container(); - $env = mockEnvService(true); - $inventory = mockInventoryService(true); - $command = new TestableBaseCommand($container, $env, $inventory); - $tester = new CommandTester($command); - - // ACT - $exitCode = $tester->execute([], ['verbosity' => OutputInterface::VERBOSITY_QUIET]); - $output = $tester->getDisplay(); - - // ASSERT - expect($exitCode)->toBe(Command::SUCCESS) - ->and($output)->toBe(''); - }); - - it('wrapper methods writeln, text, and hr respect quiet mode', function (string $method) { - // ARRANGE - $container = new Container(); - $env = mockEnvService(true); - $inventory = mockInventoryService(true); - - $command = new class ($container, $env, $inventory) extends BaseCommand { - protected function configure(): void - { - parent::configure(); - $this->setName('test-wrapper'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - parent::execute($input, $output); - - match ($input->getOption('test-method')) { - 'writeln' => $this->writeln('Test message'), - 'text' => $this->text('Test message'), - 'hr' => $this->hr(), - default => null, - }; - - return Command::SUCCESS; - } - }; - - $command->getDefinition()->addOption( - new \Symfony\Component\Console\Input\InputOption('test-method', null, \Symfony\Component\Console\Input\InputOption::VALUE_REQUIRED) - ); - - $tester = new CommandTester($command); - - // ACT - Normal mode - $tester->execute(['--test-method' => $method]); - $normalOutput = $tester->getDisplay(); - - // ACT - Quiet mode - $tester->execute(['--test-method' => $method], ['verbosity' => OutputInterface::VERBOSITY_QUIET]); - $quietOutput = $tester->getDisplay(); - - // ASSERT - expect($normalOutput)->not->toBe('') - ->and($quietOutput)->toBe(''); - })->with(['writeln', 'text', 'hr']); - it('hr displays separator line', function () { // ARRANGE $container = new Container();