container = new DefaultContainer(); $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); $this->pathProvider = new PathProvider('/home/michael/dev/michaelschiemer'); // Register all required dependencies $this->container->singleton(Cache::class, $this->cache); $this->container->singleton(Clock::class, $this->clock); $this->container->singleton(PathProvider::class, $this->pathProvider); // Register ReflectionProvider and ExecutionContext dependencies $reflectionProvider = new CachedReflectionProvider(); $executionContext = ExecutionContext::detect(); $this->container->singleton(ReflectionProvider::class, $reflectionProvider); $this->container->singleton(ExecutionContext::class, $executionContext); $this->container->singleton(InitializerProcessor::class, fn ($c) => new InitializerProcessor( $c, $c->get(ReflectionProvider::class), $c->get(ExecutionContext::class) )); $this->container->singleton(DemoCommand::class, fn () => new DemoCommand()); // App config for testing $appConfig = new AppConfig( environment: 'testing', debug: true, timezone: Timezone::UTC, locale: 'en' ); $this->container->singleton(AppConfig::class, $appConfig); // Clear all caches to ensure fresh discovery $this->cache->clear(); // Clear opcache if available if (function_exists('opcache_reset')) { opcache_reset(); } // Create test output $this->output = new class () implements \App\Framework\Console\ConsoleOutputInterface { public array $capturedLines = []; public array $errorLines = []; public function write(string $message, null|\App\Framework\Console\ConsoleColor|\App\Framework\Console\ConsoleStyle $style = null): void { $this->capturedLines[] = $message; } public function writeLine(string $message = '', ?\App\Framework\Console\ConsoleColor $color = null): void { $this->capturedLines[] = $message; } public function writeError(string $message): void { $this->errorLines[] = $message; $this->capturedLines[] = "ERROR: $message"; } public function writeSuccess(string $message): void { $this->capturedLines[] = "SUCCESS: $message"; } public function writeWarning(string $message): void { $this->capturedLines[] = "WARNING: $message"; } public function writeInfo(string $message): void { $this->capturedLines[] = "INFO: $message"; } public function newLine(int $count = 1): void { for ($i = 0; $i < $count; $i++) { $this->capturedLines[] = ''; } } public function askQuestion(string $question, ?string $default = null): string { return $default ?? ''; } public function confirm(string $question, bool $default = false): bool { return $default; } public function writeWindowTitle(string $title, int $mode = 0): void { // No-op for testing } }; }); it('performs complete console command discovery and execution flow', function () { // Step 1: Fresh discovery from scratch $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); $registry = $bootstrapper->bootstrap(); // Verify discovery found console commands $consoleCommands = $registry->attributes->get(ConsoleCommand::class); expect($consoleCommands)->not->toBeEmpty(); expect(count($consoleCommands))->toBeGreaterThanOrEqual(6); // At least the demo commands // Step 2: Create console application $app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output); // Step 3: Verify command discovery worked $argv = ['test-console', 'help']; $exitCode = $app->run($argv); expect($exitCode)->toBe(ExitCode::SUCCESS->value); $output = implode(' ', $this->output->capturedLines); expect($output)->toContain('demo:hello'); expect($output)->toContain('demo:colors'); expect($output)->not->toContain('Keine Kommandos verfügbar'); // Step 4: Execute specific command $this->output->capturedLines = []; // Clear output $argv = ['test-console', 'demo:hello', 'EndToEndTest']; $exitCode = $app->run($argv); expect($exitCode)->toBe(ExitCode::SUCCESS->value); $output = implode(' ', $this->output->capturedLines); expect($output)->toContain('Hallo, EndToEndTest!'); }); it('handles cache-miss scenario correctly', function () { // Ensure cache is completely empty $this->cache->clear(); // Verify cache is empty for discovery $paths = [$this->pathProvider->getSourcePath()]; $context = ExecutionContext::detect(); $cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, $context->getType()->value); $cachedItem = $this->cache->get($cacheKey); expect($cachedItem->isHit)->toBeFalse(); // Create console application (should trigger fresh discovery) $app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output); // Should work despite cache miss $argv = ['test-console', 'demo:hello']; $exitCode = $app->run($argv); expect($exitCode)->toBe(ExitCode::SUCCESS->value); // Verify cache is now populated $cachedItem = $this->cache->get($cacheKey); expect($cachedItem->isHit)->toBeTrue(); }); it('handles context-specific cache separation correctly', function () { // Test that different execution contexts have separate caches $paths = [$this->pathProvider->getSourcePath()]; $consoleKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, 'console'); $cliScriptKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, 'cli-script'); // Keys should be different expect($consoleKey->toString())->not->toBe($cliScriptKey->toString()); // Perform discovery (will cache under current context) $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); $registry = $bootstrapper->bootstrap(); // One cache should be hit, the other should not $consoleCached = $this->cache->get($consoleKey); $cliScriptCached = $this->cache->get($cliScriptKey); // At least one should be hit (depending on current context) $anyCacheHit = $consoleCached->isHit || $cliScriptCached->isHit; expect($anyCacheHit)->toBeTrue(); }); it('discovers minimum expected number of commands', function () { // Fresh discovery to get actual count $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); $registry = $bootstrapper->bootstrap(); $consoleCommands = $registry->attributes->get(ConsoleCommand::class); $commandCount = count($consoleCommands); // Should find at least: // - 6 demo commands (hello, colors, interactive, menu, simple-menu, wizard) // - Various framework commands (db:migrate, db:status, etc.) expect($commandCount)->toBeGreaterThanOrEqual(10); // Verify specific demo commands are present $commandNames = []; foreach ($consoleCommands as $discovered) { $command = $discovered->createAttributeInstance(); $commandNames[] = $command->name; } $expectedDemoCommands = [ 'demo:hello', 'demo:colors', 'demo:interactive', 'demo:menu', 'demo:simple-menu', 'demo:wizard', ]; foreach ($expectedDemoCommands as $expectedCommand) { expect($commandNames)->toContain($expectedCommand); } }); it('handles no duplicate command names', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); $registry = $bootstrapper->bootstrap(); $consoleCommands = $registry->attributes->get(ConsoleCommand::class); $commandNames = []; $duplicates = []; foreach ($consoleCommands as $discovered) { $command = $discovered->createAttributeInstance(); if (in_array($command->name, $commandNames)) { $duplicates[] = $command->name; } else { $commandNames[] = $command->name; } } // Should have no duplicate command names expect($duplicates)->toBeEmpty("Found duplicate commands: " . implode(', ', $duplicates)); }); it('recovers gracefully when fallback discovery is triggered', function () { // Simulate a scenario where initial discovery fails or finds no commands // by registering an empty discovery registry first $emptyRegistry = new \App\Framework\Discovery\Results\DiscoveryRegistry( attributes: new \App\Framework\Discovery\Results\AttributeRegistry([]), interfaces: new \App\Framework\Discovery\Results\InterfaceRegistry([]), templates: new \App\Framework\Discovery\Results\TemplateRegistry([]) ); $this->container->singleton(\App\Framework\Discovery\Results\DiscoveryRegistry::class, $emptyRegistry); // Create console application - should trigger fallback discovery $app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output); // Should still be able to execute commands after fallback $argv = ['test-console', 'demo:hello']; $exitCode = $app->run($argv); // May succeed if fallback worked, or fail gracefully if no commands found expect($exitCode)->toBeInt(); expect($exitCode)->toBeGreaterThanOrEqual(0); }); it('validates ConsoleCommand attribute syntax is correct throughout codebase', function () { // Perform discovery and verify all found commands have valid syntax $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); $registry = $bootstrapper->bootstrap(); $consoleCommands = $registry->attributes->get(ConsoleCommand::class); foreach ($consoleCommands as $discovered) { // Should be able to create attribute instance without errors $command = $discovered->createAttributeInstance(); expect($command)->toBeInstanceOf(ConsoleCommand::class); expect($command->name)->not->toBeEmpty(); expect($command->description)->not->toBeEmpty(); // Command name should follow expected format expect($command->name)->toMatch('/^[a-z]+:[a-z\-]+$/'); } }); it('completes full console session within reasonable time', function () { $startTime = microtime(true); // Complete flow: discovery -> application -> command execution $app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output); // Execute a command $argv = ['test-console', 'demo:hello', 'PerformanceTest']; $exitCode = $app->run($argv); $endTime = microtime(true); $duration = $endTime - $startTime; // Should complete within reasonable time (20 seconds for CI) expect($duration)->toBeLessThan(20.0); // Should still succeed expect($exitCode)->toBe(ExitCode::SUCCESS->value); $output = implode(' ', $this->output->capturedLines); expect($output)->toContain('Hallo, PerformanceTest!'); }); it('maintains memory usage within bounds during discovery and execution', function () { $initialMemory = memory_get_usage(true); // Perform complete console flow $app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output); $argv = ['test-console', 'demo:colors']; $exitCode = $app->run($argv); $finalMemory = memory_get_usage(true); $memoryIncrease = $finalMemory - $initialMemory; // Memory increase should be reasonable (less than 100MB) expect($memoryIncrease)->toBeLessThan(100 * 1024 * 1024); expect($exitCode)->toBe(ExitCode::SUCCESS->value); }); });