mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-30 12:02:05 +02:00
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
154 lines
5.4 KiB
PHP
154 lines
5.4 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class StreamPipelineTest extends TestCase {
|
|
|
|
// ── Basic pipeline execution ───────────────────────────────
|
|
|
|
public function testRunPassesContextThroughMiddleware(): void {
|
|
$pipeline = new StreamPipeline();
|
|
$pipeline->pipe(new SetAttributeMiddleware('touched', true));
|
|
|
|
$ctx = new StreamContext(1, 'u1', 'hls', []);
|
|
$result = $pipeline->run($ctx);
|
|
|
|
$this->assertTrue($result->get('touched'));
|
|
}
|
|
|
|
public function testRunWithNoMiddlewareReturnsContextUnchanged(): void {
|
|
$pipeline = new StreamPipeline();
|
|
$ctx = new StreamContext(1, 'u1', 'hls', []);
|
|
$result = $pipeline->run($ctx);
|
|
|
|
$this->assertSame($ctx, $result);
|
|
}
|
|
|
|
// ── Priority ordering ──────────────────────────────────────
|
|
|
|
public function testMiddlewareRunsInPriorityDescendingOrder(): void {
|
|
$order = [];
|
|
$pipeline = new StreamPipeline();
|
|
$pipeline->pipe(new RecordOrderMiddleware($order, 'low', 10));
|
|
$pipeline->pipe(new RecordOrderMiddleware($order, 'high', 50));
|
|
$pipeline->pipe(new RecordOrderMiddleware($order, 'mid', 30));
|
|
|
|
$pipeline->run(new StreamContext(1, 'u', 'hls', []));
|
|
|
|
$this->assertSame(['high', 'mid', 'low'], $order);
|
|
}
|
|
|
|
// ── Abort behaviour ────────────────────────────────────────
|
|
|
|
public function testAbortedContextSkipsRemainingMiddleware(): void {
|
|
$afterAbort = false;
|
|
$pipeline = new StreamPipeline();
|
|
$pipeline->pipe(new AbortingMiddleware(100));
|
|
$pipeline->pipe(new CallbackMiddleware(function (StreamContext $ctx, callable $next) use (&$afterAbort) {
|
|
$afterAbort = true;
|
|
return $next($ctx);
|
|
}, 50));
|
|
|
|
$result = $pipeline->run(new StreamContext(1, 'u', 'hls', []));
|
|
|
|
$this->assertTrue($result->isAborted());
|
|
$this->assertFalse($afterAbort);
|
|
}
|
|
|
|
public function testAbortSetsReasonAndCode(): void {
|
|
$pipeline = new StreamPipeline();
|
|
$pipeline->pipe(new AbortingMiddleware(100, 'blocked', 403));
|
|
|
|
$result = $pipeline->run(new StreamContext(1, 'u', 'hls', []));
|
|
|
|
$this->assertSame('blocked', $result->getAbortReason());
|
|
$this->assertSame(403, $result->getAbortCode());
|
|
}
|
|
|
|
// ── StreamContext attribute bag ────────────────────────────
|
|
|
|
public function testContextAttributeBag(): void {
|
|
$ctx = new StreamContext(1, 'u', 'hls', []);
|
|
$ctx->set('key', 'value');
|
|
|
|
$this->assertTrue($ctx->has('key'));
|
|
$this->assertSame('value', $ctx->get('key'));
|
|
}
|
|
|
|
public function testContextGetReturnsNullForMissingAttribute(): void {
|
|
$ctx = new StreamContext(1, 'u', 'hls', []);
|
|
|
|
$this->assertNull($ctx->get('missing'));
|
|
}
|
|
|
|
// ── getMiddleware ──────────────────────────────────────────
|
|
|
|
public function testGetMiddlewareReturnsRegisteredMiddleware(): void {
|
|
$mw = new SetAttributeMiddleware('k', 'v');
|
|
$pipeline = new StreamPipeline();
|
|
$pipeline->pipe($mw);
|
|
|
|
$this->assertContains($mw, $pipeline->getMiddleware());
|
|
}
|
|
}
|
|
|
|
// ── Middleware fixtures ────────────────────────────────────────────
|
|
|
|
final class SetAttributeMiddleware implements StreamMiddlewareInterface {
|
|
public function __construct(
|
|
private readonly string $key,
|
|
private readonly mixed $value,
|
|
private readonly int $priority = 0,
|
|
) {}
|
|
|
|
public function handle(StreamContext $ctx, callable $next): StreamContext {
|
|
$ctx->set($this->key, $this->value);
|
|
return $next($ctx);
|
|
}
|
|
|
|
public function getPriority(): int { return $this->priority; }
|
|
}
|
|
|
|
final class RecordOrderMiddleware implements StreamMiddlewareInterface {
|
|
public function __construct(
|
|
private array &$order,
|
|
private string $label,
|
|
private int $priority,
|
|
) {}
|
|
|
|
public function handle(StreamContext $ctx, callable $next): StreamContext {
|
|
$this->order[] = $this->label;
|
|
return $next($ctx);
|
|
}
|
|
|
|
public function getPriority(): int { return $this->priority; }
|
|
}
|
|
|
|
final class AbortingMiddleware implements StreamMiddlewareInterface {
|
|
public function __construct(
|
|
private int $priority,
|
|
private string $reason = 'aborted',
|
|
private int $code = 0,
|
|
) {}
|
|
|
|
public function handle(StreamContext $ctx, callable $next): StreamContext {
|
|
$ctx->abort($this->reason, $this->code);
|
|
return $ctx;
|
|
}
|
|
|
|
public function getPriority(): int { return $this->priority; }
|
|
}
|
|
|
|
final class CallbackMiddleware implements StreamMiddlewareInterface {
|
|
public function __construct(
|
|
private readonly \Closure $callback,
|
|
private readonly int $priority = 0,
|
|
) {}
|
|
|
|
public function handle(StreamContext $ctx, callable $next): StreamContext {
|
|
return ($this->callback)($ctx, $next);
|
|
}
|
|
|
|
public function getPriority(): int { return $this->priority; }
|
|
}
|