Files
server/tests/php/Unit/Runtime/ConsoleRuntimeTest.php
T
2026-07-27 08:54:37 -04:00

184 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXT\Unit\Runtime;
use KTXC\Application\Execution\ExecutionContext;
use KTXC\Application\Execution\ExecutionDescriptor;
use KTXC\Application\Execution\ExecutionOutcome;
use KTXC\Application\Execution\ExecutionRunner;
use KTXC\Application\Execution\ExecutionRunnerInterface;
use KTXC\Application\Execution\ExecutionScope;
use KTXC\Application\Execution\TerminationReport;
use KTXC\Context\IdentityContext;
use KTXC\Context\TenantContext;
use KTXC\KernelInterface;
use KTXC\Module\ModuleCollection;
use KTXC\Module\ModuleManager;
use KTXC\Runtime\Console\ConsoleRuntime;
use KTXC\Service\TenantService;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
final class ConsoleRuntimeTest extends TestCase
{
#[Test]
#[TestDox('Successful commands preserve their exit code and terminate')]
public function succeeds(): void
{
$modules = $this->createStub(ModuleManager::class);
$modules->method('list')->willReturn(new ModuleCollection());
$kernel = $this->kernel(new ConsoleContainer([ModuleManager::class => $modules]));
$exitCode = (new ConsoleRuntime($kernel))->run(
new ArrayInput(['command' => 'list', '--raw' => true]),
new BufferedOutput(),
);
self::assertSame(0, $exitCode);
self::assertSame(1, $kernel->terminations);
self::assertTrue($kernel->outcome?->successful);
}
#[Test]
#[TestDox('Console failures terminate before being rethrown')]
public function fails(): void
{
$kernel = $this->kernel(new ConsoleContainer([], new \RuntimeException('Discovery failed.')));
try {
(new ConsoleRuntime($kernel))->run(
new ArrayInput(['command' => 'list']),
new BufferedOutput(),
);
self::fail('Expected console discovery to fail.');
} catch (\RuntimeException $error) {
self::assertSame('Discovery failed.', $error->getMessage());
}
self::assertSame(1, $kernel->terminations);
self::assertFalse($kernel->outcome?->successful);
}
#[Test]
#[TestDox('Deferred termination failures do not replace command exit codes')]
public function preservesExitCodes(): void
{
$modules = $this->createStub(ModuleManager::class);
$modules->method('list')->willReturn(new ModuleCollection());
$kernel = $this->kernel(new ConsoleContainer([ModuleManager::class => $modules]));
$kernel->terminationReport = new TerminationReport(
failures: [new \RuntimeException('Deferred listener failed.')],
);
$exitCode = (new ConsoleRuntime($kernel))->run(
new ArrayInput(['command' => 'list', '--raw' => true]),
new BufferedOutput(),
);
self::assertSame(0, $exitCode);
}
private function kernel(ContainerInterface $container): ConsoleKernel
{
return new ConsoleKernel(
$container,
new TenantContext($this->createStub(TenantService::class)),
new IdentityContext(),
);
}
}
final class ConsoleKernel implements KernelInterface
{
public int $terminations = 0;
public ?ExecutionOutcome $outcome = null;
public TerminationReport $terminationReport;
public function __construct(
private readonly ContainerInterface $services,
private readonly TenantContext $tenantContext,
private readonly IdentityContext $identityContext,
) {
$this->terminationReport = new TerminationReport();
}
public function boot(): void
{
}
public function executionRunner(): ExecutionRunnerInterface
{
return new ExecutionRunner($this);
}
public function beginExecution(ExecutionDescriptor $descriptor): ExecutionScope
{
return new ExecutionScope(
$descriptor,
ExecutionContext::fromDescriptor($descriptor),
$this->tenantContext,
$this->identityContext,
);
}
public function terminateExecution(
ExecutionScope $scope,
ExecutionOutcome $outcome,
): TerminationReport {
$this->terminations++;
$this->outcome = $outcome;
$scope->dispose();
$scope->markTerminated();
return $this->terminationReport;
}
public function shutdown(): void
{
}
public function container(): ContainerInterface
{
return $this->services;
}
public function environment(): string
{
return 'test';
}
public function debug(): bool
{
return false;
}
}
final class ConsoleContainer implements ContainerInterface
{
public function __construct(
private readonly array $services,
private readonly ?\Throwable $failure = null,
) {
}
public function get(string $id): mixed
{
if ($this->failure !== null) {
throw $this->failure;
}
return $this->services[$id];
}
public function has(string $id): bool
{
return isset($this->services[$id]);
}
}