'XcVm\\Cli\\Commands\\', 'Cli/CronJobs' => 'XcVm\\Cli\\CronJobs\\', ]; /** @return array{0:string[],1:string[]} [resolved FQCNs, concrete commands implementing the interface] */ private function discover(): array { $resolved = []; $commands = []; foreach (self::DIRS as $dir => $ns) { $path = MAIN_HOME . $dir; $this->assertDirectoryExists($path); foreach (glob($path . '/*.php') as $file) { $fqcn = $ns . basename($file, '.php'); if (!class_exists($fqcn)) { continue; } $resolved[] = $fqcn; $ref = new ReflectionClass($fqcn); if (!$ref->isAbstract() && $ref->implementsInterface(CommandInterface::class)) { $commands[] = $fqcn; } } } return [$resolved, $commands]; } public function testEveryCliFileResolvesByFqcn(): void { $files = 0; foreach (self::DIRS as $dir => $ns) { $files += count(glob(MAIN_HOME . $dir . '/*.php')); } [$resolved] = $this->discover(); $this->assertSame( $files, count($resolved), 'Every Cli/Commands + Cli/CronJobs file must resolve to a class by FQCN.' ); } public function testDiscoversAStableSetOfCommands(): void { [, $commands] = $this->discover(); // The concrete command surface should not silently shrink. $this->assertGreaterThanOrEqual( 40, count($commands), 'Far fewer commands than expected — discovery likely broke.' ); foreach ($commands as $fqcn) { $this->assertTrue( (new ReflectionClass($fqcn))->implementsInterface(CommandInterface::class) ); } } }