container = new DefaultContainer(); $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); // Use correct base path for Docker environment $basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer'; $this->pathProvider = new PathProvider($basePath); // Register dependencies in container $this->container->singleton(Cache::class, $this->cache); $this->container->singleton(Clock::class, $this->clock); $this->container->singleton(PathProvider::class, $this->pathProvider); // Clear cache between tests $this->cache->clear(); // Clear any opcache if (function_exists('opcache_reset')) { opcache_reset(); } }); it('properly handles different execution contexts in discovery', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); // Test console context discovery $consoleContext = ExecutionContext::forConsole(); $paths = [$this->pathProvider->getSourcePath()]; // Perform discovery $registry = $bootstrapper->performBootstrap($this->pathProvider, $this->cache, null); expect($registry)->toBeInstanceOf(DiscoveryRegistry::class); expect($registry->isEmpty())->toBeFalse(); // Should have discovered ConsoleCommand attributes $consoleCommands = $registry->attributes->get(ConsoleCommand::class); expect($consoleCommands)->not->toBeEmpty(); }); it('finds the correct number of console commands after fresh discovery', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); // Perform fresh discovery $registry = $bootstrapper->performBootstrap($this->pathProvider, $this->cache, null); // Get console commands $consoleCommands = $registry->attributes->get(ConsoleCommand::class); // Should find a reasonable number of commands (at least the demo commands) expect(count($consoleCommands))->toBeGreaterThanOrEqual(6); // Demo commands expect(count($consoleCommands))->toBeLessThan(100); // Reasonable upper bound // Should include demo commands $commandNames = []; foreach ($consoleCommands as $discovered) { $command = $discovered->createAttributeInstance(); $commandNames[] = $command->name; } expect($commandNames)->toContain('demo:hello'); expect($commandNames)->toContain('demo:colors'); expect($commandNames)->toContain('demo:interactive'); }); it('caches discovery results properly', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->cache); // First discovery should cache results $registry1 = $bootstrapper->bootstrap(); // Check that cache contains discovery results $paths = [$this->pathProvider->getSourcePath()]; $context = ExecutionContext::detect(); $cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, $context->getType()->value); $cachedItem = $this->cache->get($cacheKey); expect($cachedItem->isHit)->toBeTrue(); // Second discovery should use cache $registry2 = $bootstrapper->bootstrap(); // Results should be equivalent expect($registry1->isEmpty())->toBe($registry2->isEmpty()); $commands1 = $registry1->attributes->get(ConsoleCommand::class); $commands2 = $registry2->attributes->get(ConsoleCommand::class); expect(count($commands1))->toBe(count($commands2)); }); it('handles cache misses gracefully with fallback discovery', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); // Ensure cache is empty $this->cache->clear(); // Bootstrap should perform fresh discovery when cache is empty $registry = $bootstrapper->bootstrap(); expect($registry)->toBeInstanceOf(DiscoveryRegistry::class); expect($registry->isEmpty())->toBeFalse(); // Should have found console commands even without cache $consoleCommands = $registry->attributes->get(ConsoleCommand::class); expect($consoleCommands)->not->toBeEmpty(); }); it('separates cache by execution context', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); $paths = [$this->pathProvider->getSourcePath()]; // Create cache keys for different contexts $consoleKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, 'console'); $cliScriptKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, 'cli-script'); // Keys should be different expect($consoleKey->toString())->not->toBe($cliScriptKey->toString()); // Cache data for console context $consoleRegistry = $bootstrapper->performBootstrap($this->pathProvider, $this->cache, null); // Check that only console cache is populated $consoleCached = $this->cache->get($consoleKey); $cliScriptCached = $this->cache->get($cliScriptKey); expect($consoleCached->isHit)->toBeTrue(); expect($cliScriptCached->isHit)->toBeFalse(); }); it('handles invalid cached data gracefully', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); $paths = [$this->pathProvider->getSourcePath()]; $context = ExecutionContext::detect(); // Put invalid data in cache $cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, $context->getType()->value); $invalidItem = CacheItem::forSetting($cacheKey, 'invalid_data'); $this->cache->set($invalidItem); // Bootstrap should handle invalid cache gracefully and perform fresh discovery $registry = $bootstrapper->bootstrap(); expect($registry)->toBeInstanceOf(DiscoveryRegistry::class); expect($registry->isEmpty())->toBeFalse(); }); it('processes initializers after discovery', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); // Bootstrap should process initializers $registry = $bootstrapper->bootstrap(); // Registry should be registered in container expect($this->container->has(DiscoveryRegistry::class))->toBeTrue(); $containerRegistry = $this->container->get(DiscoveryRegistry::class); expect($containerRegistry)->toBe($registry); }); it('detects when discovery is required', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); // Initially should require discovery expect($bootstrapper->isDiscoveryRequired())->toBeTrue(); // After bootstrap should still work $bootstrapper->bootstrap(); // The method should still be callable $isRequired = $bootstrapper->isDiscoveryRequired(); expect($isRequired)->toBeOfType('boolean'); }); it('performs incremental discovery when service is available', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); // First bootstrap $registry1 = $bootstrapper->bootstrap(); // Incremental bootstrap should work $registry2 = $bootstrapper->incrementalBootstrap(); expect($registry2)->toBeInstanceOf(DiscoveryRegistry::class); // Should have registered new registry in container expect($this->container->has(DiscoveryRegistry::class))->toBeTrue(); }); it('falls back to full bootstrap when incremental is not possible', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); // Incremental without previous bootstrap should fall back to full bootstrap $registry = $bootstrapper->incrementalBootstrap(); expect($registry)->toBeInstanceOf(DiscoveryRegistry::class); expect($registry->isEmpty())->toBeFalse(); }); }); describe('DiscoveryService Performance and Error Handling', function () { beforeEach(function () { $this->container = new DefaultContainer(); $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); // Use correct base path for Docker environment $basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer'; $this->pathProvider = new PathProvider($basePath); $this->container->singleton(Cache::class, $this->cache); $this->container->singleton(Clock::class, $this->clock); $this->container->singleton(PathProvider::class, $this->pathProvider); }); it('completes discovery within reasonable time', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); $startTime = microtime(true); $registry = $bootstrapper->bootstrap(); $endTime = microtime(true); $duration = $endTime - $startTime; // Discovery should complete within 30 seconds (generous for CI) expect($duration)->toBeLessThan(30.0); // Should still produce valid results expect($registry)->toBeInstanceOf(DiscoveryRegistry::class); expect($registry->isEmpty())->toBeFalse(); }); it('handles discovery errors gracefully', function () { // Create bootstrapper with invalid path $invalidPathProvider = new PathProvider('/nonexistent/path'); $this->container->instance(PathProvider::class, $invalidPathProvider); $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); // Should not throw exception, but may produce empty or partial results $registry = $bootstrapper->bootstrap(); expect($registry)->toBeInstanceOf(DiscoveryRegistry::class); // May be empty due to invalid path, but should not crash }); it('maintains memory usage within reasonable bounds', function () { $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock); $initialMemory = memory_get_usage(true); $registry = $bootstrapper->bootstrap(); $finalMemory = memory_get_usage(true); $memoryIncrease = $finalMemory - $initialMemory; // Memory increase should be reasonable (less than 50MB) expect($memoryIncrease)->toBeLessThan(50 * 1024 * 1024); // Should still produce valid results expect($registry)->toBeInstanceOf(DiscoveryRegistry::class); }); });