hasModule('addressbook')); self::assertNotEmpty($registry->getSearchResources()); self::assertNotEmpty($registry->getLayoutContextProviders()); self::assertNotEmpty($registry->getSessionProviders()); } public function testModuleOffModeReturnsEmptyRegistry(): void { $modulesDir = dirname(__DIR__, 2) . '/modules'; // Boot a fresh registry with no modules enabled (simulates APP_ENABLED_MODULES='') $registry = ModuleRegistry::boot($modulesDir, []); self::assertSame([], $registry->getModules()); self::assertSame([], $registry->getRoutes()); self::assertSame([], $registry->getPermissions()); self::assertSame([], $registry->getSearchResources()); self::assertSame([], $registry->getLayoutContextProviders()); self::assertSame([], $registry->getSessionProviders()); self::assertSame([], $registry->getSlotContributions('aside.tab_panel')); } public function testAddressBookModuleManifestIsValid(): void { $modulesDir = dirname(__DIR__, 2) . '/modules'; $manifestFile = $modulesDir . '/addressbook/module.php'; self::assertFileExists($manifestFile, 'Address book module manifest must exist'); $raw = require $manifestFile; self::assertIsArray($raw); self::assertSame('addressbook', $raw['id']); // Permissions owned by Core PermissionService, not module self::assertSame([], $raw['permissions']); // Asset groups owned by config/assets.php, not module self::assertSame([], $raw['asset_groups']); self::assertNotEmpty($raw['search_resources']); self::assertNotEmpty($raw['layout_context_providers']); self::assertNotEmpty($raw['session_providers']); self::assertArrayHasKey('aside.tab_panel', $raw['ui_slots']); } public function testModuleProviderClassesExist(): void { self::assertTrue( class_exists(\MintyPHP\Module\AddressBook\Providers\AddressBookSearchProvider::class), 'AddressBookSearchProvider class must be autoloadable' ); self::assertTrue( class_exists(\MintyPHP\Module\AddressBook\Providers\AddressBookLayoutProvider::class), 'AddressBookLayoutProvider class must be autoloadable' ); self::assertTrue( class_exists(\MintyPHP\Module\AddressBook\Providers\AddressBookSessionProvider::class), 'AddressBookSessionProvider class must be autoloadable' ); } public function testProviderClassesImplementContracts(): void { self::assertInstanceOf( \MintyPHP\App\Module\Contracts\SearchResourceProvider::class, new \MintyPHP\Module\AddressBook\Providers\AddressBookSearchProvider() ); self::assertInstanceOf( \MintyPHP\App\Module\Contracts\LayoutContextProvider::class, new \MintyPHP\Module\AddressBook\Providers\AddressBookLayoutProvider() ); self::assertInstanceOf( \MintyPHP\App\Module\Contracts\SessionProvider::class, new \MintyPHP\Module\AddressBook\Providers\AddressBookSessionProvider() ); } public function testConflictDetectionIsStrict(): void { $fixturesDir = sys_get_temp_dir() . '/module-arch-test-' . uniqid(); mkdir($fixturesDir . '/mod-a', 0777, true); mkdir($fixturesDir . '/mod-b', 0777, true); file_put_contents($fixturesDir . '/mod-a/module.php', ' 'mod-a', 'permissions' => ['shared.perm'], ], true) . ';'); file_put_contents($fixturesDir . '/mod-b/module.php', ' 'mod-b', 'permissions' => ['shared.perm'], ], true) . ';'); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Permission conflict'); try { ModuleRegistry::boot($fixturesDir, ['mod-a', 'mod-b']); } finally { array_map('unlink', glob($fixturesDir . '/mod-a/*') ?: []); array_map('unlink', glob($fixturesDir . '/mod-b/*') ?: []); rmdir($fixturesDir . '/mod-a'); rmdir($fixturesDir . '/mod-b'); rmdir($fixturesDir); } } }