Files
XC_VM/tests/Unit/StreamPipelineTest.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

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; }
}