Files
XC_VM/tests/Unit/ModuleLoaderPriorityTest.php
T
Divarion-D 4a0501324c feat(modules): evolve module system to extensible platform architecture
TASK-000: Remove legacy core patching system
- Delete src/core/Module/CoreCodePatcher.php
- Delete src/core/Module/CoreCodePatchableModuleInterface.php

TASK-001: Split ModuleInterface into specialized provider interfaces
- Add ServiceProviderInterface (boot, getEventSubscribers)
- Add RouteProviderInterface (registerRoutes)
- Add CommandProviderInterface (registerCommands)
- Add NavbarProviderInterface (registerNavbar)
- Add StreamMiddlewareProviderInterface
- ModuleLoader::bootAll() detects supported providers via instanceof

TASK-002: Refactor EventDispatcher
- Add ListenerProvider with priority support
- Add AbstractEvent base class
- Add StoppableEventInterface support
- Preserve legacy subscribe/publish API compatibility

TASK-003: Extend ServiceContainer
- Add ServiceContainer::decorate()
- Support priority-based decorator chains
- Add protected service registry:
  - db
  - settings
  - config
  - auth

TASK-004: Add core event catalog
- ModuleLoadedEvent
- ModuleBootedEvent
- PackageInstalledEvent
- UserAuthenticatedEvent
- UserLoggedOutEvent
- StreamStartingEvent
- StreamStartedEvent
- StreamStoppedEvent
- SettingsChangedEvent

TASK-005: Formalize subsystem boundaries
- Add BoundaryInterface
- Add MinistraBootstrap implementation
- Define exported service contracts

TASK-006: Add stream middleware pipeline
- Add StreamContext
- Add StreamMiddlewareInterface
- Add StreamPipeline
- Support middleware priority ordering

TASK-007: Add module hot reload
- Dispatch PackageInstalledEvent after installation
- Add ModuleManager::hotReload()
- Load and boot newly installed modules without service restart

TASK-008: Standardize container contracts
- Add container interfaces:
  - ContainerInterface
  - ContainerExceptionInterface
  - NotFoundExceptionInterface
- Add NotFoundException implementation
- ServiceContainer::get() throws NotFoundException

TASK-009: Refactor navbar registration
- CoreNavbarProvider implements NavbarProviderInterface
- Add registerNavbar() instance method
- ModuleLoader uses provider-based registration

TASK-010: Add module priority support
- Add priority field to module.json
- Default value: 0
- Higher priority modules load earlier
- Resolve load order by:
  1. Priority DESC
  2. Module name ASC

TASK-011: Add optional dependencies
- Add optional_dependencies field to module.json
- Missing optional dependencies do not block module loading
- Process optional dependencies after required dependencies

Tests
- Add ServiceContainer tests
- Add EventDispatcher tests
- Add StreamPipeline tests
- Add ModuleLoader priority tests

Documentation
- Rewrite module architecture documentation
- Update examples and extension development guides
- Document lifecycle, events, services, middleware, and dependencies
2026-06-14 14:25:19 +03:00

