cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); $this->fileSystemService = new FileSystemService(); // Use a real existing path to avoid stale detection issues $basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer'; $testPath = $basePath . '/src'; $this->cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService ); // Use a future time to avoid stale detection issues $futureTime = new \DateTimeImmutable('2099-01-01 00:00:00'); $this->testContext = new DiscoveryContext( paths: [$testPath], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $futureTime ); $this->testRegistry = new DiscoveryRegistry( attributes: new AttributeRegistry(), interfaces: new InterfaceRegistry(), templates: new TemplateRegistry() ); }); it('can be instantiated', function () { expect($this->cacheManager)->toBeInstanceOf(DiscoveryCacheManager::class); }); it('stores discovery results in cache', function () { $success = $this->cacheManager->store($this->testContext, $this->testRegistry); expect($success)->toBeTrue(); // Verify cache contains the data $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); expect($cached->isEmpty())->toBe($this->testRegistry->isEmpty()); }); it('retrieves cached discovery results', function () { // Store first $this->cacheManager->store($this->testContext, $this->testRegistry); // Retrieve $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); expect($cached)->not->toBeNull(); }); it('returns null for cache miss', function () { $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeNull(); }); it('invalidates cache for a context', function () { // Store first $this->cacheManager->store($this->testContext, $this->testRegistry); expect($this->cacheManager->get($this->testContext))->not->toBeNull(); // Invalidate $success = $this->cacheManager->invalidate($this->testContext); expect($success)->toBeTrue(); // Should be null now expect($this->cacheManager->get($this->testContext))->toBeNull(); }); it('clears all discovery caches', function () { // Store multiple contexts $context1 = new DiscoveryContext( paths: ['/test/path1'], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $this->clock->now() ); $context2 = new DiscoveryContext( paths: ['/test/path2'], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $this->clock->now() ); $this->cacheManager->store($context1, $this->testRegistry); $this->cacheManager->store($context2, $this->testRegistry); expect($this->cacheManager->get($context1))->not->toBeNull(); expect($this->cacheManager->get($context2))->not->toBeNull(); // Clear all $success = $this->cacheManager->clearAll(); expect($success)->toBeTrue(); // Both should be null expect($this->cacheManager->get($context1))->toBeNull(); expect($this->cacheManager->get($context2))->toBeNull(); }); it('optimizes registry before caching', function () { $registry = new DiscoveryRegistry( attributes: new AttributeRegistry(), interfaces: new InterfaceRegistry(), templates: new TemplateRegistry() ); // Store should call optimize internally $success = $this->cacheManager->store($this->testContext, $registry); expect($success)->toBeTrue(); }); }); describe('DiscoveryCacheManager - Cache Hit/Miss Scenarios', function () { beforeEach(function () { $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); $this->fileSystemService = new FileSystemService(); // Use a real existing path $basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer'; $testPath = $basePath . '/src'; $this->cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService ); $this->testContext = new DiscoveryContext( paths: ['/test/path'], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $this->clock->now() ); $this->testRegistry = new DiscoveryRegistry( attributes: new AttributeRegistry(), interfaces: new InterfaceRegistry(), templates: new TemplateRegistry() ); }); it('handles cache hit correctly', function () { $this->cacheManager->store($this->testContext, $this->testRegistry); $cached = $this->cacheManager->get($this->testContext); expect($cached)->not->toBeNull(); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); it('handles cache miss when key does not exist', function () { $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeNull(); }); it('handles cache miss when data is corrupted', function () { // Store invalid data directly in cache $key = $this->testContext->getCacheKey(); $invalidItem = CacheItem::forSet($key, 'invalid_data'); $this->cache->set($invalidItem); $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeNull(); }); it('handles cache miss when data type is wrong', function () { // Store wrong type $key = $this->testContext->getCacheKey(); $wrongTypeItem = CacheItem::forSet($key, ['not' => 'a', 'registry' => 'object']); $this->cache->set($wrongTypeItem); $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeNull(); }); }); describe('DiscoveryCacheManager - Stale Cache Detection', function () { beforeEach(function () { $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); // FileSystemService is final, so we need to use a real instance // For stale detection tests, we'll use a real path and manipulate timing $this->fileSystemService = new FileSystemService(); $this->cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService ); $this->testRegistry = new DiscoveryRegistry( attributes: new AttributeRegistry(), interfaces: new InterfaceRegistry(), templates: new TemplateRegistry() ); }); afterEach(function () { Mockery::close(); }); it('considers incremental scans as always stale', function () { $basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer'; $testPath = $basePath . '/src'; $incrementalContext = new DiscoveryContext( paths: [$testPath], scanType: ScanType::INCREMENTAL, options: new DiscoveryOptions(), startTime: $this->clock->now() ); $this->cacheManager->store($incrementalContext, $this->testRegistry); // Incremental scans should always be considered stale $cached = $this->cacheManager->get($incrementalContext); expect($cached)->toBeNull(); }); it('detects stale cache when directory is modified', function () { // Use a real path that exists $basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer'; $testPath = $basePath . '/src'; $startTime = new \DateTimeImmutable('2020-01-01 00:00:00'); $context = new DiscoveryContext( paths: [$testPath], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $startTime ); // Store cache $this->cacheManager->store($context, $this->testRegistry); // The directory will have a modification time after startTime, so it should be stale // This test verifies that stale detection works $cached = $this->cacheManager->get($context); // Should be stale because real directory modification time is after startTime expect($cached)->toBeNull(); }); it('considers cache fresh when directory is not modified', function () { // Use a real path that exists $basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer'; $testPath = $basePath . '/src'; // Use a future time so the directory modification time is before it $futureTime = new \DateTimeImmutable('2099-01-01 00:00:00'); $context = new DiscoveryContext( paths: [$testPath], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $futureTime ); // Store cache $this->cacheManager->store($context, $this->testRegistry); // Should be fresh because directory modification time is before futureTime $cached = $this->cacheManager->get($context); expect($cached)->not->toBeNull(); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); it('handles file system errors gracefully in stale detection', function () { // Use a non-existent path to trigger file system error $context = new DiscoveryContext( paths: ['/nonexistent/path/that/does/not/exist'], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $this->clock->now() ); $this->cacheManager->store($context, $this->testRegistry); // Should assume stale on error (non-existent path throws exception) $cached = $this->cacheManager->get($context); expect($cached)->toBeNull(); }); }); describe('DiscoveryCacheManager - Health Status', function () { beforeEach(function () { $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); $this->fileSystemService = new FileSystemService(); $this->cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService ); }); it('returns health status without memory manager', function () { $health = $this->cacheManager->getHealthStatus(); expect($health)->toBeArray(); expect($health)->toHaveKey('cache_driver'); expect($health)->toHaveKey('ttl_hours'); expect($health)->toHaveKey('prefix'); expect($health)->toHaveKey('memory_aware'); expect($health['memory_aware'])->toBeFalse(); }); it('returns health status with memory manager', function () { $memoryManager = new DiscoveryMemoryManager( strategy: MemoryStrategy::BATCH, memoryLimit: Byte::fromMegabytes(128), memoryPressureThreshold: 0.8, memoryMonitor: null, logger: null, eventDispatcher: null, clock: $this->clock ); $cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService, logger: null, ttlHours: 24, memoryManager: $memoryManager ); $health = $cacheManager->getHealthStatus(); expect($health)->toBeArray(); expect($health['memory_aware'])->toBeTrue(); expect($health)->toHaveKey('memory_management'); expect($health['memory_management'])->toBeArray(); expect($health['memory_management'])->toHaveKey('status'); expect($health['memory_management'])->toHaveKey('current_usage'); expect($health['memory_management'])->toHaveKey('memory_pressure'); expect($health['memory_management'])->toHaveKey('cache_level'); }); }); describe('DiscoveryCacheManager - Memory-Aware Caching', function () { beforeEach(function () { $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); $this->fileSystemService = new FileSystemService(); $this->memoryManager = new DiscoveryMemoryManager( strategy: MemoryStrategy::BATCH, memoryLimit: Byte::fromMegabytes(128), memoryPressureThreshold: 0.8, memoryMonitor: null, logger: null, eventDispatcher: null, clock: $this->clock ); $this->cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService, logger: null, ttlHours: 24, memoryManager: $this->memoryManager ); $this->testContext = new DiscoveryContext( paths: ['/test/path'], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $this->clock->now() ); $this->testRegistry = new DiscoveryRegistry( attributes: new AttributeRegistry(), interfaces: new InterfaceRegistry(), templates: new TemplateRegistry() ); }); it('uses memory-aware storage when memory manager is available', function () { $success = $this->cacheManager->store($this->testContext, $this->testRegistry); expect($success)->toBeTrue(); // Should be retrievable $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); it('determines cache level based on memory pressure', function () { // This is tested indirectly through storage, but we can verify health status $health = $this->cacheManager->getHealthStatus(); expect($health['memory_aware'])->toBeTrue(); expect($health['memory_management'])->toBeArray(); expect($health['memory_management'])->toHaveKey('cache_level'); }); it('performs memory pressure management', function () { $result = $this->cacheManager->performMemoryPressureManagement(); expect($result)->toBeArray(); expect($result)->toHaveKey('actions'); expect($result)->toHaveKey('memory_status'); expect($result)->toHaveKey('cache_level'); }); it('clears cache in critical memory situations', function () { // Store some data $this->cacheManager->store($this->testContext, $this->testRegistry); expect($this->cacheManager->get($this->testContext))->not->toBeNull(); // Create memory manager with very low limit to trigger critical status $lowMemoryManager = new DiscoveryMemoryManager( strategy: MemoryStrategy::BATCH, memoryLimit: Byte::fromBytes(1024), // Very low limit memoryPressureThreshold: 0.8, memoryMonitor: null, logger: null, eventDispatcher: null, clock: $this->clock ); $cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService, logger: null, ttlHours: 24, memoryManager: $lowMemoryManager ); // Store with low memory manager $cacheManager->store($this->testContext, $this->testRegistry); // Force memory usage to trigger critical // Note: This is hard to test directly, but the method exists $result = $cacheManager->performMemoryPressureManagement(); expect($result)->toBeArray(); }); }); describe('DiscoveryCacheManager - Tiered Caching', function () { beforeEach(function () { $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); $this->fileSystemService = new FileSystemService(); $this->memoryManager = new DiscoveryMemoryManager( strategy: MemoryStrategy::BATCH, memoryLimit: Byte::fromMegabytes(128), memoryPressureThreshold: 0.8, memoryMonitor: null, logger: null, eventDispatcher: null, clock: $this->clock ); $this->cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService, logger: null, ttlHours: 24, memoryManager: $this->memoryManager ); $this->testContext = new DiscoveryContext( paths: ['/test/path'], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $this->clock->now() ); $this->testRegistry = new DiscoveryRegistry( attributes: new AttributeRegistry(), interfaces: new InterfaceRegistry(), templates: new TemplateRegistry() ); }); it('uses tiered caching when memory manager is available', function () { $success = $this->cacheManager->store($this->testContext, $this->testRegistry); expect($success)->toBeTrue(); // Should be retrievable from tiered cache $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); it('determines appropriate cache tier based on data size and access frequency', function () { // Store data - tier determination happens internally $success = $this->cacheManager->store($this->testContext, $this->testRegistry); expect($success)->toBeTrue(); // Verify it can be retrieved (tiered cache lookup) $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); it('applies different TTL multipliers per tier', function () { // Store data - TTL adjustment happens internally based on tier $success = $this->cacheManager->store($this->testContext, $this->testRegistry); expect($success)->toBeTrue(); // Verify retrieval works $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); it('tracks access patterns for tier determination', function () { // Store and retrieve multiple times to build access pattern $this->cacheManager->store($this->testContext, $this->testRegistry); // Multiple accesses $this->cacheManager->get($this->testContext); $this->cacheManager->get($this->testContext); $this->cacheManager->get($this->testContext); // Should still work $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); }); describe('DiscoveryCacheManager - Cache Compression', function () { beforeEach(function () { $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); $this->fileSystemService = new FileSystemService(); $this->memoryManager = new DiscoveryMemoryManager( strategy: MemoryStrategy::BATCH, memoryLimit: Byte::fromMegabytes(128), memoryPressureThreshold: 0.8, memoryMonitor: null, logger: null, eventDispatcher: null, clock: $this->clock ); $this->cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService, logger: null, ttlHours: 24, memoryManager: $this->memoryManager ); $this->testContext = new DiscoveryContext( paths: ['/test/path'], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $this->clock->now() ); $this->testRegistry = new DiscoveryRegistry( attributes: new AttributeRegistry(), interfaces: new InterfaceRegistry(), templates: new TemplateRegistry() ); }); it('compresses data for appropriate tiers', function () { // Store data - compression happens internally for appropriate tiers $success = $this->cacheManager->store($this->testContext, $this->testRegistry); expect($success)->toBeTrue(); // Should be retrievable and decompressed $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); it('decompresses data when retrieving from cache', function () { // Store compressed data $this->cacheManager->store($this->testContext, $this->testRegistry); // Retrieve should decompress automatically $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); expect($cached->isEmpty())->toBe($this->testRegistry->isEmpty()); }); it('handles uncompressed data correctly', function () { // Store data that might not need compression (small size, hot tier) $success = $this->cacheManager->store($this->testContext, $this->testRegistry); expect($success)->toBeTrue(); // Should still be retrievable $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); }); describe('DiscoveryCacheManager - Cache Metrics', function () { beforeEach(function () { $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); $this->fileSystemService = new FileSystemService(); $this->memoryManager = new DiscoveryMemoryManager( strategy: MemoryStrategy::BATCH, memoryLimit: Byte::fromMegabytes(128), memoryPressureThreshold: 0.8, memoryMonitor: null, logger: null, eventDispatcher: null, clock: $this->clock ); $this->cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService, logger: null, ttlHours: 24, memoryManager: $this->memoryManager ); $this->testContext = new DiscoveryContext( paths: ['/test/path'], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $this->clock->now() ); $this->testRegistry = new DiscoveryRegistry( attributes: new AttributeRegistry(), interfaces: new InterfaceRegistry(), templates: new TemplateRegistry() ); }); it('returns null metrics when memory manager is not available', function () { $cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService ); $metrics = $cacheManager->getCacheMetrics(); expect($metrics)->toBeNull(); }); it('returns cache metrics when memory manager is available', function () { // Store and retrieve to generate metrics $this->cacheManager->store($this->testContext, $this->testRegistry); $this->cacheManager->get($this->testContext); $metrics = $this->cacheManager->getCacheMetrics(); expect($metrics)->not->toBeNull(); expect($metrics)->toBeInstanceOf(\App\Framework\Discovery\ValueObjects\CacheMetrics::class); }); it('calculates hit rate correctly', function () { // Store data $this->cacheManager->store($this->testContext, $this->testRegistry); // Multiple hits $this->cacheManager->get($this->testContext); $this->cacheManager->get($this->testContext); $metrics = $this->cacheManager->getCacheMetrics(); expect($metrics)->not->toBeNull(); // Hit rate should be calculated (exact value depends on implementation) }); it('tracks total cache size', function () { $this->cacheManager->store($this->testContext, $this->testRegistry); $metrics = $this->cacheManager->getCacheMetrics(); expect($metrics)->not->toBeNull(); expect($metrics->totalSize)->toBeInstanceOf(Byte::class); }); }); describe('DiscoveryCacheManager - Cache Events', function () { beforeEach(function () { $cacheDriver = new InMemoryCache(); $serializer = new PhpSerializer(PhpSerializerConfig::safe()); $this->cache = new GeneralCache($cacheDriver, $serializer); $this->clock = new SystemClock(); $this->fileSystemService = new FileSystemService(); // EventDispatcher is final and requires a Container // We'll use a real instance but can't verify events directly $this->container = new \App\Framework\DI\DefaultContainer(); $this->eventDispatcher = new EventDispatcher($this->container); $this->memoryManager = new DiscoveryMemoryManager( strategy: MemoryStrategy::BATCH, memoryLimit: Byte::fromMegabytes(128), memoryPressureThreshold: 0.8, memoryMonitor: null, logger: null, eventDispatcher: $this->eventDispatcher, clock: $this->clock ); $this->cacheManager = new DiscoveryCacheManager( cache: $this->cache, clock: $this->clock, fileSystemService: $this->fileSystemService, logger: null, ttlHours: 24, memoryManager: $this->memoryManager, eventDispatcher: $this->eventDispatcher ); // Use a future time to avoid stale detection issues $futureTime = new \DateTimeImmutable('2099-01-01 00:00:00'); $this->testContext = new DiscoveryContext( paths: [$testPath], scanType: ScanType::FULL, options: new DiscoveryOptions(), startTime: $futureTime ); $this->testRegistry = new DiscoveryRegistry( attributes: new AttributeRegistry(), interfaces: new InterfaceRegistry(), templates: new TemplateRegistry() ); }); it('dispatches CacheHitEvent on cache hit', function () { // Store first $this->cacheManager->store($this->testContext, $this->testRegistry); // EventDispatcher is final, so we can't verify events directly // But we can verify the cache hit works $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); it('dispatches CacheMissEvent on cache miss', function () { // EventDispatcher is final, so we can't verify events directly // But we can verify the cache miss works $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeNull(); }); it('dispatches CacheCompressionEvent when compressing data', function () { // EventDispatcher is final, so we can't verify events directly // But we can verify compression works by storing and retrieving $success = $this->cacheManager->store($this->testContext, $this->testRegistry); expect($success)->toBeTrue(); $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeInstanceOf(DiscoveryRegistry::class); }); it('handles multiple miss scenarios for different reasons', function () { // Not found scenario $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeNull(); // Corrupted data scenario $key = $this->testContext->getCacheKey(); $invalidItem = CacheItem::forSet($key, 'invalid'); $this->cache->set($invalidItem); $cached = $this->cacheManager->get($this->testContext); expect($cached)->toBeNull(); }); });