mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-29 20:02:04 +02:00
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
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user