- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
315 lines
11 KiB
PHP
315 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\LiveComponents\Cache\ComponentStateCache;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentState;
|
|
use App\Framework\StateManagement\InMemoryStateManager;
|
|
use App\Framework\StateManagement\StateManagerStatistics;
|
|
|
|
describe('ComponentStateCache with StateManager', function () {
|
|
beforeEach(function () {
|
|
// Create StateManager for ComponentState
|
|
$this->stateManager = InMemoryStateManager::for(ComponentState::class);
|
|
$this->cache = new ComponentStateCache($this->stateManager);
|
|
$this->componentId = ComponentId::create('counter', 'instance-1');
|
|
});
|
|
|
|
it('stores component state', function () {
|
|
$state = ComponentState::fromArray([
|
|
'count' => 5,
|
|
'lastUpdated' => time()
|
|
]);
|
|
|
|
$this->cache->store($this->componentId, $state);
|
|
|
|
expect($this->cache->has($this->componentId))->toBeTrue();
|
|
});
|
|
|
|
it('retrieves stored component state', function () {
|
|
$state = ComponentState::fromArray([
|
|
'count' => 10,
|
|
'label' => 'Counter'
|
|
]);
|
|
|
|
$this->cache->store($this->componentId, $state);
|
|
|
|
$retrieved = $this->cache->retrieve($this->componentId);
|
|
|
|
expect($retrieved)->toBeInstanceOf(ComponentState::class);
|
|
expect($retrieved->toArray()['count'])->toBe(10);
|
|
expect($retrieved->toArray()['label'])->toBe('Counter');
|
|
});
|
|
|
|
it('returns null for non-existent state', function () {
|
|
$nonExistentId = ComponentId::create('counter', 'does-not-exist');
|
|
|
|
$retrieved = $this->cache->retrieve($nonExistentId);
|
|
|
|
expect($retrieved)->toBeNull();
|
|
});
|
|
|
|
it('checks state existence', function () {
|
|
$state = ComponentState::fromArray(['value' => 42]);
|
|
|
|
expect($this->cache->has($this->componentId))->toBeFalse();
|
|
|
|
$this->cache->store($this->componentId, $state);
|
|
|
|
expect($this->cache->has($this->componentId))->toBeTrue();
|
|
});
|
|
|
|
it('invalidates cached state', function () {
|
|
$state = ComponentState::fromArray(['value' => 100]);
|
|
|
|
$this->cache->store($this->componentId, $state);
|
|
expect($this->cache->has($this->componentId))->toBeTrue();
|
|
|
|
$this->cache->invalidate($this->componentId);
|
|
|
|
expect($this->cache->has($this->componentId))->toBeFalse();
|
|
expect($this->cache->retrieve($this->componentId))->toBeNull();
|
|
});
|
|
|
|
it('clears all cached states', function () {
|
|
$state1 = ComponentState::fromArray(['value' => 1]);
|
|
$state2 = ComponentState::fromArray(['value' => 2]);
|
|
|
|
$id1 = ComponentId::create('counter', 'instance-1');
|
|
$id2 = ComponentId::create('counter', 'instance-2');
|
|
|
|
$this->cache->store($id1, $state1);
|
|
$this->cache->store($id2, $state2);
|
|
|
|
$this->cache->clear();
|
|
|
|
expect($this->cache->has($id1))->toBeFalse();
|
|
expect($this->cache->has($id2))->toBeFalse();
|
|
});
|
|
|
|
it('atomically updates component state', function () {
|
|
$initialState = ComponentState::fromArray([
|
|
'count' => 5,
|
|
'version' => 1
|
|
]);
|
|
|
|
$this->cache->store($this->componentId, $initialState);
|
|
|
|
$updatedState = $this->cache->update(
|
|
$this->componentId,
|
|
function ($state) {
|
|
if ($state === null) {
|
|
return ComponentState::fromArray(['count' => 1, 'version' => 1]);
|
|
}
|
|
|
|
$data = $state->toArray();
|
|
return ComponentState::fromArray([
|
|
'count' => $data['count'] + 1,
|
|
'version' => $data['version'] + 1
|
|
]);
|
|
}
|
|
);
|
|
|
|
expect($updatedState->toArray()['count'])->toBe(6);
|
|
expect($updatedState->toArray()['version'])->toBe(2);
|
|
});
|
|
|
|
it('handles null state in atomic update', function () {
|
|
$newState = $this->cache->update(
|
|
$this->componentId,
|
|
function ($state) {
|
|
return $state ?? ComponentState::fromArray([
|
|
'count' => 1,
|
|
'initialized' => true
|
|
]);
|
|
}
|
|
);
|
|
|
|
expect($newState->toArray()['count'])->toBe(1);
|
|
expect($newState->toArray()['initialized'])->toBe(true);
|
|
});
|
|
|
|
it('respects custom TTL', function () {
|
|
$state = ComponentState::fromArray(['value' => 42]);
|
|
$customTtl = Duration::fromMinutes(30);
|
|
|
|
$this->cache->store($this->componentId, $state, $customTtl);
|
|
|
|
expect($this->cache->has($this->componentId))->toBeTrue();
|
|
});
|
|
|
|
it('uses default TTL when not specified', function () {
|
|
$state = ComponentState::fromArray(['value' => 100]);
|
|
|
|
$this->cache->store($this->componentId, $state);
|
|
|
|
expect($this->cache->has($this->componentId))->toBeTrue();
|
|
});
|
|
|
|
it('provides cache statistics', function () {
|
|
$state = ComponentState::fromArray(['value' => 1]);
|
|
|
|
$this->cache->store($this->componentId, $state);
|
|
$this->cache->retrieve($this->componentId);
|
|
$this->cache->has($this->componentId);
|
|
|
|
$stats = $this->cache->getStats();
|
|
|
|
expect($stats)->toBeInstanceOf(StateManagerStatistics::class);
|
|
expect($stats->totalKeys)->toBeGreaterThanOrEqual(1);
|
|
expect($stats->setCount)->toBeGreaterThanOrEqual(1);
|
|
expect($stats->hitCount)->toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('stores with automatic TTL based on component type', function () {
|
|
$state = ComponentState::fromArray(['value' => 42]);
|
|
|
|
$this->cache->storeWithAutoTTL($this->componentId, $state, 'counter');
|
|
|
|
expect($this->cache->has($this->componentId))->toBeTrue();
|
|
});
|
|
|
|
it('applies different TTLs for different component types', function () {
|
|
$timerState = ComponentState::fromArray(['seconds' => 30]);
|
|
$chartState = ComponentState::fromArray(['data' => [1, 2, 3]]);
|
|
$cardState = ComponentState::fromArray(['title' => 'Card Title']);
|
|
|
|
$timerId = ComponentId::create('timer', 'instance-1');
|
|
$chartId = ComponentId::create('chart', 'instance-1');
|
|
$cardId = ComponentId::create('card', 'instance-1');
|
|
|
|
// timer: 5 minutes TTL
|
|
$this->cache->storeWithAutoTTL($timerId, $timerState, 'timer');
|
|
|
|
// chart: 30 minutes TTL
|
|
$this->cache->storeWithAutoTTL($chartId, $chartState, 'chart');
|
|
|
|
// card: 2 hours TTL
|
|
$this->cache->storeWithAutoTTL($cardId, $cardState, 'card');
|
|
|
|
expect($this->cache->has($timerId))->toBeTrue();
|
|
expect($this->cache->has($chartId))->toBeTrue();
|
|
expect($this->cache->has($cardId))->toBeTrue();
|
|
});
|
|
|
|
it('uses default TTL for unknown component types', function () {
|
|
$state = ComponentState::fromArray(['value' => 123]);
|
|
$unknownId = ComponentId::create('unknown-type', 'instance-1');
|
|
|
|
$this->cache->storeWithAutoTTL($unknownId, $state, 'unknown-type');
|
|
|
|
expect($this->cache->has($unknownId))->toBeTrue();
|
|
});
|
|
|
|
it('generates consistent cache keys for same component', function () {
|
|
$state = ComponentState::fromArray(['value' => 42]);
|
|
|
|
$id1 = ComponentId::create('counter', 'instance-1');
|
|
$id2 = ComponentId::create('counter', 'instance-1');
|
|
|
|
$this->cache->store($id1, $state);
|
|
|
|
// Same component ID should find the cached state
|
|
expect($this->cache->has($id2))->toBeTrue();
|
|
expect($this->cache->retrieve($id2))->not->toBeNull();
|
|
});
|
|
|
|
it('generates different cache keys for different instances', function () {
|
|
$state1 = ComponentState::fromArray(['value' => 1]);
|
|
$state2 = ComponentState::fromArray(['value' => 2]);
|
|
|
|
$id1 = ComponentId::create('counter', 'instance-1');
|
|
$id2 = ComponentId::create('counter', 'instance-2');
|
|
|
|
$this->cache->store($id1, $state1);
|
|
$this->cache->store($id2, $state2);
|
|
|
|
$retrieved1 = $this->cache->retrieve($id1);
|
|
$retrieved2 = $this->cache->retrieve($id2);
|
|
|
|
expect($retrieved1->toArray()['value'])->toBe(1);
|
|
expect($retrieved2->toArray()['value'])->toBe(2);
|
|
});
|
|
|
|
it('handles complex state data', function () {
|
|
$complexState = ComponentState::fromArray([
|
|
'user' => [
|
|
'id' => 123,
|
|
'name' => 'John Doe',
|
|
'email' => 'john@example.com'
|
|
],
|
|
'preferences' => [
|
|
'theme' => 'dark',
|
|
'notifications' => true
|
|
],
|
|
'metadata' => [
|
|
'created_at' => time(),
|
|
'updated_at' => time()
|
|
]
|
|
]);
|
|
|
|
$this->cache->store($this->componentId, $complexState);
|
|
|
|
$retrieved = $this->cache->retrieve($this->componentId);
|
|
|
|
expect($retrieved->toArray()['user']['name'])->toBe('John Doe');
|
|
expect($retrieved->toArray()['preferences']['theme'])->toBe('dark');
|
|
expect($retrieved->toArray()['metadata'])->toHaveKey('created_at');
|
|
});
|
|
|
|
it('preserves state through multiple updates', function () {
|
|
$initialState = ComponentState::fromArray(['count' => 0]);
|
|
|
|
$this->cache->store($this->componentId, $initialState);
|
|
|
|
// Update 1
|
|
$this->cache->update(
|
|
$this->componentId,
|
|
fn($state) => ComponentState::fromArray(['count' => $state->toArray()['count'] + 1])
|
|
);
|
|
|
|
// Update 2
|
|
$this->cache->update(
|
|
$this->componentId,
|
|
fn($state) => ComponentState::fromArray(['count' => $state->toArray()['count'] + 1])
|
|
);
|
|
|
|
// Update 3
|
|
$finalState = $this->cache->update(
|
|
$this->componentId,
|
|
fn($state) => ComponentState::fromArray(['count' => $state->toArray()['count'] + 1])
|
|
);
|
|
|
|
expect($finalState->toArray()['count'])->toBe(3);
|
|
});
|
|
|
|
it('correctly reports statistics after various operations', function () {
|
|
$state = ComponentState::fromArray(['value' => 1]);
|
|
|
|
// Set operations
|
|
$this->cache->store($this->componentId, $state);
|
|
$this->cache->store($this->componentId, $state);
|
|
|
|
// Get operations
|
|
$this->cache->retrieve($this->componentId);
|
|
$this->cache->retrieve($this->componentId);
|
|
|
|
// Has operations
|
|
$this->cache->has($this->componentId);
|
|
|
|
// Update operation
|
|
$this->cache->update(
|
|
$this->componentId,
|
|
fn($s) => ComponentState::fromArray(['value' => 2])
|
|
);
|
|
|
|
$stats = $this->cache->getStats();
|
|
|
|
expect($stats->setCount)->toBeGreaterThanOrEqual(2);
|
|
expect($stats->hitCount)->toBeGreaterThanOrEqual(2);
|
|
expect($stats->updateCount)->toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|