diff --git a/.ai/mcp/core.blade.php b/.ai/mcp/core.blade.php new file mode 100644 index 00000000..22ce6ac2 --- /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. diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 5b9b2ada..e90d7ece 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 473e8e51..135c793a 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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..bc8f1197 --- /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. diff --git a/all.php b/all.php index 0d2aeea7..1d5f4043 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 ad30c14c..a753d6ab 100644 --- a/composer.json +++ b/composer.json @@ -21,14 +21,15 @@ "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.8" }, "require-dev": { "laravel/pint": "1.20", "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 703a4a44..ec2edefa 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,13 +2,13 @@ - ./tests/Unit + tests/Unit - ./tests/Feature + tests/Feature - ./tests/ArchTest.php + tests/ArchTest.php diff --git a/pint.json b/pint.json index be0732e3..b37a4e47 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 00000000..35c4e0a9 --- /dev/null +++ b/rector.php @@ -0,0 +1,29 @@ +withPaths([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->withSkip([ + ReadOnlyPropertyRector::class, + EncapsedStringsToSprintfRector::class, + DisallowedEmptyRuleFixerRector::class, + BooleanInBooleanNotRuleFixerRector::class, + ]) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + codingStyle: true, + typeDeclarations: true, + earlyReturn: true, + strictBooleans: true, + )->withPhpSets(php81: true); diff --git a/src/BoostServiceProvider.php b/src/BoostServiceProvider.php index 4bd4e299..4baa5c2d 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) { @@ -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', []); @@ -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'], @@ -151,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' => [ @@ -163,29 +168,19 @@ private function registerBrowserLogger(): void ]); } - private function registerBladeDirectives(BladeCompiler $bladeCompiler): void + protected function registerBladeDirectives(BladeCompiler $bladeCompiler): void { - $bladeCompiler->directive('boostJs', fn () => ''); + $bladeCompiler->directive('boostJs', fn (): string => ''); } - private static function mapJsTypeToPsr3Level(string $type): string + protected function hookIntoResponses(Router $router): void { - return match ($type) { - 'warn' => 'warning', - 'log', 'table' => 'debug', - 'window_error', 'uncaught_error', 'unhandled_rejection' => 'error', - default => $type - }; - } - - private function hookIntoResponses(Router $router): void - { - $this->app->booted(function () use ($router) { + $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/MakesHttpRequests.php b/src/Concerns/MakesHttpRequests.php index 71b8b8ec..cb7139cb 100644 --- a/src/Concerns/MakesHttpRequests.php +++ b/src/Concerns/MakesHttpRequests.php @@ -17,8 +17,8 @@ public function client(): PendingRequest ]); // Disable SSL verification for local development URLs and testing - if (app()->environment(['local', 'testing']) || str_contains(config('boost.hosted.api_url', ''), '.test')) { - $client = $client->withoutVerifying(); + if (app()->environment(['local', 'testing']) || str_contains((string) config('boost.hosted.api_url', ''), '.test')) { + return $client->withoutVerifying(); } return $client; @@ -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 b14cf9dd..0b607ad1 100644 --- a/src/Concerns/ReadsLogs.php +++ b/src/Concerns/ReadsLogs.php @@ -12,29 +12,29 @@ 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 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]); } } @@ -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/ExecuteToolCommand.php b/src/Console/ExecuteToolCommand.php index 28478b5c..e081526f 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 ff179b56..4a621772 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -76,13 +76,14 @@ 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; $this->terminal = $terminal; $this->terminal->initDimensions(); + $this->greenTick = $this->green('✓'); $this->redCross = $this->red('✗'); @@ -92,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' @@ -112,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(); @@ -126,7 +127,7 @@ private function collectInstallationPreferences(): void $this->enforceTests = $this->determineTestEnforcement(ask: false); } - private function performInstallation(): void + protected function performInstallation(): void { $this->installGuidelines(); @@ -137,7 +138,7 @@ private function performInstallation(): void } } - private function discoverTools(): array + protected function discoverTools(): array { $tools = []; $toolDir = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Mcp', 'Tools']); @@ -158,14 +159,14 @@ private function discoverTools(): array return $tools; } - private function outro(): void + protected 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()) { @@ -188,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\\"; } @@ -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' @@ -229,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 = [ @@ -271,7 +272,7 @@ protected function boostToolsToDisable(): array /** * @return Collection */ - private function selectTargetMcpClients(): Collection + protected function selectTargetMcpClients(): Collection { if (! $this->shouldInstallMcp() && ! $this->shouldInstallHerdMcp()) { return collect(); @@ -286,7 +287,7 @@ private function selectTargetMcpClients(): Collection /** * @return Collection */ - private function selectTargetAgents(): Collection + protected function selectTargetAgents(): Collection { if (! $this->shouldInstallAiGuidelines()) { return collect(); @@ -303,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'], @@ -315,24 +316,22 @@ 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); - $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,10 +363,10 @@ 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 + protected function installGuidelines(): void { if (! $this->shouldInstallAiGuidelines()) { return; @@ -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' @@ -435,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; @@ -466,6 +465,7 @@ private function installMcpServerConfig(): void return; } + $this->newLine(); $this->info(' Installing MCP servers to your selected IDEs'); $this->newLine(); @@ -483,15 +483,25 @@ 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 = []; - $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', @@ -532,7 +543,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) { @@ -545,11 +556,21 @@ private function installMcpServerConfig(): void /** * Is the project actually using localization for their new features? */ - private function detectLocalization(): bool + protected function detectLocalization(): bool { $actuallyUsing = false; /** @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/Agent.php b/src/Contracts/Agent.php index 75a55b04..f6bbf793 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 68c106b1..53fb2929 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; @@ -24,20 +22,20 @@ 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. * - * @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 9fbcd161..b4906361 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 78457ed0..09a934c5 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 2e6550cc..9a96b052 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; @@ -18,9 +17,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; @@ -41,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'; } /** @@ -119,8 +116,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 +133,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 +159,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 258c86ad..ad446e61 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 f427ccf4..998c0acf 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 8b0b15a7..21a0cd90 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 09f5a305..9c6b242a 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) ); } @@ -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 6c8ec184..de401c5a 100644 --- a/src/Install/Detection/DirectoryDetectionStrategy.php +++ b/src/Install/Detection/DirectoryDetectionStrategy.php @@ -38,12 +38,10 @@ 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('/%([^%]+)%/', 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, '~')) { @@ -56,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 193a9897..2db3fb45 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()); } @@ -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(); @@ -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 85a06e30..826611e5 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 = [ @@ -63,13 +74,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 +121,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')); } @@ -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; } @@ -143,7 +154,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,20 +173,21 @@ 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']))); } /** * 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)) { + if (in_array($package->package()->value, $excludedPackages, true)) { $priorityEnum = Packages::from($priorityPackage); if ($this->roster->uses($priorityEnum)) { return true; @@ -183,11 +195,10 @@ protected function shouldExcludePackage(string $packageName): bool } } - return false; + return $package->indirect() && in_array($package->package(), $this->mustBeDirect, true); } /** - * @param string $dirPath * @return array */ protected function guidelinesDir(string $dirPath): array @@ -201,15 +212,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 +245,7 @@ protected function guideline(string $path): array ]); $rendered = str_replace(array_values($placeholders), array_keys($placeholders), $rendered); $rendered = str_replace(array_keys($this->storedSnippets), array_values($this->storedSnippets), $rendered); + $this->storedSnippets = []; // Clear for next use return [ @@ -247,11 +258,11 @@ 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) { + 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 +276,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 13f828fb..dbe758e4 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+'); @@ -72,11 +68,16 @@ public function write(string $guidelines): int $newContent = $frontMatter.$existingContent.$separatingNewlines.$replacement; } + // Ensure file content ends with a newline + if (! str_ends_with((string) $newContent, "\n")) { + $newContent .= "\n"; + } + if (ftruncate($handle, 0) === false || fseek($handle, 0) === -1) { 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}"); } @@ -88,7 +89,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 @@ -105,7 +106,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 98c508e5..c538afff 100644 --- a/src/Install/Mcp/FileWriter.php +++ b/src/Install/Mcp/FileWriter.php @@ -9,18 +9,13 @@ class FileWriter { - protected string $filePath; - protected string $configKey = 'mcpServers'; protected array $serversToAdd = []; protected int $defaultIndentation = 8; - public function __construct(string $filePath) - { - $this->filePath = $filePath; - } + public function __construct(protected string $filePath) {} public function configKey(string $key): self { @@ -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 cc756303..5270518d 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 f4f6545b..9db398a5 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 92ebb493..c6cab86e 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 f68df218..06076484 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 4a3e05ad..72cba096 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 0be36d0e..57356b01 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 93e8eb0a..1f954037 100644 --- a/src/Mcp/Tools/DatabaseQuery.php +++ b/src/Mcp/Tools/DatabaseQuery.php @@ -46,6 +46,7 @@ public function handle(Request $request): Response if (! $token) { return Response::error('Please pass a valid query'); } + $firstWord = strtoupper($token); // Allowed read-only commands. @@ -63,10 +64,8 @@ public function handle(Request $request): Response $isReadOnly = in_array($firstWord, $allowList, true); // Additional validation for WITH … SELECT. - if ($firstWord === 'WITH') { - if (! preg_match('/with\s+.*select\b/i', $query)) { - $isReadOnly = false; - } + if ($firstWord === 'WITH' && ! preg_match('/with\s+.*select\b/i', $query)) { + $isReadOnly = false; } if (! $isReadOnly) { @@ -79,8 +78,8 @@ public function handle(Request $request): Response return Response::json( DB::connection($connectionName)->select($query) ); - } catch (Throwable $e) { - return Response::error('Query failed: '.$e->getMessage()); + } catch (Throwable $throwable) { + return Response::error('Query failed: '.$throwable->getMessage()); } } } diff --git a/src/Mcp/Tools/DatabaseSchema.php b/src/Mcp/Tools/DatabaseSchema.php index 124f3462..2069f93b 100644 --- a/src/Mcp/Tools/DatabaseSchema.php +++ b/src/Mcp/Tools/DatabaseSchema.php @@ -34,7 +34,7 @@ public function schema(JsonSchema $schema): array { return [ 'database' => $schema->string() - ->description('Name of the database connection to dump (defaults to app\'s default connection, often not needed)'), + ->description("Name of the database connection to dump (defaults to app's default connection, often not needed)"), 'filter' => $schema->string() ->description('Filter the tables by name'), ]; @@ -49,9 +49,7 @@ public function handle(Request $request): Response $filter = $request->get('filter') ?? ''; $cacheKey = "boost:mcp:database-schema:{$connection}:{$filter}"; - $schema = Cache::remember($cacheKey, 20, function () use ($connection, $filter) { - return $this->getDatabaseStructure($connection, $filter); - }); + $schema = Cache::remember($cacheKey, 20, fn (): array => $this->getDatabaseStructure($connection, $filter)); return Response::json($schema); } @@ -72,7 +70,7 @@ protected function getAllTablesStructure(?string $connection, string $filter = ' foreach ($this->getAllTables($connection) as $table) { $tableName = $table['name']; - if ($filter && ! str_contains(strtolower($tableName), strtolower($filter))) { + if ($filter && ! str_contains(strtolower((string) $tableName), strtolower($filter))) { continue; } @@ -105,14 +103,14 @@ protected function getTableStructure(?string $connection, string $tableName): ar 'triggers' => $triggers, 'check_constraints' => $checkConstraints, ]; - } catch (Exception $e) { + } catch (Exception $exception) { Log::error('Failed to get table structure for: '.$tableName, [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), + 'error' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), ]); return [ - 'error' => 'Failed to get structure: '.$e->getMessage(), + 'error' => 'Failed to get structure: '.$exception->getMessage(), ]; } } diff --git a/src/Mcp/Tools/DatabaseSchema/DatabaseSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/DatabaseSchemaDriver.php index 07f4d688..abd2550c 100644 --- a/src/Mcp/Tools/DatabaseSchema/DatabaseSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/DatabaseSchemaDriver.php @@ -6,12 +6,7 @@ abstract class DatabaseSchemaDriver { - protected $connection; - - public function __construct($connection = null) - { - $this->connection = $connection; - } + public function __construct(protected $connection = null) {} abstract public function getViews(): array; diff --git a/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php index 8ec339f9..f0d732b5 100644 --- a/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/MySQLSchemaDriver.php @@ -43,7 +43,7 @@ public function getFunctions(): array public function getTriggers(?string $table = null): array { try { - if ($table) { + if ($table !== null && $table !== '' && $table !== '0') { return DB::connection($this->connection)->select('SHOW TRIGGERS WHERE `Table` = ?', [$table]); } diff --git a/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php index 32147416..75bff37d 100644 --- a/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/PostgreSQLSchemaDriver.php @@ -60,7 +60,7 @@ public function getTriggers(?string $table = null): array FROM information_schema.triggers WHERE trigger_schema = current_schema() '; - if ($table) { + if ($table !== null && $table !== '' && $table !== '0') { $sql .= ' AND event_object_table = ?'; return DB::connection($this->connection)->select($sql, [$table]); diff --git a/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php b/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php index 8b5ebde8..d1c2752c 100644 --- a/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php +++ b/src/Mcp/Tools/DatabaseSchema/SQLiteSchemaDriver.php @@ -36,7 +36,7 @@ public function getTriggers(?string $table = null): array { try { $sql = "SELECT name, sql FROM sqlite_master WHERE type = 'trigger'"; - if ($table) { + if ($table !== null && $table !== '' && $table !== '0') { $sql .= ' AND tbl_name = ?'; return DB::connection($this->connection)->select($sql, [$table]); diff --git a/src/Mcp/Tools/LastError.php b/src/Mcp/Tools/LastError.php index 086f319c..b3a91ddb 100644 --- a/src/Mcp/Tools/LastError.php +++ b/src/Mcp/Tools/LastError.php @@ -27,7 +27,7 @@ public function __construct() { // Register the listener only once per PHP process. if (! self::$listenerRegistered) { - Log::listen(function (MessageLogged $event) { + Log::listen(function (MessageLogged $event): void { if ($event->level === 'error') { Cache::forever('boost:last_error', [ 'timestamp' => now()->toDateTimeString(), diff --git a/src/Mcp/Tools/ListArtisanCommands.php b/src/Mcp/Tools/ListArtisanCommands.php index fdee0b79..c4740c5f 100644 --- a/src/Mcp/Tools/ListArtisanCommands.php +++ b/src/Mcp/Tools/ListArtisanCommands.php @@ -36,7 +36,7 @@ public function handle(Request $request): Response } // Sort alphabetically by name for determinism. - usort($commandList, fn ($firstCommand, $secondCommand) => strcmp($firstCommand['name'], $secondCommand['name'])); + usort($commandList, fn (array $firstCommand, array $secondCommand): int => strcmp((string) $firstCommand['name'], (string) $secondCommand['name'])); return Response::json($commandList); } diff --git a/src/Mcp/Tools/ListAvailableConfigKeys.php b/src/Mcp/Tools/ListAvailableConfigKeys.php index c1a58a0d..a00082cc 100644 --- a/src/Mcp/Tools/ListAvailableConfigKeys.php +++ b/src/Mcp/Tools/ListAvailableConfigKeys.php @@ -33,10 +33,10 @@ 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 + protected function flattenToDotNotation(array $array, string $prefix = ''): array { $results = []; @@ -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 f4c58fed..1d9231a7 100644 --- a/src/Mcp/Tools/ListRoutes.php +++ b/src/Mcp/Tools/ListRoutes.php @@ -87,9 +87,9 @@ public function handle(Request $request): Response } /** - * @param array $options + * @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/Mcp/Tools/ReadLogEntries.php b/src/Mcp/Tools/ReadLogEntries.php index 21eee16f..38e6b946 100644 --- a/src/Mcp/Tools/ReadLogEntries.php +++ b/src/Mcp/Tools/ReadLogEntries.php @@ -58,6 +58,7 @@ public function handle(Request $request): Response if ($entries === []) { return Response::text('Unable to retrieve log entries, or no entries yet.'); } + $logs = implode("\n\n", $entries); if (empty(trim($logs))) { diff --git a/src/Mcp/Tools/ReportFeedback.php b/src/Mcp/Tools/ReportFeedback.php index cde60426..6a238e9a 100644 --- a/src/Mcp/Tools/ReportFeedback.php +++ b/src/Mcp/Tools/ReportFeedback.php @@ -40,7 +40,7 @@ public function handle(Request $request): Response|Generator $apiUrl = config('boost.hosted.api_url', 'https://boost.laravel.com').'/api/feedback'; $feedback = $request->get('feedback'); - if (empty($feedback) || strlen($feedback) < 10) { + if (empty($feedback) || strlen((string) $feedback) < 10) { return Response::error('Feedback too short'); } diff --git a/src/Mcp/Tools/SearchDocs.php b/src/Mcp/Tools/SearchDocs.php index 3b7ac3bc..682a47de 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 23c55372..f90237a7 100644 --- a/src/Mcp/Tools/Tinker.php +++ b/src/Mcp/Tools/Tinker.php @@ -61,16 +61,16 @@ public function handle(Request $request): Response // If a result is an object, include the class name if (is_object($result)) { - $response['class'] = get_class($result); + $response['class'] = $result::class; } return Response::json($response); - } catch (Throwable $e) { + } catch (Throwable $throwable) { return Response::json([ - 'error' => $e->getMessage(), - 'type' => get_class($e), - 'file' => $e->getFile(), - 'line' => $e->getLine(), + 'error' => $throwable->getMessage(), + 'type' => $throwable::class, + 'file' => $throwable->getFile(), + 'line' => $throwable->getLine(), ]); } finally { diff --git a/src/Middleware/InjectBoost.php b/src/Middleware/InjectBoost.php index 5e8201d9..c6a467e2 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, @@ -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,14 +59,10 @@ 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 + protected function injectScript(string $content): string { $script = BrowserLogger::getScript(); diff --git a/tests/ArchTest.php b/tests/ArchTest.php index 1589ae44..17cbd911 100644 --- a/tests/ArchTest.php +++ b/tests/ArchTest.php @@ -12,14 +12,14 @@ arch('commands') ->expect('Laravel\Boost\Commands') - ->toExtend('Illuminate\Console\Command') + ->toExtend(\Illuminate\Console\Command::class) ->toHaveSuffix('Command'); arch('no direct env calls') ->expect('env') ->not->toBeUsedIn('Laravel\Boost') ->ignoring([ - 'Laravel\Boost\BoostServiceProvider', + \Laravel\Boost\BoostServiceProvider::class, ]); arch('tests') diff --git a/tests/Feature/BoostServiceProviderTest.php b/tests/Feature/BoostServiceProviderTest.php index 61beeef0..c852de12 100644 --- a/tests/Feature/BoostServiceProviderTest.php +++ b/tests/Feature/BoostServiceProviderTest.php @@ -5,15 +5,15 @@ use Illuminate\Support\Facades\Config; use Laravel\Boost\BoostServiceProvider; -beforeEach(function () { +beforeEach(function (): void { $this->refreshApplication(); Config::set('logging.channels.browser', null); }); -describe('boost.enabled configuration', function () { - it('does not boot boost when disabled', function () { +describe('boost.enabled configuration', function (): void { + it('does not boot boost when disabled', function (): void { Config::set('boost.enabled', false); - app()->detectEnvironment(fn () => 'local'); + app()->detectEnvironment(fn (): string => 'local'); $provider = new BoostServiceProvider(app()); $provider->register(); @@ -22,9 +22,9 @@ $this->artisan('list')->expectsOutputToContain('boost:install'); }); - it('boots boost when enabled in local environment', function () { + it('boots boost when enabled in local environment', function (): void { Config::set('boost.enabled', true); - app()->detectEnvironment(fn () => 'local'); + app()->detectEnvironment(fn (): string => 'local'); $provider = new BoostServiceProvider(app()); $provider->register(); @@ -35,11 +35,11 @@ }); }); -describe('environment restrictions', function () { - it('does not boot boost in production even when enabled', function () { +describe('environment restrictions', function (): void { + it('does not boot boost in production even when enabled', function (): void { Config::set('boost.enabled', true); Config::set('app.debug', false); - app()->detectEnvironment(fn () => 'production'); + app()->detectEnvironment(fn (): string => 'production'); $provider = new BoostServiceProvider(app()); $provider->register(); @@ -48,11 +48,11 @@ expect(config('logging.channels.browser'))->toBeNull(); }); - describe('testing environment', function () { - it('does not boot boost when debug is false', function () { + describe('testing environment', function (): void { + it('does not boot boost when debug is false', function (): void { Config::set('boost.enabled', true); Config::set('app.debug', false); - app()->detectEnvironment(fn () => 'testing'); + app()->detectEnvironment(fn (): string => 'testing'); $provider = new BoostServiceProvider(app()); $provider->register(); @@ -61,10 +61,10 @@ expect(config('logging.channels.browser'))->toBeNull(); }); - it('does not boot boost when debug is true', function () { + it('does not boot boost when debug is true', function (): void { Config::set('boost.enabled', true); Config::set('app.debug', true); - app()->detectEnvironment(fn () => 'testing'); + app()->detectEnvironment(fn (): string => 'testing'); $provider = new BoostServiceProvider(app()); $provider->register(); diff --git a/tests/Feature/Console/InstallCommandMultiselectTest.php b/tests/Feature/Console/InstallCommandMultiselectTest.php index 9ad7d53d..ce6630a7 100644 --- a/tests/Feature/Console/InstallCommandMultiselectTest.php +++ b/tests/Feature/Console/InstallCommandMultiselectTest.php @@ -5,7 +5,7 @@ use Laravel\Prompts\Key; use Laravel\Prompts\Prompt; -test('multiselect returns keys for associative array', function () { +test('multiselect returns keys for associative array', function (): void { // Mock the prompt to simulate user selecting options // Note: mcp_server is already selected by default, so we don't toggle it Prompt::fake([ @@ -33,7 +33,7 @@ expect($result)->not->toContain('Package AI Guidelines'); })->skipOnWindows(); -test('multiselect returns values for indexed array', function () { +test('multiselect returns values for indexed array', function (): void { Prompt::fake([ Key::SPACE, // Select first option Key::DOWN, // Move to second option @@ -53,7 +53,7 @@ expect($result)->toContain('Option 2'); })->skipOnWindows(); -test('multiselect behavior matches install command expectations', function () { +test('multiselect behavior matches install command expectations', function (): void { // Test the exact same structure used in InstallCommand::selectBoostFeatures() // Note: mcp_server and ai_guidelines are already selected by default Prompt::fake([ diff --git a/tests/Feature/Console/InstallCommandWslTest.php b/tests/Feature/Console/InstallCommandWslTest.php new file mode 100644 index 00000000..1367df15 --- /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 0ff07a81..5d01d69c 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,16 +24,48 @@ ->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); 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/Feature/Install/Detection/CommandDetectionStrategyTest.php b/tests/Feature/Install/Detection/CommandDetectionStrategyTest.php index 6467550a..501082b3 100644 --- a/tests/Feature/Install/Detection/CommandDetectionStrategyTest.php +++ b/tests/Feature/Install/Detection/CommandDetectionStrategyTest.php @@ -6,11 +6,11 @@ use Laravel\Boost\Install\Detection\CommandDetectionStrategy; use Laravel\Boost\Install\Enums\Platform; -beforeEach(function () { - $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 adb4fe95..e9f2c014 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,33 @@ ->not->toContain('=== phpunit/core rules ==='); }); -test('includes PHPUnit guidelines when Pest is not present', function () { +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'), new Package(Packages::PHPUNIT, 'phpunit/phpunit', '10.0.0'), diff --git a/tests/Feature/Mcp/ToolExecutorTest.php b/tests/Feature/Mcp/ToolExecutorTest.php index 08897172..59db3a7f 100644 --- a/tests/Feature/Mcp/ToolExecutorTest.php +++ b/tests/Feature/Mcp/ToolExecutorTest.php @@ -5,13 +5,13 @@ use Laravel\Boost\Mcp\Tools\Tinker; use Laravel\Mcp\Response; -test('can execute tool in subprocess', function () { +test('can execute tool in subprocess', function (): void { // Create a mock that overrides buildCommand to work with testbench $executor = Mockery::mock(ToolExecutor::class)->makePartial() ->shouldAllowMockingProtectedMethods(); $executor->shouldReceive('buildCommand') ->once() - ->andReturnUsing(fn ($toolClass, $arguments) => buildSubprocessCommand($toolClass, $arguments)); + ->andReturnUsing(fn ($toolClass, $arguments): array => buildSubprocessCommand($toolClass, $arguments)); $response = $executor->execute(GetConfig::class, ['key' => 'app.name']); @@ -30,7 +30,7 @@ expect($textContent)->toContain('Laravel'); }); -test('rejects unregistered tools', function () { +test('rejects unregistered tools', function (): void { $executor = app(ToolExecutor::class); $response = $executor->execute('NonExistentToolClass'); @@ -38,11 +38,11 @@ ->and($response->isError())->toBeTrue(); }); -test('subprocess proves fresh process isolation', function () { +test('subprocess proves fresh process isolation', function (): void { $executor = Mockery::mock(ToolExecutor::class)->makePartial() ->shouldAllowMockingProtectedMethods(); $executor->shouldReceive('buildCommand') - ->andReturnUsing(fn ($toolClass, $arguments) => buildSubprocessCommand($toolClass, $arguments)); + ->andReturnUsing(fn ($toolClass, $arguments): array => buildSubprocessCommand($toolClass, $arguments)); $response1 = $executor->execute(Tinker::class, ['code' => 'return getmypid();']); $response2 = $executor->execute(Tinker::class, ['code' => 'return getmypid();']); @@ -58,18 +58,18 @@ ->and($pid1)->not()->toBe($pid2); }); -test('subprocess sees modified autoloaded code changes', function () { +test('subprocess sees modified autoloaded code changes', function (): void { $executor = Mockery::mock(ToolExecutor::class)->makePartial() ->shouldAllowMockingProtectedMethods(); $executor->shouldReceive('buildCommand') - ->andReturnUsing(fn ($toolClass, $arguments) => buildSubprocessCommand($toolClass, $arguments)); + ->andReturnUsing(fn ($toolClass, $arguments): array => buildSubprocessCommand($toolClass, $arguments)); // Path to the GetConfig tool that we'll temporarily modify // TODO: Improve for parallelisation $toolPath = dirname(__DIR__, 3).'/src/Mcp/Tools/GetConfig.php'; $originalContent = file_get_contents($toolPath); - $cleanup = function () use ($toolPath, $originalContent) { + $cleanup = function () use ($toolPath, $originalContent): void { file_put_contents($toolPath, $originalContent); }; @@ -112,7 +112,7 @@ function buildSubprocessCommand(string $toolClass, array $arguments): array 'use Symfony\Component\Console\Output\BufferedOutput; '. // Bootstrap testbench like all.php does '$app = Testbench::createFromConfig(new TestbenchConfig([]), options: ["enables_package_discoveries" => false]); '. - 'Illuminate\Container\Container::setInstance($app); '. + (\Illuminate\Container\Container::class.'::setInstance($app); '). '$kernel = $app->make("Illuminate\Contracts\Console\Kernel"); '. '$kernel->bootstrap(); '. // Register the ExecuteToolCommand @@ -131,12 +131,12 @@ function buildSubprocessCommand(string $toolClass, array $arguments): array return [PHP_BINARY, '-r', $testScript]; } -test('respects custom timeout parameter', function () { +test('respects custom timeout parameter', function (): void { $executor = Mockery::mock(ToolExecutor::class)->makePartial() ->shouldAllowMockingProtectedMethods(); $executor->shouldReceive('buildCommand') - ->andReturnUsing(fn ($toolClass, $arguments) => buildSubprocessCommand($toolClass, $arguments)); + ->andReturnUsing(fn ($toolClass, $arguments): array => buildSubprocessCommand($toolClass, $arguments)); // Test with custom timeout - should succeed with fast code $response = $executor->execute(Tinker::class, [ @@ -147,13 +147,12 @@ function buildSubprocessCommand(string $toolClass, array $arguments): array expect($response->isError())->toBeFalse(); }); -test('clamps timeout values correctly', function () { - $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 618f8612..c87f9db5 100644 --- a/tests/Feature/Mcp/ToolRegistryTest.php +++ b/tests/Feature/Mcp/ToolRegistryTest.php @@ -3,19 +3,19 @@ use Laravel\Boost\Mcp\ToolRegistry; use Laravel\Boost\Mcp\Tools\ApplicationInfo; -test('can discover available tools', function () { +test('can discover available tools', function (): void { $tools = ToolRegistry::getAvailableTools(); expect($tools)->toBeArray() ->and($tools)->toContain(ApplicationInfo::class); }); -test('can check if tool is allowed', function () { +test('can check if tool is allowed', function (): void { expect(ToolRegistry::isToolAllowed(ApplicationInfo::class))->toBeTrue() ->and(ToolRegistry::isToolAllowed('NonExistentTool'))->toBeFalse(); }); -test('can get tool names', function () { +test('can get tool names', function (): void { $tools = ToolRegistry::getToolNames(); expect($tools)->toBeArray() @@ -23,7 +23,7 @@ ->and($tools['ApplicationInfo'])->toBe(ApplicationInfo::class); }); -test('can clear cache', function () { +test('can clear cache', function (): void { // First call caches the results $tools1 = ToolRegistry::getAvailableTools(); diff --git a/tests/Feature/Mcp/Tools/ApplicationInfoTest.php b/tests/Feature/Mcp/Tools/ApplicationInfoTest.php index d0582f9d..97923719 100644 --- a/tests/Feature/Mcp/Tools/ApplicationInfoTest.php +++ b/tests/Feature/Mcp/Tools/ApplicationInfoTest.php @@ -10,7 +10,7 @@ use Laravel\Roster\PackageCollection; use Laravel\Roster\Roster; -test('it returns application info with packages', function () { +test('it returns application info with packages', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::PEST, 'pestphp/pest', '2.0.0'), @@ -53,7 +53,7 @@ ]); }); -test('it returns application info with no packages', function () { +test('it returns application info with no packages', function (): void { $roster = Mockery::mock(Roster::class); $roster->shouldReceive('packages')->andReturn(new PackageCollection([])); diff --git a/tests/Feature/Mcp/Tools/BrowserLogsTest.php b/tests/Feature/Mcp/Tools/BrowserLogsTest.php index 91ae0bce..ba56781d 100644 --- a/tests/Feature/Mcp/Tools/BrowserLogsTest.php +++ b/tests/Feature/Mcp/Tools/BrowserLogsTest.php @@ -12,7 +12,7 @@ use Laravel\Boost\Services\BrowserLogger; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { // Clean up any existing browser.log file before each test $logFile = storage_path('logs/browser.log'); if (File::exists($logFile)) { @@ -20,7 +20,7 @@ } }); -test('it returns log entries when file exists', function () { +test('it returns log entries when file exists', function (): void { // Create a fake browser.log file with some entries $logFile = storage_path('logs/browser.log'); File::ensureDirectoryExists(dirname($logFile)); @@ -45,7 +45,7 @@ expect($response)->isToolResult(); }); -test('it returns error when entries argument is invalid', function () { +test('it returns error when entries argument is invalid', function (): void { $tool = new BrowserLogs; // Test with zero @@ -61,7 +61,7 @@ ->toolTextContains('The "entries" argument must be greater than 0.'); }); -test('it returns error when log file does not exist', function () { +test('it returns error when log file does not exist', function (): void { $tool = new BrowserLogs; $response = $tool->handle(new Request(['entries' => 10])); @@ -70,7 +70,7 @@ ->toolTextContains('No log file found, probably means no logs yet.'); }); -test('it returns error when log file is empty', function () { +test('it returns error when log file is empty', function (): void { // Create an empty browser.log file $logFile = storage_path('logs/browser.log'); File::ensureDirectoryExists(dirname($logFile)); @@ -84,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 ba6372e0..b5a1266a 100644 --- a/tests/Feature/Mcp/Tools/DatabaseConnectionsTest.php +++ b/tests/Feature/Mcp/Tools/DatabaseConnectionsTest.php @@ -5,7 +5,7 @@ use Laravel\Boost\Mcp\Tools\DatabaseConnections; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { config()->set('database.default', 'mysql'); config()->set('database.connections', [ 'mysql' => ['driver' => 'mysql'], @@ -14,7 +14,7 @@ ]); }); -test('it returns database connections', function () { +test('it returns database connections', function (): void { $tool = new DatabaseConnections; $response = $tool->handle(new Request([])); @@ -26,7 +26,7 @@ ]); }); -test('it returns empty connections when none configured', function () { +test('it returns empty connections when none configured', function (): void { config()->set('database.connections', []); $tool = new DatabaseConnections; diff --git a/tests/Feature/Mcp/Tools/DatabaseSchemaTest.php b/tests/Feature/Mcp/Tools/DatabaseSchemaTest.php index 53dd97fd..dee84026 100644 --- a/tests/Feature/Mcp/Tools/DatabaseSchemaTest.php +++ b/tests/Feature/Mcp/Tools/DatabaseSchemaTest.php @@ -8,7 +8,7 @@ use Laravel\Boost\Mcp\Tools\DatabaseSchema; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { // Switch the default connection to a file-backed SQLite DB. config()->set('database.default', 'testing'); config()->set('database.connections.testing', [ @@ -24,20 +24,20 @@ // Build a throw-away table that we expect in the dump. Schema::dropIfExists('examples'); - Schema::create('examples', function (Blueprint $table) { + Schema::create('examples', function (Blueprint $table): void { $table->id(); $table->string('name'); }); }); -afterEach(function () { +afterEach(function (): void { $dbFile = database_path('testing.sqlite'); if (File::exists($dbFile)) { File::delete($dbFile); } }); -test('it returns structured database schema', function () { +test('it returns structured database schema', function (): void { $tool = new DatabaseSchema; $response = $tool->handle(new Request([])); @@ -46,7 +46,7 @@ ->toolJsonContentToMatchArray([ 'engine' => 'sqlite', ]) - ->toolJsonContent(function ($schemaArray) { + ->toolJsonContent(function (array $schemaArray): void { expect($schemaArray)->toHaveKey('tables') ->and($schemaArray['tables'])->toHaveKey('examples'); @@ -61,9 +61,9 @@ }); }); -test('it filters tables by name', function () { +test('it filters tables by name', function (): void { // Create another table - Schema::create('users', function (Blueprint $table) { + Schema::create('users', function (Blueprint $table): void { $table->id(); $table->string('email'); }); @@ -74,7 +74,7 @@ $response = $tool->handle(new Request(['filter' => 'example'])); expect($response)->isToolResult() ->toolHasNoError() - ->toolJsonContent(function ($schemaArray) { + ->toolJsonContent(function (array $schemaArray): void { expect($schemaArray['tables'])->toHaveKey('examples') ->and($schemaArray['tables'])->not->toHaveKey('users'); }); @@ -83,7 +83,7 @@ $response = $tool->handle(new Request(['filter' => 'user'])); expect($response)->isToolResult() ->toolHasNoError() - ->toolJsonContent(function ($schemaArray) { + ->toolJsonContent(function (array $schemaArray): void { expect($schemaArray['tables'])->toHaveKey('users') ->and($schemaArray['tables'])->not->toHaveKey('examples'); }); diff --git a/tests/Feature/Mcp/Tools/GetAbsoluteUrlTest.php b/tests/Feature/Mcp/Tools/GetAbsoluteUrlTest.php index 652081dc..0b50954c 100644 --- a/tests/Feature/Mcp/Tools/GetAbsoluteUrlTest.php +++ b/tests/Feature/Mcp/Tools/GetAbsoluteUrlTest.php @@ -6,14 +6,12 @@ use Laravel\Boost\Mcp\Tools\GetAbsoluteUrl; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { config()->set('app.url', 'http://localhost'); - Route::get('/test', function () { - return 'test'; - })->name('test.route'); + Route::get('/test', fn (): string => 'test')->name('test.route'); }); -test('it returns absolute url for root path by default', function () { +test('it returns absolute url for root path by default', function (): void { $tool = new GetAbsoluteUrl; $response = $tool->handle(new Request([])); @@ -22,7 +20,7 @@ ->toolTextContains('http://localhost'); }); -test('it returns absolute url for given path', function () { +test('it returns absolute url for given path', function (): void { $tool = new GetAbsoluteUrl; $response = $tool->handle(new Request(['path' => '/dashboard'])); @@ -31,7 +29,7 @@ ->toolTextContains('http://localhost/dashboard'); }); -test('it returns absolute url for named route', function () { +test('it returns absolute url for named route', function (): void { $tool = new GetAbsoluteUrl; $response = $tool->handle(new Request(['route' => 'test.route'])); @@ -40,7 +38,7 @@ ->toolTextContains('http://localhost/test'); }); -test('it prioritizes path over route when both are provided', function () { +test('it prioritizes path over route when both are provided', function (): void { $tool = new GetAbsoluteUrl; $response = $tool->handle(new Request(['path' => '/dashboard', 'route' => 'test.route'])); @@ -49,7 +47,7 @@ ->toolTextContains('http://localhost/dashboard'); }); -test('it handles empty path', function () { +test('it handles empty path', function (): void { $tool = new GetAbsoluteUrl; $response = $tool->handle(new Request(['path' => ''])); diff --git a/tests/Feature/Mcp/Tools/GetConfigTest.php b/tests/Feature/Mcp/Tools/GetConfigTest.php index 29d2de81..8efb35a3 100644 --- a/tests/Feature/Mcp/Tools/GetConfigTest.php +++ b/tests/Feature/Mcp/Tools/GetConfigTest.php @@ -5,13 +5,13 @@ use Laravel\Boost\Mcp\Tools\GetConfig; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { config()->set('test.key', 'test_value'); config()->set('nested.config.key', 'nested_value'); config()->set('app.name', 'Test App'); }); -test('it returns config value when key exists', function () { +test('it returns config value when key exists', function (): void { $tool = new GetConfig; $response = $tool->handle(new Request(['key' => 'test.key'])); @@ -20,7 +20,7 @@ ->toolTextContains('"key": "test.key"', '"value": "test_value"'); }); -test('it returns nested config value', function () { +test('it returns nested config value', function (): void { $tool = new GetConfig; $response = $tool->handle(new Request(['key' => 'nested.config.key'])); @@ -29,7 +29,7 @@ ->toolTextContains('"key": "nested.config.key"', '"value": "nested_value"'); }); -test('it returns error when config key does not exist', function () { +test('it returns error when config key does not exist', function (): void { $tool = new GetConfig; $response = $tool->handle(new Request(['key' => 'nonexistent.key'])); @@ -38,7 +38,7 @@ ->toolTextContains("Config key 'nonexistent.key' not found."); }); -test('it works with built-in Laravel config keys', function () { +test('it works with built-in Laravel config keys', function (): void { $tool = new GetConfig; $response = $tool->handle(new Request(['key' => 'app.name'])); diff --git a/tests/Feature/Mcp/Tools/ListArtisanCommandsTest.php b/tests/Feature/Mcp/Tools/ListArtisanCommandsTest.php index 8723bebd..801b41c9 100644 --- a/tests/Feature/Mcp/Tools/ListArtisanCommandsTest.php +++ b/tests/Feature/Mcp/Tools/ListArtisanCommandsTest.php @@ -5,13 +5,13 @@ use Laravel\Boost\Mcp\Tools\ListArtisanCommands; use Laravel\Mcp\Request; -test('it returns list of artisan commands', function () { +test('it returns list of artisan commands', function (): void { $tool = new ListArtisanCommands; $response = $tool->handle(new Request([])); expect($response)->isToolResult() ->toolHasNoError() - ->toolJsonContent(function ($content) { + ->toolJsonContent(function ($content): void { expect($content)->toBeArray() ->and($content)->not->toBeEmpty(); diff --git a/tests/Feature/Mcp/Tools/ListAvailableConfigKeysTest.php b/tests/Feature/Mcp/Tools/ListAvailableConfigKeysTest.php index c2d8d062..5ce139d5 100644 --- a/tests/Feature/Mcp/Tools/ListAvailableConfigKeysTest.php +++ b/tests/Feature/Mcp/Tools/ListAvailableConfigKeysTest.php @@ -5,19 +5,19 @@ use Laravel\Boost\Mcp\Tools\ListAvailableConfigKeys; use Laravel\Mcp\Request; -beforeEach(function () { +beforeEach(function (): void { config()->set('test.simple', 'value'); config()->set('test.nested.key', 'nested_value'); config()->set('test.array', ['item1', 'item2']); }); -test('it returns list of config keys in dot notation', function () { +test('it returns list of config keys in dot notation', function (): void { $tool = new ListAvailableConfigKeys; $response = $tool->handle(new Request([])); expect($response)->isToolResult() ->toolHasNoError() - ->toolJsonContent(function ($content) { + ->toolJsonContent(function ($content): void { expect($content)->toBeArray() ->and($content)->not->toBeEmpty() // Check that it contains common Laravel config keys @@ -37,7 +37,7 @@ }); }); -test('it handles empty config gracefully', function () { +test('it handles empty config gracefully', function (): void { // Clear all config config()->set('test', null); @@ -46,7 +46,7 @@ expect($response)->isToolResult() ->toolHasNoError() - ->toolJsonContent(function ($content) { + ->toolJsonContent(function ($content): void { expect($content)->toBeArray() // Should still have Laravel default config keys ->and($content)->toContain('app.name'); diff --git a/tests/Feature/Mcp/Tools/ListRoutesTest.php b/tests/Feature/Mcp/Tools/ListRoutesTest.php index 7f8e3602..c93244e8 100644 --- a/tests/Feature/Mcp/Tools/ListRoutesTest.php +++ b/tests/Feature/Mcp/Tools/ListRoutesTest.php @@ -6,33 +6,21 @@ use Laravel\Boost\Mcp\Tools\ListRoutes; use Laravel\Mcp\Request; -beforeEach(function () { - Route::get('/admin/dashboard', function () { - return 'admin dashboard'; - })->name('admin.dashboard'); - - Route::post('/admin/users', function () { - return 'admin users'; - })->name('admin.users.store'); - - Route::get('/user/profile', function () { - return 'user profile'; - })->name('user.profile'); - - Route::get('/api/two-factor/enable', function () { - return 'two-factor enable'; - })->name('two-factor.enable'); - - Route::get('/api/v1/posts', function () { - return 'posts'; - })->name('api.posts.index'); - - Route::put('/api/v1/posts/{id}', function ($id) { - return 'update post'; - })->name('api.posts.update'); +beforeEach(function (): void { + Route::get('/admin/dashboard', fn (): string => 'admin dashboard')->name('admin.dashboard'); + + Route::post('/admin/users', fn (): string => 'admin users')->name('admin.users.store'); + + Route::get('/user/profile', fn (): string => 'user profile')->name('user.profile'); + + Route::get('/api/two-factor/enable', fn (): string => 'two-factor enable')->name('two-factor.enable'); + + Route::get('/api/v1/posts', fn (): string => 'posts')->name('api.posts.index'); + + Route::put('/api/v1/posts/{id}', fn ($id): string => 'update post')->name('api.posts.update'); }); -test('it returns list of routes without filters', function () { +test('it returns list of routes without filters', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request([])); @@ -41,7 +29,7 @@ ->toolTextContains('GET|HEAD', 'admin.dashboard', 'user.profile'); }); -test('it sanitizes name parameter wildcards and filters correctly', function () { +test('it sanitizes name parameter wildcards and filters correctly', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request(['name' => '*admin*'])); @@ -63,14 +51,14 @@ }); -test('it sanitizes method parameter wildcards and filters correctly', function () { +test('it sanitizes method parameter wildcards and filters correctly', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request(['method' => 'GET*POST'])); expect($response)->isToolResult() ->toolHasNoError() - ->toolTextContains('ERROR Your application doesn\'t have any routes matching the given criteria.'); + ->toolTextContains("ERROR Your application doesn't have any routes matching the given criteria."); $response = $tool->handle(new Request(['method' => '*GET*'])); @@ -83,7 +71,7 @@ ->and($response)->not->toolTextContains('admin.dashboard'); }); -test('it handles edge cases and empty results correctly', function () { +test('it handles edge cases and empty results correctly', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request(['name' => '*'])); @@ -94,14 +82,14 @@ $response = $tool->handle(new Request(['name' => '*nonexistent*'])); - expect($response)->toolTextContains('ERROR Your application doesn\'t have any routes matching the given criteria.'); + expect($response)->toolTextContains("ERROR Your application doesn't have any routes matching the given criteria."); $response = $tool->handle(new Request(['name' => ''])); expect($response)->toolTextContains('admin.dashboard', 'user.profile'); }); -test('it handles multiple parameters with wildcard sanitization', function () { +test('it handles multiple parameters with wildcard sanitization', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request([ @@ -122,7 +110,7 @@ expect($response)->toolTextContains('admin.users.store'); }); -test('it handles the original problematic wildcard case', function () { +test('it handles the original problematic wildcard case', function (): void { $tool = new ListRoutes; $response = $tool->handle(new Request(['name' => '*two-factor*'])); diff --git a/tests/Feature/Mcp/Tools/SearchDocsTest.php b/tests/Feature/Mcp/Tools/SearchDocsTest.php index 67517474..746489d9 100644 --- a/tests/Feature/Mcp/Tools/SearchDocsTest.php +++ b/tests/Feature/Mcp/Tools/SearchDocsTest.php @@ -10,7 +10,7 @@ use Laravel\Roster\PackageCollection; use Laravel\Roster\Roster; -test('it searches documentation successfully', function () { +test('it searches documentation successfully', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::PEST, 'pestphp/pest', '2.0.0'), @@ -30,19 +30,17 @@ ->toolHasNoError() ->toolTextContains('Documentation search results'); - Http::assertSent(function ($request) { - return $request->url() === 'https://boost.laravel.com/api/docs' && - $request->data()['queries'] === ['authentication', 'testing'] && - $request->data()['packages'] === [ - ['name' => 'laravel/framework', 'version' => '11.x'], - ['name' => 'pestphp/pest', 'version' => '2.x'], - ] && - $request->data()['token_limit'] === 10000 && - $request->data()['format'] === 'markdown'; - }); + Http::assertSent(fn ($request): bool => $request->url() === 'https://boost.laravel.com/api/docs' && + $request->data()['queries'] === ['authentication', 'testing'] && + $request->data()['packages'] === [ + ['name' => 'laravel/framework', 'version' => '11.x'], + ['name' => 'pestphp/pest', 'version' => '2.x'], + ] && + $request->data()['token_limit'] === 10000 && + $request->data()['format'] === 'markdown'); }); -test('it handles API error response', function () { +test('it handles API error response', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), ]); @@ -62,7 +60,7 @@ ->toolTextContains('Failed to search documentation: API Error'); }); -test('it filters empty queries', function () { +test('it filters empty queries', function (): void { $packages = new PackageCollection([]); $roster = Mockery::mock(Roster::class); @@ -78,15 +76,13 @@ expect($response)->isToolResult() ->toolHasNoError(); - Http::assertSent(function ($request) { - return $request->url() === 'https://boost.laravel.com/api/docs' && - $request->data()['queries'] === ['test'] && - empty($request->data()['packages']) && - $request->data()['token_limit'] === 10000; - }); + Http::assertSent(fn ($request): bool => $request->url() === 'https://boost.laravel.com/api/docs' && + $request->data()['queries'] === ['test'] && + empty($request->data()['packages']) && + $request->data()['token_limit'] === 10000); }); -test('it formats package data correctly', function () { +test('it formats package data correctly', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), new Package(Packages::LIVEWIRE, 'livewire/livewire', '3.5.1'), @@ -105,15 +101,13 @@ expect($response)->isToolResult() ->toolHasNoError(); - Http::assertSent(function ($request) { - return $request->data()['packages'] === [ - ['name' => 'laravel/framework', 'version' => '11.x'], - ['name' => 'livewire/livewire', 'version' => '3.x'], - ] && $request->data()['token_limit'] === 10000; - }); + Http::assertSent(fn ($request): bool => $request->data()['packages'] === [ + ['name' => 'laravel/framework', 'version' => '11.x'], + ['name' => 'livewire/livewire', 'version' => '3.x'], + ] && $request->data()['token_limit'] === 10000); }); -test('it handles empty results', function () { +test('it handles empty results', function (): void { $packages = new PackageCollection([]); $roster = Mockery::mock(Roster::class); @@ -131,7 +125,7 @@ ->toolTextContains('Empty response'); }); -test('it uses custom token_limit when provided', function () { +test('it uses custom token_limit when provided', function (): void { $packages = new PackageCollection([]); $roster = Mockery::mock(Roster::class); @@ -146,12 +140,10 @@ expect($response)->isToolResult()->toolHasNoError(); - Http::assertSent(function ($request) { - return $request->data()['token_limit'] === 5000; - }); + Http::assertSent(fn ($request): bool => $request->data()['token_limit'] === 5000); }); -test('it caps token_limit at maximum of 1000000', function () { +test('it caps token_limit at maximum of 1000000', function (): void { $packages = new PackageCollection([]); $roster = Mockery::mock(Roster::class); @@ -166,7 +158,5 @@ expect($response)->isToolResult()->toolHasNoError(); - Http::assertSent(function ($request) { - return $request->data()['token_limit'] === 1000000; - }); + Http::assertSent(fn ($request): bool => $request->data()['token_limit'] === 1000000); }); diff --git a/tests/Feature/Mcp/Tools/TinkerTest.php b/tests/Feature/Mcp/Tools/TinkerTest.php index f3f5dfbb..c6419a66 100644 --- a/tests/Feature/Mcp/Tools/TinkerTest.php +++ b/tests/Feature/Mcp/Tools/TinkerTest.php @@ -5,7 +5,7 @@ use Laravel\Boost\Mcp\Tools\Tinker; use Laravel\Mcp\Request; -test('executes simple php code', function () { +test('executes simple php code', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'return 2 + 2;'])); @@ -16,7 +16,7 @@ ]); }); -test('executes code with output', function () { +test('executes code with output', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'echo "Hello World"; return "test";'])); @@ -28,7 +28,7 @@ ]); }); -test('accesses laravel facades', function () { +test('accesses laravel facades', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'return config("app.name");'])); @@ -39,7 +39,7 @@ ]); }); -test('creates objects', function () { +test('creates objects', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'return new stdClass();'])); @@ -50,7 +50,7 @@ ]); }); -test('handles syntax errors', function () { +test('handles syntax errors', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'invalid syntax here'])); @@ -59,12 +59,12 @@ ->toolJsonContentToMatchArray([ 'type' => 'ParseError', ]) - ->toolJsonContent(function ($data) { + ->toolJsonContent(function ($data): void { expect($data)->toHaveKey('error'); }); }); -test('handles runtime errors', function () { +test('handles runtime errors', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'throw new Exception("Test error");'])); @@ -74,12 +74,12 @@ 'type' => 'Exception', 'error' => 'Test error', ]) - ->toolJsonContent(function ($data) { + ->toolJsonContent(function ($data): void { expect($data)->toHaveKey('error'); }); }); -test('captures multiple outputs', function () { +test('captures multiple outputs', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => 'echo "First"; echo "Second"; return "done";'])); @@ -90,7 +90,7 @@ ]); }); -test('executes code with different return types', function (string $code, mixed $expectedResult, string $expectedType) { +test('executes code with different return types', function (string $code, mixed $expectedResult, string $expectedType): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => $code])); @@ -109,7 +109,7 @@ 'float' => ['return 3.14;', 3.14, 'double'], ]); -test('handles empty code', function () { +test('handles empty code', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => ''])); @@ -120,7 +120,7 @@ ]); }); -test('handles code with no return statement', function () { +test('handles code with no return statement', function (): void { $tool = new Tinker; $response = $tool->handle(new Request(['code' => '$x = 5;'])); @@ -131,12 +131,10 @@ ]); }); -test('should register only in local environment', function () { +test('should register only in local environment', function (): void { $tool = new Tinker; - app()->detectEnvironment(function () { - return 'local'; - }); + app()->detectEnvironment(fn (): string => 'local'); expect($tool->eligibleForRegistration(Mockery::mock(Request::class)))->toBeTrue(); }); diff --git a/tests/Feature/Middleware/InjectBoostTest.php b/tests/Feature/Middleware/InjectBoostTest.php index 08f97594..797efd14 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 14b4b9fa..05eb243b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -17,9 +17,7 @@ uses(Tests\TestCase::class)->in('Feature'); -expect()->extend('isToolResult', function () { - return $this->toBeInstanceOf(Response::class); -}); +expect()->extend('isToolResult', fn () => $this->toBeInstanceOf(Response::class)); expect()->extend('toolTextContains', function (mixed ...$needles) { /** @var Response $this->value */ diff --git a/tests/Unit/Install/Cli/DisplayHelperTest.php b/tests/Unit/Install/Cli/DisplayHelperTest.php index b8771260..a05e075a 100644 --- a/tests/Unit/Install/Cli/DisplayHelperTest.php +++ b/tests/Unit/Install/Cli/DisplayHelperTest.php @@ -4,9 +4,9 @@ use Laravel\Boost\Install\Cli\DisplayHelper; -describe('DisplayHelper tests', function () { - describe('datatable tests', function () { - it('returns early for empty data', function () { +describe('DisplayHelper tests', function (): void { + describe('datatable tests', function (): void { + it('returns early for empty data', function (): void { ob_start(); DisplayHelper::datatable([]); $output = ob_get_clean(); @@ -14,7 +14,7 @@ expect($output)->toBe(''); }); - it('displays a simple single row table', function () { + it('displays a simple single row table', function (): void { ob_start(); DisplayHelper::datatable([ ['Name', 'Age'], @@ -29,7 +29,7 @@ ->and($output)->toContain('╯'); }); - it('displays a multi-row table', function () { + it('displays a multi-row table', function (): void { ob_start(); DisplayHelper::datatable([ ['Name', 'Age', 'City'], @@ -46,7 +46,7 @@ ->and($output)->toContain('┼'); }); - it('handles different data types in cells', function () { + it('handles different data types in cells', function (): void { ob_start(); DisplayHelper::datatable([ ['String', 'Number', 'Boolean'], @@ -62,7 +62,7 @@ ->and($output)->toContain('456'); }); - it('applies bold formatting to first column', function () { + it('applies bold formatting to first column', function (): void { ob_start(); DisplayHelper::datatable([ ['Header1', 'Header2'], @@ -75,7 +75,7 @@ ->and($output)->not->toContain("\e[1mHeader2\e[0m"); }); - it('handles unicode characters properly', function () { + it('handles unicode characters properly', function (): void { ob_start(); DisplayHelper::datatable([ ['名前', 'Émile'], @@ -90,8 +90,8 @@ }); }); - describe('grid test', function () { - it('returns early for empty items', function () { + describe('grid test', function (): void { + it('returns early for empty items', function (): void { ob_start(); DisplayHelper::grid([]); $output = ob_get_clean(); @@ -99,7 +99,7 @@ expect($output)->toBe(''); }); - it('displays single item grid', function () { + it('displays single item grid', function (): void { ob_start(); DisplayHelper::grid(['Item1']); $output = ob_get_clean(); @@ -111,7 +111,7 @@ ->and($output)->toContain('╯'); }); - it('displays multiple items in grid', function () { + it('displays multiple items in grid', function (): void { ob_start(); DisplayHelper::grid(['Item1', 'Item2', 'Item3', 'Item4']); $output = ob_get_clean(); @@ -122,7 +122,7 @@ ->and($output)->toContain('Item4'); }); - it('handles items of different lengths', function () { + it('handles items of different lengths', function (): void { ob_start(); DisplayHelper::grid(['Short', 'Very Long Item Name', 'Med']); $output = ob_get_clean(); @@ -132,7 +132,7 @@ ->and($output)->toContain('Med'); }); - it('respects column width parameter', function () { + it('respects column width parameter', function (): void { ob_start(); DisplayHelper::grid(['Item1', 'Item2'], 40); $output = ob_get_clean(); @@ -141,7 +141,7 @@ ->and($output)->toContain('Item2'); }); - it('handles unicode characters in grid', function () { + it('handles unicode characters in grid', function (): void { ob_start(); DisplayHelper::grid(['測試', 'café', '🚀']); $output = ob_get_clean(); @@ -151,7 +151,7 @@ ->and($output)->toContain('🚀'); }); - it('fills empty cells when items do not fill complete rows', function () { + it('fills empty cells when items do not fill complete rows', function (): void { ob_start(); DisplayHelper::grid(['Item1', 'Item2', 'Item3']); $output = ob_get_clean(); diff --git a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php index 81ae1de3..25c54906 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 = ''; @@ -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'); +}); diff --git a/tests/Unit/Install/CodeEnvironmentsDetectorTest.php b/tests/Unit/Install/CodeEnvironmentsDetectorTest.php index c11133ed..dc46503c 100644 --- a/tests/Unit/Install/CodeEnvironmentsDetectorTest.php +++ b/tests/Unit/Install/CodeEnvironmentsDetectorTest.php @@ -6,16 +6,16 @@ use Laravel\Boost\Install\CodeEnvironmentsDetector; use Laravel\Boost\Install\Enums\Platform; -beforeEach(function () { +beforeEach(function (): void { $this->container = new \Illuminate\Container\Container; $this->detector = new CodeEnvironmentsDetector($this->container); }); -afterEach(function () { +afterEach(function (): void { Mockery::close(); }); -test('discoverSystemInstalledCodeEnvironments returns detected programs', function () { +test('discoverSystemInstalledCodeEnvironments returns detected programs', function (): void { // Create mock programs $program1 = Mockery::mock(CodeEnvironment::class); $program1->shouldReceive('detectOnSystem')->with(Mockery::type(Platform::class))->andReturn(true); @@ -49,7 +49,7 @@ expect($detected)->toBe(['phpstorm', 'cursor']); }); -test('discoverSystemInstalledCodeEnvironments returns empty array when no programs detected', function () { +test('discoverSystemInstalledCodeEnvironments returns empty array when no programs detected', function (): void { $program1 = Mockery::mock(CodeEnvironment::class); $program1->shouldReceive('detectOnSystem')->with(Mockery::type(Platform::class))->andReturn(false); $program1->shouldReceive('name')->andReturn('phpstorm'); @@ -74,7 +74,7 @@ expect($detected)->toBeEmpty(); }); -test('discoverProjectInstalledCodeEnvironments detects programs in project', function () { +test('discoverProjectInstalledCodeEnvironments detects programs in project', function (): void { $basePath = '/path/to/project'; $program1 = Mockery::mock(CodeEnvironment::class); @@ -101,7 +101,7 @@ expect($detected)->toBe(['vscode', 'claudecode']); }); -test('discoverProjectInstalledCodeEnvironments returns empty array when no programs detected in project', function () { +test('discoverProjectInstalledCodeEnvironments returns empty array when no programs detected in project', function (): void { $basePath = '/path/to/project'; $program1 = Mockery::mock(CodeEnvironment::class); @@ -118,7 +118,7 @@ expect($detected)->toBeEmpty(); }); -test('discoverProjectInstalledCodeEnvironments detects applications by directory', function () { +test('discoverProjectInstalledCodeEnvironments detects applications by directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.vscode'); @@ -132,7 +132,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects applications with mixed type', function () { +test('discoverProjectInstalledCodeEnvironments detects applications with mixed type', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); file_put_contents($tempDir.'/CLAUDE.md', 'test'); @@ -145,7 +145,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects copilot with nested file path', function () { +test('discoverProjectInstalledCodeEnvironments detects copilot with nested file path', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.github'); @@ -161,7 +161,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects claude code with directory', function () { +test('discoverProjectInstalledCodeEnvironments detects claude code with directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.claude'); @@ -174,7 +174,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects phpstorm with idea directory', function () { +test('discoverProjectInstalledCodeEnvironments detects phpstorm with idea directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.idea'); @@ -188,7 +188,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects phpstorm with junie directory', function () { +test('discoverProjectInstalledCodeEnvironments detects phpstorm with junie directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.junie'); @@ -202,7 +202,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects cursor with cursor directory', function () { +test('discoverProjectInstalledCodeEnvironments detects cursor with cursor directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.cursor'); @@ -216,7 +216,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects codex with codex directory', function () { +test('discoverProjectInstalledCodeEnvironments detects codex with codex directory', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.codex'); @@ -229,7 +229,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments detects codex with AGENTS.md file', function () { +test('discoverProjectInstalledCodeEnvironments detects codex with AGENTS.md file', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); file_put_contents($tempDir.'/AGENTS.md', 'test'); @@ -242,7 +242,7 @@ rmdir($tempDir); }); -test('discoverProjectInstalledCodeEnvironments handles multiple detections', function () { +test('discoverProjectInstalledCodeEnvironments handles multiple detections', function (): void { $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($tempDir); mkdir($tempDir.'/.vscode'); diff --git a/tests/Unit/Install/Detection/CommandDetectionStrategyTest.php b/tests/Unit/Install/Detection/CommandDetectionStrategyTest.php index 6c3d01b7..d2fbfcf6 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 d587bab1..a7feaab1 100644 --- a/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php +++ b/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php @@ -6,17 +6,17 @@ use Laravel\Boost\Install\Detection\CompositeDetectionStrategy; use Laravel\Boost\Install\Enums\Platform; -beforeEach(function () { +beforeEach(function (): void { $this->firstStrategy = Mockery::mock(DetectionStrategy::class); $this->secondStrategy = Mockery::mock(DetectionStrategy::class); $this->thirdStrategy = Mockery::mock(DetectionStrategy::class); }); -afterEach(function () { +afterEach(function (): void { Mockery::close(); }); -test('returns true when first strategy succeeds', function () { +test('returns true when first strategy succeeds', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -36,7 +36,7 @@ expect($result)->toBeTrue(); }); -test('returns true when second strategy succeeds', function () { +test('returns true when second strategy succeeds', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -59,7 +59,7 @@ expect($result)->toBeTrue(); }); -test('returns false when all strategies fail', function () { +test('returns false when all strategies fail', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -89,7 +89,7 @@ expect($result)->toBeFalse(); }); -test('stops execution after first success', function () { +test('stops execution after first success', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -116,7 +116,7 @@ expect($result)->toBeTrue(); }); -test('handles empty strategies array', function () { +test('handles empty strategies array', function (): void { $composite = new CompositeDetectionStrategy([]); $result = $composite->detect(['config' => 'value']); @@ -124,7 +124,7 @@ expect($result)->toBeFalse(); }); -test('handles single strategy', function () { +test('handles single strategy', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -140,7 +140,7 @@ expect($result)->toBeTrue(); }); -test('passes platform parameter to all strategies', function () { +test('passes platform parameter to all strategies', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -163,7 +163,7 @@ expect($result)->toBeFalse(); }); -test('handles null platform parameter', function () { +test('handles null platform parameter', function (): void { $this->firstStrategy ->shouldReceive('detect') ->once() @@ -179,7 +179,7 @@ expect($result)->toBeTrue(); }); -test('handles mixed strategy types', function () { +test('handles mixed strategy types', function (): void { // This test simulates real-world usage where different strategy types // might be combined (directory, file, command, etc.) diff --git a/tests/Unit/Install/Detection/DetectionStrategyFactoryTest.php b/tests/Unit/Install/Detection/DetectionStrategyFactoryTest.php index c487aded..840d90aa 100644 --- a/tests/Unit/Install/Detection/DetectionStrategyFactoryTest.php +++ b/tests/Unit/Install/Detection/DetectionStrategyFactoryTest.php @@ -9,30 +9,30 @@ use Laravel\Boost\Install\Detection\DirectoryDetectionStrategy; use Laravel\Boost\Install\Detection\FileDetectionStrategy; -beforeEach(function () { - $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 cba43233..488c01f1 100644 --- a/tests/Unit/Install/Detection/DirectoryDetectionStrategyTest.php +++ b/tests/Unit/Install/Detection/DirectoryDetectionStrategyTest.php @@ -5,19 +5,19 @@ use Laravel\Boost\Install\Detection\DirectoryDetectionStrategy; use Laravel\Boost\Install\Enums\Platform; -beforeEach(function () { - $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 cab3b0df..431035a6 100644 --- a/tests/Unit/Install/Detection/FileDetectionStrategyTest.php +++ b/tests/Unit/Install/Detection/FileDetectionStrategyTest.php @@ -4,19 +4,19 @@ use Laravel\Boost\Install\Detection\FileDetectionStrategy; -beforeEach(function () { +beforeEach(function (): void { $this->strategy = new FileDetectionStrategy; $this->tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); mkdir($this->tempDir); }); -afterEach(function () { +afterEach(function (): void { if (is_dir($this->tempDir) && str_contains($this->tempDir, sys_get_temp_dir())) { removeDirectoryForFileTests($this->tempDir); } }); -test('detects existing file', function () { +test('detects existing file', function (): void { file_put_contents($this->tempDir.'/test.txt', 'test content'); $result = $this->strategy->detect([ @@ -27,7 +27,7 @@ expect($result)->toBeTrue(); }); -test('fails for non existent file', function () { +test('fails for non existent file', function (): void { $result = $this->strategy->detect([ 'files' => ['non_existent.txt'], 'basePath' => $this->tempDir, @@ -36,7 +36,7 @@ expect($result)->toBeFalse(); }); -test('detects multiple files first exists', function () { +test('detects multiple files first exists', function (): void { file_put_contents($this->tempDir.'/first.txt', 'content'); $result = $this->strategy->detect([ @@ -47,7 +47,7 @@ expect($result)->toBeTrue(); }); -test('detects multiple files second exists', function () { +test('detects multiple files second exists', function (): void { file_put_contents($this->tempDir.'/second.txt', 'content'); $result = $this->strategy->detect([ @@ -58,7 +58,7 @@ expect($result)->toBeTrue(); }); -test('fails when no files exist', function () { +test('fails when no files exist', function (): void { $result = $this->strategy->detect([ 'files' => ['missing1.txt', 'missing2.txt'], 'basePath' => $this->tempDir, @@ -67,7 +67,7 @@ expect($result)->toBeFalse(); }); -test('returns false when no files config', function () { +test('returns false when no files config', function (): void { $result = $this->strategy->detect([ 'basePath' => $this->tempDir, ]); @@ -75,7 +75,7 @@ expect($result)->toBeFalse(); }); -test('uses current directory when no base path', function () { +test('uses current directory when no base path', function (): void { // This test creates a file in the current working directory $currentDir = getcwd(); $testFile = $currentDir.'/temp_test_file.txt'; @@ -92,7 +92,7 @@ } }); -test('detects files in subdirectories', function () { +test('detects files in subdirectories', function (): void { mkdir($this->tempDir.'/subdir'); file_put_contents($this->tempDir.'/subdir/nested.txt', 'content'); @@ -104,7 +104,7 @@ expect($result)->toBeTrue(); }); -test('handles empty files array', function () { +test('handles empty files array', function (): void { $result = $this->strategy->detect([ 'files' => [], 'basePath' => $this->tempDir, @@ -113,7 +113,7 @@ expect($result)->toBeFalse(); }); -test('detects files with special characters', function () { +test('detects files with special characters', function (): void { file_put_contents($this->tempDir.'/file-with_special.chars.txt', 'content'); $result = $this->strategy->detect([ @@ -135,5 +135,6 @@ function removeDirectoryForFileTests(string $dir): void $path = $dir.DIRECTORY_SEPARATOR.$file; is_dir($path) ? removeDirectoryForFileTests($path) : unlink($path); } + rmdir($dir); } diff --git a/tests/Unit/Install/GuidelineWriterTest.php b/tests/Unit/Install/GuidelineWriterTest.php index d84040d1..7bbb5d87 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); @@ -60,12 +60,12 @@ $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); }); -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."); @@ -77,12 +77,12 @@ $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); }); -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); @@ -95,12 +95,35 @@ $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 handles multiline existing guidelines', 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); + + $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); +}); + +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); @@ -114,12 +137,12 @@ $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); }); -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); @@ -133,12 +156,12 @@ $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); }); -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 +171,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); @@ -165,12 +188,12 @@ $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); }); -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, ''); @@ -182,12 +205,12 @@ $writer->write('first guidelines'); $content = file_get_contents($tempFile); - expect($content)->toBe("\nfirst guidelines\n"); + expect($content)->toBe("\nfirst guidelines\n\n"); 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"); @@ -199,12 +222,12 @@ $writer->write('clean guidelines'); $content = file_get_contents($tempFile); - expect($content)->toBe("\nclean guidelines\n"); + expect($content)->toBe("\nclean guidelines\n\n"); 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); @@ -218,12 +241,12 @@ 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); }); -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); @@ -248,16 +271,16 @@ ->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); }); -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."); @@ -269,12 +292,12 @@ $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); }); -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."); @@ -286,12 +309,12 @@ $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); }); -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."); @@ -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); }); diff --git a/tests/Unit/Install/HerdTest.php b/tests/Unit/Install/HerdTest.php index 822d6b53..c60f8e6e 100644 --- a/tests/Unit/Install/HerdTest.php +++ b/tests/Unit/Install/HerdTest.php @@ -6,7 +6,7 @@ $herdTestCleanupData = []; -beforeEach(function () { +beforeEach(function (): void { global $herdTestCleanupData; $herdTestCleanupData = initializeHerdTestEnvironment(); @@ -14,7 +14,7 @@ mkdir($herdTestCleanupData['tempDir'], 0755, true); }); -afterEach(function () { +afterEach(function (): void { global $herdTestCleanupData; foreach ($herdTestCleanupData['originalEnv'] as $key => $value) { @@ -67,7 +67,7 @@ function getHerdTestTempDir(): string return $herdTestCleanupData['tempDir']; } -test('mcpPath builds correct Windows path from USERPROFILE when HOME missing', function () { +test('mcpPath builds correct Windows path from USERPROFILE when HOME missing', function (): void { unset($_SERVER['HOME']); $_SERVER['USERPROFILE'] = 'C:\\Users\\TestUser'; @@ -77,7 +77,7 @@ function getHerdTestTempDir(): string expect($herd->mcpPath())->toBe($expected); })->onlyOnWindows(); -test('isMcpAvailable returns false when MCP file is missing from home', function () { +test('isMcpAvailable returns false when MCP file is missing from home', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -87,7 +87,7 @@ function getHerdTestTempDir(): string expect($herd->isMcpAvailable())->toBeFalse(); })->onlyOnWindows(); -test('isMcpAvailable returns true when MCP file exists in home', function () { +test('isMcpAvailable returns true when MCP file exists in home', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -103,7 +103,7 @@ function getHerdTestTempDir(): string expect($herd->isMcpAvailable())->toBeTrue(); })->onlyOnWindows(); -test('isMcpAvailable returns false after MCP file is removed', function () { +test('isMcpAvailable returns false after MCP file is removed', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -123,7 +123,7 @@ function getHerdTestTempDir(): string expect($herd->isMcpAvailable())->toBeFalse(); })->onlyOnWindows(); -test('getHomePath returns HOME on non-Windows', function () { +test('getHomePath returns HOME on non-Windows', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -133,7 +133,7 @@ function getHerdTestTempDir(): string expect($herd->getHomePath())->toBe($testHome); })->skipOnWindows(); -test('getHomePath uses USERPROFILE on Windows when HOME is not set and normalizes slashes', function () { +test('getHomePath uses USERPROFILE on Windows when HOME is not set and normalizes slashes', function (): void { unset($_SERVER['HOME']); $_SERVER['USERPROFILE'] = 'C:\\Users\\TestUser'; @@ -142,7 +142,7 @@ function getHerdTestTempDir(): string expect($herd->getHomePath())->toBe('C:/Users/TestUser'); })->onlyOnWindows(); -test('isInstalled returns true when herd config directory exists on Windows', function () { +test('isInstalled returns true when herd config directory exists on Windows', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -155,7 +155,7 @@ function getHerdTestTempDir(): string expect($herd->isInstalled())->toBeTrue(); })->onlyOnWindows(); -test('isInstalled returns false when herd config directory is missing on Windows', function () { +test('isInstalled returns false when herd config directory is missing on Windows', function (): void { $testHome = getHerdTestTempDir().'/home'; mkdir($testHome, 0755, true); $_SERVER['HOME'] = $testHome; @@ -165,13 +165,13 @@ function getHerdTestTempDir(): string expect($herd->isInstalled())->toBeFalse(); })->onlyOnWindows(); -test('isWindowsPlatform returns true on Windows', function () { +test('isWindowsPlatform returns true on Windows', function (): void { $herd = new Herd; expect($herd->isWindowsPlatform())->toBeTrue(); })->onlyOnWindows(); -test('isWindowsPlatform returns false on non-Windows platforms', function () { +test('isWindowsPlatform returns false on non-Windows platforms', function (): void { $herd = new Herd; expect($herd->isWindowsPlatform())->toBeFalse(); diff --git a/tests/Unit/Install/Mcp/FileWriterTest.php b/tests/Unit/Install/Mcp/FileWriterTest.php index d400c46d..065eb7d7 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 4d06a51b..d831a1f3 100644 --- a/tests/Unit/UnitTest.php +++ b/tests/Unit/UnitTest.php @@ -2,6 +2,6 @@ declare(strict_types=1); -it('is true', function () { +it('is true', function (): void { expect(true)->toBeTrue(); });