From 3078ee7d64810b88fdf994e4a016af1238279ae2 Mon Sep 17 00:00:00 2001 From: Mark Shust Date: Sun, 26 Jul 2026 11:19:10 -0400 Subject: [PATCH 1/5] fix(core): resolve all twelve PHPStan errors in packages/core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit None of these were failing tests, and no test could have caught them — PHP tolerates every one at runtime. Extra arguments to a userland method are silently discarded, initInterception() exists on the trait so the call resolves, and docblocks are invisible to the interpreter. Only static analysis sees them, and nothing ran it. Six came in through PR #15 (the PluginProxy → generated-interceptor rewrite) on 2026-04-05; the rest date to the January CLI batches and a February routing fix. They survived review because the PR checklist in pr-review-process.md never listed PHPStan. - PluginInterceptor passed a third argument to generateInterfaceWrapper() and generateConcreteSubclass(), which take two. Dead arguments, removed. - PluginInterceptedInterface declared only getPluginTarget(), yet PluginInterceptor calls initInterception() through that type. Declared it on the interface, where the contract belongs — the PluginInterception trait already satisfies it. - ModuleManifest::$singletons was annotated array while list-style entries use int keys, which is exactly what BindingRegistry::registerModule() branches on via is_int(). The code was right and the annotation lied, making PHPStan call a live branch impossible. Behaviour is unchanged and already covered by BindingRegistryTest. - Command and CommandDefinition gained list for $aliases; collectInterfaceMethods() gained its ReflectionClass generic; Input dropped an array_values() call on a value that is already a list. The one remaining error, trait.unused on PluginInterception, is a limitation rather than a defect: every runtime consumer is an eval-generated class, so no source-visible `use` exists. Ignored narrowly by identifier and path, with the reason recorded in phpstan.neon. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/src/Attributes/Command.php | 3 ++ .../core/src/Command/CommandDefinition.php | 3 ++ packages/core/src/Command/Input.php | 4 +- packages/core/src/Module/ModuleManifest.php | 4 +- .../src/Plugin/InterceptorClassGenerator.php | 2 + .../src/Plugin/PluginInterceptedInterface.php | 18 ++++++++ .../core/src/Plugin/PluginInterceptor.php | 3 -- .../Plugin/PluginInterceptedInterfaceTest.php | 46 +++++++++++++++++++ phpstan.neon | 9 ++++ 9 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 packages/core/tests/Unit/Plugin/PluginInterceptedInterfaceTest.php diff --git a/packages/core/src/Attributes/Command.php b/packages/core/src/Attributes/Command.php index f53dc04a..556b96f3 100644 --- a/packages/core/src/Attributes/Command.php +++ b/packages/core/src/Attributes/Command.php @@ -9,6 +9,9 @@ #[Attribute(Attribute::TARGET_CLASS)] readonly class Command { + /** + * @param list $aliases + */ public function __construct( public string $name, public string $description = '', diff --git a/packages/core/src/Command/CommandDefinition.php b/packages/core/src/Command/CommandDefinition.php index c7c4c371..8df7ef6d 100644 --- a/packages/core/src/Command/CommandDefinition.php +++ b/packages/core/src/Command/CommandDefinition.php @@ -9,6 +9,9 @@ */ readonly class CommandDefinition { + /** + * @param list $aliases + */ public function __construct( public string $commandClass, public string $name, diff --git a/packages/core/src/Command/Input.php b/packages/core/src/Command/Input.php index ebbcfbed..6949f138 100644 --- a/packages/core/src/Command/Input.php +++ b/packages/core/src/Command/Input.php @@ -27,11 +27,11 @@ public function getCommand(): ?string /** * Returns arguments after the command name (index 0 = script, index 1 = command). * - * @return array + * @return list */ public function getArguments(): array { - return array_values(array_slice($this->arguments, 2)); + return array_slice($this->arguments, 2); } /** diff --git a/packages/core/src/Module/ModuleManifest.php b/packages/core/src/Module/ModuleManifest.php index 0b90c7bc..cf3d0c94 100644 --- a/packages/core/src/Module/ModuleManifest.php +++ b/packages/core/src/Module/ModuleManifest.php @@ -22,7 +22,9 @@ * @param array $after Modules to load before this one (from module.php) * @param array $before Modules to load after this one (from module.php) * @param array $bindings Interface to implementation bindings (from module.php) - * @param array $singletons Shared interface to implementation bindings (from module.php) + * @param array $singletons Shared interface to implementation bindings (from module.php). + * String keys bind interface => implementation; int keys are list-style entries naming a class to + * autowire as a singleton, which BindingRegistry::registerModule() distinguishes via is_int(). * @param string $path Absolute path to module directory * @param string $source Discovery source: vendor, modules, or app * @param array $autoload PSR-4 autoload configuration from composer.json (namespace => path) diff --git a/packages/core/src/Plugin/InterceptorClassGenerator.php b/packages/core/src/Plugin/InterceptorClassGenerator.php index 1adcd2d6..e6f68d8e 100644 --- a/packages/core/src/Plugin/InterceptorClassGenerator.php +++ b/packages/core/src/Plugin/InterceptorClassGenerator.php @@ -181,6 +181,8 @@ private function extractClassName(string $code): string /** * Collect all methods from a reflected interface, including inherited ones. * + * @param ReflectionClass $reflection + * * @return array */ private function collectInterfaceMethods(ReflectionClass $reflection): array diff --git a/packages/core/src/Plugin/PluginInterceptedInterface.php b/packages/core/src/Plugin/PluginInterceptedInterface.php index 82328c72..f2ce236d 100644 --- a/packages/core/src/Plugin/PluginInterceptedInterface.php +++ b/packages/core/src/Plugin/PluginInterceptedInterface.php @@ -4,8 +4,26 @@ namespace Marko\Core\Plugin; +use Marko\Core\Container\ContainerInterface; + interface PluginInterceptedInterface { + /** + * Initialize the interception state. Interceptor classes are constructed without + * arguments (interface wrappers) or without a constructor at all (concrete + * subclasses), so PluginInterceptor calls this immediately after construction. + * + * Implemented by the PluginInterception trait, which every interceptor uses. + * + * @param class-string $pluginTargetClass + */ + public function initInterception( + object $pluginTarget, + string $pluginTargetClass, + ContainerInterface $pluginContainer, + PluginRegistry $pluginRegistry, + ): void; + /** * Get the underlying target instance that this interceptor wraps. */ diff --git a/packages/core/src/Plugin/PluginInterceptor.php b/packages/core/src/Plugin/PluginInterceptor.php index e907f505..aad7a197 100644 --- a/packages/core/src/Plugin/PluginInterceptor.php +++ b/packages/core/src/Plugin/PluginInterceptor.php @@ -59,7 +59,6 @@ public function createProxy( $className = $this->generator->generateInterfaceWrapper( $originalId, $this->registry, - $this->container, ); /** @var PluginInterceptedInterface&object $instance */ @@ -77,7 +76,6 @@ public function createProxy( $className = $this->generator->generateInterfaceWrapper( $effectiveTarget, $this->registry, - $this->container, ); /** @var PluginInterceptedInterface&object $instance */ @@ -91,7 +89,6 @@ public function createProxy( $className = $this->generator->generateConcreteSubclass( $resolvedId, $this->registry, - $this->container, ); /** @var PluginInterceptedInterface&object $instance */ diff --git a/packages/core/tests/Unit/Plugin/PluginInterceptedInterfaceTest.php b/packages/core/tests/Unit/Plugin/PluginInterceptedInterfaceTest.php new file mode 100644 index 00000000..2af78f64 --- /dev/null +++ b/packages/core/tests/Unit/Plugin/PluginInterceptedInterfaceTest.php @@ -0,0 +1,46 @@ +hasMethod('initInterception'))->toBeTrue( + 'PluginInterceptor calls initInterception() through this interface, so the interface must declare it', + ); +}); + +it('declares initInterception with the signature the trait implements', function (): void { + $interface = (new ReflectionClass(PluginInterceptedInterface::class))->getMethod('initInterception'); + $trait = (new ReflectionClass(PluginInterception::class))->getMethod('initInterception'); + + $typeName = function (ReflectionParameter $p): string { + $type = $p->getType(); + + return $type instanceof ReflectionNamedType ? $type->getName() : (string) $type; + }; + + expect(array_map($typeName, $interface->getParameters())) + ->toBe(array_map($typeName, $trait->getParameters())) + ->and(array_map($typeName, $interface->getParameters())) + ->toBe(['object', 'string', ContainerInterface::class, PluginRegistry::class]); +}); + +it('is satisfied by the trait every interceptor uses', function (): void { + $interceptor = new class () implements PluginInterceptedInterface + { + use PluginInterception; + }; + + expect($interceptor)->toBeInstanceOf(PluginInterceptedInterface::class); +}); diff --git a/phpstan.neon b/phpstan.neon index 1a3b4c82..48c9e560 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,4 +4,13 @@ parameters: - packages/core/src excludePaths: - packages/core/tests + ignoreErrors: + # Every runtime consumer of PluginInterception is an eval-generated interceptor class, + # so no source-visible `use` exists for PHPStan to find and it reports the trait as + # unused. The trait is genuinely used; this is a limitation of analysing generated code, + # not a defect. Tracked for removal in favour of a collaborator object — see the + # "No traits" note in the trait's own docblock. + - + identifier: trait.unused + path: packages/core/src/Plugin/PluginInterception.php tmpDir: .phpstan-cache From 78cf23f6157b3b320c2a8542864cce8040f428df Mon Sep 17 00:00:00 2001 From: Mark Shust Date: Sun, 26 Jul 2026 11:19:20 -0400 Subject: [PATCH 2/5] ci: gate every PR on tests, lint, and static analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There was no PR CI in this repo at all — the only workflows were auto-label, deploy-docs, readme-package-check, split, and sync-issue-templates. Nothing ran tests, phpcs, php-cs-fixer, or PHPStan on a pull request, so "develop is green" rested entirely on whoever last remembered to run them by hand. That is how core carried twelve PHPStan errors for months, and how php-cs-fixer drifted on 129 files. Add a CI workflow with three jobs — Tests, Lint, Static analysis — triggered on every pull request with no paths: filter, so no PR can route around the gate. Add composer phpstan and composer ci so the gate is one command locally, and add both to the PR review checklist that omitted PHPStan. The integration-destructive group is deliberately excluded from the gate: it deletes vendor/ and composer.lock, runs composer update, and re-runs the whole suite in a subprocess, so it measures install integrity rather than behaviour and is order-dependent under --parallel. It now runs nightly instead of never, and bin/release.sh still runs it before any tag is cut. phpcs.xml excludes the deliberately-unparseable codeindexer fixture. It cannot be valid PHP by construction — that is the test case — and phpcs aborts on the file. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/pr-review-process.md | 10 ++++- .github/workflows/ci.yml | 79 ++++++++++++++++++++++++++++++++++ .github/workflows/nightly.yml | 31 ++++++++++++++ CLAUDE.md | 8 ++++ composer.json | 7 +++ phpcs.xml | 7 +++ tests/CiWorkflowTest.php | 80 +++++++++++++++++++++++++++++++++++ 7 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/nightly.yml create mode 100644 tests/CiWorkflowTest.php diff --git a/.claude/pr-review-process.md b/.claude/pr-review-process.md index 0a51f38c..630410b4 100644 --- a/.claude/pr-review-process.md +++ b/.claude/pr-review-process.md @@ -40,15 +40,19 @@ If there are conflicts, resolve them. If the rebase is clean, proceed. ## 4. Run the test suite and lint ```bash +composer ci # everything the CI gate runs composer test # full suite (excludes integration-destructive) ./vendor/bin/pest packages//tests # package-specific tests ./vendor/bin/phpcs --standard=phpcs.xml packages// # phpcs ./vendor/bin/php-cs-fixer fix packages// --dry-run # cs-fixer +composer phpstan # static analysis ``` -All tests must pass and lint must be clean. Fix ALL lint errors in touched files before merging, including pre-existing ones. +All tests must pass, lint must be clean, and **PHPStan must report zero errors**. Fix ALL lint errors in touched files before merging, including pre-existing ones. -**No PR CI workflow exists in this repo.** `gh pr checks ` will report no checks. The merge-readiness signal is local lint + tests passing plus `gh pr view --json mergeStateStatus,mergeable` returning `CLEAN`/`MERGEABLE`. There is no async CI to wait for — do not write "will merge once CI is green" in PR comments. +**PHPStan is not optional and is not covered by `composer test`.** It went unrun for months precisely because this checklist omitted it, and `packages/core` accumulated 12 errors through normal PR review — type-level drift that PHP tolerates at runtime, so no test could have caught it. `composer ci` runs the whole gate in one command; prefer it. + +**The `CI` workflow gates every PR.** `gh pr checks ` reports `Tests`, `Lint`, and `Static analysis`. A red check blocks the merge — `develop` is never allowed to carry a failing test, lint error, or PHPStan error. The `integration-destructive` group is excluded from the gate (it deletes `vendor/`, runs `composer update`, and re-runs the suite in a subprocess); it runs on the `Nightly` schedule and again inside `bin/release.sh` before any tag is cut. ## 5. Review the code @@ -241,6 +245,8 @@ Before merging any package PR: - [ ] `composer test` passes - [ ] `./vendor/bin/phpcs` clean on touched files - [ ] `./vendor/bin/php-cs-fixer fix --dry-run` clean on touched files +- [ ] `composer phpstan` reports zero errors +- [ ] `gh pr checks ` green — `Tests`, `Lint`, `Static analysis` all passing - [ ] New code has corresponding tests - [ ] No hardcoded paths or environment-specific values - [ ] No `final` classes (including exceptions) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6c83a094 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: CI + +on: + pull_request: + push: + branches: + - develop + - main + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: none + ini-values: memory_limit=2G + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + # Excludes integration-destructive: that group deletes vendor/ and composer.lock, + # runs `composer update`, and re-runs the whole suite in a subprocess. It verifies + # install integrity rather than application behaviour, so it runs on the nightly + # schedule below and is enforced again by bin/release.sh before any tag is cut. + - name: Run test suite + run: composer test + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: none + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: PHP_CodeSniffer + run: vendor/bin/phpcs --standard=phpcs.xml + + - name: PHP CS Fixer + run: vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff + + static: + name: Static analysis + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: none + ini-values: memory_limit=2G + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: PHPStan + run: composer phpstan diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 00000000..5447b398 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,31 @@ +name: Nightly + +# The integration-destructive group deletes vendor/ and composer.lock, runs a full +# `composer update`, and re-runs the suite in a subprocess. That is too slow and too +# stateful for a per-PR gate, but it must not go unrun between releases — bin/release.sh +# is otherwise the first thing to discover a broken install, at the worst moment. +on: + schedule: + - cron: '0 9 * * *' + workflow_dispatch: + +jobs: + destructive: + name: Full suite including destructive integration + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: none + ini-values: memory_limit=2G + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Run full test suite + run: composer test:all diff --git a/CLAUDE.md b/CLAUDE.md index 74c93cf9..3aa5b7dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,8 +27,16 @@ composer test:all # Lint (fix) ./vendor/bin/php-cs-fixer fix + +# Static analysis — NOT part of composer test; must be zero errors +composer phpstan + +# Everything the CI gate runs (tests + lint + static analysis) +composer ci ``` +Every PR is gated by the `CI` workflow on `Tests`, `Lint`, and `Static analysis`. A red check blocks the merge; `develop` never carries a failing test, lint error, or PHPStan error. + ## Key Conventions - **Branch naming** - all branches use `feature/{name}` format, regardless of change type (fix, feature, refactor, docs, etc.). Never use `fix/`, `chore/`, or other prefixes diff --git a/composer.json b/composer.json index 51a4d574..6b558faf 100644 --- a/composer.json +++ b/composer.json @@ -492,6 +492,13 @@ "php-cs-fixer fix --config=.php-cs-fixer.php", "phpcbf --standard=phpcs.xml || true" ], + "phpstan": "php -d memory_limit=2G vendor/bin/phpstan analyse", + "ci": [ + "@test", + "@cs:check", + "php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff", + "@phpstan" + ], "rector": "rector process" }, "config": { diff --git a/phpcs.xml b/phpcs.xml index 26dc798a..d357ba23 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -8,6 +8,13 @@ */vendor/* */.phpunit.cache/* + + */tests/Fixtures/*/src/Broken/* + diff --git a/tests/CiWorkflowTest.php b/tests/CiWorkflowTest.php new file mode 100644 index 00000000..cdf57b38 --- /dev/null +++ b/tests/CiWorkflowTest.php @@ -0,0 +1,80 @@ +toBeTrue('.github/workflows/ci.yml must exist'); +}); + +it('gates every pull request, not just those touching certain paths', function () use ($ci): void { + // A paths: filter here would let a PR skip the gate entirely. + $trigger = substr($ci, 0, (int) strpos($ci, 'jobs:')); + + expect($trigger) + ->toContain('pull_request') + ->not->toContain('paths:'); +}); + +it('also runs on pushes to develop and main', function () use ($ci): void { + expect($ci) + ->toContain('branches:') + ->toContain('- develop') + ->toContain('- main'); +}); + +it('runs tests, lint, and static analysis as separate jobs', function () use ($ci): void { + expect($ci) + ->toContain('name: Tests') + ->toContain('name: Lint') + ->toContain('name: Static analysis') + ->toContain('composer test') + ->toContain('phpcs --standard=phpcs.xml') + ->toContain('php-cs-fixer fix --config=.php-cs-fixer.php --dry-run') + ->toContain('composer phpstan'); +}); + +it('pins PHP 8.5 in every job', function () use ($ci): void { + expect(substr_count($ci, "php-version: '8.5'"))->toBe(substr_count($ci, 'runs-on: ubuntu-latest')); +}); + +it('pins actions to a major version rather than a floating ref', function () use ($ci, $nightly): void { + preg_match_all('/uses: (\S+)/', $ci . $nightly, $matches); + + expect($matches[1])->not->toBeEmpty(); + + foreach ($matches[1] as $action) { + expect($action)->toMatch('/@v\d+$/'); + } +}); + +it('runs the destructive group on a schedule instead of on every PR', function () use ($ci, $nightly): void { + expect($nightly) + ->toContain('schedule:') + ->toContain('cron:') + ->toContain('composer test:all') + ->and($ci)->toContain('composer test') + ->and($ci)->not->toContain('test:all'); +}); + +it('exposes composer phpstan and composer ci scripts the workflow depends on', function (): void { + $composer = json_decode(file_get_contents(dirname(__DIR__) . '/composer.json'), true); + + expect($composer['scripts'])->toHaveKey('phpstan') + ->and($composer['scripts'])->toHaveKey('ci') + ->and($composer['scripts']['ci'])->toContain('@test') + ->and($composer['scripts']['ci'])->toContain('@phpstan'); +}); + +it('adds phpstan to the PR review checklist that previously omitted it', function (): void { + $process = file_get_contents(dirname(__DIR__) . '/.claude/pr-review-process.md'); + + expect($process) + ->toContain('composer phpstan') + ->toContain('PHPStan is not optional') + ->not->toContain('No PR CI workflow exists in this repo'); +}); From dd46d89bf3bbcc451a39646fd939811911731aee Mon Sep 17 00:00:00 2001 From: Mark Shust Date: Sun, 26 Jul 2026 11:19:30 -0400 Subject: [PATCH 3/5] style: converge php-cs-fixer and phpcbf across the repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical only — no behaviour, no logic, no test expectations changed. Produced entirely by `composer cs:fix`; every hunk is one of the two formatters' own output. php-cs-fixer was failing a dry run on 129 files on develop, and phpcbf on 40 across 15. Neither was ever enforced repo-wide: the review checklist asks for both, but only "on touched files", so drift accumulated everywhere nobody happened to edit. The CI gate added in the previous commit checks the whole repo, which cannot go green while that backlog exists. The two tools also disagreed on twelve files — phpcbf's multi-line reformatting produced output php-cs-fixer wanted to change back. `composer cs:fix` runs php-cs-fixer first and phpcbf second, so phpcbf wins, and running it once converges: a second pass reports zero for both. That ordering is now load-bearing for the Lint job, not just a convenience. Co-Authored-By: Claude Opus 5 (1M context) --- .../tests/ResolverIntegrationTest.php | 10 +- .../tests/TemplateMigrationTest.php | 2 +- .../tests/TemplateExistenceTest.php | 32 +- .../Controller/DashboardControllerTest.php | 2 +- .../tests/Unit/EngineSiblingCleanupTest.php | 2 +- .../Unit/Exceptions/NoDriverExceptionTest.php | 4 +- .../tests/KnownDriversValidationTest.php | 2 +- .../tests/Unit/Guard/SessionGuardTest.php | 18 +- .../tests/KnownDriversValidationTest.php | 12 +- .../tests/Unit/MarketplaceTest.php | 44 +- .../tests/Unit/MarkoLspPluginTest.php | 16 +- .../tests/Unit/MarkoSkillsPluginTest.php | 10 +- .../claude-plugins/tests/Unit/ReadmeTest.php | 8 +- .../Unit/Skills/CreateModuleSkillTest.php | 26 +- .../src/Attributes/AttributeParser.php | 18 +- .../codeindexer/src/Config/ConfigScanner.php | 6 +- .../codeindexer/src/Module/ModuleWalker.php | 3 +- .../src/Translations/TranslationScanner.php | 8 +- .../Unit/Attributes/AttributeParserTest.php | 54 +- .../tests/Unit/Cache/IndexCacheTest.php | 1470 +++++++++-------- .../tests/Unit/Config/ConfigScannerTest.php | 22 +- .../codeindexer/tests/Unit/SkeletonTest.php | 24 +- packages/core/src/Commands/ListCommand.php | 2 +- packages/core/src/Module/ModuleAutoloader.php | 6 +- .../PluginInterceptionIntegrationTest.php | 12 +- packages/core/tests/Unit/ApplicationTest.php | 2 +- .../Unit/Module/DependencyResolverTest.php | 131 +- .../Unit/Plugin/PluginInterceptorTest.php | 69 +- .../Query/MySqlQueryBuilderAggregatesTest.php | 6 +- .../Query/MySqlQueryBuilderGroupByTest.php | 16 +- .../database/src/Entity/EntityCollection.php | 3 +- .../src/Entity/EntityCompanionStorage.php | 3 +- .../database/src/Entity/EntityHydrator.php | 3 +- .../src/Entity/RelationshipLoader.php | 12 +- .../database/src/Schema/SchemaRegistry.php | 2 +- .../Attributes/RelationshipAttributeTest.php | 2 +- .../tests/Entity/EntityCollectionTest.php | 2 +- .../tests/Entity/EntityHydratorTest.php | 28 +- .../EntityMetadataFactoryRelationshipTest.php | 14 +- packages/database/tests/Entity/EntityTest.php | 14 +- .../tests/KnownDriversValidationTest.php | 2 +- .../tests/Schema/SchemaRegistryTest.php | 24 +- packages/devai/src/Commands/UpdateCommand.php | 3 +- .../Process/ConfirmationPrompterInterface.php | 5 +- packages/devai/tests/Helpers.php | 14 +- .../ClaudeCodeInstallMonorepoTest.php | 28 +- .../tests/Unit/Agents/ClaudeCodeAgentTest.php | 13 +- .../Installation/IntelephenseEnsurerTest.php | 3 +- .../Unit/Skills/SkillsDistributorTest.php | 76 +- .../Unit/Writing/GuidelinesWriterTest.php | 35 +- .../src/Exceptions/DevServerException.php | 3 +- .../docs-fts/src/Indexing/FtsIndexBuilder.php | 7 +- .../Unit/Indexing/FtsIndexBuilderTest.php | 10 +- packages/docs-fts/tests/Unit/SkeletonTest.php | 4 +- .../tests/Unit/AstroPipelineTest.php | 15 +- .../tests/KnownDriversValidationTest.php | 2 +- .../tests/Unit/UrlLinkificationTest.php | 6 +- .../tests/KnownDriversValidationTest.php | 2 +- .../tests/KnownDriversValidationTest.php | 2 +- .../framework/tests/ArchitectureDocTest.php | 8 +- .../tests/PackagistPublishingTest.php | 12 +- .../framework/tests/RootComposerJsonTest.php | 34 +- .../tests/Unit/FilesystemHealthCheckTest.php | 36 +- .../http/tests/KnownDriversValidationTest.php | 4 +- .../tests/KnownDriversValidationTest.php | 2 +- packages/layout/src/ComponentCollector.php | 6 +- packages/layout/src/ComponentDataResolver.php | 6 +- .../src/DiscoveringComponentCollector.php | 3 +- .../AmbiguousSortOrderException.php | 3 +- .../src/Exceptions/CircularSlotException.php | 3 +- .../DuplicateComponentException.php | 3 +- .../src/Exceptions/SlotNotFoundException.php | 3 +- packages/layout/src/HandleResolver.php | 6 +- packages/layout/src/LayoutProcessor.php | 3 +- packages/layout/src/LayoutResolver.php | 3 +- .../AmbiguousSortOrderExceptionTest.php | 4 +- .../Exceptions/CircularSlotExceptionTest.php | 4 +- .../ComponentNotFoundExceptionTest.php | 4 +- .../DuplicateComponentExceptionTest.php | 4 +- .../LayoutNotFoundExceptionTest.php | 4 +- .../Exceptions/SlotNotFoundExceptionTest.php | 4 +- .../tests/Unit/ComponentCollectorTest.php | 2 +- .../tests/Unit/LayoutProcessorNestedTest.php | 23 +- .../layout/tests/Unit/LayoutProcessorTest.php | 9 +- .../Unit/Middleware/LayoutMiddlewareTest.php | 3 +- .../lsp/src/Features/AttributeFeature.php | 16 +- .../lsp/src/Features/ConfigKeyFeature.php | 6 +- packages/lsp/src/Features/TemplateFeature.php | 5 +- .../lsp/src/Features/TranslationFeature.php | 6 +- .../lsp/src/Server/DiagnosticsNotifier.php | 6 +- packages/lsp/src/Server/DocumentStore.php | 6 +- packages/lsp/src/Server/LspServer.php | 6 +- .../Unit/Features/TemplateFeatureTest.php | 1 + .../lsp/tests/Unit/Server/LspServerTest.php | 38 +- .../Unit/Server/PublishDiagnosticsTest.php | 4 +- .../Unit/Exceptions/NoDriverExceptionTest.php | 2 +- packages/mcp/src/Commands/ServeCommand.php | 3 +- packages/mcp/src/Protocol/JsonRpcProtocol.php | 15 +- packages/mcp/src/Server/McpServer.php | 3 +- packages/mcp/src/Tools/CheckConfigKeyTool.php | 3 +- .../mcp/src/Tools/ResolveTemplateTool.php | 2 +- .../Adapters/MarkoConsoleDispatcher.php | 3 +- .../Runtime/Adapters/MarkoQueryConnection.php | 3 +- .../mcp/tests/Unit/Server/McpServerTest.php | 22 +- .../Unit/Tools/CheckConfigKeyToolTest.php | 8 +- .../Tools/FindPluginsTargetingToolTest.php | 6 +- .../Unit/Tools/GetConfigSchemaToolTest.php | 4 +- .../mcp/tests/Unit/Tools/ListToolsTest.php | 26 +- .../Unit/Tools/ResolveTemplateToolTest.php | 2 +- .../Tools/Runtime/ReadLogEntriesToolTest.php | 18 +- .../Runtime/RunConsoleCommandToolTest.php | 3 +- .../Tools/Runtime/ToolFailureModeTest.php | 8 +- .../tests/Unit/Tools/SearchDocsToolTest.php | 4 +- .../tests/KnownDriversValidationTest.php | 2 +- .../tests/KnownDriversValidationTest.php | 4 +- .../Unit/Exceptions/NoDriverExceptionTest.php | 4 +- .../src/Driver/PgSqlPublisher.php | 3 +- .../tests/Driver/PgSqlPublisherTest.php | 6 +- .../tests/Driver/PgSqlSubscriberTest.php | 9 +- .../tests/PgSqlPubSubConnectionTest.php | 6 +- .../src/Driver/RedisPublisher.php | 3 +- .../tests/Driver/RedisPublisherTest.php | 3 +- .../tests/RedisPubSubConnectionTest.php | 3 +- .../pubsub/src/Exceptions/PubSubException.php | 9 +- .../tests/KnownDriversValidationTest.php | 4 +- packages/pubsub/tests/MessageTest.php | 8 +- .../pubsub/tests/SubscriberInterfaceTest.php | 24 +- .../tests/KnownDriversValidationTest.php | 8 +- .../tests/Unit/RenameReferencesTest.php | 16 +- packages/session-file/tests/ModuleTest.php | 13 +- .../tests/KnownDriversValidationTest.php | 2 +- .../Unit/Middleware/SessionMiddlewareTest.php | 3 +- .../tests/KnownDriversSuggestParityTest.php | 4 +- .../skeleton/tests/PackageStructureTest.php | 17 +- .../AssertionFailedExceptionTest.php | 4 +- .../tests/Unit/TestCase/TestCaseTest.php | 44 +- .../Exceptions/NoDriverExceptionTest.php | 2 +- .../tests/KnownDriversValidationTest.php | 4 +- packages/view-twig/src/ModuleLoader.php | 3 +- .../Exceptions/NoDriverExceptionTest.php | 2 +- .../Feature/CrossEngineTemplateParityTest.php | 14 +- .../view/tests/KnownDriversValidationTest.php | 2 +- 142 files changed, 1539 insertions(+), 1486 deletions(-) diff --git a/packages/admin-panel-latte/tests/ResolverIntegrationTest.php b/packages/admin-panel-latte/tests/ResolverIntegrationTest.php index 1afb7d5a..fb87ad48 100644 --- a/packages/admin-panel-latte/tests/ResolverIntegrationTest.php +++ b/packages/admin-panel-latte/tests/ResolverIntegrationTest.php @@ -12,7 +12,7 @@ 'ModuleTemplateResolver resolves admin-panel::dashboard/index to the new admin-panel-latte path', function (): void { $adminPanelLatteDir = dirname(__DIR__); - + $modules = [ new ModuleManifest( name: 'marko/admin-panel-latte', @@ -22,14 +22,14 @@ function (): void { extra: ['marko' => ['templates_for' => 'marko/admin-panel']], ), ]; - + $resolver = new ModuleTemplateResolver( new ModuleRepository($modules), new ViewConfig(new FakeConfigRepository(['view.extension' => '.latte'])), ); - + $result = $resolver->resolve('admin-panel::dashboard/index'); - + expect($result)->toBe($adminPanelLatteDir . '/resources/views/dashboard/index.latte'); - } + }, ); diff --git a/packages/admin-panel-latte/tests/TemplateMigrationTest.php b/packages/admin-panel-latte/tests/TemplateMigrationTest.php index f8f8d715..e4655aa9 100644 --- a/packages/admin-panel-latte/tests/TemplateMigrationTest.php +++ b/packages/admin-panel-latte/tests/TemplateMigrationTest.php @@ -31,7 +31,7 @@ $oldTestPath = dirname(__DIR__, 2) . '/admin-panel/tests/Unit/Template/LayoutTemplateTest.php'; expect(file_exists($oldTestPath))->toBeFalse( - 'LayoutTemplateTest.php should not exist in admin-panel/tests/Unit/Template/' + 'LayoutTemplateTest.php should not exist in admin-panel/tests/Unit/Template/', ); }); diff --git a/packages/admin-panel-twig/tests/TemplateExistenceTest.php b/packages/admin-panel-twig/tests/TemplateExistenceTest.php index 4c841acc..8e8deddf 100644 --- a/packages/admin-panel-twig/tests/TemplateExistenceTest.php +++ b/packages/admin-panel-twig/tests/TemplateExistenceTest.php @@ -9,47 +9,47 @@ 'auth/login.twig exists and renders a form with email and password fields', function () use ($viewsDir): void { $path = $viewsDir . '/auth/login.twig'; - + expect(file_exists($path))->toBeTrue(); - + $contents = file_get_contents($path); - + expect($contents) ->toContain('and($contents)->toContain('name="email"') ->and($contents)->toContain('name="password"'); - } + }, ); test( 'layout/base.twig exists and contains HTML doctype, sidebar include, and content block', function () use ($viewsDir): void { $path = $viewsDir . '/layout/base.twig'; - + expect(file_exists($path))->toBeTrue(); - + $contents = file_get_contents($path); - + expect($contents) ->toContain('') ->and($contents)->toContain("{% include 'admin-panel::partials/sidebar'") ->and($contents)->toContain('{% block content %}'); - } + }, ); test( 'dashboard/index.twig exists and extends layout/base.twig with a content block', function () use ($viewsDir): void { $path = $viewsDir . '/dashboard/index.twig'; - + expect(file_exists($path))->toBeTrue(); - + $contents = file_get_contents($path); - + expect($contents) ->toContain("{% extends 'admin-panel::layout/base' %}") ->and($contents)->toContain('{% block content %}'); - } + }, ); test('partials/sidebar.twig exists and iterates menu items', function () use ($viewsDir): void { @@ -95,16 +95,16 @@ function () use ($viewsDir): void { function () use ($viewsDir): void { $layoutPath = $viewsDir . '/layout/base.twig'; $dashboardPath = $viewsDir . '/dashboard/index.twig'; - + expect(file_exists($layoutPath))->toBeTrue() ->and(file_exists($dashboardPath))->toBeTrue(); - + $layoutContents = file_get_contents($layoutPath); $dashboardContents = file_get_contents($dashboardPath); - + expect($layoutContents)->toContain('{% block content %}') ->and($dashboardContents)->toContain('{% block content %}') ->and($dashboardContents)->toContain('{% endblock %}'); - } + }, ); }); diff --git a/packages/admin-panel/tests/Unit/Controller/DashboardControllerTest.php b/packages/admin-panel/tests/Unit/Controller/DashboardControllerTest.php index 83b7ba1c..68b568e7 100644 --- a/packages/admin-panel/tests/Unit/Controller/DashboardControllerTest.php +++ b/packages/admin-panel/tests/Unit/Controller/DashboardControllerTest.php @@ -107,7 +107,7 @@ public function getMenuItems(): array it('requires authentication via AdminAuthMiddleware for dashboard', function (): void { $middlewareAttributes = (new ReflectionMethod(DashboardController::class, 'index'))->getAttributes( - Middleware::class + Middleware::class, ); expect($middlewareAttributes)->toHaveCount(1) diff --git a/packages/admin-panel/tests/Unit/EngineSiblingCleanupTest.php b/packages/admin-panel/tests/Unit/EngineSiblingCleanupTest.php index d24b6ad3..8ba04e1f 100644 --- a/packages/admin-panel/tests/Unit/EngineSiblingCleanupTest.php +++ b/packages/admin-panel/tests/Unit/EngineSiblingCleanupTest.php @@ -6,7 +6,7 @@ $viewsDir = dirname(__DIR__, 2) . '/resources/views'; expect(is_dir($viewsDir))->toBeFalse( - 'resources/views/ directory should not exist after engine sibling extraction' + 'resources/views/ directory should not exist after engine sibling extraction', ); }); diff --git a/packages/admin/tests/Unit/Exceptions/NoDriverExceptionTest.php b/packages/admin/tests/Unit/Exceptions/NoDriverExceptionTest.php index 932e77f5..237350cc 100644 --- a/packages/admin/tests/Unit/Exceptions/NoDriverExceptionTest.php +++ b/packages/admin/tests/Unit/Exceptions/NoDriverExceptionTest.php @@ -11,14 +11,14 @@ function (): void { $reflection = new ReflectionClass(NoDriverException::class); $constants = $reflection->getConstants(); - + expect($constants)->toHaveKey('DRIVER_PACKAGES') ->and($constants['DRIVER_PACKAGES'])->toBe([ 'marko/admin-api', 'marko/admin-auth', 'marko/admin-panel', ]); - } + }, ); it('provides suggestion with composer require commands for all driver packages', function (): void { diff --git a/packages/authentication/tests/KnownDriversValidationTest.php b/packages/authentication/tests/KnownDriversValidationTest.php index 9a837a6f..ace23791 100644 --- a/packages/authentication/tests/KnownDriversValidationTest.php +++ b/packages/authentication/tests/KnownDriversValidationTest.php @@ -11,7 +11,7 @@ 'skeleton suggest block contains all authentication drivers', function () use ($knownDriversPath, $skeletonComposerPath) { KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath); - } + }, ); test('every authentication driver follows marko slash prefix pattern', function () use ($knownDriversPath) { diff --git a/packages/authentication/tests/Unit/Guard/SessionGuardTest.php b/packages/authentication/tests/Unit/Guard/SessionGuardTest.php index 5870ccd1..271892bb 100644 --- a/packages/authentication/tests/Unit/Guard/SessionGuardTest.php +++ b/packages/authentication/tests/Unit/Guard/SessionGuardTest.php @@ -319,24 +319,21 @@ public function retrieveByCredentials(array $credentials): ?AuthenticatableInter public function validateCredentials( AuthenticatableInterface $user, array $credentials, - ): bool - { + ): bool { return false; } public function retrieveByRememberToken( int|string $identifier, string $token, - ): ?AuthenticatableInterface - { + ): ?AuthenticatableInterface { return $this->userByRememberToken; } public function updateRememberToken( AuthenticatableInterface $user, ?string $token, - ): void - { + ): void { $user->setRememberToken($token); } }; @@ -428,24 +425,21 @@ public function retrieveByCredentials(array $credentials): ?AuthenticatableInter public function validateCredentials( AuthenticatableInterface $user, array $credentials, - ): bool - { + ): bool { return false; } public function retrieveByRememberToken( int|string $identifier, string $token, - ): ?AuthenticatableInterface - { + ): ?AuthenticatableInterface { return $this->userByRememberToken; } public function updateRememberToken( AuthenticatableInterface $user, ?string $token, - ): void - { + ): void { $user->setRememberToken($token); } }; diff --git a/packages/cache/tests/KnownDriversValidationTest.php b/packages/cache/tests/KnownDriversValidationTest.php index dbe97400..e4cdb8da 100644 --- a/packages/cache/tests/KnownDriversValidationTest.php +++ b/packages/cache/tests/KnownDriversValidationTest.php @@ -12,7 +12,7 @@ 'skeleton suggest block contains all cache drivers', function () use ($knownDriversPath, $skeletonComposerPath): void { KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath); - } + }, ); test('every cache driver follows marko slash prefix pattern', function () use ($knownDriversPath): void { @@ -23,18 +23,18 @@ function () use ($knownDriversPath, $skeletonComposerPath): void { 'validation test skips skeleton parity assertion when skeleton is absent', function () use ($knownDriversPath): void { $nonExistentPath = __DIR__ . '/non-existent-path/composer.json'; - + expect(file_exists($nonExistentPath))->toBeFalse(); - + $skipped = false; - + try { KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $nonExistentPath); } catch (SkippedWithMessageException $e) { $skipped = true; expect($e->getMessage())->toContain('not found'); } - + expect($skipped)->toBeTrue(); - } + }, ); diff --git a/packages/claude-plugins/tests/Unit/MarketplaceTest.php b/packages/claude-plugins/tests/Unit/MarketplaceTest.php index db5374c7..c2d414b6 100644 --- a/packages/claude-plugins/tests/Unit/MarketplaceTest.php +++ b/packages/claude-plugins/tests/Unit/MarketplaceTest.php @@ -59,16 +59,16 @@ 'marketplace.json lives at the monorepo repo root at .claude-plugin/marketplace.json (NOT inside packages/claude-plugins/)', function (): void { $insidePackage = dirname(__DIR__, 2) . '/.claude-plugin/marketplace.json'; - + expect(file_exists($this->marketplacePath))->toBeTrue() ->and(file_exists($insidePackage))->toBeFalse(); - } + }, ); it('marketplace.json references the official schema URL via $schema', function (): void { expect($this->marketplace)->toHaveKey('$schema') ->and($this->marketplace['$schema'])->toBe( - 'https://json.schemastore.org/claude-code-plugin-marketplace.json' + 'https://json.schemastore.org/claude-code-plugin-marketplace.json', ); }); @@ -91,7 +91,7 @@ function (): void { ->and($plugin['author'])->toHaveKey('name') ->and($plugin)->toHaveKey('category'); } - } + }, ); it( @@ -100,35 +100,35 @@ function (): void { $plugins = $this->marketplace['plugins']; $names = array_column($plugins, 'name'); $sources = array_column($plugins, 'source'); - + expect(count($plugins))->toBe(3) ->and($names)->toContain('marko-skills') ->and($names)->toContain('marko-lsp') ->and($names)->toContain('marko-mcp'); - + // Sources must be explicit relative paths from marketplace root. - // Anthropic's marketplace schema rejects bare-name sources with metadata.pluginRoot. - foreach ($sources as $source) { + // Anthropic's marketplace schema rejects bare-name sources with metadata.pluginRoot. + foreach ($sources as $source) { expect($source)->toStartWith('./packages/claude-plugins/plugins/'); } - } + }, ); it( 'marketplace.json plugin sources point at the actual plugin directories under packages/claude-plugins/plugins/', function (): void { $plugins = $this->marketplace['plugins']; - + $expected = [ 'marko-skills' => './packages/claude-plugins/plugins/marko-skills', 'marko-lsp' => './packages/claude-plugins/plugins/marko-lsp', 'marko-mcp' => './packages/claude-plugins/plugins/marko-mcp', ]; - + foreach ($plugins as $plugin) { expect($plugin['source'])->toBe($expected[$plugin['name']]); } - } + }, ); it( @@ -138,21 +138,21 @@ function (): void { ->and($this->marketplace['owner'])->toHaveKey('name') ->and($this->marketplace['owner']['name'])->toBeString() ->and(strlen($this->marketplace['owner']['name']))->toBeGreaterThan(0); - } + }, ); it( 'marketplace.json conforms to the schema captured in Task 001\'s schemas/marketplace.json artifact', function (): void { // Required fields per Task 001 finding F7: name (string), owner (object with name), plugins (array) - expect($this->marketplace)->toBeArray() - ->and($this->marketplace)->toHaveKey('name') - ->and($this->marketplace['name'])->toBeString() - ->and($this->marketplace)->toHaveKey('owner') - ->and($this->marketplace['owner'])->toBeArray() - ->and($this->marketplace['owner'])->toHaveKey('name') - ->and($this->marketplace)->toHaveKey('plugins') - ->and($this->marketplace['plugins'])->toBeArray(); - } + expect($this->marketplace)->toBeArray() + ->and($this->marketplace)->toHaveKey('name') + ->and($this->marketplace['name'])->toBeString() + ->and($this->marketplace)->toHaveKey('owner') + ->and($this->marketplace['owner'])->toBeArray() + ->and($this->marketplace['owner'])->toHaveKey('name') + ->and($this->marketplace)->toHaveKey('plugins') + ->and($this->marketplace['plugins'])->toBeArray(); + }, ); }); diff --git a/packages/claude-plugins/tests/Unit/MarkoLspPluginTest.php b/packages/claude-plugins/tests/Unit/MarkoLspPluginTest.php index aeb4caf4..827f5b9b 100644 --- a/packages/claude-plugins/tests/Unit/MarkoLspPluginTest.php +++ b/packages/claude-plugins/tests/Unit/MarkoLspPluginTest.php @@ -15,11 +15,11 @@ 'plugin.json declares name "marko-lsp" and a description matching the marketplace.json entry', function (): void { $manifest = json_decode(file_get_contents($this->pluginJsonPath), true); - + expect(file_exists($this->pluginJsonPath))->toBeTrue() ->and($manifest['name'])->toBe('marko-lsp') ->and($manifest['description'])->toBe('Marko LSP bundle (PHP intelephense) for Claude Code.'); - } + }, ); it('plugin.json includes author with name "Marko Framework"', function (): void { @@ -39,11 +39,11 @@ function (): void { '.lsp.json top-level structure is an object keyed by server name (not wrapped in lspServers per Task 001 F1 verbatim)', function (): void { $lsp = json_decode(file_get_contents($this->lspJsonPath), true); - + expect(file_exists($this->lspJsonPath))->toBeTrue() ->and(array_key_exists('marko-lsp', $lsp))->toBeTrue() ->and(array_key_exists('lspServers', $lsp))->toBeFalse(); - } + }, ); it('.lsp.json marko-lsp.command is "${CLAUDE_PLUGIN_ROOT}/bin/marko-lsp"', function (): void { @@ -64,13 +64,13 @@ function (): void { function (): void { $lsp = json_decode(file_get_contents($this->lspJsonPath), true); $ext = $lsp['marko-lsp']['extensionToLanguage']; - + expect($ext)->toBeArray() ->and($ext)->toHaveCount(1) ->and(array_key_exists('.php', $ext))->toBeTrue() ->and($ext['.php'])->toBe('php') ->and(array_key_exists('.latte', $ext))->toBeFalse(); - } + }, ); it('.lsp.json each extensionToLanguage key starts with a leading dot', function (): void { @@ -115,12 +115,12 @@ function (): void { 'README.md explains marko-lsp coexists with php-lsp (intelephense), what marko-lsp adds beyond it, the recommendation to uninstall php-lsp@claude-plugins-official to avoid duplication, and how to verify with claude plugin list', function (): void { $contents = file_exists($this->readmePath) ? file_get_contents($this->readmePath) : ''; - + expect(file_exists($this->readmePath))->toBeTrue() ->and(str_contains($contents, 'intelephense'))->toBeTrue() ->and(str_contains($contents, 'php-lsp@claude-plugins-official'))->toBeTrue() ->and(str_contains($contents, 'uninstall'))->toBeTrue() ->and(str_contains($contents, 'claude plugin list'))->toBeTrue(); - } + }, ); }); diff --git a/packages/claude-plugins/tests/Unit/MarkoSkillsPluginTest.php b/packages/claude-plugins/tests/Unit/MarkoSkillsPluginTest.php index 00de50a0..82d2edac 100644 --- a/packages/claude-plugins/tests/Unit/MarkoSkillsPluginTest.php +++ b/packages/claude-plugins/tests/Unit/MarkoSkillsPluginTest.php @@ -19,12 +19,12 @@ 'plugin.json description states the plugin provides scaffolding skills for Marko modules and plugins', function (): void { $manifest = json_decode(file_get_contents($this->pluginJsonPath), true); - + expect($manifest)->toHaveKey('description') ->and($manifest['description'])->toBe( - 'Marko-specific skills (create-module, create-plugin) for Claude Code.' + 'Marko-specific skills (create-module, create-plugin) for Claude Code.', ); - } + }, ); it('plugin.json includes author with name "Marko Framework"', function (): void { @@ -51,13 +51,13 @@ function (): void { function (): void { $readmePath = $this->pluginRoot . '/README.md'; $content = file_exists($readmePath) ? file_get_contents($readmePath) : ''; - + expect(file_exists($readmePath))->toBeTrue() ->and($content)->toContain('create-module') ->and($content)->toContain('create-plugin') ->and($content)->toContain('/marko-skills:create-module') ->and($content)->toContain('/marko-skills:create-plugin') ->and($content)->toContain('/plugin install marko-skills@marko'); - } + }, ); }); diff --git a/packages/claude-plugins/tests/Unit/ReadmeTest.php b/packages/claude-plugins/tests/Unit/ReadmeTest.php index cdb18378..9798241e 100644 --- a/packages/claude-plugins/tests/Unit/ReadmeTest.php +++ b/packages/claude-plugins/tests/Unit/ReadmeTest.php @@ -28,7 +28,7 @@ function (): void { ->and($this->contents)->toContain('marko-skills') ->and($this->contents)->toContain('marko-lsp') ->and($this->contents)->toContain('marko-mcp'); - } + }, ); it('README.md includes the install command via marko devai:install', function (): void { @@ -45,12 +45,12 @@ function (): void { 'README.md follows the slim-pointer convention from docs/DOCS-STANDARDS.md (no exhaustive feature lists, no inline code samples beyond a quick install example)', function (): void { expect($this->contents)->not->toBeNull(); - + // Slim-pointer: file should be short (under 80 lines) - $lines = substr_count($this->contents, "\n") + 1; + $lines = substr_count($this->contents, "\n") + 1; expect($lines)->toBeLessThan(80) // Must have a Documentation section ->and($this->contents)->toContain('## Documentation'); - } + }, ); }); diff --git a/packages/claude-plugins/tests/Unit/Skills/CreateModuleSkillTest.php b/packages/claude-plugins/tests/Unit/Skills/CreateModuleSkillTest.php index 6838841b..3935b255 100644 --- a/packages/claude-plugins/tests/Unit/Skills/CreateModuleSkillTest.php +++ b/packages/claude-plugins/tests/Unit/Skills/CreateModuleSkillTest.php @@ -13,12 +13,12 @@ 'SKILL.md frontmatter has name "create-module" and a non-empty description with concrete trigger examples', function (): void { $content = file_exists($this->skillMd) ? file_get_contents($this->skillMd) : ''; - + // Parse YAML frontmatter between the two --- delimiters - preg_match('/^---\n(.*?)\n---/s', $content, $matches); - + preg_match('/^---\n(.*?)\n---/s', $content, $matches); + $frontmatter = $matches[1] ?? ''; - + expect(file_exists($this->skillMd))->toBeTrue() ->and($matches)->not->toBeEmpty() ->and($frontmatter)->toContain('name: create-module') @@ -26,7 +26,7 @@ function (): void { ->and($frontmatter)->toContain('description:') // Must contain concrete trigger examples (in the body — YAML block scalar or inline) ->and($content)->toContain('create a module named'); - } + }, ); it('SKILL.md is under 500 lines', function (): void { @@ -53,9 +53,9 @@ function (): void { 'SKILL.md instructs the agent to copy templates from assets/ rather than inlining file content', function (): void { $content = file_get_contents($this->skillMd); - + expect($content)->toContain('assets/'); - } + }, ); it( @@ -63,15 +63,15 @@ function (): void { function (): void { $tmplPath = $this->assetsDir . '/composer.json.tmpl'; $content = file_exists($tmplPath) ? file_get_contents($tmplPath) : ''; - + // Substitute placeholders with literal strings and validate JSON - $substituted = str_replace( + $substituted = str_replace( ['{{vendor}}', '{{name}}', '{{Vendor}}', '{{Name}}'], ['acme', 'payment', 'Acme', 'Payment'], $content, ); $decoded = json_decode($substituted, true); - + expect(file_exists($tmplPath))->toBeTrue() ->and($content)->toContain('{{vendor}}') ->and($content)->toContain('{{name}}') @@ -82,7 +82,7 @@ function (): void { ->and($decoded)->toHaveKey('autoload') ->and($decoded)->toHaveKey('extra') ->and($decoded['extra']['marko']['module'])->toBeTrue(); - } + }, ); it('composer.json.monorepo.tmpl uses self.version constraints for marko/* requirements', function (): void { @@ -142,8 +142,8 @@ function (): void { 'the original SKILL.md at packages/devai/resources/ai/skills/marko-create-module/ is deleted', function (): void { $originalPath = dirname(__DIR__, 4) . '/devai/resources/ai/skills/marko-create-module/SKILL.md'; - + expect(file_exists($originalPath))->toBeFalse(); - } + }, ); }); diff --git a/packages/codeindexer/src/Attributes/AttributeParser.php b/packages/codeindexer/src/Attributes/AttributeParser.php index cc790447..68ea658a 100644 --- a/packages/codeindexer/src/Attributes/AttributeParser.php +++ b/packages/codeindexer/src/Attributes/AttributeParser.php @@ -342,8 +342,7 @@ private function findClasses(array $ast): array private function walkNodes( array $nodes, callable $callback, - ): void - { + ): void { foreach ($nodes as $node) { if (!$node instanceof Node) { continue; @@ -372,8 +371,7 @@ private function resolveClassName(Class_ $class): string private function classIsInNamespace( Class_ $class, ModuleInfo $module, - ): bool - { + ): bool { $className = $this->resolveClassName($class); $ns = rtrim($module->namespace, '\\') . '\\'; @@ -409,8 +407,7 @@ private function methodAttributes(Node\Stmt\ClassMethod $method): array private function attrNameMatches( Attribute $attr, string $fqn, - ): bool - { + ): bool { $name = $attr->name; if ($name instanceof Node\Name\FullyQualified || $name instanceof Node\Name) { return ltrim($name->toString(), '\\') === ltrim($fqn, '\\'); @@ -426,8 +423,7 @@ private function getArgValueAsString( array $args, string $namedKey, int $positionalIndex, - ): ?string - { + ): ?string { $value = $this->getArgValue($args, $namedKey, $positionalIndex); if ($value === null) { return null; @@ -454,8 +450,7 @@ private function getArgValueAsInt( array $args, string $namedKey, int $positionalIndex, - ): ?int - { + ): ?int { $value = $this->getArgValue($args, $namedKey, $positionalIndex); if ($value === null) { return null; @@ -475,8 +470,7 @@ private function getArgValue( array $args, string $namedKey, int $positionalIndex, - ): ?Node\Expr - { + ): ?Node\Expr { // Try named arg first foreach ($args as $arg) { if ($arg->name !== null && $arg->name->toString() === $namedKey) { diff --git a/packages/codeindexer/src/Config/ConfigScanner.php b/packages/codeindexer/src/Config/ConfigScanner.php index 9f39707c..e115cb62 100644 --- a/packages/codeindexer/src/Config/ConfigScanner.php +++ b/packages/codeindexer/src/Config/ConfigScanner.php @@ -56,8 +56,7 @@ public function diagnostics(): array private function scanFile( string $file, string $moduleName, - ): array - { + ): array { $code = file_get_contents($file); if ($code === false) { return []; @@ -105,8 +104,7 @@ private function flattenArray( string $prefix, string $file, string $moduleName, - ): array - { + ): array { $entries = []; foreach ($array->items as $item) { diff --git a/packages/codeindexer/src/Module/ModuleWalker.php b/packages/codeindexer/src/Module/ModuleWalker.php index 5662f4a8..1d1dd644 100644 --- a/packages/codeindexer/src/Module/ModuleWalker.php +++ b/packages/codeindexer/src/Module/ModuleWalker.php @@ -4,6 +4,7 @@ namespace Marko\CodeIndexer\Module; +use Closure; use FilesystemIterator; use Marko\CodeIndexer\ValueObject\ModuleInfo; use Marko\Core\Path\ProjectPaths; @@ -163,7 +164,7 @@ private function loadManifest(string $path): array private static function stripClosures(array $value): array { foreach ($value as $k => $v) { - if ($v instanceof \Closure) { + if ($v instanceof Closure) { $value[$k] = ''; } elseif (is_array($v)) { $value[$k] = self::stripClosures($v); diff --git a/packages/codeindexer/src/Translations/TranslationScanner.php b/packages/codeindexer/src/Translations/TranslationScanner.php index afa03796..8687952f 100644 --- a/packages/codeindexer/src/Translations/TranslationScanner.php +++ b/packages/codeindexer/src/Translations/TranslationScanner.php @@ -49,8 +49,7 @@ private function scanFile( string $group, string $locale, string $moduleName, - ): array - { + ): array { $code = file_get_contents($file); if ($code === false) { return []; @@ -89,8 +88,7 @@ private function flattenArray( string $locale, string $moduleName, string $file, - ): array - { + ): array { $entries = []; foreach ($array->items as $item) { @@ -113,7 +111,7 @@ private function flattenArray( $group, $locale, $moduleName, - $file + $file, )]; } else { $entries[] = new TranslationEntry( diff --git a/packages/codeindexer/tests/Unit/Attributes/AttributeParserTest.php b/packages/codeindexer/tests/Unit/Attributes/AttributeParserTest.php index f3dd31bd..f6c11a70 100644 --- a/packages/codeindexer/tests/Unit/Attributes/AttributeParserTest.php +++ b/packages/codeindexer/tests/Unit/Attributes/AttributeParserTest.php @@ -70,9 +70,9 @@ class Broken { public function oops(: void {} } 'does not require target classes to be autoloadable (pure AST traversal, no class loading)', function () use ($fixtureBase): void { // Create a temporary module pointing to a file that references non-autoloadable classes - $tmpDir = sys_get_temp_dir() . '/marko-ast-test-' . uniqid(); + $tmpDir = sys_get_temp_dir() . '/marko-ast-test-' . uniqid(); mkdir($tmpDir . '/src', 0777, true); - + file_put_contents($tmpDir . '/src/NonAutoloadableObserver.php', <<<'PHP' $parser->observers($module))->not->toThrow(Throwable::class); - } + // but crucially, no exception is thrown even though the classes don't exist. + expect(fn () => $parser->observers($module))->not->toThrow(Throwable::class); + }, ); it( 'resolves short attribute names via file use statements to fully qualified class names', function () use ($fixtureBase, $fixtureModule): void { // The fixture files use short names like `Observer`, `Plugin`, etc. via `use` statements. - // The NameResolver must expand them to FQNs for matching to work. - $parser = new AttributeParser(); - + // The NameResolver must expand them to FQNs for matching to work. + $parser = new AttributeParser(); + // If NameResolver was NOT used, none of these would return results - // because the attribute names would remain unresolved short names. - expect($parser->observers($fixtureModule))->not->toBeEmpty() - ->and($parser->plugins($fixtureModule))->not->toBeEmpty() - ->and($parser->preferences($fixtureModule))->not->toBeEmpty() - ->and($parser->commands($fixtureModule))->not->toBeEmpty() - ->and($parser->routes($fixtureModule))->not->toBeEmpty(); - } + // because the attribute names would remain unresolved short names. + expect($parser->observers($fixtureModule))->not->toBeEmpty() + ->and($parser->plugins($fixtureModule))->not->toBeEmpty() + ->and($parser->preferences($fixtureModule))->not->toBeEmpty() + ->and($parser->commands($fixtureModule))->not->toBeEmpty() + ->and($parser->routes($fixtureModule))->not->toBeEmpty(); + }, ); it('returns empty entries for modules with no attributes', function (): void { @@ -163,16 +163,16 @@ function () use ($fixtureBase, $fixtureModule): void { function () use ($fixtureModule): void { $parser = new AttributeParser(); $routes = $parser->routes($fixtureModule); - + $active = array_values(array_filter($routes, fn ($e) => $e->method !== 'DISABLED')); - + expect($active)->toHaveCount(6); - + $byAction = []; foreach ($active as $entry) { $byAction[$entry->action] = $entry; } - + expect($byAction['index']->method)->toBe('GET') ->and($byAction['index']->path)->toBe('/posts') ->and($byAction['show']->method)->toBe('GET') @@ -181,7 +181,7 @@ function () use ($fixtureModule): void { ->and($byAction['update']->method)->toBe('PUT') ->and($byAction['patch']->method)->toBe('PATCH') ->and($byAction['destroy']->method)->toBe('DELETE'); - } + }, ); it('parses Command attributes from class-level declarations', function () use ($fixtureModule): void { @@ -201,14 +201,14 @@ function () use ($fixtureModule): void { function () use ($fixtureModule): void { $parser = new AttributeParser(); $preferences = $parser->preferences($fixtureModule); - + expect($preferences)->toHaveCount(1); - + $entry = $preferences[0]; expect($entry->implementation)->toEndWith('CustomLoggerPreference') ->and($entry->interface)->toBe('Fixture\AttributeFixtures\Contracts\LoggerInterface') ->and($entry->module)->toBe('fixture/attributefixtures'); - } + }, ); it('parses Plugin attributes and associates methods to target class', function () use ($fixtureModule): void { diff --git a/packages/codeindexer/tests/Unit/Cache/IndexCacheTest.php b/packages/codeindexer/tests/Unit/Cache/IndexCacheTest.php index 53c27738..3f196656 100644 --- a/packages/codeindexer/tests/Unit/Cache/IndexCacheTest.php +++ b/packages/codeindexer/tests/Unit/Cache/IndexCacheTest.php @@ -1184,370 +1184,410 @@ public function scan(ModuleInfo $m): array // ── staleness-recheck-on-read tests ────────────────────────────────────────── -it('it reflects a newly added app module on the next read after the data was already loaded', function () use (&$tmpDirs): void { - $rootPath = makeTempDir(); - $tmpDirs[] = $rootPath; - - // Initial app module on disk - $appDir = $rootPath . '/app'; - $initialModulePath = $appDir . '/initial-module'; - mkdir($initialModulePath . '/src', 0755, true); - file_put_contents($initialModulePath . '/src/Initial.php', ' $modules */ - public function __construct(private array &$modules) {} + $initialModule = new ModuleInfo('test/initial', $initialModulePath, 'Test\\Initial\\'); - public function walk(): array + // Spy walker: starts returning 1 module, will return 2 after we update $modules + $modules = [$initialModule]; + $walker = new class ($modules) extends ModuleWalker { - return $this->modules; - } - }; + /** @param list $modules */ + public function __construct(private array &$modules) {} - $attributeParser = new class () extends AttributeParser - { - public function observers(ModuleInfo $m): array - { - return []; - } + public function walk(): array + { + return $this->modules; + } + }; - public function plugins(ModuleInfo $m): array + $attributeParser = new class () extends AttributeParser { - return []; - } + public function observers(ModuleInfo $m): array + { + return []; + } - public function preferences(ModuleInfo $m): array - { - return []; - } + public function plugins(ModuleInfo $m): array + { + return []; + } - public function commands(ModuleInfo $m): array - { - return []; - } + public function preferences(ModuleInfo $m): array + { + return []; + } - public function routes(ModuleInfo $m): array - { - return []; - } - }; - $configScanner = new class () extends ConfigScanner - { - public function scan(ModuleInfo $m): array + public function commands(ModuleInfo $m): array + { + return []; + } + + public function routes(ModuleInfo $m): array + { + return []; + } + }; + $configScanner = new class () extends ConfigScanner { - return []; - } - }; - $templateScanner = new class () extends TemplateScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $templateScanner = new class () extends TemplateScanner { - return []; - } - }; - $translationScanner = new class () extends TranslationScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $translationScanner = new class () extends TranslationScanner { - return []; - } - }; - - $cache = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); + public function scan(ModuleInfo $m): array + { + return []; + } + }; - // First read populates $this->data (singleton scenario) - $first = $cache->getModules(); - expect($first)->toHaveCount(1); + $cache = makeIndexCache( + $rootPath, + $walker, + $attributeParser, + $configScanner, + $templateScanner, + $translationScanner, + ); - // Simulate a new app module appearing on disk AND being tracked by the walker - $newModulePath = $appDir . '/new-module'; - mkdir($newModulePath . '/src', 0755, true); - file_put_contents($newModulePath . '/src/New.php', 'data (singleton scenario) + $first = $cache->getModules(); + expect($first)->toHaveCount(1); - // Same instance: second read must detect the addition and rebuild - $second = $cache->getModules(); - expect($second)->toHaveCount(2); -}); + // Simulate a new app module appearing on disk AND being tracked by the walker + $newModulePath = $appDir . '/new-module'; + mkdir($newModulePath . '/src', 0755, true); + file_put_contents($newModulePath . '/src/New.php', 'getModules(); + expect($second)->toHaveCount(2); + }, +); - $appDir = $rootPath . '/app'; - $modulePath = $appDir . '/route-module'; - mkdir($modulePath . '/src', 0755, true); - file_put_contents($modulePath . '/src/Ctrl.php', ' $modules */ - public function __construct(private array &$modules) {} + $module = new ModuleInfo('test/route-module', $modulePath, 'Test\\Route\\'); + $modules = [$module]; - public function walk(): array + // Route list controlled externally + $routes = []; + $walker = new class ($modules) extends ModuleWalker { - return $this->modules; - } - }; - $attributeParser = new class ($routes) extends AttributeParser - { - /** @param list $routes */ - public function __construct(private array &$routes) {} + /** @param list $modules */ + public function __construct(private array &$modules) {} - public function observers(ModuleInfo $m): array + public function walk(): array + { + return $this->modules; + } + }; + $attributeParser = new class ($routes) extends AttributeParser { - return []; - } + /** @param list $routes */ + public function __construct(private array &$routes) {} - public function plugins(ModuleInfo $m): array - { - return []; - } + public function observers(ModuleInfo $m): array + { + return []; + } - public function preferences(ModuleInfo $m): array - { - return []; - } + public function plugins(ModuleInfo $m): array + { + return []; + } - public function commands(ModuleInfo $m): array - { - return []; - } + public function preferences(ModuleInfo $m): array + { + return []; + } - public function routes(ModuleInfo $m): array - { - return $this->routes; - } - }; - $configScanner = new class () extends ConfigScanner - { - public function scan(ModuleInfo $m): array + public function commands(ModuleInfo $m): array + { + return []; + } + + public function routes(ModuleInfo $m): array + { + return $this->routes; + } + }; + $configScanner = new class () extends ConfigScanner { - return []; - } - }; - $templateScanner = new class () extends TemplateScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $templateScanner = new class () extends TemplateScanner { - return []; - } - }; - $translationScanner = new class () extends TranslationScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $translationScanner = new class () extends TranslationScanner { - return []; - } - }; - - $cache = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); - - // First read: no routes - $first = $cache->getRoutes(); - expect($first)->toBeEmpty(); + public function scan(ModuleInfo $m): array + { + return []; + } + }; - // Simulate a new file change on disk - file_put_contents($modulePath . '/src/NewRoute.php', 'getRoutes(); - expect($second)->toHaveCount(1); -}); + // First read: no routes + $first = $cache->getRoutes(); + expect($first)->toBeEmpty(); -it('it reflects an edit to a tracked app or modules source file on the next read after first load', function () use (&$tmpDirs): void { - $rootPath = makeTempDir(); - $tmpDirs[] = $rootPath; + // Simulate a new file change on disk + file_put_contents($modulePath . '/src/NewRoute.php', 'getRoutes(); + expect($second)->toHaveCount(1); + }, +); - $module = new ModuleInfo('test/edit-module', $modulePath, 'Test\\Edit\\'); - $modules = [$module]; - $routeList = []; +it( + 'it reflects an edit to a tracked app or modules source file on the next read after first load', + function () use (&$tmpDirs): void { + $rootPath = makeTempDir(); + $tmpDirs[] = $rootPath; - $walker = new class ($modules) extends ModuleWalker - { - /** @param list $modules */ - public function __construct(private array &$modules) {} + $appDir = $rootPath . '/app'; + $modulePath = $appDir . '/edit-module'; + mkdir($modulePath . '/src', 0755, true); + $srcFile = $modulePath . '/src/Service.php'; + file_put_contents($srcFile, 'modules; - } - }; - $attributeParser = new class ($routeList) extends AttributeParser - { - /** @param list $routes */ - public function __construct(private array &$routes) {} + $module = new ModuleInfo('test/edit-module', $modulePath, 'Test\\Edit\\'); + $modules = [$module]; + $routeList = []; - public function observers(ModuleInfo $m): array + $walker = new class ($modules) extends ModuleWalker { - return []; - } + /** @param list $modules */ + public function __construct(private array &$modules) {} - public function plugins(ModuleInfo $m): array + public function walk(): array + { + return $this->modules; + } + }; + $attributeParser = new class ($routeList) extends AttributeParser { - return []; - } + /** @param list $routes */ + public function __construct(private array &$routes) {} - public function preferences(ModuleInfo $m): array - { - return []; - } + public function observers(ModuleInfo $m): array + { + return []; + } - public function commands(ModuleInfo $m): array - { - return []; - } + public function plugins(ModuleInfo $m): array + { + return []; + } - public function routes(ModuleInfo $m): array - { - return $this->routes; - } - }; - $configScanner = new class () extends ConfigScanner - { - public function scan(ModuleInfo $m): array + public function preferences(ModuleInfo $m): array + { + return []; + } + + public function commands(ModuleInfo $m): array + { + return []; + } + + public function routes(ModuleInfo $m): array + { + return $this->routes; + } + }; + $configScanner = new class () extends ConfigScanner { - return []; - } - }; - $templateScanner = new class () extends TemplateScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $templateScanner = new class () extends TemplateScanner { - return []; - } - }; - $translationScanner = new class () extends TranslationScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $translationScanner = new class () extends TranslationScanner { - return []; - } - }; - - $cache = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); + public function scan(ModuleInfo $m): array + { + return []; + } + }; - // First read - $first = $cache->getRoutes(); - expect($first)->toBeEmpty(); + $cache = makeIndexCache( + $rootPath, + $walker, + $attributeParser, + $configScanner, + $templateScanner, + $translationScanner, + ); - // Simulate a file edit with a newer mtime - $cacheFile = $rootPath . '/.marko/index.cache'; - touch($srcFile, filemtime($cacheFile) + 10); - $routeList[] = makeIndexRoute(); + // First read + $first = $cache->getRoutes(); + expect($first)->toBeEmpty(); - // Same instance: second read detects the edit and rebuilds - $second = $cache->getRoutes(); - expect($second)->toHaveCount(1); -}); + // Simulate a file edit with a newer mtime + $cacheFile = $rootPath . '/.marko/index.cache'; + touch($srcFile, filemtime($cacheFile) + 10); + $routeList[] = makeIndexRoute(); -it('it reflects a deleted app module on the next read by comparing the tracked path set', function () use (&$tmpDirs): void { - $rootPath = makeTempDir(); - $tmpDirs[] = $rootPath; + // Same instance: second read detects the edit and rebuilds + $second = $cache->getRoutes(); + expect($second)->toHaveCount(1); + }, +); - $appDir = $rootPath . '/app'; - $modulePath = $appDir . '/delete-module'; - mkdir($modulePath . '/src', 0755, true); - $srcFile = $modulePath . '/src/ToDelete.php'; - file_put_contents($srcFile, ' $modules */ - public function __construct(private array &$modules) {} + $module = new ModuleInfo('test/delete-module', $modulePath, 'Test\\Delete\\'); + $modules = [$module]; - public function walk(): array - { - return $this->modules; - } - }; - $attributeParser = new class () extends AttributeParser - { - public function observers(ModuleInfo $m): array + $walker = new class ($modules) extends ModuleWalker { - return []; - } + /** @param list $modules */ + public function __construct(private array &$modules) {} - public function plugins(ModuleInfo $m): array + public function walk(): array + { + return $this->modules; + } + }; + $attributeParser = new class () extends AttributeParser { - return []; - } + public function observers(ModuleInfo $m): array + { + return []; + } - public function preferences(ModuleInfo $m): array - { - return []; - } + public function plugins(ModuleInfo $m): array + { + return []; + } - public function commands(ModuleInfo $m): array - { - return []; - } + public function preferences(ModuleInfo $m): array + { + return []; + } - public function routes(ModuleInfo $m): array - { - return []; - } - }; - $configScanner = new class () extends ConfigScanner - { - public function scan(ModuleInfo $m): array + public function commands(ModuleInfo $m): array + { + return []; + } + + public function routes(ModuleInfo $m): array + { + return []; + } + }; + $configScanner = new class () extends ConfigScanner { - return []; - } - }; - $templateScanner = new class () extends TemplateScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $templateScanner = new class () extends TemplateScanner { - return []; - } - }; - $translationScanner = new class () extends TranslationScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $translationScanner = new class () extends TranslationScanner { - return []; - } - }; + public function scan(ModuleInfo $m): array + { + return []; + } + }; - $cache = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); + $cache = makeIndexCache( + $rootPath, + $walker, + $attributeParser, + $configScanner, + $templateScanner, + $translationScanner, + ); - // First read: 1 module with 1 src file - $first = $cache->getModules(); - expect($first)->toHaveCount(1); + // First read: 1 module with 1 src file + $first = $cache->getModules(); + expect($first)->toHaveCount(1); - // Delete the source file on disk and remove module from walker - unlink($srcFile); - rmdir($modulePath . '/src'); - rmdir($modulePath); - $modules = []; + // Delete the source file on disk and remove module from walker + unlink($srcFile); + rmdir($modulePath . '/src'); + rmdir($modulePath); + $modules = []; - // Same instance: second read detects the deletion via path-set comparison - $second = $cache->getModules(); - expect($second)->toBeEmpty(); -}); + // Same instance: second read detects the deletion via path-set comparison + $second = $cache->getModules(); + expect($second)->toBeEmpty(); + }, +); it('it does not rebuild when no tracked file under app or modules changed', function () use (&$tmpDirs): void { $rootPath = makeTempDir(); @@ -1624,7 +1664,14 @@ public function scan(ModuleInfo $m): array } }; - $cache = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); + $cache = makeIndexCache( + $rootPath, + $walker, + $attributeParser, + $configScanner, + $templateScanner, + $translationScanner, + ); // First read triggers build (observers() called once per module) $cache->getModules(); @@ -1639,477 +1686,534 @@ public function scan(ModuleInfo $m): array ->and($scanCountAfterThree)->toBe($scanCountAfterFirst); }); -it('it does not trigger a rebuild when only a vendor file changes after first load', function () use (&$tmpDirs): void { - $rootPath = makeTempDir(); - $tmpDirs[] = $rootPath; - - // Create a vendor module on disk (path under vendor/) - $vendorModulePath = $rootPath . '/vendor/vendor-org/vendor-pkg'; - mkdir($vendorModulePath . '/src', 0755, true); - $vendorSrcFile = $vendorModulePath . '/src/VendorClass.php'; - file_put_contents($vendorSrcFile, 'vendorModule, $this->appModule]; - } - }; - // Count observers() calls — these only fire during build(), not during isStale() - $attributeParser = new class ($scanCount) extends AttributeParser - { - public function __construct(private int &$scanCount) {} + public function __construct( + private readonly ModuleInfo $vendorModule, + private readonly ModuleInfo $appModule, + ) {} - public function observers(ModuleInfo $m): array + public function walk(): array + { + return [$this->vendorModule, $this->appModule]; + } + }; + // Count observers() calls — these only fire during build(), not during isStale() + $attributeParser = new class ($scanCount) extends AttributeParser { - $this->scanCount++; + public function __construct(private int &$scanCount) {} - return []; - } + public function observers(ModuleInfo $m): array + { + $this->scanCount++; - public function plugins(ModuleInfo $m): array - { - return []; - } + return []; + } - public function preferences(ModuleInfo $m): array - { - return []; - } + public function plugins(ModuleInfo $m): array + { + return []; + } - public function commands(ModuleInfo $m): array - { - return []; - } + public function preferences(ModuleInfo $m): array + { + return []; + } - public function routes(ModuleInfo $m): array - { - return []; - } - }; - $configScanner = new class () extends ConfigScanner - { - public function scan(ModuleInfo $m): array + public function commands(ModuleInfo $m): array + { + return []; + } + + public function routes(ModuleInfo $m): array + { + return []; + } + }; + $configScanner = new class () extends ConfigScanner { - return []; - } - }; - $templateScanner = new class () extends TemplateScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $templateScanner = new class () extends TemplateScanner { - return []; - } - }; - $translationScanner = new class () extends TranslationScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $translationScanner = new class () extends TranslationScanner { - return []; - } - }; - - $cache = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); + public function scan(ModuleInfo $m): array + { + return []; + } + }; - // First read — builds (observers called for each module: vendor + app = 2 calls) - $cache->getModules(); - $scanCountAfterFirst = $scanCount; + $cache = makeIndexCache( + $rootPath, + $walker, + $attributeParser, + $configScanner, + $templateScanner, + $translationScanner, + ); - // Touch only a vendor file (newer than cache) - $cacheFile = $rootPath . '/.marko/index.cache'; - touch($vendorSrcFile, filemtime($cacheFile) + 10); + // First read — builds (observers called for each module: vendor + app = 2 calls) + $cache->getModules(); + $scanCountAfterFirst = $scanCount; - // Second read: vendor change must NOT trigger rebuild — scan count must not increase - $cache->getModules(); - $scanCountAfterSecond = $scanCount; + // Touch only a vendor file (newer than cache) + $cacheFile = $rootPath . '/.marko/index.cache'; + touch($vendorSrcFile, filemtime($cacheFile) + 10); - expect($scanCountAfterSecond)->toBe($scanCountAfterFirst); -}); + // Second read: vendor change must NOT trigger rebuild — scan count must not increase + $cache->getModules(); + $scanCountAfterSecond = $scanCount; -it('it rebuilds at most once for several successive reads following a single change', function () use (&$tmpDirs): void { - $rootPath = makeTempDir(); - $tmpDirs[] = $rootPath; + expect($scanCountAfterSecond)->toBe($scanCountAfterFirst); + }, +); - $appDir = $rootPath . '/app'; - $modulePath = $appDir . '/once-module'; - mkdir($modulePath . '/src', 0755, true); - $srcFile = $modulePath . '/src/Once.php'; - file_put_contents($srcFile, 'module]; - } - }; - // Count observers() calls — only happen during build() - $attributeParser = new class ($scanCount) extends AttributeParser - { - public function __construct(private int &$scanCount) {} + public function __construct(private readonly ModuleInfo $module) {} - public function observers(ModuleInfo $m): array + public function walk(): array + { + return [$this->module]; + } + }; + // Count observers() calls — only happen during build() + $attributeParser = new class ($scanCount) extends AttributeParser { - $this->scanCount++; + public function __construct(private int &$scanCount) {} - return []; - } + public function observers(ModuleInfo $m): array + { + $this->scanCount++; - public function plugins(ModuleInfo $m): array - { - return []; - } + return []; + } - public function preferences(ModuleInfo $m): array - { - return []; - } + public function plugins(ModuleInfo $m): array + { + return []; + } - public function commands(ModuleInfo $m): array - { - return []; - } + public function preferences(ModuleInfo $m): array + { + return []; + } - public function routes(ModuleInfo $m): array - { - return []; - } - }; - $configScanner = new class () extends ConfigScanner - { - public function scan(ModuleInfo $m): array + public function commands(ModuleInfo $m): array + { + return []; + } + + public function routes(ModuleInfo $m): array + { + return []; + } + }; + $configScanner = new class () extends ConfigScanner { - return []; - } - }; - $templateScanner = new class () extends TemplateScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $templateScanner = new class () extends TemplateScanner { - return []; - } - }; - $translationScanner = new class () extends TranslationScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $translationScanner = new class () extends TranslationScanner { - return []; - } - }; - - $cache = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); + public function scan(ModuleInfo $m): array + { + return []; + } + }; - // First read - $cache->getModules(); - $scanCountAfterFirst = $scanCount; + $cache = makeIndexCache( + $rootPath, + $walker, + $attributeParser, + $configScanner, + $templateScanner, + $translationScanner, + ); - // Backdate the cache file so the existing src file appears newer than the cache. - // After the rebuild, the new cache is written at "now" which is > the src file - // mtime, so subsequent reads are not stale (no rebuild storm). - $cacheFile = $rootPath . '/.marko/index.cache'; - touch($cacheFile, filemtime($srcFile) - 20); + // First read + $cache->getModules(); + $scanCountAfterFirst = $scanCount; - // Three successive reads after one change — only ONE rebuild should fire - $cache->getModules(); - $cache->getModules(); - $cache->getModules(); - $scanCountAfterThree = $scanCount; + // Backdate the cache file so the existing src file appears newer than the cache. + // After the rebuild, the new cache is written at "now" which is > the src file + // mtime, so subsequent reads are not stale (no rebuild storm). + $cacheFile = $rootPath . '/.marko/index.cache'; + touch($cacheFile, filemtime($srcFile) - 20); - // Only one additional build should have fired (scan count increases by 1 per build call) - expect($scanCountAfterThree - $scanCountAfterFirst)->toBe(1); -}); + // Three successive reads after one change — only ONE rebuild should fire + $cache->getModules(); + $cache->getModules(); + $cache->getModules(); + $scanCountAfterThree = $scanCount; -it('it treats a cache payload missing the tracked path set as stale so old caches rebuild once', function () use (&$tmpDirs): void { - $rootPath = makeTempDir(); - $tmpDirs[] = $rootPath; + // Only one additional build should have fired (scan count increases by 1 per build call) + expect($scanCountAfterThree - $scanCountAfterFirst)->toBe(1); + }, +); - $appDir = $rootPath . '/app'; - $modulePath = $appDir . '/legacy-module'; - mkdir($modulePath . '/src', 0755, true); - file_put_contents($modulePath . '/src/Legacy.php', ' $modules */ - public function __construct(private array &$modules) {} + $module = new ModuleInfo('test/legacy', $modulePath, 'Test\\Legacy\\'); + $modules = [$module]; - public function walk(): array - { - return $this->modules; - } - }; - $attributeParser = new class () extends AttributeParser - { - public function observers(ModuleInfo $m): array + $walker = new class ($modules) extends ModuleWalker { - return []; - } + /** @param list $modules */ + public function __construct(private array &$modules) {} - public function plugins(ModuleInfo $m): array + public function walk(): array + { + return $this->modules; + } + }; + $attributeParser = new class () extends AttributeParser { - return []; - } + public function observers(ModuleInfo $m): array + { + return []; + } - public function preferences(ModuleInfo $m): array - { - return []; - } + public function plugins(ModuleInfo $m): array + { + return []; + } - public function commands(ModuleInfo $m): array - { - return []; - } + public function preferences(ModuleInfo $m): array + { + return []; + } - public function routes(ModuleInfo $m): array + public function commands(ModuleInfo $m): array + { + return []; + } + + public function routes(ModuleInfo $m): array + { + return []; + } + }; + $configScanner = new class () extends ConfigScanner { - return []; - } - }; - $configScanner = new class () extends ConfigScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $templateScanner = new class () extends TemplateScanner { - return []; - } - }; - $templateScanner = new class () extends TemplateScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $translationScanner = new class () extends TranslationScanner { - return []; - } - }; - $translationScanner = new class () extends TranslationScanner - { - public function scan(ModuleInfo $m): array - { - return []; - } - }; + public function scan(ModuleInfo $m): array + { + return []; + } + }; - // Write a legacy cache payload without 'trackedPaths' key - mkdir($rootPath . '/.marko', 0755, true); - $legacyPayload = [ - 'modules' => [$module], - 'observers' => [], - 'plugins' => [], - 'preferences' => [], - 'commands' => [], - 'routes' => [], - 'configKeys' => [], - 'templates' => [], - 'translationKeys' => [], - // intentionally no 'trackedPaths' key - ]; - file_put_contents($rootPath . '/.marko/index.cache', serialize($legacyPayload)); - - $cache = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); - - // Must not throw, must load (or rebuild) cleanly - $result = $cache->getModules(); - expect($result)->toHaveCount(1); -}); + // Write a legacy cache payload without 'trackedPaths' key + mkdir($rootPath . '/.marko', 0755, true); + $legacyPayload = [ + 'modules' => [$module], + 'observers' => [], + 'plugins' => [], + 'preferences' => [], + 'commands' => [], + 'routes' => [], + 'configKeys' => [], + 'templates' => [], + 'translationKeys' => [], + // intentionally no 'trackedPaths' key + ]; + file_put_contents($rootPath . '/.marko/index.cache', serialize($legacyPayload)); -it('it persists the tracked path set as a plain array or string that requires no new unserialize allowed class', function () use (&$tmpDirs): void { - $rootPath = makeTempDir(); - $tmpDirs[] = $rootPath; + $cache = makeIndexCache( + $rootPath, + $walker, + $attributeParser, + $configScanner, + $templateScanner, + $translationScanner, + ); - $appDir = $rootPath . '/app'; - $modulePath = $appDir . '/tracked-module'; - mkdir($modulePath . '/src', 0755, true); - file_put_contents($modulePath . '/src/Tracked.php', 'getModules(); + expect($result)->toHaveCount(1); + }, +); - $module = new ModuleInfo('test/tracked', $modulePath, 'Test\\Tracked\\'); +it( + 'it persists the tracked path set as a plain array or string that requires no new unserialize allowed class', + function () use (&$tmpDirs): void { + $rootPath = makeTempDir(); + $tmpDirs[] = $rootPath; - $walker = new class ($module) extends ModuleWalker - { - public function __construct(private readonly ModuleInfo $module) {} + $appDir = $rootPath . '/app'; + $modulePath = $appDir . '/tracked-module'; + mkdir($modulePath . '/src', 0755, true); + file_put_contents($modulePath . '/src/Tracked.php', 'module]; - } - }; - $attributeParser = new class () extends AttributeParser - { - public function observers(ModuleInfo $m): array - { - return []; - } + $module = new ModuleInfo('test/tracked', $modulePath, 'Test\\Tracked\\'); - public function plugins(ModuleInfo $m): array + $walker = new class ($module) extends ModuleWalker { - return []; - } + public function __construct(private readonly ModuleInfo $module) {} - public function preferences(ModuleInfo $m): array + public function walk(): array + { + return [$this->module]; + } + }; + $attributeParser = new class () extends AttributeParser { - return []; - } + public function observers(ModuleInfo $m): array + { + return []; + } - public function commands(ModuleInfo $m): array - { - return []; - } + public function plugins(ModuleInfo $m): array + { + return []; + } - public function routes(ModuleInfo $m): array - { - return []; - } - }; - $configScanner = new class () extends ConfigScanner - { - public function scan(ModuleInfo $m): array + public function preferences(ModuleInfo $m): array + { + return []; + } + + public function commands(ModuleInfo $m): array + { + return []; + } + + public function routes(ModuleInfo $m): array + { + return []; + } + }; + $configScanner = new class () extends ConfigScanner { - return []; - } - }; - $templateScanner = new class () extends TemplateScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $templateScanner = new class () extends TemplateScanner { - return []; - } - }; - $translationScanner = new class () extends TranslationScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $translationScanner = new class () extends TranslationScanner { - return []; - } - }; - - $cache = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); - $cache->build(); - - $cacheFile = $rootPath . '/.marko/index.cache'; - $raw = file_get_contents($cacheFile); - - // Deserialize with the existing allowlist (no new classes) - $data = unserialize($raw, [ - 'allowed_classes' => [ - CommandEntry::class, - ConfigKeyEntry::class, - ModuleInfo::class, - ObserverEntry::class, - PluginEntry::class, - PreferenceEntry::class, - RouteEntry::class, - TemplateEntry::class, - TranslationEntry::class, - ], - ]); - - expect($data)->toBeArray() - ->and($data)->toHaveKey('trackedPaths') - ->and($data['trackedPaths'])->toBeArray(); + public function scan(ModuleInfo $m): array + { + return []; + } + }; - // Each entry in trackedPaths must be a plain string - foreach ($data['trackedPaths'] as $path) { - expect($path)->toBeString(); - } -}); + $cache = makeIndexCache( + $rootPath, + $walker, + $attributeParser, + $configScanner, + $templateScanner, + $translationScanner, + ); + $cache->build(); -it('it preserves first-load semantics by loading a fresh on-disk cache without rescanning', function () use (&$tmpDirs): void { - $rootPath = makeTempDir(); - $tmpDirs[] = $rootPath; + $cacheFile = $rootPath . '/.marko/index.cache'; + $raw = file_get_contents($cacheFile); + + // Deserialize with the existing allowlist (no new classes) + $data = unserialize($raw, [ + 'allowed_classes' => [ + CommandEntry::class, + ConfigKeyEntry::class, + ModuleInfo::class, + ObserverEntry::class, + PluginEntry::class, + PreferenceEntry::class, + RouteEntry::class, + TemplateEntry::class, + TranslationEntry::class, + ], + ]); + + expect($data)->toBeArray() + ->and($data)->toHaveKey('trackedPaths') + ->and($data['trackedPaths'])->toBeArray(); + + // Each entry in trackedPaths must be a plain string + foreach ($data['trackedPaths'] as $path) { + expect($path)->toBeString(); + } + }, +); - $scanCount = 0; +it( + 'it preserves first-load semantics by loading a fresh on-disk cache without rescanning', + function () use (&$tmpDirs): void { + $rootPath = makeTempDir(); + $tmpDirs[] = $rootPath; - $walker = new class () extends ModuleWalker - { - public function __construct() {} + $scanCount = 0; - public function walk(): array + $walker = new class () extends ModuleWalker { - return []; - } - }; - $attributeParser = new class ($scanCount) extends AttributeParser - { - public function __construct(private int &$count) {} + public function __construct() {} - public function observers(ModuleInfo $m): array + public function walk(): array + { + return []; + } + }; + $attributeParser = new class ($scanCount) extends AttributeParser { - $this->count++; + public function __construct(private int &$count) {} - return []; - } + public function observers(ModuleInfo $m): array + { + $this->count++; - public function plugins(ModuleInfo $m): array - { - return []; - } + return []; + } - public function preferences(ModuleInfo $m): array - { - return []; - } + public function plugins(ModuleInfo $m): array + { + return []; + } - public function commands(ModuleInfo $m): array - { - return []; - } + public function preferences(ModuleInfo $m): array + { + return []; + } - public function routes(ModuleInfo $m): array - { - return []; - } - }; - $configScanner = new class () extends ConfigScanner - { - public function scan(ModuleInfo $m): array + public function commands(ModuleInfo $m): array + { + return []; + } + + public function routes(ModuleInfo $m): array + { + return []; + } + }; + $configScanner = new class () extends ConfigScanner { - return []; - } - }; - $templateScanner = new class () extends TemplateScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $templateScanner = new class () extends TemplateScanner { - return []; - } - }; - $translationScanner = new class () extends TranslationScanner - { - public function scan(ModuleInfo $m): array + public function scan(ModuleInfo $m): array + { + return []; + } + }; + $translationScanner = new class () extends TranslationScanner { - return []; - } - }; + public function scan(ModuleInfo $m): array + { + return []; + } + }; - // Build and save cache using first instance - $cache = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); - $cache->build(); + // Build and save cache using first instance + $cache = makeIndexCache( + $rootPath, + $walker, + $attributeParser, + $configScanner, + $templateScanner, + $translationScanner, + ); + $cache->build(); - // Load on a fresh instance — should not re-invoke scanners - $cache2 = makeIndexCache($rootPath, $walker, $attributeParser, $configScanner, $templateScanner, $translationScanner); - $loaded = $cache2->load(); + // Load on a fresh instance — should not re-invoke scanners + $cache2 = makeIndexCache( + $rootPath, + $walker, + $attributeParser, + $configScanner, + $templateScanner, + $translationScanner, + ); + $loaded = $cache2->load(); - expect($loaded)->toBeTrue() - ->and($scanCount)->toBe(0); -}); + expect($loaded)->toBeTrue() + ->and($scanCount)->toBe(0); + }, +); diff --git a/packages/codeindexer/tests/Unit/Config/ConfigScannerTest.php b/packages/codeindexer/tests/Unit/Config/ConfigScannerTest.php index 43ece86c..04c8d007 100644 --- a/packages/codeindexer/tests/Unit/Config/ConfigScannerTest.php +++ b/packages/codeindexer/tests/Unit/Config/ConfigScannerTest.php @@ -23,27 +23,27 @@ function () use ($fixtures): void { $scanner = new ConfigScanner(); $module = new ModuleInfo('module-c', $fixtures . '/module-c', 'Marko\\ModuleC'); - + $entries = $scanner->scan($module); $diagnostics = $scanner->diagnostics(); - + $keys = array_map(fn (ConfigKeyEntry $e): string => $e->key, $entries); $byKey = []; foreach ($entries as $entry) { $byKey[$entry->key] = $entry; } - + // dynamic.php returns a function call — no keys, but diagnostic recorded - $diagnosticFiles = array_map(fn (array $d): string => basename($d['file']), $diagnostics); + $diagnosticFiles = array_map(fn (array $d): string => basename($d['file']), $diagnostics); expect($diagnosticFiles)->toContain('dynamic.php'); - + // partial.php has a mix — known keys indexed, dynamic value recorded as type='dynamic' - expect($keys)->toContain('partial.driver') - ->and($keys)->toContain('partial.port') - ->and($keys)->toContain('partial.host') - ->and($byKey['partial.port']->type)->toBe('dynamic') - ->and($byKey['partial.port']->defaultValue)->toBeNull(); - } + expect($keys)->toContain('partial.driver') + ->and($keys)->toContain('partial.port') + ->and($keys)->toContain('partial.host') + ->and($byKey['partial.port']->type)->toBe('dynamic') + ->and($byKey['partial.port']->defaultValue)->toBeNull(); + }, ); it('does not include or eval config files — reads values via AST only', function (): void { diff --git a/packages/codeindexer/tests/Unit/SkeletonTest.php b/packages/codeindexer/tests/Unit/SkeletonTest.php index f94125af..fe0907b1 100644 --- a/packages/codeindexer/tests/Unit/SkeletonTest.php +++ b/packages/codeindexer/tests/Unit/SkeletonTest.php @@ -79,7 +79,7 @@ 'defines the IndexCacheInterface contract (the one swappable seam — cache backend)', function (): void { expect(interface_exists(IndexCacheInterface::class))->toBeTrue(); - } + }, ); it( @@ -96,58 +96,58 @@ function (): void { TemplateEntry::class, TranslationEntry::class, ]; - + foreach ($valueObjects as $class) { $reflection = new ReflectionClass($class); expect($reflection->isReadOnly())->toBeTrue("Expected $class to be readonly"); } - + $moduleInfo = new ModuleInfo('test', '/path', 'Ns'); expect($moduleInfo->name)->toBe('test') ->and($moduleInfo->path)->toBe('/path') ->and($moduleInfo->namespace)->toBe('Ns'); - + $observerEntry = new ObserverEntry('Cls', 'evt', 'mth', 10); expect($observerEntry->class)->toBe('Cls') ->and($observerEntry->event)->toBe('evt') ->and($observerEntry->method)->toBe('mth') ->and($observerEntry->sortOrder)->toBe(10); - + $pluginEntry = new PluginEntry('Cls', 'tgt', 'mth', 'before', 5); expect($pluginEntry->class)->toBe('Cls') ->and($pluginEntry->target)->toBe('tgt') ->and($pluginEntry->method)->toBe('mth') ->and($pluginEntry->type)->toBe('before') ->and($pluginEntry->sortOrder)->toBe(5); - + $preferenceEntry = new PreferenceEntry('Iface', 'Impl', 'mod'); expect($preferenceEntry->interface)->toBe('Iface') ->and($preferenceEntry->implementation)->toBe('Impl') ->and($preferenceEntry->module)->toBe('mod'); - + $commandEntry = new CommandEntry('cmd:name', 'CmdCls', 'desc'); expect($commandEntry->name)->toBe('cmd:name') ->and($commandEntry->class)->toBe('CmdCls') ->and($commandEntry->description)->toBe('desc'); - + $routeEntry = new RouteEntry('GET', '/path', 'Ctrl', 'index'); expect($routeEntry->method)->toBe('GET') ->and($routeEntry->path)->toBe('/path') ->and($routeEntry->class)->toBe('Ctrl') ->and($routeEntry->action)->toBe('index'); - + $configKeyEntry = new ConfigKeyEntry('app.name', 'string', 'Marko', 'core'); expect($configKeyEntry->key)->toBe('app.name') ->and($configKeyEntry->type)->toBe('string') ->and($configKeyEntry->defaultValue)->toBe('Marko') ->and($configKeyEntry->module)->toBe('core'); - + $templateEntry = new TemplateEntry('mod', 'tmpl-1', '/tmpl.latte', 'latte'); expect($templateEntry->moduleName)->toBe('mod') ->and($templateEntry->templateName)->toBe('tmpl-1') ->and($templateEntry->absolutePath)->toBe('/tmpl.latte') ->and($templateEntry->extension)->toBe('latte'); - + $translationEntry = new TranslationEntry( key: 'messages.hello', group: 'messages', @@ -162,5 +162,5 @@ function (): void { ->and($translationEntry->locale)->toBe('en') ->and($translationEntry->namespace)->toBe('mod') ->and($translationEntry->module)->toBe('mod'); - } + }, ); diff --git a/packages/core/src/Commands/ListCommand.php b/packages/core/src/Commands/ListCommand.php index 0289b5b1..c1c04c41 100644 --- a/packages/core/src/Commands/ListCommand.php +++ b/packages/core/src/Commands/ListCommand.php @@ -36,7 +36,7 @@ public function execute( if ($definition->aliases !== []) { $displayNames[$definition->name] = $definition->name . ' (' . implode( ', ', - $definition->aliases + $definition->aliases, ) . ')'; } else { $displayNames[$definition->name] = $definition->name; diff --git a/packages/core/src/Module/ModuleAutoloader.php b/packages/core/src/Module/ModuleAutoloader.php index b6993eaa..62749ecd 100644 --- a/packages/core/src/Module/ModuleAutoloader.php +++ b/packages/core/src/Module/ModuleAutoloader.php @@ -58,8 +58,10 @@ public function register(): void } } - private function registerPsr4(string $namespace, string $basePath): void - { + private function registerPsr4( + string $namespace, + string $basePath, + ): void { spl_autoload_register(function (string $class) use ($namespace, $basePath): void { if (!str_starts_with($class, $namespace)) { return; diff --git a/packages/core/tests/Feature/Plugin/PluginInterceptionIntegrationTest.php b/packages/core/tests/Feature/Plugin/PluginInterceptionIntegrationTest.php index 118fe6d4..4233dfd9 100644 --- a/packages/core/tests/Feature/Plugin/PluginInterceptionIntegrationTest.php +++ b/packages/core/tests/Feature/Plugin/PluginInterceptionIntegrationTest.php @@ -49,8 +49,7 @@ class PIIT_HasherAfterPlugin public function hash( mixed $result, string $value, - ): string - { + ): string { self::$log[] = "after:$result"; return strtoupper((string) $result); @@ -73,8 +72,7 @@ public function hash(string $value): null public function hashAfter( mixed $result, string $value, - ): string - { + ): string { self::$log[] = "after:$result"; return (string) $result; @@ -107,8 +105,7 @@ class PIIT_FirstAfterPlugin public function hash( mixed $result, string $value, - ): string - { + ): string { self::$log[] = "first-after:$result"; return "[$result]"; @@ -123,8 +120,7 @@ class PIIT_SecondAfterPlugin public function hash( mixed $result, string $value, - ): string - { + ): string { self::$log[] = "second-after:$result"; return "$result!"; diff --git a/packages/core/tests/Unit/ApplicationTest.php b/packages/core/tests/Unit/ApplicationTest.php index f567c2c3..f6e0a5e3 100644 --- a/packages/core/tests/Unit/ApplicationTest.php +++ b/packages/core/tests/Unit/ApplicationTest.php @@ -1696,7 +1696,7 @@ public function execute( expect(fn () => $app->handleRequest()) ->toThrow( RuntimeException::class, - 'Cannot handle HTTP requests: marko/routing is not installed. Run: composer require marko/routing' + 'Cannot handle HTTP requests: marko/routing is not installed. Run: composer require marko/routing', ); }); diff --git a/packages/core/tests/Unit/Module/DependencyResolverTest.php b/packages/core/tests/Unit/Module/DependencyResolverTest.php index f09909b5..3dc7377b 100644 --- a/packages/core/tests/Unit/Module/DependencyResolverTest.php +++ b/packages/core/tests/Unit/Module/DependencyResolverTest.php @@ -345,54 +345,60 @@ ->not->toContain('not installed'); }); -it('throws a missing dependency error (not an empty-chain circular error) for a before-after soft-ordering deadlock', function (): void { - // Module A declares both after: [B] and before: [B] — an unsatisfiable ordering deadlock - $moduleA = new ModuleManifest( - name: 'vendor/module-a', - version: '1.0.0', - after: ['vendor/module-b'], - before: ['vendor/module-b'], - ); - $moduleB = new ModuleManifest( - name: 'vendor/module-b', - version: '1.0.0', - ); - - $resolver = new DependencyResolver(); - - expect(fn () => $resolver->resolve([$moduleA, $moduleB])) - ->toThrow(MissingDependencyException::class); -}); - -it('names the unsorted modules and their unmet ordering constraints in the missing-dependency message', function (): void { - // Module A declares both after: [B] and before: [B] — unsatisfiable - $moduleA = new ModuleManifest( - name: 'vendor/module-a', - version: '1.0.0', - after: ['vendor/module-b'], - before: ['vendor/module-b'], - ); - $moduleB = new ModuleManifest( - name: 'vendor/module-b', - version: '1.0.0', - ); - - $resolver = new DependencyResolver(); - - $exception = null; - - try { - $resolver->resolve([$moduleA, $moduleB]); - } catch (MissingDependencyException $e) { - $exception = $e; - } - - expect($exception)->not->toBeNull() - ->and($exception->getMessage()) - ->toContain('vendor/module-a') - ->toContain('after:vendor/module-b') - ->toContain('before:vendor/module-b'); -}); +it( + 'throws a missing dependency error (not an empty-chain circular error) for a before-after soft-ordering deadlock', + function (): void { + // Module A declares both after: [B] and before: [B] — an unsatisfiable ordering deadlock + $moduleA = new ModuleManifest( + name: 'vendor/module-a', + version: '1.0.0', + after: ['vendor/module-b'], + before: ['vendor/module-b'], + ); + $moduleB = new ModuleManifest( + name: 'vendor/module-b', + version: '1.0.0', + ); + + $resolver = new DependencyResolver(); + + expect(fn () => $resolver->resolve([$moduleA, $moduleB])) + ->toThrow(MissingDependencyException::class); + }, +); + +it( + 'names the unsorted modules and their unmet ordering constraints in the missing-dependency message', + function (): void { + // Module A declares both after: [B] and before: [B] — unsatisfiable + $moduleA = new ModuleManifest( + name: 'vendor/module-a', + version: '1.0.0', + after: ['vendor/module-b'], + before: ['vendor/module-b'], + ); + $moduleB = new ModuleManifest( + name: 'vendor/module-b', + version: '1.0.0', + ); + + $resolver = new DependencyResolver(); + + $exception = null; + + try { + $resolver->resolve([$moduleA, $moduleB]); + } catch (MissingDependencyException $e) { + $exception = $e; + } + + expect($exception)->not->toBeNull() + ->and($exception->getMessage()) + ->toContain('vendor/module-a') + ->toContain('after:vendor/module-b') + ->toContain('before:vendor/module-b'); + }, +); it('throws a circular dependency error with a populated chain for a real two-module require cycle', function (): void { // A requires B, B requires A — a real cycle via require @@ -479,19 +485,22 @@ expect($names)->toBe(['vendor/module-a', 'vendor/module-b']); }); -it('still resolves successfully when an enabled module requires a non-marko composer package (absent from the module list)', function (): void { - $moduleA = new ModuleManifest( - name: 'vendor/module-a', - version: '1.0.0', - require: ['psr/container' => '^2.0', 'symfony/http-foundation' => '^6.0'], - ); - - $resolver = new DependencyResolver(); - $sorted = $resolver->resolve([$moduleA]); - - expect($sorted)->toHaveCount(1) - ->and($sorted[0]->name)->toBe('vendor/module-a'); -}); +it( + 'still resolves successfully when an enabled module requires a non-marko composer package (absent from the module list)', + function (): void { + $moduleA = new ModuleManifest( + name: 'vendor/module-a', + version: '1.0.0', + require: ['psr/container' => '^2.0', 'symfony/http-foundation' => '^6.0'], + ); + + $resolver = new DependencyResolver(); + $sorted = $resolver->resolve([$moduleA]); + + expect($sorted)->toHaveCount(1) + ->and($sorted[0]->name)->toBe('vendor/module-a'); + }, +); it('throws a missing dependency error naming a module that requires a disabled dependency', function (): void { $moduleA = new ModuleManifest( diff --git a/packages/core/tests/Unit/Plugin/PluginInterceptorTest.php b/packages/core/tests/Unit/Plugin/PluginInterceptorTest.php index df749604..117db97e 100644 --- a/packages/core/tests/Unit/Plugin/PluginInterceptorTest.php +++ b/packages/core/tests/Unit/Plugin/PluginInterceptorTest.php @@ -53,8 +53,7 @@ class PIT_GreeterAfterPlugin public function greet( mixed $result, string $name, - ): mixed - { + ): mixed { self::$callLog[] = 'PIT_GreeterAfterPlugin::greet'; return strtoupper((string) $result); @@ -136,8 +135,7 @@ class PIT_ArgsService public function process( string $name, int $count, - ): string - { + ): string { self::$callLog[] = "PIT_ArgsService::process($name, $count)"; return "processed: $name, $count"; @@ -151,8 +149,7 @@ class PIT_ArgsBeforePlugin public function process( string $name, int $count, - ): ?string - { + ): ?string { self::$receivedArgs = ['name' => $name, 'count' => $count]; PIT_ArgsService::$callLog[] = "PIT_ArgsBeforePlugin::process($name, $count)"; @@ -165,8 +162,7 @@ class PIT_ArgsModifyingBeforePlugin public function process( string $name, int $count, - ): ?array - { + ): ?array { return ['modified-name', $count + 10]; } } @@ -179,8 +175,7 @@ public function process( mixed $result, string $name, int $count, - ): mixed - { + ): mixed { self::$receivedArgs = ['result' => $result, 'name' => $name, 'count' => $count]; return $result; @@ -264,8 +259,7 @@ class PIT_RequiredParamService public function create( string $name, int $age, - ): string - { + ): string { return "$name is $age"; } } @@ -275,8 +269,7 @@ class PIT_WrongCountBeforePlugin public function create( string $name, int $age, - ): ?array - { + ): ?array { return ['only-one']; } } @@ -290,8 +283,7 @@ class PIT_ChainArgService public function transform( string $text, int $mult, - ): string - { + ): string { return "result: $text x$mult"; } } @@ -301,8 +293,7 @@ class PIT_ChainArgFirstPlugin public function transform( string $text, int $mult, - ): ?array - { + ): ?array { return ["$text-first", $mult + 1]; } } @@ -312,8 +303,7 @@ class PIT_ChainArgSecondPlugin public function transform( string $text, int $mult, - ): ?array - { + ): ?array { return ["$text-second", $mult + 1]; } } @@ -349,8 +339,7 @@ class PIT_CompleteFlowAfterPlugin public function process( mixed $result, string $input, - ): string - { + ): string { PIT_CompleteFlowService::$callLog[] = "PIT_CompleteFlowAfterPlugin::process($result, $input)"; return "$result [modified]"; @@ -587,7 +576,7 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): $proxy = $interceptor->createProxy( PIT_GreeterInterface::class, PIT_ConcreteGreeter::class, - new PIT_ConcreteGreeter() + new PIT_ConcreteGreeter(), ); expect($proxy)->toBeInstanceOf(PIT_GreeterInterface::class); @@ -653,7 +642,7 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): $proxy = $interceptor->createProxy( PIT_ShortCircuitService::class, PIT_ShortCircuitService::class, - new PIT_ShortCircuitService() + new PIT_ShortCircuitService(), ); $result = $proxy->fetch('mykey'); @@ -676,7 +665,7 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): $proxy = $interceptor->createProxy( PIT_ShortCircuitService::class, PIT_ShortCircuitService::class, - new PIT_ShortCircuitService() + new PIT_ShortCircuitService(), ); $result = $proxy->fetch('mykey'); @@ -717,7 +706,7 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): $proxy = $interceptor->createProxy( PIT_RequiredParamService::class, PIT_RequiredParamService::class, - new PIT_RequiredParamService() + new PIT_RequiredParamService(), ); expect(fn () => $proxy->create('Alice', 30))->toThrow(PluginArgumentCountException::class); @@ -743,7 +732,7 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): $proxy = $interceptor->createProxy( PIT_ChainArgService::class, PIT_ChainArgService::class, - new PIT_ChainArgService() + new PIT_ChainArgService(), ); $result = $proxy->transform('hello', 1); @@ -793,7 +782,7 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): $proxy->process('test', 42); expect(PIT_ArgsAfterPlugin::$receivedArgs)->toBe( - ['result' => 'processed: test, 42', 'name' => 'test', 'count' => 42] + ['result' => 'processed: test, 42', 'name' => 'test', 'count' => 42], ); }); @@ -817,7 +806,7 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): $proxy = $interceptor->createProxy( PIT_ResultModService::class, PIT_ResultModService::class, - new PIT_ResultModService() + new PIT_ResultModService(), ); $result = $proxy->getValue(); @@ -875,7 +864,7 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): $proxy = $interceptor->createProxy( PIT_CompleteFlowService::class, PIT_CompleteFlowService::class, - new PIT_CompleteFlowService() + new PIT_CompleteFlowService(), ); $result = $proxy->process('test'); @@ -946,7 +935,7 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): $proxy = $interceptor->createProxy( PIT_MethodNameService::class, PIT_MethodNameService::class, - new PIT_MethodNameService() + new PIT_MethodNameService(), ); $result = $proxy->save('hello'); @@ -970,7 +959,7 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): $proxy = $interceptor->createProxy( PIT_MethodNameService::class, PIT_MethodNameService::class, - new PIT_MethodNameService() + new PIT_MethodNameService(), ); $result = $proxy->save('test data'); @@ -984,26 +973,26 @@ function makePluginInterceptor(Container $container, PluginRegistry $registry): function (): void { $container = new Container(); $registry = new PluginRegistry(); - + $registry->register(new PluginDefinition( pluginClass: PIT_HasherPlugin::class, targetClass: PIT_HasherInterface::class, beforeMethods: ['hash' => ['pluginMethod' => 'hash', 'sortOrder' => 10]], )); - + $interceptor = makePluginInterceptor($container, $registry); $proxy = $interceptor->createProxy( PIT_HasherInterface::class, PIT_BcryptHasher::class, - new PIT_BcryptHasher() + new PIT_BcryptHasher(), ); - + $result = $proxy->hash('secret'); - + expect(PIT_HasherPlugin::$callLog)->toBe(['PIT_HasherPlugin::hash(secret)']) ->and(PIT_BcryptHasher::$callLog)->toBe(['PIT_BcryptHasher::hash(secret)']) ->and($result)->toBe('bcrypt:secret'); - } + }, ); it('exposes original target via getPluginTarget', function (): void { @@ -1040,8 +1029,8 @@ function (): void { fn () => $interceptor->createProxy( PIT_ReadonlyService::class, PIT_ReadonlyService::class, - new PIT_ReadonlyService() - ) + new PIT_ReadonlyService(), + ), ) ->toThrow(PluginException::class); }); diff --git a/packages/database-mysql/tests/Query/MySqlQueryBuilderAggregatesTest.php b/packages/database-mysql/tests/Query/MySqlQueryBuilderAggregatesTest.php index 5f56f393..3e4f1b74 100644 --- a/packages/database-mysql/tests/Query/MySqlQueryBuilderAggregatesTest.php +++ b/packages/database-mysql/tests/Query/MySqlQueryBuilderAggregatesTest.php @@ -176,7 +176,7 @@ function (): void { ->toThrow(InvalidColumnException::class) ->and(fn () => $this->builder->table('scores')->avg('/*bad*/')) ->toThrow(InvalidColumnException::class); - } + }, ); it( @@ -186,7 +186,7 @@ function (): void { $method = $reflection->getMethod('count'); $returnType = $method->getReturnType(); $params = $method->getParameters(); - + expect($returnType?->getName())->toBe('int') ->and($returnType?->allowsNull())->toBeFalse() ->and($params)->toHaveCount(1) @@ -194,6 +194,6 @@ function (): void { ->and($params[0]->isOptional())->toBeTrue() ->and($params[0]->allowsNull())->toBeTrue() ->and($params[0]->getDefaultValue())->toBeNull(); - } + }, ); }); diff --git a/packages/database-mysql/tests/Query/MySqlQueryBuilderGroupByTest.php b/packages/database-mysql/tests/Query/MySqlQueryBuilderGroupByTest.php index 4de5d546..b5282c12 100644 --- a/packages/database-mysql/tests/Query/MySqlQueryBuilderGroupByTest.php +++ b/packages/database-mysql/tests/Query/MySqlQueryBuilderGroupByTest.php @@ -32,8 +32,7 @@ public function isConnected(): bool public function query( string $sql, array $bindings = [], - ): array - { + ): array { $this->lastSql = $sql; $this->lastBindings = $bindings; @@ -43,8 +42,7 @@ public function query( public function execute( string $sql, array $bindings = [], - ): int - { + ): int { return 0; } @@ -169,7 +167,7 @@ function (): void { $sql = ''; $bindings = []; $conn = makeRecordingConnection($sql, $bindings); - + expect( fn () => (new MySqlQueryBuilder($conn)) ->table('orders') @@ -177,7 +175,7 @@ function (): void { ->groupBy('status; DROP TABLE orders--') ->get(), )->toThrow(InvalidColumnException::class); - } + }, ); it('rejects HAVING expressions containing semicolons or SQL comments', function (): void { @@ -219,7 +217,7 @@ function (): void { $sql = ''; $bindings = []; $conn = makeRecordingConnection($sql, $bindings); - + (new MySqlQueryBuilder($conn)) ->table('orders') ->select('status', 'country') @@ -227,11 +225,11 @@ function (): void { ->groupBy('status', 'country') ->having('COUNT(*) BETWEEN ? AND ?', [3, 10]) ->get(); - + expect($sql)->toBe( 'SELECT `status`, `country` FROM `orders` WHERE `active` = ? GROUP BY `status`, `country` HAVING COUNT(*) BETWEEN ? AND ?', ) ->and($bindings)->toBe([1, 3, 10]); - } + }, ); }); diff --git a/packages/database/src/Entity/EntityCollection.php b/packages/database/src/Entity/EntityCollection.php index c21ecb46..eff21661 100644 --- a/packages/database/src/Entity/EntityCollection.php +++ b/packages/database/src/Entity/EntityCollection.php @@ -118,8 +118,7 @@ public function pluck(string $property): array public function sortBy( string $property, bool $descending = false, - ): self - { + ): self { $sorted = $this->entities; usort($sorted, function (Entity $a, Entity $b) use ($property, $descending): int { $result = $a->$property <=> $b->$property; diff --git a/packages/database/src/Entity/EntityCompanionStorage.php b/packages/database/src/Entity/EntityCompanionStorage.php index 5e99eec3..83d330fb 100644 --- a/packages/database/src/Entity/EntityCompanionStorage.php +++ b/packages/database/src/Entity/EntityCompanionStorage.php @@ -70,8 +70,7 @@ public function get(Entity $entity): array public function attach( Entity $entity, Entity $companion, - ): void - { + ): void { $bag = $this->companions[$entity] ?? []; $bag[$companion::class] = $companion; $this->companions[$entity] = $bag; diff --git a/packages/database/src/Entity/EntityHydrator.php b/packages/database/src/Entity/EntityHydrator.php index 5ad3a0fb..e8e40341 100644 --- a/packages/database/src/Entity/EntityHydrator.php +++ b/packages/database/src/Entity/EntityHydrator.php @@ -238,8 +238,7 @@ public function registerOriginalValues( public function attachCompanion( Entity $entity, Entity $companion, - ): void - { + ): void { EntityCompanionStorage::instance()->attach($entity, $companion); } diff --git a/packages/database/src/Entity/RelationshipLoader.php b/packages/database/src/Entity/RelationshipLoader.php index 3812f5ac..89ef3417 100644 --- a/packages/database/src/Entity/RelationshipLoader.php +++ b/packages/database/src/Entity/RelationshipLoader.php @@ -413,8 +413,7 @@ private function loadBelongsToMany( private function collectPropertyValues( array $entities, string $propertyName, - ): array - { + ): array { return array_map(fn (Entity $entity) => $this->getPropertyValue($entity, $propertyName), $entities); } @@ -424,8 +423,7 @@ private function collectPropertyValues( private function getPropertyValue( Entity $entity, string $propertyName, - ): mixed - { + ): mixed { $reflection = new ReflectionClass($entity); $property = $reflection->getProperty($propertyName); @@ -443,8 +441,7 @@ private function setProperty( Entity $entity, string $propertyName, mixed $value, - ): void - { + ): void { $reflection = new ReflectionClass($entity); $property = $reflection->getProperty($propertyName); @@ -468,8 +465,7 @@ private function setPropertyOnAll( array $entities, string $propertyName, mixed $value, - ): void - { + ): void { foreach ($entities as $entity) { $this->setProperty($entity, $propertyName, $value); } diff --git a/packages/database/src/Schema/SchemaRegistry.php b/packages/database/src/Schema/SchemaRegistry.php index 9bff39e1..ea4a12c8 100644 --- a/packages/database/src/Schema/SchemaRegistry.php +++ b/packages/database/src/Schema/SchemaRegistry.php @@ -170,7 +170,7 @@ public function registerEntities( // Merge foreign keys (use parent table name for FK name generation) foreach ($this->schemaBuilder->buildForeignKeysForTable( $parentMetadata->tableName, - $extenderMetadata->columns + $extenderMetadata->columns, ) as $fk) { $table = $table->withForeignKey($fk); } diff --git a/packages/database/tests/Attributes/RelationshipAttributeTest.php b/packages/database/tests/Attributes/RelationshipAttributeTest.php index 65452f96..d50a28a2 100644 --- a/packages/database/tests/Attributes/RelationshipAttributeTest.php +++ b/packages/database/tests/Attributes/RelationshipAttributeTest.php @@ -25,7 +25,7 @@ class RelUserEntity extends Entity RelRoleEntity::class, pivotClass: RelUserRoleEntity::class, foreignKey: 'user_id', - relatedKey: 'role_id' + relatedKey: 'role_id', )] public array $roles = []; } diff --git a/packages/database/tests/Entity/EntityCollectionTest.php b/packages/database/tests/Entity/EntityCollectionTest.php index 939325b9..de26938c 100644 --- a/packages/database/tests/Entity/EntityCollectionTest.php +++ b/packages/database/tests/Entity/EntityCollectionTest.php @@ -161,7 +161,7 @@ function makeEntity(int $id, string $name, ?string $role = null): Entity $collection = new EntityCollection([makeEntity(1, 'Alice'), makeEntity(2, 'Bob')]); expect( - $collection->contains(fn (Entity $e): bool => $e->name === 'Charlie') + $collection->contains(fn (Entity $e): bool => $e->name === 'Charlie'), )->toBeFalse(); // @phpstan-ignore-line }); }); diff --git a/packages/database/tests/Entity/EntityHydratorTest.php b/packages/database/tests/Entity/EntityHydratorTest.php index 676d4389..dbe957ad 100644 --- a/packages/database/tests/Entity/EntityHydratorTest.php +++ b/packages/database/tests/Entity/EntityHydratorTest.php @@ -664,7 +664,7 @@ public function parse(string $entityClass): EntityMetadata expect($entity->companions())->toHaveCount(2) ->and($entity->companion(HydratorTestProductExt::class))->toBeInstanceOf(HydratorTestProductExt::class) ->and($entity->companion(HydratorTestProductPricing::class))->toBeInstanceOf( - HydratorTestProductPricing::class + HydratorTestProductPricing::class, ); }); @@ -821,55 +821,55 @@ public function parse(string $entityClass): EntityMetadata 'does not require the EntityMetadataFactory call for entities without extenders (no extra parse)', function (): void { // Factory with no entries — if parse() is called it will throw a key error - $factory = createStubMetadataFactory([]); + $factory = createStubMetadataFactory([]); $hydrator = new EntityHydrator($factory); $metadata = createProductMetadata(); // extenders: [] $row = ['id' => 1, 'name' => 'Safe']; - + // Should not throw even though factory has no entries - /** @var HydratorTestProduct $entity */ + /** @var HydratorTestProduct $entity */ $entity = $hydrator->hydrate(HydratorTestProduct::class, $row, $metadata); - + expect($entity)->toBeInstanceOf(HydratorTestProduct::class); - } + }, ); it( 'constructs without the EntityMetadataFactory and hydrates non-extended entities correctly (backward compat)', function (): void { $hydrator = new EntityHydrator(); // no factory - $metadata = createProductMetadata(); // extenders: [] + $metadata = createProductMetadata(); // extenders: [] $row = ['id' => 10, 'name' => 'Compat']; /** @var HydratorTestProduct $entity */ $entity = $hydrator->hydrate(HydratorTestProduct::class, $row, $metadata); - + expect($entity)->toBeInstanceOf(HydratorTestProduct::class) ->and($entity->id)->toBe(10) ->and($entity->name)->toBe('Compat'); - } + }, ); it( 'correctly hydrates companions when the factory has not seen the extender classes before (on-demand parse)', function (): void { // Use a real EntityMetadataFactory — it has never parsed HydratorTestProductExt before - $factory = new EntityMetadataFactory(); + $factory = new EntityMetadataFactory(); $hydrator = new EntityHydrator($factory); $metadata = createProductMetadataWithExtenders(HydratorTestProductExt::class); - + $row = ['id' => 4, 'name' => 'Fresh', 'sku' => 'FRS-04', 'stock' => 7]; /** @var HydratorTestProduct $entity */ $entity = $hydrator->hydrate(HydratorTestProduct::class, $row, $metadata); - + /** @var HydratorTestProductExt $ext */ $ext = $entity->companion(HydratorTestProductExt::class); - + expect($ext)->toBeInstanceOf(HydratorTestProductExt::class) ->and($ext->sku)->toBe('FRS-04') ->and($ext->stock)->toBe(7); - } + }, ); // ------------------------------------------------------------------------- diff --git a/packages/database/tests/Entity/EntityMetadataFactoryRelationshipTest.php b/packages/database/tests/Entity/EntityMetadataFactoryRelationshipTest.php index c4c3000c..7c654c84 100644 --- a/packages/database/tests/Entity/EntityMetadataFactoryRelationshipTest.php +++ b/packages/database/tests/Entity/EntityMetadataFactoryRelationshipTest.php @@ -307,7 +307,7 @@ entityClass: Entity::class, pivotClass: Entity::class, foreignKey: 'user_id', - relatedKey: 'role_id' + relatedKey: 'role_id', )] public array $roles = []; }; @@ -327,7 +327,7 @@ entityClass: Entity::class, pivotClass: Entity::class, foreignKey: 'user_id', - relatedKey: 'role_id' + relatedKey: 'role_id', )] public array $roles = []; }; @@ -347,7 +347,7 @@ entityClass: Entity::class, pivotClass: Entity::class, foreignKey: 'user_id', - relatedKey: 'role_id' + relatedKey: 'role_id', )] public array $roles = []; }; @@ -367,7 +367,7 @@ entityClass: Entity::class, pivotClass: Entity::class, foreignKey: 'user_id', - relatedKey: 'role_id' + relatedKey: 'role_id', )] public array $roles = []; }; @@ -387,7 +387,7 @@ entityClass: Entity::class, pivotClass: Entity::class, foreignKey: 'user_id', - relatedKey: 'role_id' + relatedKey: 'role_id', )] public array $roles = []; }; @@ -407,7 +407,7 @@ entityClass: Entity::class, pivotClass: Entity::class, foreignKey: 'user_id', - relatedKey: 'role_id' + relatedKey: 'role_id', )] public array $roles = []; }; @@ -484,7 +484,7 @@ entityClass: Entity::class, pivotClass: Entity::class, foreignKey: 'user_id', - relatedKey: 'role_id' + relatedKey: 'role_id', )] public EntityCollection $roles; }; diff --git a/packages/database/tests/Entity/EntityTest.php b/packages/database/tests/Entity/EntityTest.php index 5a1500db..7f238669 100644 --- a/packages/database/tests/Entity/EntityTest.php +++ b/packages/database/tests/Entity/EntityTest.php @@ -147,14 +147,14 @@ function (): void { $parent = new ParentEntity(); $companionViaEntity = new CompanionEntity(); $companionViaHydrator = new AnotherCompanionEntity(); - + // Attach one via Entity public API, one via hydrator internal API - $parent->attachCompanion($companionViaEntity); + $parent->attachCompanion($companionViaEntity); $hydrator->attachCompanion($parent, $companionViaHydrator); - + // Both are visible through the same Entity::companions() call - expect($parent->companions())->toHaveCount(2) - ->and($parent->companion(CompanionEntity::class))->toBe($companionViaEntity) - ->and($parent->companion(AnotherCompanionEntity::class))->toBe($companionViaHydrator); - } + expect($parent->companions())->toHaveCount(2) + ->and($parent->companion(CompanionEntity::class))->toBe($companionViaEntity) + ->and($parent->companion(AnotherCompanionEntity::class))->toBe($companionViaHydrator); + }, ); diff --git a/packages/database/tests/KnownDriversValidationTest.php b/packages/database/tests/KnownDriversValidationTest.php index 3f32e2a1..8f45e47d 100644 --- a/packages/database/tests/KnownDriversValidationTest.php +++ b/packages/database/tests/KnownDriversValidationTest.php @@ -11,7 +11,7 @@ 'skeleton suggest block contains all database drivers', function () use ($knownDriversPath, $skeletonComposerPath): void { KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath); - } + }, ); test('every database driver follows marko slash prefix pattern', function () use ($knownDriversPath): void { diff --git a/packages/database/tests/Schema/SchemaRegistryTest.php b/packages/database/tests/Schema/SchemaRegistryTest.php index f321f9bc..e6a12d0f 100644 --- a/packages/database/tests/Schema/SchemaRegistryTest.php +++ b/packages/database/tests/Schema/SchemaRegistryTest.php @@ -299,38 +299,38 @@ 'updates the EntityMetadataFactory cache so that a subsequent parse(parentClass) returns metadata with extenders populated', function (): void { $this->registry->registerEntities([ProductEntity::class, ProductExtenderEntity::class]); - + $cachedMetadata = $this->metadataFactory->parse(ProductEntity::class); - + expect($cachedMetadata->extenders)->toContain(ProductExtenderEntity::class); - } + }, ); it( 'handles registration order independence when an extender appears before its parent in the input array', function (): void { // Extender listed first, parent second — must still merge correctly - $this->registry->registerEntities([ProductSecondExtenderEntity::class, ProductEntity::class]); - + $this->registry->registerEntities([ProductSecondExtenderEntity::class, ProductEntity::class]); + $table = $this->registry->getTable('products'); $columnNames = array_map(fn ($c) => $c->name, $table->columns); - + expect($table)->not->toBeNull() ->and($columnNames)->toContain('barcode'); - } + }, ); it( 'includes a discovered extender from EntityDiscovery in the merged table (regression test for discovery integration)', function (): void { // Both ProductEntity and ProductExtenderEntity extend Entity with #[Table], - // so EntityDiscovery would find both. Simulate by passing both to registerEntities. - $this->registry->registerEntities([ProductEntity::class, ProductExtenderEntity::class]); - + // so EntityDiscovery would find both. Simulate by passing both to registerEntities. + $this->registry->registerEntities([ProductEntity::class, ProductExtenderEntity::class]); + $table = $this->registry->getTable('products'); - + expect($table)->not->toBeNull() ->and($table->columns)->toHaveCount(3) ->and($this->registry->getEntityClass('products'))->toBe(ProductEntity::class); - } + }, ); diff --git a/packages/devai/src/Commands/UpdateCommand.php b/packages/devai/src/Commands/UpdateCommand.php index afa48e32..1a605dc2 100644 --- a/packages/devai/src/Commands/UpdateCommand.php +++ b/packages/devai/src/Commands/UpdateCommand.php @@ -24,8 +24,7 @@ public function __construct( public function execute( Input $input, Output $output, - ): int - { + ): int { $projectRoot = (string) getcwd(); $marker = $projectRoot . '/.marko/devai.json'; diff --git a/packages/devai/src/Process/ConfirmationPrompterInterface.php b/packages/devai/src/Process/ConfirmationPrompterInterface.php index 181d9075..651989f8 100644 --- a/packages/devai/src/Process/ConfirmationPrompterInterface.php +++ b/packages/devai/src/Process/ConfirmationPrompterInterface.php @@ -8,5 +8,8 @@ interface ConfirmationPrompterInterface { public function isInteractive(): bool; - public function confirm(string $question, bool $default): bool; + public function confirm( + string $question, + bool $default, + ): bool; } diff --git a/packages/devai/tests/Helpers.php b/packages/devai/tests/Helpers.php index 06c01e2e..b62f3860 100644 --- a/packages/devai/tests/Helpers.php +++ b/packages/devai/tests/Helpers.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Marko\CodeIndexer\Module\ModuleWalker; +use Marko\CodeIndexer\ValueObject\ModuleInfo; use Marko\DevAi\Installation\InstallationContext; use Marko\DevAi\Process\CommandRunnerInterface; use Marko\DevAi\ValueObject\GuidelinesContent; @@ -45,7 +46,8 @@ function devaiRemoveDir(string $dir): void */ function devaiRunner(bool $onPath = false, string $listOutput = ''): CommandRunnerInterface { - return new class ($onPath, $listOutput) implements CommandRunnerInterface { + return new class ($onPath, $listOutput) implements CommandRunnerInterface + { /** @var list}> */ public array $calls = []; @@ -57,8 +59,7 @@ public function __construct( public function run( string $command, array $args = [], - ): array - { + ): array { $this->calls[] = ['command' => $command, 'args' => $args]; if (($args[0] ?? '') === 'mcp' && ($args[1] ?? '') === 'list') { return ['exitCode' => 0, 'stdout' => $this->listOutput, 'stderr' => '']; @@ -106,12 +107,13 @@ function devaiContext( * ModuleWalkerInterface was removed (#97); doubles now extend the concrete * ModuleWalker and override walk(). * - * @param list<\Marko\CodeIndexer\ValueObject\ModuleInfo> $modules + * @param list $modules */ function devaiWalker(array $modules = []): ModuleWalker { - return new class ($modules) extends ModuleWalker { - /** @param list<\Marko\CodeIndexer\ValueObject\ModuleInfo> $modules */ + return new class ($modules) extends ModuleWalker + { + /** @param list $modules */ public function __construct(private array $modules) {} public function walk(): array diff --git a/packages/devai/tests/Integration/ClaudeCodeInstallMonorepoTest.php b/packages/devai/tests/Integration/ClaudeCodeInstallMonorepoTest.php index f4cbefc6..2996d43f 100644 --- a/packages/devai/tests/Integration/ClaudeCodeInstallMonorepoTest.php +++ b/packages/devai/tests/Integration/ClaudeCodeInstallMonorepoTest.php @@ -35,8 +35,7 @@ public function __construct( public function run( string $cmd, array $args = [], - ): array - { + ): array { $this->calls[] = [$cmd, $args]; if ($cmd === 'claude' && ($args[0] ?? '') === 'mcp' && ($args[1] ?? '') === 'list') { return ['exitCode' => 0, 'stdout' => $this->listOutput, 'stderr' => '']; @@ -99,8 +98,7 @@ public function __construct(public string $listOutput) {} public function run( string $cmd, array $args = [], - ): array - { + ): array { $this->calls[] = [$cmd, $args]; if ($cmd === 'claude' && ($args[0] ?? '') === 'mcp' && ($args[1] ?? '') === 'list') { return ['exitCode' => 0, 'stdout' => $this->listOutput, 'stderr' => '']; @@ -184,12 +182,12 @@ function integMonorepoRunInstall( 'monorepo fixture (with stub packages/claude-plugins) produces settings.json with the path/local source shape per Task 001', function (): void { integMonorepoRunInstall($this->root); - + $data = json_decode((string) file_get_contents($this->root . '/.claude/settings.json'), true); $source = $data['extraKnownMarketplaces']['marko']['source']; expect($source['source'])->toBe('local') ->and($source['path'])->toBe('.'); - } + }, ); it('monorepo install still creates AGENTS.md at the project root', function (): void { @@ -258,7 +256,7 @@ function (): void { try { integMonorepoRunInstall($externalRoot); - + $data = json_decode((string) file_get_contents($externalRoot . '/.claude/settings.json'), true); $source = $data['extraKnownMarketplaces']['marko']['source']; expect($source['source'])->toBe('github') @@ -266,7 +264,7 @@ function (): void { } finally { integMonorepoRemoveTempDir($externalRoot); } - } + }, ); // --------------------------------------------------------------------------- @@ -289,9 +287,9 @@ function (): void { $runner = integMonorepoRunnerWithLsp(intelephenseOnPath: false, npmOnPath: true); $orchestrator = integMonorepoOrchestrator($runner); $ctx = new InstallationContext(selectedAgents: ['claude-code'], skipLspDeps: false); - + $orchestrator->install($ctx, $this->root); - + $npmInstallCalls = array_filter( $runner->calls, fn ($call) => $call[0] === 'npm' @@ -299,7 +297,7 @@ function (): void { && in_array('intelephense', $call[1], true), ); expect(array_values($npmInstallCalls))->not->toBeEmpty(); - } + }, ); it('skips intelephense installation when --skip-lsp-deps is passed', function (): void { @@ -323,17 +321,17 @@ function (): void { $runner = integMonorepoRunnerWithLsp(intelephenseOnPath: false, npmOnPath: true); $orchestrator = integMonorepoOrchestrator($runner); $ctx = new InstallationContext(selectedAgents: ['claude-code'], skipLspDeps: true); - + $orchestrator->install($ctx, $this->root); - + $settingsPath = $this->root . '/.claude/settings.json'; expect(file_exists($settingsPath))->toBeTrue(); - + $data = json_decode((string) file_get_contents($settingsPath), true); expect($data['extraKnownMarketplaces'])->toHaveKey('marko') ->and($data['enabledPlugins']['marko-skills@marko'])->toBeTrue() ->and($data['enabledPlugins']['marko-lsp@marko'])->toBeTrue() ->and($data['enabledPlugins']['marko-mcp@marko'])->toBeTrue(); - } + }, ); }); diff --git a/packages/devai/tests/Unit/Agents/ClaudeCodeAgentTest.php b/packages/devai/tests/Unit/Agents/ClaudeCodeAgentTest.php index a51e1cec..80a664c0 100644 --- a/packages/devai/tests/Unit/Agents/ClaudeCodeAgentTest.php +++ b/packages/devai/tests/Unit/Agents/ClaudeCodeAgentTest.php @@ -182,12 +182,15 @@ function (): void { }, ); - it('writes CLAUDE.md instructing the agent to trust its own writes rather than using introspection tools to confirm scaffolding', function (): void { - $this->agent->install(devaiContext('body'), $this->root); + it( + 'writes CLAUDE.md instructing the agent to trust its own writes rather than using introspection tools to confirm scaffolding', + function (): void { + $this->agent->install(devaiContext('body'), $this->root); - $claudeMd = (string) file_get_contents($this->root . '/CLAUDE.md'); - expect($claudeMd)->toContain('introspection tools are for discovering pre-existing code'); - }); + $claudeMd = (string) file_get_contents($this->root . '/CLAUDE.md'); + expect($claudeMd)->toContain('introspection tools are for discovering pre-existing code'); + }, + ); }); // --------------------------------------------------------------------------- diff --git a/packages/devai/tests/Unit/Installation/IntelephenseEnsurerTest.php b/packages/devai/tests/Unit/Installation/IntelephenseEnsurerTest.php index 8e05402b..70534b65 100644 --- a/packages/devai/tests/Unit/Installation/IntelephenseEnsurerTest.php +++ b/packages/devai/tests/Unit/Installation/IntelephenseEnsurerTest.php @@ -31,8 +31,7 @@ public function __construct( public function run( string $command, array $args = [], - ): array - { + ): array { $this->calls[] = [$command, $args]; if ($command === 'npm') { diff --git a/packages/devai/tests/Unit/Skills/SkillsDistributorTest.php b/packages/devai/tests/Unit/Skills/SkillsDistributorTest.php index 6a41d01c..8b69f30e 100644 --- a/packages/devai/tests/Unit/Skills/SkillsDistributorTest.php +++ b/packages/devai/tests/Unit/Skills/SkillsDistributorTest.php @@ -257,118 +257,118 @@ function makeSkillDir(string $base, string $skillName, array $files = []): void 'CodexAgent installer reads skill source from packages/claude-plugins/plugins/marko-skills/skills/ (not the legacy path)', function (): void { $projectRoot = dirname(dirname(__DIR__, 3), 2); - + $walker = makeWalker([]); $distributor = new SkillsDistributor($walker, $projectRoot); $bundles = $distributor->collect(); - + $allSkillKeys = []; foreach ($bundles as $bundle) { $allSkillKeys = array_merge($allSkillKeys, array_keys($bundle->skills)); } - + // Skills must come from the new canonical path, not resources/ai/skills - expect($allSkillKeys)->toContain('create-module/SKILL.md') - ->and($allSkillKeys)->not->toContain('marko-create-module/SKILL.md'); - } + expect($allSkillKeys)->toContain('create-module/SKILL.md') + ->and($allSkillKeys)->not->toContain('marko-create-module/SKILL.md'); + }, ); it( 'CursorAgent installer reads skill source from packages/claude-plugins/plugins/marko-skills/skills/', function (): void { $projectRoot = dirname(dirname(__DIR__, 3), 2); - + $walker = makeWalker([]); $distributor = new SkillsDistributor($walker, $projectRoot); $bundles = $distributor->collect(); - + $allSkillKeys = []; foreach ($bundles as $bundle) { $allSkillKeys = array_merge($allSkillKeys, array_keys($bundle->skills)); } - + expect($allSkillKeys)->toContain('create-module/SKILL.md') ->and($allSkillKeys)->toContain('create-plugin/SKILL.md'); - } + }, ); it( 'CopilotAgent installer reads skill source from packages/claude-plugins/plugins/marko-skills/skills/', function (): void { $projectRoot = dirname(dirname(__DIR__, 3), 2); - + $walker = makeWalker([]); $distributor = new SkillsDistributor($walker, $projectRoot); $bundles = $distributor->collect(); - + $allSkillKeys = []; foreach ($bundles as $bundle) { $allSkillKeys = array_merge($allSkillKeys, array_keys($bundle->skills)); } - + expect($allSkillKeys)->toContain('create-module/SKILL.md') ->and($allSkillKeys)->toContain('create-plugin/SKILL.md'); - } + }, ); it( 'GeminiCliAgent installer reads skill source from packages/claude-plugins/plugins/marko-skills/skills/', function (): void { $projectRoot = dirname(dirname(__DIR__, 3), 2); - + $walker = makeWalker([]); $distributor = new SkillsDistributor($walker, $projectRoot); $bundles = $distributor->collect(); - + $allSkillKeys = []; foreach ($bundles as $bundle) { $allSkillKeys = array_merge($allSkillKeys, array_keys($bundle->skills)); } - + expect($allSkillKeys)->toContain('create-module/SKILL.md') ->and($allSkillKeys)->toContain('create-plugin/SKILL.md'); - } + }, ); it( 'JunieAgent installer reads skill source from packages/claude-plugins/plugins/marko-skills/skills/', function (): void { $projectRoot = dirname(dirname(__DIR__, 3), 2); - + $walker = makeWalker([]); $distributor = new SkillsDistributor($walker, $projectRoot); $bundles = $distributor->collect(); - + $allSkillKeys = []; foreach ($bundles as $bundle) { $allSkillKeys = array_merge($allSkillKeys, array_keys($bundle->skills)); } - + expect($allSkillKeys)->toContain('create-module/SKILL.md') ->and($allSkillKeys)->toContain('create-plugin/SKILL.md'); - } + }, ); it( 'each agent copies the entire skill directory (SKILL.md plus assets/ and references/) so template references resolve', function (): void { $projectRoot = dirname(dirname(__DIR__, 3), 2); - + $walker = makeWalker([]); $distributor = new SkillsDistributor($walker, $projectRoot); $bundles = $distributor->collect(); - + $allSkillKeys = []; foreach ($bundles as $bundle) { $allSkillKeys = array_merge($allSkillKeys, array_keys($bundle->skills)); } - + // Assets from create-module are bundled - expect($allSkillKeys)->toContain('create-module/SKILL.md') - ->and($allSkillKeys)->toContain('create-module/assets/module.php.tmpl') - ->and($allSkillKeys)->toContain('create-plugin/SKILL.md') - ->and($allSkillKeys)->toContain('create-plugin/assets/PluginClass.php.tmpl'); - } + expect($allSkillKeys)->toContain('create-module/SKILL.md') + ->and($allSkillKeys)->toContain('create-module/assets/module.php.tmpl') + ->and($allSkillKeys)->toContain('create-plugin/SKILL.md') + ->and($allSkillKeys)->toContain('create-plugin/assets/PluginClass.php.tmpl'); + }, ); it( @@ -376,9 +376,9 @@ function (): void { function (): void { $devaiPackageRoot = dirname(__DIR__, 3); $legacySkillsDir = $devaiPackageRoot . '/resources/ai/skills'; - + expect(is_dir($legacySkillsDir))->toBeFalse(); - } + }, ); it('no duplicate skill content exists across the codebase — grep verification', function (): void { @@ -391,7 +391,7 @@ function (): void { function (): void { $srcDir = dirname(__DIR__, 3) . '/src'; $found = []; - + $iter = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($srcDir, RecursiveDirectoryIterator::SKIP_DOTS), ); @@ -401,21 +401,21 @@ function (): void { } $content = (string) file_get_contents($file->getPathname()); // The only permitted occurrence is the MODULE_SKILLS_REL_PATH constant in SkillsDistributor - // (which scans third-party modules). Any other file having this string is a legacy reference. - $occurrences = substr_count($content, 'resources/ai/skills'); + // (which scans third-party modules). Any other file having this string is a legacy reference. + $occurrences = substr_count($content, 'resources/ai/skills'); if ($occurrences === 0) { continue; } // SkillsDistributor is allowed exactly one occurrence (the MODULE_SKILLS_REL_PATH constant) - $basename = basename($file->getPathname()); + $basename = basename($file->getPathname()); if ($basename === 'SkillsDistributor.php' && $occurrences === 1) { continue; } $found[] = $file->getPathname(); } - + expect($found)->toBe([]); - } + }, ); it('skips a skill directory missing SKILL.md and records a warning', function (): void { diff --git a/packages/devai/tests/Unit/Writing/GuidelinesWriterTest.php b/packages/devai/tests/Unit/Writing/GuidelinesWriterTest.php index cc79d991..14643a37 100644 --- a/packages/devai/tests/Unit/Writing/GuidelinesWriterTest.php +++ b/packages/devai/tests/Unit/Writing/GuidelinesWriterTest.php @@ -127,25 +127,28 @@ devaiRemoveDir($tmpDir); }); -it('returns skipped and leaves the file untouched when a begin marker exists but the end marker is missing', function (): void { - $tmpDir = devaiTempDir(); - $path = $tmpDir . '/GUIDELINES.md'; +it( + 'returns skipped and leaves the file untouched when a begin marker exists but the end marker is missing', + function (): void { + $tmpDir = devaiTempDir(); + $path = $tmpDir . '/GUIDELINES.md'; - $malformed = "# Header\n" . GuidelinesWriter::MARKER_BEGIN . "\nOrphaned content with no end marker.\n"; - file_put_contents($path, $malformed); + $malformed = "# Header\n" . GuidelinesWriter::MARKER_BEGIN . "\nOrphaned content with no end marker.\n"; + file_put_contents($path, $malformed); - // Drain any prior notices - GuidelinesWriter::takeNotices(); + // Drain any prior notices + GuidelinesWriter::takeNotices(); - $outcome = GuidelinesWriter::write($path, 'New content'); + $outcome = GuidelinesWriter::write($path, 'New content'); - $contents = (string) file_get_contents($path); + $contents = (string) file_get_contents($path); - expect($outcome)->toBe(WriteOutcome::SkippedNoMarkers) - ->and($contents)->toBe($malformed); + expect($outcome)->toBe(WriteOutcome::SkippedNoMarkers) + ->and($contents)->toBe($malformed); - devaiRemoveDir($tmpDir); -}); + devaiRemoveDir($tmpDir); + }, +); it('embeds the marko devai:update regenerate hint inside the wrapped region', function (): void { $tmpDir = devaiTempDir(); @@ -157,7 +160,11 @@ $beginPos = strpos($contents, GuidelinesWriter::MARKER_BEGIN); $endPos = strpos($contents, GuidelinesWriter::MARKER_END); - $wrappedRegion = substr($contents, (int) $beginPos, (int) $endPos - (int) $beginPos + strlen(GuidelinesWriter::MARKER_END)); + $wrappedRegion = substr( + $contents, + (int) $beginPos, + (int) $endPos - (int) $beginPos + strlen(GuidelinesWriter::MARKER_END), + ); expect($wrappedRegion)->toContain('marko devai:update'); diff --git a/packages/devserver/src/Exceptions/DevServerException.php b/packages/devserver/src/Exceptions/DevServerException.php index 4009cf98..db1bf9c0 100644 --- a/packages/devserver/src/Exceptions/DevServerException.php +++ b/packages/devserver/src/Exceptions/DevServerException.php @@ -11,8 +11,7 @@ class DevServerException extends MarkoException public static function processFailedToStart( string $name, string $command, - ): self - { + ): self { return new self( message: "Failed to start process '$name' with command: $command", context: 'While starting development services', diff --git a/packages/docs-fts/src/Indexing/FtsIndexBuilder.php b/packages/docs-fts/src/Indexing/FtsIndexBuilder.php index 8414c0a2..53c30092 100644 --- a/packages/docs-fts/src/Indexing/FtsIndexBuilder.php +++ b/packages/docs-fts/src/Indexing/FtsIndexBuilder.php @@ -21,7 +21,7 @@ public function build(string $outputPath): void if ($pages === []) { throw DocsException::searchFailed( - 'No pages found in MarkdownRepository — check docs-markdown is installed' + 'No pages found in MarkdownRepository — check docs-markdown is installed', ); } @@ -55,7 +55,7 @@ public function build(string $outputPath): void $insertFts = $pdo->prepare('INSERT INTO docs_fts (page_id, title, content) VALUES (:id, :title, :content)'); $insertMeta = $pdo->prepare( - 'INSERT INTO docs_meta (page_id, url, section, title, last_updated) VALUES (:id, :url, :section, :title, :ts)' + 'INSERT INTO docs_meta (page_id, url, section, title, last_updated) VALUES (:id, :url, :section, :title, :ts)', ); $pdo->beginTransaction(); @@ -82,8 +82,7 @@ public function build(string $outputPath): void private function extractTitle( string $markdown, string $fallback, - ): string - { + ): string { if (preg_match('/^---\s*\n.*?title:\s*["\']?([^"\'\n]+)["\']?.*?\n---/s', $markdown, $m)) { return trim($m[1]); } diff --git a/packages/docs-fts/tests/Unit/Indexing/FtsIndexBuilderTest.php b/packages/docs-fts/tests/Unit/Indexing/FtsIndexBuilderTest.php index a84c8cfd..b4794e13 100644 --- a/packages/docs-fts/tests/Unit/Indexing/FtsIndexBuilderTest.php +++ b/packages/docs-fts/tests/Unit/Indexing/FtsIndexBuilderTest.php @@ -97,7 +97,7 @@ function tempDbPath(): string $pdo = new PDO('sqlite:' . $dbPath); $row = $pdo->query("SELECT page_id, url, section, title FROM docs_meta WHERE page_id = 'guide/install'")->fetch( - PDO::FETCH_ASSOC + PDO::FETCH_ASSOC, ); expect($row)->toBeArray() @@ -122,7 +122,7 @@ function tempDbPath(): string $pdo = new PDO('sqlite:' . $dbPath); $results = $pdo->query("SELECT page_id FROM docs_fts WHERE docs_fts MATCH 'install' ORDER BY rank")->fetchAll( - PDO::FETCH_COLUMN + PDO::FETCH_COLUMN, ); expect($results)->not->toBeEmpty() @@ -190,10 +190,10 @@ function tempDbPath(): string function (): void { $reflection = new ReflectionClass(BuildIndexCommand::class); $attributes = $reflection->getAttributes(Command::class); - + expect($attributes)->toHaveCount(1) ->and($attributes[0]->newInstance()->name)->toBe('docs-fts:build'); - + expect($reflection->implementsInterface(CommandInterface::class))->toBeTrue(); - } + }, ); diff --git a/packages/docs-fts/tests/Unit/SkeletonTest.php b/packages/docs-fts/tests/Unit/SkeletonTest.php index 7bed21af..6396bb6a 100644 --- a/packages/docs-fts/tests/Unit/SkeletonTest.php +++ b/packages/docs-fts/tests/Unit/SkeletonTest.php @@ -9,12 +9,12 @@ function (): void { $composerPath = dirname(__DIR__, 2) . '/composer.json'; $composer = json_decode((string) file_get_contents($composerPath), true); - + expect(file_exists($composerPath))->toBeTrue() ->and($composer['name'])->toBe('marko/docs-fts') ->and($composer['require'])->toHaveKey('marko/docs') ->and($composer['require'])->toHaveKey('marko/docs-markdown'); - } + }, ); it('declares ext-pdo_sqlite as a required PHP extension', function (): void { diff --git a/packages/docs-markdown/tests/Unit/AstroPipelineTest.php b/packages/docs-markdown/tests/Unit/AstroPipelineTest.php index 8390c49a..57f5bd64 100644 --- a/packages/docs-markdown/tests/Unit/AstroPipelineTest.php +++ b/packages/docs-markdown/tests/Unit/AstroPipelineTest.php @@ -14,12 +14,15 @@ expect(is_link($symlinkPath))->toBeTrue(); }); -it('produces a clean build of marko.build/docs after the path change', function () use ($symlinkPath, $packageDocs): void { - $target = readlink($symlinkPath); - $resolvedTarget = realpath(dirname($symlinkPath) . '/' . $target); - - expect($resolvedTarget)->toBe(realpath($packageDocs)); -}); +it( + 'produces a clean build of marko.build/docs after the path change', + function () use ($symlinkPath, $packageDocs): void { + $target = readlink($symlinkPath); + $resolvedTarget = realpath(dirname($symlinkPath) . '/' . $target); + + expect($resolvedTarget)->toBe(realpath($packageDocs)); + }, +); it('renders the same pages count as before the migration', function () use ($symlinkPath): void { // Count .md and .mdx files reachable through the symlinked source. diff --git a/packages/encryption/tests/KnownDriversValidationTest.php b/packages/encryption/tests/KnownDriversValidationTest.php index 290c0eb7..80afc060 100644 --- a/packages/encryption/tests/KnownDriversValidationTest.php +++ b/packages/encryption/tests/KnownDriversValidationTest.php @@ -11,7 +11,7 @@ 'skeleton suggest block contains all encryption drivers', function () use ($knownDriversPath, $skeletonComposerPath) { KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath); - } + }, ); test('every encryption driver follows marko slash prefix pattern', function () use ($knownDriversPath) { diff --git a/packages/errors-advanced/tests/Unit/UrlLinkificationTest.php b/packages/errors-advanced/tests/Unit/UrlLinkificationTest.php index 68758869..23fb0d07 100644 --- a/packages/errors-advanced/tests/Unit/UrlLinkificationTest.php +++ b/packages/errors-advanced/tests/Unit/UrlLinkificationTest.php @@ -175,12 +175,12 @@ function (): void { [$formatter, $report] = createFormatterWithReport( message: 'Visit www.example.com for help', ); - + $output = $formatter->format($report); - + expect($output)->not->toContain('toBeEmpty( - 'These require-dev constraints are not self.version: ' . implode(', ', $violations) + 'These require-dev constraints are not self.version: ' . implode(', ', $violations), ); }); @@ -91,7 +91,7 @@ function getPackageComposerFiles(): array } expect($violations)->toBeEmpty( - 'devserver has non-self.version marko/* constraints: ' . implode(', ', $violations) + 'devserver has non-self.version marko/* constraints: ' . implode(', ', $violations), ); }); @@ -136,7 +136,7 @@ function getPackageComposerFiles(): array } expect($violations)->toBeEmpty( - 'These non-marko dependencies incorrectly use self.version: ' . implode(', ', $violations) + 'These non-marko dependencies incorrectly use self.version: ' . implode(', ', $violations), ); }); @@ -172,6 +172,6 @@ function getPackageComposerFiles(): array } expect($violations)->toBeEmpty( - 'Structural violations in package composer.json files: ' . implode(', ', $violations) + 'Structural violations in package composer.json files: ' . implode(', ', $violations), ); }); diff --git a/packages/framework/tests/RootComposerJsonTest.php b/packages/framework/tests/RootComposerJsonTest.php index fd928332..e6a9b56d 100644 --- a/packages/framework/tests/RootComposerJsonTest.php +++ b/packages/framework/tests/RootComposerJsonTest.php @@ -85,38 +85,38 @@ 'adds a require section entry for all 73 marko packages set to self.version', function () use ($rootComposer, $allPackages): void { expect($rootComposer)->toHaveKey('require'); - + foreach ($allPackages as $package) { expect($rootComposer['require'])->toHaveKey($package) ->and($rootComposer['require'][$package])->toBe('self.version'); } - } + }, ); it( 'does not have a replace section (path repos install as symlinks without it)', function () use ($rootComposer): void { expect($rootComposer)->not->toHaveKey('replace'); - } + }, ); it( 'adds repositories section with path repos for all 73 packages', function () use ($rootComposer, $allPackages): void { expect($rootComposer)->toHaveKey('repositories'); - + $repoUrls = array_column($rootComposer['repositories'], 'url'); - + foreach ($allPackages as $package) { $packageName = str_replace('marko/', '', $package); expect(in_array("packages/$packageName", $repoUrls, true))->toBeTrue(); } - + foreach ($rootComposer['repositories'] as $repo) { expect($repo)->toHaveKey('type') ->and($repo['type'])->toBe('path'); } - } + }, ); it('removes all manual PSR-4 autoload entries for marko packages', function () use ($rootComposer): void { @@ -135,18 +135,18 @@ function () use ($rootComposer, $allPackages): void { 'keeps autoload-dev entries for test namespaces (Composer does not merge autoload-dev from dependencies)', function () use ($rootComposer, $allPackages): void { // autoload-dev must remain in root: Composer only applies a package's autoload-dev - // when it is the root package, so test namespaces for all monorepo packages must - // be declared here to be discoverable when running the test suite. - expect($rootComposer)->toHaveKey('autoload-dev') - ->and($rootComposer['autoload-dev'])->toHaveKey('psr-4'); - + // when it is the root package, so test namespaces for all monorepo packages must + // be declared here to be discoverable when running the test suite. + expect($rootComposer)->toHaveKey('autoload-dev') + ->and($rootComposer['autoload-dev'])->toHaveKey('psr-4'); + $devPsr4 = $rootComposer['autoload-dev']['psr-4']; $hasAtLeastOneTestNamespace = array_any( array_keys($devPsr4), fn (string $ns): bool => str_ends_with($ns, 'Tests\\'), ); expect($hasAtLeastOneTestNamespace)->toBeTrue(); - } + }, ); it('removes the autoload files entry for packages/env/src/functions.php', function () use ($rootComposer): void { @@ -164,7 +164,7 @@ function () use ($rootComposer): void { ->and($rootComposer['require'])->toHaveKey('ext-fileinfo') ->and($rootComposer['require'])->toHaveKey('ext-gd') ->and($rootComposer['require'])->toHaveKey('ext-imagick'); - + $expectedDevPackages = [ 'amphp/postgres', 'amphp/redis', @@ -178,11 +178,11 @@ function () use ($rootComposer): void { 'slevomat/coding-standard', 'squizlabs/php_codesniffer', ]; - + foreach ($expectedDevPackages as $package) { expect($rootComposer['require-dev'])->toHaveKey($package); } - } + }, ); it('preserves scripts, config, and other root-level settings', function () use ($rootComposer): void { @@ -202,7 +202,7 @@ function () use ($rootComposer): void { function () use ($rootComposer): void { expect($rootComposer)->toHaveKey('minimum-stability') ->and($rootComposer['minimum-stability'])->toBe('stable'); - } + }, ); it('keeps prefer-stable as true', function () use ($rootComposer): void { diff --git a/packages/health/tests/Unit/FilesystemHealthCheckTest.php b/packages/health/tests/Unit/FilesystemHealthCheckTest.php index 67244f53..88d89157 100644 --- a/packages/health/tests/Unit/FilesystemHealthCheckTest.php +++ b/packages/health/tests/Unit/FilesystemHealthCheckTest.php @@ -48,8 +48,7 @@ public function write( string $path, string $contents, array $options = [], - ): bool - { + ): bool { $this->written[$path] = $contents; return true; @@ -59,16 +58,14 @@ public function writeStream( string $path, mixed $resource, array $options = [], - ): bool - { + ): bool { return true; } public function append( string $path, string $contents, - ): bool - { + ): bool { return true; } @@ -82,16 +79,14 @@ public function delete(string $path): bool public function copy( string $source, string $destination, - ): bool - { + ): bool { return true; } public function move( string $source, string $destination, - ): bool - { + ): bool { return true; } @@ -128,8 +123,7 @@ public function deleteDirectory(string $path): bool public function setVisibility( string $path, string $visibility, - ): bool - { + ): bool { return true; } @@ -186,8 +180,7 @@ public function write( string $path, string $contents, array $options = [], - ): bool - { + ): bool { throw new RuntimeException('Filesystem not writable'); } @@ -195,16 +188,14 @@ public function writeStream( string $path, mixed $resource, array $options = [], - ): bool - { + ): bool { return false; } public function append( string $path, string $contents, - ): bool - { + ): bool { return false; } @@ -216,16 +207,14 @@ public function delete(string $path): bool public function copy( string $source, string $destination, - ): bool - { + ): bool { return false; } public function move( string $source, string $destination, - ): bool - { + ): bool { return false; } @@ -262,8 +251,7 @@ public function deleteDirectory(string $path): bool public function setVisibility( string $path, string $visibility, - ): bool - { + ): bool { return false; } diff --git a/packages/http/tests/KnownDriversValidationTest.php b/packages/http/tests/KnownDriversValidationTest.php index c111eaee..f24614f7 100644 --- a/packages/http/tests/KnownDriversValidationTest.php +++ b/packages/http/tests/KnownDriversValidationTest.php @@ -9,9 +9,9 @@ test( 'skeleton suggest block contains all http drivers', - fn () => KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath) + fn () => KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath), ); test( 'every http driver follows marko slash prefix pattern', - fn () => KnownDriversValidator::assertDocsUrlsResolveToValidPattern($knownDriversPath) + fn () => KnownDriversValidator::assertDocsUrlsResolveToValidPattern($knownDriversPath), ); diff --git a/packages/inertia/tests/KnownDriversValidationTest.php b/packages/inertia/tests/KnownDriversValidationTest.php index d2d84e9c..374e742b 100644 --- a/packages/inertia/tests/KnownDriversValidationTest.php +++ b/packages/inertia/tests/KnownDriversValidationTest.php @@ -11,7 +11,7 @@ 'skeleton suggest block contains all inertia drivers', function () use ($knownDriversPath, $skeletonComposerPath) { KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath); - } + }, ); test('every inertia driver follows marko slash prefix pattern', function () use ($knownDriversPath) { diff --git a/packages/layout/src/ComponentCollector.php b/packages/layout/src/ComponentCollector.php index 96bec78e..d818c7a8 100644 --- a/packages/layout/src/ComponentCollector.php +++ b/packages/layout/src/ComponentCollector.php @@ -27,8 +27,7 @@ public function __construct( public function collect( array $classNames, string $handle, - ): ComponentCollection - { + ): ComponentCollection { $collection = new ComponentCollection(); foreach ($classNames as $className) { @@ -142,8 +141,7 @@ private function resolveHandles(string|array $handle): array private function resolveClassReferenceHandle( string $controllerClass, string $action, - ): ?string - { + ): ?string { foreach ($this->routeCollection->all() as $route) { if ($route->controller === $controllerClass && $route->action === $action) { return $this->handleResolver->generate($route->path, $controllerClass, $action); diff --git a/packages/layout/src/ComponentDataResolver.php b/packages/layout/src/ComponentDataResolver.php index 906f9788..b98ae9cc 100644 --- a/packages/layout/src/ComponentDataResolver.php +++ b/packages/layout/src/ComponentDataResolver.php @@ -21,8 +21,7 @@ public function resolve( object $component, array $routeParams, Request $request, - ): array - { + ): array { if (!method_exists($component, 'data')) { return []; } @@ -54,8 +53,7 @@ public function resolve( private function castToType( mixed $value, ?ReflectionType $type, - ): mixed - { + ): mixed { if (!$type instanceof ReflectionNamedType) { return $value; } diff --git a/packages/layout/src/DiscoveringComponentCollector.php b/packages/layout/src/DiscoveringComponentCollector.php index ee9ecc86..b80da6a8 100644 --- a/packages/layout/src/DiscoveringComponentCollector.php +++ b/packages/layout/src/DiscoveringComponentCollector.php @@ -30,8 +30,7 @@ public function __construct( public function collect( array $classNames, string $handle, - ): ComponentCollection - { + ): ComponentCollection { $discovered = $this->discoverComponentClasses(); $merged = array_values(array_unique(array_merge($discovered, $classNames))); diff --git a/packages/layout/src/Exceptions/AmbiguousSortOrderException.php b/packages/layout/src/Exceptions/AmbiguousSortOrderException.php index 8513d20d..853be530 100644 --- a/packages/layout/src/Exceptions/AmbiguousSortOrderException.php +++ b/packages/layout/src/Exceptions/AmbiguousSortOrderException.php @@ -13,8 +13,7 @@ public static function forComponents( string $slot, int $sortOrder, array $components, - ): self - { + ): self { $componentList = implode(', ', $components); return new self( diff --git a/packages/layout/src/Exceptions/CircularSlotException.php b/packages/layout/src/Exceptions/CircularSlotException.php index 127d9617..1f503e87 100644 --- a/packages/layout/src/Exceptions/CircularSlotException.php +++ b/packages/layout/src/Exceptions/CircularSlotException.php @@ -12,8 +12,7 @@ class CircularSlotException extends LayoutException public static function forSlot( string $slot, array $chain, - ): self - { + ): self { $chainPath = implode(' -> ', $chain); return new self( diff --git a/packages/layout/src/Exceptions/DuplicateComponentException.php b/packages/layout/src/Exceptions/DuplicateComponentException.php index 4d55b932..4162b396 100644 --- a/packages/layout/src/Exceptions/DuplicateComponentException.php +++ b/packages/layout/src/Exceptions/DuplicateComponentException.php @@ -10,8 +10,7 @@ public static function forComponent( string $name, string $moduleA, string $moduleB, - ): self - { + ): self { return new self( message: "Component '$name' is registered in multiple modules.", context: "Component '$name' is registered in both '$moduleA' and '$moduleB'.", diff --git a/packages/layout/src/Exceptions/SlotNotFoundException.php b/packages/layout/src/Exceptions/SlotNotFoundException.php index 834385a2..cf2855ae 100644 --- a/packages/layout/src/Exceptions/SlotNotFoundException.php +++ b/packages/layout/src/Exceptions/SlotNotFoundException.php @@ -9,8 +9,7 @@ class SlotNotFoundException extends LayoutException public static function forSlot( string $slot, string $layout, - ): self - { + ): self { return new self( message: "Slot '$slot' not found in layout '$layout'.", context: "Attempted to fill slot '$slot' in layout '$layout' but the slot is not defined.", diff --git a/packages/layout/src/HandleResolver.php b/packages/layout/src/HandleResolver.php index 253b33e8..18757acf 100644 --- a/packages/layout/src/HandleResolver.php +++ b/packages/layout/src/HandleResolver.php @@ -10,8 +10,7 @@ public function generate( string $path, string $controllerClass, string $action, - ): string - { + ): string { $segments = explode('/', trim($path, '/')); $routePart = $segments[0] !== '' ? $segments[0] : 'index'; @@ -24,8 +23,7 @@ public function generate( public function matches( string $componentHandle, string $pageHandle, - ): bool - { + ): bool { if ($componentHandle === 'default') { return true; } diff --git a/packages/layout/src/LayoutProcessor.php b/packages/layout/src/LayoutProcessor.php index 97c9b71a..1763d601 100644 --- a/packages/layout/src/LayoutProcessor.php +++ b/packages/layout/src/LayoutProcessor.php @@ -127,8 +127,7 @@ private function renderSlot( private function detectCircularReferences( ComponentCollection $collection, array $topLevelSlots, - ): void - { + ): void { // Build a map: slot name -> sub-slots it leads to (via components in that slot) /** @var array> $slotToSubSlots */ $slotToSubSlots = []; diff --git a/packages/layout/src/LayoutResolver.php b/packages/layout/src/LayoutResolver.php index e03511f4..479f5f4f 100644 --- a/packages/layout/src/LayoutResolver.php +++ b/packages/layout/src/LayoutResolver.php @@ -26,8 +26,7 @@ public function resolve( string $controllerClass, string $method, - ): array - { + ): array { $classReflection = new ReflectionClass($controllerClass); $methodReflection = new ReflectionMethod($controllerClass, $method); diff --git a/packages/layout/tests/Exceptions/AmbiguousSortOrderExceptionTest.php b/packages/layout/tests/Exceptions/AmbiguousSortOrderExceptionTest.php index f10e3708..12c82bae 100644 --- a/packages/layout/tests/Exceptions/AmbiguousSortOrderExceptionTest.php +++ b/packages/layout/tests/Exceptions/AmbiguousSortOrderExceptionTest.php @@ -9,10 +9,10 @@ 'has an AmbiguousSortOrderException with a static factory method providing message, context, and suggestion', function (): void { $exception = AmbiguousSortOrderException::forComponents('header', 10, ['ComponentA', 'ComponentB']); - + expect($exception)->toBeInstanceOf(LayoutException::class) ->and($exception->getMessage())->toContain('header') ->and($exception->getContext())->not->toBeEmpty() ->and($exception->getSuggestion())->not->toBeEmpty(); - } + }, ); diff --git a/packages/layout/tests/Exceptions/CircularSlotExceptionTest.php b/packages/layout/tests/Exceptions/CircularSlotExceptionTest.php index deb97de0..9c935cc6 100644 --- a/packages/layout/tests/Exceptions/CircularSlotExceptionTest.php +++ b/packages/layout/tests/Exceptions/CircularSlotExceptionTest.php @@ -9,10 +9,10 @@ 'has a CircularSlotException with a static factory method providing message, context, and suggestion', function (): void { $exception = CircularSlotException::forSlot('main', ['main', 'nested', 'main']); - + expect($exception)->toBeInstanceOf(LayoutException::class) ->and($exception->getMessage())->toContain('main') ->and($exception->getContext())->not->toBeEmpty() ->and($exception->getSuggestion())->not->toBeEmpty(); - } + }, ); diff --git a/packages/layout/tests/Exceptions/ComponentNotFoundExceptionTest.php b/packages/layout/tests/Exceptions/ComponentNotFoundExceptionTest.php index 51b37d54..bb41cb24 100644 --- a/packages/layout/tests/Exceptions/ComponentNotFoundExceptionTest.php +++ b/packages/layout/tests/Exceptions/ComponentNotFoundExceptionTest.php @@ -9,10 +9,10 @@ 'has a ComponentNotFoundException with a static factory method providing message, context, and suggestion', function (): void { $exception = ComponentNotFoundException::forComponent('alert'); - + expect($exception)->toBeInstanceOf(LayoutException::class) ->and($exception->getMessage())->toContain('alert') ->and($exception->getContext())->not->toBeEmpty() ->and($exception->getSuggestion())->not->toBeEmpty(); - } + }, ); diff --git a/packages/layout/tests/Exceptions/DuplicateComponentExceptionTest.php b/packages/layout/tests/Exceptions/DuplicateComponentExceptionTest.php index 84d7e8c6..819468b8 100644 --- a/packages/layout/tests/Exceptions/DuplicateComponentExceptionTest.php +++ b/packages/layout/tests/Exceptions/DuplicateComponentExceptionTest.php @@ -9,10 +9,10 @@ 'has a DuplicateComponentException with a static factory method providing message, context, and suggestion', function (): void { $exception = DuplicateComponentException::forComponent('header', 'ModuleA', 'ModuleB'); - + expect($exception)->toBeInstanceOf(LayoutException::class) ->and($exception->getMessage())->toContain('header') ->and($exception->getContext())->not->toBeEmpty() ->and($exception->getSuggestion())->not->toBeEmpty(); - } + }, ); diff --git a/packages/layout/tests/Exceptions/LayoutNotFoundExceptionTest.php b/packages/layout/tests/Exceptions/LayoutNotFoundExceptionTest.php index 549d17fb..0ec94dbb 100644 --- a/packages/layout/tests/Exceptions/LayoutNotFoundExceptionTest.php +++ b/packages/layout/tests/Exceptions/LayoutNotFoundExceptionTest.php @@ -9,10 +9,10 @@ 'has a LayoutNotFoundException with a static factory method providing message, context, and suggestion', function (): void { $exception = LayoutNotFoundException::forLayout('admin'); - + expect($exception)->toBeInstanceOf(LayoutException::class) ->and($exception->getMessage())->toContain('admin') ->and($exception->getContext())->not->toBeEmpty() ->and($exception->getSuggestion())->not->toBeEmpty(); - } + }, ); diff --git a/packages/layout/tests/Exceptions/SlotNotFoundExceptionTest.php b/packages/layout/tests/Exceptions/SlotNotFoundExceptionTest.php index 0ea55b27..bed19075 100644 --- a/packages/layout/tests/Exceptions/SlotNotFoundExceptionTest.php +++ b/packages/layout/tests/Exceptions/SlotNotFoundExceptionTest.php @@ -9,10 +9,10 @@ 'has a SlotNotFoundException with a static factory method providing message, context, and suggestion', function (): void { $exception = SlotNotFoundException::forSlot('sidebar', 'main-layout'); - + expect($exception)->toBeInstanceOf(LayoutException::class) ->and($exception->getMessage())->toContain('sidebar') ->and($exception->getContext())->not->toBeEmpty() ->and($exception->getSuggestion())->not->toBeEmpty(); - } + }, ); diff --git a/packages/layout/tests/Unit/ComponentCollectorTest.php b/packages/layout/tests/Unit/ComponentCollectorTest.php index c0ab25e3..c1beffab 100644 --- a/packages/layout/tests/Unit/ComponentCollectorTest.php +++ b/packages/layout/tests/Unit/ComponentCollectorTest.php @@ -28,7 +28,7 @@ class OtherPageComponent {} #[Component( template: 'multi/component.phtml', slot: 'content', - handle: ['catalog_product_show', 'catalog_category_view'] + handle: ['catalog_product_show', 'catalog_category_view'], )] class MultiHandleComponent {} diff --git a/packages/layout/tests/Unit/LayoutProcessorNestedTest.php b/packages/layout/tests/Unit/LayoutProcessorNestedTest.php index 9301b559..9698fd86 100644 --- a/packages/layout/tests/Unit/LayoutProcessorNestedTest.php +++ b/packages/layout/tests/Unit/LayoutProcessorNestedTest.php @@ -35,7 +35,7 @@ class LpnFixtureRootComponent {} slot: 'content', handle: 'default', sortOrder: 10, - slots: ['tab.details', 'tab.reviews'] + slots: ['tab.details', 'tab.reviews'], )] class LpnFixtureTabsComponent {} @@ -64,7 +64,7 @@ class LpnFixtureNavComponent {} slot: 'main', handle: 'default', sortOrder: 10, - slots: ['body.sidebar', 'body.content'] + slots: ['body.sidebar', 'body.content'], )] class LpnFixtureBodyComponent {} @@ -80,7 +80,7 @@ class LpnFixtureMainContentComponent {} slot: 'content', handle: 'default', sortOrder: 10, - slots: ['tab.details'] + slots: ['tab.details'], )] class LpnFixtureDeepTabsComponent {} @@ -89,7 +89,7 @@ class LpnFixtureDeepTabsComponent {} slot: 'tab.details', handle: 'default', sortOrder: 10, - slots: ['detail.images', 'detail.description'] + slots: ['detail.images', 'detail.description'], )] class LpnFixtureDeepDetailsComponent {} @@ -100,7 +100,7 @@ class LpnFixtureDetailImagesComponent {} template: 'components/detail-description.html', slot: 'detail.description', handle: 'default', - sortOrder: 20 + sortOrder: 20, )] class LpnFixtureDetailDescriptionComponent {} @@ -114,7 +114,7 @@ class LpnFixtureDetailDescriptionComponent {} slot: 'content', handle: 'default', sortOrder: 10, - slots: ['cycle.y'] + slots: ['cycle.y'], )] class LpnFixtureCycleXComponent {} @@ -123,7 +123,7 @@ class LpnFixtureCycleXComponent {} slot: 'cycle.y', handle: 'default', sortOrder: 10, - slots: ['cycle.x'] + slots: ['cycle.x'], )] class LpnFixtureCycleYComponent {} @@ -141,8 +141,7 @@ public function __construct(private readonly ComponentCollection $stub) {} public function collect( array $classNames, string $handle, - ): ComponentCollection - { + ): ComponentCollection { return $this->stub; } @@ -162,16 +161,14 @@ public function __construct(private readonly mixed $renderFn) {} public function render( string $template, array $data = [], - ): Response - { + ): Response { return Response::html(($this->renderFn)($template, $data)); } public function renderToString( string $template, array $data = [], - ): string - { + ): string { return ($this->renderFn)($template, $data); } }; diff --git a/packages/layout/tests/Unit/LayoutProcessorTest.php b/packages/layout/tests/Unit/LayoutProcessorTest.php index c9b65467..f33741cf 100644 --- a/packages/layout/tests/Unit/LayoutProcessorTest.php +++ b/packages/layout/tests/Unit/LayoutProcessorTest.php @@ -96,8 +96,7 @@ public function __construct(private readonly ComponentCollection $stub) {} public function collect( array $classNames, string $handle, - ): ComponentCollection - { + ): ComponentCollection { return $this->stub; } @@ -118,16 +117,14 @@ public function __construct(private readonly mixed $renderFn) {} public function render( string $template, array $data = [], - ): Response - { + ): Response { return Response::html(($this->renderFn)($template, $data)); } public function renderToString( string $template, array $data = [], - ): string - { + ): string { return ($this->renderFn)($template, $data); } }; diff --git a/packages/layout/tests/Unit/Middleware/LayoutMiddlewareTest.php b/packages/layout/tests/Unit/Middleware/LayoutMiddlewareTest.php index 4b9548db..d9b9b6b2 100644 --- a/packages/layout/tests/Unit/Middleware/LayoutMiddlewareTest.php +++ b/packages/layout/tests/Unit/Middleware/LayoutMiddlewareTest.php @@ -72,8 +72,7 @@ public function __construct(private readonly ?MatchedRoute $stub) {} public function match( string $method, string $path, - ): ?MatchedRoute - { + ): ?MatchedRoute { return $this->stub; } }; diff --git a/packages/lsp/src/Features/AttributeFeature.php b/packages/lsp/src/Features/AttributeFeature.php index 86faf1d3..022326f9 100644 --- a/packages/lsp/src/Features/AttributeFeature.php +++ b/packages/lsp/src/Features/AttributeFeature.php @@ -17,8 +17,10 @@ public function __construct(private IndexCache $index) {} * * @return ?array{attribute: string, parameter: string, partial: string} */ - public function detectContext(string $lineText, int $col): ?array - { + public function detectContext( + string $lineText, + int $col, + ): ?array { $prefix = substr($lineText, 0, $col); // Match: #[AttributeName(paramName: 'partial' or paramName: ClassName @@ -41,8 +43,10 @@ public function detectContext(string $lineText, int $col): ?array * * @return list */ - public function complete(string $lineText, int $col): array - { + public function complete( + string $lineText, + int $col, + ): array { $ctx = $this->detectContext($lineText, $col); if ($ctx === null) { @@ -52,7 +56,9 @@ public function complete(string $lineText, int $col): array return match ([$ctx['attribute'], $ctx['parameter']]) { ['Observer', 'event'] => $this->completeEventClasses($ctx['partial']), ['Plugin', 'target'] => $this->completeAllClasses($ctx['partial']), - ['Get', ''], ['Post', ''], ['Put', ''], ['Patch', ''], ['Delete', ''] => $this->completeRoutePaths($ctx['partial']), + ['Get', ''], ['Post', ''], ['Put', ''], ['Patch', ''], ['Delete', ''] => $this->completeRoutePaths( + $ctx['partial'], + ), ['Middleware', ''] => $this->completeMiddlewareClasses($ctx['partial']), default => [], }; diff --git a/packages/lsp/src/Features/ConfigKeyFeature.php b/packages/lsp/src/Features/ConfigKeyFeature.php index 453d4005..83124341 100644 --- a/packages/lsp/src/Features/ConfigKeyFeature.php +++ b/packages/lsp/src/Features/ConfigKeyFeature.php @@ -20,8 +20,7 @@ public function __construct(private IndexCache $index) {} public function detectContext( string $lineText, int $col, - ): ?string - { + ): ?string { $prefix = substr($lineText, 0, $col); $pattern = '/->\s*(' . implode('|', self::CONFIG_GETTER_METHODS) . ')\s*\(\s*[\'"]([^\'"]*)$/'; @@ -146,8 +145,7 @@ public function diagnostics(string $documentText): array public function suggestSimilar( string $key, int $max = 3, - ): array - { + ): array { $candidates = array_map(fn (ConfigKeyEntry $e) => $e->key, $this->index->getConfigKeys()); $scored = array_map(fn (string $c) => ['key' => $c, 'distance' => levenshtein($key, $c)], $candidates); usort($scored, fn (array $a, array $b) => $a['distance'] <=> $b['distance']); diff --git a/packages/lsp/src/Features/TemplateFeature.php b/packages/lsp/src/Features/TemplateFeature.php index 99b0ccba..910e5920 100644 --- a/packages/lsp/src/Features/TemplateFeature.php +++ b/packages/lsp/src/Features/TemplateFeature.php @@ -20,7 +20,7 @@ public function complete(string $partial): array if ($partial !== '' && !str_starts_with($fullName, $partial) && !str_starts_with( $t->templateName, - $partial + $partial, )) { continue; } @@ -135,8 +135,7 @@ public function diagnostics(string $documentText): array public function suggestSimilar( string $template, int $max = 3, - ): array - { + ): array { $candidates = []; foreach ($this->index->getTemplates() as $t) { diff --git a/packages/lsp/src/Features/TranslationFeature.php b/packages/lsp/src/Features/TranslationFeature.php index 8f0a099e..384877f2 100644 --- a/packages/lsp/src/Features/TranslationFeature.php +++ b/packages/lsp/src/Features/TranslationFeature.php @@ -22,8 +22,7 @@ public function __construct( public function detectContext( string $lineText, int $col, - ): ?string - { + ): ?string { $prefix = substr($lineText, 0, $col); $pattern = '/->\s*(' . implode('|', self::TRANSLATOR_METHODS) . ')\s*\(\s*[\'"]([^\'"]*)$/'; @@ -178,8 +177,7 @@ public function diagnostics(string $documentText): array public function suggestSimilar( string $key, int $max = 3, - ): array - { + ): array { $candidates = []; foreach ($this->index->getTranslationKeys() as $entry) { diff --git a/packages/lsp/src/Server/DiagnosticsNotifier.php b/packages/lsp/src/Server/DiagnosticsNotifier.php index 85cd9104..ac073ca5 100644 --- a/packages/lsp/src/Server/DiagnosticsNotifier.php +++ b/packages/lsp/src/Server/DiagnosticsNotifier.php @@ -19,8 +19,10 @@ public function __construct(private LspProtocol $protocol) {} * * @param list> $diagnostics */ - public function publish(string $uri, array $diagnostics): void - { + public function publish( + string $uri, + array $diagnostics, + ): void { $stamped = array_map( fn (array $diag) => array_merge($diag, ['source' => self::SOURCE]), $diagnostics, diff --git a/packages/lsp/src/Server/DocumentStore.php b/packages/lsp/src/Server/DocumentStore.php index 8c53b0f1..0a38506a 100644 --- a/packages/lsp/src/Server/DocumentStore.php +++ b/packages/lsp/src/Server/DocumentStore.php @@ -12,16 +12,14 @@ class DocumentStore public function open( string $uri, string $text, - ): void - { + ): void { $this->documents[$uri] = $text; } public function update( string $uri, string $text, - ): void - { + ): void { $this->documents[$uri] = $text; } diff --git a/packages/lsp/src/Server/LspServer.php b/packages/lsp/src/Server/LspServer.php index a7c55ffd..2178be46 100644 --- a/packages/lsp/src/Server/LspServer.php +++ b/packages/lsp/src/Server/LspServer.php @@ -266,8 +266,10 @@ private function codeLensRequest(array $params): array * Aggregate diagnostics from all features and publish a single * textDocument/publishDiagnostics notification for the given URI. */ - private function publishDiagnosticsFor(string $uri, string $text): void - { + private function publishDiagnosticsFor( + string $uri, + string $text, + ): void { if ($this->notifier === null) { return; } diff --git a/packages/lsp/tests/Unit/Features/TemplateFeatureTest.php b/packages/lsp/tests/Unit/Features/TemplateFeatureTest.php index 1377e51f..217b60b1 100644 --- a/packages/lsp/tests/Unit/Features/TemplateFeatureTest.php +++ b/packages/lsp/tests/Unit/Features/TemplateFeatureTest.php @@ -25,6 +25,7 @@ public function getTemplates(): array function makeTemplateEntry(string $moduleName, string $templateName, string $absolutePath = '', string $extension = 'php'): TemplateEntry { $absolutePath = $absolutePath ?: "/modules/$moduleName/resources/views/$templateName.$extension"; + return new TemplateEntry($moduleName, $templateName, $absolutePath, $extension); } diff --git a/packages/lsp/tests/Unit/Server/LspServerTest.php b/packages/lsp/tests/Unit/Server/LspServerTest.php index 6e3c6b5a..9cfd1ebd 100644 --- a/packages/lsp/tests/Unit/Server/LspServerTest.php +++ b/packages/lsp/tests/Unit/Server/LspServerTest.php @@ -2,12 +2,13 @@ declare(strict_types=1); -use Marko\CodeIndexer\Cache\IndexCache; use Marko\CodeIndexer\Attributes\AttributeParser; +use Marko\CodeIndexer\Cache\IndexCache; use Marko\CodeIndexer\Config\ConfigScanner; use Marko\CodeIndexer\Module\ModuleWalker; -use Marko\CodeIndexer\Views\TemplateScanner; use Marko\CodeIndexer\Translations\TranslationScanner; +use Marko\CodeIndexer\ValueObject\ModuleInfo; +use Marko\CodeIndexer\Views\TemplateScanner; use Marko\Core\Path\ProjectPaths; use Marko\Lsp\Features\AttributeFeature; use Marko\Lsp\Features\CodeLensFeature; @@ -20,7 +21,8 @@ function makeNullIndexCache(): IndexCache { - $emptyWalker = new class () extends ModuleWalker { + $emptyWalker = new class () extends ModuleWalker + { public function __construct() {} public function walk(): array @@ -28,46 +30,50 @@ public function walk(): array return []; } }; - $emptyAttributeParser = new class () extends AttributeParser { - public function observers(\Marko\CodeIndexer\ValueObject\ModuleInfo $m): array + $emptyAttributeParser = new class () extends AttributeParser + { + public function observers(ModuleInfo $m): array { return []; } - public function plugins(\Marko\CodeIndexer\ValueObject\ModuleInfo $m): array + public function plugins(ModuleInfo $m): array { return []; } - public function preferences(\Marko\CodeIndexer\ValueObject\ModuleInfo $m): array + public function preferences(ModuleInfo $m): array { return []; } - public function commands(\Marko\CodeIndexer\ValueObject\ModuleInfo $m): array + public function commands(ModuleInfo $m): array { return []; } - public function routes(\Marko\CodeIndexer\ValueObject\ModuleInfo $m): array + public function routes(ModuleInfo $m): array { return []; } }; - $emptyConfigScanner = new class () extends ConfigScanner { - public function scan(\Marko\CodeIndexer\ValueObject\ModuleInfo $m): array + $emptyConfigScanner = new class () extends ConfigScanner + { + public function scan(ModuleInfo $m): array { return []; } }; - $emptyTemplateScanner = new class () extends TemplateScanner { - public function scan(\Marko\CodeIndexer\ValueObject\ModuleInfo $m): array + $emptyTemplateScanner = new class () extends TemplateScanner + { + public function scan(ModuleInfo $m): array { return []; } }; - $emptyTranslationScanner = new class () extends TranslationScanner { - public function scan(\Marko\CodeIndexer\ValueObject\ModuleInfo $m): array + $emptyTranslationScanner = new class () extends TranslationScanner + { + public function scan(ModuleInfo $m): array { return []; } @@ -110,7 +116,7 @@ function readResponse($out): array it('responds to initialize with server capabilities', function () { $this->protocol->handleMessage( - json_encode(['jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [], 'id' => 1]) + json_encode(['jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [], 'id' => 1]), ); $response = readResponse($this->out); expect($response['result']['capabilities'])->toBeArray() diff --git a/packages/lsp/tests/Unit/Server/PublishDiagnosticsTest.php b/packages/lsp/tests/Unit/Server/PublishDiagnosticsTest.php index c5dd06ba..bf5c08b8 100644 --- a/packages/lsp/tests/Unit/Server/PublishDiagnosticsTest.php +++ b/packages/lsp/tests/Unit/Server/PublishDiagnosticsTest.php @@ -2,14 +2,14 @@ declare(strict_types=1); -use Marko\CodeIndexer\Cache\IndexCache; use Marko\CodeIndexer\Attributes\AttributeParser; +use Marko\CodeIndexer\Cache\IndexCache; use Marko\CodeIndexer\Config\ConfigScanner; use Marko\CodeIndexer\Module\ModuleWalker; -use Marko\CodeIndexer\Views\TemplateScanner; use Marko\CodeIndexer\Translations\TranslationScanner; use Marko\CodeIndexer\ValueObject\ConfigKeyEntry; use Marko\CodeIndexer\ValueObject\ModuleInfo; +use Marko\CodeIndexer\Views\TemplateScanner; use Marko\Core\Path\ProjectPaths; use Marko\Lsp\Features\AttributeFeature; use Marko\Lsp\Features\CodeLensFeature; diff --git a/packages/mail/tests/Unit/Exceptions/NoDriverExceptionTest.php b/packages/mail/tests/Unit/Exceptions/NoDriverExceptionTest.php index 23eff5aa..96f898e1 100644 --- a/packages/mail/tests/Unit/Exceptions/NoDriverExceptionTest.php +++ b/packages/mail/tests/Unit/Exceptions/NoDriverExceptionTest.php @@ -61,7 +61,7 @@ $exception = NoDriverException::noDriverInstalled(); expect($exception->getContext())->toBe( - 'Attempted to resolve a mail interface but no implementation is bound.' + 'Attempted to resolve a mail interface but no implementation is bound.', ); }); diff --git a/packages/mcp/src/Commands/ServeCommand.php b/packages/mcp/src/Commands/ServeCommand.php index f30987dd..a76520f2 100644 --- a/packages/mcp/src/Commands/ServeCommand.php +++ b/packages/mcp/src/Commands/ServeCommand.php @@ -24,8 +24,7 @@ public function __construct( public function execute( Input $input, Output $output, - ): int - { + ): int { fwrite(STDERR, "Marko MCP server starting on stdio...\n"); $this->server->serve(); fwrite(STDERR, "Marko MCP server shut down.\n"); diff --git a/packages/mcp/src/Protocol/JsonRpcProtocol.php b/packages/mcp/src/Protocol/JsonRpcProtocol.php index 5084d48a..2268de15 100644 --- a/packages/mcp/src/Protocol/JsonRpcProtocol.php +++ b/packages/mcp/src/Protocol/JsonRpcProtocol.php @@ -24,8 +24,7 @@ public function __construct( public function registerMethod( string $method, callable $handler, - ): void - { + ): void { $this->handlers[$method] = $handler; } @@ -61,17 +60,17 @@ public function handleMessage(string $jsonLine): void $request = json_decode($jsonLine, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $e) { $this->writeResponse( - ['jsonrpc' => '2.0', 'error' => ['code' => -32700, 'message' => 'Parse error: ' . $e->getMessage()], 'id' => null] + ['jsonrpc' => '2.0', 'error' => ['code' => -32700, 'message' => 'Parse error: ' . $e->getMessage()], 'id' => null], ); return; } if (!is_array( - $request + $request, ) || !isset($request['jsonrpc']) || $request['jsonrpc'] !== '2.0' || !isset($request['method'])) { $this->writeResponse( - ['jsonrpc' => '2.0', 'error' => ['code' => -32600, 'message' => 'Invalid Request'], 'id' => $request['id'] ?? null] + ['jsonrpc' => '2.0', 'error' => ['code' => -32600, 'message' => 'Invalid Request'], 'id' => $request['id'] ?? null], ); return; @@ -87,7 +86,7 @@ public function handleMessage(string $jsonLine): void return; } $this->writeResponse( - ['jsonrpc' => '2.0', 'error' => ['code' => -32601, 'message' => "Method not found: $method"], 'id' => $id] + ['jsonrpc' => '2.0', 'error' => ['code' => -32601, 'message' => "Method not found: $method"], 'id' => $id], ); return; @@ -104,14 +103,14 @@ public function handleMessage(string $jsonLine): void return; } $this->writeResponse( - ['jsonrpc' => '2.0', 'error' => ['code' => $e->getJsonRpcCode(), 'message' => $e->getMessage()], 'id' => $id] + ['jsonrpc' => '2.0', 'error' => ['code' => $e->getJsonRpcCode(), 'message' => $e->getMessage()], 'id' => $id], ); } catch (Throwable $e) { if ($isNotification) { return; } $this->writeResponse( - ['jsonrpc' => '2.0', 'error' => ['code' => -32603, 'message' => 'Internal error: ' . $e->getMessage()], 'id' => $id] + ['jsonrpc' => '2.0', 'error' => ['code' => -32603, 'message' => 'Internal error: ' . $e->getMessage()], 'id' => $id], ); } } diff --git a/packages/mcp/src/Server/McpServer.php b/packages/mcp/src/Server/McpServer.php index ad12dbb4..04f35dde 100644 --- a/packages/mcp/src/Server/McpServer.php +++ b/packages/mcp/src/Server/McpServer.php @@ -147,8 +147,7 @@ private function toolsCall(array $params): array private function validateArgs( array $args, array $schema, - ): void - { + ): void { $required = $schema['required'] ?? []; foreach ($required as $field) { if (!array_key_exists($field, $args)) { diff --git a/packages/mcp/src/Tools/CheckConfigKeyTool.php b/packages/mcp/src/Tools/CheckConfigKeyTool.php index 5f9c8864..cbba69bb 100644 --- a/packages/mcp/src/Tools/CheckConfigKeyTool.php +++ b/packages/mcp/src/Tools/CheckConfigKeyTool.php @@ -67,8 +67,7 @@ private function closestMatches( string $needle, array $candidates, int $max, - ): array - { + ): array { $scored = array_map(fn ($c) => ['key' => $c, 'distance' => levenshtein($needle, $c)], $candidates); usort($scored, fn ($a, $b) => $a['distance'] <=> $b['distance']); diff --git a/packages/mcp/src/Tools/ResolveTemplateTool.php b/packages/mcp/src/Tools/ResolveTemplateTool.php index ae16bc4d..38476930 100644 --- a/packages/mcp/src/Tools/ResolveTemplateTool.php +++ b/packages/mcp/src/Tools/ResolveTemplateTool.php @@ -50,7 +50,7 @@ public function handle(array $arguments): array return [ 'content' => [['type' => 'text', 'text' => "Template not found: $template\nAvailable in module '$module': " . implode( ', ', - $names + $names, )]], 'isError' => true, ]; diff --git a/packages/mcp/src/Tools/Runtime/Adapters/MarkoConsoleDispatcher.php b/packages/mcp/src/Tools/Runtime/Adapters/MarkoConsoleDispatcher.php index 8b8186f4..11de1df3 100644 --- a/packages/mcp/src/Tools/Runtime/Adapters/MarkoConsoleDispatcher.php +++ b/packages/mcp/src/Tools/Runtime/Adapters/MarkoConsoleDispatcher.php @@ -27,8 +27,7 @@ public function __construct( public function dispatch( string $command, array $args = [], - ): array - { + ): array { $stdout = fopen('php://memory', 'r+'); $stderr = fopen('php://memory', 'r+'); diff --git a/packages/mcp/src/Tools/Runtime/Adapters/MarkoQueryConnection.php b/packages/mcp/src/Tools/Runtime/Adapters/MarkoQueryConnection.php index 46f03e5c..bc4813ed 100644 --- a/packages/mcp/src/Tools/Runtime/Adapters/MarkoQueryConnection.php +++ b/packages/mcp/src/Tools/Runtime/Adapters/MarkoQueryConnection.php @@ -23,8 +23,7 @@ public function __construct( public function query( string $sql, array $params = [], - ): array - { + ): array { return array_values($this->connection->query($sql, $params)); } } diff --git a/packages/mcp/tests/Unit/Server/McpServerTest.php b/packages/mcp/tests/Unit/Server/McpServerTest.php index 731aa669..5aa382bb 100644 --- a/packages/mcp/tests/Unit/Server/McpServerTest.php +++ b/packages/mcp/tests/Unit/Server/McpServerTest.php @@ -16,7 +16,7 @@ it('responds to initialize with protocol version and capabilities', function (): void { $this->protocol->handleMessage( - json_encode(['jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [], 'id' => 1]) + json_encode(['jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [], 'id' => 1]), ); rewind($this->out); $response = json_decode((string) stream_get_contents($this->out), true); @@ -31,7 +31,7 @@ // namespace `mcp__marko/mcp__` is invalid. Tool identifiers must // not contain slashes — keep the self-reported name dash-separated. $this->protocol->handleMessage( - json_encode(['jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [], 'id' => 1]) + json_encode(['jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [], 'id' => 1]), ); rewind($this->out); $response = json_decode((string) stream_get_contents($this->out), true); @@ -55,7 +55,7 @@ public function handle(array $arguments): array )); $this->protocol->handleMessage( - json_encode(['jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [], 'id' => 1]) + json_encode(['jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [], 'id' => 1]), ); rewind($this->out); $response = json_decode((string) stream_get_contents($this->out), true); @@ -130,8 +130,8 @@ public function handle(array $arguments): array $this->protocol->handleMessage( json_encode( - ['jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => ['name' => 'greet', 'arguments' => ['name' => 'world']], 'id' => 3] - ) + ['jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => ['name' => 'greet', 'arguments' => ['name' => 'world']], 'id' => 3], + ), ); rewind($this->out); $response = json_decode((string) stream_get_contents($this->out), true); @@ -142,8 +142,8 @@ public function handle(array $arguments): array it('returns JSON-RPC error for unknown tool names', function (): void { $this->protocol->handleMessage( json_encode( - ['jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => ['name' => 'missing', 'arguments' => []], 'id' => 4] - ) + ['jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => ['name' => 'missing', 'arguments' => []], 'id' => 4], + ), ); rewind($this->out); $response = json_decode((string) stream_get_contents($this->out), true); @@ -169,8 +169,8 @@ public function handle(array $arguments): array // Missing required field 'q' $this->protocol->handleMessage( json_encode( - ['jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => ['name' => 'search', 'arguments' => []], 'id' => 5] - ) + ['jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => ['name' => 'search', 'arguments' => []], 'id' => 5], + ), ); rewind($this->out); $response = json_decode((string) stream_get_contents($this->out), true); @@ -196,8 +196,8 @@ public function handle(array $arguments): array $this->protocol->handleMessage( json_encode( - ['jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => ['name' => 'fetch', 'arguments' => []], 'id' => 6] - ) + ['jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => ['name' => 'fetch', 'arguments' => []], 'id' => 6], + ), ); rewind($this->out); $response = json_decode((string) stream_get_contents($this->out), true); diff --git a/packages/mcp/tests/Unit/Tools/CheckConfigKeyToolTest.php b/packages/mcp/tests/Unit/Tools/CheckConfigKeyToolTest.php index ec843a08..9f108063 100644 --- a/packages/mcp/tests/Unit/Tools/CheckConfigKeyToolTest.php +++ b/packages/mcp/tests/Unit/Tools/CheckConfigKeyToolTest.php @@ -37,7 +37,7 @@ public function getConfigKeys(): array defaultValue: 'file', module: 'Marko_Cache', file: 'config/cache.php', - line: 5 + line: 5, ), ]; $index = makeFakeIndexCacheForCheck($entries); @@ -62,7 +62,7 @@ public function getConfigKeys(): array defaultValue: 'file', module: 'Marko_Cache', file: 'config/cache.php', - line: 5 + line: 5, ), new ConfigKeyEntry( key: 'cache.store', @@ -70,7 +70,7 @@ public function getConfigKeys(): array defaultValue: 'default', module: 'Marko_Cache', file: 'config/cache.php', - line: 10 + line: 10, ), ]; $index = makeFakeIndexCacheForCheck($entries); @@ -94,7 +94,7 @@ public function getConfigKeys(): array defaultValue: 'localhost', module: 'Marko_Mail', file: 'config/mail.php', - line: 12 + line: 12, ), ]; $index = makeFakeIndexCacheForCheck($entries); diff --git a/packages/mcp/tests/Unit/Tools/FindPluginsTargetingToolTest.php b/packages/mcp/tests/Unit/Tools/FindPluginsTargetingToolTest.php index d3d11b21..cdba340f 100644 --- a/packages/mcp/tests/Unit/Tools/FindPluginsTargetingToolTest.php +++ b/packages/mcp/tests/Unit/Tools/FindPluginsTargetingToolTest.php @@ -55,14 +55,14 @@ class: 'App\Plugins\OrderPlugin', ); $index = makeFakeIndexCacheForPlugins([$plugin]); $tool = FindPluginsTargetingTool::definition($index); - + $result = $tool->handler->handle(['target' => 'App\Services\OrderService']); - + expect($result['content'][0]['text'])->toContain('App\Plugins\OrderPlugin'); expect($result['content'][0]['text'])->toContain('beforePlace'); expect($result['content'][0]['text'])->toContain('before'); expect($result['content'][0]['text'])->toContain('10'); - } + }, ); it('returns empty list for classes with no plugins', function (): void { diff --git a/packages/mcp/tests/Unit/Tools/GetConfigSchemaToolTest.php b/packages/mcp/tests/Unit/Tools/GetConfigSchemaToolTest.php index bb1a2fd8..36a70852 100644 --- a/packages/mcp/tests/Unit/Tools/GetConfigSchemaToolTest.php +++ b/packages/mcp/tests/Unit/Tools/GetConfigSchemaToolTest.php @@ -30,7 +30,7 @@ public function getConfigKeys(): array defaultValue: 'file', module: 'Marko_Cache', file: 'config/cache.php', - line: 5 + line: 5, ), new ConfigKeyEntry( key: 'mail.host', @@ -38,7 +38,7 @@ public function getConfigKeys(): array defaultValue: 'localhost', module: 'Marko_Mail', file: 'config/mail.php', - line: 3 + line: 3, ), ]; $index = makeFakeIndexCache($entries); diff --git a/packages/mcp/tests/Unit/Tools/ListToolsTest.php b/packages/mcp/tests/Unit/Tools/ListToolsTest.php index 900975ad..456cf8d8 100644 --- a/packages/mcp/tests/Unit/Tools/ListToolsTest.php +++ b/packages/mcp/tests/Unit/Tools/ListToolsTest.php @@ -71,12 +71,12 @@ public function getRoutes(): array new CommandEntry( name: 'cache:clear', class: 'Marko\\Cache\\Command\\ClearCommand', - description: 'Clears cache' + description: 'Clears cache', ), new CommandEntry( name: 'module:list', class: 'Marko\\Core\\Command\\ModuleListCommand', - description: 'Lists modules' + description: 'Lists modules', ), ]); @@ -103,13 +103,13 @@ class: 'Marko\\Core\\Command\\ModuleListCommand', method: 'GET', path: '/api/users', class: 'Marko\\Api\\Controller\\UserController', - action: 'index' + action: 'index', ), new RouteEntry( method: 'POST', path: '/api/users', class: 'Marko\\Api\\Controller\\UserController', - action: 'store' + action: 'store', ), ]); @@ -142,12 +142,12 @@ class: 'Marko\\Api\\Controller\\UserController', new CommandEntry( name: 'cache:clear', class: 'Marko\\Cache\\Command\\ClearCommand', - description: 'Clears cache' + description: 'Clears cache', ), new CommandEntry( name: 'module:list', class: 'Marko\\Core\\Command\\ModuleListCommand', - description: 'Lists modules' + description: 'Lists modules', ), ], routes: [ @@ -155,13 +155,13 @@ class: 'Marko\\Core\\Command\\ModuleListCommand', method: 'GET', path: '/api/users', class: 'Marko\\Api\\Controller\\UserController', - action: 'index' + action: 'index', ), new RouteEntry( method: 'POST', path: '/api/orders', class: 'Marko\\Api\\Controller\\OrderController', - action: 'store' + action: 'store', ), ], ); @@ -202,19 +202,19 @@ class: 'Marko\\Api\\Controller\\OrderController', method: 'GET', path: '/api/users', class: 'Marko\\Api\\Controller\\UserController', - action: 'index' + action: 'index', ), ], ); $moduleText = ListModulesTool::definition($cache)->handler->handle( - ['filter' => 'nonexistent'] + ['filter' => 'nonexistent'], )['content'][0]['text']; $cmdText = ListCommandsTool::definition($cache)->handler->handle( - ['filter' => 'nonexistent'] + ['filter' => 'nonexistent'], )['content'][0]['text']; $routeText = ListRoutesTool::definition($cache)->handler->handle( - ['filter' => 'nonexistent'] + ['filter' => 'nonexistent'], )['content'][0]['text']; expect($moduleText)->toBe('(no modules found)') @@ -239,7 +239,7 @@ class: 'Marko\\Api\\Controller\\UserController', method: 'GET', path: '/api/users', class: 'Marko\\Api\\Controller\\UserController', - action: 'index' + action: 'index', ), ], ); diff --git a/packages/mcp/tests/Unit/Tools/ResolveTemplateToolTest.php b/packages/mcp/tests/Unit/Tools/ResolveTemplateToolTest.php index 760c5547..55c4ddc8 100644 --- a/packages/mcp/tests/Unit/Tools/ResolveTemplateToolTest.php +++ b/packages/mcp/tests/Unit/Tools/ResolveTemplateToolTest.php @@ -59,7 +59,7 @@ public function getTemplates(): array $result = $tool->handler->handle(['template' => 'App\Catalog::product/view']); expect($result['content'][0]['text'])->toContain( - '/var/www/modules/catalog/resources/views/product/view.blade.php' + '/var/www/modules/catalog/resources/views/product/view.blade.php', ); expect($result)->not->toHaveKey('isError'); }); diff --git a/packages/mcp/tests/Unit/Tools/Runtime/ReadLogEntriesToolTest.php b/packages/mcp/tests/Unit/Tools/Runtime/ReadLogEntriesToolTest.php index 4a3f1a9e..6a4d139a 100644 --- a/packages/mcp/tests/Unit/Tools/Runtime/ReadLogEntriesToolTest.php +++ b/packages/mcp/tests/Unit/Tools/Runtime/ReadLogEntriesToolTest.php @@ -26,24 +26,24 @@ function (): void { '[2024-01-01 00:00:02] INFO: Request completed', '[2024-01-01 00:00:03] WARNING: High memory usage', ]); - + $definition = ReadLogEntriesTool::definition($reader); - + expect($definition->name)->toBe('read_log_entries'); - + // Default count - $result = $definition->handler->handle([]); + $result = $definition->handler->handle([]); $text = $result['content'][0]['text']; - + expect($text)->toContain('ERROR: Something failed') ->and($text)->toContain('INFO: Request completed') ->and($text)->toContain('WARNING: High memory usage'); - + // Explicit count - $result2 = $definition->handler->handle(['count' => 1]); + $result2 = $definition->handler->handle(['count' => 1]); $text2 = $result2['content'][0]['text']; - + expect($text2)->toContain('WARNING: High memory usage') ->and($text2)->not->toContain('ERROR: Something failed'); - } + }, ); diff --git a/packages/mcp/tests/Unit/Tools/Runtime/RunConsoleCommandToolTest.php b/packages/mcp/tests/Unit/Tools/Runtime/RunConsoleCommandToolTest.php index d0f73fe5..89a0ddba 100644 --- a/packages/mcp/tests/Unit/Tools/Runtime/RunConsoleCommandToolTest.php +++ b/packages/mcp/tests/Unit/Tools/Runtime/RunConsoleCommandToolTest.php @@ -14,8 +14,7 @@ public function __construct(private readonly array $result) {} public function dispatch( string $command, array $args = [], - ): array - { + ): array { return $this->result; } }; diff --git a/packages/mcp/tests/Unit/Tools/Runtime/ToolFailureModeTest.php b/packages/mcp/tests/Unit/Tools/Runtime/ToolFailureModeTest.php index 5d7c8814..33a7dd27 100644 --- a/packages/mcp/tests/Unit/Tools/Runtime/ToolFailureModeTest.php +++ b/packages/mcp/tests/Unit/Tools/Runtime/ToolFailureModeTest.php @@ -18,14 +18,13 @@ public function __construct() {} public function dispatch( string $command, array $args = [], - ): array - { + ): array { throw new RuntimeException('Command not found: nonexistent'); } }; $consoleResult = RunConsoleCommandTool::definition($throwingDispatcher)->handler->handle( - ['command' => 'nonexistent'] + ['command' => 'nonexistent'], ); expect($consoleResult['content'][0]['text'])->toContain('ERROR') ->and($consoleResult['isError'] ?? false)->toBeTrue(); @@ -38,8 +37,7 @@ public function __construct() {} public function query( string $sql, array $params = [], - ): array - { + ): array { return []; } })->handler->handle(['sql' => 'DROP TABLE users']); diff --git a/packages/mcp/tests/Unit/Tools/SearchDocsToolTest.php b/packages/mcp/tests/Unit/Tools/SearchDocsToolTest.php index 6e2eea0c..ce08e0e0 100644 --- a/packages/mcp/tests/Unit/Tools/SearchDocsToolTest.php +++ b/packages/mcp/tests/Unit/Tools/SearchDocsToolTest.php @@ -73,8 +73,8 @@ public function driverName(): string // Missing required 'query' field $protocol->handleMessage( json_encode( - ['jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => ['name' => 'search_docs', 'arguments' => []], 'id' => 1] - ) + ['jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => ['name' => 'search_docs', 'arguments' => []], 'id' => 1], + ), ); rewind($out); $response = json_decode((string) stream_get_contents($out), true); diff --git a/packages/notification/tests/KnownDriversValidationTest.php b/packages/notification/tests/KnownDriversValidationTest.php index 6a94a268..a37da75f 100644 --- a/packages/notification/tests/KnownDriversValidationTest.php +++ b/packages/notification/tests/KnownDriversValidationTest.php @@ -11,7 +11,7 @@ 'skeleton suggest block contains all notification drivers', function () use ($knownDriversPath, $skeletonComposerPath) { KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath); - } + }, ); test('every notification driver follows marko slash prefix pattern', function () use ($knownDriversPath) { diff --git a/packages/page-cache/tests/KnownDriversValidationTest.php b/packages/page-cache/tests/KnownDriversValidationTest.php index 582a7c6a..2c89c0b0 100644 --- a/packages/page-cache/tests/KnownDriversValidationTest.php +++ b/packages/page-cache/tests/KnownDriversValidationTest.php @@ -9,9 +9,9 @@ test( 'skeleton suggest block contains all page-cache drivers', - fn () => KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath) + fn () => KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath), ); test( 'every page-cache driver follows marko slash prefix pattern', - fn () => KnownDriversValidator::assertDocsUrlsResolveToValidPattern($knownDriversPath) + fn () => KnownDriversValidator::assertDocsUrlsResolveToValidPattern($knownDriversPath), ); diff --git a/packages/page-cache/tests/Unit/Exceptions/NoDriverExceptionTest.php b/packages/page-cache/tests/Unit/Exceptions/NoDriverExceptionTest.php index 95bd9a96..5c577991 100644 --- a/packages/page-cache/tests/Unit/Exceptions/NoDriverExceptionTest.php +++ b/packages/page-cache/tests/Unit/Exceptions/NoDriverExceptionTest.php @@ -17,8 +17,8 @@ 'page-cache NoDriverException exposes a noDriverInstalled() factory (renamed from noBinding for consistency)', function (): void { $exception = NoDriverException::noDriverInstalled(); - + expect($exception)->toBeInstanceOf(NoDriverException::class) ->and($exception)->toBeInstanceOf(PageCacheException::class); - } + }, ); diff --git a/packages/pubsub-pgsql/src/Driver/PgSqlPublisher.php b/packages/pubsub-pgsql/src/Driver/PgSqlPublisher.php index d29d9ac0..7e1cfc63 100644 --- a/packages/pubsub-pgsql/src/Driver/PgSqlPublisher.php +++ b/packages/pubsub-pgsql/src/Driver/PgSqlPublisher.php @@ -19,8 +19,7 @@ public function __construct( public function publish( string $channel, Message $message, - ): void - { + ): void { $prefixed = $this->config->prefix() . $channel; $this->connection->connection()->notify($prefixed, $message->payload); } diff --git a/packages/pubsub-pgsql/tests/Driver/PgSqlPublisherTest.php b/packages/pubsub-pgsql/tests/Driver/PgSqlPublisherTest.php index c2d45081..cbf0dba8 100644 --- a/packages/pubsub-pgsql/tests/Driver/PgSqlPublisherTest.php +++ b/packages/pubsub-pgsql/tests/Driver/PgSqlPublisherTest.php @@ -27,8 +27,7 @@ class PublisherStubPostgresConnection implements PostgresConnection public function notify( string $channel, string $payload = '', - ): PostgresResult - { + ): PostgresResult { $this->notifyCalls[] = ['channel' => $channel, 'payload' => $payload]; return new PublisherStubPostgresResult(); @@ -57,8 +56,7 @@ public function prepare(string $sql): PostgresStatement public function execute( string $sql, array $params = [], - ): PostgresResult - { + ): PostgresResult { throw new RuntimeException('Not implemented in stub'); } diff --git a/packages/pubsub-pgsql/tests/Driver/PgSqlSubscriberTest.php b/packages/pubsub-pgsql/tests/Driver/PgSqlSubscriberTest.php index 762d7221..afc3cc56 100644 --- a/packages/pubsub-pgsql/tests/Driver/PgSqlSubscriberTest.php +++ b/packages/pubsub-pgsql/tests/Driver/PgSqlSubscriberTest.php @@ -33,8 +33,7 @@ class MockPostgresListener implements PostgresListener, IteratorAggregate public function __construct( string $channel, array $notifications = [], - ) - { + ) { $this->channel = $channel; $this->notifications = $notifications; } @@ -88,8 +87,7 @@ public function listen(string $channel): PostgresListener public function notify( string $channel, string $payload = '', - ): PostgresResult - { + ): PostgresResult { throw new RuntimeException('Not implemented in stub'); } @@ -111,8 +109,7 @@ public function prepare(string $sql): PostgresStatement public function execute( string $sql, array $params = [], - ): PostgresResult - { + ): PostgresResult { throw new RuntimeException('Not implemented in stub'); } diff --git a/packages/pubsub-pgsql/tests/PgSqlPubSubConnectionTest.php b/packages/pubsub-pgsql/tests/PgSqlPubSubConnectionTest.php index 48a22dbb..1f80f65d 100644 --- a/packages/pubsub-pgsql/tests/PgSqlPubSubConnectionTest.php +++ b/packages/pubsub-pgsql/tests/PgSqlPubSubConnectionTest.php @@ -19,8 +19,7 @@ class ConnectionStubPostgresConnection implements PostgresConnection public function notify( string $channel, string $payload = '', - ): PostgresResult - { + ): PostgresResult { throw new RuntimeException('Not implemented in stub'); } @@ -47,8 +46,7 @@ public function prepare(string $sql): PostgresStatement public function execute( string $sql, array $params = [], - ): PostgresResult - { + ): PostgresResult { throw new RuntimeException('Not implemented in stub'); } diff --git a/packages/pubsub-redis/src/Driver/RedisPublisher.php b/packages/pubsub-redis/src/Driver/RedisPublisher.php index 87fd463b..68f4ff56 100644 --- a/packages/pubsub-redis/src/Driver/RedisPublisher.php +++ b/packages/pubsub-redis/src/Driver/RedisPublisher.php @@ -19,8 +19,7 @@ public function __construct( public function publish( string $channel, Message $message, - ): void - { + ): void { $prefixed = $this->config->prefix() . $channel; $this->connection->client()->publish($prefixed, $message->payload); } diff --git a/packages/pubsub-redis/tests/Driver/RedisPublisherTest.php b/packages/pubsub-redis/tests/Driver/RedisPublisherTest.php index b49370d8..4ffa4852 100644 --- a/packages/pubsub-redis/tests/Driver/RedisPublisherTest.php +++ b/packages/pubsub-redis/tests/Driver/RedisPublisherTest.php @@ -32,8 +32,7 @@ class PublisherTestStubRedisLink implements RedisLink public function execute( string $command, array $parameters, - ): RedisResponse - { + ): RedisResponse { $this->calls[] = ['command' => $command, 'parameters' => $parameters]; return new PublisherTestStubRedisResponse(); diff --git a/packages/pubsub-redis/tests/RedisPubSubConnectionTest.php b/packages/pubsub-redis/tests/RedisPubSubConnectionTest.php index 9bfafee7..9e824ef5 100644 --- a/packages/pubsub-redis/tests/RedisPubSubConnectionTest.php +++ b/packages/pubsub-redis/tests/RedisPubSubConnectionTest.php @@ -30,8 +30,7 @@ class StubRedisLink implements RedisLink public function execute( string $command, array $parameters, - ): RedisResponse - { + ): RedisResponse { $this->calls[] = ['command' => $command, 'parameters' => $parameters]; return new StubRedisResponse(); diff --git a/packages/pubsub/src/Exceptions/PubSubException.php b/packages/pubsub/src/Exceptions/PubSubException.php index c4cbde42..799ddc95 100644 --- a/packages/pubsub/src/Exceptions/PubSubException.php +++ b/packages/pubsub/src/Exceptions/PubSubException.php @@ -11,8 +11,7 @@ class PubSubException extends MarkoException public static function connectionFailed( string $driver, string $reason, - ): self - { + ): self { return new self( message: "Failed to connect to pub/sub driver '$driver'.", context: "Connection attempt to driver '$driver' failed: $reason", @@ -23,8 +22,7 @@ public static function connectionFailed( public static function subscriptionFailed( string $channel, string $reason, - ): self - { + ): self { return new self( message: "Failed to subscribe to channel '$channel'.", context: "Subscription to channel '$channel' failed: $reason", @@ -35,8 +33,7 @@ public static function subscriptionFailed( public static function publishFailed( string $channel, string $reason, - ): self - { + ): self { return new self( message: "Failed to publish to channel '$channel'.", context: "Publish to channel '$channel' failed: $reason", diff --git a/packages/pubsub/tests/KnownDriversValidationTest.php b/packages/pubsub/tests/KnownDriversValidationTest.php index 0713ada2..61000501 100644 --- a/packages/pubsub/tests/KnownDriversValidationTest.php +++ b/packages/pubsub/tests/KnownDriversValidationTest.php @@ -9,9 +9,9 @@ test( 'skeleton suggest block contains all pubsub drivers', - fn () => KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath) + fn () => KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath), ); test( 'every pubsub driver follows marko slash prefix pattern', - fn () => KnownDriversValidator::assertDocsUrlsResolveToValidPattern($knownDriversPath) + fn () => KnownDriversValidator::assertDocsUrlsResolveToValidPattern($knownDriversPath), ); diff --git a/packages/pubsub/tests/MessageTest.php b/packages/pubsub/tests/MessageTest.php index a501cf0c..07de8002 100644 --- a/packages/pubsub/tests/MessageTest.php +++ b/packages/pubsub/tests/MessageTest.php @@ -9,13 +9,13 @@ 'creates readonly Message value object with channel, payload, and optional pattern properties', function (): void { $reflection = new ReflectionClass(Message::class); - + expect($reflection->isReadOnly())->toBeTrue(); - + $channelProp = $reflection->getProperty('channel'); $payloadProp = $reflection->getProperty('payload'); $patternProp = $reflection->getProperty('pattern'); - + expect($channelProp->isPublic())->toBeTrue() ->and($channelProp->getType()?->getName())->toBe('string') ->and($payloadProp->isPublic())->toBeTrue() @@ -23,7 +23,7 @@ function (): void { ->and($patternProp->isPublic())->toBeTrue() ->and($patternProp->getType()?->allowsNull())->toBeTrue() ->and($patternProp->getType()?->getName())->toBe('string'); - } + }, ); it('creates Message with all properties accessible', function (): void { diff --git a/packages/pubsub/tests/SubscriberInterfaceTest.php b/packages/pubsub/tests/SubscriberInterfaceTest.php index c4c4cf3a..c2a7b822 100644 --- a/packages/pubsub/tests/SubscriberInterfaceTest.php +++ b/packages/pubsub/tests/SubscriberInterfaceTest.php @@ -10,43 +10,43 @@ 'defines SubscriberInterface with subscribe method accepting variadic channels returning Subscription', function (): void { $reflection = new ReflectionClass(SubscriberInterface::class); - + expect($reflection->isInterface())->toBeTrue() ->and($reflection->hasMethod('subscribe'))->toBeTrue(); - + $method = $reflection->getMethod('subscribe'); - + expect($method->isPublic())->toBeTrue() ->and($method->getReturnType()?->getName())->toBe(Subscription::class) ->and($method->getParameters())->toHaveCount(1); - + $channelsParam = $method->getParameters()[0]; - + expect($channelsParam->getName())->toBe('channels') ->and($channelsParam->getType()?->getName())->toBe('string') ->and($channelsParam->isVariadic())->toBeTrue(); - } + }, ); it( 'defines SubscriberInterface with psubscribe method accepting variadic patterns returning Subscription', function (): void { $reflection = new ReflectionClass(SubscriberInterface::class); - + expect($reflection->isInterface())->toBeTrue() ->and($reflection->hasMethod('psubscribe'))->toBeTrue(); - + $method = $reflection->getMethod('psubscribe'); - + expect($method->isPublic())->toBeTrue() ->and($method->getReturnType()?->getName())->toBe(Subscription::class) ->and($method->getParameters())->toHaveCount(1); - + $patternsParam = $method->getParameters()[0]; - + expect($patternsParam->getName())->toBe('patterns') ->and($patternsParam->getType()?->getName())->toBe('string') ->and($patternsParam->isVariadic())->toBeTrue(); - } + }, ); }); diff --git a/packages/queue/tests/KnownDriversValidationTest.php b/packages/queue/tests/KnownDriversValidationTest.php index 1173c88e..b5bec8c2 100644 --- a/packages/queue/tests/KnownDriversValidationTest.php +++ b/packages/queue/tests/KnownDriversValidationTest.php @@ -23,17 +23,17 @@ 'it lists marko/queue-sync first as the recommended development default', function () use ($knownDriversPath): void { $drivers = require $knownDriversPath; - + expect(array_key_first($drivers))->toBe('marko/queue-sync'); - } + }, ); test( 'skeleton suggest block contains all queue drivers', - fn () => KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath) + fn () => KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath), ); test( 'every queue driver follows marko slash prefix pattern', - fn () => KnownDriversValidator::assertDocsUrlsResolveToValidPattern($knownDriversPath) + fn () => KnownDriversValidator::assertDocsUrlsResolveToValidPattern($knownDriversPath), ); diff --git a/packages/ratelimiter/tests/Unit/RenameReferencesTest.php b/packages/ratelimiter/tests/Unit/RenameReferencesTest.php index 5bdaa64c..f1bef953 100644 --- a/packages/ratelimiter/tests/Unit/RenameReferencesTest.php +++ b/packages/ratelimiter/tests/Unit/RenameReferencesTest.php @@ -7,26 +7,26 @@ function (): void { $monorepoRoot = dirname(__DIR__, 4); $ratelimiterPackage = dirname(__DIR__, 2); - + $composerFiles = glob($monorepoRoot . '/packages/*/composer.json') ?: []; $composerFiles[] = $monorepoRoot . '/composer.json'; - + $hits = []; - + foreach ($composerFiles as $file) { // Skip files inside the ratelimiter package itself - if (str_starts_with(realpath($file), realpath($ratelimiterPackage))) { + if (str_starts_with(realpath($file), realpath($ratelimiterPackage))) { continue; } - + $contents = file_get_contents($file); if (str_contains($contents, 'marko/rate-limiting')) { $hits[] = $file; } } - + expect($hits)->toBe([], 'Found marko/rate-limiting references in: ' . implode(', ', $hits)); - } + }, ); it('has zero grep hits for Marko\\RateLimiting namespace outside the ratelimiter package', function (): void { @@ -91,7 +91,7 @@ function (): void { exec( '/opt/homebrew/Cellar/php/8.5.1_2/bin/php ' . escapeshellarg( - $composerBin + $composerBin, ) . ' dump-autoload --ignore-platform-reqs 2>&1', $output, $exitCode, diff --git a/packages/session-file/tests/ModuleTest.php b/packages/session-file/tests/ModuleTest.php index dbee43f3..f294c541 100644 --- a/packages/session-file/tests/ModuleTest.php +++ b/packages/session-file/tests/ModuleTest.php @@ -71,9 +71,12 @@ } }); -it('orders the session middleware after page-cache when the file driver and page-cache are both installed', function (): void { - $module = require dirname(__DIR__) . '/module.php'; +it( + 'orders the session middleware after page-cache when the file driver and page-cache are both installed', + function (): void { + $module = require dirname(__DIR__) . '/module.php'; - expect($module['sequence'])->toHaveKey('after') - ->and($module['sequence']['after'])->toContain('marko/page-cache'); -}); + expect($module['sequence'])->toHaveKey('after') + ->and($module['sequence']['after'])->toContain('marko/page-cache'); + }, +); diff --git a/packages/session/tests/KnownDriversValidationTest.php b/packages/session/tests/KnownDriversValidationTest.php index 623814a3..1dff64a5 100644 --- a/packages/session/tests/KnownDriversValidationTest.php +++ b/packages/session/tests/KnownDriversValidationTest.php @@ -11,7 +11,7 @@ 'skeleton suggest block contains all session drivers', function () use ($knownDriversPath, $skeletonComposerPath) { KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath); - } + }, ); test('every session driver follows marko slash prefix pattern', function () use ($knownDriversPath) { diff --git a/packages/session/tests/Unit/Middleware/SessionMiddlewareTest.php b/packages/session/tests/Unit/Middleware/SessionMiddlewareTest.php index 707e1ba1..5475a6a0 100644 --- a/packages/session/tests/Unit/Middleware/SessionMiddlewareTest.php +++ b/packages/session/tests/Unit/Middleware/SessionMiddlewareTest.php @@ -144,8 +144,7 @@ public function save(): void public function get( string $key, mixed $default = null, - ): mixed - { + ): mixed { return $default; } diff --git a/packages/skeleton/tests/KnownDriversSuggestParityTest.php b/packages/skeleton/tests/KnownDriversSuggestParityTest.php index 57c560a3..42f0d484 100644 --- a/packages/skeleton/tests/KnownDriversSuggestParityTest.php +++ b/packages/skeleton/tests/KnownDriversSuggestParityTest.php @@ -41,7 +41,7 @@ expect($skeletonSuggest)->toHaveKey('marko/database-readwrite') ->and($skeletonSuggest['marko/database-readwrite'])->toBe( - 'Read/write connection splitting decorator (optional — works alongside a base driver)' + 'Read/write connection splitting decorator (optional — works alongside a base driver)', ); }); @@ -53,7 +53,7 @@ expect($skeletonSuggest)->toHaveKey('marko/page-cache-entity') ->and($skeletonSuggest['marko/page-cache-entity'])->toBe( - 'Auto-purges page-cache tags on entity save/delete (optional add-on)' + 'Auto-purges page-cache tags on entity save/delete (optional add-on)', ); }); diff --git a/packages/skeleton/tests/PackageStructureTest.php b/packages/skeleton/tests/PackageStructureTest.php index 878cc59b..b179ae46 100644 --- a/packages/skeleton/tests/PackageStructureTest.php +++ b/packages/skeleton/tests/PackageStructureTest.php @@ -212,13 +212,16 @@ expect($content)->toContain('Marko\Testing\TestCase'); }); -it('ships placeholders so every directory the phpunit.xml testsuites reference exists (app, modules, tests)', function (): void { - $base = __DIR__ . '/..'; - - expect(is_dir($base . '/app'))->toBeTrue() - ->and(is_dir($base . '/modules'))->toBeTrue() - ->and(is_dir($base . '/tests'))->toBeTrue(); -}); +it( + 'ships placeholders so every directory the phpunit.xml testsuites reference exists (app, modules, tests)', + function (): void { + $base = __DIR__ . '/..'; + + expect(is_dir($base . '/app'))->toBeTrue() + ->and(is_dir($base . '/modules'))->toBeTrue() + ->and(is_dir($base . '/tests'))->toBeTrue(); + }, +); it('runs pest successfully on a freshly scaffolded skeleton with no added test files', function (): void { // Simulate a fresh skeleton install: create a temporary directory with the diff --git a/packages/testing/tests/Unit/Exceptions/AssertionFailedExceptionTest.php b/packages/testing/tests/Unit/Exceptions/AssertionFailedExceptionTest.php index e583adfd..52fedfaf 100644 --- a/packages/testing/tests/Unit/Exceptions/AssertionFailedExceptionTest.php +++ b/packages/testing/tests/Unit/Exceptions/AssertionFailedExceptionTest.php @@ -13,12 +13,12 @@ function (): void { context: 'some context', suggestion: 'some suggestion', ); - + expect($exception)->toBeInstanceOf(MarkoException::class) ->and($exception->getMessage())->toBe('Test assertion failed') ->and($exception->getContext())->toBe('some context') ->and($exception->getSuggestion())->toBe('some suggestion'); - } + }, ); it('creates AssertionFailedException with static factory methods for common assertion failures', function (): void { diff --git a/packages/testing/tests/Unit/TestCase/TestCaseTest.php b/packages/testing/tests/Unit/TestCase/TestCaseTest.php index ab34203e..0dfcc8bb 100644 --- a/packages/testing/tests/Unit/TestCase/TestCaseTest.php +++ b/packages/testing/tests/Unit/TestCase/TestCaseTest.php @@ -128,27 +128,33 @@ public function callRegister(): void expect($after2)->toBe($after1); }); -it('does not error and resolves nothing when run from a package dir whose root has no app or modules modules', function (): void { - // Simulate running from within the marko monorepo where vendor/ exists but app/ and modules/ do not - $monoRepoRoot = dirname(__DIR__, 5); // packages/testing/tests/Unit/TestCase -> monorepo root +it( + 'does not error and resolves nothing when run from a package dir whose root has no app or modules modules', + function (): void { + // Simulate running from within the marko monorepo where vendor/ exists but app/ and modules/ do not + $monoRepoRoot = dirname(__DIR__, 5); // packages/testing/tests/Unit/TestCase -> monorepo root - $tc = new class ('test') extends TestCase - { - public static string $root = ''; - - protected function projectRoot(): string + $tc = new class ('test') extends TestCase { - return self::$root; - } - }; + public static string $root = ''; - $tc::$root = $monoRepoRoot; + protected function projectRoot(): string + { + return self::$root; + } + }; - expect(fn () => $tc->setUp())->not->toThrow(Throwable::class); -}); + $tc::$root = $monoRepoRoot; -it('has registered the autoloaders by the time the test body runs so an App class resolves inside the test closure', function (): void { - // HomeService was registered by the setUp() of an earlier test in this process. - // This closure body executes after setUp(), confirming registration timing is sufficient. - expect(class_exists('App\Home\HomeService'))->toBeTrue(); -}); + expect(fn () => $tc->setUp())->not->toThrow(Throwable::class); + }, +); + +it( + 'has registered the autoloaders by the time the test body runs so an App class resolves inside the test closure', + function (): void { + // HomeService was registered by the setUp() of an earlier test in this process. + // This closure body executes after setUp(), confirming registration timing is sufficient. + expect(class_exists('App\Home\HomeService'))->toBeTrue(); + }, +); diff --git a/packages/translation/tests/Exceptions/NoDriverExceptionTest.php b/packages/translation/tests/Exceptions/NoDriverExceptionTest.php index fa74e0f6..e1fc3058 100644 --- a/packages/translation/tests/Exceptions/NoDriverExceptionTest.php +++ b/packages/translation/tests/Exceptions/NoDriverExceptionTest.php @@ -67,7 +67,7 @@ $exception = NoDriverException::noDriverInstalled(); expect($exception->getContext())->toContain( - 'Attempted to resolve a translation interface but no implementation is bound.' + 'Attempted to resolve a translation interface but no implementation is bound.', ); }); diff --git a/packages/translation/tests/KnownDriversValidationTest.php b/packages/translation/tests/KnownDriversValidationTest.php index eb1eca7e..1bdb07de 100644 --- a/packages/translation/tests/KnownDriversValidationTest.php +++ b/packages/translation/tests/KnownDriversValidationTest.php @@ -9,9 +9,9 @@ test( 'skeleton suggest block contains all translation drivers', - fn () => KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath) + fn () => KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath), ); test( 'every translation driver follows marko slash prefix pattern', - fn () => KnownDriversValidator::assertDocsUrlsResolveToValidPattern($knownDriversPath) + fn () => KnownDriversValidator::assertDocsUrlsResolveToValidPattern($knownDriversPath), ); diff --git a/packages/view-twig/src/ModuleLoader.php b/packages/view-twig/src/ModuleLoader.php index aca00c99..8ee82f42 100644 --- a/packages/view-twig/src/ModuleLoader.php +++ b/packages/view-twig/src/ModuleLoader.php @@ -60,8 +60,7 @@ public function getCacheKey(string $name): string public function isFresh( string $name, int $time, - ): bool - { + ): bool { $path = $this->resolvePath($name); return filemtime($path) <= $time; diff --git a/packages/view/tests/Exceptions/NoDriverExceptionTest.php b/packages/view/tests/Exceptions/NoDriverExceptionTest.php index fec7b508..f19552d6 100644 --- a/packages/view/tests/Exceptions/NoDriverExceptionTest.php +++ b/packages/view/tests/Exceptions/NoDriverExceptionTest.php @@ -78,7 +78,7 @@ $exception = NoDriverException::noDriverInstalled(); expect($exception->getContext())->toContain( - 'Attempted to resolve ViewInterface but no implementation is bound.' + 'Attempted to resolve ViewInterface but no implementation is bound.', ); }); diff --git a/packages/view/tests/Feature/CrossEngineTemplateParityTest.php b/packages/view/tests/Feature/CrossEngineTemplateParityTest.php index 688e1960..caeed98a 100644 --- a/packages/view/tests/Feature/CrossEngineTemplateParityTest.php +++ b/packages/view/tests/Feature/CrossEngineTemplateParityTest.php @@ -25,7 +25,7 @@ function extractEngineSuffix(string $packageName, string $parentName): ?string if (!is_dir($packagesDir) || basename($packagesDir) !== 'packages') { $this->markTestSkipped( - 'Not running inside the monorepo packages directory — skipping cross-engine parity check.' + 'Not running inside the monorepo packages directory — skipping cross-engine parity check.', ); } @@ -64,7 +64,7 @@ function extractEngineSuffix(string $packageName, string $parentName): ?string $description = $engineMeta['description'] ?? $engineName; $message = "Parent module '$parent' has template providers for [" . implode( ', ', - array_keys($foundEngines) + array_keys($foundEngines), ) . "] but is missing a provider for engine '$engineName' ($description). " . "Expected a package like 'marko/" . basename($parent) . "-$engineName' declaring " @@ -174,16 +174,16 @@ function extractEngineSuffix(string $packageName, string $parentName): ?string 'it ignores packages whose extracted suffix is not in known-engines (e.g., admin-panel-twig-extra produces suffix twig-extra and is skipped if not registered)', function () { $enginesPath = dirname(__DIR__, 2) . '/known-engines.php'; - + if (!file_exists($enginesPath)) { $this->markTestSkipped('known-engines.php not found — marko/view not installed standalone?'); } - + $engines = require $enginesPath; - + $suffix = extractEngineSuffix('marko/admin-panel-twig-extra', 'marko/admin-panel'); - + expect($suffix)->toBe('twig-extra') ->and(isset($engines[$suffix]))->toBeFalse(); - } + }, ); diff --git a/packages/view/tests/KnownDriversValidationTest.php b/packages/view/tests/KnownDriversValidationTest.php index 2ee95b86..2a5c93d6 100644 --- a/packages/view/tests/KnownDriversValidationTest.php +++ b/packages/view/tests/KnownDriversValidationTest.php @@ -11,7 +11,7 @@ 'skeleton suggest block contains all view drivers', function () use ($knownDriversPath, $skeletonComposerPath): void { KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath); - } + }, ); test('every view driver follows marko slash prefix pattern', function () use ($knownDriversPath): void { From c4bc9cc90f3c326d9df84304ed136a4aeb492e6a Mon Sep 17 00:00:00 2001 From: Mark Shust Date: Sun, 26 Jul 2026 11:21:44 -0400 Subject: [PATCH 4/5] ci: exclude unparseable fixtures from php-cs-fixer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit php-cs-fixer lints every file before fixing and exits 4 when one will not parse, even with zero files to change. Three fixtures are invalid PHP on purpose — the config and codeindexer packages test what happens when the tooling is handed a broken file — so the Lint job would have failed on arrival, permanently, on a condition no one could fix without deleting the test cases. Caught by running the gate end to end and checking the real exit code. Piping the command through grep, as I had been, reports grep's status and hides this completely. Co-Authored-By: Claude Opus 5 (1M context) --- .php-cs-fixer.php | 7 +++++++ tests/CiWorkflowTest.php | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 79864550..98e32aed 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -17,6 +17,13 @@ ]) ->exclude('vendor') ->exclude('.phpunit.cache') + // Fixtures that are invalid PHP on purpose. The config and codeindexer packages test how + // they behave when handed an unparseable file, so these cannot be made valid — that is the + // test case. php-cs-fixer lints before fixing and exits non-zero on them even when it has + // nothing to change, which would fail the Lint job on a permanently red herring. + ->notPath('config/tests/Unit/fixtures/syntax-error.php') + ->notPath('codeindexer/tests/Fixtures/AttributeFixtures/src/Broken/SyntaxError.php') + ->notPath('codeindexer/tests/Fixtures/ConfigFixtures/module-d/config/broken.php') ->name('*.php') ->ignoreDotFiles(true) ->ignoreVCS(true); diff --git a/tests/CiWorkflowTest.php b/tests/CiWorkflowTest.php index cdf57b38..0df14f3e 100644 --- a/tests/CiWorkflowTest.php +++ b/tests/CiWorkflowTest.php @@ -70,6 +70,26 @@ ->and($composer['scripts']['ci'])->toContain('@phpstan'); }); +it('excludes deliberately-unparseable fixtures from both linters', function (): void { + // php-cs-fixer lints before fixing and exits 4 on invalid PHP even with nothing to change; + // phpcs aborts on the file. Either would fail the Lint job forever. + $csFixer = file_get_contents(dirname(__DIR__) . '/.php-cs-fixer.php'); + $phpcs = file_get_contents(dirname(__DIR__) . '/phpcs.xml'); + + $brokenFixtures = [ + 'packages/config/tests/Unit/fixtures/syntax-error.php', + 'packages/codeindexer/tests/Fixtures/AttributeFixtures/src/Broken/SyntaxError.php', + 'packages/codeindexer/tests/Fixtures/ConfigFixtures/module-d/config/broken.php', + ]; + + foreach ($brokenFixtures as $fixture) { + expect(file_exists(dirname(__DIR__) . '/' . $fixture))->toBeTrue("$fixture should still exist"); + expect($csFixer)->toContain(str_replace('packages/', '', $fixture)); + } + + expect($phpcs)->toContain('src/Broken/'); +}); + it('adds phpstan to the PR review checklist that previously omitted it', function (): void { $process = file_get_contents(dirname(__DIR__) . '/.claude/pr-review-process.md'); From 73be6209bd85d12a226823bc3d9286841b14c995 Mon Sep 17 00:00:00 2001 From: Mark Shust Date: Sun, 26 Jul 2026 11:24:58 -0400 Subject: [PATCH 5/5] fix(tests): remove machine-specific assumptions the CI gate exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three tests passed only on the author's machine. The gate caught all three on its first run, which is the argument for the gate. - ratelimiter and skeleton hardcoded /opt/homebrew/Cellar/php/8.5.1_2/bin/php, and ratelimiter fell back to /opt/homebrew/bin/composer. pr-review-process.md forbids exactly this ("no hardcoded paths or environment-specific values — use PHP_BINARY"), but nothing enforced it. Both now use PHP_BINARY; the composer lookup skips rather than guessing a Homebrew path. - PidFileTest built its process group through a string proc_open command, which runs via /bin/sh, so the reported pid belongs to the shell rather than to PHP. macOS sh execs in place and the pids coincide; Ubuntu's dash does not, so posix_setsid() created the group under a pid the assertions never inspected. Switched to the array form, which execs directly, and added a pcntl/posix guard. Co-Authored-By: Claude Opus 5 (1M context) --- packages/devserver/tests/Process/PidFileTest.php | 9 ++++++++- packages/ratelimiter/tests/Unit/RenameReferencesTest.php | 6 +++--- packages/skeleton/tests/PackageStructureTest.php | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/devserver/tests/Process/PidFileTest.php b/packages/devserver/tests/Process/PidFileTest.php index 7d6ee3e9..dc3d8ab4 100644 --- a/packages/devserver/tests/Process/PidFileTest.php +++ b/packages/devserver/tests/Process/PidFileTest.php @@ -131,6 +131,10 @@ function removeDir(string $dir): void }); it('detects a process group as running when parent died but child lives', function (): void { + if (!function_exists('pcntl_fork') || !function_exists('posix_setsid')) { + test()->markTestSkipped('pcntl and posix are required to build a detached process group'); + } + $tmpDir = sys_get_temp_dir() . '/pid-file-test-' . uniqid(); mkdir($tmpDir, 0755, true); $pidFile = new PidFile($tmpDir); @@ -149,8 +153,11 @@ function removeDir(string $dir): void exit(0); PHP; $encoded = base64_encode($script); + // Array form execs directly. A string command goes through /bin/sh, so the reported pid + // would be the shell's rather than PHP's — macOS sh execs in place and hides that, dash + // does not, and posix_setsid() then builds the group under a pid the assertions never see. $proc = proc_open( - "$php -r 'eval(base64_decode(\"$encoded\"));'", + [$php, '-r', 'eval(base64_decode("' . $encoded . '"));'], [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, ); diff --git a/packages/ratelimiter/tests/Unit/RenameReferencesTest.php b/packages/ratelimiter/tests/Unit/RenameReferencesTest.php index f1bef953..9a91a11c 100644 --- a/packages/ratelimiter/tests/Unit/RenameReferencesTest.php +++ b/packages/ratelimiter/tests/Unit/RenameReferencesTest.php @@ -81,16 +81,16 @@ function (): void { it('runs composer dump-autoload without errors', function (): void { $monorepoRoot = dirname(__DIR__, 4); - $composerBin = trim(shell_exec('which composer') ?: ''); + $composerBin = trim(shell_exec('command -v composer') ?: ''); if ($composerBin === '') { - $composerBin = '/opt/homebrew/bin/composer'; + test()->markTestSkipped('composer is not on PATH'); } $output = []; $exitCode = 0; exec( - '/opt/homebrew/Cellar/php/8.5.1_2/bin/php ' . escapeshellarg( + escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg( $composerBin, ) . ' dump-autoload --ignore-platform-reqs 2>&1', $output, diff --git a/packages/skeleton/tests/PackageStructureTest.php b/packages/skeleton/tests/PackageStructureTest.php index b179ae46..f4e3813e 100644 --- a/packages/skeleton/tests/PackageStructureTest.php +++ b/packages/skeleton/tests/PackageStructureTest.php @@ -240,8 +240,9 @@ function (): void { $vendorAutoload = __DIR__ . '/../../../vendor/autoload.php'; $command = sprintf( - 'cd %s && /opt/homebrew/Cellar/php/8.5.1_2/bin/php -d memory_limit=512M %s -c %s --bootstrap %s --no-coverage 2>&1', + 'cd %s && %s -d memory_limit=512M %s -c %s --bootstrap %s --no-coverage 2>&1', escapeshellarg($tmpDir), + escapeshellarg(PHP_BINARY), escapeshellarg($vendorPest), escapeshellarg($tmpDir . '/phpunit.xml'), escapeshellarg($vendorAutoload),