From 968edb26337fb89032ac7fe832e4a7dce1294d06 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 18 Sep 2025 14:23:55 +0100 Subject: [PATCH 01/10] docs: updates changelog --- CHANGELOG.md | 271 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bc8f119 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,271 @@ +# Release Notes + +## [Unreleased](https://github.com/laravel/boost/compare/v1.1.5...main) + +## [v1.2.0](https://github.com/laravel/boost/compare/v1.1.5...v1.2.0) - 2025-09-18 + +* uses latest version of laravel mcp + +## [v1.1.5](https://github.com/laravel/boost/compare/v1.1.4...v1.1.5) - 2025-09-18 + +### What's Changed + +* docs: README: light/dark mode logo by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/148 +* ci: remove unneeded SSH keys now MCP package is public by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/255 +* fix: remove stray parenthesis in lifecycle hook guidance in livewire guidelines by [@mohammedyh](https://github.com/mohammedyh) in https://github.com/laravel/boost/pull/261 +* fix: correct syntax in Tailwind v4 import code snippet by [@mr-chetan](https://github.com/mr-chetan) in https://github.com/laravel/boost/pull/221 +* tests: convert multiple expectations into chain by [@felipeArnold](https://github.com/felipeArnold) in https://github.com/laravel/boost/pull/232 +* Add Codex Guideline Support by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/boost/pull/258 +* Update scroll value for Agent selection box by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/boost/pull/262 +* Add support for Vite CSP nonce by [@nckrtl](https://github.com/nckrtl) in https://github.com/laravel/boost/pull/142 + +### New Contributors + +* [@mohammedyh](https://github.com/mohammedyh) made their first contribution in https://github.com/laravel/boost/pull/261 +* [@mr-chetan](https://github.com/mr-chetan) made their first contribution in https://github.com/laravel/boost/pull/221 +* [@nckrtl](https://github.com/nckrtl) made their first contribution in https://github.com/laravel/boost/pull/142 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.1.4...v1.1.5 + +## [v1.1.4](https://github.com/laravel/boost/compare/v1.1.3...v1.1.4) - 2025-09-04 + +### What's Changed + +* feat: add windows to tests CI check by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/244 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.1.3...v1.1.4 + +## [v1.1.3](https://github.com/laravel/boost/compare/v1.1.2...v1.1.3) - 2025-09-04 + +### What's Changed + +* fix: package priorities should work on php8.1 by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/243 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.1.2...v1.1.3 + +## [v1.1.2](https://github.com/laravel/boost/compare/v1.1.1...v1.1.2) - 2025-09-04 + +### What's Changed + +* feat: add package priority guideline inclusion by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/242 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.1.1...v1.1.2 + +## [v1.1.1](https://github.com/laravel/boost/compare/v1.1.0...v1.1.1) - 2025-09-04 + +### What's Changed + +* Add strict types declaration in Inertia.php by [@felipeArnold](https://github.com/felipeArnold) in https://github.com/laravel/boost/pull/229 +* feat: update roster requirement, fixes #237 now phpunit will be detected by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/239 + +### New Contributors + +* [@felipeArnold](https://github.com/felipeArnold) made their first contribution in https://github.com/laravel/boost/pull/229 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.1.0...v1.1.1 + +## [v1.1.0](https://github.com/laravel/boost/compare/v1.0.21...v1.1.0) - 2025-09-04 + +### What's Changed + +* Always-on process isolation: eliminate conditional complexity by [@andreilungeanu](https://github.com/andreilungeanu) in https://github.com/laravel/boost/pull/184 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.21...v1.1.0 + +## [v1.0.21](https://github.com/laravel/boost/compare/v1.0.20...v1.0.21) - 2025-09-03 + +### What's Changed + +* Fix random 'parse error' when running test suite by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/223 +* Clarify ListRoutes name parameter description for better tool calling by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/boost/pull/182 +* Streamline ToolResult assertions in tests by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/boost/pull/225 +* Allow guideline overriding by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/219 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.20...v1.0.21 + +## [v1.0.20](https://github.com/laravel/boost/compare/v1.0.19...v1.0.20) - 2025-08-28 + +### What's Changed + +* fix: defer InjectBoost middleware registration until app is booted by [@Sairahcaz](https://github.com/Sairahcaz) in https://github.com/laravel/boost/pull/172 +* feat: add robust MCP file configuration writer by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/204 +* Feat: Detect env changes by default, fixes 130 by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/217 + +### New Contributors + +* [@Sairahcaz](https://github.com/Sairahcaz) made their first contribution in https://github.com/laravel/boost/pull/172 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.19...v1.0.20 + +## [v1.0.19](https://github.com/laravel/boost/compare/v1.0.18...v1.0.19) - 2025-08-27 + +### What's Changed + +* Refactor creating laravel application instance using Testbench by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/boost/pull/127 +* Fix Tailwind CSS title on README.md for consistency by [@xavizera](https://github.com/xavizera) in https://github.com/laravel/boost/pull/159 +* feat: don't run Boost during testing by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/144 +* Hide Internal Command `ExecuteToolCommand.php` from Artisan List by [@yitzwillroth](https://github.com/yitzwillroth) in https://github.com/laravel/boost/pull/155 +* chore: removes non necessary php version constrant by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/boost/pull/166 +* chore: removes non necessary pint version constrant by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/boost/pull/167 +* Do not autoload classes while boost:install by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/boost/pull/180 +* fix: prevent unwanted "null" file creation on Windows during installation by [@andreilungeanu](https://github.com/andreilungeanu) in https://github.com/laravel/boost/pull/189 +* Improve `InjectBoost` middleware for response-type handling by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/boost/pull/179 +* docs: README: Add Nova 4.x and 5.x by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/213 +* refactor: change ./artisan to artisan by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/214 +* feat: guidelines: add Inertia form guidelines by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/211 + +### New Contributors + +* [@crynobone](https://github.com/crynobone) made their first contribution in https://github.com/laravel/boost/pull/127 +* [@xavizera](https://github.com/xavizera) made their first contribution in https://github.com/laravel/boost/pull/159 +* [@nunomaduro](https://github.com/nunomaduro) made their first contribution in https://github.com/laravel/boost/pull/166 +* [@andreilungeanu](https://github.com/andreilungeanu) made their first contribution in https://github.com/laravel/boost/pull/189 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.18...v1.0.19 + +## [v1.0.18](https://github.com/laravel/boost/compare/v1.0.17...v1.0.18) - 2025-08-16 + +### What's Changed + +* fix: Prevent install command from breaking when `/tests` doesn't exist by [@sagalbot](https://github.com/sagalbot) in https://github.com/laravel/boost/pull/93 +* [1.x] Add enabled option to `config/boost.php`. by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/boost/pull/143 + +### New Contributors + +* [@sagalbot](https://github.com/sagalbot) made their first contribution in https://github.com/laravel/boost/pull/93 +* [@xiCO2k](https://github.com/xiCO2k) made their first contribution in https://github.com/laravel/boost/pull/143 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.17...v1.0.18 + +## [v1.0.17](https://github.com/laravel/boost/compare/v1.0.16...v1.0.17) - 2025-08-14 + +### What's Changed + +* Fix: Replace APP_DEBUG with environment-based gating by [@eduardocruz](https://github.com/eduardocruz) in https://github.com/laravel/boost/pull/90 + +### New Contributors + +* [@eduardocruz](https://github.com/eduardocruz) made their first contribution in https://github.com/laravel/boost/pull/90 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.16...v1.0.17 + +## [v1.0.16](https://github.com/laravel/boost/compare/v1.0.15...v1.0.16) - 2025-08-14 + +### What's Changed + +* refactor: streamline path resolution and simplify the MCP client interface by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/boost/pull/111 +* Fix PHPStorm using absolute paths by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/109 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.15...v1.0.16 + +## [v1.0.15](https://github.com/laravel/boost/compare/v1.0.14...v1.0.15) - 2025-08-14 + +### What's Changed + +* fixes #67 by only finding files that begin with an uppercase letter by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/116 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.14...v1.0.15 + +## [v1.0.14](https://github.com/laravel/boost/compare/v1.0.13...v1.0.14) - 2025-08-14 + +### What's Changed + +* Fixes #85 by adding verbatim to flux component example by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/114 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.13...v1.0.14 + +## [v1.0.13](https://github.com/laravel/boost/compare/v1.0.12...v1.0.13) - 2025-08-14 + +### What's Changed + +* Fix volt blade parsing by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/112 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.12...v1.0.13 + +## [v1.0.12](https://github.com/laravel/boost/compare/v1.0.11...v1.0.12) - 2025-08-14 + +### What's Changed + +* tool: tinker: try to nudge away from creating test users ahead of time by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/108 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.11...v1.0.12 + +## [v1.0.11](https://github.com/laravel/boost/compare/v1.0.10...v1.0.11) - 2025-08-14 + +### What's Changed + +* tools: report-feedback: strengthen language on privacy by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/103 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.10...v1.0.11 + +## [v1.0.10](https://github.com/laravel/boost/compare/v1.0.9...v1.0.10) - 2025-08-14 + +### What's Changed + +* fixes #70 - make sure foundational rules are composed by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/84 +* Update the bug report template's system info section by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/boost/pull/98 +* Update Filament Guidelines by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/boost/pull/35 +* Fix: Prevent autoloading non class-like files during discovery to avoid "FatalError: Cannot redeclare function" by [@zdearo](https://github.com/zdearo) in https://github.com/laravel/boost/pull/99 + +### New Contributors + +* [@zdearo](https://github.com/zdearo) made their first contribution in https://github.com/laravel/boost/pull/99 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.9...v1.0.10 + +## [v1.0.9](https://github.com/laravel/boost/compare/v1.0.8...v1.0.9) - 2025-08-13 + +### What's Changed + +* fixes #80 - install Boost MCP into Claude via file instead of shell by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/82 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.8...v1.0.9 + +## [v1.0.8](https://github.com/laravel/boost/compare/v1.0.3...v1.0.8) - 2025-08-13 + +### What's Changed + +* fixes #80 by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/81 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.7...v1.0.8 + +## [v1.0.3](https://github.com/laravel/boost/compare/v1.0.2...v1.0.3) - 2025-08-13 + +### What's Changed + +* Update Pint Guideline to Use `--dirty` Flag by [@yitzwillroth](https://github.com/yitzwillroth) in https://github.com/laravel/boost/pull/43 +* docs: README: add filament by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/58 +* Fix Herd detection by [@mpociot](https://github.com/mpociot) in https://github.com/laravel/boost/pull/61 +* fix #49: disable boost inject if HTML isn't expected by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/60 + +### New Contributors + +* [@yitzwillroth](https://github.com/yitzwillroth) made their first contribution in https://github.com/laravel/boost/pull/43 +* [@mpociot](https://github.com/mpociot) made their first contribution in https://github.com/laravel/boost/pull/61 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.2...v1.0.3 + +## [v1.0.2](https://github.com/laravel/boost/compare/v1.0.1...v1.0.2) - 2025-08-13 + +### What's Changed + +* update laravel/roster version by [@ashleyhindle](https://github.com/ashleyhindle) in https://github.com/laravel/boost/pull/42 +* Update core.blade.php by [@meatpaste](https://github.com/meatpaste) in https://github.com/laravel/boost/pull/41 + +### New Contributors + +* [@meatpaste](https://github.com/meatpaste) made their first contribution in https://github.com/laravel/boost/pull/41 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.1...v1.0.2 + +## [v1.0.1](https://github.com/laravel/boost/compare/v1.0.0...v1.0.1) - 2025-08-13 + +**Full Changelog**: https://github.com/laravel/boost/compare/v1.0.0...v1.0.1 + +## [v1.0.0](https://github.com/laravel/boost/compare/v0.1.0...v1.0.0) - 2025-08-13 + +- Initial release of Laravel Boost. + +## v0.1.0 (202x-xx-xx) + +Initial pre-release. From 2d1ae6f6d99ee17f1b6c51209a1ab93892df8c29 Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Thu, 18 Sep 2025 14:54:43 +0100 Subject: [PATCH 02/10] Update roster to 0.2.7 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ad30c14..7be1798 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", "laravel/mcp": "^0.2.0", "laravel/prompts": "0.1.25|^0.3.6", - "laravel/roster": "^0.2.6" + "laravel/roster": "^0.2.7" }, "require-dev": { "laravel/pint": "1.20", From d33a84fbfd078563125c06c742926d9ba3c8b932 Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Thu, 18 Sep 2025 20:11:00 +0100 Subject: [PATCH 03/10] docs: guidelines: initial laravel/mcp guidelines (#267) * docs: guidelines: initial laravel/mcp guidelines Our documentation is great, so the guidelines don't need to be very detailed * docs: guidelines: MCP - add http/https node issue * Update core.blade.php --------- Co-authored-by: Taylor Otwell --- .ai/mcp/core.blade.php | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .ai/mcp/core.blade.php diff --git a/.ai/mcp/core.blade.php b/.ai/mcp/core.blade.php new file mode 100644 index 0000000..22ce6ac --- /dev/null +++ b/.ai/mcp/core.blade.php @@ -0,0 +1,7 @@ +## Laravel MCP + +- MCP (Model Context Protocol) is very new. You must use the `search-docs` tool to get documentation for how to write and test Laravel MCP servers, tools, resources, and prompts effectively. +- MCP servers need to be registered with a route or handle in `routes/ai.php`. Typically, they will be registered using `Mcp::web()` to register a HTTP streaming MCP server. +- Servers are very testable - use the `search-docs` tool to find testing instructions. +- Do not run `mcp:start`. This command hangs waiting for JSON RPC MCP requests. +- Some MCP clients use Node, which has its own certificate store. If a user tries to connect to their web MCP server locally using https://, it could fail due to this reason. They will need to switch to http:// during local development. From ec96dedda0eb0ec2a511a0a9038c3c1eb8dddef8 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Mon, 22 Sep 2025 16:37:15 +0530 Subject: [PATCH 04/10] Update Pint Config and Add Rector * Add Pint * Add Rector * Fix Tests --------- Co-authored-by: Ashley Hindle --- all.php | 6 +- composer.json | 15 +- phpunit.xml.dist | 6 +- pint.json | 218 +----------------- rector.php | 29 +++ src/BoostServiceProvider.php | 23 +- src/Concerns/MakesHttpRequests.php | 6 +- src/Concerns/ReadsLogs.php | 4 +- src/Console/ExecuteToolCommand.php | 6 +- src/Console/InstallCommand.php | 40 ++-- src/Contracts/Agent.php | 4 - src/Contracts/McpClient.php | 10 +- src/Install/Assists/Inertia.php | 22 +- src/Install/Cli/DisplayHelper.php | 55 +++-- .../CodeEnvironment/CodeEnvironment.php | 23 +- src/Install/CodeEnvironmentsDetector.php | 11 +- src/Install/Contracts/DetectionStrategy.php | 2 +- .../Detection/CompositeDetectionStrategy.php | 6 +- .../Detection/DetectionStrategyFactory.php | 8 +- .../Detection/DirectoryDetectionStrategy.php | 4 +- src/Install/GuidelineAssist.php | 16 +- src/Install/GuidelineComposer.php | 32 ++- src/Install/GuidelineWriter.php | 14 +- src/Install/Mcp/FileWriter.php | 45 ++-- src/Mcp/Boost.php | 15 +- src/Mcp/Methods/CallToolWithExecutor.php | 10 +- src/Mcp/Resources/ApplicationInfo.php | 4 +- src/Mcp/ToolExecutor.php | 15 +- src/Mcp/ToolRegistry.php | 4 +- src/Mcp/Tools/ApplicationInfo.php | 6 +- src/Mcp/Tools/DatabaseQuery.php | 11 +- src/Mcp/Tools/DatabaseSchema.php | 16 +- .../DatabaseSchema/DatabaseSchemaDriver.php | 7 +- .../DatabaseSchema/MySQLSchemaDriver.php | 2 +- .../DatabaseSchema/PostgreSQLSchemaDriver.php | 2 +- .../DatabaseSchema/SQLiteSchemaDriver.php | 2 +- src/Mcp/Tools/LastError.php | 2 +- src/Mcp/Tools/ListArtisanCommands.php | 2 +- src/Mcp/Tools/ListAvailableConfigKeys.php | 3 +- src/Mcp/Tools/ListRoutes.php | 2 +- src/Mcp/Tools/ReadLogEntries.php | 1 + src/Mcp/Tools/ReportFeedback.php | 2 +- src/Mcp/Tools/SearchDocs.php | 21 +- src/Mcp/Tools/Tinker.php | 12 +- src/Middleware/InjectBoost.php | 8 +- tests/ArchTest.php | 4 +- tests/Feature/BoostServiceProviderTest.php | 28 +-- .../Console/InstallCommandMultiselectTest.php | 6 +- .../CodeEnvironmentPathResolutionTest.php | 8 +- .../CommandDetectionStrategyTest.php | 16 +- .../Feature/Install/GuidelineComposerTest.php | 32 +-- tests/Feature/Mcp/ToolExecutorTest.php | 27 ++- tests/Feature/Mcp/ToolRegistryTest.php | 8 +- .../Feature/Mcp/Tools/ApplicationInfoTest.php | 4 +- tests/Feature/Mcp/Tools/BrowserLogsTest.php | 45 ++-- .../Mcp/Tools/DatabaseConnectionsTest.php | 6 +- .../Feature/Mcp/Tools/DatabaseSchemaTest.php | 18 +- .../Feature/Mcp/Tools/GetAbsoluteUrlTest.php | 16 +- tests/Feature/Mcp/Tools/GetConfigTest.php | 10 +- .../Mcp/Tools/ListArtisanCommandsTest.php | 4 +- .../Mcp/Tools/ListAvailableConfigKeysTest.php | 10 +- tests/Feature/Mcp/Tools/ListRoutesTest.php | 52 ++--- tests/Feature/Mcp/Tools/SearchDocsTest.php | 60 ++--- tests/Feature/Mcp/Tools/TinkerTest.php | 30 ++- tests/Feature/Middleware/InjectBoostTest.php | 37 ++- tests/Pest.php | 4 +- tests/Unit/Install/Cli/DisplayHelperTest.php | 32 +-- .../CodeEnvironment/CodeEnvironmentTest.php | 60 ++--- .../Install/CodeEnvironmentsDetectorTest.php | 32 +-- .../CommandDetectionStrategyTest.php | 106 ++++----- .../CompositeDetectionStrategyTest.php | 22 +- .../DetectionStrategyFactoryTest.php | 30 +-- .../DirectoryDetectionStrategyTest.php | 36 +-- .../Detection/FileDetectionStrategyTest.php | 25 +- tests/Unit/Install/GuidelineWriterTest.php | 40 ++-- tests/Unit/Install/HerdTest.php | 24 +- tests/Unit/Install/Mcp/FileWriterTest.php | 60 +++-- tests/Unit/UnitTest.php | 2 +- 78 files changed, 712 insertions(+), 934 deletions(-) create mode 100644 rector.php diff --git a/all.php b/all.php index 0d2aeea..1d5f404 100644 --- a/all.php +++ b/all.php @@ -32,7 +32,7 @@ public function packages(): \Laravel\Roster\PackageCollection $packageName = basename($dir); // Skip special directories handled elsewhere in GuidelineComposer - if (in_array($packageName, ['boost', 'herd'])) { + if (in_array($packageName, ['boost', 'herd'], true)) { continue; } @@ -88,10 +88,10 @@ public function packages(): \Laravel\Roster\PackageCollection } }; -$herd = new Herd(); +$herd = new Herd; // Create GuidelineComposer with all config options enabled to get ALL guidelines -$config = new GuidelineConfig(); +$config = new GuidelineConfig; $config->laravelStyle = true; $config->hasAnApi = true; $config->caresAboutLocalization = true; diff --git a/composer.json b/composer.json index 7be1798..133282f 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,12 +53,16 @@ }, "scripts": { "lint": [ - "vendor/bin/pint", - "vendor/bin/phpstan" + "pint", + "phpstan --memory-limit=-1", + "rector" ], - "test": [ - "vendor/bin/pest" + "test": "pest", + "test:lint": [ + "pint --test", + "rector --dry-run" ], + "test:types": "phpstan", "check": [ "@composer lint", "@composer test" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 703a4a4..ec2edef 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/pint.json b/pint.json index be0732e..b37a4e4 100644 --- a/pint.json +++ b/pint.json @@ -1,212 +1,18 @@ { - "preset": "empty", + "preset": "laravel", "rules": { - "align_multiline_comment": true, - "array_indentation": true, - "array_syntax": { - "syntax": "short" + "global_namespace_import": { + "import_classes": true, + "import_constants": true }, - "binary_operator_spaces": { - "default": "single_space" - }, - "blank_line_after_namespace": true, - "blank_line_after_opening_tag": true, - "blank_line_before_statement": { - "statements": [ - "return" - ] - }, - "blank_line_between_import_groups": true, - "blank_lines_before_namespace": true, - "braces_position": { - "control_structures_opening_brace": "same_line", - "functions_opening_brace": "next_line_unless_newline_at_signature_end", - "anonymous_functions_opening_brace": "same_line", - "classes_opening_brace": "next_line_unless_newline_at_signature_end", - "anonymous_classes_opening_brace": "next_line_unless_newline_at_signature_end", - "allow_single_line_empty_anonymous_classes": false, - "allow_single_line_anonymous_functions": false - }, - "cast_spaces": true, - "class_definition": true, - "class_reference_name_casing": true, - "clean_namespace": true, - "compact_nullable_type_declaration": true, - "concat_space": true, - "constant_case": { - "case": "lower" - }, - "control_structure_braces": true, - "declare_equal_normalize": true, - "elseif": true, - "encoding": true, + "phpdoc_types": false, + "combine_consecutive_issets": true, + "combine_consecutive_unsets": true, "explicit_string_variable": true, - "full_opening_tag": true, - "function_declaration": true, - "heredoc_to_nowdoc": true, - "include": true, - "increment_style": { - "style": "post" - }, - "indentation_type": true, - "integer_literal_case": true, - "lambda_not_used_import": true, - "line_ending": true, - "list_syntax": { - "syntax": "short" - }, - "lowercase_cast": true, - "lowercase_keywords": true, - "lowercase_static_reference": true, - "magic_constant_casing": true, - "magic_method_casing": true, - "method_argument_space": { - "on_multiline": "ignore" - }, "method_chaining_indentation": true, - "multiline_whitespace_before_semicolons": { - "strategy": "no_multi_line" - }, - "native_function_casing": true, - "native_type_declaration_casing": true, - "no_alternative_syntax": true, - "no_binary_string": true, - "no_blank_lines_after_class_opening": true, - "no_blank_lines_after_phpdoc": true, - "no_closing_tag": true, - "no_empty_phpdoc": true, - "no_empty_statement": true, - "no_extra_blank_lines": { - "tokens": [ - "extra", - "throw", - "use" - ] - }, - "no_leading_import_slash": true, - "no_leading_namespace_whitespace": true, - "no_mixed_echo_print": { - "use": "echo" - }, - "no_multiline_whitespace_around_double_arrow": true, - "no_short_bool_cast": true, - "no_singleline_whitespace_before_semicolons": true, - "no_space_around_double_colon": true, - "no_spaces_around_offset": { - "positions": [ - "inside", - "outside" - ] - }, - "no_spaces_after_function_name": true, - "no_trailing_comma_in_singleline": true, - "no_trailing_whitespace": true, - "no_trailing_whitespace_in_comment": true, - "no_unneeded_braces": true, - "no_unneeded_control_parentheses": true, - "no_unneeded_import_alias": true, - "no_unset_cast": true, - "no_unused_imports": true, - "no_useless_return": true, - "no_whitespace_before_comma_in_array": true, - "no_whitespace_in_blank_line": true, - "normalize_index_brace": true, - "not_operator_with_successor_space": true, - "nullable_type_declaration_for_default_null_value": true, - "object_operator_without_whitespace": true, - "ordered_imports": { - "sort_algorithm": "alpha", - "imports_order": [ - "const", - "class", - "function" - ] - }, - "phpdoc_align": { - "align": "left", - "spacing": { - "param": 1 - } - }, - "phpdoc_indent": true, - "phpdoc_inline_tag_normalizer": true, - "phpdoc_no_access": true, - "phpdoc_no_package": true, - "phpdoc_no_useless_inheritdoc": true, - "phpdoc_order": { - "order": [ - "param", - "return", - "throws" - ] - }, - "phpdoc_return_self_reference": true, - "phpdoc_scalar": true, - "phpdoc_separation": { - "groups": [ - [ - "deprecated", - "link", - "see", - "since" - ], - [ - "author", - "copyright", - "license" - ], - [ - "category", - "package", - "subpackage" - ], - [ - "property", - "property-read", - "property-write" - ], - [ - "param", - "return" - ] - ] - }, - "phpdoc_single_line_var_spacing": true, - "phpdoc_summary": true, - "phpdoc_trim": true, - "phpdoc_types": true, - "phpdoc_var_without_name": true, - "return_type_declaration": { - "space_before": "none" - }, - "short_scalar_cast": true, - "single_blank_line_at_eof": true, - "single_class_element_per_statement": true, - "single_import_per_statement": true, - "single_line_after_imports": true, - "single_line_comment_style": true, - "single_quote": true, - "space_after_semicolon": true, - "spaces_inside_parentheses": true, - "standardize_not_equals": true, - "switch_case_semicolon_to_colon": true, - "switch_case_space": true, - "switch_continue_to_break": true, - "ternary_operator_spaces": true, - "trailing_comma_in_multiline": true, - "trim_array_spaces": true, - "type_declaration_spaces": true, - "types_spaces": true, - "unary_operator_spaces": true, - "visibility_required": { - "elements": [ - "method", - "property" - ] - }, - "whitespace_after_comma_in_array": true - }, - "notPath": [ - "stubs/tool.stub.php" - ] + "no_binary_string": false, + "strict_comparison": true, + "strict_param": true, + "ternary_to_null_coalescing": true + } } diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..35c4e0a --- /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 4bd4e29..13db699 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 71b8b8e..cb7139c 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; @@ -30,7 +30,7 @@ public function get(string $url): Response } /** - * @param array $json + * @param array $json */ public function json(string $url, array $json): Response { diff --git a/src/Concerns/ReadsLogs.php b/src/Concerns/ReadsLogs.php index b14cf9d..8aaa475 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 28478b5..e081526 100644 --- a/src/Console/ExecuteToolCommand.php +++ b/src/Console/ExecuteToolCommand.php @@ -32,7 +32,7 @@ public function handle(): int } // Decode arguments - $arguments = json_decode(base64_decode($argumentsEncoded), true); + $arguments = json_decode(base64_decode($argumentsEncoded, true), true); if (json_last_error() !== JSON_ERROR_NONE) { $this->error('Invalid arguments format: '.json_last_error_msg()); @@ -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 ff179b5..da09e2a 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/Contracts/Agent.php b/src/Contracts/Agent.php index 75a55b0..f6bbf79 100644 --- a/src/Contracts/Agent.php +++ b/src/Contracts/Agent.php @@ -11,8 +11,6 @@ interface Agent { /** * Get the display name of the Agent. - * - * @return string|null */ public function agentName(): ?string; @@ -25,8 +23,6 @@ public function guidelinesPath(): string; /** * Determine if the guideline file requires frontmatter. - * - * @return bool */ public function frontmatter(): bool; } diff --git a/src/Contracts/McpClient.php b/src/Contracts/McpClient.php index 68c106b..5015952 100644 --- a/src/Contracts/McpClient.php +++ b/src/Contracts/McpClient.php @@ -11,8 +11,6 @@ interface McpClient { /** * Get the display name of the MCP (Model Context Protocol) client. - * - * @return string|null */ public function mcpClientName(): ?string; @@ -34,10 +32,10 @@ public function getArtisanPath(): string; /** * Install an MCP server configuration in this IDE. * - * @param string $key Server identifier/name - * @param string $command Executable command to run the MCP server - * @param array $args Command line arguments - * @param array $env Environment variables + * @param string $key Server identifier/name + * @param string $command Executable command to run the MCP server + * @param array $args Command line arguments + * @param array $env Environment variables * @return bool True if installation succeeded, false otherwise */ public function installMcp(string $key, string $command, array $args = [], array $env = []): bool; diff --git a/src/Install/Assists/Inertia.php b/src/Install/Assists/Inertia.php index 9fbcd16..b490636 100644 --- a/src/Install/Assists/Inertia.php +++ b/src/Install/Assists/Inertia.php @@ -9,17 +9,23 @@ class Inertia { - public function __construct(private Roster $roster) - { - } + 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 78457ed..09a934c 100644 --- a/src/Install/Cli/DisplayHelper.php +++ b/src/Install/Cli/DisplayHelper.php @@ -9,38 +9,54 @@ class DisplayHelper { private const UNICODE_TOP_LEFT = '╭'; + private const UNICODE_TOP_RIGHT = '╮'; + private const UNICODE_BOTTOM_LEFT = '╰'; + private const UNICODE_BOTTOM_RIGHT = '╯'; + private const UNICODE_HORIZONTAL = '─'; + private const UNICODE_VERTICAL = '│'; + private const UNICODE_CROSS = '┼'; + private const UNICODE_TOP_T = '┬'; + private const UNICODE_BOTTOM_T = '┴'; + private const UNICODE_LEFT_T = '├'; + private const UNICODE_RIGHT_T = '┤'; private const BORDER_TOP = 'top'; + private const BORDER_MIDDLE = 'middle'; + private const BORDER_BOTTOM = 'bottom'; private const CELL_PADDING = 2; + private const GRID_CELL_PADDING = 4; + private const ANSI_BOLD = "\e[1m"; + private const ANSI_RESET = "\e[0m"; + private const SPACE = ' '; /** - * @param array> $data + * @param array> $data */ 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; @@ -53,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++; } @@ -61,11 +78,11 @@ public static function datatable(array $data, int $maxWidth = 80): void } /** - * @param array $items + * @param array $items */ public static function grid(array $items, int $maxWidth = 80): void { - if (empty($items)) { + if ($items === []) { return; } @@ -88,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++; } @@ -95,9 +113,9 @@ public static function grid(array $items, int $maxWidth = 80): void echo self::SPACE.self::buildBorder($cellWidths, $leftChar, $rightChar, $joinChar).PHP_EOL; } - private static function getBorderChars(string $type): array - { - return match($type) { + private static function getBorderChars(string $type): array + { + return match ($type) { self::BORDER_TOP => [self::UNICODE_TOP_LEFT, self::UNICODE_TOP_RIGHT, self::UNICODE_TOP_T], self::BORDER_MIDDLE => [self::UNICODE_LEFT_T, self::UNICODE_RIGHT_T, self::UNICODE_CROSS], self::BORDER_BOTTOM => [self::UNICODE_BOTTOM_LEFT, self::UNICODE_BOTTOM_RIGHT, self::UNICODE_BOTTOM_T], @@ -106,7 +124,7 @@ private static function getBorderChars(string $type): array } /** - * @param array> $data + * @param array> $data * @return array */ private static function calculateColumnWidths(array $data): array @@ -123,7 +141,7 @@ private static function calculateColumnWidths(array $data): array } /** - * @param array $widths + * @param array $widths */ private static function buildBorder(array $widths, string $leftChar, string $rightChar, string $joinChar): string { @@ -134,14 +152,13 @@ private static function buildBorder(array $widths, string $leftChar, string $rig $border .= $joinChar; } } - $border .= $rightChar; - return $border; + return $border.$rightChar; } /** - * @param array $row - * @param array $columnWidths + * @param array $row + * @param array $columnWidths */ private static function buildDataRow(array $row, array $columnWidths): string { @@ -158,25 +175,23 @@ private static function buildDataRow(array $row, array $columnWidths): string } /** - * @param array $row + * @param array $row */ private static function buildGridRow(array $row, int $cellWidth, int $cellsPerRow): string { $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 2e6550c..782b028 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -18,9 +18,7 @@ abstract class CodeEnvironment { public bool $useAbsolutePathForMcp = false; - public function __construct(protected readonly DetectionStrategyFactory $strategyFactory) - { - } + public function __construct(protected readonly DetectionStrategyFactory $strategyFactory) {} abstract public function name(): string; @@ -119,8 +117,8 @@ public function mcpConfigKey(): string /** * Install MCP server using the appropriate strategy. * - * @param array $args - * @param array $env + * @param array $args + * @param array $env * * @throws FileNotFoundException */ @@ -136,8 +134,8 @@ public function installMcp(string $key, string $command, array $args = [], array /** * Install MCP server using a shell command strategy. * - * @param array $args - * @param array $env + * @param array $args + * @param array $env */ protected function installShellMcp(string $key, string $command, array $args = [], array $env = []): bool { @@ -162,20 +160,23 @@ 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'); } /** * Install MCP server using a file-based configuration strategy. * - * @param array $args - * @param array $env + * @param array $args + * @param array $env * * @throws FileNotFoundException */ diff --git a/src/Install/CodeEnvironmentsDetector.php b/src/Install/CodeEnvironmentsDetector.php index 258c86a..ad446e6 100644 --- a/src/Install/CodeEnvironmentsDetector.php +++ b/src/Install/CodeEnvironmentsDetector.php @@ -29,8 +29,7 @@ class CodeEnvironmentsDetector public function __construct( private readonly Container $container - ) { - } + ) {} /** * Detect installed applications on the current platform. @@ -42,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(); } @@ -56,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/Contracts/DetectionStrategy.php b/src/Install/Contracts/DetectionStrategy.php index f427ccf..998c0ac 100644 --- a/src/Install/Contracts/DetectionStrategy.php +++ b/src/Install/Contracts/DetectionStrategy.php @@ -11,7 +11,7 @@ interface DetectionStrategy /** * Detect if the application is installed on the machine. * - * @param array{command?:string, basePath?:string, files?:array, paths?:array} $config + * @param array{command?:string, basePath?:string, files?:array, paths?:array} $config */ public function detect(array $config, ?Platform $platform = null): bool; } diff --git a/src/Install/Detection/CompositeDetectionStrategy.php b/src/Install/Detection/CompositeDetectionStrategy.php index 8b0b15a..21a0cd9 100644 --- a/src/Install/Detection/CompositeDetectionStrategy.php +++ b/src/Install/Detection/CompositeDetectionStrategy.php @@ -10,11 +10,9 @@ class CompositeDetectionStrategy implements DetectionStrategy { /** - * @param DetectionStrategy[] $strategies + * @param DetectionStrategy[] $strategies */ - public function __construct(private readonly array $strategies) - { - } + public function __construct(private readonly array $strategies) {} public function detect(array $config, ?Platform $platform = null): bool { diff --git a/src/Install/Detection/DetectionStrategyFactory.php b/src/Install/Detection/DetectionStrategyFactory.php index 09f5a30..7afdfd5 100644 --- a/src/Install/Detection/DetectionStrategyFactory.php +++ b/src/Install/Detection/DetectionStrategyFactory.php @@ -11,18 +11,18 @@ class DetectionStrategyFactory { private const TYPE_DIRECTORY = 'directory'; + private const TYPE_COMMAND = 'command'; + private const TYPE_FILE = 'file'; - public function __construct(private readonly Container $container) - { - } + public function __construct(private readonly Container $container) {} 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 6c8ec18..ccb9029 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 193a989..7ee2a55 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 85a06e3..eb5bdec 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -63,13 +63,13 @@ public function customGuidelinePath(string $path = ''): string * Static method to compose guidelines from a collection. * Can be used without Laravel dependencies. * - * @param Collection $guidelines + * @param Collection $guidelines */ 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')); } @@ -143,7 +143,7 @@ protected function find(): Collection ); // Always add package core $packageGuidelines = $this->guidelinesDir($guidelineDir.'/'.$package->majorVersion()); foreach ($packageGuidelines as $guideline) { - $suffix = $guideline['name'] == 'core' ? '' : '/'.$guideline['name']; + $suffix = $guideline['name'] === 'core' ? '' : '/'.$guideline['name']; $guidelines->put( $guidelineDir.'/v'.$package->majorVersion().$suffix, $guideline @@ -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']))); } /** @@ -175,7 +176,7 @@ protected function find(): Collection protected function shouldExcludePackage(string $packageName): bool { foreach ($this->packagePriorities as $priorityPackage => $excludedPackages) { - if (in_array($packageName, $excludedPackages)) { + if (in_array($packageName, $excludedPackages, true)) { $priorityEnum = Packages::from($priorityPackage); if ($this->roster->uses($priorityEnum)) { return true; @@ -187,7 +188,6 @@ protected function shouldExcludePackage(string $packageName): bool } /** - * @param string $dirPath * @return array */ protected function guidelinesDir(string $dirPath): array @@ -201,15 +201,14 @@ 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)); } /** - * @param string $path * @return array{content: string, name: string, path: ?string, custom: bool} */ protected function guideline(string $path): array @@ -235,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 [ @@ -249,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).'___'; @@ -265,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 13f828f..5207866 100644 --- a/src/Install/GuidelineWriter.php +++ b/src/Install/GuidelineWriter.php @@ -17,9 +17,7 @@ class GuidelineWriter public const NOOP = 3; - public function __construct(protected Agent $agent) - { - } + public function __construct(protected Agent $agent) {} /** * @return \Laravel\Boost\Install\GuidelineWriter::NEW|\Laravel\Boost\Install\GuidelineWriter::REPLACED|\Laravel\Boost\Install\GuidelineWriter::FAILED|\Laravel\Boost\Install\GuidelineWriter::NOOP @@ -33,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+'); @@ -76,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}"); } @@ -105,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 98c508e..c538aff 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 { @@ -30,10 +25,9 @@ public function configKey(string $key): self } /** - * @param string $key MCP Server Name - * @param string $command - * @param array $args - * @param array $env + * @param string $key MCP Server Name + * @param array $args + * @param array $env */ public function addServer(string $key, string $command, array $args = [], array $env = []): self { @@ -81,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 @@ -106,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; } @@ -117,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 @@ -202,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); @@ -262,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 @@ -276,7 +267,7 @@ protected function findCommaInsertionPoint(string $content, int $openBracePos, i $char = $content[$i]; // Skip whitespace and newlines - if (in_array($char, [' ', "\t", "\n", "\r"])) { + if (in_array($char, [' ', "\t", "\n", "\r"], true)) { continue; } @@ -285,16 +276,17 @@ protected function findCommaInsertionPoint(string $content, int $openBracePos, i // Find start of this line $lineStart = strrpos($content, "\n", $i - strlen($content)) ?: 0; $i = $lineStart; + continue; } // 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 cc75630..5270518 100644 --- a/src/Mcp/Boost.php +++ b/src/Mcp/Boost.php @@ -4,6 +4,7 @@ namespace Laravel\Boost\Mcp; +use DirectoryIterator; use Laravel\Boost\Mcp\Methods\CallToolWithExecutor; use Laravel\Boost\Mcp\Resources\ApplicationInfo; use Laravel\Mcp\Server; @@ -53,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; @@ -71,7 +72,7 @@ protected function discoverTools(): array $tools = []; $excludedTools = config('boost.mcp.tools.exclude', []); - $toolDir = new \DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Tools'); + $toolDir = new DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Tools'); foreach ($toolDir as $toolFile) { if ($toolFile->isFile() && $toolFile->getExtension() === 'php') { @@ -100,7 +101,7 @@ protected function discoverResources(): array $resources = []; $excludedResources = config('boost.mcp.resources.exclude', []); - $resourceDir = new \DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Resources'); + $resourceDir = new DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Resources'); foreach ($resourceDir as $resourceFile) { if ($resourceFile->isFile() && $resourceFile->getExtension() === 'php') { @@ -129,7 +130,7 @@ protected function discoverPrompts(): array $prompts = []; $excludedPrompts = config('boost.mcp.prompts.exclude', []); - $promptDir = new \DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Prompts'); + $promptDir = new DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Prompts'); foreach ($promptDir as $promptFile) { if ($promptFile->isFile() && $promptFile->getExtension() === 'php') { diff --git a/src/Mcp/Methods/CallToolWithExecutor.php b/src/Mcp/Methods/CallToolWithExecutor.php index f4f6545..9db398a 100644 --- a/src/Mcp/Methods/CallToolWithExecutor.php +++ b/src/Mcp/Methods/CallToolWithExecutor.php @@ -15,7 +15,7 @@ use Laravel\Mcp\Server\Transport\JsonRpcResponse; use Throwable; -class CallToolWithExecutor implements Method, Errable +class CallToolWithExecutor implements Errable, Method { use InteractsWithResponses; @@ -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/Resources/ApplicationInfo.php b/src/Mcp/Resources/ApplicationInfo.php index 92ebb49..c6cab86 100644 --- a/src/Mcp/Resources/ApplicationInfo.php +++ b/src/Mcp/Resources/ApplicationInfo.php @@ -12,9 +12,7 @@ class ApplicationInfo extends Resource { - public function __construct(protected ToolExecutor $toolExecutor) - { - } + public function __construct(protected ToolExecutor $toolExecutor) {} /** * The resource's description. diff --git a/src/Mcp/ToolExecutor.php b/src/Mcp/ToolExecutor.php index f68df21..0607648 100644 --- a/src/Mcp/ToolExecutor.php +++ b/src/Mcp/ToolExecutor.php @@ -13,10 +13,6 @@ class ToolExecutor { - public function __construct() - { - } - public function execute(string $toolClass, array $arguments = []): Response { if (! ToolRegistry::isToolAllowed($toolClass)) { @@ -56,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}"); @@ -78,7 +74,7 @@ protected function getTimeout(array $arguments): int /** * Reconstruct a Response from JSON data. * - * @param array $data + * @param array $data */ protected function reconstructResponse(array $data): Response { @@ -106,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); } @@ -121,8 +117,7 @@ protected function reconstructResponse(array $data): Response /** * Build the command array for executing a tool in a subprocess. * - * @param string $toolClass - * @param array $arguments + * @param array $arguments * @return array */ protected function buildCommand(string $toolClass, array $arguments): array diff --git a/src/Mcp/ToolRegistry.php b/src/Mcp/ToolRegistry.php index 4a3e05a..72cba09 100644 --- a/src/Mcp/ToolRegistry.php +++ b/src/Mcp/ToolRegistry.php @@ -4,6 +4,8 @@ namespace Laravel\Boost\Mcp; +use DirectoryIterator; + class ToolRegistry { /** @var array|null */ @@ -24,7 +26,7 @@ public static function getAvailableTools(): array // Discover tools from the Tools directory $excludedTools = config('boost.mcp.tools.exclude', []); - $toolDir = new \DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Tools'); + $toolDir = new DirectoryIterator(__DIR__.DIRECTORY_SEPARATOR.'Tools'); foreach ($toolDir as $toolFile) { if ($toolFile->isFile() && $toolFile->getExtension() === 'php') { diff --git a/src/Mcp/Tools/ApplicationInfo.php b/src/Mcp/Tools/ApplicationInfo.php index 0be36d0..57356b0 100644 --- a/src/Mcp/Tools/ApplicationInfo.php +++ b/src/Mcp/Tools/ApplicationInfo.php @@ -15,9 +15,7 @@ #[IsReadOnly] class ApplicationInfo extends Tool { - public function __construct(protected Roster $roster, protected GuidelineAssist $guidelineAssist) - { - } + public function __construct(protected Roster $roster, protected GuidelineAssist $guidelineAssist) {} /** * The tool's description. @@ -33,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 93e8eb0..1f95403 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 124f346..2069f93 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 07f4d68..abd2550 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 8ec339f..f0d732b 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 3214741..75bff37 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 8b5ebde..d1c2752 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 086f319..b3a91dd 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 fdee0b7..c4740c5 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 c1a58a0..2c3f0a8 100644 --- a/src/Mcp/Tools/ListAvailableConfigKeys.php +++ b/src/Mcp/Tools/ListAvailableConfigKeys.php @@ -33,7 +33,7 @@ public function handle(Request $request): Response /** * Flatten a multi-dimensional config array into dot notation keys. * - * @param array> $array + * @param array> $array * @return array */ private function flattenToDotNotation(array $array, string $prefix = ''): array @@ -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/ListRoutes.php b/src/Mcp/Tools/ListRoutes.php index f4c58fe..09b8f6a 100644 --- a/src/Mcp/Tools/ListRoutes.php +++ b/src/Mcp/Tools/ListRoutes.php @@ -87,7 +87,7 @@ public function handle(Request $request): Response } /** - * @param array $options + * @param array $options */ private function artisan(string $command, array $options = []): string { diff --git a/src/Mcp/Tools/ReadLogEntries.php b/src/Mcp/Tools/ReadLogEntries.php index 21eee16..38e6b94 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 cde6042..6a238e9 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 3b7ac3b..682a47d 100644 --- a/src/Mcp/Tools/SearchDocs.php +++ b/src/Mcp/Tools/SearchDocs.php @@ -12,19 +12,18 @@ use Laravel\Mcp\Server\Tool; use Laravel\Roster\Package; use Laravel\Roster\Roster; +use Throwable; class SearchDocs extends Tool { use MakesHttpRequests; - public function __construct(protected Roster $roster) - { - } + 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. @@ -56,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 { @@ -64,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)); + $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'; @@ -78,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; @@ -98,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 23c5537..f90237a 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 5e8201d..89ebad9 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 1589ae4..17cbd91 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 61beeef..c852de1 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 9ad7d53..ce6630a 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 0ff07a8..aa7c209 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 6467550..501082b 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 () { - $this->strategy = new CommandDetectionStrategy(); +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 adb4fe9..3034372 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 0889717..59db3a7 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 () { - $executor = new ToolExecutor(); +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 618f861..c87f9db 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 d0582f9..9792371 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 91ae0bc..ba56781 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,14 +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(); @@ -102,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)); @@ -146,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') @@ -181,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' @@ -199,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') @@ -210,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']); @@ -218,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' @@ -246,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' @@ -269,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 ba6372e..b5a1266 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 53dd97f..dee8402 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 652081d..0b50954 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 29d2de8..8efb35a 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 8723beb..801b41c 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 c2d8d06..5ce139d 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 7f8e360..c93244e 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 6751747..746489d 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 f3f5dfb..c6419a6 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 08f9759..797efd1 100644 --- a/tests/Feature/Middleware/InjectBoostTest.php +++ b/tests/Feature/Middleware/InjectBoostTest.php @@ -8,29 +8,28 @@ 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'); }); function createMiddlewareResponse($response): SymfonyResponse { - $middleware = new InjectBoost(); - $request = new Request(); + $middleware = new InjectBoost; + $request = new Request; $next = fn ($request) => $response; 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('Codestin Search App
')) ->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 14b4b9f..05eb243 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 b877126..a05e075 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 81ae1de..80d0e8d 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 c11133e..dc46503 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 6c3d01b..d2fbfcf 100644 --- a/tests/Unit/Install/Detection/CommandDetectionStrategyTest.php +++ b/tests/Unit/Install/Detection/CommandDetectionStrategyTest.php @@ -6,74 +6,74 @@ use Laravel\Boost\Install\Detection\CommandDetectionStrategy; use Laravel\Boost\Install\Enums\Platform; -beforeEach(function () { - $this->strategy = new CommandDetectionStrategy(); +beforeEach(function (): void { + $this->strategy = new CommandDetectionStrategy; }); - test('detects command with successful exit code', function () { - Process::fake([ - 'which php' => Process::result(exitCode: 0), - ]); +test('detects command with successful exit code', function (): void { + Process::fake([ + 'which php' => Process::result(exitCode: 0), + ]); - $result = $this->strategy->detect([ - 'command' => 'which php', - ]); + $result = $this->strategy->detect([ + 'command' => 'which php', + ]); - expect($result)->toBeTrue(); - })->skip(); + expect($result)->toBeTrue(); +})->skip(); - test('fails for command with non zero exit code', function () { - Process::fake([ - 'which nonexistent' => Process::result(exitCode: 1), - ]); +test('fails for command with non zero exit code', function (): void { + Process::fake([ + 'which nonexistent' => Process::result(exitCode: 1), + ]); - $result = $this->strategy->detect([ - 'command' => 'which nonexistent', - ]); + $result = $this->strategy->detect([ + 'command' => 'which nonexistent', + ]); - expect($result)->toBeFalse(); - })->skip(); + expect($result)->toBeFalse(); +})->skip(); - test('returns false when no command config', function () { - $result = $this->strategy->detect([ - 'other_config' => 'value', - ]); +test('returns false when no command config', function (): void { + $result = $this->strategy->detect([ + 'other_config' => 'value', + ]); - expect($result)->toBeFalse(); - })->skip(); + expect($result)->toBeFalse(); +})->skip(); - test('handles command with output', function () { - Process::fake([ - 'echo test' => Process::result(output: 'test', exitCode: 0), - ]); +test('handles command with output', function (): void { + Process::fake([ + 'echo test' => Process::result(output: 'test', exitCode: 0), + ]); - $result = $this->strategy->detect([ - 'command' => 'echo test', - ]); + $result = $this->strategy->detect([ + 'command' => 'echo test', + ]); - expect($result)->toBeTrue(); - })->skip(); + expect($result)->toBeTrue(); +})->skip(); - test('handles command with error output', function () { - Process::fake([ - 'invalid-command' => Process::result(errorOutput: 'command not found', exitCode: 127), - ]); +test('handles command with error output', function (): void { + Process::fake([ + 'invalid-command' => Process::result(errorOutput: 'command not found', exitCode: 127), + ]); - $result = $this->strategy->detect([ - 'command' => 'invalid-command', - ]); + $result = $this->strategy->detect([ + 'command' => 'invalid-command', + ]); - expect($result)->toBeFalse(); - })->skip(); + expect($result)->toBeFalse(); +})->skip(); - test('works with different platforms parameter', function () { - Process::fake([ - 'where code' => Process::result(exitCode: 0), - ]); +test('works with different platforms parameter', function (): void { + Process::fake([ + 'where code' => Process::result(exitCode: 0), + ]); - $result = $this->strategy->detect([ - 'command' => 'where code', - ], Platform::Windows); + $result = $this->strategy->detect([ + 'command' => 'where code', + ], Platform::Windows); - expect($result)->toBeTrue(); - })->skip(); + expect($result)->toBeTrue(); +})->skip(); diff --git a/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php b/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php index d587bab..a7feaab 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 c487ade..840d90a 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 () { - $this->container = new Container(); +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 cba4323..488c01f 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 () { - $this->strategy = new DirectoryDetectionStrategy(); +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 cab3b0d..431035a 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 d84040d..6cab099 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 822d6b5..c60f8e6 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 d400c46..065eb7d 100644 --- a/tests/Unit/Install/Mcp/FileWriterTest.php +++ b/tests/Unit/Install/Mcp/FileWriterTest.php @@ -8,24 +8,25 @@ use Illuminate\Support\Str; use Laravel\Boost\Install\Mcp\FileWriter; 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') @@ -34,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(); @@ -42,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); @@ -66,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 = ''; @@ -87,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 @@ -95,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 = ''; @@ -114,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( @@ -145,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( @@ -162,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); + $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); + $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); + $reflection = new ReflectionClass($writer); $method = $reflection->getMethod('generateServerJson'); - $method->setAccessible(true); // Test with simple server $result = $method->invokeArgs($writer, ['boost', ['command' => 'php']]); @@ -219,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); + $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'); @@ -236,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( @@ -258,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( @@ -282,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(); @@ -336,7 +334,7 @@ expect($boostCounts)->toBe(1); }); -test('injects into empty configKey object', function () { +test('injects into empty configKey object', function (): void { $writtenContent = ''; mockFileOperations( @@ -357,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( @@ -379,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 4d06a51..d831a1f 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(); }); From 2687e0c2780d0eab3767f3a61f3ecbbc6be21b17 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Mon, 22 Sep 2025 16:42:07 +0530 Subject: [PATCH 05/10] update checkout action to version 5 (#271) Signed-off-by: Pushpak Chhajed --- .github/workflows/static-analysis.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 5b9b2ad..e90d7ec 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 473e8e5..135c793 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,7 +35,7 @@ jobs: git config --global core.eol lf - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -74,7 +74,7 @@ jobs: git config --global core.eol lf - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 From 1f95db8da6f8358b139bdce520b1a4de504bbf58 Mon Sep 17 00:00:00 2001 From: Hichem Taboukouyout Date: Mon, 22 Sep 2025 12:18:44 +0100 Subject: [PATCH 06/10] Enhance MCP commands for WSL compatibility (#121) * Enhance MCP commands for WSL compatibility * Add optional parameter to `getPhpPath` for absolute path control * feat: add tests for WSL and absolute path overrides * refactor: simplify 'isRunningInWsl' * refactor: reduce conditional on boost mcp install * rector: update test return types --------- Co-authored-by: Ashley Hindle --- src/Console/InstallCommand.php | 27 ++++++++- src/Contracts/McpClient.php | 4 +- .../CodeEnvironment/CodeEnvironment.php | 9 ++- .../Feature/Console/InstallCommandWslTest.php | 58 +++++++++++++++++++ .../CodeEnvironmentPathResolutionTest.php | 32 ++++++++++ .../CodeEnvironment/CodeEnvironmentTest.php | 10 ++++ 6 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 tests/Feature/Console/InstallCommandWslTest.php diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index da09e2a..9e0786a 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -487,11 +487,21 @@ private function installMcpServerConfig(): void $this->output->write(" {$ideDisplay}... "); $results = []; - $php = $mcpClient->getPhpPath(); if ($this->shouldInstallMcp()) { + $inWsl = $this->isRunningInWsl(); + $mcp = array_filter([ + 'laravel-boost', + $inWsl ? 'wsl' : false, + $mcpClient->getPhpPath($inWsl), + $mcpClient->getArtisanPath($inWsl), + 'boost:mcp', + ]); try { - $artisan = $mcpClient->getArtisanPath(); - $result = $mcpClient->installMcp('laravel-boost', $php, [$artisan, 'boost:mcp']); + $result = $mcpClient->installMcp( + array_shift($mcp), + array_shift($mcp), + $mcp + ); if ($result) { $results[] = $this->greenTick.' Boost'; @@ -507,6 +517,7 @@ private function installMcpServerConfig(): void // Install Herd MCP if enabled if ($this->shouldInstallHerdMcp()) { + $php = $mcpClient->getPhpPath(); try { $result = $mcpClient->installMcp( key: 'herd', @@ -552,4 +563,14 @@ private function detectLocalization(): bool /** @phpstan-ignore-next-line */ return $actuallyUsing && is_dir(base_path('lang')); } + + /** + * Are we running inside a Windows Subsystem for Linux (WSL) environment? + * This differentiates between a regular Linux installation and a WSL. + */ + private function isRunningInWsl(): bool + { + // Check for WSL-specific environment variables. + return ! empty(getenv('WSL_DISTRO_NAME')) || ! empty(getenv('IS_WSL')); + } } diff --git a/src/Contracts/McpClient.php b/src/Contracts/McpClient.php index 5015952..53fb292 100644 --- a/src/Contracts/McpClient.php +++ b/src/Contracts/McpClient.php @@ -22,12 +22,12 @@ public function useAbsolutePathForMcp(): bool; /** * Get the PHP executable path for this MCP client. */ - public function getPhpPath(): string; + public function getPhpPath(bool $forceAbsolutePath = false): string; /** * Get the artisan path for this MCP client. */ - public function getArtisanPath(): string; + public function getArtisanPath(bool $forceAbsolutePath = false): string; /** * Install an MCP server configuration in this IDE. diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index 782b028..9a96b05 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -5,7 +5,6 @@ namespace Laravel\Boost\Install\CodeEnvironment; use Illuminate\Contracts\Filesystem\FileNotFoundException; -use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Process; use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Contracts\McpClient; @@ -39,14 +38,14 @@ public function useAbsolutePathForMcp(): bool return $this->useAbsolutePathForMcp; } - public function getPhpPath(): string + public function getPhpPath(bool $forceAbsolutePath = false): string { - return $this->useAbsolutePathForMcp() ? PHP_BINARY : 'php'; + return ($this->useAbsolutePathForMcp() || $forceAbsolutePath) ? PHP_BINARY : 'php'; } - public function getArtisanPath(): string + public function getArtisanPath(bool $forceAbsolutePath = false): string { - return $this->useAbsolutePathForMcp() ? base_path('artisan') : 'artisan'; + return ($this->useAbsolutePathForMcp() || $forceAbsolutePath) ? base_path('artisan') : 'artisan'; } /** diff --git a/tests/Feature/Console/InstallCommandWslTest.php b/tests/Feature/Console/InstallCommandWslTest.php new file mode 100644 index 0000000..1367df1 --- /dev/null +++ b/tests/Feature/Console/InstallCommandWslTest.php @@ -0,0 +1,58 @@ +getMethod('isRunningInWsl'); + + expect($method->invoke($command))->toBeTrue(); +}); + +test('isRunningInWsl returns true when IS_WSL is set', function (): void { + putenv('IS_WSL=1'); + + $command = new InstallCommand; + $reflection = new ReflectionClass($command); + $method = $reflection->getMethod('isRunningInWsl'); + + expect($method->invoke($command))->toBeTrue(); +}); + +test('isRunningInWsl returns true when both WSL env vars are set', function (): void { + putenv('WSL_DISTRO_NAME=Ubuntu'); + putenv('IS_WSL=true'); + + $command = new InstallCommand; + $reflection = new ReflectionClass($command); + $method = $reflection->getMethod('isRunningInWsl'); + + expect($method->invoke($command))->toBeTrue(); +}); + +test('isRunningInWsl returns false when no WSL env vars are set', function (): void { + putenv('WSL_DISTRO_NAME'); + putenv('IS_WSL'); + + $command = new InstallCommand; + $reflection = new ReflectionClass($command); + $method = $reflection->getMethod('isRunningInWsl'); + + expect($method->invoke($command))->toBeFalse(); +}); + +test('isRunningInWsl returns false when WSL env vars are empty strings', function (): void { + putenv('WSL_DISTRO_NAME='); + putenv('IS_WSL='); + + $command = new InstallCommand; + $reflection = new ReflectionClass($command); + $method = $reflection->getMethod('isRunningInWsl'); + + expect($method->invoke($command))->toBeFalse(); +}); diff --git a/tests/Feature/Install/CodeEnvironment/CodeEnvironmentPathResolutionTest.php b/tests/Feature/Install/CodeEnvironment/CodeEnvironmentPathResolutionTest.php index aa7c209..5d01d69 100644 --- a/tests/Feature/Install/CodeEnvironment/CodeEnvironmentPathResolutionTest.php +++ b/tests/Feature/Install/CodeEnvironment/CodeEnvironmentPathResolutionTest.php @@ -37,3 +37,35 @@ expect($cursor->getArtisanPath())->toBe('artisan'); }); + +test('CodeEnvironment returns absolute paths when forceAbsolutePath is true', function (): void { + $strategyFactory = Mockery::mock(DetectionStrategyFactory::class); + $cursor = new Cursor($strategyFactory); + + expect($cursor->getPhpPath(true))->toBe(PHP_BINARY); + expect($cursor->getArtisanPath(true))->toEndWith('artisan') + ->and($cursor->getArtisanPath(true))->not()->toBe('artisan'); +}); + +test('CodeEnvironment maintains relative paths when forceAbsolutePath is false', function (): void { + $strategyFactory = Mockery::mock(DetectionStrategyFactory::class); + $cursor = new Cursor($strategyFactory); + + expect($cursor->getPhpPath(false))->toBe('php'); + expect($cursor->getArtisanPath(false))->toBe('artisan'); +}); + +test('PhpStorm paths remain absolute regardless of forceAbsolutePath parameter', function (): void { + $strategyFactory = Mockery::mock(DetectionStrategyFactory::class); + $phpStorm = new PhpStorm($strategyFactory); + + // PhpStorm always uses absolute paths, so forceAbsolutePath shouldn't change behavior + expect($phpStorm->getPhpPath(true))->toBe(PHP_BINARY); + expect($phpStorm->getPhpPath(false))->toBe(PHP_BINARY); + + $artisanPath = $phpStorm->getArtisanPath(true); + expect($artisanPath)->toEndWith('artisan') + ->and($artisanPath)->not()->toBe('artisan'); + + expect($phpStorm->getArtisanPath(false))->toBe($artisanPath); +}); diff --git a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php index 80d0e8d..3976185 100644 --- a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php +++ b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php @@ -409,3 +409,13 @@ public function mcpConfigPath(): string ->and($capturedPath)->toBe($vscode->mcpConfigPath()) ->and($capturedContent)->toBe(fixture('mcp-expected.json5')); }); + +test('getPhpPath uses absolute paths when forceAbsolutePath is true', function (): void { + $environment = new TestCodeEnvironment($this->strategyFactory); + expect($environment->getPhpPath(true))->toBe(PHP_BINARY); +}); + +test('getPhpPath maintains default behavior when forceAbsolutePath is false', function (): void { + $environment = new TestCodeEnvironment($this->strategyFactory); + expect($environment->getPhpPath(false))->toBe('php'); +}); From 92d442e3c2ce2e22138c86180672b5a80b079802 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Mon, 22 Sep 2025 14:21:11 +0300 Subject: [PATCH 07/10] Updated access modifiers from `private` to `protected` across multiple files (#249) Co-authored-by: Ashley Hindle --- src/BoostServiceProvider.php | 14 +++---- src/Concerns/ReadsLogs.php | 12 +++--- src/Console/InstallCommand.php | 42 +++++++++---------- .../Detection/DetectionStrategyFactory.php | 2 +- .../Detection/DirectoryDetectionStrategy.php | 4 +- src/Install/GuidelineAssist.php | 2 +- src/Install/GuidelineComposer.php | 2 +- src/Install/GuidelineWriter.php | 2 +- src/Mcp/Tools/ListAvailableConfigKeys.php | 2 +- src/Mcp/Tools/ListRoutes.php | 2 +- src/Middleware/InjectBoost.php | 4 +- 11 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/BoostServiceProvider.php b/src/BoostServiceProvider.php index 13db699..4baa5c2 100644 --- a/src/BoostServiceProvider.php +++ b/src/BoostServiceProvider.php @@ -76,7 +76,7 @@ public function boot(Router $router): void } } - private function registerPublishing(): void + protected function registerPublishing(): void { if ($this->app->runningInConsole()) { $this->publishes([ @@ -85,7 +85,7 @@ private function registerPublishing(): void } } - private function registerCommands(): void + protected function registerCommands(): void { if ($this->app->runningInConsole()) { $this->commands([ @@ -96,7 +96,7 @@ private function registerCommands(): void } } - private function registerRoutes(): void + protected function registerRoutes(): void { Route::post('/_boost/browser-logs', function (Request $request) { $logs = $request->input('logs', []); @@ -156,7 +156,7 @@ private static function buildLogMessageFromData(array $data): string return implode(' ', $messages); } - private function registerBrowserLogger(): void + protected function registerBrowserLogger(): void { config([ 'logging.channels.browser' => [ @@ -168,19 +168,19 @@ private function registerBrowserLogger(): void ]); } - private function registerBladeDirectives(BladeCompiler $bladeCompiler): void + protected function registerBladeDirectives(BladeCompiler $bladeCompiler): void { $bladeCompiler->directive('boostJs', fn (): string => ''); } - private function hookIntoResponses(Router $router): void + protected function hookIntoResponses(Router $router): void { $this->app->booted(function () use ($router): void { $router->pushMiddlewareToGroup('web', InjectBoost::class); }); } - private function shouldRun(): bool + protected function shouldRun(): bool { if (! config('boost.enabled', true)) { return false; diff --git a/src/Concerns/ReadsLogs.php b/src/Concerns/ReadsLogs.php index 8aaa475..0b607ad 100644 --- a/src/Concerns/ReadsLogs.php +++ b/src/Concerns/ReadsLogs.php @@ -12,27 +12,27 @@ trait ReadsLogs * Regular expression fragments and default chunk-window sizes used when * scanning log files. Declaring them once keeps every consumer in sync. */ - private function getTimestampRegex(): string + protected function getTimestampRegex(): string { return '\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\]'; } - private function getEntrySplitRegex(): string + protected function getEntrySplitRegex(): string { return '/(?='.$this->getTimestampRegex().')/'; } - private function getErrorEntryRegex(): string + protected function getErrorEntryRegex(): string { return '/^'.$this->getTimestampRegex().'.*\\.ERROR:/'; } - private function getChunkSizeStart(): int + protected function getChunkSizeStart(): int { return 64 * 1024; // 64 kB } - private function getChunkSizeMax(): int + protected function getChunkSizeMax(): int { return 1024 * 1024; // 1 MB } @@ -114,7 +114,7 @@ protected function readLastErrorEntry(string $logFile): ?string * * @return string[] */ - private function scanLogChunkForEntries(string $logFile, int $chunkSize): array + protected function scanLogChunkForEntries(string $logFile, int $chunkSize): array { $fileSize = filesize($logFile); if ($fileSize === false) { diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 9e0786a..4a62177 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -76,7 +76,7 @@ public function handle(CodeEnvironmentsDetector $codeEnvironmentsDetector, Herd $this->outro(); } - private function bootstrap(CodeEnvironmentsDetector $codeEnvironmentsDetector, Herd $herd, Terminal $terminal): void + protected function bootstrap(CodeEnvironmentsDetector $codeEnvironmentsDetector, Herd $herd, Terminal $terminal): void { $this->codeEnvironmentsDetector = $codeEnvironmentsDetector; $this->herd = $herd; @@ -93,14 +93,14 @@ private function bootstrap(CodeEnvironmentsDetector $codeEnvironmentsDetector, H $this->projectName = config('app.name'); } - private function displayBoostHeader(): void + protected function displayBoostHeader(): void { note($this->boostLogo()); intro('✦ Laravel Boost :: Install :: We Must Ship ✦'); note("Let's give {$this->bgYellow($this->black($this->bold($this->projectName)))} a Boost"); } - private function boostLogo(): string + protected function boostLogo(): string { return <<<'HEADER' @@ -113,13 +113,13 @@ private function boostLogo(): string HEADER; } - private function discoverEnvironment(): void + protected function discoverEnvironment(): void { $this->systemInstalledCodeEnvironments = $this->codeEnvironmentsDetector->discoverSystemInstalledCodeEnvironments(); $this->projectInstalledCodeEnvironments = $this->codeEnvironmentsDetector->discoverProjectInstalledCodeEnvironments(base_path()); } - private function collectInstallationPreferences(): void + protected function collectInstallationPreferences(): void { $this->selectedBoostFeatures = $this->selectBoostFeatures(); $this->selectedTargetMcpClient = $this->selectTargetMcpClients(); @@ -127,7 +127,7 @@ private function collectInstallationPreferences(): void $this->enforceTests = $this->determineTestEnforcement(ask: false); } - private function performInstallation(): void + protected function performInstallation(): void { $this->installGuidelines(); @@ -138,7 +138,7 @@ private function performInstallation(): void } } - private function discoverTools(): array + protected function discoverTools(): array { $tools = []; $toolDir = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Mcp', 'Tools']); @@ -159,7 +159,7 @@ private function discoverTools(): array return $tools; } - private function outro(): void + protected function outro(): void { $label = 'https://boost.laravel.com/installed'; @@ -189,7 +189,7 @@ private function outro(): void echo $this->black($this->bold($text.$link)).$this->reset(PHP_EOL).$this->reset(PHP_EOL); } - private function hyperlink(string $label, string $url): string + protected function hyperlink(string $label, string $url): string { return "\033]8;;{$url}\007{$label}\033]8;;\033\\"; } @@ -230,7 +230,7 @@ protected function determineTestEnforcement(bool $ask = true): bool /** * @return Collection */ - private function selectBoostFeatures(): Collection + protected function selectBoostFeatures(): Collection { $defaultInstallOptions = ['mcp_server', 'ai_guidelines']; $installOptions = [ @@ -272,7 +272,7 @@ protected function boostToolsToDisable(): array /** * @return Collection */ - private function selectTargetMcpClients(): Collection + protected function selectTargetMcpClients(): Collection { if (! $this->shouldInstallMcp() && ! $this->shouldInstallHerdMcp()) { return collect(); @@ -287,7 +287,7 @@ private function selectTargetMcpClients(): Collection /** * @return Collection */ - private function selectTargetAgents(): Collection + protected function selectTargetAgents(): Collection { if (! $this->shouldInstallAiGuidelines()) { return collect(); @@ -304,7 +304,7 @@ private function selectTargetAgents(): Collection * * @return array{scroll: int, required: bool, displayMethod: string} */ - private function getSelectionConfig(string $contractClass): array + protected function getSelectionConfig(string $contractClass): array { return match ($contractClass) { Agent::class => ['scroll' => 5, 'required' => false, 'displayMethod' => 'agentName'], @@ -316,7 +316,7 @@ private function getSelectionConfig(string $contractClass): array /** * @return Collection */ - private function selectCodeEnvironments(string $contractClass, string $label): Collection + protected function selectCodeEnvironments(string $contractClass, string $label): Collection { $allEnvironments = $this->codeEnvironmentsDetector->getCodeEnvironments(); $config = $this->getSelectionConfig($contractClass); @@ -366,7 +366,7 @@ private function selectCodeEnvironments(string $contractClass, string $label): C return $selectedClasses->map(fn ($className) => $availableEnvironments->first(fn ($env): bool => $env::class === $className)); } - private function installGuidelines(): void + protected function installGuidelines(): void { if (! $this->shouldInstallAiGuidelines()) { return; @@ -434,27 +434,27 @@ private function installGuidelines(): void } } - private function shouldInstallAiGuidelines(): bool + protected function shouldInstallAiGuidelines(): bool { return $this->selectedBoostFeatures->contains('ai_guidelines'); } - private function shouldInstallStyleGuidelines(): bool + protected function shouldInstallStyleGuidelines(): bool { return false; } - private function shouldInstallMcp(): bool + protected function shouldInstallMcp(): bool { return $this->selectedBoostFeatures->contains('mcp_server'); } - private function shouldInstallHerdMcp(): bool + protected function shouldInstallHerdMcp(): bool { return $this->selectedBoostFeatures->contains('herd_mcp'); } - private function installMcpServerConfig(): void + protected function installMcpServerConfig(): void { if (! $this->shouldInstallMcp() && ! $this->shouldInstallHerdMcp()) { return; @@ -556,7 +556,7 @@ private function installMcpServerConfig(): void /** * Is the project actually using localization for their new features? */ - private function detectLocalization(): bool + protected function detectLocalization(): bool { $actuallyUsing = false; diff --git a/src/Install/Detection/DetectionStrategyFactory.php b/src/Install/Detection/DetectionStrategyFactory.php index 7afdfd5..9c6b242 100644 --- a/src/Install/Detection/DetectionStrategyFactory.php +++ b/src/Install/Detection/DetectionStrategyFactory.php @@ -41,7 +41,7 @@ public function makeFromConfig(array $config): DetectionStrategy return $this->make($type, $config); } - private function inferTypeFromConfig(array $config): string|array + protected function inferTypeFromConfig(array $config): string|array { $typeMap = [ 'files' => self::TYPE_FILE, diff --git a/src/Install/Detection/DirectoryDetectionStrategy.php b/src/Install/Detection/DirectoryDetectionStrategy.php index ccb9029..de401c5 100644 --- a/src/Install/Detection/DirectoryDetectionStrategy.php +++ b/src/Install/Detection/DirectoryDetectionStrategy.php @@ -38,7 +38,7 @@ public function detect(array $config, ?Platform $platform = null): bool return false; } - private function expandPath(string $path, ?Platform $platform = null): string + protected function expandPath(string $path, ?Platform $platform = null): string { if ($platform === Platform::Windows) { return preg_replace_callback('/%([^%]+)%/', fn (array $matches) => getenv($matches[1]) ?: $matches[0], $path); @@ -54,7 +54,7 @@ private function expandPath(string $path, ?Platform $platform = null): string return $path; } - private function isAbsolutePath(string $path): bool + protected function isAbsolutePath(string $path): bool { return str_starts_with($path, '/') || str_starts_with($path, '\\') || diff --git a/src/Install/GuidelineAssist.php b/src/Install/GuidelineAssist.php index 7ee2a55..2db3fb4 100644 --- a/src/Install/GuidelineAssist.php +++ b/src/Install/GuidelineAssist.php @@ -59,7 +59,7 @@ public function enums(): array * * @return array */ - private function discover(callable $cb): array + protected function discover(callable $cb): array { $classes = []; $appPath = app_path(); diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index eb5bdec..22568d2 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -247,7 +247,7 @@ protected function guideline(string $path): array private array $storedSnippets = []; - private function processBoostSnippets(string $content): string + protected function processBoostSnippets(string $content): string { return preg_replace_callback('/(?[\'"])(?P[^\1]*?)\1(?:\s*,\s*(?P[\'"])(?P[^\3]*?)\3)?\s*\)(?P.*?)@endboostsnippet/s', function ($matches): string { $name = $matches['name']; diff --git a/src/Install/GuidelineWriter.php b/src/Install/GuidelineWriter.php index 5207866..2851e52 100644 --- a/src/Install/GuidelineWriter.php +++ b/src/Install/GuidelineWriter.php @@ -84,7 +84,7 @@ public function write(string $guidelines): int return $replaced ? self::REPLACED : self::NEW; } - private function acquireLockWithRetry(mixed $handle, string $filePath, int $maxRetries = 3): void + protected function acquireLockWithRetry(mixed $handle, string $filePath, int $maxRetries = 3): void { $attempts = 0; $delay = 100000; // Start with 100ms in microseconds diff --git a/src/Mcp/Tools/ListAvailableConfigKeys.php b/src/Mcp/Tools/ListAvailableConfigKeys.php index 2c3f0a8..a00082c 100644 --- a/src/Mcp/Tools/ListAvailableConfigKeys.php +++ b/src/Mcp/Tools/ListAvailableConfigKeys.php @@ -36,7 +36,7 @@ public function handle(Request $request): Response * @param array> $array * @return array */ - private function flattenToDotNotation(array $array, string $prefix = ''): array + protected function flattenToDotNotation(array $array, string $prefix = ''): array { $results = []; diff --git a/src/Mcp/Tools/ListRoutes.php b/src/Mcp/Tools/ListRoutes.php index 09b8f6a..1d9231a 100644 --- a/src/Mcp/Tools/ListRoutes.php +++ b/src/Mcp/Tools/ListRoutes.php @@ -89,7 +89,7 @@ public function handle(Request $request): Response /** * @param array $options */ - private function artisan(string $command, array $options = []): string + protected function artisan(string $command, array $options = []): string { $output = new BufferedOutput; $response = Artisan::call($command, $options, $output); diff --git a/src/Middleware/InjectBoost.php b/src/Middleware/InjectBoost.php index 89ebad9..c6a467e 100644 --- a/src/Middleware/InjectBoost.php +++ b/src/Middleware/InjectBoost.php @@ -34,7 +34,7 @@ public function handle(Request $request, Closure $next): Response return $response; } - private function shouldInject(Response $response): bool + protected function shouldInject(Response $response): bool { $responseTypes = [ StreamedResponse::class, @@ -62,7 +62,7 @@ private function shouldInject(Response $response): bool return ! str_contains($content, 'browser-logger-active'); } - private function injectScript(string $content): string + protected function injectScript(string $content): string { $script = BrowserLogger::getScript(); From 8f7494cb3edb9edbb5aa1db791edee8a8509d80b Mon Sep 17 00:00:00 2001 From: "Vytautas M." Date: Mon, 22 Sep 2025 14:43:38 +0300 Subject: [PATCH 08/10] Ensure guideline file content ends with a newline (#113) * Ensure file content ends with a newline * Update GuidelineWriterTest * Test to avoid adding extra newline --------- Co-authored-by: Ashley Hindle --- src/Install/GuidelineWriter.php | 5 +++ tests/Unit/Install/GuidelineWriterTest.php | 49 ++++++++++++++++------ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/Install/GuidelineWriter.php b/src/Install/GuidelineWriter.php index 2851e52..a9115ea 100644 --- a/src/Install/GuidelineWriter.php +++ b/src/Install/GuidelineWriter.php @@ -68,6 +68,11 @@ public function write(string $guidelines): int $newContent = $frontMatter.$existingContent.$separatingNewlines.$replacement; } + // Ensure file content ends with a newline + if (! str_ends_with($newContent, "\n")) { + $newContent .= "\n"; + } + if (ftruncate($handle, 0) === false || fseek($handle, 0) === -1) { throw new RuntimeException("Failed to reset file pointer: {$filePath}"); } diff --git a/tests/Unit/Install/GuidelineWriterTest.php b/tests/Unit/Install/GuidelineWriterTest.php index 6cab099..d2bb040 100644 --- a/tests/Unit/Install/GuidelineWriterTest.php +++ b/tests/Unit/Install/GuidelineWriterTest.php @@ -60,7 +60,7 @@ $writer->write('test guidelines content'); $content = file_get_contents($tempFile); - expect($content)->toBe("\ntest guidelines content\n"); + expect($content)->toBe("\ntest guidelines content\n\n"); unlink($tempFile); }); @@ -77,7 +77,7 @@ $writer->write('new guidelines'); $content = file_get_contents($tempFile); - expect($content)->toBe("# Existing content\n\nSome text here.\n\n===\n\n\nnew guidelines\n"); + expect($content)->toBe("# Existing content\n\nSome text here.\n\n===\n\n\nnew guidelines\n\n"); unlink($tempFile); }); @@ -95,7 +95,30 @@ $writer->write('updated guidelines'); $content = file_get_contents($tempFile); - expect($content)->toBe("# Header\n\n\nupdated guidelines\n\n\n# Footer"); + expect($content)->toBe("# Header\n\n\nupdated guidelines\n\n\n# Footer\n"); + + unlink($tempFile); +}); + +test('it avoids adding extra newline if one already exists', function () { + $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); + $initialContent = "# Header\n\n\nold guidelines\n\n\n# Footer\n"; + file_put_contents($tempFile, $initialContent); + + $agent = Mockery::mock(Agent::class); + $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); + $agent->shouldReceive('frontmatter')->andReturn(false); + + $writer = new GuidelineWriter($agent); + $writer->write('updated guidelines'); + + $content = file_get_contents($tempFile); + expect($content)->toBe("# Header\n\n\nupdated guidelines\n\n\n# Footer\n"); + + // Assert no double newline at the end + expect(substr($content, -2))->not->toBe("\n\n"); + // Assert still ends with exactly one newline + expect(substr($content, -1))->toBe("\n"); unlink($tempFile); }); @@ -114,7 +137,7 @@ $content = file_get_contents($tempFile); // Should replace in-place, preserving structure - expect($content)->toBe("Start\n\nsingle line\n\nEnd"); + expect($content)->toBe("Start\n\nsingle line\n\nEnd\n"); unlink($tempFile); }); @@ -133,7 +156,7 @@ $content = file_get_contents($tempFile); // Should replace first occurrence, second block remains untouched due to non-greedy matching - expect($content)->toBe("Start\n\nreplacement\n\nMiddle\n\nsecond\n\nEnd"); + expect($content)->toBe("Start\n\nreplacement\n\nMiddle\n\nsecond\n\nEnd\n"); unlink($tempFile); }); @@ -165,7 +188,7 @@ $writer->write('my guidelines'); $content = file_get_contents($tempFile); - expect($content)->toBe("# Title\n\nParagraph 1\n\nParagraph 2\n\n===\n\n\nmy guidelines\n"); + expect($content)->toBe("# Title\n\nParagraph 1\n\nParagraph 2\n\n===\n\n\nmy guidelines\n\n"); unlink($tempFile); }); @@ -182,7 +205,7 @@ $writer->write('first guidelines'); $content = file_get_contents($tempFile); - expect($content)->toBe("\nfirst guidelines\n"); + expect($content)->toBe("\nfirst guidelines\n\n"); unlink($tempFile); }); @@ -199,7 +222,7 @@ $writer->write('clean guidelines'); $content = file_get_contents($tempFile); - expect($content)->toBe("\nclean guidelines\n"); + expect($content)->toBe("\nclean guidelines\n\n"); unlink($tempFile); }); @@ -218,7 +241,7 @@ expect($result)->toBe(GuidelineWriter::REPLACED); $content = file_get_contents($tempFile); - expect($content)->toBe("# Title\n\n\nShould not be touched\n\n\n\nnew guidelines\n\n\n\nAlso untouched\n"); + expect($content)->toBe("# Title\n\n\nShould not be touched\n\n\n\nnew guidelines\n\n\n\nAlso untouched\n\n"); unlink($tempFile); }); @@ -248,7 +271,7 @@ ->and($content)->toContain('More content here.'); // Verify exact structure - expect($content)->toBe("# My Project\n\n\nupdated guidelines from boost\n\n\n# User Added Section\nThis content was added by the user after the guidelines.\n\n## Another user section\nMore content here."); + expect($content)->toBe("# My Project\n\n\nupdated guidelines from boost\n\n\n# User Added Section\nThis content was added by the user after the guidelines.\n\n## Another user section\nMore content here.\n"); unlink($tempFile); }); @@ -269,7 +292,7 @@ $writer->write('new guidelines'); $content = file_get_contents($tempFile); - expect($content)->toBe("---\nalwaysApply: true\n---\n# Existing content\n\nSome text here.\n\n===\n\n\nnew guidelines\n"); + expect($content)->toBe("---\nalwaysApply: true\n---\n# Existing content\n\nSome text here.\n\n===\n\n\nnew guidelines\n\n"); unlink($tempFile); }); @@ -286,7 +309,7 @@ $writer->write('new guidelines'); $content = file_get_contents($tempFile); - expect($content)->toBe("---\ncustomOption: true\n---\n# Existing content\n\nSome text here.\n\n===\n\n\nnew guidelines\n"); + expect($content)->toBe("---\ncustomOption: true\n---\n# Existing content\n\nSome text here.\n\n===\n\n\nnew guidelines\n\n"); unlink($tempFile); }); @@ -304,7 +327,7 @@ expect($result)->toBe(GuidelineWriter::NEW); $content = file_get_contents($tempFile); - expect($content)->toBe("# Existing content\n\nSome text here.\n\n===\n\n\nnew guidelines\n"); + expect($content)->toBe("# Existing content\n\nSome text here.\n\n===\n\n\nnew guidelines\n\n"); unlink($tempFile); }); From 6eb3c0e44108fb2516b6f2b794b0b3674f4f56b1 Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Mon, 22 Sep 2025 17:39:00 +0100 Subject: [PATCH 09/10] Don't include mcp always (#273) * feat: update laravel/roster constraint to 0.2.8 * feat: only include MCP guidelines if direct dependency Every Boost user would get the MCP guidelines because they have an indirect dependency on laravel/mcp. Users should only get the MCP guidelines if they _directly_ require laravel/mcp. * fix array shape on mustBeDirect * Fix Linting changes Signed-off-by: Pushpak Chhajed * Fix code styling * Formatting Signed-off-by: Pushpak Chhajed --------- Signed-off-by: Pushpak Chhajed Co-authored-by: Pushpak Chhajed Co-authored-by: pushpak1300 <31663512+pushpak1300@users.noreply.github.com> --- composer.json | 2 +- src/Install/GuidelineComposer.php | 19 +++++++++++--- .../Feature/Install/GuidelineComposerTest.php | 26 +++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 133282f..a753d6a 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", "laravel/mcp": "^0.2.0", "laravel/prompts": "0.1.25|^0.3.6", - "laravel/roster": "^0.2.7" + "laravel/roster": "^0.2.8" }, "require-dev": { "laravel/pint": "1.20", diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index 22568d2..826611e 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -7,6 +7,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Blade; use Laravel\Roster\Enums\Packages; +use Laravel\Roster\Package; use Laravel\Roster\Roster; use Symfony\Component\Finder\Exception\DirectoryNotFoundException; use Symfony\Component\Finder\Finder; @@ -30,6 +31,16 @@ class GuidelineComposer */ protected array $packagePriorities; + /** + * Only include guidelines for these package names if they're a direct requirement. + * This fixes every Boost user getting the MCP guidelines due to indirect import. + * + * @var array + * */ + protected array $mustBeDirect = [ + Packages::MCP, + ]; + public function __construct(protected Roster $roster, protected Herd $herd) { $this->packagePriorities = [ @@ -131,7 +142,7 @@ protected function find(): Collection // We don't add guidelines for packages unsupported by Roster right now foreach ($this->roster->packages() as $package) { // Skip packages that should be excluded due to priority rules - if ($this->shouldExcludePackage($package->package()->value)) { + if ($this->shouldExcludePackage($package)) { continue; } @@ -173,10 +184,10 @@ protected function find(): Collection /** * Determines if a package should be excluded from guidelines based on priority rules. */ - protected function shouldExcludePackage(string $packageName): bool + protected function shouldExcludePackage(Package $package): bool { foreach ($this->packagePriorities as $priorityPackage => $excludedPackages) { - if (in_array($packageName, $excludedPackages, true)) { + if (in_array($package->package()->value, $excludedPackages, true)) { $priorityEnum = Packages::from($priorityPackage); if ($this->roster->uses($priorityEnum)) { return true; @@ -184,7 +195,7 @@ protected function shouldExcludePackage(string $packageName): bool } } - return false; + return $package->indirect() && in_array($package->package(), $this->mustBeDirect, true); } /** diff --git a/tests/Feature/Install/GuidelineComposerTest.php b/tests/Feature/Install/GuidelineComposerTest.php index 3034372..e9f2c01 100644 --- a/tests/Feature/Install/GuidelineComposerTest.php +++ b/tests/Feature/Install/GuidelineComposerTest.php @@ -316,6 +316,32 @@ ->not->toContain('=== phpunit/core rules ==='); }); +test('excludes laravel/mcp guidelines when indirectly required', function (): void { + $packages = new PackageCollection([ + new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), + (new Package(Packages::MCP, 'laravel/mcp', '0.2.2'))->setDirect(false), + ]); + + $this->roster->shouldReceive('packages')->andReturn($packages); + $this->roster->shouldReceive('uses')->with(Packages::LARAVEL)->andReturn(true); + $this->roster->shouldReceive('uses')->with(Packages::MCP)->andReturn(true); + + expect($this->composer->compose())->not->toContain('Mcp::web'); +}); + +test('includes laravel/mcp guidelines when directly required', function (): void { + $packages = new PackageCollection([ + new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), + (new Package(Packages::MCP, 'laravel/mcp', '0.2.2'))->setDirect(true), + ]); + + $this->roster->shouldReceive('packages')->andReturn($packages); + $this->roster->shouldReceive('uses')->with(Packages::LARAVEL)->andReturn(true); + $this->roster->shouldReceive('uses')->with(Packages::MCP)->andReturn(true); + + expect($this->composer->compose())->toContain('Mcp::web'); +}); + test('includes PHPUnit guidelines when Pest is not present', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), From 84cd7630849df6f54d8cccb047fba5d83442ef93 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 23 Sep 2025 13:01:42 +0530 Subject: [PATCH 10/10] Fix test name (#274) Signed-off-by: Pushpak Chhajed Co-authored-by: Thai Nguyen Hung --- src/Install/GuidelineWriter.php | 2 +- tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php | 4 ++-- tests/Unit/Install/GuidelineWriterTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Install/GuidelineWriter.php b/src/Install/GuidelineWriter.php index a9115ea..dbe758e 100644 --- a/src/Install/GuidelineWriter.php +++ b/src/Install/GuidelineWriter.php @@ -69,7 +69,7 @@ public function write(string $guidelines): int } // Ensure file content ends with a newline - if (! str_ends_with($newContent, "\n")) { + if (! str_ends_with((string) $newContent, "\n")) { $newContent .= "\n"; } diff --git a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php index 3976185..25c5490 100644 --- a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php +++ b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php @@ -122,13 +122,13 @@ public function mcpConfigPath(): string expect($environment->mcpClientName())->toBe('Test Environment'); }); -test('IsAgent returns true when implements Agent interface and has agentName', function (): void { +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 (): void { +test('isAgent returns false when does not implement Agent interface', function (): void { $environment = new TestCodeEnvironment($this->strategyFactory); expect($environment->isAgent())->toBe(false); diff --git a/tests/Unit/Install/GuidelineWriterTest.php b/tests/Unit/Install/GuidelineWriterTest.php index d2bb040..7bbb5d8 100644 --- a/tests/Unit/Install/GuidelineWriterTest.php +++ b/tests/Unit/Install/GuidelineWriterTest.php @@ -100,7 +100,7 @@ unlink($tempFile); }); -test('it avoids adding extra newline if one already exists', function () { +test('it avoids adding extra newline if one already exists', function (): void { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); $initialContent = "# Header\n\n\nold guidelines\n\n\n# Footer\n"; file_put_contents($tempFile, $initialContent);