From 71a5911694f06d62dd45cebe77d28c68230a0a96 Mon Sep 17 00:00:00 2001 From: Manuel Christlieb Date: Sun, 2 Aug 2026 17:02:06 +0200 Subject: [PATCH] chore: adopt bambamboole/extended-testbench for the package tooling The bridge makes Boost work under Testbench out of the box, which replaces the hand-rolled overrides in workbench/app/Support that pointed Boost's config, guideline and skill composers at the package root. Aligns the repo with what its package:init scaffolds: - Pest 4 -> 5, abandoned mrpunyapal/peststan -> pestphp/pest-plugin-phpstan, added rector/rector. Dropped the explicit orchestra/testbench constraint; the bridge requires ^11. - artisan symlink -> the committed PHP shim. - phpunit.xml -> phpunit.xml.dist (plus a Unit suite and the env block), phpstan.neon -> phpstan.neon.dist (plus database/), new rector.php. - Scripts renamed to the generated shape: lint, stan, refactor, check. test stays scoped to Unit+Feature so check never pulls in the Browser suite, which needs a built bundle and a browser. - pint.json drops the blank_line_after_opening_tag override, which is the blank-line-only churn across ~60 files. - .gitignore and .gitattributes reconciled with the generated entries. - .ai/guidelines/git.md and the Comments section of development.md are now shipped by the package and registered via boost.json's packages key. rector.php skips two rules: RemoveUnusedPublicMethodParameterRector, which strips the model argument Laravel's Gate passes to policy methods, and FlipTypeControlToUseExclusiveTypeRector, which rewrites null checks as inline fully-qualified instanceof. --- .ai/guidelines/conventions.md | 2 +- .ai/guidelines/development.md | 25 ++---- .ai/guidelines/git.md | 12 --- .gitattributes | 5 +- .github/workflows/ci.yml | 4 +- .gitignore | 10 ++- README.md | 2 +- artisan | 5 +- boost.json | 3 + composer.json | 29 +++---- database/factories/MediaFactory.php | 1 + .../2026_07_29_000001_create_media_table.php | 1 + ..._000002_create_media_attachments_table.php | 1 + phpstan.neon => phpstan.neon.dist | 7 +- phpunit.xml => phpunit.xml.dist | 13 ++++ pint.json | 3 +- rector.php | 26 +++++++ src/Actions/DeleteMediaAction.php | 1 + src/Actions/DeleteSelectedMediaAction.php | 1 + src/Actions/UpdateMediaAction.php | 1 + src/Actions/UploadMediaAction.php | 1 + src/Components/MediaLibrary.php | 1 + .../Commands/GenerateConversionsCommand.php | 1 + src/Forms/Components/MediaPicker.php | 3 +- src/Forms/RichEditor/MediaImage.php | 1 + src/Forms/RichEditor/MediaImageNode.php | 2 + src/Jobs/GenerateMediaConversions.php | 3 +- src/MediaServiceProvider.php | 1 + src/Models/Attachment.php | 2 + src/Models/Concerns/HasMedia.php | 1 + src/Models/Media.php | 1 + src/Policies/MediaPolicy.php | 1 + src/Rules/AttachableMedia.php | 1 + src/Tables/Filters/MediaTypeFilter.php | 1 + src/Tables/MediaTable.php | 1 + tests/Browser/MediaImageEditorTest.php | 1 + tests/Browser/MediaLibraryTest.php | 1 + tests/Browser/MediaPickerTest.php | 1 + tests/BrowserTestCase.php | 1 + tests/Feature/CollectionConversionsTest.php | 13 ++-- tests/Feature/DisplayIntegrationTest.php | 1 + .../GenerateConversionsCommandTest.php | 1 + .../Feature/GenerateMediaConversionsTest.php | 25 +++--- tests/Feature/HasMediaTest.php | 1 + tests/Feature/ManageMediaActionsTest.php | 1 + tests/Feature/MediaImageExtensionTest.php | 1 + tests/Feature/MediaImageNodeTest.php | 1 + tests/Feature/MediaLibraryComponentTest.php | 1 + tests/Feature/MediaModelConfigTest.php | 1 + tests/Feature/MediaModelTest.php | 1 + tests/Feature/MediaPickerFieldTest.php | 1 + tests/Feature/MediaPolicyTest.php | 1 + tests/Feature/MediaTableTest.php | 1 + tests/Feature/TranslationParityTest.php | 1 + tests/Feature/UploadMediaActionTest.php | 1 + tests/Fixtures/ConversionProduct.php | 1 + tests/Fixtures/CustomMedia.php | 1 + tests/Fixtures/DenyMediaPolicy.php | 1 + tests/Fixtures/PartialConversionMedia.php | 1 + tests/Fixtures/TwoConversionMedia.php | 1 + tests/Pest.php | 3 +- tests/Support/Browser.php | 1 + tests/TestCase.php | 1 + tests/Unit/.gitkeep | 0 workbench/app/Factories/ProductFactory.php | 1 + workbench/app/Forms/ProductMediaForm.php | 1 + workbench/app/Layouts/AppLayout.php | 1 + workbench/app/Models/Product.php | 2 + workbench/app/Models/User.php | 1 + workbench/app/Pages/MediaLibraryPage.php | 1 + workbench/app/Pages/ProductMediaPage.php | 1 + workbench/app/Pages/WorkbenchPage.php | 1 + .../Providers/WorkbenchServiceProvider.php | 50 +----------- workbench/app/Seeders/DatabaseSeeder.php | 1 + workbench/app/Support/BoostConfig.php | 77 ------------------- .../app/Support/BoostGuidelineComposer.php | 17 ---- workbench/app/Support/BoostSkillComposer.php | 45 ----------- workbench/app/Tables/ProductMediaTable.php | 1 + workbench/app/WorkbenchConfig.php | 1 + ...001_01_01_000001_create_products_table.php | 1 + 80 files changed, 171 insertions(+), 270 deletions(-) delete mode 100644 .ai/guidelines/git.md mode change 120000 => 100755 artisan rename phpstan.neon => phpstan.neon.dist (63%) rename phpunit.xml => phpunit.xml.dist (55%) create mode 100644 rector.php create mode 100644 tests/Unit/.gitkeep delete mode 100644 workbench/app/Support/BoostConfig.php delete mode 100644 workbench/app/Support/BoostGuidelineComposer.php delete mode 100644 workbench/app/Support/BoostSkillComposer.php diff --git a/.ai/guidelines/conventions.md b/.ai/guidelines/conventions.md index 34cfeed..6129280 100644 --- a/.ai/guidelines/conventions.md +++ b/.ai/guidelines/conventions.md @@ -24,7 +24,7 @@ - **Uploads have two paths.** Multipart uploads go through `UploadMediaAction` and land on `config('media.disk')`; with `media.signed_uploads` enabled the action instead finalizes an already-uploaded temp key out of the `tmp/` prefix. Accepted types come from `config('media.accepted_types')` (mime patterns, `image/*` wildcards included). -- **Version coupling.** The package requires `lattice-php/lattice` `>=0.29.0 <1.0.0` — pre-1.0 minors are treated as +- **Version coupling.** The package requires `lattice-php/lattice` `>=0.36.0 <1.0.0` — pre-1.0 minors are treated as compatible, so a caret (which pins one 0.x minor) is deliberately avoided. Features that depend on newer core APIs must raise the lower bound and wait for the corresponding core release. diff --git a/.ai/guidelines/development.md b/.ai/guidelines/development.md index b7e0534..e97ac49 100644 --- a/.ai/guidelines/development.md +++ b/.ai/guidelines/development.md @@ -3,40 +3,27 @@ - This is a first-party companion package for [Lattice](https://github.com/lattice-php/lattice). It ships the media library — the PHP models, component, field, actions, and table plus its React renderer as **source** (no separate npm package); the consumer's build compiles it via Lattice's `lattice()` Vite plugin. -- The package is developed with Orchestra Testbench, not a full Laravel app. `artisan` at the repo root is a symlink to - `vendor/bin/testbench`, so `php artisan ` boots the Testbench skeleton with Lattice's and this package's - service providers. -- Run the PHP suite with `composer test` (the Feature suite; the Browser suite is excluded on purpose). +- Run the PHP suite with `composer test` (the Unit and Feature suites; the Browser suite is excluded on purpose). - Run the JavaScript (renderer) suite with `npm test` (Vitest). The tests exercise the renderer against the **published** `@lattice-php/lattice`, so `npm install` before running them. - Run the browser suite with `composer test:browser`. It rebuilds the workbench bundle first (`npm run build`) so it can never test stale assets — do not bypass that by invoking Pest's Browser suite directly after renderer changes. - Serve the workbench demo app with `composer serve` (starts at `/media`; `/media-picker` exercises the field inside a form — `testbench.yaml` holds the env/migrations/build wiring). -- The AI tooling overrides for Boost live in `workbench/app/Support/` and are wired in - `Workbench\App\Providers\WorkbenchServiceProvider`. They point Boost at the package root instead of the Testbench - skeleton. +- `bambamboole/extended-testbench` is what makes Boost work in a package repo: it rebases `base_path()` to the package + root for `boost:*` and `mcp:*` commands only, and ships the comments/git/Testbench guidelines composed into + `CLAUDE.md` and `AGENTS.md`. Do not reintroduce local Boost overrides. - `CLAUDE.md` and `AGENTS.md` are generated (git-ignored). They regenerate automatically after `composer install`; run `php artisan boost:update` (or `composer boost:refresh`) by hand after editing files in `.ai/guidelines/`. ## Verification - Before finishing a change, run the gate that matches what you touched: - - PHP change → `composer check` (Pint, PHPStan, Pest). + - PHP change → `composer check` (Pint, PHPStan, Rector, Pest). - Renderer change → `npm run typecheck` and `npm test`. - Anything touching the workbench app, an endpoint, or interactive behavior → additionally `composer test:browser`. - Never report green without having run the gate. CI runs all three. -## Comments - -- Code must be self-explanatory: reach for clear names, small functions, and types before a comment. -- Do not add comments. A comment is a last resort and explains only *why* something is done, never *what* the code does. -- When you encounter an obsolete, redundant, or "what" comment, delete it. -- Keep PHPDoc/JSDoc only when it carries type information, public API intent, static-analysis value, or a non-obvious - constraint. -- Keep comments that explain framework quirks, ordering requirements, browser/test timing, or other constraints that are - hard to infer from the code alone. - ## Testing - Prefer feature tests for backend behavior — serialize a component and assert its wire shape, drive an action or form @@ -45,6 +32,6 @@ `InteractsWithLatticeComponents` trait (`latticeRef()`, `callAction()`, `callBulkAction()`, `submitForm()`). - For renderer behavior — grid rendering, selection, search, the detail slideout, the picker's hidden inputs — use the Vitest suite in `resources/js`, with the local `test-support.ts` fixtures. -- For real-browser coverage (Playwright via Pest 4) use `tests/Browser`; `BrowserTestCase` guards against a missing or +- For real-browser coverage (Playwright via Pest) use `tests/Browser`; `BrowserTestCase` guards against a missing or stale workbench build, and the `assert*Eventually` helpers in `tests/Support/Browser.php` absorb async UI settling. - It is acceptable to add stable `data-test` attributes when they make assertions clearer or less brittle. diff --git a/.ai/guidelines/git.md b/.ai/guidelines/git.md deleted file mode 100644 index 3c2ee1b..0000000 --- a/.ai/guidelines/git.md +++ /dev/null @@ -1,12 +0,0 @@ -# Git & Pull Requests - -- Never credit the agent. No `Co-Authored-By` trailer and no "generated by" attribution in commit messages or pull - request descriptions. -- Make meaningful commits: one logical change per commit with a conventional-commit subject (`feat:`, `fix:`, `chore:`, - `refactor:`, `test:`, `docs:`). Squash throwaway "wip" commits before opening a PR. -- Never commit agent planning artifacts or scratch files (`.superpowers/`, `.codex/`, `.agents/`, and similar), even when - they informed the work. -- `CLAUDE.md` and `AGENTS.md` are generated and git-ignored — never commit them. Commit the sources under `.ai/` and - `boost.json` instead. -- Keep pull request descriptions compact and concise: what changed and why, in a few lines. No filler. -- For renderer/visual changes, include a screenshot or a concrete before/after in the PR description. diff --git a/.gitattributes b/.gitattributes index f10ced4..1fea846 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,9 +9,10 @@ resources/js/**/*.test.* export-ignore /.gitignore export-ignore /artisan export-ignore /boost.json export-ignore -/phpunit.xml export-ignore -/phpstan.neon export-ignore +/phpunit.xml.dist export-ignore +/phpstan.neon.dist export-ignore /pint.json export-ignore +/rector.php export-ignore /testbench.yaml export-ignore /vite.config.ts export-ignore /vite.lattice-deep-imports.ts export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65df5b4..5714b4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ env: jobs: php: - name: PHP ${{ matrix.php-version }} (${{ matrix.php-version == '8.4' && 'pint, phpstan, pest' || 'pest' }}) + name: PHP ${{ matrix.php-version }} (${{ matrix.php-version == '8.4' && 'pint, phpstan, rector, pest' || 'pest' }}) runs-on: ubuntu-latest strategy: fail-fast: false @@ -53,7 +53,7 @@ jobs: - run: composer install --prefer-dist --no-progress --no-interaction - - name: Pint, PHPStan, Pest + - name: Pint, PHPStan, Rector, Pest if: ${{ matrix.php-version == '8.4' }} run: composer check diff --git a/.gitignore b/.gitignore index 34ed947..1c5678d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,16 @@ /vendor/ /node_modules/ -composer.lock +/composer.lock .env .env.* -.phpunit.cache/ +/.phpunit.cache/ .phpunit.result.cache /.idea/ -/AGENTS.md /CLAUDE.md -/.claude/ +/AGENTS.md +/.mcp.json +/.claude/skills/ /.agents/ +/.junie/ /docs/superpowers/ /.superpowers/ diff --git a/README.md b/README.md index 023ddd0..18eb1ca 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ them like any Laravel package translation). ```bash composer install && npm install -composer check # pint --test, phpstan, pest (Feature) +composer check # pint --test, phpstan, rector --dry-run, pest (Unit + Feature) npm run typecheck && npm test composer test:browser # rebuilds the workbench bundle, then runs the Playwright suite composer serve # workbench demo app: /media (library) and /media-picker (field in a form) diff --git a/artisan b/artisan deleted file mode 120000 index 82e2255..0000000 --- a/artisan +++ /dev/null @@ -1 +0,0 @@ -vendor/bin/testbench \ No newline at end of file diff --git a/artisan b/artisan new file mode 100755 index 0000000..2afe5b8 --- /dev/null +++ b/artisan @@ -0,0 +1,4 @@ +#!/usr/bin/env php +=0.36.0 <1.0.0" }, "require-dev": { + "bambamboole/extended-testbench": "^0.3", "bambamboole/laravel-i18next": "^0.6.0", "larastan/larastan": "^3.0", "laravel/boost": "^2.4", "laravel/pint": "^1.16", "league/flysystem-aws-s3-v3": "^3.34", - "mrpunyapal/peststan": "^0.2.12", - "orchestra/testbench": "^9.0 || ^10.0 || ^11.0", - "pestphp/pest": "^4.0", - "pestphp/pest-plugin-browser": "^4.3", - "pestphp/pest-plugin-laravel": "^4.0" + "pestphp/pest": "^5.0", + "pestphp/pest-plugin-browser": "^5.0", + "pestphp/pest-plugin-laravel": "^5.0", + "pestphp/pest-plugin-phpstan": "^5.0", + "rector/rector": "^2.0" }, "autoload": { "psr-4": { @@ -54,20 +55,20 @@ "post-update-cmd": [ "@boost:refresh" ], - "boost:update": "@php vendor/bin/testbench boost:update --no-interaction", - "boost:refresh": "[ -n \"$CI\" ] || [ ! -f vendor/bin/testbench ] || vendor/bin/testbench boost:update --no-interaction", + "boost:refresh": "[ -n \"$CI\" ] || [ ! -f vendor/bin/testbench ] || [ ! -f boost.json ] || vendor/bin/testbench boost:update --no-interaction || true", "serve": "@php vendor/bin/testbench serve", - "test": "@php vendor/bin/pest --testsuite Feature", + "test": "pest --testsuite=Unit,Feature", "test:browser": [ "npm run build", - "@php vendor/bin/pest --testsuite Browser" + "pest --testsuite=Browser" ], - "test:lint": "@php vendor/bin/pint --test", - "lint": "@php vendor/bin/pint", - "analyse": "@php vendor/bin/phpstan analyse", + "lint": "pint --format agent", + "stan": "phpstan analyse", + "refactor": "rector", "check": [ - "@test:lint", - "@analyse", + "pint --test", + "phpstan analyse", + "rector --dry-run", "@test" ] }, diff --git a/database/factories/MediaFactory.php b/database/factories/MediaFactory.php index 48872ed..6424b62 100644 --- a/database/factories/MediaFactory.php +++ b/database/factories/MediaFactory.php @@ -1,4 +1,5 @@ + + tests/Unit + tests/Feature @@ -17,4 +20,14 @@ src + + + + + + + + + + diff --git a/pint.json b/pint.json index 735d7fc..4b4f940 100644 --- a/pint.json +++ b/pint.json @@ -1,7 +1,6 @@ { "preset": "laravel", "rules": { - "declare_strict_types": true, - "blank_line_after_opening_tag": false + "declare_strict_types": true } } diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..efe71a3 --- /dev/null +++ b/rector.php @@ -0,0 +1,26 @@ +withPaths([__DIR__.'/src', __DIR__.'/tests', __DIR__.'/workbench/app']) + ->withPhpSets() + ->withPreparedSets(deadCode: true, codeQuality: true, typeDeclarations: true) + ->withSkip([ + // Untyped Pest closures are the house style in tests/; typing every test callback + // is churn that adds no safety. Scoped to tests/ so src/ keeps the rule. + AddClosureVoidReturnTypeWhereNoReturnRector::class => [__DIR__.'/tests'], + + // Laravel resolves many signatures by reflection — policy methods, middleware handle(), + // listeners, authorize(). Stripping a parameter the body ignores breaks them at runtime. + RemoveUnusedPublicMethodParameterRector::class, + + // Rewrites `$x !== null` to `$x instanceof `, which is neither + // shorter nor clearer than the null check it replaces. + FlipTypeControlToUseExclusiveTypeRector::class, + ]); diff --git a/src/Actions/DeleteMediaAction.php b/src/Actions/DeleteMediaAction.php index 3332010..5d6eb3e 100644 --- a/src/Actions/DeleteMediaAction.php +++ b/src/Actions/DeleteMediaAction.php @@ -1,4 +1,5 @@ */ + #[\Override] public function addAttributes(): array { return [ diff --git a/src/Jobs/GenerateMediaConversions.php b/src/Jobs/GenerateMediaConversions.php index 2491844..637578c 100644 --- a/src/Jobs/GenerateMediaConversions.php +++ b/src/Jobs/GenerateMediaConversions.php @@ -1,4 +1,5 @@ media->getKey())) + new WithoutOverlapping((string) $this->media->getKey()) ->releaseAfter(30) ->expireAfter(300), ]; diff --git a/src/MediaServiceProvider.php b/src/MediaServiceProvider.php index 5c6296d..d4478c1 100644 --- a/src/MediaServiceProvider.php +++ b/src/MediaServiceProvider.php @@ -14,6 +14,7 @@ final class MediaServiceProvider extends ServiceProvider { + #[\Override] public function register(): void { $this->mergeConfigFrom(__DIR__.'/../config/media.php', 'media'); diff --git a/src/Models/Attachment.php b/src/Models/Attachment.php index 61f9f9c..820723e 100644 --- a/src/Models/Attachment.php +++ b/src/Models/Attachment.php @@ -1,4 +1,5 @@ */ + #[\Override] protected function casts(): array { return ['meta' => 'array']; diff --git a/src/Models/Concerns/HasMedia.php b/src/Models/Concerns/HasMedia.php index c43385c..62f479b 100644 --- a/src/Models/Concerns/HasMedia.php +++ b/src/Models/Concerns/HasMedia.php @@ -1,4 +1,5 @@ create(), 'gallery'))->handle(); + new GenerateMediaConversions($media, Product::factory()->create(), 'gallery')->handle(); $media->refresh(); expect(array_keys($media->conversions()))->toEqualCanonicalizing(['thumb', 'card']) @@ -103,7 +104,7 @@ function conversionProduct(string $name): ConversionProduct test('a bare string reuses the globally defined conversion of that name', function (): void { $media = galleryImage(); - (new GenerateMediaConversions($media, conversionProduct('legacy'), 'legacy'))->handle(); + new GenerateMediaConversions($media, conversionProduct('legacy'), 'legacy')->handle(); $media->refresh(); expect(array_keys($media->conversions()))->toBe(['thumb']) @@ -115,7 +116,7 @@ function conversionProduct(string $name): ConversionProduct $product = conversionProduct('typo'); expect(function () use ($media, $product): void { - (new GenerateMediaConversions($media, $product, 'typo'))->handle(); + new GenerateMediaConversions($media, $product, 'typo')->handle(); })->toThrow(RuntimeException::class, 'The [thumbnail] media conversion is not defined'); }); @@ -123,10 +124,10 @@ function conversionProduct(string $name): ConversionProduct $media = galleryImage(); $product = conversionProduct('shared'); - (new GenerateMediaConversions($media, $product, 'gallery'))->handle(); + new GenerateMediaConversions($media, $product, 'gallery')->handle(); $generated = $media->refresh()->conversions(); - (new GenerateMediaConversions($media, $product, 'hero'))->handle(); + new GenerateMediaConversions($media, $product, 'hero')->handle(); expect($media->refresh()->conversions())->toBe($generated) ->and(Storage::disk('public')->files('media/conversions'))->toHaveCount(2); @@ -137,7 +138,7 @@ function conversionProduct(string $name): ConversionProduct $product = Product::factory()->create(); $product->syncMedia([$media->getKey()], 'gallery'); - (new GenerateMediaConversions($media, $product, 'gallery'))->handle(); + new GenerateMediaConversions($media, $product, 'gallery')->handle(); $paths = $media->refresh()->conversionPaths(); $product->syncMedia([], 'gallery'); diff --git a/tests/Feature/DisplayIntegrationTest.php b/tests/Feature/DisplayIntegrationTest.php index 8acea68..db5386d 100644 --- a/tests/Feature/DisplayIntegrationTest.php +++ b/tests/Feature/DisplayIntegrationTest.php @@ -1,4 +1,5 @@ handle(); + new GenerateMediaConversions($media)->handle(); $media->refresh(); expect($media->width)->toBe(600) @@ -79,7 +80,7 @@ function expectWarning(string $fragment): void test('a non-convertible media is skipped without a conversion map', function (): void { $media = Media::factory()->create(['path' => 'media/logo.svg', 'mime_type' => 'image/svg+xml']); - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); expect($media->refresh()->meta)->toBeNull() ->and(Storage::disk('public')->allFiles())->toBe([]); @@ -94,7 +95,7 @@ function expectWarning(string $fragment): void ]]); unreadableDisk(); - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); expect($media->refresh()->conversionPath('thumb'))->toBe('x.webp'); }); @@ -104,7 +105,7 @@ function expectWarning(string $fragment): void $map = ['thumb' => ['path' => 'media/conversions/source-thumb.webp', 'width' => 400, 'height' => 400]]; $media->update(['meta' => ['conversions' => $map]]); - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); $media->refresh(); expect($media->width)->toBe(600) @@ -117,7 +118,7 @@ function expectWarning(string $fragment): void $media = storedImage(600, 400); $media->update(['mime_type' => 'application/octet-stream']); - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); expect($media->refresh()->conversionPath('thumb'))->toBe('media/conversions/source-thumb.webp'); }); @@ -127,7 +128,7 @@ function expectWarning(string $fragment): void unreadableDisk(); expect(function () use ($media): void { - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); })->toThrow(RuntimeException::class, 'The source was read.'); }); @@ -136,7 +137,7 @@ function expectWarning(string $fragment): void $media = storedImage(600, 400); expect(function () use ($media): void { - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); })->toThrow(RuntimeException::class, 'The [broken] media conversion must return an '.Image::class.' instance.'); $media->refresh(); @@ -158,7 +159,7 @@ function expectWarning(string $fragment): void try { ini_set('memory_limit', '512M'); - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); } finally { ini_set('memory_limit', $limit); } @@ -170,7 +171,7 @@ function expectWarning(string $fragment): void $media = Media::factory()->create(['path' => 'media/gone.jpg']); expectWarning('the source file is gone'); - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); expect($media->refresh()->meta)->toBeNull(); }); @@ -180,7 +181,7 @@ function expectWarning(string $fragment): void $media = Media::factory()->create(['path' => 'media/lying.jpg']); expectWarning('not a convertible image'); - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); expect($media->refresh()->meta)->toBeNull(); }); @@ -194,7 +195,7 @@ function expectWarning(string $fragment): void $media = Media::factory()->create(['path' => 'media/mislabelled.jpg', 'mime_type' => 'image/jpeg']); expectWarning('not a convertible image'); - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); expect($media->refresh()->meta)->toBeNull(); }); @@ -204,7 +205,7 @@ function expectWarning(string $fragment): void $media = Media::factory()->create(['path' => 'media/corrupt.png', 'mime_type' => 'image/png']); expectWarning('could not process the source'); - (new GenerateMediaConversions($media))->handle(); + new GenerateMediaConversions($media)->handle(); $media->refresh(); expect($media->conversions())->toBe([]) diff --git a/tests/Feature/HasMediaTest.php b/tests/Feature/HasMediaTest.php index 8b905e7..dba002a 100644 --- a/tests/Feature/HasMediaTest.php +++ b/tests/Feature/HasMediaTest.php @@ -1,4 +1,5 @@ in('Feature'); +uses(TestCase::class)->in('Unit', 'Feature'); uses(BrowserTestCase::class)->in('Browser'); /** diff --git a/tests/Support/Browser.php b/tests/Support/Browser.php index 48b8577..47d9e8e 100644 --- a/tests/Support/Browser.php +++ b/tests/Support/Browser.php @@ -1,4 +1,5 @@ */ + #[\Override] protected function casts(): array { return ['body' => 'array']; diff --git a/workbench/app/Models/User.php b/workbench/app/Models/User.php index 70bfae9..2deb148 100644 --- a/workbench/app/Models/User.php +++ b/workbench/app/Models/User.php @@ -1,4 +1,5 @@ serveWorkbenchTranslations(); - $this->readBoostConfigFromPackageRoot(); - } - - public function boot(): void - { - $this->pointBoostAtPackageRoot(); - $this->redirectBoostSkillsToPackageRoot(); } private function serveWorkbenchTranslations(): void @@ -38,39 +25,4 @@ private function serveWorkbenchTranslations(): void $loader->addNamespace('workbench', package_path('workbench/lang')); }); } - - private function readBoostConfigFromPackageRoot(): void - { - if (! class_exists(Config::class)) { - return; - } - - $this->app->singleton(Config::class, fn (): Config => new BoostConfig); - $this->app->bind(GuidelineComposer::class, BoostGuidelineComposer::class); - $this->app->bind(SkillComposer::class, BoostSkillComposer::class); - } - - private function pointBoostAtPackageRoot(): void - { - if (! class_exists(Roster::class)) { - return; - } - - $this->app->singleton(Roster::class, fn (): Roster => Roster::scan(package_path())); - } - - private function redirectBoostSkillsToPackageRoot(): void - { - if (! class_exists(Roster::class)) { - return; - } - - $skeleton = ltrim(str_replace(package_path(), '', base_path()), '/'); - $upToPackageRoot = str_repeat('../', substr_count($skeleton, '/') + 1); - - config([ - 'boost.agents.claude_code.skills_path' => $upToPackageRoot.'.claude/skills', - 'boost.agents.codex.skills_path' => $upToPackageRoot.'.agents/skills', - ]); - } } diff --git a/workbench/app/Seeders/DatabaseSeeder.php b/workbench/app/Seeders/DatabaseSeeder.php index 452d7dc..5ab13dd 100644 --- a/workbench/app/Seeders/DatabaseSeeder.php +++ b/workbench/app/Seeders/DatabaseSeeder.php @@ -1,4 +1,5 @@ path(); - - if (! file_exists($path)) { - return false; - } - - json_decode((string) file_get_contents($path), true); - - return json_last_error() === JSON_ERROR_NONE; - } - - #[\Override] - public function flush(): void - { - $path = $this->path(); - - if (file_exists($path)) { - unlink($path); - } - } - - #[\Override] - protected function set(string $key, mixed $value): void - { - $config = array_filter($this->all(), fn ($value): bool => $value !== null && $value !== []); - - data_set($config, $key, $value); - - ksort($config); - - file_put_contents( - $this->path(), - Str::of(json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))->append(PHP_EOL), - ); - } - - /** - * @return array - */ - #[\Override] - protected function all(): array - { - $path = $this->path(); - - if (! file_exists($path)) { - return []; - } - - $config = json_decode((string) file_get_contents($path), true); - - if (json_last_error() !== JSON_ERROR_NONE) { - return []; - } - - return $config ?? []; - } -} diff --git a/workbench/app/Support/BoostGuidelineComposer.php b/workbench/app/Support/BoostGuidelineComposer.php deleted file mode 100644 index 6a90086..0000000 --- a/workbench/app/Support/BoostGuidelineComposer.php +++ /dev/null @@ -1,17 +0,0 @@ -userGuidelineDir.'/'.ltrim($path, '/')); - } -} diff --git a/workbench/app/Support/BoostSkillComposer.php b/workbench/app/Support/BoostSkillComposer.php deleted file mode 100644 index 82d9984..0000000 --- a/workbench/app/Support/BoostSkillComposer.php +++ /dev/null @@ -1,45 +0,0 @@ -map(fn (string $skillPath): ?Skill => $this->parseSkill($skillPath, 'user', custom: false)) - ->filter() - ->keyBy(fn (Skill $skill): string => $skill->name); - } - - #[\Override] - protected function discoverPackageSpecificUserSkills(): Collection - { - $userAiPath = package_path('.ai'); - - if (! is_dir($userAiPath)) { - return collect(); - } - - return $this->discoverPackagePaths($userAiPath) - ->flatMap(fn (array $package): Collection => $this->discoverSkillsFromPath( - $package['path'], - $package['name'], - $package['version'], - )); - } -} diff --git a/workbench/app/Tables/ProductMediaTable.php b/workbench/app/Tables/ProductMediaTable.php index d7886d4..71cb826 100644 --- a/workbench/app/Tables/ProductMediaTable.php +++ b/workbench/app/Tables/ProductMediaTable.php @@ -1,4 +1,5 @@