Merge pull request #714 from flightphp/feat/align-runway-commands-with-skeleton

Align Runway generators with skeleton App\ layout
loader-rework v3.19.0
n0nag0n 2 weeks ago committed by GitHub
commit ee03274498
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -99,7 +99,8 @@
]
},
"suggest": {
"latte/latte": "Latte template engine",
"twig/twig": "Twig template engine (recommended for apps, e.g. flightphp/skeleton)",
"latte/latte": "Latte template engine (optional alternative to Twig)",
"tracy/tracy": "Tracy debugger",
"phpstan/phpstan": "PHP Static Analyzer"
},

@ -463,8 +463,7 @@ class Engine
}
throw new Exception(
"Middleware class '$middleware' not found. "
. "Is it being correctly autoloaded with Flight::path()?"
"Middleware class '$middleware' not found. Is it being correctly autoloaded with Flight::path()?"
);
}

@ -42,20 +42,7 @@ class AiGenerateInstructionsCommand extends AbstractBaseCommand
public function execute(): int
{
$io = $this->app()->io();
if (empty($this->config['runway'])) {
$configFile = $this->configFile;
$io = $this->app()->io();
$io->warn(
'The --config-file option is deprecated. '
. 'Move your config values to the \'runway\' key in the config.php file for configuration.',
true
);
$runwayConfig = json_decode(file_get_contents($configFile), true) ?? [];
} else {
$runwayConfig = $this->config['runway'];
}
$runwayConfig = $this->resolveRunwayConfig($io);
// Check for runway creds ai
if (empty($runwayConfig['ai'])) {
@ -64,8 +51,79 @@ class AiGenerateInstructionsCommand extends AbstractBaseCommand
}
$io->info('Let\'s gather some project details to generate AI coding instructions.', true);
$userDetails = $this->gatherProjectDetails($io);
$prompt = $this->buildPrompt($userDetails, $this->loadExistingInstructions());
// Read LLM creds
$creds = $runwayConfig['ai'];
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $creds['api_key'],
];
$data = [
'model' => $creds['model'],
'messages' => [
[
'role' => 'system',
// phpcs:ignore Generic.Files.LineLength
'content' => 'You are a helpful AI coding assistant focused on the Flight Framework for PHP. You are up to date with all your knowledge from https://docs.flightphp.com. As an expert into the programming language PHP, you are top notch at architecting out proper instructions for FlightPHP projects. Output a single AGENTS.md document only.',
],
['role' => 'user', 'content' => $prompt],
],
'temperature' => 0.2,
];
$jsonData = json_encode($data);
// add info line that this may take a few minutes
$io->info('Generating AI instructions, this may take a few minutes...', true);
// Ask questions
$result = $this->callLlmApi($creds['base_url'], $headers, $jsonData, $io);
if ($result === false) {
return 1;
}
$response = json_decode($result, true);
$instructions = $response['choices'][0]['message']['content'] ?? '';
if (!$instructions) {
$io->error('No instructions returned from LLM.', true);
return 1;
}
$agentsPath = $this->projectRoot . 'AGENTS.md';
$io->info('Updating AGENTS.md...', true);
file_put_contents($agentsPath, $instructions);
$io->ok('AI instructions updated successfully in AGENTS.md.', true);
return 0;
}
/**
* Resolve runway config from config.php or deprecated --config-file.
*
* @param object $io
*
* @return array<string,mixed>
*/
protected function resolveRunwayConfig($io): array
{
if (empty($this->config['runway'])) {
$io->warn(
'The --config-file option is deprecated. Move your config values to the \'runway\' key in the config.php file for configuration.', // phpcs:ignore
true
);
return json_decode(file_get_contents($this->configFile), true) ?? [];
}
return $this->config['runway'];
}
/**
* Prompt the user for project details used to generate instructions.
*
* @param object $io
*
* @return array<string,string>
*/
protected function gatherProjectDetails($io): array
{
$projectDesc = $io->prompt('Please describe what your project is for?');
$database = $io->prompt(
@ -74,8 +132,8 @@ class AiGenerateInstructionsCommand extends AbstractBaseCommand
);
$templating = $io->prompt(
'What HTML templating engine will you plan on using (if any)? (recommend latte)',
'latte'
'What HTML templating engine will you plan on using (if any)? (recommend twig)',
'twig'
);
$security = $io->confirm('Is security an important element of this project?', 'y');
@ -95,10 +153,7 @@ class AiGenerateInstructionsCommand extends AbstractBaseCommand
$api = $io->confirm('Will this project expose an API?', 'n');
$other = $io->prompt('Any other important requirements or context? (optional)', 'no');
// Prepare prompt for LLM
$contextFile = $this->projectRoot . '.github/copilot-instructions.md';
$context = file_exists($contextFile) === true ? file_get_contents($contextFile) : '';
$userDetails = [
return [
'Project Description' => $projectDesc,
'Database' => $database,
'Templating Engine' => $templating,
@ -110,83 +165,66 @@ class AiGenerateInstructionsCommand extends AbstractBaseCommand
'API' => $api ? 'yes' : 'no',
'Other' => $other,
];
$detailsText = "";
}
/**
* Build the LLM user prompt from answers and existing instructions.
*
* @param array<string,string> $userDetails
* @param string $context
*
* @return string
*/
protected function buildPrompt(array $userDetails, string $context): string
{
$detailsText = '';
foreach ($userDetails as $k => $v) {
$detailsText .= "$k: $v\n";
}
// phpcs:disable Generic.Files.LineLength
$prompt = <<<EOT
You are an AI coding assistant. Update the following project instructions for this Flight PHP project based on the latest user answers. Only output the new instructions, no extra commentary.
User answers:
$detailsText
Current instructions:
$context
EOT; // phpcs:ignore
You are an AI coding assistant. Write or update project instructions for this Flight PHP project based on the latest user answers. Only output the new instructions (markdown suitable for AGENTS.md), no extra commentary.
// Read LLM creds
$creds = $runwayConfig['ai'];
$apiKey = $creds['api_key'];
$model = $creds['model'];
$baseUrl = $creds['base_url'];
Conventions to encode in the instructions (unless the user answers clearly contradict them):
- Use App\\ namespaces: App\\Controller, App\\Middleware, App\\Model, App\\Utils, App\\Command
- Controllers live in app/Controller/; inject flight\\Engine and other services via the DI container (Dice). Do not use the Flight:: facade in the app layer.
- Prefer flight\\database\\SimplePdo for database access (PdoWrapper is deprecated). Use ActiveRecord for models when an ORM is needed.
- Prefer Twig for HTML views when a templating engine is used.
- AGENTS.md is the sole AI instruction surface (no separate Copilot/Cursor/Gemini/Windsurf rule files). Scoped AGENTS.md files under app/ directories are fine when useful.
- Keep Flight simple and fast; avoid unnecessary abstractions.
// Prepare curl call (OpenAI compatible)
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
];
$data = [
'model' => $model,
'messages' => [
[
'role' => 'system',
'content' => 'You are a helpful AI coding assistant focused on the Flight Framework for PHP. '
. 'You are up to date with all your knowledge from https://docs.flightphp.com. '
. 'As an expert into the programming language PHP, '
. 'you are top notch at architecting out proper instructions for FlightPHP projects.'
],
['role' => 'user', 'content' => $prompt],
],
'temperature' => 0.2,
];
$jsonData = json_encode($data);
User answers:
$detailsText
Current instructions:
$context
EOT;
// phpcs:enable Generic.Files.LineLength
// add info line that this may take a few minutes
$io->info('Generating AI instructions, this may take a few minutes...', true);
return $prompt;
}
$result = $this->callLlmApi($baseUrl, $headers, $jsonData, $io);
if ($result === false) {
return 1;
}
$response = json_decode($result, true);
$instructions = $response['choices'][0]['message']['content'] ?? '';
if (!$instructions) {
$io->error('No instructions returned from LLM.', true);
return 1;
/**
* Load existing project instructions for context.
* Prefers AGENTS.md; falls back to legacy .github/copilot-instructions.md.
*
* @return string
*/
protected function loadExistingInstructions(): string
{
$agentsFile = $this->projectRoot . 'AGENTS.md';
if (file_exists($agentsFile) === true) {
$content = file_get_contents($agentsFile);
return $content !== false ? $content : '';
}
// Write to files
$io->info(
'Updating .github/copilot-instructions.md, '
. '.cursor/rules/project-overview.mdc, '
. '.gemini/GEMINI.md, .windsurfrules and AGENTS.md...',
true
);
if (!is_dir($this->projectRoot . '.github')) {
mkdir($this->projectRoot . '.github', 0755, true);
}
if (!is_dir($this->projectRoot . '.cursor/rules')) {
mkdir($this->projectRoot . '.cursor/rules', 0755, true);
$legacyFile = $this->projectRoot . '.github/copilot-instructions.md';
if (file_exists($legacyFile) === true) {
$content = file_get_contents($legacyFile);
return $content !== false ? $content : '';
}
if (!is_dir($this->projectRoot . '.gemini')) {
mkdir($this->projectRoot . '.gemini', 0755, true);
}
file_put_contents($this->projectRoot . '.github/copilot-instructions.md', $instructions);
file_put_contents($this->projectRoot . '.cursor/rules/project-overview.mdc', $instructions);
file_put_contents($this->projectRoot . '.gemini/GEMINI.md', $instructions);
file_put_contents($this->projectRoot . '.windsurfrules', $instructions);
file_put_contents($this->projectRoot . 'AGENTS.md', $instructions);
$io->ok('AI instructions updated successfully.', true);
return 0;
return '';
}
/**

@ -10,6 +10,16 @@ use Nette\PhpGenerator\PhpNamespace;
class ControllerCommand extends AbstractBaseCommand
{
/**
* Relative directory under app_root for controllers (skeleton: App\Controller).
*/
private const CONTROLLER_DIR = 'Controller';
/**
* PSR-4 namespace for generated controllers.
*/
private const CONTROLLER_NAMESPACE = 'App\\Controller';
/**
* Construct
*
@ -32,8 +42,7 @@ class ControllerCommand extends AbstractBaseCommand
if (empty($this->config['runway'])) {
$io->warn(
'Using a .runway-config.json file is deprecated. '
. 'Move your config values to app/config/config.php with `php runway config:migrate`.',
'Using a .runway-config.json file is deprecated. Move your config values to app/config/config.php with `php runway config:migrate`.', // phpcs:ignore
true
); // @codeCoverageIgnore
@ -61,7 +70,8 @@ class ControllerCommand extends AbstractBaseCommand
$controller .= 'Controller';
}
$controllerPath = $this->projectRoot . '/' . $runwayConfig['app_root'] . 'controllers/' . $controller . '.php';
$appRoot = rtrim(str_replace('\\', '/', $runwayConfig['app_root']), '/') . '/';
$controllerPath = $this->projectRoot . '/' . $appRoot . self::CONTROLLER_DIR . '/' . $controller . '.php';
if (file_exists($controllerPath) === true) {
$io->error($controller . ' already exists.', true);
return;
@ -75,12 +85,12 @@ class ControllerCommand extends AbstractBaseCommand
$file = new PhpFile();
$file->setStrictTypes();
$namespace = new PhpNamespace('app\\controllers');
$namespace = new PhpNamespace(self::CONTROLLER_NAMESPACE);
$namespace->addUse('flight\\Engine');
$class = new ClassType($controller);
$class->addProperty('app')
->setVisibility('protected')
->setVisibility('private')
->setType('flight\\Engine')
->addComment('@var Engine');
$method = $class->addMethod('__construct')
@ -93,7 +103,7 @@ class ControllerCommand extends AbstractBaseCommand
$namespace->add($class);
$file->addNamespace($namespace);
$this->persistClass($controller, $file, $runwayConfig['app_root']);
$this->persistClass($controller, $file, $appRoot);
$io->ok('Controller successfully created at ' . $controllerPath, true);
}
@ -111,7 +121,7 @@ class ControllerCommand extends AbstractBaseCommand
{
$printer = new \Nette\PhpGenerator\PsrPrinter();
file_put_contents(
$this->projectRoot . '/' . $appRoot . 'controllers/' . $controllerName . '.php',
$this->projectRoot . '/' . $appRoot . self::CONTROLLER_DIR . '/' . $controllerName . '.php',
$printer->printFile($file)
);
}

@ -43,8 +43,7 @@ class RouteCommand extends AbstractBaseCommand
if (empty($this->config['runway'])) {
$io->warn(
'Using a .runway-config.json file is deprecated. '
. 'Move your config values to app/config/config.php with `php runway config:migrate`.',
'Using a .runway-config.json file is deprecated. Move your config values to app/config/config.php with `php runway config:migrate`.', // phpcs:ignore
true
); // @codeCoverageIgnore

@ -415,8 +415,7 @@ class Dispatcher
// Final check to make sure it's actually a class and a method, or throw an error
if (is_object($class) === false && class_exists($class) === false) {
$exception = new Exception(
"Class '$class' not found. "
. "Is it being correctly autoloaded with Flight::path()?"
"Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"
);
// If this tried to resolve a class in a container and failed somehow, throw the exception

@ -47,10 +47,11 @@ class Loader
/**
* Registers a class.
*
* @param string $name Registry name
* @param class-string<T>|(Closure(): T) $class Class name or function to instantiate class
* @param array<int, mixed> $params Class initialization parameters
* @param null|(Closure(T $instance): void) $callback $callback Function to call after object instantiation
* @param string $name Registry name
* @param class-string<T>|(Closure(): T) $class Class name or function to instantiate class
* @param array<int, mixed> $params Class initialization parameters
* @param null|(Closure(T $instance): void) $callback Function to call after object instantiation
*
* @template T of object
*/
public function register(string $name, $class, array $params = [], ?callable $callback = null): void

@ -347,7 +347,8 @@ class SimplePdo extends PdoWrapper
$columnCount = count($columns);
foreach ($columns as $col) {
$this->requireSafeIdentifier((string) $col);
$columnName = (string) $col;
$this->requireSafeIdentifier($columnName);
}
// Validate all rows have same columns
@ -382,7 +383,8 @@ class SimplePdo extends PdoWrapper
$columns = array_keys($data);
foreach ($columns as $col) {
$this->requireSafeIdentifier((string) $col);
$columnName = (string) $col;
$this->requireSafeIdentifier($columnName);
}
$placeholders = array_fill(0, count($data), '?');
@ -422,8 +424,9 @@ class SimplePdo extends PdoWrapper
$sets = [];
foreach (array_keys($data) as $column) {
$this->requireSafeIdentifier((string) $column);
$sets[] = "$column = ?";
$columnName = (string) $column;
$this->requireSafeIdentifier($columnName);
$sets[] = "$columnName = ?";
}
$sql = sprintf(

@ -1,7 +1,2 @@
parameters:
ignoreErrors:
-
rawMessage: 'Method flight\core\Dispatcher::parseStringClassAndMethod() should return array{class-string|object, string} but returns non-empty-list<string>.'
identifier: return.type
count: 1
path: flight/core/Dispatcher.php
ignoreErrors: []

@ -154,8 +154,7 @@ class DispatcherTest extends TestCase
$this->expectException(Exception::class);
$this->expectExceptionMessage(
"Class 'NonExistentClass' not found. "
. "Is it being correctly autoloaded with Flight::path()?"
"Class 'NonExistentClass' not found. Is it being correctly autoloaded with Flight::path()?"
);
$this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']);
@ -285,8 +284,7 @@ class DispatcherTest extends TestCase
$this->expectException(ArgumentCountError::class);
$this->expectExceptionMessageMatches(
'#Too few arguments to function tests\\\\classes\\\\TesterClass::__construct\(\), 1 passed'
. ' .+ and exactly 6 expected#'
'#Too few arguments to function tests\\\\classes\\\\TesterClass::__construct\(\), 1 passed .+ and exactly 6 expected#'
);
$this->dispatcher->execute(TesterClass::class . '->instanceMethod');

@ -336,23 +336,15 @@ class RequestBodyParserTest extends TestCase
$parts[] = "Content-Disposition: form-data; name=\"\"; filename=\"empty.txt\"\r\n\r\nemptyNameValue";
// G: invalid filename triggers sanitized fallback
$parts[] = "Content-Disposition: form-data; "
. "name=\"filebad\"; "
. "filename=\"a*b?.txt\"\r\nContent-Type: text/plain\r\n\r\nFILEBAD";
$parts[] = "Content-Disposition: form-data; name=\"filebad\"; filename=\"a*b?.txt\"\r\nContent-Type: text/plain\r\n\r\nFILEBAD";
// H1 & H2: two files same key for aggregation logic (arrays)
$parts[] = "Content-Disposition: form-data; "
. "name=\"filemulti\"; "
. "filename=\"one.txt\"\r\nContent-Type: text/plain\r\n\r\nONE";
$parts[] = "Content-Disposition: form-data; name=\"filemulti\"; filename=\"one.txt\"\r\nContent-Type: text/plain\r\n\r\nONE";
$parts[] = "Content-Disposition: form-data; "
. "name=\"filemulti\"; "
. "filename=\"two.txt\"\r\nContent-Type: text/plain\r\n\r\nTWO";
$parts[] = "Content-Disposition: form-data; name=\"filemulti\"; filename=\"two.txt\"\r\nContent-Type: text/plain\r\n\r\nTWO";
// I: file exceeding total bytes triggers UPLOAD_ERR_INI_SIZE
$parts[] = "Content-Disposition: form-data; "
. "name=\"filebig\"; "
. "filename=\"big.txt\"\r\nContent-Type: text/plain\r\n\r\n"
$parts[] = "Content-Disposition: form-data; name=\"filebig\"; filename=\"big.txt\"\r\nContent-Type: text/plain\r\n\r\n"
. str_repeat('A', 10);
// Build full body
@ -410,13 +402,9 @@ class RequestBodyParserTest extends TestCase
// and header param extraction (preg_match_all)
$boundary = 'BOUNDARYEMPTY';
$validFilePart = "Content-Disposition: form-data; "
. "name=\"fileok\"; "
. "filename=\"ok.txt\"\r\nContent-Type: text/plain\r\n\r\nOK";
$validFilePart = "Content-Disposition: form-data; name=\"fileok\"; filename=\"ok.txt\"\r\nContent-Type: text/plain\r\n\r\nOK";
$emptyNameFilePart = "Content-Disposition: form-data; "
. "name=\"[]\"; "
. "filename=\"empty.txt\"\r\nContent-Type: text/plain\r\n\r\nSHOULD_SKIP";
$emptyNameFilePart = "Content-Disposition: form-data; name=\"[]\"; filename=\"empty.txt\"\r\nContent-Type: text/plain\r\n\r\nSHOULD_SKIP";
$body = '--'
. $boundary

@ -33,8 +33,7 @@ class ContainerDefault
public function testUi(): void
{
echo '<span id="infotext">Route text:</span> '
. 'The container successfully injected a value into the engine! Engine class: <b>'
echo '<span id="infotext">Route text:</span> The container successfully injected a value into the engine! Engine class: <b>'
. get_class($this->app)
. '</b> test_me_out Value: <b>'
. $this->app->get('test_me_out')

@ -71,6 +71,27 @@ class AiGenerateInstructionsCommandTest extends TestCase
file_put_contents(self::$in, implode("\n", $lines) . "\n");
}
/**
* Default interactive answers (templating default is twig).
*
* @return array<int,string>
*/
protected function defaultAnswers(): array
{
return [
'desc',
'mysql',
'twig',
'y',
'y',
'flight/lib',
'Docker',
'2',
'y',
'context info',
];
}
protected function setProjectRoot($command, $path)
{
$reflection = new \ReflectionClass(get_class($command));
@ -97,7 +118,7 @@ class AiGenerateInstructionsCommandTest extends TestCase
$this->setInput([
'desc',
'none',
'latte',
'twig',
'y',
'y',
'none',
@ -121,26 +142,15 @@ class AiGenerateInstructionsCommandTest extends TestCase
$this->assertStringContainsString('Missing AI configuration', file_get_contents(self::$ou));
}
public function testWritesInstructionsToFiles()
public function testWritesInstructionsToAgentsMdOnly()
{
$creds = [
'api_key' => 'key',
'model' => 'gpt-4o',
'base_url' => 'https://api.openai.com',
];
$this->setInput([
'desc',
'mysql',
'latte',
'y',
'y',
'flight/lib',
'Docker',
'2',
'y',
'context info'
]);
$mockInstructions = "# Project Instructions\n\nUse MySQL, Latte, Docker.";
$this->setInput($this->defaultAnswers());
$mockInstructions = "# Project Instructions\n\nUse MySQL, Twig, Docker.";
$cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class)
->setConstructorArgs([
[
@ -163,32 +173,117 @@ class AiGenerateInstructionsCommandTest extends TestCase
'ai:generate-instructions',
]);
$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 . '.gemini/GEMINI.md');
$this->assertFileExists($this->baseDir . '.windsurfrules');
$this->assertFileExists($this->baseDir . 'AGENTS.md');
$this->assertSame($mockInstructions, file_get_contents($this->baseDir . 'AGENTS.md'));
$this->assertFileDoesNotExist($this->baseDir . '.github/copilot-instructions.md');
$this->assertFileDoesNotExist($this->baseDir . '.cursor/rules/project-overview.mdc');
$this->assertFileDoesNotExist($this->baseDir . '.gemini/GEMINI.md');
$this->assertFileDoesNotExist($this->baseDir . '.windsurfrules');
$this->assertStringContainsString('Updating AGENTS.md', file_get_contents(self::$ou));
}
public function testNoInstructionsReturnedFromLlm()
public function testUsesExistingAgentsMdAsContext()
{
$creds = [
'api_key' => 'key',
'model' => 'gpt-4o',
'base_url' => 'https://api.openai.com',
];
$this->setInput([
'desc',
'mysql',
'latte',
'y',
'y',
'flight/lib',
'Docker',
'2',
'y',
'context info'
$existing = "# Existing AGENTS\n\nKeep this context.";
file_put_contents($this->baseDir . 'AGENTS.md', $existing);
$this->setInput($this->defaultAnswers());
$cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class)
->setConstructorArgs([
[
'runway' => ['ai' => $creds]
]
])
->onlyMethods(['callLlmApi'])
->getMock();
$this->setProjectRoot($cmd, $this->baseDir);
$cmd->expects($this->once())
->method('callLlmApi')
->with(
$this->anything(),
$this->anything(),
$this->callback(function ($jsonData) use ($existing) {
$data = json_decode($jsonData, true);
$userContent = $data['messages'][1]['content'] ?? '';
return strpos($userContent, $existing) !== false;
}),
$this->anything()
)
->willReturn(json_encode([
'choices' => [
['message' => ['content' => "# Updated\n\nDone."]]
]
]));
$app = $this->newApp($cmd);
$result = $app->handle([
'runway',
'ai:generate-instructions',
]);
$this->assertSame(0, $result);
}
public function testFallsBackToLegacyCopilotInstructionsForContext()
{
$creds = [
'api_key' => 'key',
'model' => 'gpt-4o',
'base_url' => 'https://api.openai.com',
];
$legacy = "# Legacy copilot instructions\n\nOld layout.";
mkdir($this->baseDir . '.github', 0777, true);
file_put_contents($this->baseDir . '.github/copilot-instructions.md', $legacy);
$this->setInput($this->defaultAnswers());
$cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class)
->setConstructorArgs([
[
'runway' => ['ai' => $creds]
]
])
->onlyMethods(['callLlmApi'])
->getMock();
$this->setProjectRoot($cmd, $this->baseDir);
$cmd->expects($this->once())
->method('callLlmApi')
->with(
$this->anything(),
$this->anything(),
$this->callback(function ($jsonData) use ($legacy) {
$data = json_decode($jsonData, true);
$userContent = $data['messages'][1]['content'] ?? '';
return strpos($userContent, $legacy) !== false;
}),
$this->anything()
)
->willReturn(json_encode([
'choices' => [
['message' => ['content' => "# New AGENTS.md\n\nMigrated."]]
]
]));
$app = $this->newApp($cmd);
$result = $app->handle([
'runway',
'ai:generate-instructions',
]);
$this->assertSame(0, $result);
$this->assertFileExists($this->baseDir . 'AGENTS.md');
// Legacy file is left alone; only AGENTS.md is written
$this->assertSame($legacy, file_get_contents($this->baseDir . '.github/copilot-instructions.md'));
}
public function testNoInstructionsReturnedFromLlm()
{
$creds = [
'api_key' => 'key',
'model' => 'gpt-4o',
'base_url' => 'https://api.openai.com',
];
$this->setInput($this->defaultAnswers());
$cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class)
->setConstructorArgs([
[
@ -220,18 +315,7 @@ class AiGenerateInstructionsCommandTest extends TestCase
'model' => 'gpt-4o',
'base_url' => 'https://api.openai.com',
];
$this->setInput([
'desc',
'mysql',
'latte',
'y',
'y',
'flight/lib',
'Docker',
'2',
'y',
'context info'
]);
$this->setInput($this->defaultAnswers());
$cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class)
->setConstructorArgs([
[
@ -263,19 +347,8 @@ class AiGenerateInstructionsCommandTest extends TestCase
];
$configFile = $this->baseDir . 'old-config.json';
file_put_contents($configFile, 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.";
$this->setInput($this->defaultAnswers());
$mockInstructions = "# Project Instructions\n\nUse MySQL, Twig, Docker.";
// runway key is MISSING from config to trigger deprecated logic
$cmd = $this->getMockBuilder(AiGenerateInstructionsCommand::class)
->setConstructorArgs([[]])
@ -297,6 +370,7 @@ class AiGenerateInstructionsCommandTest extends TestCase
]);
$this->assertSame(0, $result);
$this->assertStringContainsString('The --config-file option is deprecated', file_get_contents(self::$ou));
$this->assertFileExists($this->baseDir . '.github/copilot-instructions.md');
$this->assertFileExists($this->baseDir . 'AGENTS.md');
$this->assertFileDoesNotExist($this->baseDir . '.github/copilot-instructions.md');
}
}

@ -33,12 +33,14 @@ class ControllerCommandTest extends TestCase
unlink(static::$ou);
}
if (file_exists(__DIR__ . '/controllers/TestController.php')) {
unlink(__DIR__ . '/controllers/TestController.php');
$controllerFile = __DIR__ . '/Controller/TestController.php';
if (file_exists($controllerFile)) {
unlink($controllerFile);
}
if (file_exists(__DIR__ . '/controllers/')) {
rmdir(__DIR__ . '/controllers/');
$controllerDir = __DIR__ . '/Controller/';
if (is_dir($controllerDir)) {
rmdir($controllerDir);
}
// Thanks Windows
@ -65,8 +67,8 @@ class ControllerCommandTest extends TestCase
public function testControllerAlreadyExists(): void
{
$app = $this->newApp('test', '0.0.1');
mkdir(__DIR__ . '/controllers/');
file_put_contents(__DIR__ . '/controllers/TestController.php', '<?php class TestController {}');
mkdir(__DIR__ . '/Controller/');
file_put_contents(__DIR__ . '/Controller/TestController.php', '<?php class TestController {}');
$app->add(new ControllerCommand(['runway' => ['app_root' => 'tests/commands/']]));
$app->handle(['runway', 'make:controller', 'Test']);
@ -79,6 +81,10 @@ class ControllerCommandTest extends TestCase
$app->add(new ControllerCommand(['runway' => ['app_root' => 'tests/commands/']]));
$app->handle(['runway', 'make:controller', 'Test']);
$this->assertFileExists(__DIR__ . '/controllers/TestController.php');
$controllerFile = __DIR__ . '/Controller/TestController.php';
$this->assertFileExists($controllerFile);
$contents = file_get_contents($controllerFile);
$this->assertStringContainsString('namespace App\\Controller;', $contents);
$this->assertStringContainsString('class TestController', $contents);
}
}

@ -153,20 +153,20 @@ Flight::group('', function () {
// Test 14: Overwrite the body with a middleware
Flight::route('/overwrite', function () {
echo <<<'html'
<span id="infotext">Route text:</span>
This route status is that it
<span style="color:red; font-weight: bold;">failed</span>
html;
echo <<<'HTML'
<span id="infotext">Route text:</span>
This route status is that it
<span style="color:red; font-weight: bold;">failed</span>
HTML;
})->addMiddleware([new OverwriteBodyMiddleware()]);
// Test 15: UTF8 Chars in url
Flight::route('/わたしはひとです', function () {
echo <<<'html'
<span id="infotext">Route text:</span>
This route status is that it
<span style="color:green; font-weight: bold;">succeeded はい!!!</span>
html;
echo <<<'HTML'
<span id="infotext">Route text:</span>
This route status is that it
<span style="color:green; font-weight: bold;">succeeded はい!!!</span>
HTML;
});
// Test 16: UTF8 Chars in url with utf8 params

Loading…
Cancel
Save