log->append($this->name); } }; } public function testPipelineRunsStagesInOrder(): void { $log = new ArrayObject(); (new BootPipeline([ $this->recordingStage('a', $log), $this->recordingStage('b', $log), $this->recordingStage('c', $log), ]))->run($this->state()); $this->assertSame(['a', 'b', 'c'], $log->getArrayCopy()); } public function testPipelineAbortsOnThrow(): void { $log = new ArrayObject(); $boom = new class implements BootStageInterface { public function run(BootState $state): void { throw new RuntimeException('boom'); } }; $this->expectException(RuntimeException::class); try { (new BootPipeline([$boom, $this->recordingStage('after', $log)]))->run($this->state()); } finally { $this->assertNotContains('after', $log->getArrayCopy(), 'stage after a throwing stage must not run'); } } /** * @param array $options * @return list */ private function classesFor(BootContext $ctx, array $options = []): array { $opts = array_merge(BootKernel::defaults($ctx), $options); return array_map('get_class', StageProfiles::for($ctx, $opts)); } public function testMinimalProfile(): void { $this->assertSame([ ConstantsStage::class, ConfigStage::class, FloodProtectionStage::class, HostVerificationStage::class, ContainerPopulateStage::class, ], $this->classesFor(BootContext::Minimal)); } public function testCliProfileDefaultsOmitRedisAndProcessTitle(): void { $this->assertSame([ ConstantsStage::class, ConfigStage::class, FloodProtectionStage::class, HostVerificationStage::class, DatabaseStage::class, LegacyCoreStage::class, ContainerPopulateStage::class, HealthCheckStage::class, ], $this->classesFor(BootContext::Cli)); } public function testCliProfileAddsRedisAndProcessTitleWhenRequested(): void { $classes = $this->classesFor(BootContext::Cli, ['redis' => true, 'process' => 'worker']); $this->assertContains(RedisStage::class, $classes); $this->assertContains(ProcessTitleStage::class, $classes); } public function testStreamProfileIsDatabaseOnly(): void { $this->assertSame([ ConstantsStage::class, ConfigStage::class, FloodProtectionStage::class, HostVerificationStage::class, DatabaseStage::class, ContainerPopulateStage::class, HealthCheckStage::class, ], $this->classesFor(BootContext::Stream)); } public function testAdminProfileFullSequence(): void { $this->assertSame([ ConstantsStage::class, ConfigStage::class, FloodProtectionStage::class, HostVerificationStage::class, AttributionVerificationStage::class, SessionStage::class, DatabaseStage::class, LegacyCoreStage::class, RedisStage::class, AdminApiStage::class, TranslatorStage::class, AdminShutdownStage::class, StatusConstantsStage::class, AdminGlobalsStage::class, ContainerPopulateStage::class, HealthCheckStage::class, ], $this->classesFor(BootContext::Admin)); } public function testMinimalHasNoHealthCheck(): void { $this->assertNotContains(HealthCheckStage::class, $this->classesFor(BootContext::Minimal)); } /** WebApi has no kernel profile — it boots via WebApiBootstrap, so misuse must fail loudly. */ public function testWebApiContextHasNoKernelProfile(): void { $this->expectException(\LogicException::class); StageProfiles::for(BootContext::WebApi, []); } public function testDefaultsPerContext(): void { $this->assertTrue(BootKernel::defaults(BootContext::Admin)['redis']); $this->assertFalse(BootKernel::defaults(BootContext::Cli)['redis']); $this->assertTrue(BootKernel::defaults(BootContext::Stream)['cached']); $this->assertFalse(BootKernel::defaults(BootContext::Admin)['cached']); } }