From 6742770cc1c3ec6c8ab64654d743589efaf50bf6 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sat, 28 Jun 2025 17:04:00 -0600 Subject: [PATCH] Added unit tests and adjusted some syntax --- .../AiGenerateInstructionsCommand.php | 92 ++++-- flight/commands/AiInitCommand.php | 199 +++++++------ .../AiGenerateInstructionsCommandTest.php | 199 +++++++++++++ tests/commands/AiInitCommandTest.php | 266 ++++++++++++++++++ tests/server/index.php | 6 +- 5 files changed, 637 insertions(+), 125 deletions(-) create mode 100644 tests/commands/AiGenerateInstructionsCommandTest.php create mode 100644 tests/commands/AiInitCommandTest.php diff --git a/flight/commands/AiGenerateInstructionsCommand.php b/flight/commands/AiGenerateInstructionsCommand.php index bdb2e8b..096d32b 100644 --- a/flight/commands/AiGenerateInstructionsCommand.php +++ b/flight/commands/AiGenerateInstructionsCommand.php @@ -1,20 +1,44 @@ option('--creds-file', 'Path to .runway-creds.json file', null, ''); + $this->option('--base-dir', 'Project base directory (for testing or custom use)', null, ''); } + /** + * Executes the command logic for generating AI instructions. + * + * This method is called to perform the main functionality of the + * AiGenerateInstructionsCommand. It should contain the steps required + * to generate and output instructions using AI, based on the command's + * configuration and input. + * + * @return int + */ public function execute() { $io = $this->app()->io(); - $baseDir = getcwd() . DIRECTORY_SEPARATOR; - $runwayCredsFile = $baseDir . '.runway-creds.json'; + $baseDir = $this->baseDir ? rtrim($this->baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : getcwd() . DIRECTORY_SEPARATOR; + $runwayCredsFile = $this->credsFile ?: $baseDir . '.runway-creds.json'; // Check for runway creds if (!file_exists($runwayCredsFile)) { @@ -55,11 +79,13 @@ class AiGenerateInstructionsCommand extends Command foreach ($userDetails as $k => $v) { $detailsText .= "$k: $v\n"; } - $prompt = "" . - "You are an AI coding assistant. Update the following project instructions for this FlightPHP project based on the latest user answers. " . - "Only output the new instructions, no extra commentary.\n" . - "User answers:\n$detailsText\n" . - "Current instructions:\n$context\n"; + $prompt = <<info('Generating AI instructions, this may take a few minutes...', true); + // add info line that this may take a few minutes + $io->info('Generating AI instructions, this may take a few minutes...', true); - $ch = curl_init($baseUrl . '/v1/chat/completions'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); - $result = curl_exec($ch); - if (curl_errno($ch)) { - $io->error('Failed to call LLM API: ' . curl_error($ch), true); + $result = $this->callLlmApi($baseUrl, $headers, $jsonData, $io); + if ($result === false) { return 1; } - curl_close($ch); $response = json_decode($result, true); $instructions = $response['choices'][0]['message']['content'] ?? ''; if (!$instructions) { @@ -108,13 +127,42 @@ class AiGenerateInstructionsCommand extends Command if (!is_dir($baseDir . '.github')) { mkdir($baseDir . '.github', 0755, true); } - if (!is_dir($baseDir . '.cursor/rules')) { - mkdir($baseDir . '.cursor/rules', 0755, true); - } + if (!is_dir($baseDir . '.cursor/rules')) { + mkdir($baseDir . '.cursor/rules', 0755, true); + } file_put_contents($baseDir . '.github/copilot-instructions.md', $instructions); file_put_contents($baseDir . '.cursor/rules/project-overview.mdc', $instructions); file_put_contents($baseDir . '.windsurfrules', $instructions); $io->ok('AI instructions updated successfully.', true); return 0; } + + /** + * Make the LLM API call using curl + * + * @param string $baseUrl + * @param array $headers + * @param string $jsonData + * @param object $io + * + * @return string|false + * + * @codeCoverageIgnore + */ + protected function callLlmApi($baseUrl, $headers, $jsonData, $io) + { + $ch = curl_init($baseUrl . '/v1/chat/completions'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); + $result = curl_exec($ch); + if (curl_errno($ch)) { + $io->error('Failed to call LLM API: ' . curl_error($ch), true); + curl_close($ch); + return false; + } + curl_close($ch); + return $result; + } } diff --git a/flight/commands/AiInitCommand.php b/flight/commands/AiInitCommand.php index 83fba49..f60ce95 100644 --- a/flight/commands/AiInitCommand.php +++ b/flight/commands/AiInitCommand.php @@ -1,138 +1,137 @@ option('--gitignore-file', 'Path to .gitignore file', null, '') + ->option('--creds-file', 'Path to .runway-creds.json file', null, ''); } /** * Executes the function * - * @return void + * @return int */ public function execute() { - $io = $this->app()->io(); + $io = $this->app()->io(); $io->info('Welcome to AI Init!', true); - // if runway creds already exist, prompt to overwrite - $baseDir = getcwd() . DIRECTORY_SEPARATOR; - $runwayCredsFile = $baseDir . '.runway-creds.json'; - - // make sure the .runway-creds.json file is not already present - if (file_exists($runwayCredsFile)) { - $io->error('.runway-creds.json file already exists. Please remove it before running this command.', true); - // prompt to overwrite - $overwrite = $io->confirm('Do you want to overwrite the existing .runway-creds.json file?', 'n'); - if ($overwrite === false) { - $io->info('Exiting without changes.', true); - return 0; - } - } - - // Prompt for API provider with validation - do { - $api = $io->prompt('Which LLM API do you want to use? (openai, grok, claude) [openai]', 'openai'); - $api = strtolower(trim($api)); - if (!in_array($api, ['openai', 'grok', 'claude'], true)) { - $io->error('Invalid API provider. Please enter one of: openai, grok, claude.', true); - $api = ''; - } - } while (empty($api)); - - // Prompt for base URL with validation - do { - switch($api) { - case 'openai': - $defaultBaseUrl = 'https://api.openai.com'; - break; - case 'grok': - $defaultBaseUrl = 'https://api.x.ai'; - break; - case 'claude': - $defaultBaseUrl = 'https://api.anthropic.com'; - break; - default: - $defaultBaseUrl = ''; - } - $baseUrl = $io->prompt('Enter the base URL for the LLM API', $defaultBaseUrl); - $baseUrl = trim($baseUrl); - if (empty($baseUrl) || !filter_var($baseUrl, FILTER_VALIDATE_URL)) { - $io->error('Base URL cannot be empty and must be a valid URL.', true); - $baseUrl = ''; - } - } while (empty($baseUrl)); + $baseDir = getcwd() . DIRECTORY_SEPARATOR; + $runwayCredsFile = $this->credsFile ?: $baseDir . '.runway-creds.json'; + $gitignoreFile = $this->gitignoreFile ?: $baseDir . '.gitignore'; - // Validate API key input - do { - $apiKey = $io->prompt('Enter your API key for ' . $api); - if (empty(trim($apiKey))) { - $io->error('API key cannot be empty. Please enter a valid API key.', true); + // make sure the .runway-creds.json file is not already present + if (file_exists($runwayCredsFile)) { + $io->error('.runway-creds.json file already exists. Please remove it before running this command.', true); + // prompt to overwrite + $overwrite = $io->confirm('Do you want to overwrite the existing .runway-creds.json file?', 'n'); + if ($overwrite === false) { + $io->info('Exiting without changes.', true); + return 0; } - } while (empty(trim($apiKey))); + } + + // Prompt for API provider with validation + $allowedApis = [ + '1' => 'openai', + '2' => 'grok', + '3' => 'claude' + ]; + $apiChoice = strtolower(trim($io->choice('Which LLM API do you want to use?', $allowedApis, '1'))); + $api = $allowedApis[$apiChoice] ?? 'openai'; + + // Prompt for base URL with validation + switch ($api) { + case 'openai': + $defaultBaseUrl = 'https://api.openai.com'; + break; + case 'grok': + $defaultBaseUrl = 'https://api.x.ai'; + break; + case 'claude': + $defaultBaseUrl = 'https://api.anthropic.com'; + break; + } + $baseUrl = trim($io->prompt('Enter the base URL for the LLM API', $defaultBaseUrl)); + if (empty($baseUrl) || !filter_var($baseUrl, FILTER_VALIDATE_URL)) { + $io->error('Base URL cannot be empty and must be a valid URL.', true); + return 1; + } + + // Validate API key input + $apiKey = trim($io->prompt('Enter your API key for ' . $api)); + if (empty($apiKey)) { + $io->error('API key cannot be empty. Please enter a valid API key.', true); + return 1; + } // Validate model input - do { - switch($api) { - case 'openai': - $defaultModel = 'gpt-4o'; - break; - case 'grok': - $defaultModel = 'grok-3-beta'; - break; - case 'claude': - $defaultModel = 'claude-3-opus'; - break; - default: - $defaultModel = ''; - } - $model = $io->prompt('Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc)', $defaultModel); - if (empty(trim($model))) { - $io->error('Model name cannot be empty. Please enter a valid model name.', true); - } - } while (empty(trim($model))); + switch ($api) { + case 'openai': + $defaultModel = 'gpt-4o'; + break; + case 'grok': + $defaultModel = 'grok-3-beta'; + break; + case 'claude': + $defaultModel = 'claude-3-opus'; + break; + } + $model = trim($io->prompt('Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc)', $defaultModel)); $creds = [ 'provider' => $api, 'api_key' => $apiKey, 'model' => $model, - 'base_url' => $baseUrl, + 'base_url' => $baseUrl, ]; $json = json_encode($creds, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); $file = $runwayCredsFile; - if (file_put_contents($file, $json) === false) { - $io->error('Failed to write credentials to ' . $file, true); - return 1; - } + file_put_contents($file, $json); + + // change permissions to 600 + chmod($file, 0600); + $io->ok('Credentials saved to ' . $file, true); - // run a check to make sure that the creds file is in the .gitignore file - $gitignoreFile = $baseDir . '.gitignore'; - if (!file_exists($gitignoreFile)) { - // create the .gitignore file if it doesn't exist - file_put_contents($gitignoreFile, ".runway-creds.json\n"); - $io->info('.gitignore file created and .runway-creds.json added to it.', true); - } else { - // check if the .runway-creds.json file is already in the .gitignore file - $gitignoreContents = file_get_contents($gitignoreFile); - if (strpos($gitignoreContents, '.runway-creds.json') === false) { - // add the .runway-creds.json file to the .gitignore file - file_put_contents($gitignoreFile, "\n.runway-creds.json\n", FILE_APPEND); - $io->info('.runway-creds.json added to .gitignore file.', true); - } else { - $io->info('.runway-creds.json is already in the .gitignore file.', true); - } - } + // run a check to make sure that the creds file is in the .gitignore file + // use $gitignoreFile instead of hardcoded path + if (!file_exists($gitignoreFile)) { + // create the .gitignore file if it doesn't exist + file_put_contents($gitignoreFile, basename($runwayCredsFile) . "\n"); + $io->info(basename($gitignoreFile) . ' file created and ' . basename($runwayCredsFile) . ' added to it.', true); + } else { + // check if the creds file is already in the .gitignore file + $gitignoreContents = file_get_contents($gitignoreFile); + if (strpos($gitignoreContents, basename($runwayCredsFile)) === false) { + // add the creds file to the .gitignore file + file_put_contents($gitignoreFile, "\n" . basename($runwayCredsFile) . "\n", FILE_APPEND); + $io->info(basename($runwayCredsFile) . ' added to ' . basename($gitignoreFile) . ' file.', true); + } else { + $io->info(basename($runwayCredsFile) . ' is already in the ' . basename($gitignoreFile) . ' file.', true); + } + } return 0; } diff --git a/tests/commands/AiGenerateInstructionsCommandTest.php b/tests/commands/AiGenerateInstructionsCommandTest.php new file mode 100644 index 0000000..6e09484 --- /dev/null +++ b/tests/commands/AiGenerateInstructionsCommandTest.php @@ -0,0 +1,199 @@ +baseDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flightphp-test-basedir-' . uniqid('', true) . DIRECTORY_SEPARATOR; + if (!is_dir($this->baseDir)) { + mkdir($this->baseDir, 0777, true); + } + $this->runwayCredsFile = $this->baseDir . 'dummy-creds.json'; + if (file_exists($this->runwayCredsFile)) { + unlink($this->runwayCredsFile); + } + @unlink($this->baseDir . '.github/copilot-instructions.md'); + @unlink($this->baseDir . '.cursor/rules/project-overview.mdc'); + @unlink($this->baseDir . '.windsurfrules'); + @rmdir($this->baseDir . '.github'); + @rmdir($this->baseDir . '.cursor/rules'); + @rmdir($this->baseDir . '.cursor'); + } + + public function tearDown(): void + { + if (file_exists(self::$in)) { + unlink(self::$in); + } + if (file_exists(self::$ou)) { + unlink(self::$ou); + } + if (file_exists($this->runwayCredsFile)) { + unlink($this->runwayCredsFile); + } + @unlink($this->baseDir . '.github/copilot-instructions.md'); + @unlink($this->baseDir . '.cursor/rules/project-overview.mdc'); + @unlink($this->baseDir . '.windsurfrules'); + @rmdir($this->baseDir . '.github'); + @rmdir($this->baseDir . '.cursor/rules'); + @rmdir($this->baseDir . '.cursor'); + if (is_dir($this->baseDir . '.cursor/rules')) { + @rmdir($this->baseDir . '.cursor/rules'); + } + if (is_dir($this->baseDir . '.cursor')) { + @rmdir($this->baseDir . '.cursor'); + } + if (is_dir($this->baseDir . '.github')) { + @rmdir($this->baseDir . '.github'); + } + if (is_dir($this->baseDir)) { + @rmdir($this->baseDir); + } + } + + protected function newApp($command): Application + { + $app = new Application('test', '0.0.1', function ($exitCode) { + return $exitCode; + }); + $app->io(new Interactor(self::$in, self::$ou)); + $app->add($command); + return $app; + } + + protected function setInput(array $lines): void + { + file_put_contents(self::$in, implode("\n", $lines) . "\n"); + } + + public function testFailsIfCredsFileMissing() + { + $this->setInput([ + 'desc', 'none', 'latte', 'y', 'y', 'none', 'Docker', '1', 'n', 'no' + ]); + $cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class) + ->onlyMethods(['callLlmApi']) + ->getMock(); + $app = $this->newApp($cmd); + $result = $app->handle([ + 'runway', 'ai:generate-instructions', + '--creds-file=' . $this->runwayCredsFile, + '--base-dir=' . $this->baseDir + ]); + $this->assertSame(1, $result); + $this->assertFileDoesNotExist($this->baseDir . '.github/copilot-instructions.md'); + } + + public function testWritesInstructionsToFiles() + { + $creds = [ + 'api_key' => 'key', + 'model' => 'gpt-4o', + 'base_url' => 'https://api.openai.com', + ]; + file_put_contents($this->runwayCredsFile, json_encode($creds)); + $this->setInput([ + 'desc', 'mysql', 'latte', 'y', 'y', 'flight/lib', 'Docker', '2', 'y', 'context info' + ]); + $mockInstructions = "# Project Instructions\n\nUse MySQL, Latte, Docker."; + $cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class) + ->onlyMethods(['callLlmApi']) + ->getMock(); + $cmd->expects($this->once()) + ->method('callLlmApi') + ->willReturn(json_encode([ + 'choices' => [ + ['message' => ['content' => $mockInstructions]] + ] + ])); + $app = $this->newApp($cmd); + $result = $app->handle([ + 'runway', 'ai:generate-instructions', + '--creds-file=' . $this->runwayCredsFile, + '--base-dir=' . $this->baseDir + ]); + $this->assertSame(0, $result); + $this->assertFileExists($this->baseDir . '.github/copilot-instructions.md'); + $this->assertFileExists($this->baseDir . '.cursor/rules/project-overview.mdc'); + $this->assertFileExists($this->baseDir . '.windsurfrules'); + $this->assertStringContainsString('MySQL', file_get_contents($this->baseDir . '.github/copilot-instructions.md')); + $this->assertStringContainsString('MySQL', file_get_contents($this->baseDir . '.cursor/rules/project-overview.mdc')); + $this->assertStringContainsString('MySQL', file_get_contents($this->baseDir . '.windsurfrules')); + } + + public function testNoInstructionsReturnedFromLlm() + { + $creds = [ + 'api_key' => 'key', + 'model' => 'gpt-4o', + 'base_url' => 'https://api.openai.com', + ]; + file_put_contents($this->runwayCredsFile, json_encode($creds)); + $this->setInput([ + 'desc', 'mysql', 'latte', 'y', 'y', 'flight/lib', 'Docker', '2', 'y', 'context info' + ]); + $cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class) + ->onlyMethods(['callLlmApi']) + ->getMock(); + $cmd->expects($this->once()) + ->method('callLlmApi') + ->willReturn(json_encode([ + 'choices' => [ + ['message' => ['content' => '']] + ] + ])); + $app = $this->newApp($cmd); + $result = $app->handle([ + 'runway', 'ai:generate-instructions', + '--creds-file=' . $this->runwayCredsFile, + '--base-dir=' . $this->baseDir + ]); + $this->assertSame(1, $result); + $this->assertFileDoesNotExist($this->baseDir . '.github/copilot-instructions.md'); + } + + public function testLlmApiCallFails() + { + $creds = [ + 'api_key' => 'key', + 'model' => 'gpt-4o', + 'base_url' => 'https://api.openai.com', + ]; + file_put_contents($this->runwayCredsFile, json_encode($creds)); + $this->setInput([ + 'desc', 'mysql', 'latte', 'y', 'y', 'flight/lib', 'Docker', '2', 'y', 'context info' + ]); + $cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class) + ->onlyMethods(['callLlmApi']) + ->getMock(); + $cmd->expects($this->once()) + ->method('callLlmApi') + ->willReturn(false); + $app = $this->newApp($cmd); + $result = $app->handle([ + 'runway', 'ai:generate-instructions', + '--creds-file=' . $this->runwayCredsFile, + '--base-dir=' . $this->baseDir + ]); + $this->assertSame(1, $result); + $this->assertFileDoesNotExist($this->baseDir . '.github/copilot-instructions.md'); + } +} diff --git a/tests/commands/AiInitCommandTest.php b/tests/commands/AiInitCommandTest.php new file mode 100644 index 0000000..1e7dd51 --- /dev/null +++ b/tests/commands/AiInitCommandTest.php @@ -0,0 +1,266 @@ +baseDir = getcwd() . DIRECTORY_SEPARATOR; + $this->runwayCredsFile = __DIR__ . DIRECTORY_SEPARATOR . 'dummy-creds-' . uniqid('', true) . '.json'; + $this->gitignoreFile = __DIR__ . DIRECTORY_SEPARATOR . 'dummy-gitignore-' . uniqid('', true); + if (file_exists($this->runwayCredsFile)) { + unlink($this->runwayCredsFile); + } + if (file_exists($this->gitignoreFile)) { + unlink($this->gitignoreFile); + } + } + + public function tearDown(): void + { + if (file_exists(self::$in)) { + unlink(self::$in); + } + if (file_exists(self::$ou)) { + unlink(self::$ou); + } + if (file_exists($this->runwayCredsFile)) { + if (is_dir($this->runwayCredsFile)) { + rmdir($this->runwayCredsFile); + } else { + unlink($this->runwayCredsFile); + } + } + if (file_exists($this->gitignoreFile)) { + unlink($this->gitignoreFile); + } + } + + protected function newApp(): Application + { + $app = new Application('test', '0.0.1', function ($exitCode) { + return $exitCode; + }); + $app->io(new Interactor(self::$in, self::$ou)); + return $app; + } + + protected function setInput(array $lines): void + { + file_put_contents(self::$in, implode("\n", $lines) . "\n"); + } + + public function testInitCreatesCredsAndGitignore() + { + $this->setInput([ + '1', // provider + '', // accept default base url + 'test-key', // api key + '', // accept default model + ]); + $app = $this->newApp(); + $app->add(new AiInitCommand()); + $result = $app->handle([ + 'runway', 'ai:init', + '--creds-file=' . $this->runwayCredsFile, + '--gitignore-file=' . $this->gitignoreFile + ]); + $this->assertSame(0, $result); + $this->assertFileExists($this->runwayCredsFile); + $creds = json_decode(file_get_contents($this->runwayCredsFile), true); + $this->assertSame('openai', $creds['provider']); + $this->assertSame('test-key', $creds['api_key']); + $this->assertSame('gpt-4o', $creds['model']); + $this->assertSame('https://api.openai.com', $creds['base_url']); + $this->assertFileExists($this->gitignoreFile); + $this->assertStringContainsString(basename($this->runwayCredsFile), file_get_contents($this->gitignoreFile)); + } + + public function testInitWithExistingCredsNoOverwrite() + { + file_put_contents($this->runwayCredsFile, '{}'); + $this->setInput([ + 'n', // do not overwrite + ]); + $app = $this->newApp(); + $app->add(new AiInitCommand()); + $result = $app->handle([ + 'runway', 'ai:init', + '--creds-file=' . $this->runwayCredsFile, + '--gitignore-file=' . $this->gitignoreFile + ]); + $this->assertSame(0, $result); + $this->assertSame('{}', file_get_contents($this->runwayCredsFile)); + } + + public function testInitWithExistingCredsOverwrite() + { + file_put_contents($this->runwayCredsFile, '{}'); + $this->setInput([ + 'y', // overwrite + '2', // provider + '', // accept default base url + 'grok-key', // api key + '', // accept default model + ]); + $app = $this->newApp(); + $app->add(new AiInitCommand()); + $result = $app->handle([ + 'runway', 'ai:init', + '--creds-file=' . $this->runwayCredsFile, + '--gitignore-file=' . $this->gitignoreFile + ]); + $this->assertSame(0, $result); + $creds = json_decode(file_get_contents($this->runwayCredsFile), true); + $this->assertSame('grok', $creds['provider']); + $this->assertSame('grok-key', $creds['api_key']); + $this->assertSame('grok-3-beta', $creds['model']); + $this->assertSame('https://api.x.ai', $creds['base_url']); + } + + public function testEmptyApiKeyPromptsAgain() + { + $this->setInput([ + '1', + '', // accept default base url + '', // empty api key, should error and exit + ]); + $app = $this->newApp(); + $app->add(new AiInitCommand()); + $result = $app->handle([ + 'runway', 'ai:init', + '--creds-file=' . $this->runwayCredsFile, + '--gitignore-file=' . $this->gitignoreFile + ]); + $this->assertSame(1, $result); + $this->assertFileDoesNotExist($this->runwayCredsFile); + } + + public function testEmptyModelPrompts() + { + $this->setInput([ + '1', + '', + 'key', + '', // accept default model (should use default) + ]); + $app = $this->newApp(); + $app->add(new AiInitCommand()); + $result = $app->handle([ + 'runway', 'ai:init', + '--creds-file=' . $this->runwayCredsFile, + '--gitignore-file=' . $this->gitignoreFile + ]); + $this->assertSame(0, $result); + $creds = json_decode(file_get_contents($this->runwayCredsFile), true); + $this->assertSame('gpt-4o', $creds['model']); + } + + public function testGitignoreAlreadyHasCreds() + { + file_put_contents($this->gitignoreFile, basename($this->runwayCredsFile) . "\n"); + $this->setInput([ + '1', + '', + 'key', + '', + ]); + $app = $this->newApp(); + $app->add(new AiInitCommand()); + $result = $app->handle([ + 'runway', 'ai:init', + '--creds-file=' . $this->runwayCredsFile, + '--gitignore-file=' . $this->gitignoreFile + ]); + $this->assertSame(0, $result); + $this->assertFileExists($this->gitignoreFile); + $lines = file($this->gitignoreFile, FILE_IGNORE_NEW_LINES); + $this->assertContains(basename($this->runwayCredsFile), $lines); + $this->assertCount(1, array_filter($lines, function ($l) { + return trim($l) === basename($this->runwayCredsFile); + })); + } + + public function testInitWithClaudeProvider() + { + $this->setInput([ + '3', // provider (claude) + '', // accept default base url + 'claude-key', // api key + '', // accept default model + ]); + $app = $this->newApp(); + $app->add(new AiInitCommand()); + $result = $app->handle([ + 'runway', 'ai:init', + '--creds-file=' . $this->runwayCredsFile, + '--gitignore-file=' . $this->gitignoreFile + ]); + $this->assertSame(0, $result); + $creds = json_decode(file_get_contents($this->runwayCredsFile), true); + $this->assertSame('claude', $creds['provider']); + $this->assertSame('claude-key', $creds['api_key']); + $this->assertSame('claude-3-opus', $creds['model']); + $this->assertSame('https://api.anthropic.com', $creds['base_url']); + } + + public function testAddsCredsFileToExistingGitignoreIfMissing() + { + // .gitignore exists but does not contain creds file + file_put_contents($this->gitignoreFile, "vendor\nnode_modules\n.DS_Store\n"); + $this->setInput([ + '1', // provider + '', // accept default base url + 'test-key', // api key + '', // accept default model + ]); + $app = $this->newApp(); + $app->add(new AiInitCommand()); + $result = $app->handle([ + 'runway', 'ai:init', + '--creds-file=' . $this->runwayCredsFile, + '--gitignore-file=' . $this->gitignoreFile + ]); + $this->assertSame(0, $result); + $lines = file($this->gitignoreFile, FILE_IGNORE_NEW_LINES); + $this->assertContains(basename($this->runwayCredsFile), $lines); + $this->assertCount(1, array_filter($lines, function ($l) { + return trim($l) === basename($this->runwayCredsFile); + })); + } + + public function testInvalidBaseUrlFails() + { + $this->setInput([ + '1', // provider + 'not-a-valid-url', // invalid base url + ]); + $app = $this->newApp(); + $app->add(new AiInitCommand()); + $result = $app->handle([ + 'runway', 'ai:init', + '--creds-file=' . $this->runwayCredsFile, + '--gitignore-file=' . $this->gitignoreFile + ]); + $this->assertSame(1, $result); + $this->assertFileDoesNotExist($this->runwayCredsFile); + } +} diff --git a/tests/server/index.php b/tests/server/index.php index e9bf043..a4cd289 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -98,9 +98,9 @@ Flight::group('', function () { Flight::render('template.phtml', ['name' => $name]); }); - Flight::route('/template-data/@data', function ($data) { - Flight::render('template.phtml', ['data' => $data]); - }); + Flight::route('/template-data/@data', function ($data) { + Flight::render('template.phtml', ['data' => $data]); + }); // Test 8: Throw an error Flight::route('/error', function () {