146 lines
5.6 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
/**
* Tests for features added in roadmap TASK-010 (priority) and TASK-011 (optional_dependencies).
*/
final class ModuleLoaderPriorityTest extends TestCase {
// ── Priority system (TASK-010) ─────────────────────────────
public function testHigherPriorityModuleLoadsFirst(): void {
$root = $this->createModulesRoot();
$this->createModule($root, 'low-prio', [], ['priority' => 10]);
$this->createModule($root, 'high-prio', [], ['priority' => 50]);
$this->createModule($root, 'mid-prio', [], ['priority' => 30]);
$loader = new ModuleLoader();
$loader->loadAll($root);
$this->assertSame(
['high-prio', 'mid-prio', 'low-prio'],
array_keys($loader->getModules())
);
}
public function testEqualPriorityModulesLoadAlphabetically(): void {
$root = $this->createModulesRoot();
$this->createModule($root, 'zebra', [], ['priority' => 0]);
$this->createModule($root, 'apple', [], ['priority' => 0]);
$this->createModule($root, 'mango', [], ['priority' => 0]);
$loader = new ModuleLoader();
$loader->loadAll($root);
$this->assertSame(
['apple', 'mango', 'zebra'],
array_keys($loader->getModules())
);
}
public function testDefaultPriorityIsZero(): void {
$root = $this->createModulesRoot();
// No priority key in manifest at all
$this->createModule($root, 'no-priority');
$loader = new ModuleLoader();
$loader->loadAll($root);
$this->assertTrue($loader->isLoaded('no-priority'));
$this->assertSame(0, $loader->getManifest('no-priority')['priority']);
}
// ── Optional dependencies (TASK-011) ──────────────────────
public function testMissingOptionalDependencyDoesNotThrow(): void {
$root = $this->createModulesRoot();
$this->createModule($root, 'main-module', ['optional_dependencies' => ['ghost-module']]);
$loader = new ModuleLoader();
$loader->loadAll($root);
$this->assertTrue($loader->isLoaded('main-module'));
}
public function testPresentOptionalDependencyLoadsBeforeDependent(): void {
$root = $this->createModulesRoot();
$this->createModule($root, 'opt-dep');
$this->createModule($root, 'consumer', ['optional_dependencies' => ['opt-dep']]);
$loader = new ModuleLoader();
$loader->loadAll($root);
$keys = array_keys($loader->getModules());
$this->assertLessThan(
array_search('consumer', $keys),
array_search('opt-dep', $keys),
"'opt-dep' must be loaded before 'consumer'"
);
}
public function testRequiredDependencyStillThrowsWhenMissing(): void {
$root = $this->createModulesRoot();
$this->createModule($root, 'needs-ghost', ['dependencies' => ['ghost-module']]);
$loader = new ModuleLoader();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessageMatches('/requires missing dependency/');
$loader->loadAll($root);
}
// ── Helpers ───────────────────────────────────────────────
private function createModulesRoot(): string {
$path = sys_get_temp_dir() . '/xc_vm_priority_test_' . bin2hex(random_bytes(6));
mkdir($path, 0775, true);
return $path;
}
private function createModule(string $root, string $name, array $manifestOverrides = [], array $extraManifest = []): void {
$modulePath = $root . '/' . $name;
mkdir($modulePath, 0775, true);
$className = $this->resolveClassName($name);
$manifest = array_merge([
'name' => $name,
'description' => 'test module',
'version' => '1.0.0',
'requires_core' => '>=2.0',
'environment' => 'main',
'dependencies' => [],
'optional_dependencies' => [],
'has_navbar' => false,
'has_settings' => false,
'priority' => 0,
], $manifestOverrides, $extraManifest);
file_put_contents(
$modulePath . '/module.json',
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
$php = '<?php' . "\n"
. 'class ' . $className . ' implements ModuleInterface {' . "\n"
. "\tpublic function getName(): string { return '{$name}'; }" . "\n"
. "\tpublic function getVersion(): string { return '1.0.0'; }" . "\n"
. "\tpublic function boot(ServiceContainer \$container): void {}" . "\n"
. "\tpublic function registerRoutes(Router \$router): void {}" . "\n"
. "\tpublic function registerCommands(CommandRegistry \$registry): void {}" . "\n"
. "\tpublic function getEventSubscribers(): array { return []; }" . "\n"
. "\tpublic function install(): void {}" . "\n"
. "\tpublic function uninstall(): void {}" . "\n"
. "\tpublic function registerNavbar(): void {}" . "\n"
. '}' . "\n";
file_put_contents($modulePath . '/' . $className . '.php', $php);
}
private function resolveClassName(string $name): string {
$parts = explode('-', $name);
return implode('', array_map('ucfirst', $parts)) . 'Module';
}
}