cwd = getcwd(); $this->tempVendor = sys_get_temp_dir() . '/tasker_integration_test_' . uniqid(); mkdir($this->tempVendor, 0777, true); mkdir($this->tempVendor . '/composer', 0777, true); } protected function tearDown(): void { $this->removeDirectory($this->tempVendor); chdir($this->cwd); } private function removeDirectory(string $path): void { if (is_link($path)) { unlink($path); return; } if (!is_dir($path)) { if (file_exists($path)) { unlink($path); } return; } $files = array_diff(scandir($path), ['.', '..']); foreach ($files as $file) { $this->removeDirectory("$path/$file"); } rmdir($path); } public function testCommandDiscoveryViaComposer(): void { // Mock installed.json $installedJson = [ 'packages' => [ [ 'name' => 'vendor/package', 'extra' => [ 'phred-tasker' => [ 'commands' => [ IntegrationMockCommand::class ] ] ] ] ] ]; file_put_contents($this->tempVendor . '/composer/installed.json', json_encode($installedJson)); // Mock root composer.json in the temp dir (we'll chdir there) $rootComposer = [ 'extra' => [ 'phred-tasker' => [ 'commands' => [ // Root commands can also be here ] ] ] ]; $testDir = sys_get_temp_dir() . '/tasker_root_' . uniqid(); mkdir($testDir, 0777, true); file_put_contents($testDir . '/composer.json', json_encode($rootComposer)); // Symlink vendor to the testDir symlink($this->tempVendor, $testDir . '/vendor'); chdir($testDir); $runner = new Runner(); $runner->discover(); $this->assertInstanceOf(IntegrationMockCommand::class, $runner->find('integration:mock')); $this->removeDirectory($testDir); } public function testFullExecutionFlow(): void { $runner = new Runner(); $command = new IntegrationMockCommand(); $runner->register($command); $parser = new ArgvParser(['bin/tasker', 'integration:mock', 'val1']); // IO Wiring $output = new OutputAdapter(false); // no ansi for easier testing $output->setVerbosity($parser->getVerbosity()); $cmdArgs = $command->getArguments(); $remaining = $parser->getRemainingArguments(); $mappedArgs = []; $i = 0; foreach ($cmdArgs as $name => $desc) { if (isset($remaining[$i])) { $mappedArgs[$name] = $remaining[$i]; } $i++; } $input = new InputAdapter($mappedArgs, []); ob_start(); $exitCode = $runner->run($command, $input, $output); $content = ob_get_clean(); $this->assertEquals(0, $exitCode); $this->assertStringContainsString('Mock executed with arg: val1', $content); } public function testArgvParserEdgeCases(): void { // Combined flags rejection is NOT implemented in current ArgvParser, // it treats unknown things as remaining arguments. // Let's test current behavior and see if it meets needs. $parser = new ArgvParser(['bin/tasker', '-vn', 'cmd']); // Current implementation: // -vn is not recognized as -v and -n, it goes to remaining. $this->assertEquals(1, $parser->getVerbosity()); $this->assertFalse($parser->isNoInteraction()); $this->assertContains('-vn', $parser->getRemainingArguments()); } public function testHelpOutput(): void { $runner = new Runner(); $command = $runner->find('help'); $this->assertNotNull($command); $input = new InputAdapter(['command' => 'list'], []); $output = new OutputAdapter(false); ob_start(); $runner->run($command, $input, $output); $content = ob_get_clean(); $this->assertStringContainsString('Usage:', $content); $this->assertStringContainsString('tasker help ', $content); $this->assertStringContainsString('tasker list', $content); } } class IntegrationMockCommand implements CommandInterface { public function getName(): string { return 'integration:mock'; } public function getDescription(): string { return 'Mock for integration test'; } public function getArguments(): array { return ['arg1' => 'An argument']; } public function getOptions(): array { return []; } public function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Mock executed with arg: ' . $input->getArgument('arg1')); return 0; } }