fix(console): comprehensive TUI rendering fixes

- Fix Enter key detection: handle multiple Enter key formats (\n, \r, \r\n)
- Reduce flickering: lower render frequency from 60 FPS to 30 FPS
- Fix menu bar visibility: re-render menu bar after content to prevent overwriting
- Fix content positioning: explicit line positioning for categories and commands
- Fix line shifting: clear lines before writing, control newlines manually
- Limit visible items: prevent overflow with maxVisibleCategories/Commands
- Improve CPU usage: increase sleep interval when no events processed

This fixes:
- Enter key not working for selection
- Strong flickering of the application
- Menu bar not visible or being overwritten
- Top half of selection list not displayed
- Lines being shifted/misaligned
This commit is contained in:
2025-11-10 11:06:07 +01:00
parent 6bc78f5540
commit 8f3c15ddbb
106 changed files with 9082 additions and 4483 deletions

View File

@@ -0,0 +1,853 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Discovery;
use Mockery;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CachePrefix;
use App\Framework\Cache\CacheResult;
use App\Framework\Cache\Driver\InMemoryCache;
use App\Framework\Cache\GeneralCache;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\SystemClock;
use App\Framework\Discovery\Events\CacheCompressionEvent;
use App\Framework\Discovery\Events\CacheHitEvent;
use App\Framework\Discovery\Events\CacheMissEvent;
use App\Framework\Discovery\Memory\DiscoveryMemoryManager;
use App\Framework\Discovery\Memory\MemoryStatus;
use App\Framework\Discovery\Memory\MemoryStatusInfo;
use App\Framework\Discovery\Results\AttributeRegistry;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\Results\InterfaceRegistry;
use App\Framework\Discovery\Results\TemplateRegistry;
use App\Framework\Discovery\Storage\DiscoveryCacheManager;
use App\Framework\Discovery\ValueObjects\CacheLevel;
use App\Framework\Discovery\ValueObjects\CacheTier;
use App\Framework\Discovery\ValueObjects\CompressionLevel;
use App\Framework\Discovery\ValueObjects\DiscoveryContext;
use App\Framework\Discovery\ValueObjects\DiscoveryOptions;
use App\Framework\Discovery\ValueObjects\MemoryStrategy;
use App\Framework\Discovery\ValueObjects\ScanType;
use App\Framework\Filesystem\FileSystemService;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Filesystem\ValueObjects\FileMetadata;
use App\Framework\Serializer\Php\PhpSerializer;
use App\Framework\Serializer\Php\PhpSerializerConfig;
describe('DiscoveryCacheManager - Basic Operations', 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 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();
});
});