container = ServiceContainer::getInstance(); // Store a fresh EventDispatcher instance in the container and wire // it as the static singleton so both DI callers and static callers // share the same listener store within this test. $dispatcher = new EventDispatcher(); EventDispatcher::setInstance($dispatcher); $this->container->set('events', $dispatcher); } protected function tearDown(): void { ServiceContainer::resetInstance(); Router::resetInstance(); NavbarRegistry::reset(); EventDispatcher::resetInstance(); TestBootCallTracker::reset(); } // ── bootAll() lifecycle ─────────────────────────────────────────────── public function testBootAllCallsBootOnModule(): void { $root = $this->createRoot(); $this->createModule($root, 'boot-alpha', boot: "\\TestBootCallTracker::record('boot:boot-alpha');"); $loader = new ModuleLoader(); $loader->loadAll($root); $loader->bootAll($this->container); $this->assertTrue(TestBootCallTracker::has('boot:boot-alpha')); } public function testBootAllCallsRegisterNavbarOnModule(): void { $root = $this->createRoot(); $this->createModule($root, 'navbar-beta', navbar: "\\TestBootCallTracker::record('navbar:navbar-beta');"); $loader = new ModuleLoader(); $loader->loadAll($root); $loader->bootAll($this->container); $this->assertTrue(TestBootCallTracker::has('navbar:navbar-beta')); } public function testBootAllCallsRegisterRoutesWhenRouterProvided(): void { $root = $this->createRoot(); $this->createModule($root, 'routes-gamma', routes: "\\TestBootCallTracker::record('routes:routes-gamma');"); $loader = new ModuleLoader(); $loader->loadAll($root); $loader->bootAll($this->container, Router::getInstance()); $this->assertTrue(TestBootCallTracker::has('routes:routes-gamma')); } public function testBootAllSkipsRoutesWhenRouterIsNull(): void { $root = $this->createRoot(); $this->createModule($root, 'noroutes-delta', routes: "\\TestBootCallTracker::record('routes:noroutes-delta');"); $loader = new ModuleLoader(); $loader->loadAll($root); $loader->bootAll($this->container, null); $this->assertFalse(TestBootCallTracker::has('routes:noroutes-delta')); } public function testBootAllRegistersEventSubscribers(): void { $root = $this->createRoot(); $this->createModule($root, 'events-epsilon', subscribers: "'test.event.epsilon' => function() {}"); $loader = new ModuleLoader(); $loader->loadAll($root); $loader->bootAll($this->container); $this->assertTrue(EventDispatcher::hasListeners('test.event.epsilon')); } public function testBootAllWithMultipleModulesCallsAllBoots(): void { $root = $this->createRoot(); $this->createModule($root, 'multi-one', boot: "\\TestBootCallTracker::record('boot:multi-one');"); $this->createModule($root, 'multi-two', boot: "\\TestBootCallTracker::record('boot:multi-two');"); $loader = new ModuleLoader(); $loader->loadAll($root); $loader->bootAll($this->container); $this->assertTrue(TestBootCallTracker::has('boot:multi-one')); $this->assertTrue(TestBootCallTracker::has('boot:multi-two')); } // ── BaseModule subclass integration ─────────────────────────────────── public function testModuleExtendingBaseModuleBootsCorrectly(): void { $root = $this->createRoot(); $this->createBaseModuleSubclass($root, 'base-eta'); $loader = new ModuleLoader(); $loader->loadAll($root); $loader->bootAll($this->container); $this->assertTrue(TestBootCallTracker::has('boot:base-eta')); } // ── getModule() / getManifest() / isLoaded() ───────────────────────── public function testGetModuleReturnsInstanceAfterLoad(): void { $root = $this->createRoot(); $this->createModule($root, 'get-zeta'); $loader = new ModuleLoader(); $loader->loadAll($root); $module = $loader->getModule('get-zeta'); $this->assertInstanceOf(ModuleInterface::class, $module); $this->assertSame('get-zeta', $module->getName()); } public function testGetModuleReturnsNullForUnknown(): void { $loader = new ModuleLoader(); $loader->loadAll($this->createRoot()); $this->assertNull($loader->getModule('no-such-module')); } public function testIsLoadedReturnsFalseForUnknown(): void { $loader = new ModuleLoader(); $loader->loadAll($this->createRoot()); $this->assertFalse($loader->isLoaded('no-such-module')); } public function testGetManifestReturnsModuleMetadata(): void { $root = $this->createRoot(); $this->createModule($root, 'manifest-theta'); $loader = new ModuleLoader(); $loader->loadAll($root); $manifest = $loader->getManifest('manifest-theta'); $this->assertIsArray($manifest); $this->assertSame('manifest-theta', $manifest['name']); $this->assertSame('1.0.0', $manifest['version']); } // ── Helpers ─────────────────────────────────────────────────────────── private function createRoot(): string { $path = sys_get_temp_dir() . '/xc_vm_boot_' . bin2hex(random_bytes(6)); mkdir($path, 0775, true); return $path; } private function className(string $name): string { return implode('', array_map('ucfirst', explode('-', $name))) . 'Module'; } private function writeManifest(string $root, string $name): void { $manifest = [ 'name' => $name, 'description' => 'boot test module', 'version' => '1.0.0', 'requires_core' => '>=2.0', 'environment' => 'main', 'dependencies' => [], 'has_navbar' => false, 'has_settings' => false, ]; file_put_contents( $root . '/' . $name . '/module.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ); } private function createModule( string $root, string $name, string $boot = '', string $navbar = '', string $routes = '', string $subscribers = '' ): void { $dir = $root . '/' . $name; mkdir($dir, 0775, true); $this->writeManifest($root, $name); $cls = $this->className($name); $ns = $this->namespace($name); $subscriberReturn = $subscribers !== '' ? "return [{$subscribers}];" : 'return [];'; $code = "writeManifest($root, $name); $cls = $this->className($name); $ns = $this->namespace($name); $code = "pascalName($name); } private function pascalName(string $name): string { return implode('', array_map('ucfirst', explode('-', $name))); } }