diff --git a/composer.json b/composer.json index 7108160f..133282fa 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "mockery/mockery": "^1.6.12", "orchestra/testbench": "^8.36.0|^9.15.0|^10.6", "pestphp/pest": "^2.36.0|^3.8.4", - "phpstan/phpstan": "^2.1.27" + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.1" }, "autoload": { "psr-4": { @@ -52,13 +53,15 @@ }, "scripts": { "lint": [ - "vendor/bin/pint", - "vendor/bin/phpstan --memory-limit=-1" + "pint", + "phpstan --memory-limit=-1", + "rector" ], - "test": [ - "vendor/bin/pest" + "test": "pest", + "test:lint": [ + "pint --test", + "rector --dry-run" ], - "test:lint": "pint --test", "test:types": "phpstan", "check": [ "@composer lint", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 703a4a44..ec2edefa 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,13 +2,13 @@ - ./tests/Unit + tests/Unit - ./tests/Feature + tests/Feature - ./tests/ArchTest.php + tests/ArchTest.php diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..35c4e0a9 --- /dev/null +++ b/rector.php @@ -0,0 +1,29 @@ +withPaths([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->withSkip([ + ReadOnlyPropertyRector::class, + EncapsedStringsToSprintfRector::class, + DisallowedEmptyRuleFixerRector::class, + BooleanInBooleanNotRuleFixerRector::class, + ]) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + codingStyle: true, + typeDeclarations: true, + earlyReturn: true, + strictBooleans: true, + )->withPhpSets(php81: true); diff --git a/src/BoostServiceProvider.php b/src/BoostServiceProvider.php index 4bd4e299..13db6999 100644 --- a/src/BoostServiceProvider.php +++ b/src/BoostServiceProvider.php @@ -40,7 +40,7 @@ public function register(): void ]; $cacheKey = 'boost.roster.scan'; - $lastModified = max(array_map(fn ($path) => file_exists($path) ? filemtime($path) : 0, $lockFiles)); + $lastModified = max(array_map(fn (string $path): int|false => file_exists($path) ? filemtime($path) : 0, $lockFiles)); $cached = cache()->get($cacheKey); if ($cached && isset($cached['timestamp']) && $cached['timestamp'] >= $lastModified) { @@ -113,7 +113,12 @@ private function registerRoutes(): void * } $log */ foreach ($logs as $log) { $logger->write( - level: self::mapJsTypeToPsr3Level($log['type']), + level: match ($log['type']) { + 'warn' => 'warning', + 'log', 'table' => 'debug', + 'window_error', 'uncaught_error', 'unhandled_rejection' => 'error', + default => $log['type'] + }, message: self::buildLogMessageFromData($log['data']), context: [ 'url' => $log['url'], @@ -165,22 +170,12 @@ private function registerBrowserLogger(): void private function registerBladeDirectives(BladeCompiler $bladeCompiler): void { - $bladeCompiler->directive('boostJs', fn () => ''); - } - - private static function mapJsTypeToPsr3Level(string $type): string - { - return match ($type) { - 'warn' => 'warning', - 'log', 'table' => 'debug', - 'window_error', 'uncaught_error', 'unhandled_rejection' => 'error', - default => $type - }; + $bladeCompiler->directive('boostJs', fn (): string => ''); } private function hookIntoResponses(Router $router): void { - $this->app->booted(function () use ($router) { + $this->app->booted(function () use ($router): void { $router->pushMiddlewareToGroup('web', InjectBoost::class); }); } diff --git a/src/Concerns/MakesHttpRequests.php b/src/Concerns/MakesHttpRequests.php index 3eb75c0c..cb7139cb 100644 --- a/src/Concerns/MakesHttpRequests.php +++ b/src/Concerns/MakesHttpRequests.php @@ -17,8 +17,8 @@ public function client(): PendingRequest ]); // Disable SSL verification for local development URLs and testing - if (app()->environment(['local', 'testing']) || str_contains(config('boost.hosted.api_url', ''), '.test')) { - $client = $client->withoutVerifying(); + if (app()->environment(['local', 'testing']) || str_contains((string) config('boost.hosted.api_url', ''), '.test')) { + return $client->withoutVerifying(); } return $client; diff --git a/src/Concerns/ReadsLogs.php b/src/Concerns/ReadsLogs.php index b14cf9dd..8aaa4752 100644 --- a/src/Concerns/ReadsLogs.php +++ b/src/Concerns/ReadsLogs.php @@ -34,7 +34,7 @@ private function getChunkSizeStart(): int private function getChunkSizeMax(): int { - return 1 * 1024 * 1024; // 1 MB + return 1024 * 1024; // 1 MB } /** @@ -96,7 +96,7 @@ protected function readLastErrorEntry(string $logFile): ?string for ($i = count($entries) - 1; $i >= 0; $i--) { if ($this->isErrorEntry($entries[$i])) { - return trim($entries[$i]); + return trim((string) $entries[$i]); } } diff --git a/src/Console/ExecuteToolCommand.php b/src/Console/ExecuteToolCommand.php index 5262b0d4..e081526f 100644 --- a/src/Console/ExecuteToolCommand.php +++ b/src/Console/ExecuteToolCommand.php @@ -47,8 +47,8 @@ public function handle(): int try { /** @var Response $response */ $response = $tool->handle($request); // @phpstan-ignore-line - } catch (Throwable $e) { - $errorResult = Response::error("Tool execution failed (E_THROWABLE): {$e->getMessage()}"); + } catch (Throwable $throwable) { + $errorResult = Response::error("Tool execution failed (E_THROWABLE): {$throwable->getMessage()}"); $this->error(json_encode([ 'isError' => true, diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index ff179b56..da09e2ad 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -83,6 +83,7 @@ private function bootstrap(CodeEnvironmentsDetector $codeEnvironmentsDetector, H $this->terminal = $terminal; $this->terminal->initDimensions(); + $this->greenTick = $this->green('✓'); $this->redCross = $this->red('✗'); @@ -162,10 +163,10 @@ private function outro(): void { $label = 'https://boost.laravel.com/installed'; - $ideNames = $this->selectedTargetMcpClient->map(fn (McpClient $mcpClient) => 'i:'.$mcpClient->mcpClientName()) + $ideNames = $this->selectedTargetMcpClient->map(fn (McpClient $mcpClient): string => 'i:'.$mcpClient->mcpClientName()) ->toArray(); - $agentNames = $this->selectedTargetAgents->map(fn (Agent $agent) => 'a:'.$agent->agentName())->toArray(); - $boostFeatures = $this->selectedBoostFeatures->map(fn ($feature) => 'b:'.$feature)->toArray(); + $agentNames = $this->selectedTargetAgents->map(fn (Agent $agent): string => 'a:'.$agent->agentName())->toArray(); + $boostFeatures = $this->selectedBoostFeatures->map(fn ($feature): string => 'b:'.$feature)->toArray(); $guidelines = []; if ($this->shouldInstallAiGuidelines()) { @@ -211,12 +212,12 @@ protected function determineTestEnforcement(bool $ask = true): bool $hasMinimumTests = Str::of($process->getOutput()) ->trim() ->explode("\n") - ->filter(fn ($line) => str_contains($line, '::')) + ->filter(fn ($line): bool => str_contains($line, '::')) ->count() >= self::MIN_TEST_COUNT; } if (! $hasMinimumTests && $ask) { - $hasMinimumTests = select( + return select( label: 'Should AI always create tests?', options: ['Yes', 'No'], default: 'Yes' @@ -320,19 +321,17 @@ private function selectCodeEnvironments(string $contractClass, string $label): C $allEnvironments = $this->codeEnvironmentsDetector->getCodeEnvironments(); $config = $this->getSelectionConfig($contractClass); - $availableEnvironments = $allEnvironments->filter(function (CodeEnvironment $environment) use ($contractClass) { - return $environment instanceof $contractClass; - }); + $availableEnvironments = $allEnvironments->filter(fn (CodeEnvironment $environment): bool => $environment instanceof $contractClass); if ($availableEnvironments->isEmpty()) { return collect(); } - $options = $availableEnvironments->mapWithKeys(function (CodeEnvironment $environment) use ($config) { + $options = $availableEnvironments->mapWithKeys(function (CodeEnvironment $environment) use ($config): array { $displayMethod = $config['displayMethod']; $displayText = $environment->{$displayMethod}(); - return [get_class($environment) => $displayText]; + return [$environment::class => $displayText]; })->sort(); $detectedClasses = []; @@ -342,9 +341,9 @@ private function selectCodeEnvironments(string $contractClass, string $label): C )); foreach ($installedEnvNames as $envKey) { - $matchingEnv = $availableEnvironments->first(fn (CodeEnvironment $env) => strtolower($envKey) === strtolower($env->name())); + $matchingEnv = $availableEnvironments->first(fn (CodeEnvironment $env): bool => strtolower((string) $envKey) === strtolower($env->name())); if ($matchingEnv) { - $detectedClasses[] = get_class($matchingEnv); + $detectedClasses[] = $matchingEnv::class; } } @@ -354,9 +353,9 @@ private function selectCodeEnvironments(string $contractClass, string $label): C default: array_unique($detectedClasses), scroll: $config['scroll'], required: $config['required'], - hint: empty($detectedClasses) ? '' : sprintf('Auto-detected %s for you', + hint: $detectedClasses === [] ? '' : sprintf('Auto-detected %s for you', Arr::join(array_map(function ($className) use ($availableEnvironments, $config) { - $env = $availableEnvironments->first(fn ($env) => get_class($env) === $className); + $env = $availableEnvironments->first(fn ($env): bool => $env::class === $className); $displayMethod = $config['displayMethod']; return $env->{$displayMethod}(); @@ -364,7 +363,7 @@ private function selectCodeEnvironments(string $contractClass, string $label): C ) ))->sort(); - return $selectedClasses->map(fn ($className) => $availableEnvironments->first(fn ($env) => get_class($env) === $className)); + return $selectedClasses->map(fn ($className) => $availableEnvironments->first(fn ($env): bool => $env::class === $className)); } private function installGuidelines(): void @@ -392,7 +391,7 @@ private function installGuidelines(): void $this->info(sprintf(' Adding %d guidelines to your selected agents', $guidelines->count())); DisplayHelper::grid( $guidelines - ->map(fn ($guideline, string $key) => $key.($guideline['custom'] ? '*' : '')) + ->map(fn ($guideline, string $key): string => $key.($guideline['custom'] ? '*' : '')) ->values() ->sort() ->toArray(), @@ -408,7 +407,7 @@ private function installGuidelines(): void /** @var CodeEnvironment $agent */ foreach ($this->selectedTargetAgents as $agent) { $agentName = $agent->agentName(); - $displayAgentName = str_pad($agentName, $longestAgentName); + $displayAgentName = str_pad((string) $agentName, $longestAgentName); $this->output->write(" {$displayAgentName}... "); /** @var Agent $agent */ try { @@ -424,7 +423,7 @@ private function installGuidelines(): void $this->newLine(); - if (count($failed) > 0) { + if ($failed !== []) { $this->error(sprintf('✗ Failed to install guidelines to %d agent%s:', count($failed), count($failed) === 1 ? '' : 's' @@ -466,6 +465,7 @@ private function installMcpServerConfig(): void return; } + $this->newLine(); $this->info(' Installing MCP servers to your selected IDEs'); $this->newLine(); @@ -483,7 +483,7 @@ private function installMcpServerConfig(): void /** @var McpClient $mcpClient */ foreach ($this->selectedTargetMcpClient as $mcpClient) { $ideName = $mcpClient->mcpClientName(); - $ideDisplay = str_pad($ideName, $longestIdeName); + $ideDisplay = str_pad((string) $ideName, $longestIdeName); $this->output->write(" {$ideDisplay}... "); $results = []; @@ -532,7 +532,7 @@ private function installMcpServerConfig(): void $this->newLine(); - if (count($failed) > 0) { + if ($failed !== []) { $this->error(sprintf('%s Some MCP servers failed to install:', $this->redCross)); foreach ($failed as $ideName => $errors) { foreach ($errors as $server => $error) { diff --git a/src/Install/Assists/Inertia.php b/src/Install/Assists/Inertia.php index 8083bddf..b4906361 100644 --- a/src/Install/Assists/Inertia.php +++ b/src/Install/Assists/Inertia.php @@ -13,11 +13,19 @@ public function __construct(private Roster $roster) {} public function gte(string $version): bool { - return - $this->roster->usesVersion(Packages::INERTIA_LARAVEL, $version, '>=') || - $this->roster->usesVersion(Packages::INERTIA_REACT, $version, '>=') || - $this->roster->usesVersion(Packages::INERTIA_SVELTE, $version, '>=') || - $this->roster->usesVersion(Packages::INERTIA_VUE, $version, '>='); + if ($this->roster->usesVersion(Packages::INERTIA_LARAVEL, $version, '>=')) { + return true; + } + + if ($this->roster->usesVersion(Packages::INERTIA_REACT, $version, '>=')) { + return true; + } + + if ($this->roster->usesVersion(Packages::INERTIA_SVELTE, $version, '>=')) { + return true; + } + + return $this->roster->usesVersion(Packages::INERTIA_VUE, $version, '>='); } public function hasFormComponent(): bool diff --git a/src/Install/Cli/DisplayHelper.php b/src/Install/Cli/DisplayHelper.php index 31aca203..09a934c5 100644 --- a/src/Install/Cli/DisplayHelper.php +++ b/src/Install/Cli/DisplayHelper.php @@ -51,12 +51,12 @@ class DisplayHelper */ public static function datatable(array $data, int $maxWidth = 80): void { - if (! $data) { + if ($data === []) { return; } $columnWidths = self::calculateColumnWidths($data); - $columnWidths = array_map(fn ($width) => $width + self::CELL_PADDING, $columnWidths); + $columnWidths = array_map(fn (int $width): int => $width + self::CELL_PADDING, $columnWidths); [$leftChar, $rightChar, $joinChar] = self::getBorderChars(self::BORDER_TOP); echo self::buildBorder($columnWidths, $leftChar, $rightChar, $joinChar).PHP_EOL; @@ -69,6 +69,7 @@ public static function datatable(array $data, int $maxWidth = 80): void [$leftChar, $rightChar, $joinChar] = self::getBorderChars(self::BORDER_MIDDLE); echo self::buildBorder($columnWidths, $leftChar, $rightChar, $joinChar).PHP_EOL; } + $rowCount++; } @@ -81,7 +82,7 @@ public static function datatable(array $data, int $maxWidth = 80): void */ public static function grid(array $items, int $maxWidth = 80): void { - if (empty($items)) { + if ($items === []) { return; } @@ -104,6 +105,7 @@ public static function grid(array $items, int $maxWidth = 80): void [$leftChar, $rightChar, $joinChar] = self::getBorderChars(self::BORDER_MIDDLE); echo self::SPACE.self::buildBorder($cellWidths, $leftChar, $rightChar, $joinChar).PHP_EOL; } + $rowCount++; } @@ -150,9 +152,8 @@ private static function buildBorder(array $widths, string $leftChar, string $rig $border .= $joinChar; } } - $border .= $rightChar; - return $border; + return $border.$rightChar; } /** @@ -181,18 +182,16 @@ private static function buildGridRow(array $row, int $cellWidth, int $cellsPerRo $line = self::UNICODE_VERTICAL; $cells = array_map( - fn ($index) => self::formatGridCell($row[$index] ?? '', $cellWidth), + fn (int $index): string => self::formatGridCell($row[$index] ?? '', $cellWidth), range(0, $cellsPerRow - 1) ); - $line .= implode(self::UNICODE_VERTICAL, $cells).self::UNICODE_VERTICAL; - - return $line; + return $line.(implode(self::UNICODE_VERTICAL, $cells).self::UNICODE_VERTICAL); } private static function formatGridCell(string $item, int $cellWidth): string { - if (! $item) { + if ($item === '' || $item === '0') { return str_repeat(self::SPACE, $cellWidth); } diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index 9f212a24..782b028d 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -160,13 +160,16 @@ protected function installShellMcp(string $key, string $command, array $args = [ ], [ $key, $command, - implode(' ', array_map(fn ($arg) => '"'.$arg.'"', $args)), + implode(' ', array_map(fn (string $arg): string => '"'.$arg.'"', $args)), trim($envString), ], $shellCommand); $result = Process::run($command); + if ($result->successful()) { + return true; + } - return $result->successful() || str_contains($result->errorOutput(), 'already exists'); + return str_contains($result->errorOutput(), 'already exists'); } /** diff --git a/src/Install/CodeEnvironmentsDetector.php b/src/Install/CodeEnvironmentsDetector.php index f99a46f2..ad446e61 100644 --- a/src/Install/CodeEnvironmentsDetector.php +++ b/src/Install/CodeEnvironmentsDetector.php @@ -41,8 +41,8 @@ public function discoverSystemInstalledCodeEnvironments(): array $platform = Platform::current(); return $this->getCodeEnvironments() - ->filter(fn (CodeEnvironment $program) => $program->detectOnSystem($platform)) - ->map(fn (CodeEnvironment $program) => $program->name()) + ->filter(fn (CodeEnvironment $program): bool => $program->detectOnSystem($platform)) + ->map(fn (CodeEnvironment $program): string => $program->name()) ->values() ->toArray(); } @@ -55,8 +55,8 @@ public function discoverSystemInstalledCodeEnvironments(): array public function discoverProjectInstalledCodeEnvironments(string $basePath): array { return $this->getCodeEnvironments() - ->filter(fn ($program) => $program->detectInProject($basePath)) - ->map(fn ($program) => $program->name()) + ->filter(fn ($program): bool => $program->detectInProject($basePath)) + ->map(fn ($program): string => $program->name()) ->values() ->toArray(); } diff --git a/src/Install/Detection/DetectionStrategyFactory.php b/src/Install/Detection/DetectionStrategyFactory.php index 28ae0e4e..7afdfd56 100644 --- a/src/Install/Detection/DetectionStrategyFactory.php +++ b/src/Install/Detection/DetectionStrategyFactory.php @@ -22,7 +22,7 @@ public function make(string|array $type, array $config = []): DetectionStrategy { if (is_array($type)) { return new CompositeDetectionStrategy( - array_map(fn ($singleType) => $this->make($singleType, $config), $type) + array_map(fn ($singleType): \Laravel\Boost\Install\Contracts\DetectionStrategy => $this->make($singleType, $config), $type) ); } diff --git a/src/Install/Detection/DirectoryDetectionStrategy.php b/src/Install/Detection/DirectoryDetectionStrategy.php index 6c8ec184..ccb9029d 100644 --- a/src/Install/Detection/DirectoryDetectionStrategy.php +++ b/src/Install/Detection/DirectoryDetectionStrategy.php @@ -41,9 +41,7 @@ public function detect(array $config, ?Platform $platform = null): bool private function expandPath(string $path, ?Platform $platform = null): string { if ($platform === Platform::Windows) { - return preg_replace_callback('/%([^%]+)%/', function ($matches) { - return getenv($matches[1]) ?: $matches[0]; - }, $path); + return preg_replace_callback('/%([^%]+)%/', fn (array $matches) => getenv($matches[1]) ?: $matches[0], $path); } if (str_starts_with($path, '~')) { diff --git a/src/Install/GuidelineAssist.php b/src/Install/GuidelineAssist.php index 193a9897..7ee2a55f 100644 --- a/src/Install/GuidelineAssist.php +++ b/src/Install/GuidelineAssist.php @@ -25,8 +25,8 @@ class GuidelineAssist public function __construct(public Roster $roster) { - $this->modelPaths = $this->discover(fn ($reflection) => ($reflection->isSubclassOf(Model::class) && ! $reflection->isAbstract())); - $this->controllerPaths = $this->discover(fn (ReflectionClass $reflection) => (stripos($reflection->getName(), 'controller') !== false || stripos($reflection->getNamespaceName(), 'controller') !== false)); + $this->modelPaths = $this->discover(fn ($reflection): bool => ($reflection->isSubclassOf(Model::class) && ! $reflection->isAbstract())); + $this->controllerPaths = $this->discover(fn (ReflectionClass $reflection): bool => (stripos($reflection->getName(), 'controller') !== false || stripos($reflection->getNamespaceName(), 'controller') !== false)); $this->enumPaths = $this->discover(fn ($reflection) => $reflection->isEnum()); } @@ -68,7 +68,7 @@ private function discover(callable $cb): array return ['app-path-isnt-a-directory' => $appPath]; } - if (empty(self::$classes)) { + if (self::$classes === []) { $finder = Finder::create() ->in($appPath) ->files() @@ -130,10 +130,8 @@ public function fileHasClassLike(string $path): bool $tokens = token_get_all($code); foreach ($tokens as $token) { - if (is_array($token)) { - if (in_array($token[0], [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true)) { - return $cache[$path] = true; - } + if (is_array($token) && in_array($token[0], [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true)) { + return $cache[$path] = true; } } @@ -142,7 +140,7 @@ public function fileHasClassLike(string $path): bool public function shouldEnforceStrictTypes(): bool { - if (empty($this->modelPaths)) { + if ($this->modelPaths === []) { return false; } @@ -154,7 +152,7 @@ public function shouldEnforceStrictTypes(): bool public function enumContents(): string { - if (empty($this->enumPaths)) { + if ($this->enumPaths === []) { return ''; } diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index c28130f8..eb5bdecd 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -68,8 +68,8 @@ public function customGuidelinePath(string $path = ''): string public static function composeGuidelines(Collection $guidelines): string { return str_replace("\n\n\n\n", "\n\n", trim($guidelines - ->filter(fn ($guideline) => ! empty(trim($guideline['content']))) - ->map(fn ($guideline, $key) => "\n=== {$key} rules ===\n\n".trim($guideline['content'])) + ->filter(fn ($guideline): bool => ! empty(trim($guideline['content']))) + ->map(fn ($guideline, $key): string => "\n=== {$key} rules ===\n\n".trim($guideline['content'])) ->join("\n\n")) ); } @@ -110,7 +110,7 @@ protected function find(): Collection // $phpMajorMinor = PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION; // $guidelines->put('php/v'.$phpMajorMinor, $this->guidelinesDir('php/'.$phpMajorMinor)); - if (str_contains(config('app.url'), '.test') && $this->herd->isInstalled()) { + if (str_contains((string) config('app.url'), '.test') && $this->herd->isInstalled()) { $guidelines->put('herd', $this->guideline('herd/core')); } @@ -162,11 +162,12 @@ protected function find(): Collection if ($pathsUsed->contains($guideline['path'])) { continue; // Don't include this twice if it's an override } + $guidelines->put('.ai/'.$guideline['name'], $guideline); } return $guidelines - ->where(fn (array $guideline) => ! empty(trim($guideline['content']))); + ->where(fn (array $guideline): bool => ! empty(trim((string) $guideline['content']))); } /** @@ -200,11 +201,11 @@ protected function guidelinesDir(string $dirPath): array ->files() ->in($dirPath) ->name('*.blade.php'); - } catch (DirectoryNotFoundException $e) { + } catch (DirectoryNotFoundException) { return []; } - return array_map(fn ($file) => $this->guideline($file->getRealPath()), iterator_to_array($finder)); + return array_map(fn (\Symfony\Component\Finder\SplFileInfo $file): array => $this->guideline($file->getRealPath()), iterator_to_array($finder)); } /** @@ -233,6 +234,7 @@ protected function guideline(string $path): array ]); $rendered = str_replace(array_values($placeholders), array_keys($placeholders), $rendered); $rendered = str_replace(array_keys($this->storedSnippets), array_values($this->storedSnippets), $rendered); + $this->storedSnippets = []; // Clear for next use return [ @@ -247,9 +249,9 @@ protected function guideline(string $path): array private function processBoostSnippets(string $content): string { - return preg_replace_callback('/(?[\'"])(?P[^\1]*?)\1(?:\s*,\s*(?P[\'"])(?P[^\3]*?)\3)?\s*\)(?P.*?)@endboostsnippet/s', function ($matches) { + return preg_replace_callback('/(?[\'"])(?P[^\1]*?)\1(?:\s*,\s*(?P[\'"])(?P[^\3]*?)\3)?\s*\)(?P.*?)@endboostsnippet/s', function ($matches): string { $name = $matches['name']; - $lang = ! empty($matches['lang']) ? $matches['lang'] : 'html'; + $lang = empty($matches['lang']) ? 'html' : $matches['lang']; $snippetContent = $matches['content']; $placeholder = '___BOOST_SNIPPET_'.count($this->storedSnippets).'___'; @@ -263,17 +265,15 @@ private function processBoostSnippets(string $content): string protected function prependPackageGuidelinePath(string $path): string { $path = preg_replace('/\.blade\.php$/', '', $path); - $path = str_replace('/', DIRECTORY_SEPARATOR, __DIR__.'/../../.ai/'.$path.'.blade.php'); - return $path; + return str_replace('/', DIRECTORY_SEPARATOR, __DIR__.'/../../.ai/'.$path.'.blade.php'); } protected function prependUserGuidelinePath(string $path): string { $path = preg_replace('/\.blade\.php$/', '', $path); - $path = str_replace('/', DIRECTORY_SEPARATOR, $this->customGuidelinePath($path.'.blade.php')); - return $path; + return str_replace('/', DIRECTORY_SEPARATOR, $this->customGuidelinePath($path.'.blade.php')); } protected function guidelinePath(string $path): ?string diff --git a/src/Install/GuidelineWriter.php b/src/Install/GuidelineWriter.php index 699faeab..52078668 100644 --- a/src/Install/GuidelineWriter.php +++ b/src/Install/GuidelineWriter.php @@ -31,10 +31,8 @@ public function write(string $guidelines): int $filePath = $this->agent->guidelinesPath(); $directory = dirname($filePath); - if (! is_dir($directory)) { - if (! mkdir($directory, 0755, true)) { - throw new RuntimeException("Failed to create directory: {$directory}"); - } + if (! is_dir($directory) && ! mkdir($directory, 0755, true)) { + throw new RuntimeException("Failed to create directory: {$directory}"); } $handle = fopen($filePath, 'c+'); @@ -74,7 +72,7 @@ public function write(string $guidelines): int throw new RuntimeException("Failed to reset file pointer: {$filePath}"); } - if (fwrite($handle, $newContent) === false) { + if (fwrite($handle, (string) $newContent) === false) { throw new RuntimeException("Failed to write to file: {$filePath}"); } @@ -103,7 +101,7 @@ private function acquireLockWithRetry(mixed $handle, string $filePath, int $maxR } // Exponential backoff with jitter - $jitter = rand(0, (int) ($delay * 0.1)); + $jitter = random_int(0, (int) ($delay * 0.1)); usleep($delay + $jitter); $delay *= 2; } diff --git a/src/Install/Mcp/FileWriter.php b/src/Install/Mcp/FileWriter.php index 65b4b5f4..c538afff 100644 --- a/src/Install/Mcp/FileWriter.php +++ b/src/Install/Mcp/FileWriter.php @@ -9,18 +9,13 @@ class FileWriter { - protected string $filePath; - protected string $configKey = 'mcpServers'; protected array $serversToAdd = []; protected int $defaultIndentation = 8; - public function __construct(string $filePath) - { - $this->filePath = $filePath; - } + public function __construct(protected string $filePath) {} public function configKey(string $key): self { @@ -80,9 +75,9 @@ protected function updateJson5File(string $content): bool if (preg_match($configKeyPattern, $content, $matches, PREG_OFFSET_CAPTURE)) { return $this->injectIntoExistingConfigKey($content, $matches); - } else { - return $this->injectNewConfigKey($content); } + + return $this->injectNewConfigKey($content); } protected function injectIntoExistingConfigKey(string $content, array $matches): bool @@ -105,7 +100,7 @@ protected function injectIntoExistingConfigKey(string $content, array $matches): // Filter out servers that already exist $serversToAdd = $this->filterExistingServers($content, $openBracePos, $closeBracePos); - if (empty($serversToAdd)) { + if ($serversToAdd === []) { return true; } @@ -116,6 +111,7 @@ protected function injectIntoExistingConfigKey(string $content, array $matches): foreach ($serversToAdd as $key => $serverConfig) { $serverJsonParts[] = $this->generateServerJson($key, $serverConfig, $indentLength); } + $serversJson = implode(','."\n", $serverJsonParts); // Check if we need a comma and add it to the preceding content @@ -201,7 +197,7 @@ protected function generateServerJson(string $key, array $serverConfig, int $bas $firstLine = array_shift($lines); $indentedLines = [ "{$baseIndent}\"{$key}\": {$firstLine}", - ...array_map(fn ($line) => $baseIndent.$line, $lines), + ...array_map(fn (string $line): string => $baseIndent.$line, $lines), ]; return "\n".implode("\n", $indentedLines); @@ -261,11 +257,7 @@ protected function needsCommaBeforeClosingBrace(string $content, int $openBraceP } // If ends with comma, no additional comma needed - if (Str::endsWith($trimmed, ',')) { - return false; - } - - return true; + return ! Str::endsWith($trimmed, ','); } protected function findCommaInsertionPoint(string $content, int $openBracePos, int $closeBracePos): int @@ -291,10 +283,10 @@ protected function findCommaInsertionPoint(string $content, int $openBracePos, i // Found last meaningful character, comma goes after it if ($char !== ',') { return $i + 1; - } else { - // Already has comma, no insertion needed - return -1; } + + // Already has comma, no insertion needed + return -1; } // Fallback - insert right after opening brace @@ -403,7 +395,12 @@ protected function fileExists(): bool protected function shouldWriteNew(): bool { - return ! $this->fileExists() || File::size($this->filePath) < 3; // To account for files that are just `{}` + if (! $this->fileExists()) { + return true; + } + + return File::size($this->filePath) < 3; + // To account for files that are just `{}` } protected function readFile(): string diff --git a/src/Mcp/Boost.php b/src/Mcp/Boost.php index 38fa8d56..5270518d 100644 --- a/src/Mcp/Boost.php +++ b/src/Mcp/Boost.php @@ -54,11 +54,11 @@ class Boost extends Server */ protected array $prompts = []; - public function boot(): void + protected function boot(): void { - collect($this->discoverTools())->each(fn (string $tool) => $this->tools[] = $tool); - collect($this->discoverResources())->each(fn (string $resource) => $this->resources[] = $resource); - collect($this->discoverPrompts())->each(fn (string $prompt) => $this->prompts[] = $prompt); + collect($this->discoverTools())->each(fn (string $tool): string => $this->tools[] = $tool); + collect($this->discoverResources())->each(fn (string $resource): string => $this->resources[] = $resource); + collect($this->discoverPrompts())->each(fn (string $prompt): string => $this->prompts[] = $prompt); // Override the tools/call method to use our ToolExecutor $this->methods['tools/call'] = CallToolWithExecutor::class; diff --git a/src/Mcp/Methods/CallToolWithExecutor.php b/src/Mcp/Methods/CallToolWithExecutor.php index afe17d26..9db398a5 100644 --- a/src/Mcp/Methods/CallToolWithExecutor.php +++ b/src/Mcp/Methods/CallToolWithExecutor.php @@ -53,12 +53,12 @@ public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpc } try { - $response = $this->executor->execute(get_class($tool), $arguments); - } catch (Throwable $e) { - $response = Response::error('Tool execution error: '.$e->getMessage()); + $response = $this->executor->execute($tool::class, $arguments); + } catch (Throwable $throwable) { + $response = Response::error('Tool execution error: '.$throwable->getMessage()); } - return $this->toJsonRpcResponse($request, $response, fn ($responses) => [ + return $this->toJsonRpcResponse($request, $response, fn ($responses): array => [ 'content' => $responses->map(fn ($response) => $response->content()->toTool($tool))->all(), 'isError' => $responses->contains(fn ($response) => $response->isError()), ]); diff --git a/src/Mcp/ToolExecutor.php b/src/Mcp/ToolExecutor.php index 73692bcc..06076484 100644 --- a/src/Mcp/ToolExecutor.php +++ b/src/Mcp/ToolExecutor.php @@ -13,8 +13,6 @@ class ToolExecutor { - public function __construct() {} - public function execute(string $toolClass, array $arguments = []): Response { if (! ToolRegistry::isToolAllowed($toolClass)) { @@ -54,12 +52,12 @@ protected function executeInSubprocess(string $toolClass, array $arguments): Res } return $this->reconstructResponse($decoded); - } catch (ProcessTimedOutException $e) { + } catch (ProcessTimedOutException) { $process->stop(); return Response::error("Tool execution timed out after {$this->getTimeout($arguments)} seconds"); - } catch (ProcessFailedException $e) { + } catch (ProcessFailedException) { $errorOutput = $process->getErrorOutput().$process->getOutput(); return Response::error("Process tool execution failed: {$errorOutput}"); @@ -104,7 +102,7 @@ protected function reconstructResponse(array $data): Response if (is_array($firstContent)) { $text = $firstContent['text'] ?? ''; - $decoded = json_decode($text, true); + $decoded = json_decode((string) $text, true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { return Response::json($decoded); } diff --git a/src/Mcp/Tools/ApplicationInfo.php b/src/Mcp/Tools/ApplicationInfo.php index 01cd2f7f..57356b01 100644 --- a/src/Mcp/Tools/ApplicationInfo.php +++ b/src/Mcp/Tools/ApplicationInfo.php @@ -31,7 +31,7 @@ public function handle(Request $request): Response 'php_version' => PHP_VERSION, 'laravel_version' => app()->version(), 'database_engine' => config('database.default'), - 'packages' => $this->roster->packages()->map(fn (Package $package) => ['roster_name' => $package->name(), 'version' => $package->version(), 'package_name' => $package->rawName()]), + 'packages' => $this->roster->packages()->map(fn (Package $package): array => ['roster_name' => $package->name(), 'version' => $package->version(), 'package_name' => $package->rawName()]), 'models' => array_keys($this->guidelineAssist->models()), ]); } diff --git a/src/Mcp/Tools/DatabaseQuery.php b/src/Mcp/Tools/DatabaseQuery.php index 93e8eb0a..1f954037 100644 --- a/src/Mcp/Tools/DatabaseQuery.php +++ b/src/Mcp/Tools/DatabaseQuery.php @@ -46,6 +46,7 @@ public function handle(Request $request): Response if (! $token) { return Response::error('Please pass a valid query'); } + $firstWord = strtoupper($token); // Allowed read-only commands. @@ -63,10 +64,8 @@ public function handle(Request $request): Response $isReadOnly = in_array($firstWord, $allowList, true); // Additional validation for WITH … SELECT. - if ($firstWord === 'WITH') { - if (! preg_match('/with\s+.*select\b/i', $query)) { - $isReadOnly = false; - } + if ($firstWord === 'WITH' && ! preg_match('/with\s+.*select\b/i', $query)) { + $isReadOnly = false; } if (! $isReadOnly) { @@ -79,8 +78,8 @@ public function handle(Request $request): Response return Response::json( DB::connection($connectionName)->select($query) ); - } catch (Throwable $e) { - return Response::error('Query failed: '.$e->getMessage()); + } catch (Throwable $throwable) { + return Response::error('Query failed: '.$throwable->getMessage()); } } } diff --git a/src/Mcp/Tools/DatabaseSchema.php b/src/Mcp/Tools/DatabaseSchema.php index 124f3462..2069f93b 100644 --- a/src/Mcp/Tools/DatabaseSchema.php +++ b/src/Mcp/Tools/DatabaseSchema.php @@ -34,7 +34,7 @@ public function schema(JsonSchema $schema): array { return [ 'database' => $schema->string() - ->description('Name of the database connection to dump (defaults to app\'s default connection, often not needed)'), + ->description("Name of the database connection to dump (defaults to app's default connection, often not needed)"), 'filter' => $schema->string() ->description('Filter the tables by name'), ]; @@ -49,9 +49,7 @@ public function handle(Request $request): Response $filter = $request->get('filter') ?? ''; $cacheKey = "boost:mcp:database-schema:{$connection}:{$filter}"; - $schema = Cache::remember($cacheKey, 20, function () use ($connection, $filter) { - return $this->getDatabaseStructure($connection, $filter); - }); + $schema = Cache::remember($cacheKey, 20, fn (): array => $this->getDatabaseStructure($connection, $filter)); return Response::json($schema); } @@ -72,7 +70,7 @@ protected function getAllTablesStructure(?string $connection, string $filter = ' foreach ($this->getAllTables($connection) as $table) { $tableName = $table['name']; - if ($filter && ! str_contains(strtolower($tableName), strtolower($filter))) { + if ($filter && ! str_contains(strtolower((string) $tableName), strtolower($filter))) { continue; } @@ -105,14 +103,14 @@ protected function getTableStructure(?string $connection, string $tableName): ar 'triggers' => $triggers, 'check_constraints' => $checkConstraints, ]; - } catch (Exception $e) { + } catch (Exception $exception) { Log::error('Failed to get table structure for: '.$tableName, [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), + 'error' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), ]); return [ - 'error' => 'Failed to get structure: '.$e->getMessage(), + 'error' => 'Failed to get structure: '.$exception->getMessage(), ]; } } diff --git a/src/Mcp/Tools/DatabaseSchema/DatabaseSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/DatabaseSchemaDriver.php index 07f4d688..abd2550c 100644 --- a/src/Mcp/Tools/DatabaseSchema/DatabaseSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/DatabaseSchemaDriver.php @@ -6,12 +6,7 @@ abstract class DatabaseSchemaDriver { - protected $connection; - - public function __construct($connection = null) - { - $this->connection = $connection; - } + public function __construct(protected $connection = null) {} abstract public function getViews(): array; diff --git a/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php index 8ec339f9..f0d732b5 100644 --- a/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php @@ -43,7 +43,7 @@ public function getFunctions(): array public function getTriggers(?string $table = null): array { try { - if ($table) { + if ($table !== null && $table !== '' && $table !== '0') { return DB::connection($this->connection)->select('SHOW TRIGGERS WHERE `Table` = ?', [$table]); } diff --git a/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php index 32147416..75bff37d 100644 --- a/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php @@ -60,7 +60,7 @@ public function getTriggers(?string $table = null): array FROM information_schema.triggers WHERE trigger_schema = current_schema() '; - if ($table) { + if ($table !== null && $table !== '' && $table !== '0') { $sql .= ' AND event_object_table = ?'; return DB::connection($this->connection)->select($sql, [$table]); diff --git a/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php index 8b5ebde8..d1c2752c 100644 --- a/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php @@ -36,7 +36,7 @@ public function getTriggers(?string $table = null): array { try { $sql = "SELECT name, sql FROM sqlite_master WHERE type = 'trigger'"; - if ($table) { + if ($table !== null && $table !== '' && $table !== '0') { $sql .= ' AND tbl_name = ?'; return DB::connection($this->connection)->select($sql, [$table]); diff --git a/src/Mcp/Tools/LastError.php b/src/Mcp/Tools/LastError.php index 086f319c..b3a91ddb 100644 --- a/src/Mcp/Tools/LastError.php +++ b/src/Mcp/Tools/LastError.php @@ -27,7 +27,7 @@ public function __construct() { // Register the listener only once per PHP process. if (! self::$listenerRegistered) { - Log::listen(function (MessageLogged $event) { + Log::listen(function (MessageLogged $event): void { if ($event->level === 'error') { Cache::forever('boost:last_error', [ 'timestamp' => now()->toDateTimeString(), diff --git a/src/Mcp/Tools/ListArtisanCommands.php b/src/Mcp/Tools/ListArtisanCommands.php index fdee0b79..c4740c5f 100644 --- a/src/Mcp/Tools/ListArtisanCommands.php +++ b/src/Mcp/Tools/ListArtisanCommands.php @@ -36,7 +36,7 @@ public function handle(Request $request): Response } // Sort alphabetically by name for determinism. - usort($commandList, fn ($firstCommand, $secondCommand) => strcmp($firstCommand['name'], $secondCommand['name'])); + usort($commandList, fn (array $firstCommand, array $secondCommand): int => strcmp((string) $firstCommand['name'], (string) $secondCommand['name'])); return Response::json($commandList); } diff --git a/src/Mcp/Tools/ListAvailableConfigKeys.php b/src/Mcp/Tools/ListAvailableConfigKeys.php index da167640..2c3f0a8f 100644 --- a/src/Mcp/Tools/ListAvailableConfigKeys.php +++ b/src/Mcp/Tools/ListAvailableConfigKeys.php @@ -50,6 +50,7 @@ private function flattenToDotNotation(array $array, string $prefix = ''): array if ($prefix === '' && is_numeric($key)) { continue; } + $results[] = $currentKey; } } diff --git a/src/Mcp/Tools/ReadLogEntries.php b/src/Mcp/Tools/ReadLogEntries.php index 21eee16f..38e6b946 100644 --- a/src/Mcp/Tools/ReadLogEntries.php +++ b/src/Mcp/Tools/ReadLogEntries.php @@ -58,6 +58,7 @@ public function handle(Request $request): Response if ($entries === []) { return Response::text('Unable to retrieve log entries, or no entries yet.'); } + $logs = implode("\n\n", $entries); if (empty(trim($logs))) { diff --git a/src/Mcp/Tools/ReportFeedback.php b/src/Mcp/Tools/ReportFeedback.php index cde60426..6a238e9a 100644 --- a/src/Mcp/Tools/ReportFeedback.php +++ b/src/Mcp/Tools/ReportFeedback.php @@ -40,7 +40,7 @@ public function handle(Request $request): Response|Generator $apiUrl = config('boost.hosted.api_url', 'https://boost.laravel.com').'/api/feedback'; $feedback = $request->get('feedback'); - if (empty($feedback) || strlen($feedback) < 10) { + if (empty($feedback) || strlen((string) $feedback) < 10) { return Response::error('Feedback too short'); } diff --git a/src/Mcp/Tools/SearchDocs.php b/src/Mcp/Tools/SearchDocs.php index 08d4d714..682a47de 100644 --- a/src/Mcp/Tools/SearchDocs.php +++ b/src/Mcp/Tools/SearchDocs.php @@ -23,7 +23,7 @@ public function __construct(protected Roster $roster) {} /** * The tool's description. */ - protected string $description = 'Search for up-to-date version-specific documentation related to this project and its packages. This tool will search Laravel hosted documentation based on the packages installed and is perfect for all Laravel ecosystem packages. Laravel, Inertia, Pest, Livewire, Filament, Nova, Tailwind, and more. You must use this tool to search for Laravel-ecosystem docs before using other approaches. The results provided are for this project\'s package version and does not cover all versions of the package.'; + protected string $description = "Search for up-to-date version-specific documentation related to this project and its packages. This tool will search Laravel hosted documentation based on the packages installed and is perfect for all Laravel ecosystem packages. Laravel, Inertia, Pest, Livewire, Filament, Nova, Tailwind, and more. You must use this tool to search for Laravel-ecosystem docs before using other approaches. The results provided are for this project's package version and does not cover all versions of the package."; /** * Get the tool's input schema. @@ -55,7 +55,7 @@ public function handle(Request $request): Response|Generator $queries = array_filter( array_map('trim', $request->get('queries')), - fn ($query) => $query !== '' && $query !== '*' + fn (string $query): bool => $query !== '' && $query !== '*' ); try { @@ -63,10 +63,10 @@ public function handle(Request $request): Response|Generator // Only search in specific packages if ($packagesFilter) { - $packagesCollection = $packagesCollection->filter(fn (Package $package) => in_array($package->rawName(), $packagesFilter, true)); + $packagesCollection = $packagesCollection->filter(fn (Package $package): bool => in_array($package->rawName(), $packagesFilter, true)); } - $packages = $packagesCollection->map(function (Package $package) { + $packages = $packagesCollection->map(function (Package $package): array { $name = $package->rawName(); $version = $package->majorVersion().'.x'; @@ -77,8 +77,8 @@ public function handle(Request $request): Response|Generator }); $packages = $packages->values()->toArray(); - } catch (Throwable $e) { - return Response::error('Failed to get packages: '.$e->getMessage()); + } catch (Throwable $throwable) { + return Response::error('Failed to get packages: '.$throwable->getMessage()); } $tokenLimit = $request->get('token_limit') ?? 10000; @@ -97,8 +97,8 @@ public function handle(Request $request): Response|Generator if (! $response->successful()) { return Response::error('Failed to search documentation: '.$response->body()); } - } catch (Throwable $e) { - return Response::error('HTTP request failed: '.$e->getMessage()); + } catch (Throwable $throwable) { + return Response::error('HTTP request failed: '.$throwable->getMessage()); } return Response::text($response->body()); diff --git a/src/Mcp/Tools/Tinker.php b/src/Mcp/Tools/Tinker.php index 23c55372..f90237a7 100644 --- a/src/Mcp/Tools/Tinker.php +++ b/src/Mcp/Tools/Tinker.php @@ -61,16 +61,16 @@ public function handle(Request $request): Response // If a result is an object, include the class name if (is_object($result)) { - $response['class'] = get_class($result); + $response['class'] = $result::class; } return Response::json($response); - } catch (Throwable $e) { + } catch (Throwable $throwable) { return Response::json([ - 'error' => $e->getMessage(), - 'type' => get_class($e), - 'file' => $e->getFile(), - 'line' => $e->getLine(), + 'error' => $throwable->getMessage(), + 'type' => $throwable::class, + 'file' => $throwable->getFile(), + 'line' => $throwable->getLine(), ]); } finally { diff --git a/src/Middleware/InjectBoost.php b/src/Middleware/InjectBoost.php index 5e8201d9..89ebad9c 100644 --- a/src/Middleware/InjectBoost.php +++ b/src/Middleware/InjectBoost.php @@ -48,7 +48,7 @@ private function shouldInject(Response $response): bool } } - if (! str_contains($response->headers->get('content-type', ''), 'html')) { + if (! str_contains((string) $response->headers->get('content-type', ''), 'html')) { return false; } @@ -59,11 +59,7 @@ private function shouldInject(Response $response): bool } // Check if already injected - if (str_contains($content, 'browser-logger-active')) { - return false; - } - - return true; + return ! str_contains($content, 'browser-logger-active'); } private function injectScript(string $content): string diff --git a/tests/ArchTest.php b/tests/ArchTest.php index 1589ae44..17cbd911 100644 --- a/tests/ArchTest.php +++ b/tests/ArchTest.php @@ -12,14 +12,14 @@ arch('commands') ->expect('Laravel\Boost\Commands') - ->toExtend('Illuminate\Console\Command') + ->toExtend(\Illuminate\Console\Command::class) ->toHaveSuffix('Command'); arch('no direct env calls') ->expect('env') ->not->toBeUsedIn('Laravel\Boost') ->ignoring([ - 'Laravel\Boost\BoostServiceProvider', + \Laravel\Boost\BoostServiceProvider::class, ]); arch('tests') diff --git a/tests/Feature/BoostServiceProviderTest.php b/tests/Feature/BoostServiceProviderTest.php index 61beeef0..c852de12 100644 --- a/tests/Feature/BoostServiceProviderTest.php +++ b/tests/Feature/BoostServiceProviderTest.php @@ -5,15 +5,15 @@ use Illuminate\Support\Facades\Config; use Laravel\Boost\BoostServiceProvider; -beforeEach(function () { +beforeEach(function (): void { $this->refreshApplication(); Config::set('logging.channels.browser', null); }); -describe('boost.enabled configuration', function () { - it('does not boot boost when disabled', function () { +describe('boost.enabled configuration', function (): void { + it('does not boot boost when disabled', function (): void { Config::set('boost.enabled', false); - app()->detectEnvironment(fn () => 'local'); + app()->detectEnvironment(fn (): string => 'local'); $provider = new BoostServiceProvider(app()); $provider->register(); @@ -22,9 +22,9 @@ $this->artisan('list')->expectsOutputToContain('boost:install'); }); - it('boots boost when enabled in local environment', function () { + it('boots boost when enabled in local environment', function (): void { Config::set('boost.enabled', true); - app()->detectEnvironment(fn () => 'local'); + app()->detectEnvironment(fn (): string => 'local'); $provider = new BoostServiceProvider(app()); $provider->register(); @@ -35,11 +35,11 @@ }); }); -describe('environment restrictions', function () { - it('does not boot boost in production even when enabled', function () { +describe('environment restrictions', function (): void { + it('does not boot boost in production even when enabled', function (): void { Config::set('boost.enabled', true); Config::set('app.debug', false); - app()->detectEnvironment(fn () => 'production'); + app()->detectEnvironment(fn (): string => 'production'); $provider = new BoostServiceProvider(app()); $provider->register(); @@ -48,11 +48,11 @@ expect(config('logging.channels.browser'))->toBeNull(); }); - describe('testing environment', function () { - it('does not boot boost when debug is false', function () { + describe('testing environment', function (): void { + it('does not boot boost when debug is false', function (): void { Config::set('boost.enabled', true); Config::set('app.debug', false); - app()->detectEnvironment(fn () => 'testing'); + app()->detectEnvironment(fn (): string => 'testing'); $provider = new BoostServiceProvider(app()); $provider->register(); @@ -61,10 +61,10 @@ expect(config('logging.channels.browser'))->toBeNull(); }); - it('does not boot boost when debug is true', function () { + it('does not boot boost when debug is true', function (): void { Config::set('boost.enabled', true); Config::set('app.debug', true); - app()->detectEnvironment(fn () => 'testing'); + app()->detectEnvironment(fn (): string => 'testing'); $provider = new BoostServiceProvider(app()); $provider->register(); diff --git a/tests/Feature/Console/InstallCommandMultiselectTest.php b/tests/Feature/Console/InstallCommandMultiselectTest.php index 9ad7d53d..ce6630a7 100644 --- a/tests/Feature/Console/InstallCommandMultiselectTest.php +++ b/tests/Feature/Console/InstallCommandMultiselectTest.php @@ -5,7 +5,7 @@ use Laravel\Prompts\Key; use Laravel\Prompts\Prompt; -test('multiselect returns keys for associative array', function () { +test('multiselect returns keys for associative array', function (): void { // Mock the prompt to simulate user selecting options // Note: mcp_server is already selected by default, so we don't toggle it Prompt::fake([ @@ -33,7 +33,7 @@ expect($result)->not->toContain('Package AI Guidelines'); })->skipOnWindows(); -test('multiselect returns values for indexed array', function () { +test('multiselect returns values for indexed array', function (): void { Prompt::fake([ Key::SPACE, // Select first option Key::DOWN, // Move to second option @@ -53,7 +53,7 @@ expect($result)->toContain('Option 2'); })->skipOnWindows(); -test('multiselect behavior matches install command expectations', function () { +test('multiselect behavior matches install command expectations', function (): void { // Test the exact same structure used in InstallCommand::selectBoostFeatures() // Note: mcp_server and ai_guidelines are already selected by default Prompt::fake([ diff --git a/tests/Feature/Install/CodeEnvironment/CodeEnvironmentPathResolutionTest.php b/tests/Feature/Install/CodeEnvironment/CodeEnvironmentPathResolutionTest.php index 0ff07a81..aa7c209e 100644 --- a/tests/Feature/Install/CodeEnvironment/CodeEnvironmentPathResolutionTest.php +++ b/tests/Feature/Install/CodeEnvironment/CodeEnvironmentPathResolutionTest.php @@ -6,14 +6,14 @@ use Laravel\Boost\Install\CodeEnvironment\PhpStorm; use Laravel\Boost\Install\Detection\DetectionStrategyFactory; -test('PhpStorm returns absolute PHP_BINARY path', function () { +test('PhpStorm returns absolute PHP_BINARY path', function (): void { $strategyFactory = Mockery::mock(DetectionStrategyFactory::class); $phpStorm = new PhpStorm($strategyFactory); expect($phpStorm->getPhpPath())->toBe(PHP_BINARY); }); -test('PhpStorm returns absolute artisan path', function () { +test('PhpStorm returns absolute artisan path', function (): void { $strategyFactory = Mockery::mock(DetectionStrategyFactory::class); $phpStorm = new PhpStorm($strategyFactory); @@ -24,14 +24,14 @@ ->and($artisanPath)->not()->toBe('artisan'); }); -test('Cursor returns relative php string', function () { +test('Cursor returns relative php string', function (): void { $strategyFactory = Mockery::mock(DetectionStrategyFactory::class); $cursor = new Cursor($strategyFactory); expect($cursor->getPhpPath())->toBe('php'); }); -test('Cursor returns relative artisan path', function () { +test('Cursor returns relative artisan path', function (): void { $strategyFactory = Mockery::mock(DetectionStrategyFactory::class); $cursor = new Cursor($strategyFactory); diff --git a/tests/Feature/Install/Detection/CommandDetectionStrategyTest.php b/tests/Feature/Install/Detection/CommandDetectionStrategyTest.php index 1dbb0bed..501082b3 100644 --- a/tests/Feature/Install/Detection/CommandDetectionStrategyTest.php +++ b/tests/Feature/Install/Detection/CommandDetectionStrategyTest.php @@ -6,11 +6,11 @@ use Laravel\Boost\Install\Detection\CommandDetectionStrategy; use Laravel\Boost\Install\Enums\Platform; -beforeEach(function () { +beforeEach(function (): void { $this->strategy = new CommandDetectionStrategy; }); -test('detects command with successful exit code', function () { +test('detects command with successful exit code', function (): void { Process::fake([ 'which php' => Process::result(exitCode: 0), ]); @@ -22,7 +22,7 @@ expect($result)->toBeTrue(); }); -test('fails for command with non zero exit code', function () { +test('fails for command with non zero exit code', function (): void { Process::fake([ 'which nonexistent' => Process::result(exitCode: 1), ]); @@ -34,7 +34,7 @@ expect($result)->toBeFalse(); }); -test('returns false when no command config', function () { +test('returns false when no command config', function (): void { $result = $this->strategy->detect([ 'other_config' => 'value', ]); @@ -42,7 +42,7 @@ expect($result)->toBeFalse(); }); -test('handles command with output', function () { +test('handles command with output', function (): void { Process::fake([ 'echo test' => Process::result(output: 'test', exitCode: 0), ]); @@ -54,7 +54,7 @@ expect($result)->toBeTrue(); }); -test('handles command with error output', function () { +test('handles command with error output', function (): void { Process::fake([ 'invalid-command' => Process::result(errorOutput: 'command not found', exitCode: 127), ]); @@ -66,7 +66,7 @@ expect($result)->toBeFalse(); }); -test('works with different platforms parameter', function () { +test('works with different platforms parameter', function (): void { Process::fake([ 'where code' => Process::result(exitCode: 0), ]); diff --git a/tests/Feature/Install/GuidelineComposerTest.php b/tests/Feature/Install/GuidelineComposerTest.php index adb4fe95..30343725 100644 --- a/tests/Feature/Install/GuidelineComposerTest.php +++ b/tests/Feature/Install/GuidelineComposerTest.php @@ -10,7 +10,7 @@ use Laravel\Roster\PackageCollection; use Laravel\Roster\Roster; -beforeEach(function () { +beforeEach(function (): void { $this->roster = Mockery::mock(Roster::class); $this->herd = Mockery::mock(Herd::class); $this->herd->shouldReceive('isInstalled')->andReturn(false)->byDefault(); @@ -21,7 +21,7 @@ $this->composer = new GuidelineComposer($this->roster, $this->herd); }); -test('includes Inertia React conditional guidelines based on version', function (string $version, bool $shouldIncludeForm, bool $shouldInclude212Features) { +test('includes Inertia React conditional guidelines based on version', function (string $version, bool $shouldIncludeForm, bool $shouldInclude212Features): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::INERTIA_REACT, 'inertiajs/inertia-react', $version), @@ -84,7 +84,7 @@ 'version 2.2.0 (all features)' => ['2.2.0', true, true], ]); -test('includes package guidelines only for installed packages', function () { +test('includes package guidelines only for installed packages', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::PEST, 'pestphp/pest', '3.0.0'), @@ -99,7 +99,7 @@ ->not->toContain('=== inertia-react/core rules ==='); }); -test('excludes conditional guidelines when config is false', function () { +test('excludes conditional guidelines when config is false', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), ]); @@ -123,7 +123,7 @@ ->not->toContain('=== tests rules ==='); }); -test('includes Herd guidelines only when on .test domain and Herd is installed', function (string $appUrl, bool $herdInstalled, bool $shouldInclude) { +test('includes Herd guidelines only when on .test domain and Herd is installed', function (string $appUrl, bool $herdInstalled, bool $shouldInclude): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), ]); @@ -147,7 +147,7 @@ 'localhost with Herd' => ['http://localhost:8000', true, false], ]); -test('composes guidelines with proper formatting', function () { +test('composes guidelines with proper formatting', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), ]); @@ -166,7 +166,7 @@ ->toMatch('/=== \w+.*? rules ===/'); }); -test('handles multiple package versions correctly', function () { +test('handles multiple package versions correctly', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::INERTIA_REACT, 'inertiajs/inertia-react', '2.1.0'), @@ -212,7 +212,7 @@ ->toContain('=== pest/core rules ==='); }); -test('filters out empty guidelines', function () { +test('filters out empty guidelines', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), ]); @@ -226,7 +226,7 @@ ->not->toMatch('/=== \w+.*? rules ===\s*===/'); }); -test('returns list of used guidelines', function () { +test('returns list of used guidelines', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::PEST, 'pestphp/pest', '3.0.1', true), @@ -252,7 +252,7 @@ ->toContain('pest/core'); }); -test('includes user custom guidelines from .ai/guidelines directory', function () { +test('includes user custom guidelines from .ai/guidelines directory', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), ]); @@ -262,7 +262,7 @@ $composer = Mockery::mock(GuidelineComposer::class, [$this->roster, $this->herd])->makePartial(); $composer ->shouldReceive('customGuidelinePath') - ->andReturnUsing(fn ($path = '') => realpath(\Pest\testDirectory('fixtures/.ai/guidelines')).'/'.ltrim($path, '/')); + ->andReturnUsing(fn ($path = ''): string => realpath(\Pest\testDirectory('fixtures/.ai/guidelines')).'/'.ltrim((string) $path, '/')); expect($composer->compose()) ->toContain('=== .ai/custom-rule rules ===') @@ -275,7 +275,7 @@ ->toContain('.ai/project-specific'); }); -test('non-empty custom guidelines override Boost guidelines', function () { +test('non-empty custom guidelines override Boost guidelines', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), ]); @@ -285,10 +285,10 @@ $composer = Mockery::mock(GuidelineComposer::class, [$this->roster, $this->herd])->makePartial(); $composer ->shouldReceive('customGuidelinePath') - ->andReturnUsing(fn ($path = '') => realpath(\Pest\testDirectory('fixtures/.ai/guidelines')).'/'.ltrim($path, '/')); + ->andReturnUsing(fn ($path = ''): string => realpath(\Pest\testDirectory('fixtures/.ai/guidelines')).'/'.ltrim((string) $path, '/')); $guidelines = $composer->compose(); - $overrideStringCount = substr_count($guidelines, 'Thanks though, appreciate you'); + $overrideStringCount = substr_count((string) $guidelines, 'Thanks though, appreciate you'); expect($overrideStringCount)->toBe(1) ->and($guidelines) @@ -299,7 +299,7 @@ ->toContain('.ai/project-specific'); }); -test('excludes PHPUnit guidelines when Pest is present due to package priority', function () { +test('excludes PHPUnit guidelines when Pest is present due to package priority', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::PEST, 'pestphp/pest', '3.0.0'), @@ -316,7 +316,7 @@ ->not->toContain('=== phpunit/core rules ==='); }); -test('includes PHPUnit guidelines when Pest is not present', function () { +test('includes PHPUnit guidelines when Pest is not present', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::PHPUNIT, 'phpunit/phpunit', '10.0.0'), diff --git a/tests/Feature/Mcp/ToolExecutorTest.php b/tests/Feature/Mcp/ToolExecutorTest.php index 597a1baa..59db3a7f 100644 --- a/tests/Feature/Mcp/ToolExecutorTest.php +++ b/tests/Feature/Mcp/ToolExecutorTest.php @@ -5,13 +5,13 @@ use Laravel\Boost\Mcp\Tools\Tinker; use Laravel\Mcp\Response; -test('can execute tool in subprocess', function () { +test('can execute tool in subprocess', function (): void { // Create a mock that overrides buildCommand to work with testbench $executor = Mockery::mock(ToolExecutor::class)->makePartial() ->shouldAllowMockingProtectedMethods(); $executor->shouldReceive('buildCommand') ->once() - ->andReturnUsing(fn ($toolClass, $arguments) => buildSubprocessCommand($toolClass, $arguments)); + ->andReturnUsing(fn ($toolClass, $arguments): array => buildSubprocessCommand($toolClass, $arguments)); $response = $executor->execute(GetConfig::class, ['key' => 'app.name']); @@ -30,7 +30,7 @@ expect($textContent)->toContain('Laravel'); }); -test('rejects unregistered tools', function () { +test('rejects unregistered tools', function (): void { $executor = app(ToolExecutor::class); $response = $executor->execute('NonExistentToolClass'); @@ -38,11 +38,11 @@ ->and($response->isError())->toBeTrue(); }); -test('subprocess proves fresh process isolation', function () { +test('subprocess proves fresh process isolation', function (): void { $executor = Mockery::mock(ToolExecutor::class)->makePartial() ->shouldAllowMockingProtectedMethods(); $executor->shouldReceive('buildCommand') - ->andReturnUsing(fn ($toolClass, $arguments) => buildSubprocessCommand($toolClass, $arguments)); + ->andReturnUsing(fn ($toolClass, $arguments): array => buildSubprocessCommand($toolClass, $arguments)); $response1 = $executor->execute(Tinker::class, ['code' => 'return getmypid();']); $response2 = $executor->execute(Tinker::class, ['code' => 'return getmypid();']); @@ -58,18 +58,18 @@ ->and($pid1)->not()->toBe($pid2); }); -test('subprocess sees modified autoloaded code changes', function () { +test('subprocess sees modified autoloaded code changes', function (): void { $executor = Mockery::mock(ToolExecutor::class)->makePartial() ->shouldAllowMockingProtectedMethods(); $executor->shouldReceive('buildCommand') - ->andReturnUsing(fn ($toolClass, $arguments) => buildSubprocessCommand($toolClass, $arguments)); + ->andReturnUsing(fn ($toolClass, $arguments): array => buildSubprocessCommand($toolClass, $arguments)); // Path to the GetConfig tool that we'll temporarily modify // TODO: Improve for parallelisation $toolPath = dirname(__DIR__, 3).'/src/Mcp/Tools/GetConfig.php'; $originalContent = file_get_contents($toolPath); - $cleanup = function () use ($toolPath, $originalContent) { + $cleanup = function () use ($toolPath, $originalContent): void { file_put_contents($toolPath, $originalContent); }; @@ -112,7 +112,7 @@ function buildSubprocessCommand(string $toolClass, array $arguments): array 'use Symfony\Component\Console\Output\BufferedOutput; '. // Bootstrap testbench like all.php does '$app = Testbench::createFromConfig(new TestbenchConfig([]), options: ["enables_package_discoveries" => false]); '. - 'Illuminate\Container\Container::setInstance($app); '. + (\Illuminate\Container\Container::class.'::setInstance($app); '). '$kernel = $app->make("Illuminate\Contracts\Console\Kernel"); '. '$kernel->bootstrap(); '. // Register the ExecuteToolCommand @@ -131,12 +131,12 @@ function buildSubprocessCommand(string $toolClass, array $arguments): array return [PHP_BINARY, '-r', $testScript]; } -test('respects custom timeout parameter', function () { +test('respects custom timeout parameter', function (): void { $executor = Mockery::mock(ToolExecutor::class)->makePartial() ->shouldAllowMockingProtectedMethods(); $executor->shouldReceive('buildCommand') - ->andReturnUsing(fn ($toolClass, $arguments) => buildSubprocessCommand($toolClass, $arguments)); + ->andReturnUsing(fn ($toolClass, $arguments): array => buildSubprocessCommand($toolClass, $arguments)); // Test with custom timeout - should succeed with fast code $response = $executor->execute(Tinker::class, [ @@ -147,13 +147,12 @@ function buildSubprocessCommand(string $toolClass, array $arguments): array expect($response->isError())->toBeFalse(); }); -test('clamps timeout values correctly', function () { +test('clamps timeout values correctly', function (): void { $executor = new ToolExecutor; // Test timeout clamping using reflection to access protected method $reflection = new ReflectionClass($executor); $method = $reflection->getMethod('getTimeout'); - $method->setAccessible(true); // Test default expect($method->invoke($executor, []))->toBe(180); diff --git a/tests/Feature/Mcp/ToolRegistryTest.php b/tests/Feature/Mcp/ToolRegistryTest.php index 618f8612..c87f9db5 100644 --- a/tests/Feature/Mcp/ToolRegistryTest.php +++ b/tests/Feature/Mcp/ToolRegistryTest.php @@ -3,19 +3,19 @@ use Laravel\Boost\Mcp\ToolRegistry; use Laravel\Boost\Mcp\Tools\ApplicationInfo; -test('can discover available tools', function () { +test('can discover available tools', function (): void { $tools = ToolRegistry::getAvailableTools(); expect($tools)->toBeArray() ->and($tools)->toContain(ApplicationInfo::class); }); -test('can check if tool is allowed', function () { +test('can check if tool is allowed', function (): void { expect(ToolRegistry::isToolAllowed(ApplicationInfo::class))->toBeTrue() ->and(ToolRegistry::isToolAllowed('NonExistentTool'))->toBeFalse(); }); -test('can get tool names', function () { +test('can get tool names', function (): void { $tools = ToolRegistry::getToolNames(); expect($tools)->toBeArray() @@ -23,7 +23,7 @@ ->and($tools['ApplicationInfo'])->toBe(ApplicationInfo::class); }); -test('can clear cache', function () { +test('can clear cache', function (): void { // First call caches the results $tools1 = ToolRegistry::getAvailableTools(); diff --git a/tests/Feature/Mcp/Tools/ApplicationInfoTest.php b/tests/Feature/Mcp/Tools/ApplicationInfoTest.php index d0582f9d..97923719 100644 --- a/tests/Feature/Mcp/Tools/ApplicationInfoTest.php +++ b/tests/Feature/Mcp/Tools/ApplicationInfoTest.php @@ -10,7 +10,7 @@ use Laravel\Roster\PackageCollection; use Laravel\Roster\Roster; -test('it returns application info with packages', function () { +test('it returns application info with packages', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::PEST, 'pestphp/pest', '2.0.0'), @@ -53,7 +53,7 @@ ]); }); -test('it returns application info with no packages', function () { +test('it returns application info with no packages', function (): void { $roster = Mockery::mock(Roster::class); $roster->shouldReceive('packages')->andReturn(new PackageCollection([])); diff --git a/tests/Feature/Mcp/Tools/BrowserLogsTest.php b/tests/Feature/Mcp/Tools/BrowserLogsTest.php index fd2b0098..ba56781d 100644 --- a/tests/Feature/Mcp/Tools/BrowserLogsTest.php +++ b/tests/Feature/Mcp/Tools/BrowserLogsTest.php @@ -12,7 +12,7 @@ use Laravel\Boost\Services\BrowserLogger; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { // Clean up any existing browser.log file before each test $logFile = storage_path('logs/browser.log'); if (File::exists($logFile)) { @@ -20,7 +20,7 @@ } }); -test('it returns log entries when file exists', function () { +test('it returns log entries when file exists', function (): void { // Create a fake browser.log file with some entries $logFile = storage_path('logs/browser.log'); File::ensureDirectoryExists(dirname($logFile)); @@ -45,7 +45,7 @@ expect($response)->isToolResult(); }); -test('it returns error when entries argument is invalid', function () { +test('it returns error when entries argument is invalid', function (): void { $tool = new BrowserLogs; // Test with zero @@ -61,7 +61,7 @@ ->toolTextContains('The "entries" argument must be greater than 0.'); }); -test('it returns error when log file does not exist', function () { +test('it returns error when log file does not exist', function (): void { $tool = new BrowserLogs; $response = $tool->handle(new Request(['entries' => 10])); @@ -70,7 +70,7 @@ ->toolTextContains('No log file found, probably means no logs yet.'); }); -test('it returns error when log file is empty', function () { +test('it returns error when log file is empty', function (): void { // Create an empty browser.log file $logFile = storage_path('logs/browser.log'); File::ensureDirectoryExists(dirname($logFile)); @@ -84,13 +84,13 @@ ->toolTextContains('Unable to retrieve log entries, or no logs'); }); -test('@boostJs blade directive renders browser logger script', function () { +test('@boostJs blade directive renders browser logger script', function (): void { // Ensure route exists - Route::post('/_boost/browser-logs', function () {})->name('boost.browser-logs'); + Route::post('/_boost/browser-logs', function (): void {})->name('boost.browser-logs'); $blade = Blade::compileString('@boostJs'); - expect($blade)->toBe(''); + expect($blade)->toBe(''); // Test that the script contains expected content $script = BrowserLogger::getScript(); @@ -101,7 +101,7 @@ ->and($script)->toContain('window.onerror'); }); -test('browser logs endpoint processes logs correctly', function () { +test('browser logs endpoint processes logs correctly', function (): void { Log::shouldReceive('channel') ->with('browser') ->andReturn($logger = Mockery::mock(\Illuminate\Log\Logger::class)); @@ -145,7 +145,7 @@ $response->assertJson(['status' => 'logged']); }); -test('browser logs endpoint handles complex nested data', function () { +test('browser logs endpoint handles complex nested data', function (): void { $this->withoutExceptionHandling(); Log::shouldReceive('channel') @@ -180,7 +180,7 @@ $response->assertOk(); }); -test('InjectBoost middleware injects script into HTML response', function () { +test('InjectBoost middleware injects script into HTML response', function (): void { $middleware = new InjectBoost; $html = <<<'HTML' @@ -198,9 +198,7 @@ $request = HttpRequest::create('/'); $response = new \Illuminate\Http\Response($html, 200, ['Content-Type' => 'text/html']); - $result = $middleware->handle($request, function ($req) use ($response) { - return $response; - }); + $result = $middleware->handle($request, fn ($req): \Illuminate\Http\Response => $response); $content = $result->getContent(); expect($content)->toContain('browser-logger-active') @@ -209,7 +207,7 @@ ->and(substr_count($content, 'browser-logger-active'))->toBe(1); }); -test('InjectBoost middleware does not inject into non-HTML responses', function () { +test('InjectBoost middleware does not inject into non-HTML responses', function (): void { $middleware = new InjectBoost; $json = json_encode(['status' => 'ok']); @@ -217,16 +215,14 @@ $request = HttpRequest::create('/'); $response = new \Illuminate\Http\Response($json); - $result = $middleware->handle($request, function ($req) use ($response) { - return $response; - }); + $result = $middleware->handle($request, fn ($req): \Illuminate\Http\Response => $response); $content = $result->getContent(); expect($content)->toBe($json) ->and($content)->not->toContain('browser-logger-active'); }); -test('InjectBoost middleware does not inject script twice', function () { +test('InjectBoost middleware does not inject script twice', function (): void { $middleware = new InjectBoost; $html = <<<'HTML' @@ -245,15 +241,13 @@ $request = HttpRequest::create('/'); $response = new \Illuminate\Http\Response($html); - $result = $middleware->handle($request, function ($req) use ($response) { - return $response; - }); + $result = $middleware->handle($request, fn ($req): \Illuminate\Http\Response => $response); $content = $result->getContent(); expect(substr_count($content, 'browser-logger-active'))->toBe(1); }); -test('InjectBoost middleware injects before body tag when no head tag', function () { +test('InjectBoost middleware injects before body tag when no head tag', function (): void { $middleware = new InjectBoost; $html = <<<'HTML' @@ -268,9 +262,7 @@ $request = HttpRequest::create('/'); $response = new \Illuminate\Http\Response($html, 200, ['Content-Type' => 'text/html']); - $result = $middleware->handle($request, function ($req) use ($response) { - return $response; - }); + $result = $middleware->handle($request, fn ($req): \Illuminate\Http\Response => $response); $content = $result->getContent(); expect($content)->toContain('browser-logger-active') diff --git a/tests/Feature/Mcp/Tools/DatabaseConnectionsTest.php b/tests/Feature/Mcp/Tools/DatabaseConnectionsTest.php index ba6372e0..b5a1266a 100644 --- a/tests/Feature/Mcp/Tools/DatabaseConnectionsTest.php +++ b/tests/Feature/Mcp/Tools/DatabaseConnectionsTest.php @@ -5,7 +5,7 @@ use Laravel\Boost\Mcp\Tools\DatabaseConnections; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { config()->set('database.default', 'mysql'); config()->set('database.connections', [ 'mysql' => ['driver' => 'mysql'], @@ -14,7 +14,7 @@ ]); }); -test('it returns database connections', function () { +test('it returns database connections', function (): void { $tool = new DatabaseConnections; $response = $tool->handle(new Request([])); @@ -26,7 +26,7 @@ ]); }); -test('it returns empty connections when none configured', function () { +test('it returns empty connections when none configured', function (): void { config()->set('database.connections', []); $tool = new DatabaseConnections; diff --git a/tests/Feature/Mcp/Tools/DatabaseSchemaTest.php b/tests/Feature/Mcp/Tools/DatabaseSchemaTest.php index 53dd97fd..dee84026 100644 --- a/tests/Feature/Mcp/Tools/DatabaseSchemaTest.php +++ b/tests/Feature/Mcp/Tools/DatabaseSchemaTest.php @@ -8,7 +8,7 @@ use Laravel\Boost\Mcp\Tools\DatabaseSchema; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { // Switch the default connection to a file-backed SQLite DB. config()->set('database.default', 'testing'); config()->set('database.connections.testing', [ @@ -24,20 +24,20 @@ // Build a throw-away table that we expect in the dump. Schema::dropIfExists('examples'); - Schema::create('examples', function (Blueprint $table) { + Schema::create('examples', function (Blueprint $table): void { $table->id(); $table->string('name'); }); }); -afterEach(function () { +afterEach(function (): void { $dbFile = database_path('testing.sqlite'); if (File::exists($dbFile)) { File::delete($dbFile); } }); -test('it returns structured database schema', function () { +test('it returns structured database schema', function (): void { $tool = new DatabaseSchema; $response = $tool->handle(new Request([])); @@ -46,7 +46,7 @@ ->toolJsonContentToMatchArray([ 'engine' => 'sqlite', ]) - ->toolJsonContent(function ($schemaArray) { + ->toolJsonContent(function (array $schemaArray): void { expect($schemaArray)->toHaveKey('tables') ->and($schemaArray['tables'])->toHaveKey('examples'); @@ -61,9 +61,9 @@ }); }); -test('it filters tables by name', function () { +test('it filters tables by name', function (): void { // Create another table - Schema::create('users', function (Blueprint $table) { + Schema::create('users', function (Blueprint $table): void { $table->id(); $table->string('email'); }); @@ -74,7 +74,7 @@ $response = $tool->handle(new Request(['filter' => 'example'])); expect($response)->isToolResult() ->toolHasNoError() - ->toolJsonContent(function ($schemaArray) { + ->toolJsonContent(function (array $schemaArray): void { expect($schemaArray['tables'])->toHaveKey('examples') ->and($schemaArray['tables'])->not->toHaveKey('users'); }); @@ -83,7 +83,7 @@ $response = $tool->handle(new Request(['filter' => 'user'])); expect($response)->isToolResult() ->toolHasNoError() - ->toolJsonContent(function ($schemaArray) { + ->toolJsonContent(function (array $schemaArray): void { expect($schemaArray['tables'])->toHaveKey('users') ->and($schemaArray['tables'])->not->toHaveKey('examples'); }); diff --git a/tests/Feature/Mcp/Tools/GetAbsoluteUrlTest.php b/tests/Feature/Mcp/Tools/GetAbsoluteUrlTest.php index 652081dc..0b50954c 100644 --- a/tests/Feature/Mcp/Tools/GetAbsoluteUrlTest.php +++ b/tests/Feature/Mcp/Tools/GetAbsoluteUrlTest.php @@ -6,14 +6,12 @@ use Laravel\Boost\Mcp\Tools\GetAbsoluteUrl; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { config()->set('app.url', 'http://localhost'); - Route::get('/test', function () { - return 'test'; - })->name('test.route'); + Route::get('/test', fn (): string => 'test')->name('test.route'); }); -test('it returns absolute url for root path by default', function () { +test('it returns absolute url for root path by default', function (): void { $tool = new GetAbsoluteUrl; $response = $tool->handle(new Request([])); @@ -22,7 +20,7 @@ ->toolTextContains('http://localhost'); }); -test('it returns absolute url for given path', function () { +test('it returns absolute url for given path', function (): void { $tool = new GetAbsoluteUrl; $response = $tool->handle(new Request(['path' => '/dashboard'])); @@ -31,7 +29,7 @@ ->toolTextContains('http://localhost/dashboard'); }); -test('it returns absolute url for named route', function () { +test('it returns absolute url for named route', function (): void { $tool = new GetAbsoluteUrl; $response = $tool->handle(new Request(['route' => 'test.route'])); @@ -40,7 +38,7 @@ ->toolTextContains('http://localhost/test'); }); -test('it prioritizes path over route when both are provided', function () { +test('it prioritizes path over route when both are provided', function (): void { $tool = new GetAbsoluteUrl; $response = $tool->handle(new Request(['path' => '/dashboard', 'route' => 'test.route'])); @@ -49,7 +47,7 @@ ->toolTextContains('http://localhost/dashboard'); }); -test('it handles empty path', function () { +test('it handles empty path', function (): void { $tool = new GetAbsoluteUrl; $response = $tool->handle(new Request(['path' => ''])); diff --git a/tests/Feature/Mcp/Tools/GetConfigTest.php b/tests/Feature/Mcp/Tools/GetConfigTest.php index 29d2de81..8efb35a3 100644 --- a/tests/Feature/Mcp/Tools/GetConfigTest.php +++ b/tests/Feature/Mcp/Tools/GetConfigTest.php @@ -5,13 +5,13 @@ use Laravel\Boost\Mcp\Tools\GetConfig; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { config()->set('test.key', 'test_value'); config()->set('nested.config.key', 'nested_value'); config()->set('app.name', 'Test App'); }); -test('it returns config value when key exists', function () { +test('it returns config value when key exists', function (): void { $tool = new GetConfig; $response = $tool->handle(new Request(['key' => 'test.key'])); @@ -20,7 +20,7 @@ ->toolTextContains('"key": "test.key"', '"value": "test_value"'); }); -test('it returns nested config value', function () { +test('it returns nested config value', function (): void { $tool = new GetConfig; $response = $tool->handle(new Request(['key' => 'nested.config.key'])); @@ -29,7 +29,7 @@ ->toolTextContains('"key": "nested.config.key"', '"value": "nested_value"'); }); -test('it returns error when config key does not exist', function () { +test('it returns error when config key does not exist', function (): void { $tool = new GetConfig; $response = $tool->handle(new Request(['key' => 'nonexistent.key'])); @@ -38,7 +38,7 @@ ->toolTextContains("Config key 'nonexistent.key' not found."); }); -test('it works with built-in Laravel config keys', function () { +test('it works with built-in Laravel config keys', function (): void { $tool = new GetConfig; $response = $tool->handle(new Request(['key' => 'app.name'])); diff --git a/tests/Feature/Mcp/Tools/ListArtisanCommandsTest.php b/tests/Feature/Mcp/Tools/ListArtisanCommandsTest.php index 8723bebd..801b41c9 100644 --- a/tests/Feature/Mcp/Tools/ListArtisanCommandsTest.php +++ b/tests/Feature/Mcp/Tools/ListArtisanCommandsTest.php @@ -5,13 +5,13 @@ use Laravel\Boost\Mcp\Tools\ListArtisanCommands; use Laravel\Mcp\Request; -test('it returns list of artisan commands', function () { +test('it returns list of artisan commands', function (): void { $tool = new ListArtisanCommands; $response = $tool->handle(new Request([])); expect($response)->isToolResult() ->toolHasNoError() - ->toolJsonContent(function ($content) { + ->toolJsonContent(function ($content): void { expect($content)->toBeArray() ->and($content)->not->toBeEmpty(); diff --git a/tests/Feature/Mcp/Tools/ListAvailableConfigKeysTest.php b/tests/Feature/Mcp/Tools/ListAvailableConfigKeysTest.php index c2d8d062..5ce139d5 100644 --- a/tests/Feature/Mcp/Tools/ListAvailableConfigKeysTest.php +++ b/tests/Feature/Mcp/Tools/ListAvailableConfigKeysTest.php @@ -5,19 +5,19 @@ use Laravel\Boost\Mcp\Tools\ListAvailableConfigKeys; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { config()->set('test.simple', 'value'); config()->set('test.nested.key', 'nested_value'); config()->set('test.array', ['item1', 'item2']); }); -test('it returns list of config keys in dot notation', function () { +test('it returns list of config keys in dot notation', function (): void { $tool = new ListAvailableConfigKeys; $response = $tool->handle(new Request([])); expect($response)->isToolResult() ->toolHasNoError() - ->toolJsonContent(function ($content) { + ->toolJsonContent(function ($content): void { expect($content)->toBeArray() ->and($content)->not->toBeEmpty() // Check that it contains common Laravel config keys @@ -37,7 +37,7 @@ }); }); -test('it handles empty config gracefully', function () { +test('it handles empty config gracefully', function (): void { // Clear all config config()->set('test', null); @@ -46,7 +46,7 @@ expect($response)->isToolResult() ->toolHasNoError() - ->toolJsonContent(function ($content) { + ->toolJsonContent(function ($content): void { expect($content)->toBeArray() // Should still have Laravel default config keys ->and($content)->toContain('app.name'); diff --git a/tests/Feature/Mcp/Tools/ListRoutesTest.php b/tests/Feature/Mcp/Tools/ListRoutesTest.php index 7f8e3602..c93244e8 100644 --- a/tests/Feature/Mcp/Tools/ListRoutesTest.php +++ b/tests/Feature/Mcp/Tools/ListRoutesTest.php @@ -6,33 +6,21 @@ use Laravel\Boost\Mcp\Tools\ListRoutes; use Laravel\Mcp\Request; -beforeEach(function () { - Route::get('/admin/dashboard', function () { - return 'admin dashboard'; - })->name('admin.dashboard'); - - Route::post('/admin/users', function () { - return 'admin users'; - })->name('admin.users.store'); - - Route::get('/user/profile', function () { - return 'user profile'; - })->name('user.profile'); - - Route::get('/api/two-factor/enable', function () { - return 'two-factor enable'; - })->name('two-factor.enable'); - - Route::get('/api/v1/posts', function () { - return 'posts'; - })->name('api.posts.index'); - - Route::put('/api/v1/posts/{id}', function ($id) { - return 'update post'; - })->name('api.posts.update'); +beforeEach(function (): void { + Route::get('/admin/dashboard', fn (): string => 'admin dashboard')->name('admin.dashboard'); + + Route::post('/admin/users', fn (): string => 'admin users')->name('admin.users.store'); + + Route::get('/user/profile', fn (): string => 'user profile')->name('user.profile'); + + Route::get('/api/two-factor/enable', fn (): string => 'two-factor enable')->name('two-factor.enable'); + + Route::get('/api/v1/posts', fn (): string => 'posts')->name('api.posts.index'); + + Route::put('/api/v1/posts/{id}', fn ($id): string => 'update post')->name('api.posts.update'); }); -test('it returns list of routes without filters', function () { +test('it returns list of routes without filters', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request([])); @@ -41,7 +29,7 @@ ->toolTextContains('GET|HEAD', 'admin.dashboard', 'user.profile'); }); -test('it sanitizes name parameter wildcards and filters correctly', function () { +test('it sanitizes name parameter wildcards and filters correctly', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request(['name' => '*admin*'])); @@ -63,14 +51,14 @@ }); -test('it sanitizes method parameter wildcards and filters correctly', function () { +test('it sanitizes method parameter wildcards and filters correctly', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request(['method' => 'GET*POST'])); expect($response)->isToolResult() ->toolHasNoError() - ->toolTextContains('ERROR Your application doesn\'t have any routes matching the given criteria.'); + ->toolTextContains("ERROR Your application doesn't have any routes matching the given criteria."); $response = $tool->handle(new Request(['method' => '*GET*'])); @@ -83,7 +71,7 @@ ->and($response)->not->toolTextContains('admin.dashboard'); }); -test('it handles edge cases and empty results correctly', function () { +test('it handles edge cases and empty results correctly', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request(['name' => '*'])); @@ -94,14 +82,14 @@ $response = $tool->handle(new Request(['name' => '*nonexistent*'])); - expect($response)->toolTextContains('ERROR Your application doesn\'t have any routes matching the given criteria.'); + expect($response)->toolTextContains("ERROR Your application doesn't have any routes matching the given criteria."); $response = $tool->handle(new Request(['name' => ''])); expect($response)->toolTextContains('admin.dashboard', 'user.profile'); }); -test('it handles multiple parameters with wildcard sanitization', function () { +test('it handles multiple parameters with wildcard sanitization', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request([ @@ -122,7 +110,7 @@ expect($response)->toolTextContains('admin.users.store'); }); -test('it handles the original problematic wildcard case', function () { +test('it handles the original problematic wildcard case', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request(['name' => '*two-factor*'])); diff --git a/tests/Feature/Mcp/Tools/SearchDocsTest.php b/tests/Feature/Mcp/Tools/SearchDocsTest.php index 67517474..746489d9 100644 --- a/tests/Feature/Mcp/Tools/SearchDocsTest.php +++ b/tests/Feature/Mcp/Tools/SearchDocsTest.php @@ -10,7 +10,7 @@ use Laravel\Roster\PackageCollection; use Laravel\Roster\Roster; -test('it searches documentation successfully', function () { +test('it searches documentation successfully', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::PEST, 'pestphp/pest', '2.0.0'), @@ -30,19 +30,17 @@ ->toolHasNoError() ->toolTextContains('Documentation search results'); - Http::assertSent(function ($request) { - return $request->url() === 'https://boost.laravel.com/api/docs' && - $request->data()['queries'] === ['authentication', 'testing'] && - $request->data()['packages'] === [ - ['name' => 'laravel/framework', 'version' => '11.x'], - ['name' => 'pestphp/pest', 'version' => '2.x'], - ] && - $request->data()['token_limit'] === 10000 && - $request->data()['format'] === 'markdown'; - }); + Http::assertSent(fn ($request): bool => $request->url() === 'https://boost.laravel.com/api/docs' && + $request->data()['queries'] === ['authentication', 'testing'] && + $request->data()['packages'] === [ + ['name' => 'laravel/framework', 'version' => '11.x'], + ['name' => 'pestphp/pest', 'version' => '2.x'], + ] && + $request->data()['token_limit'] === 10000 && + $request->data()['format'] === 'markdown'); }); -test('it handles API error response', function () { +test('it handles API error response', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), ]); @@ -62,7 +60,7 @@ ->toolTextContains('Failed to search documentation: API Error'); }); -test('it filters empty queries', function () { +test('it filters empty queries', function (): void { $packages = new PackageCollection([]); $roster = Mockery::mock(Roster::class); @@ -78,15 +76,13 @@ expect($response)->isToolResult() ->toolHasNoError(); - Http::assertSent(function ($request) { - return $request->url() === 'https://boost.laravel.com/api/docs' && - $request->data()['queries'] === ['test'] && - empty($request->data()['packages']) && - $request->data()['token_limit'] === 10000; - }); + Http::assertSent(fn ($request): bool => $request->url() === 'https://boost.laravel.com/api/docs' && + $request->data()['queries'] === ['test'] && + empty($request->data()['packages']) && + $request->data()['token_limit'] === 10000); }); -test('it formats package data correctly', function () { +test('it formats package data correctly', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::LIVEWIRE, 'livewire/livewire', '3.5.1'), @@ -105,15 +101,13 @@ expect($response)->isToolResult() ->toolHasNoError(); - Http::assertSent(function ($request) { - return $request->data()['packages'] === [ - ['name' => 'laravel/framework', 'version' => '11.x'], - ['name' => 'livewire/livewire', 'version' => '3.x'], - ] && $request->data()['token_limit'] === 10000; - }); + Http::assertSent(fn ($request): bool => $request->data()['packages'] === [ + ['name' => 'laravel/framework', 'version' => '11.x'], + ['name' => 'livewire/livewire', 'version' => '3.x'], + ] && $request->data()['token_limit'] === 10000); }); -test('it handles empty results', function () { +test('it handles empty results', function (): void { $packages = new PackageCollection([]); $roster = Mockery::mock(Roster::class); @@ -131,7 +125,7 @@ ->toolTextContains('Empty response'); }); -test('it uses custom token_limit when provided', function () { +test('it uses custom token_limit when provided', function (): void { $packages = new PackageCollection([]); $roster = Mockery::mock(Roster::class); @@ -146,12 +140,10 @@ expect($response)->isToolResult()->toolHasNoError(); - Http::assertSent(function ($request) { - return $request->data()['token_limit'] === 5000; - }); + Http::assertSent(fn ($request): bool => $request->data()['token_limit'] === 5000); }); -test('it caps token_limit at maximum of 1000000', function () { +test('it caps token_limit at maximum of 1000000', function (): void { $packages = new PackageCollection([]); $roster = Mockery::mock(Roster::class); @@ -166,7 +158,5 @@ expect($response)->isToolResult()->toolHasNoError(); - Http::assertSent(function ($request) { - return $request->data()['token_limit'] === 1000000; - }); + Http::assertSent(fn ($request): bool => $request->data()['token_limit'] === 1000000); }); diff --git a/tests/Feature/Mcp/Tools/TinkerTest.php b/tests/Feature/Mcp/Tools/TinkerTest.php index f3f5dfbb..c6419a66 100644 --- a/tests/Feature/Mcp/Tools/TinkerTest.php +++ b/tests/Feature/Mcp/Tools/TinkerTest.php @@ -5,7 +5,7 @@ use Laravel\Boost\Mcp\Tools\Tinker; use Laravel\Mcp\Request; -test('executes simple php code', function () { +test('executes simple php code', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'return 2 + 2;'])); @@ -16,7 +16,7 @@ ]); }); -test('executes code with output', function () { +test('executes code with output', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'echo "Hello World"; return "test";'])); @@ -28,7 +28,7 @@ ]); }); -test('accesses laravel facades', function () { +test('accesses laravel facades', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'return config("app.name");'])); @@ -39,7 +39,7 @@ ]); }); -test('creates objects', function () { +test('creates objects', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'return new stdClass();'])); @@ -50,7 +50,7 @@ ]); }); -test('handles syntax errors', function () { +test('handles syntax errors', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'invalid syntax here'])); @@ -59,12 +59,12 @@ ->toolJsonContentToMatchArray([ 'type' => 'ParseError', ]) - ->toolJsonContent(function ($data) { + ->toolJsonContent(function ($data): void { expect($data)->toHaveKey('error'); }); }); -test('handles runtime errors', function () { +test('handles runtime errors', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'throw new Exception("Test error");'])); @@ -74,12 +74,12 @@ 'type' => 'Exception', 'error' => 'Test error', ]) - ->toolJsonContent(function ($data) { + ->toolJsonContent(function ($data): void { expect($data)->toHaveKey('error'); }); }); -test('captures multiple outputs', function () { +test('captures multiple outputs', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'echo "First"; echo "Second"; return "done";'])); @@ -90,7 +90,7 @@ ]); }); -test('executes code with different return types', function (string $code, mixed $expectedResult, string $expectedType) { +test('executes code with different return types', function (string $code, mixed $expectedResult, string $expectedType): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => $code])); @@ -109,7 +109,7 @@ 'float' => ['return 3.14;', 3.14, 'double'], ]); -test('handles empty code', function () { +test('handles empty code', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => ''])); @@ -120,7 +120,7 @@ ]); }); -test('handles code with no return statement', function () { +test('handles code with no return statement', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => '$x = 5;'])); @@ -131,12 +131,10 @@ ]); }); -test('should register only in local environment', function () { +test('should register only in local environment', function (): void { $tool = new Tinker; - app()->detectEnvironment(function () { - return 'local'; - }); + app()->detectEnvironment(fn (): string => 'local'); expect($tool->eligibleForRegistration(Mockery::mock(Request::class)))->toBeTrue(); }); diff --git a/tests/Feature/Middleware/InjectBoostTest.php b/tests/Feature/Middleware/InjectBoostTest.php index 3ec297ad..797efd14 100644 --- a/tests/Feature/Middleware/InjectBoostTest.php +++ b/tests/Feature/Middleware/InjectBoostTest.php @@ -8,13 +8,14 @@ use Illuminate\Support\Facades\Vite; use Illuminate\Testing\TestResponse; use Laravel\Boost\Middleware\InjectBoost; +use Pest\Expectation; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpFoundation\StreamedResponse; -beforeEach(function () { +beforeEach(function (): void { $this->app['view']->addNamespace('test', __DIR__.'/../../fixtures'); }); @@ -27,10 +28,8 @@ function createMiddlewareResponse($response): SymfonyResponse return $middleware->handle($request, $next); } -it('preserves the original view response type', function () { - Route::get('injection-test', function () { - return view('test::injection-test'); - })->middleware(InjectBoost::class); +it('preserves the original view response type', function (): void { + Route::get('injection-test', fn (): \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory => view('test::injection-test'))->middleware(InjectBoost::class); $response = $this->get('injection-test'); @@ -39,16 +38,16 @@ function createMiddlewareResponse($response): SymfonyResponse ->assertSee('Browser logger active (MCP server detected).'); }); -it('does not inject for special response types', function ($responseType, $responseFactory) { +it('does not inject for special response types', function ($responseType, $responseFactory): void { $response = $responseFactory(); $result = createMiddlewareResponse($response); expect($result)->toBeInstanceOf($responseType); })->with([ - 'streamed' => [StreamedResponse::class, fn () => new StreamedResponse], - 'json' => [JsonResponse::class, fn () => new JsonResponse(['data' => 'test'])], - 'redirect' => [RedirectResponse::class, fn () => new RedirectResponse('http://example.com')], - 'binary' => [BinaryFileResponse::class, function () { + 'streamed' => [StreamedResponse::class, fn (): \Symfony\Component\HttpFoundation\StreamedResponse => new StreamedResponse], + 'json' => [JsonResponse::class, fn (): \Symfony\Component\HttpFoundation\JsonResponse => new JsonResponse(['data' => 'test'])], + 'redirect' => [RedirectResponse::class, fn (): \Symfony\Component\HttpFoundation\RedirectResponse => new RedirectResponse('http://example.com')], + 'binary' => [BinaryFileResponse::class, function (): \Symfony\Component\HttpFoundation\BinaryFileResponse { $tempFile = tempnam(sys_get_temp_dir(), 'test'); file_put_contents($tempFile, 'test content'); @@ -56,7 +55,7 @@ function createMiddlewareResponse($response): SymfonyResponse }], ]); -it('does not inject when conditions are not met', function ($scenario, $responseFactory, $assertion) { +it('does not inject when conditions are not met', function ($scenario, $responseFactory, $assertion): void { $response = $responseFactory(); $result = createMiddlewareResponse($response); @@ -65,22 +64,22 @@ function createMiddlewareResponse($response): SymfonyResponse 'non-html content type' => [ 'scenario', fn () => (new Response('test'))->withHeaders(['content-type' => 'application/json']), - fn ($result) => expect($result->getContent())->toBe('test'), + fn ($result): Expectation => expect($result->getContent())->toBe('test'), ], 'missing html skeleton' => [ 'scenario', fn () => (new Response('test'))->withHeaders(['content-type' => 'text/html']), - fn ($result) => expect($result->getContent())->toBe('test'), + fn ($result): Expectation => expect($result->getContent())->toBe('test'), ], 'already injected' => [ 'scenario', fn () => (new Response('Test
')) ->withHeaders(['content-type' => 'text/html']), - fn ($result) => expect($result->getContent())->toContain('browser-logger-active'), + fn ($result): Expectation => expect($result->getContent())->toContain('browser-logger-active'), ], ]); -it('injects script in html responses', function ($html) { +it('injects script in html responses', function ($html): void { $response = new Response($html); $response->headers->set('content-type', 'text/html'); @@ -92,12 +91,12 @@ function createMiddlewareResponse($response): SymfonyResponse 'without head/body tags' => 'Test', ]); -it('handles CSP nonce attribute correctly', function ($nonce, $assertions) { +it('handles CSP nonce attribute correctly', function ($nonce, $assertions): void { if ($nonce) { Vite::useCspNonce($nonce); } - Route::get('injection-test', fn () => view('test::injection-test')) + Route::get('injection-test', fn (): \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory => view('test::injection-test')) ->middleware(InjectBoost::class); $response = $this->get('injection-test')->assertViewIs('test::injection-test'); diff --git a/tests/Pest.php b/tests/Pest.php index 14b4b9fa..05eb243b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -17,9 +17,7 @@ uses(Tests\TestCase::class)->in('Feature'); -expect()->extend('isToolResult', function () { - return $this->toBeInstanceOf(Response::class); -}); +expect()->extend('isToolResult', fn () => $this->toBeInstanceOf(Response::class)); expect()->extend('toolTextContains', function (mixed ...$needles) { /** @var Response $this->value */ diff --git a/tests/Unit/Install/Cli/DisplayHelperTest.php b/tests/Unit/Install/Cli/DisplayHelperTest.php index b8771260..a05e075a 100644 --- a/tests/Unit/Install/Cli/DisplayHelperTest.php +++ b/tests/Unit/Install/Cli/DisplayHelperTest.php @@ -4,9 +4,9 @@ use Laravel\Boost\Install\Cli\DisplayHelper; -describe('DisplayHelper tests', function () { - describe('datatable tests', function () { - it('returns early for empty data', function () { +describe('DisplayHelper tests', function (): void { + describe('datatable tests', function (): void { + it('returns early for empty data', function (): void { ob_start(); DisplayHelper::datatable([]); $output = ob_get_clean(); @@ -14,7 +14,7 @@ expect($output)->toBe(''); }); - it('displays a simple single row table', function () { + it('displays a simple single row table', function (): void { ob_start(); DisplayHelper::datatable([ ['Name', 'Age'], @@ -29,7 +29,7 @@ ->and($output)->toContain('╯'); }); - it('displays a multi-row table', function () { + it('displays a multi-row table', function (): void { ob_start(); DisplayHelper::datatable([ ['Name', 'Age', 'City'], @@ -46,7 +46,7 @@ ->and($output)->toContain('┼'); }); - it('handles different data types in cells', function () { + it('handles different data types in cells', function (): void { ob_start(); DisplayHelper::datatable([ ['String', 'Number', 'Boolean'], @@ -62,7 +62,7 @@ ->and($output)->toContain('456'); }); - it('applies bold formatting to first column', function () { + it('applies bold formatting to first column', function (): void { ob_start(); DisplayHelper::datatable([ ['Header1', 'Header2'], @@ -75,7 +75,7 @@ ->and($output)->not->toContain("\e[1mHeader2\e[0m"); }); - it('handles unicode characters properly', function () { + it('handles unicode characters properly', function (): void { ob_start(); DisplayHelper::datatable([ ['名前', 'Émile'], @@ -90,8 +90,8 @@ }); }); - describe('grid test', function () { - it('returns early for empty items', function () { + describe('grid test', function (): void { + it('returns early for empty items', function (): void { ob_start(); DisplayHelper::grid([]); $output = ob_get_clean(); @@ -99,7 +99,7 @@ expect($output)->toBe(''); }); - it('displays single item grid', function () { + it('displays single item grid', function (): void { ob_start(); DisplayHelper::grid(['Item1']); $output = ob_get_clean(); @@ -111,7 +111,7 @@ ->and($output)->toContain('╯'); }); - it('displays multiple items in grid', function () { + it('displays multiple items in grid', function (): void { ob_start(); DisplayHelper::grid(['Item1', 'Item2', 'Item3', 'Item4']); $output = ob_get_clean(); @@ -122,7 +122,7 @@ ->and($output)->toContain('Item4'); }); - it('handles items of different lengths', function () { + it('handles items of different lengths', function (): void { ob_start(); DisplayHelper::grid(['Short', 'Very Long Item Name', 'Med']); $output = ob_get_clean(); @@ -132,7 +132,7 @@ ->and($output)->toContain('Med'); }); - it('respects column width parameter', function () { + it('respects column width parameter', function (): void { ob_start(); DisplayHelper::grid(['Item1', 'Item2'], 40); $output = ob_get_clean(); @@ -141,7 +141,7 @@ ->and($output)->toContain('Item2'); }); - it('handles unicode characters in grid', function () { + it('handles unicode characters in grid', function (): void { ob_start(); DisplayHelper::grid(['測試', 'café', '🚀']); $output = ob_get_clean(); @@ -151,7 +151,7 @@ ->and($output)->toContain('🚀'); }); - it('fills empty cells when items do not fill complete rows', function () { + it('fills empty cells when items do not fill complete rows', function (): void { ob_start(); DisplayHelper::grid(['Item1', 'Item2', 'Item3']); $output = ob_get_clean(); diff --git a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php index 81ae1de3..80d0e8d0 100644 --- a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php +++ b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php @@ -16,12 +16,12 @@ use Laravel\Boost\Install\Enums\Platform; use Mockery; -beforeEach(function () { +beforeEach(function (): void { $this->strategyFactory = Mockery::mock(DetectionStrategyFactory::class); $this->strategy = Mockery::mock(DetectionStrategy::class); }); -afterEach(function () { +afterEach(function (): void { Mockery::close(); }); @@ -65,7 +65,7 @@ public function mcpConfigPath(): string } } -test('detectOnSystem delegates to strategy factory and detection strategy', function () { +test('detectOnSystem delegates to strategy factory and detection strategy', function (): void { $platform = Platform::Darwin; $config = ['paths' => ['/test/path']]; @@ -87,7 +87,7 @@ public function mcpConfigPath(): string expect($result)->toBe(true); }); -test('detectInProject merges config with basePath and delegates to strategy', function () { +test('detectInProject merges config with basePath and delegates to strategy', function (): void { $basePath = '/project/path'; $projectConfig = ['files' => ['test.config']]; $mergedConfig = ['files' => ['test.config'], 'basePath' => $basePath]; @@ -110,73 +110,73 @@ public function mcpConfigPath(): string expect($result)->toBe(false); }); -test('agentName returns displayName by default', function () { +test('agentName returns displayName by default', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); expect($environment->agentName())->toBe('Test Environment'); }); -test('mcpClientName returns displayName by default', function () { +test('mcpClientName returns displayName by default', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); expect($environment->mcpClientName())->toBe('Test Environment'); }); -test('IsAgent returns true when implements Agent interface and has agentName', function () { +test('IsAgent returns true when implements Agent interface and has agentName', function (): void { $agent = new TestAgent($this->strategyFactory); expect($agent->isAgent())->toBe(true); }); -test('IsAgent returns false when does not implement Agent interface', function () { +test('IsAgent returns false when does not implement Agent interface', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); expect($environment->isAgent())->toBe(false); }); -test('isMcpClient returns true when implements McpClient interface and has mcpClientName', function () { +test('isMcpClient returns true when implements McpClient interface and has mcpClientName', function (): void { $mcpClient = new TestMcpClient($this->strategyFactory); expect($mcpClient->isMcpClient())->toBe(true); }); -test('isMcpClient returns false when does not implement McpClient interface', function () { +test('isMcpClient returns false when does not implement McpClient interface', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); expect($environment->isMcpClient())->toBe(false); }); -test('mcpInstallationStrategy returns File by default', function () { +test('mcpInstallationStrategy returns File by default', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); expect($environment->mcpInstallationStrategy())->toBe(McpInstallationStrategy::FILE); }); -test('shellMcpCommand returns null by default', function () { +test('shellMcpCommand returns null by default', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); expect($environment->shellMcpCommand())->toBe(null); }); -test('mcpConfigPath returns null by default', function () { +test('mcpConfigPath returns null by default', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); expect($environment->mcpConfigPath())->toBe(null); }); -test('frontmatter returns false by default', function () { +test('frontmatter returns false by default', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); expect($environment->frontmatter())->toBe(false); }); -test('mcpConfigKey returns mcpServers by default', function () { +test('mcpConfigKey returns mcpServers by default', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); expect($environment->mcpConfigKey())->toBe('mcpServers'); }); -test('installMcp uses Shell strategy when configured', function () { +test('installMcp uses Shell strategy when configured', function (): void { $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); $environment->shouldAllowMockingProtectedMethods(); @@ -193,7 +193,7 @@ public function mcpConfigPath(): string expect($result)->toBe(true); }); -test('installMcp uses File strategy when configured', function () { +test('installMcp uses File strategy when configured', function (): void { $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); $environment->shouldAllowMockingProtectedMethods(); @@ -210,7 +210,7 @@ public function mcpConfigPath(): string expect($result)->toBe(true); }); -test('installMcp returns false for None strategy', function () { +test('installMcp returns false for None strategy', function (): void { $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); $environment->shouldReceive('mcpInstallationStrategy') @@ -221,7 +221,7 @@ public function mcpConfigPath(): string expect($result)->toBe(false); }); -test('installShellMcp returns false when shellMcpCommand is null', function () { +test('installShellMcp returns false when shellMcpCommand is null', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); $result = $environment->installMcp('test-key', 'test-command'); @@ -229,7 +229,7 @@ public function mcpConfigPath(): string expect($result)->toBe(false); }); -test('installShellMcp executes command with placeholders replaced', function () { +test('installShellMcp executes command with placeholders replaced', function (): void { $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); $environment->shouldAllowMockingProtectedMethods(); @@ -245,11 +245,9 @@ public function mcpConfigPath(): string Process::shouldReceive('run') ->once() - ->with(Mockery::on(function ($command) { - return str_contains($command, 'install test-key test-command "arg1" "arg2"') && - str_contains($command, '-e ENV1="value1"') && - str_contains($command, '-e ENV2="value2"'); - })) + ->with(Mockery::on(fn ($command): bool => str_contains((string) $command, 'install test-key test-command "arg1" "arg2"') && + str_contains((string) $command, '-e ENV1="value1"') && + str_contains((string) $command, '-e ENV2="value2"'))) ->andReturn($mockResult); $result = $environment->installMcp('test-key', 'test-command', ['arg1', 'arg2'], ['env1' => 'value1', 'env2' => 'value2']); @@ -257,7 +255,7 @@ public function mcpConfigPath(): string expect($result)->toBe(true); }); -test('installShellMcp returns true when process fails but has already exists error', function () { +test('installShellMcp returns true when process fails but has already exists error', function (): void { $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); $environment->shouldAllowMockingProtectedMethods(); @@ -280,7 +278,7 @@ public function mcpConfigPath(): string expect($result)->toBe(true); }); -test('installFileMcp returns false when mcpConfigPath is null', function () { +test('installFileMcp returns false when mcpConfigPath is null', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); $result = $environment->installMcp('test-key', 'test-command'); @@ -288,9 +286,10 @@ public function mcpConfigPath(): string expect($result)->toBe(false); }); -test('installFileMcp creates new config file when none exists', function () { +test('installFileMcp creates new config file when none exists', function (): void { $environment = Mockery::mock(TestMcpClient::class)->makePartial(); $environment->shouldAllowMockingProtectedMethods(); + $capturedContent = ''; $expectedContent = <<<'JSON' { @@ -332,9 +331,10 @@ public function mcpConfigPath(): string ->and($capturedContent)->toBe($expectedContent); }); -test('installFileMcp updates existing config file', function () { +test('installFileMcp updates existing config file', function (): void { $environment = Mockery::mock(TestMcpClient::class)->makePartial(); $environment->shouldAllowMockingProtectedMethods(); + $capturedPath = ''; $capturedContent = ''; @@ -384,7 +384,7 @@ public function mcpConfigPath(): string }); -test('installFileMcp works with existing config file using JSON 5', function () { +test('installFileMcp works with existing config file using JSON 5', function (): void { $vscode = new VSCode($this->strategyFactory); $capturedPath = ''; $capturedContent = ''; diff --git a/tests/Unit/Install/CodeEnvironmentsDetectorTest.php b/tests/Unit/Install/CodeEnvironmentsDetectorTest.php index c11133ed..dc46503c 100644 --- a/tests/Unit/Install/CodeEnvironmentsDetectorTest.php +++ b/tests/Unit/Install/CodeEnvironmentsDetectorTest.php @@ -6,16 +6,16 @@ use Laravel\Boost\Install\CodeEnvironmentsDetector; use Laravel\Boost\Install\Enums\Platform; -beforeEach(function () { +beforeEach(function (): void { $this->container = new \Illuminate\Container\Container; $this->detector = new CodeEnvironmentsDetector($this->container); }); -afterEach(function () { +afterEach(function (): void { Mockery::close(); }); -test('discoverSystemInstalledCodeEnvironments returns detected programs', function () { +test('discoverSystemInstalledCodeEnvironments returns detected programs', function (): void { // Create mock programs $program1 = Mockery::mock(CodeEnvironment::class); $program1->shouldReceive('detectOnSystem')->with(Mockery::type(Platform::class))->andReturn(true); @@ -49,7 +49,7 @@ expect($detected)->toBe(['phpstorm', 'cursor']); }); -test('discoverSystemInstalledCodeEnvironments returns empty array when no programs detected', function () { +test('discoverSystemInstalledCodeEnvironments returns empty array when no programs detected', function (): void { $program1 = Mockery::mock(CodeEnvironment::class); $program1->shouldReceive('detectOnSystem')->with(Mockery::type(Platform::class))->andReturn(false); $program1->shouldReceive('name')->andReturn('phpstorm'); @@ -74,7 +74,7 @@ expect($detected)->toBeEmpty(); }); -test('discoverProjectInstalledCodeEnvironments detects programs in project', function () { +test('discoverProjectInstalledCodeEnvironments detects programs in project', function (): void { $basePath = '/path/to/project'; $program1 = Mockery::mock(CodeEnvironment::class); @@ -101,7 +101,7 @@ expect($detected)->toBe(['vscode', 'claudecode']); }); -test('discoverProjectInstalledCodeEnvironments returns empty array when no programs detected in project', function () { +test('discoverProjectInstalledCodeEnvironments returns empty array when no programs detected in project', function (): void { $basePath = '/path/to/project'; $program1 = Mockery::mock(CodeEnvironment::class); @@ -118,7 +118,7 @@ expect($detected)->toBeEmpty(); }); -test('discoverProjectInstalledCodeEnvironments detects applications by directory', function () { +test('discoverProjectInstalledCodeEnvironments detects applications by directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.vscode'); @@ -132,7 +132,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects applications with mixed type', function () { +test('discoverProjectInstalledCodeEnvironments detects applications with mixed type', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); file_put_contents($tempDir.'/CLAUDE.md', 'test'); @@ -145,7 +145,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects copilot with nested file path', function () { +test('discoverProjectInstalledCodeEnvironments detects copilot with nested file path', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.github'); @@ -161,7 +161,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects claude code with directory', function () { +test('discoverProjectInstalledCodeEnvironments detects claude code with directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.claude'); @@ -174,7 +174,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects phpstorm with idea directory', function () { +test('discoverProjectInstalledCodeEnvironments detects phpstorm with idea directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.idea'); @@ -188,7 +188,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects phpstorm with junie directory', function () { +test('discoverProjectInstalledCodeEnvironments detects phpstorm with junie directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.junie'); @@ -202,7 +202,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects cursor with cursor directory', function () { +test('discoverProjectInstalledCodeEnvironments detects cursor with cursor directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.cursor'); @@ -216,7 +216,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects codex with codex directory', function () { +test('discoverProjectInstalledCodeEnvironments detects codex with codex directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.codex'); @@ -229,7 +229,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects codex with AGENTS.md file', function () { +test('discoverProjectInstalledCodeEnvironments detects codex with AGENTS.md file', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); file_put_contents($tempDir.'/AGENTS.md', 'test'); @@ -242,7 +242,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments handles multiple detections', function () { +test('discoverProjectInstalledCodeEnvironments handles multiple detections', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.vscode'); diff --git a/tests/Unit/Install/Detection/CommandDetectionStrategyTest.php b/tests/Unit/Install/Detection/CommandDetectionStrategyTest.php index c8aefd9f..d2fbfcf6 100644 --- a/tests/Unit/Install/Detection/CommandDetectionStrategyTest.php +++ b/tests/Unit/Install/Detection/CommandDetectionStrategyTest.php @@ -6,11 +6,11 @@ use Laravel\Boost\Install\Detection\CommandDetectionStrategy; use Laravel\Boost\Install\Enums\Platform; -beforeEach(function () { +beforeEach(function (): void { $this->strategy = new CommandDetectionStrategy; }); -test('detects command with successful exit code', function () { +test('detects command with successful exit code', function (): void { Process::fake([ 'which php' => Process::result(exitCode: 0), ]); @@ -22,7 +22,7 @@ expect($result)->toBeTrue(); })->skip(); -test('fails for command with non zero exit code', function () { +test('fails for command with non zero exit code', function (): void { Process::fake([ 'which nonexistent' => Process::result(exitCode: 1), ]); @@ -34,7 +34,7 @@ expect($result)->toBeFalse(); })->skip(); -test('returns false when no command config', function () { +test('returns false when no command config', function (): void { $result = $this->strategy->detect([ 'other_config' => 'value', ]); @@ -42,7 +42,7 @@ expect($result)->toBeFalse(); })->skip(); -test('handles command with output', function () { +test('handles command with output', function (): void { Process::fake([ 'echo test' => Process::result(output: 'test', exitCode: 0), ]); @@ -54,7 +54,7 @@ expect($result)->toBeTrue(); })->skip(); -test('handles command with error output', function () { +test('handles command with error output', function (): void { Process::fake([ 'invalid-command' => Process::result(errorOutput: 'command not found', exitCode: 127), ]); @@ -66,7 +66,7 @@ expect($result)->toBeFalse(); })->skip(); -test('works with different platforms parameter', function () { +test('works with different platforms parameter', function (): void { Process::fake([ 'where code' => Process::result(exitCode: 0), ]); diff --git a/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php b/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php index d587bab1..a7feaab1 100644 --- a/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php +++ b/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php @@ -6,17 +6,17 @@ use Laravel\Boost\Install\Detection\CompositeDetectionStrategy; use Laravel\Boost\Install\Enums\Platform; -beforeEach(function () { +beforeEach(function (): void { $this->firstStrategy = Mockery::mock(DetectionStrategy::class); $this->secondStrategy = Mockery::mock(DetectionStrategy::class); $this->thirdStrategy = Mockery::mock(DetectionStrategy::class); }); -afterEach(function () { +afterEach(function (): void { Mockery::close(); }); -test('returns true when first strategy succeeds', function () { +test('returns true when first strategy succeeds', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -36,7 +36,7 @@ expect($result)->toBeTrue(); }); -test('returns true when second strategy succeeds', function () { +test('returns true when second strategy succeeds', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -59,7 +59,7 @@ expect($result)->toBeTrue(); }); -test('returns false when all strategies fail', function () { +test('returns false when all strategies fail', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -89,7 +89,7 @@ expect($result)->toBeFalse(); }); -test('stops execution after first success', function () { +test('stops execution after first success', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -116,7 +116,7 @@ expect($result)->toBeTrue(); }); -test('handles empty strategies array', function () { +test('handles empty strategies array', function (): void { $composite = new CompositeDetectionStrategy([]); $result = $composite->detect(['config' => 'value']); @@ -124,7 +124,7 @@ expect($result)->toBeFalse(); }); -test('handles single strategy', function () { +test('handles single strategy', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -140,7 +140,7 @@ expect($result)->toBeTrue(); }); -test('passes platform parameter to all strategies', function () { +test('passes platform parameter to all strategies', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -163,7 +163,7 @@ expect($result)->toBeFalse(); }); -test('handles null platform parameter', function () { +test('handles null platform parameter', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -179,7 +179,7 @@ expect($result)->toBeTrue(); }); -test('handles mixed strategy types', function () { +test('handles mixed strategy types', function (): void { // This test simulates real-world usage where different strategy types // might be combined (directory, file, command, etc.) diff --git a/tests/Unit/Install/Detection/DetectionStrategyFactoryTest.php b/tests/Unit/Install/Detection/DetectionStrategyFactoryTest.php index 579edc08..840d90aa 100644 --- a/tests/Unit/Install/Detection/DetectionStrategyFactoryTest.php +++ b/tests/Unit/Install/Detection/DetectionStrategyFactoryTest.php @@ -9,30 +9,30 @@ use Laravel\Boost\Install\Detection\DirectoryDetectionStrategy; use Laravel\Boost\Install\Detection\FileDetectionStrategy; -beforeEach(function () { +beforeEach(function (): void { $this->container = new Container; $this->factory = new DetectionStrategyFactory($this->container); }); -test('creates directory strategy from string', function () { +test('creates directory strategy from string', function (): void { $strategy = $this->factory->make('directory'); expect($strategy)->toBeInstanceOf(DirectoryDetectionStrategy::class); }); -test('creates file strategy from string', function () { +test('creates file strategy from string', function (): void { $strategy = $this->factory->make('file'); expect($strategy)->toBeInstanceOf(FileDetectionStrategy::class); }); -test('creates command strategy from string', function () { +test('creates command strategy from string', function (): void { $strategy = $this->factory->make('command'); expect($strategy)->toBeInstanceOf(CommandDetectionStrategy::class); }); -test('creates composite strategy from array of strings', function () { +test('creates composite strategy from array of strings', function (): void { $strategy = $this->factory->make([ 'directory', 'file', @@ -41,7 +41,7 @@ expect($strategy)->toBeInstanceOf(CompositeDetectionStrategy::class); }); -test('creates composite strategy from mixed array', function () { +test('creates composite strategy from mixed array', function (): void { $strategy = $this->factory->make([ 'directory', 'file', @@ -51,18 +51,18 @@ expect($strategy)->toBeInstanceOf(CompositeDetectionStrategy::class); }); -test('throws exception for unknown string type', function () { +test('throws exception for unknown string type', function (): void { expect(fn () => $this->factory->make('unknown')) ->toThrow(InvalidArgumentException::class); }); -test('empty array creates composite strategy', function () { +test('empty array creates composite strategy', function (): void { $strategy = $this->factory->make([]); expect($strategy)->toBeInstanceOf(CompositeDetectionStrategy::class); }); -test('makeFromConfig infers directory type from paths key', function () { +test('makeFromConfig infers directory type from paths key', function (): void { $strategy = $this->factory->makeFromConfig([ 'paths' => ['/some/path'], ]); @@ -70,7 +70,7 @@ expect($strategy)->toBeInstanceOf(DirectoryDetectionStrategy::class); }); -test('makeFromConfig infers file type from files key', function () { +test('makeFromConfig infers file type from files key', function (): void { $strategy = $this->factory->makeFromConfig([ 'files' => ['file.txt'], ]); @@ -78,7 +78,7 @@ expect($strategy)->toBeInstanceOf(FileDetectionStrategy::class); }); -test('makeFromConfig infers command type from command key', function () { +test('makeFromConfig infers command type from command key', function (): void { $strategy = $this->factory->makeFromConfig([ 'command' => 'which code', ]); @@ -86,7 +86,7 @@ expect($strategy)->toBeInstanceOf(CommandDetectionStrategy::class); }); -test('makeFromConfig creates composite strategy from multiple keys', function () { +test('makeFromConfig creates composite strategy from multiple keys', function (): void { $strategy = $this->factory->makeFromConfig([ 'paths' => ['.claude'], 'files' => ['CLAUDE.md'], @@ -95,13 +95,13 @@ expect($strategy)->toBeInstanceOf(CompositeDetectionStrategy::class); }); -test('makeFromConfig throws exception for unknown config keys', function () { +test('makeFromConfig throws exception for unknown config keys', function (): void { expect(fn () => $this->factory->makeFromConfig([ 'unknown_key' => 'value', ]))->toThrow(InvalidArgumentException::class, 'Cannot infer detection type from config keys'); }); -test('makeFromConfig throws exception for empty config', function () { +test('makeFromConfig throws exception for empty config', function (): void { expect(fn () => $this->factory->makeFromConfig([])) ->toThrow(InvalidArgumentException::class, 'Cannot infer detection type from config keys'); }); diff --git a/tests/Unit/Install/Detection/DirectoryDetectionStrategyTest.php b/tests/Unit/Install/Detection/DirectoryDetectionStrategyTest.php index 5e091604..488c01f1 100644 --- a/tests/Unit/Install/Detection/DirectoryDetectionStrategyTest.php +++ b/tests/Unit/Install/Detection/DirectoryDetectionStrategyTest.php @@ -5,19 +5,19 @@ use Laravel\Boost\Install\Detection\DirectoryDetectionStrategy; use Laravel\Boost\Install\Enums\Platform; -beforeEach(function () { +beforeEach(function (): void { $this->strategy = new DirectoryDetectionStrategy; $this->tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($this->tempDir); }); -afterEach(function () { +afterEach(function (): void { if (is_dir($this->tempDir)) { removeDirectory($this->tempDir); } }); -test('detects existing directory', function () { +test('detects existing directory', function (): void { $testDir = $this->tempDir.'/test_app'; mkdir($testDir); @@ -29,7 +29,7 @@ expect($result)->toBeTrue(); }); -test('fails for non existent directory', function () { +test('fails for non existent directory', function (): void { $result = $this->strategy->detect([ 'paths' => ['non_existent'], 'basePath' => $this->tempDir, @@ -38,7 +38,7 @@ expect($result)->toBeFalse(); }); -test('detects absolute path', function () { +test('detects absolute path', function (): void { $testDir = $this->tempDir.'/absolute_test'; mkdir($testDir); @@ -49,7 +49,7 @@ expect($result)->toBeTrue(); }); -test('detects multiple paths first exists', function () { +test('detects multiple paths first exists', function (): void { $testDir = $this->tempDir.'/first_exists'; mkdir($testDir); @@ -61,7 +61,7 @@ expect($result)->toBeTrue(); }); -test('detects multiple paths second exists', function () { +test('detects multiple paths second exists', function (): void { $testDir = $this->tempDir.'/second_exists'; mkdir($testDir); @@ -73,7 +73,7 @@ expect($result)->toBeTrue(); }); -test('fails when no paths exist', function () { +test('fails when no paths exist', function (): void { $result = $this->strategy->detect([ 'paths' => ['missing1', 'missing2'], 'basePath' => $this->tempDir, @@ -82,7 +82,7 @@ expect($result)->toBeFalse(); }); -test('returns false when no paths config', function () { +test('returns false when no paths config', function (): void { $result = $this->strategy->detect([ 'basePath' => $this->tempDir, ]); @@ -90,7 +90,7 @@ expect($result)->toBeFalse(); }); -test('uses current directory when no base path', function () { +test('uses current directory when no base path', function (): void { // This test creates a directory in the current working directory $currentDir = getcwd(); $testDir = $currentDir.'/temp_test_dir'; @@ -107,7 +107,7 @@ } }); -test('detects with glob pattern', function () { +test('detects with glob pattern', function (): void { // Create test directories with patterns mkdir($this->tempDir.'/app_v1'); mkdir($this->tempDir.'/app_v2'); @@ -120,7 +120,7 @@ expect($result)->toBeTrue(); }); -test('fails with glob pattern no matches', function () { +test('fails with glob pattern no matches', function (): void { $result = $this->strategy->detect([ 'paths' => ['nonexistent_*'], 'basePath' => $this->tempDir, @@ -129,7 +129,7 @@ expect($result)->toBeFalse(); }); -test('expands tilde home directory', function () { +test('expands tilde home directory', function (): void { // Mock HOME environment variable $originalHome = getenv('HOME'); putenv('HOME='.$this->tempDir); @@ -152,7 +152,7 @@ } }); -test('expands windows environment variables', function () { +test('expands windows environment variables', function (): void { // Mock environment variable for Windows putenv('TESTVAR='.$this->tempDir); mkdir($this->tempDir.'/windows_test'); @@ -168,7 +168,7 @@ } }); -test('handles missing environment variable on windows', function () { +test('handles missing environment variable on windows', function (): void { $result = $this->strategy->detect([ 'paths' => ['%NONEXISTENT%/test'], ], Platform::Windows); @@ -176,10 +176,9 @@ expect($result)->toBeFalse(); }); -test('identifies absolute paths correctly', function () { +test('identifies absolute paths correctly', function (): void { $reflectionClass = new \ReflectionClass($this->strategy); $isAbsolutePathMethod = $reflectionClass->getMethod('isAbsolutePath'); - $isAbsolutePathMethod->setAccessible(true); // Unix absolute paths expect($isAbsolutePathMethod->invoke($this->strategy, '/usr/local/bin'))->toBeTrue(); @@ -205,5 +204,6 @@ function removeDirectory(string $dir): void $path = $dir.DIRECTORY_SEPARATOR.$file; is_dir($path) ? removeDirectory($path) : unlink($path); } + rmdir($dir); } diff --git a/tests/Unit/Install/Detection/FileDetectionStrategyTest.php b/tests/Unit/Install/Detection/FileDetectionStrategyTest.php index cab3b0df..431035a6 100644 --- a/tests/Unit/Install/Detection/FileDetectionStrategyTest.php +++ b/tests/Unit/Install/Detection/FileDetectionStrategyTest.php @@ -4,19 +4,19 @@ use Laravel\Boost\Install\Detection\FileDetectionStrategy; -beforeEach(function () { +beforeEach(function (): void { $this->strategy = new FileDetectionStrategy; $this->tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($this->tempDir); }); -afterEach(function () { +afterEach(function (): void { if (is_dir($this->tempDir) && str_contains($this->tempDir, sys_get_temp_dir())) { removeDirectoryForFileTests($this->tempDir); } }); -test('detects existing file', function () { +test('detects existing file', function (): void { file_put_contents($this->tempDir.'/test.txt', 'test content'); $result = $this->strategy->detect([ @@ -27,7 +27,7 @@ expect($result)->toBeTrue(); }); -test('fails for non existent file', function () { +test('fails for non existent file', function (): void { $result = $this->strategy->detect([ 'files' => ['non_existent.txt'], 'basePath' => $this->tempDir, @@ -36,7 +36,7 @@ expect($result)->toBeFalse(); }); -test('detects multiple files first exists', function () { +test('detects multiple files first exists', function (): void { file_put_contents($this->tempDir.'/first.txt', 'content'); $result = $this->strategy->detect([ @@ -47,7 +47,7 @@ expect($result)->toBeTrue(); }); -test('detects multiple files second exists', function () { +test('detects multiple files second exists', function (): void { file_put_contents($this->tempDir.'/second.txt', 'content'); $result = $this->strategy->detect([ @@ -58,7 +58,7 @@ expect($result)->toBeTrue(); }); -test('fails when no files exist', function () { +test('fails when no files exist', function (): void { $result = $this->strategy->detect([ 'files' => ['missing1.txt', 'missing2.txt'], 'basePath' => $this->tempDir, @@ -67,7 +67,7 @@ expect($result)->toBeFalse(); }); -test('returns false when no files config', function () { +test('returns false when no files config', function (): void { $result = $this->strategy->detect([ 'basePath' => $this->tempDir, ]); @@ -75,7 +75,7 @@ expect($result)->toBeFalse(); }); -test('uses current directory when no base path', function () { +test('uses current directory when no base path', function (): void { // This test creates a file in the current working directory $currentDir = getcwd(); $testFile = $currentDir.'/temp_test_file.txt'; @@ -92,7 +92,7 @@ } }); -test('detects files in subdirectories', function () { +test('detects files in subdirectories', function (): void { mkdir($this->tempDir.'/subdir'); file_put_contents($this->tempDir.'/subdir/nested.txt', 'content'); @@ -104,7 +104,7 @@ expect($result)->toBeTrue(); }); -test('handles empty files array', function () { +test('handles empty files array', function (): void { $result = $this->strategy->detect([ 'files' => [], 'basePath' => $this->tempDir, @@ -113,7 +113,7 @@ expect($result)->toBeFalse(); }); -test('detects files with special characters', function () { +test('detects files with special characters', function (): void { file_put_contents($this->tempDir.'/file-with_special.chars.txt', 'content'); $result = $this->strategy->detect([ @@ -135,5 +135,6 @@ function removeDirectoryForFileTests(string $dir): void $path = $dir.DIRECTORY_SEPARATOR.$file; is_dir($path) ? removeDirectoryForFileTests($path) : unlink($path); } + rmdir($dir); } diff --git a/tests/Unit/Install/GuidelineWriterTest.php b/tests/Unit/Install/GuidelineWriterTest.php index d84040d1..6cab0991 100644 --- a/tests/Unit/Install/GuidelineWriterTest.php +++ b/tests/Unit/Install/GuidelineWriterTest.php @@ -5,7 +5,7 @@ use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Install\GuidelineWriter; -test('it returns NOOP when guidelines are empty', function () { +test('it returns NOOP when guidelines are empty', function (): void { $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn('/tmp/test.md'); @@ -15,7 +15,7 @@ expect($result)->toBe(GuidelineWriter::NOOP); }); -test('it creates directory when it does not exist', function () { +test('it creates directory when it does not exist', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); $filePath = $tempDir.'/subdir/test.md'; @@ -35,7 +35,7 @@ rmdir($tempDir); }); -test('it throws exception when directory creation fails', function () { +test('it throws exception when directory creation fails', function (): void { // Use a path that cannot be created (root directory with insufficient permissions) $filePath = '/root/boost_test/test.md'; @@ -45,11 +45,11 @@ $writer = new GuidelineWriter($agent); - expect(fn () => $writer->write('test guidelines')) + expect(fn (): int => $writer->write('test guidelines')) ->toThrow(RuntimeException::class, 'Failed to create directory: /root/boost_test'); })->skipOnWindows(); -test('it writes guidelines to new file', function () { +test('it writes guidelines to new file', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); $agent = Mockery::mock(Agent::class); @@ -65,7 +65,7 @@ unlink($tempFile); }); -test('it writes guidelines to existing file without existing guidelines', function () { +test('it writes guidelines to existing file without existing guidelines', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "# Existing content\n\nSome text here."); @@ -82,7 +82,7 @@ unlink($tempFile); }); -test('it replaces existing guidelines in-place', function () { +test('it replaces existing guidelines in-place', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); $initialContent = "# Header\n\n\nold guidelines\n\n\n# Footer"; file_put_contents($tempFile, $initialContent); @@ -100,7 +100,7 @@ unlink($tempFile); }); -test('it handles multiline existing guidelines', function () { +test('it handles multiline existing guidelines', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); $initialContent = "Start\n\nline 1\nline 2\nline 3\n\nEnd"; file_put_contents($tempFile, $initialContent); @@ -119,7 +119,7 @@ unlink($tempFile); }); -test('it handles multiple guideline blocks', function () { +test('it handles multiple guideline blocks', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); $initialContent = "Start\n\nfirst\n\nMiddle\n\nsecond\n\nEnd"; file_put_contents($tempFile, $initialContent); @@ -138,7 +138,7 @@ unlink($tempFile); }); -test('it throws exception when file cannot be opened', function () { +test('it throws exception when file cannot be opened', function (): void { // Use a directory path instead of file path to cause fopen to fail $dirPath = sys_get_temp_dir(); @@ -148,11 +148,11 @@ $writer = new GuidelineWriter($agent); - expect(fn () => $writer->write('test guidelines')) + expect(fn (): int => $writer->write('test guidelines')) ->toThrow(RuntimeException::class, "Failed to open file: {$dirPath}"); })->skipOnWindows(); -test('it preserves file content structure with proper spacing', function () { +test('it preserves file content structure with proper spacing', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); $initialContent = "# Title\n\nParagraph 1\n\nParagraph 2"; file_put_contents($tempFile, $initialContent); @@ -170,7 +170,7 @@ unlink($tempFile); }); -test('it handles empty file', function () { +test('it handles empty file', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, ''); @@ -187,7 +187,7 @@ unlink($tempFile); }); -test('it handles file with only whitespace', function () { +test('it handles file with only whitespace', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, " \n\n \t \n"); @@ -204,7 +204,7 @@ unlink($tempFile); }); -test('it does not interfere with other XML-like tags', function () { +test('it does not interfere with other XML-like tags', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); $initialContent = "# Title\n\n\nShould not be touched\n\n\n\nOld guidelines\n\n\n\nAlso untouched\n"; file_put_contents($tempFile, $initialContent); @@ -223,7 +223,7 @@ unlink($tempFile); }); -test('it preserves user content after guidelines when replacing', function () { +test('it preserves user content after guidelines when replacing', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); $initialContent = "# My Project\n\n\nold guidelines\n\n\n# User Added Section\nThis content was added by the user after the guidelines.\n\n## Another user section\nMore content here."; file_put_contents($tempFile, $initialContent); @@ -253,11 +253,11 @@ unlink($tempFile); }); -test('it retries file locking on contention', function () { +test('it retries file locking on contention', function (): void { expect(true)->toBeTrue(); // Mark as passing for now })->todo(); -test('it adds frontmatter when agent supports it and file has no existing frontmatter', function () { +test('it adds frontmatter when agent supports it and file has no existing frontmatter', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "# Existing content\n\nSome text here."); @@ -274,7 +274,7 @@ unlink($tempFile); }); -test('it does not add frontmatter when agent supports it but file already has frontmatter', function () { +test('it does not add frontmatter when agent supports it but file already has frontmatter', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "---\ncustomOption: true\n---\n# Existing content\n\nSome text here."); @@ -291,7 +291,7 @@ unlink($tempFile); }); -test('it does not add frontmatter when agent does not support it', function () { +test('it does not add frontmatter when agent does not support it', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "# Existing content\n\nSome text here."); diff --git a/tests/Unit/Install/HerdTest.php b/tests/Unit/Install/HerdTest.php index 822d6b53..c60f8e6e 100644 --- a/tests/Unit/Install/HerdTest.php +++ b/tests/Unit/Install/HerdTest.php @@ -6,7 +6,7 @@ $herdTestCleanupData = []; -beforeEach(function () { +beforeEach(function (): void { global $herdTestCleanupData; $herdTestCleanupData = initializeHerdTestEnvironment(); @@ -14,7 +14,7 @@ mkdir($herdTestCleanupData['tempDir'], 0755, true); }); -afterEach(function () { +afterEach(function (): void { global $herdTestCleanupData; foreach ($herdTestCleanupData['originalEnv'] as $key => $value) { @@ -67,7 +67,7 @@ function getHerdTestTempDir(): string return $herdTestCleanupData['tempDir']; } -test('mcpPath builds correct Windows path from USERPROFILE when HOME missing', function () { +test('mcpPath builds correct Windows path from USERPROFILE when HOME missing', function (): void { unset($_SERVER['HOME']); $_SERVER['USERPROFILE'] = 'C:\\Users\\TestUser'; @@ -77,7 +77,7 @@ function getHerdTestTempDir(): string expect($herd->mcpPath())->toBe($expected); })->onlyOnWindows(); -test('isMcpAvailable returns false when MCP file is missing from home', function () { +test('isMcpAvailable returns false when MCP file is missing from home', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -87,7 +87,7 @@ function getHerdTestTempDir(): string expect($herd->isMcpAvailable())->toBeFalse(); })->onlyOnWindows(); -test('isMcpAvailable returns true when MCP file exists in home', function () { +test('isMcpAvailable returns true when MCP file exists in home', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -103,7 +103,7 @@ function getHerdTestTempDir(): string expect($herd->isMcpAvailable())->toBeTrue(); })->onlyOnWindows(); -test('isMcpAvailable returns false after MCP file is removed', function () { +test('isMcpAvailable returns false after MCP file is removed', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -123,7 +123,7 @@ function getHerdTestTempDir(): string expect($herd->isMcpAvailable())->toBeFalse(); })->onlyOnWindows(); -test('getHomePath returns HOME on non-Windows', function () { +test('getHomePath returns HOME on non-Windows', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -133,7 +133,7 @@ function getHerdTestTempDir(): string expect($herd->getHomePath())->toBe($testHome); })->skipOnWindows(); -test('getHomePath uses USERPROFILE on Windows when HOME is not set and normalizes slashes', function () { +test('getHomePath uses USERPROFILE on Windows when HOME is not set and normalizes slashes', function (): void { unset($_SERVER['HOME']); $_SERVER['USERPROFILE'] = 'C:\\Users\\TestUser'; @@ -142,7 +142,7 @@ function getHerdTestTempDir(): string expect($herd->getHomePath())->toBe('C:/Users/TestUser'); })->onlyOnWindows(); -test('isInstalled returns true when herd config directory exists on Windows', function () { +test('isInstalled returns true when herd config directory exists on Windows', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -155,7 +155,7 @@ function getHerdTestTempDir(): string expect($herd->isInstalled())->toBeTrue(); })->onlyOnWindows(); -test('isInstalled returns false when herd config directory is missing on Windows', function () { +test('isInstalled returns false when herd config directory is missing on Windows', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -165,13 +165,13 @@ function getHerdTestTempDir(): string expect($herd->isInstalled())->toBeFalse(); })->onlyOnWindows(); -test('isWindowsPlatform returns true on Windows', function () { +test('isWindowsPlatform returns true on Windows', function (): void { $herd = new Herd; expect($herd->isWindowsPlatform())->toBeTrue(); })->onlyOnWindows(); -test('isWindowsPlatform returns false on non-Windows platforms', function () { +test('isWindowsPlatform returns false on non-Windows platforms', function (): void { $herd = new Herd; expect($herd->isWindowsPlatform())->toBeFalse(); diff --git a/tests/Unit/Install/Mcp/FileWriterTest.php b/tests/Unit/Install/Mcp/FileWriterTest.php index 2f950e90..065eb7d7 100644 --- a/tests/Unit/Install/Mcp/FileWriterTest.php +++ b/tests/Unit/Install/Mcp/FileWriterTest.php @@ -10,23 +10,23 @@ use Mockery; use ReflectionClass; -beforeEach(function () { +beforeEach(function (): void { Mockery::close(); }); -test('constructor sets file path', function () { +test('constructor sets file path', function (): void { $writer = new FileWriter('/path/to/mcp.json'); expect($writer)->toBeInstanceOf(FileWriter::class); }); -test('configKey method returns self for chaining', function () { +test('configKey method returns self for chaining', function (): void { $writer = new FileWriter('/path/to/mcp.json'); $result = $writer->configKey('customKey'); expect($result)->toBe($writer); }); -test('addServer method returns self for chaining', function () { +test('addServer method returns self for chaining', function (): void { $writer = new FileWriter('/path/to/mcp.json'); $result = $writer ->configKey('servers') @@ -35,7 +35,7 @@ expect($result)->toBe($writer); }); -test('save method returns boolean', function () { +test('save method returns boolean', function (): void { mockFileOperations(); $writer = new FileWriter('/path/to/mcp.json'); $result = $writer->save(); @@ -43,7 +43,7 @@ expect($result)->toBe(true); }); -test('written data is correct for brand new file', function (string $configKey, array $servers, string $expectedJson) { +test('written data is correct for brand new file', function (string $configKey, array $servers, string $expectedJson): void { $writtenPath = ''; $writtenContent = ''; mockFileOperations(capturedPath: $writtenPath, capturedContent: $writtenContent); @@ -67,7 +67,7 @@ expect($simpleContents)->toEqual($expectedJson); })->with(newFileServerConfigurations()); -test('updates existing plain JSON file using simple method', function () { +test('updates existing plain JSON file using simple method', function (): void { $writtenPath = ''; $writtenContent = ''; @@ -88,7 +88,7 @@ expect($result)->toBeTrue(); - $decoded = json_decode($writtenContent, true); + $decoded = json_decode((string) $writtenContent, true); expect($decoded)->toHaveKey('existing'); expect($decoded)->toHaveKey('other'); expect($decoded)->toHaveKey('nested.key'); // From fixture @@ -96,7 +96,7 @@ expect($decoded['servers']['new-server']['command'])->toBe('npm'); }); -test('adds to existing mcpServers in plain JSON', function () { +test('adds to existing mcpServers in plain JSON', function (): void { $writtenPath = ''; $writtenContent = ''; @@ -115,14 +115,14 @@ expect($result)->toBeTrue(); - $decoded = json_decode($writtenContent, true); + $decoded = json_decode((string) $writtenContent, true); expect($decoded)->toHaveKey('mcpServers.existing-server'); // Original preserved expect($decoded)->toHaveKey('mcpServers.boost'); // New server added expect($decoded['mcpServers']['boost']['command'])->toBe('php'); }); -test('preserves complex JSON5 features that VS Code supports', function () { +test('preserves complex JSON5 features that VS Code supports', function (): void { $writtenContent = ''; mockFileOperations( @@ -146,7 +146,7 @@ expect($writtenContent)->toContain('MYSQL_HOST'); // Preserve complex nested structure }); -test('detects plain JSON with comments inside strings as safe', function () { +test('detects plain JSON with comments inside strings as safe', function (): void { $writtenContent = ''; mockFileOperations( @@ -163,39 +163,36 @@ expect($result)->toBeTrue(); - $decoded = json_decode($writtenContent, true); + $decoded = json_decode((string) $writtenContent, true); expect($decoded)->toHaveKey('exampleCode'); // Original preserved expect($decoded)->toHaveKey('mcpServers.new-server'); // New server added expect($decoded['exampleCode'])->toContain('// here is the example code'); // Comment in string preserved }); -test('hasUnquotedComments detects comments correctly', function (string $content, bool $expected, string $description) { +test('hasUnquotedComments detects comments correctly', function (string $content, bool $expected, string $description): void { $writer = new FileWriter('/tmp/test.json'); $reflection = new ReflectionClass($writer); $method = $reflection->getMethod('hasUnquotedComments'); - $method->setAccessible(true); $result = $method->invokeArgs($writer, [$content]); expect($result)->toBe($expected, $description); })->with(commentDetectionCases()); -test('trailing comma detection works across newlines', function (string $content, bool $expected, string $description) { +test('trailing comma detection works across newlines', function (string $content, bool $expected, string $description): void { $writer = new FileWriter('/tmp/test.json'); $reflection = new ReflectionClass($writer); $method = $reflection->getMethod('isPlainJson'); - $method->setAccessible(true); $result = $method->invokeArgs($writer, [$content]); expect($result)->toBe($expected, $description); })->with(trailingCommaCases()); -test('generateServerJson creates correct JSON snippet', function () { +test('generateServerJson creates correct JSON snippet', function (): void { $writer = new FileWriter('/tmp/test.json'); $reflection = new ReflectionClass($writer); $method = $reflection->getMethod('generateServerJson'); - $method->setAccessible(true); // Test with simple server $result = $method->invokeArgs($writer, ['boost', ['command' => 'php']]); @@ -220,14 +217,14 @@ }'); }); -test('fixture mcp-no-configkey.json5 is detected as JSON5 and will use injectNewConfigKey', function () { +test('fixture mcp-no-configkey.json5 is detected as JSON5 and will use injectNewConfigKey', function (): void { $content = fixture('mcp-no-configkey.json5'); $writer = new FileWriter('/tmp/test.json'); $reflection = new ReflectionClass($writer); // Verify it's detected as JSON5 (not plain JSON) $isPlainJsonMethod = $reflection->getMethod('isPlainJson'); - $isPlainJsonMethod->setAccessible(true); + $isPlainJson = $isPlainJsonMethod->invokeArgs($writer, [$content]); expect($isPlainJson)->toBeFalse('Should be detected as JSON5 due to comments'); @@ -237,7 +234,7 @@ expect($hasConfigKey)->toBe(0, 'Should not have mcpServers key, triggering injectNewConfigKey'); }); -test('injects new configKey when it does not exist', function () { +test('injects new configKey when it does not exist', function (): void { $writtenContent = ''; mockFileOperations( @@ -259,7 +256,7 @@ expect($writtenContent)->toContain('// No mcpServers key at all'); // Preserve existing comments }); -test('injects into existing configKey preserving JSON5 features', function () { +test('injects into existing configKey preserving JSON5 features', function (): void { $writtenContent = ''; mockFileOperations( @@ -283,7 +280,7 @@ expect($writtenContent)->toContain('// Ooo, pretty cool'); // Inline comments preserved }); -test('injecting twice into existing JSON 5 doesn\'t cause duplicates', function () { +test("injecting twice into existing JSON 5 doesn't cause duplicates", function (): void { $capturedContent = ''; File::clearResolvedInstances(); @@ -337,7 +334,7 @@ expect($boostCounts)->toBe(1); }); -test('injects into empty configKey object', function () { +test('injects into empty configKey object', function (): void { $writtenContent = ''; mockFileOperations( @@ -358,7 +355,7 @@ expect($writtenContent)->toContain('test_input'); // Existing content preserved }); -test('preserves trailing commas when injecting into existing servers', function () { +test('preserves trailing commas when injecting into existing servers', function (): void { $writtenContent = ''; mockFileOperations( @@ -380,7 +377,7 @@ ->and($writtenContent)->toContain('arg1'); // Existing args preserved }); -test('detectIndentation works correctly with various patterns', function (string $content, int $position, int $expected, string $description) { +test('detectIndentation works correctly with various patterns', function (string $content, int $position, int $expected, string $description): void { $writer = new FileWriter('/tmp/test.json'); $result = $writer->detectIndentation($content, $position); diff --git a/tests/Unit/UnitTest.php b/tests/Unit/UnitTest.php index 4d06a51b..d831a1f3 100644 --- a/tests/Unit/UnitTest.php +++ b/tests/Unit/UnitTest.php @@ -2,6 +2,6 @@ declare(strict_types=1); -it('is true', function () { +it('is true', function (): void { expect(true)->toBeTrue(); });