Skip to content

Add test-state cleanup registrars#415

Merged
binaryfire merged 15 commits into
0.4from
feature/test-state-cleanup-registrars
Jul 5, 2026
Merged

Add test-state cleanup registrars#415
binaryfire merged 15 commits into
0.4from
feature/test-state-cleanup-registrars

Conversation

@binaryfire

@binaryfire binaryfire commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

This adds a first-class after-each test cleanup registration path for Hypervel apps and packages.

The testing package already owns framework-level static cleanup through its PHPUnit extension. This change keeps that centralized, but adds a small app/package layer on top so userland packages can clean worker-lifetime state without requiring every consuming app to know about their internals.

What changed:

  • Add AfterEachTestCleanup, a named callback registry for after-each cleanup.
  • Add TestStateRegistrars, discovered during PHPUnit extension bootstrap from extra.hypervel.test-state.
  • Run app/package callbacks before framework cleanup, while still flushing framework state in a finally.
  • Extract no-write package metadata discovery from PackageManifest so testing bootstrap can read Composer metadata without writing bootstrap/cache.
  • Document the app/package API and update contributor cleanup guidance.

A package can now declare:

"extra": {
    "hypervel": {
        "test-state": [
            "Vendor\\Package\\Testing\\TestState"
        ]
    }
}

and that registrar will be loaded before the first test in each PHPUnit/Paratest worker, even if that worker only runs unit tests that never boot an application.

A few details worth calling out:

  • Root app registrars are appended after package registrars.
  • dont-discover suppresses package test-state registrars consistently with provider discovery.
  • Root test-state still runs when root dont-discover disables package discovery.
  • Declared bad registrars fail fast with attributed errors. Missing or malformed Composer metadata degrades to framework cleanup only.
  • AfterEachTestCleanup::forgetCallbacks() is intentionally documented as test-internal because it clears discovered app/package registrations for the whole worker.

The Hypervel app skeleton was updated separately on its own 0.4 branch to register the extension and provide an empty Tests\Support\TestState class by default.

Tests:

  • ./vendor/bin/phpunit --no-progress tests/Testing/PHPUnit/TestStateRegistrarsTest.php
  • composer fix

Final composer fix result: php-cs-fixer clean, phpstan clean, Paratest passed.

Summary by CodeRabbit

  • New Features
    • Added extensible per-worker test-state cleanup hooks, letting apps and packages register named after-each cleanup callbacks.
    • Enabled automatic discovery of cleanup registrars from project composer metadata.
  • Bug Fixes
    • Ensured app/package cleanup callbacks run before framework cleanup and that framework cleanup still happens even if a callback throws.
    • Improved robustness of package discovery when composer metadata is missing or malformed.
  • Documentation
    • Updated testing and package development guides with “Test State Cleanup” instructions.
    • Clarified cleanup registration guidance in AGENTS documentation.

binaryfire added 12 commits July 5, 2026 01:31
Document the agreed design for extending after-each test cleanup to apps and packages.

The plan records why cleanup registration happens from the PHPUnit extension rather than service providers, how package metadata is discovered without writing the package manifest, and how app skeletons should expose a generated Tests\Support\TestState entry point.

It also captures the testing strategy, self-review checklist, and the final decisions from the Codesonic design review so future work can understand the intended shape of the API.
Move Composer installed package scanning out of PackageManifest::build() into a reusable no-write helper.

The new discoverInstalledPackages() method preserves the existing build semantics while making package metadata available to PHPUnit extension bootstrap, where writing bootstrap/cache is not appropriate.

The extraction keeps build() using the virtual packagesToIgnore() seam so testbench can continue to override root composer ignores. The existing format() method remains available and delegates to a static formatter for callers that still depend on the instance method.

The helper also centralizes metadata parsing robustness for missing or malformed installed.json files, non-array dont-discover metadata, and wildcard package discovery disables.
Add regression coverage for PackageManifest::discoverInstalledPackages() so the extracted helper stays byte-compatible with the manifest that build() writes for the existing fixtures.

The tests cover Composer 2 and legacy installed.json shapes, version-only packages, wildcard dont-discover handling, missing installed metadata, malformed installed metadata, and malformed root composer metadata.

This locks in the parsing behavior needed by PHPUnit bootstrap discovery without requiring the helper to write package cache files.
Add a fixture-level root dont-discover entry and assert testbench package manifest builds still ignore that project-level metadata during build.

This protects the virtual packagesToIgnore() seam used by testbench: the foundation manifest can read root composer ignores, while testbench remains free to manage discovery filtering at read time through its own ignore list and required-package behavior.
Introduce AfterEachTestCleanup as the process-local registry for app and package cleanup callbacks that should run after every PHPUnit test.

Callbacks are keyed by name so registrations are idempotent and duplicate names are last-wins. runCallbacks() continues through all registered callbacks before rethrowing the first exception, preventing one broken cleanup from suppressing the rest.

The registry intentionally has no flushState() method because its callbacks are suite-level registrations that must survive for the PHPUnit worker lifetime. Tests cover registration order, persistence, replacement, clearing, root-after-package ordering, and exception isolation.
Refactor AfterEachTestSubscriber so notify() delegates to flushStateAfterTest(), which runs app and package callbacks before the framework-owned cleanup list.

The framework cleanup body now lives in flushFrameworkState(), and it still runs from a finally block so framework state is flushed even when a custom cleanup callback throws.

The existing first-party optional package cleanup methods remain grouped at the bottom of the framework cleanup list.
Add focused tests for AfterEachTestSubscriber::flushStateAfterTest() using an anonymous subclass that observes framework cleanup without tearing down the live test process.

The tests prove custom callbacks run before framework cleanup and that framework cleanup still runs when a custom callback throws, while preserving the original callback exception.
Discover app and package test-state registrars during PHPUnit extension bootstrap, before the after-each subscriber is registered.

Package registrars are read from installed package metadata through the no-write PackageManifest helper, while root registrars are read from root composer.json and appended last. This makes cleanup available even in workers that only execute app-less unit tests and never boot a Hypervel application.

The testing package now declares its direct filesystem dependency because registrar discovery reads Composer metadata directly.
Add coverage for package and root test-state registrar discovery, package-before-root callback ordering, root and package dont-discover behavior, duplicate callback names, and COMPOSER_VENDOR_DIR handling.

The test matrix also locks in the fail-fast boundary for declared bad metadata: non-string registrar values, missing classes, and classes without register() throw attributed RuntimeExceptions, while missing or malformed composer metadata degrades to no custom cleanup.
Add the Test State Cleanup section to the testing guide and explain how applications should use tests/Support/TestState as one app-level cleanup entry point.

The macro guidance now reflects that Hypervel already flushes framework macroable classes after each test, so apps should only register cleanup for their own worker-lifetime state. The docs also warn that forgetCallbacks() clears discovered app and package registrations for the current PHPUnit worker.
Document the extra.hypervel.test-state composer metadata for package authors.

The new package docs explain that registrar classes must be normal package autoload classes, should aggregate cleanup for the package's stateful classes, and are discovered during PHPUnit extension bootstrap so cleanup runs even in workers that never boot an application.
Clarify where different kinds of test cleanup belong after introducing app and package test-state registrars.

Framework-owned classes still belong in AfterEachTestSubscriber, first-party optional package cleanup remains grouped at the bottom of that subscriber, and third-party packages, private packages, and applications should use extra.hypervel.test-state registrars instead.

The guidance also corrects the stale subscriber path and explicitly warns not to add AfterEachTestCleanup itself to the subscriber, because its callbacks must persist for the PHPUnit worker lifetime.
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@binaryfire, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6896bc92-82fe-4a93-9998-35baea888b29

📥 Commits

Reviewing files that changed from the base of the PR and between d2cefa9 and c67b90b.

📒 Files selected for processing (3)
  • src/foundation/src/PackageManifest.php
  • tests/Foundation/FoundationPackageManifestTest.php
  • tests/Testing/PHPUnit/TestStateRegistrarsTest.php
📝 Walkthrough

Walkthrough

This PR adds an extensible after-each PHPUnit test cleanup mechanism, Composer-metadata-based registrar discovery, PackageManifest discovery helpers, bootstrap wiring, documentation updates, and tests.

Changes

Test State Cleanup Feature

Layer / File(s) Summary
Design plan document
docs/plans/2026-07-04-testing-after-each-cleanup-registrars.md
Adds a full design/implementation plan describing the cleanup registry, discovery mechanism, PackageManifest extraction, skeleton updates, documentation, and testing plan.
AfterEachTestCleanup registry
src/testing/src/PHPUnit/AfterEachTestCleanup.php, tests/Testing/PHPUnit/AfterEachTestCleanupTest.php
Adds a named callback registry with ordered execution, overwrite-by-name registration, deferred exception rethrow, and cleanup-reset support, plus tests covering ordering, replacement, persistence, and failure handling.
Subscriber wiring
src/testing/src/PHPUnit/AfterEachTestSubscriber.php, tests/Testing/PHPUnit/AfterEachTestSubscriberTest.php
Introduces flushStateAfterTest() so custom callbacks run before framework cleanup inside a try/finally, with tests verifying ordering and exception behavior.
PackageManifest discovery helper
src/foundation/src/PackageManifest.php, src/foundation/composer.json, src/testbench/composer.json, tests/Foundation/FoundationPackageManifestTest.php, tests/Testbench/Foundation/PackageManifestTest.php, tests/Testbench/Foundation/Fixtures/PackageManifest/composer.json
Extracts installed-package discovery and root ignore parsing helpers, adds Composer runtime dependencies, and expands fixture/test coverage for discovery, ignore, and malformed metadata cases.
Registrar discovery and bootstrap
src/testing/src/PHPUnit/TestStateRegistrars.php, src/testing/src/PHPUnit/AfterEachTestExtension.php, src/testing/composer.json, tests/Testing/PHPUnit/TestStateRegistrarsTest.php
Adds metadata-driven registrar discovery and validation, wires it into PHPUnit extension bootstrap, adds filesystem support, and covers discovery order, error handling, and environment overrides.
Documentation updates
AGENTS.md, src/boost/docs/testing.md, src/boost/docs/packages.md
Adds “Test State Cleanup” guidance and updates macro/static cleanup instructions to use the new registration flow.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main addition of test-state cleanup registrars.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/test-state-cleanup-registrars

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a first-class after-each test cleanup path for Hypervel apps and packages. The main changes are:

  • Adds a named cleanup callback registry for PHPUnit workers.
  • Discovers package and root test-state registrars from Composer metadata.
  • Runs app and package cleanup before framework cleanup.
  • Extracts no-write package metadata discovery from the package manifest.
  • Updates tests, docs, and contributor guidance for the new cleanup flow.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
src/foundation/src/PackageManifest.php Extracts installed package discovery and guards malformed Composer metadata shapes.
src/testing/src/PHPUnit/TestStateRegistrars.php Adds Composer metadata discovery and validation for test-state registrar classes.
src/testing/src/PHPUnit/AfterEachTestCleanup.php Adds the worker-lifetime registry for named after-each cleanup callbacks.
src/testing/src/PHPUnit/AfterEachTestSubscriber.php Runs registered app and package cleanup before framework-owned cleanup.
tests/Testing/PHPUnit/TestStateRegistrarsTest.php Covers registrar discovery, ordering, invalid declarations, and malformed metadata handling.
tests/Foundation/FoundationPackageManifestTest.php Covers no-write package discovery and malformed package metadata handling.

Reviews (4): Last reviewed commit: "fix(foundation): guard root hypervel met..." | Re-trigger Greptile

Comment thread src/foundation/src/PackageManifest.php
Comment thread src/testing/src/PHPUnit/TestStateRegistrars.php Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a first-class mechanism for Hypervel applications and packages to register after-each-test cleanup (worker-lifetime/static state) that runs in every PHPUnit/Paratest worker, even when no application is booted. It keeps framework-owned cleanup centralized in the existing PHPUnit subscriber while layering an app/package registry and Composer-metadata discovery on top.

Changes:

  • Introduce AfterEachTestCleanup (named callback registry) and TestStateRegistrars (Composer extra.hypervel.test-state discovery at PHPUnit extension bootstrap).
  • Run app/package callbacks before framework cleanup, while ensuring framework cleanup always runs via finally.
  • Extract no-write installed-package discovery from PackageManifest so testing bootstrap can read Composer metadata without writing bootstrap/cache, and document the new API.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/Testing/PHPUnit/TestStateRegistrarsTest.php Coverage for registrar discovery order, dont-discover behavior, failure modes, and env vendor dir override.
tests/Testing/PHPUnit/AfterEachTestSubscriberTest.php Verifies custom callbacks run before framework cleanup and framework cleanup still runs on exceptions.
tests/Testing/PHPUnit/AfterEachTestCleanupTest.php Verifies callback naming, ordering, replacement, persistence, and exception behavior.
tests/Testbench/Foundation/PackageManifestTest.php Ensures testbench build does not apply root composer dont-discover during manifest build.
tests/Testbench/Foundation/Fixtures/PackageManifest/composer.json Fixture update to include root-level dont-discover for the new test.
tests/Foundation/FoundationPackageManifestTest.php Adds coverage for discoverInstalledPackages() behavior and resilience for missing/malformed metadata.
src/testing/src/PHPUnit/TestStateRegistrars.php Discovers and invokes app/package test-state registrars from Composer metadata.
src/testing/src/PHPUnit/AfterEachTestSubscriber.php Refactors cleanup flow to run custom callbacks first, then framework cleanup in a finally.
src/testing/src/PHPUnit/AfterEachTestExtension.php Bootstraps registrar discovery before registering the global after-each subscriber.
src/testing/src/PHPUnit/AfterEachTestCleanup.php New process-local registry for after-each cleanup callbacks (named, worker-lifetime).
src/testing/composer.json Adds an explicit dependency needed by new discovery/cleanup code paths.
src/foundation/src/PackageManifest.php Extracts installed-package discovery into a pure helper and reuses it for build().
src/boost/docs/testing.md Documents app-level test-state cleanup and clarifies macro cleanup guidance.
src/boost/docs/packages.md Documents package-level extra.hypervel.test-state registrars and usage pattern.
docs/plans/2026-07-04-testing-after-each-cleanup-registrars.md Design plan and rationale for the architecture and behavior.
AGENTS.md Updates contributor guidance to prefer extra.hypervel.test-state for non-framework cleanup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/testing/composer.json

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/testing/src/PHPUnit/AfterEachTestCleanup.php (1)

42-58: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Later callback exceptions are silently discarded.

Only the first Throwable is retained ($exception ??= $throwable); any exceptions thrown by subsequent callbacks are swallowed with no trace at all. With multiple apps/packages registering cleanup, a second (or third) package's cleanup failure could go completely unnoticed while only the first failure is ever surfaced.

Consider at least logging the discarded exceptions (e.g., via trigger_error() or PHP's error log) so later failures aren't fully invisible.

♻️ Suggested improvement
     public static function runCallbacks(): void
     {
         /** `@var` null|Throwable $exception */
         $exception = null;

         foreach (static::$callbacks as $callback) {
             try {
                 $callback();
             } catch (Throwable $throwable) {
-                $exception ??= $throwable;
+                if ($exception === null) {
+                    $exception = $throwable;
+                } else {
+                    trigger_error((string) $throwable, E_USER_WARNING);
+                }
             }
         }

         if ($exception !== null) {
             throw $exception;
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/testing/src/PHPUnit/AfterEachTestCleanup.php` around lines 42 - 58, The
runCallbacks() method in AfterEachTestCleanup only preserves the first Throwable
and silently drops later cleanup failures. Update the callback loop so any
additional exceptions are at least reported when caught, while still rethrowing
the first stored exception at the end; use the existing runCallbacks() and
$exception handling to locate the change.
src/testing/composer.json (1)

31-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Declare composer-runtime-api since TestStateRegistrars uses Composer\InstalledVersions.

TestStateRegistrars::installedRootPath() calls Composer\InstalledVersions::getRootPackage(), but this require block only adds hypervel/filesystem. Composer's own docs state: "If your code relies on any of these runtime features, you should require "composer-runtime-api": "^2.0" in your composer.json." Without it, nothing guarantees the class is available to consumers with unusual autoload setups, and composer validate/dependency tooling won't reflect the real contract.

♻️ Proposed fix
     "require": {
         "php": "^8.4",
         "ext-dom": "*",
+        "composer-runtime-api": "^2.0",
         "mockery/mockery": "1.6.x-dev",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/testing/composer.json` around lines 31 - 53, Add an explicit composer
runtime dependency in the testing composer manifest because
TestStateRegistrars::installedRootPath() depends on Composer\InstalledVersions.
Update the require block in composer.json to declare composer-runtime-api with a
compatible 2.x constraint, alongside the existing hypervel and symfony packages,
so the contract matches the runtime usage and dependency tooling reflects it.
src/testing/src/PHPUnit/TestStateRegistrars.php (1)

80-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate composer.json-reading logic with PackageManifest::packagesToIgnoreFromComposer.

rootRegistrars() re-implements the same isFile/json_decode/is_array guard sequence already extracted into PackageManifest::packagesToIgnoreFromComposer (just reading test-state instead of dont-discover). Consider factoring a shared "read root composer extra key" helper (e.g. on PackageManifest) so malformed-JSON handling stays in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/testing/src/PHPUnit/TestStateRegistrars.php` around lines 80 - 95, The
`rootRegistrars()` method is duplicating the composer.json read/validate flow
already handled in `PackageManifest::packagesToIgnoreFromComposer`. Refactor the
shared isFile/json_decode/is_array logic into a reusable helper on
`PackageManifest` (or similar) that reads a requested `extra.hypervel` key, then
update `rootRegistrars()` to use that helper for `test-state` so malformed JSON
handling lives in one place.
tests/Testing/PHPUnit/TestStateRegistrarsTest.php (1)

276-338: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move generic test-support classes into a test-specific namespace.

TestStateRegistrarRecorder, PackageTestStateRegistrar, RootTestStateRegistrar, RegistrarWithoutRegisterMethod, TestStateRegistrarsProbe, etc. are declared directly in the shared Hypervel\Tests\Testing\PHPUnit namespace. Any other test file in this directory declaring a similarly-named helper (e.g. another "Recorder" or "Probe" class) will collide with these.

Based on path instructions: "When helper classes in tests have collision-prone generic names, use a test-specific namespace that ends with the test class name."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Testing/PHPUnit/TestStateRegistrarsTest.php` around lines 276 - 338,
Move the shared helper classes out of the generic Hypervel\Tests\Testing\PHPUnit
namespace into a test-specific namespace tied to TestStateRegistrarsTest so they
won’t collide with similarly named helpers in other test files. Update the
declarations for TestStateRegistrarRecorder, PackageTestStateRegistrar,
ReplacementPackageTestStateRegistrar, RootTestStateRegistrar,
RegistrarWithoutRegisterMethod, and TestStateRegistrarsProbe together, and keep
their usage within TestStateRegistrarsTest aligned with the new namespace.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/boost/docs/testing.md`:
- Around line 176-199: The example still points to a standalone test support
class under tests/Support, which conflicts with the repo convention that such
files live in a capitalized Fixtures directory. Update the example so the
TestState class and its namespace reflect the Fixtures location, and keep the
register and flushState references aligned with that renamed support path. Make
sure any referenced stateful helpers like InvoiceNumbers, TaxRates, and
ReceiptMacros are shown as part of the Fixtures-based test support structure.

---

Nitpick comments:
In `@src/testing/composer.json`:
- Around line 31-53: Add an explicit composer runtime dependency in the testing
composer manifest because TestStateRegistrars::installedRootPath() depends on
Composer\InstalledVersions. Update the require block in composer.json to declare
composer-runtime-api with a compatible 2.x constraint, alongside the existing
hypervel and symfony packages, so the contract matches the runtime usage and
dependency tooling reflects it.

In `@src/testing/src/PHPUnit/AfterEachTestCleanup.php`:
- Around line 42-58: The runCallbacks() method in AfterEachTestCleanup only
preserves the first Throwable and silently drops later cleanup failures. Update
the callback loop so any additional exceptions are at least reported when
caught, while still rethrowing the first stored exception at the end; use the
existing runCallbacks() and $exception handling to locate the change.

In `@src/testing/src/PHPUnit/TestStateRegistrars.php`:
- Around line 80-95: The `rootRegistrars()` method is duplicating the
composer.json read/validate flow already handled in
`PackageManifest::packagesToIgnoreFromComposer`. Refactor the shared
isFile/json_decode/is_array logic into a reusable helper on `PackageManifest`
(or similar) that reads a requested `extra.hypervel` key, then update
`rootRegistrars()` to use that helper for `test-state` so malformed JSON
handling lives in one place.

In `@tests/Testing/PHPUnit/TestStateRegistrarsTest.php`:
- Around line 276-338: Move the shared helper classes out of the generic
Hypervel\Tests\Testing\PHPUnit namespace into a test-specific namespace tied to
TestStateRegistrarsTest so they won’t collide with similarly named helpers in
other test files. Update the declarations for TestStateRegistrarRecorder,
PackageTestStateRegistrar, ReplacementPackageTestStateRegistrar,
RootTestStateRegistrar, RegistrarWithoutRegisterMethod, and
TestStateRegistrarsProbe together, and keep their usage within
TestStateRegistrarsTest aligned with the new namespace.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 62f57a96-ec58-41b6-8439-0dc492385f62

📥 Commits

Reviewing files that changed from the base of the PR and between 7ee28c6 and cc0ab1f.

📒 Files selected for processing (16)
  • AGENTS.md
  • docs/plans/2026-07-04-testing-after-each-cleanup-registrars.md
  • src/boost/docs/packages.md
  • src/boost/docs/testing.md
  • src/foundation/src/PackageManifest.php
  • src/testing/composer.json
  • src/testing/src/PHPUnit/AfterEachTestCleanup.php
  • src/testing/src/PHPUnit/AfterEachTestExtension.php
  • src/testing/src/PHPUnit/AfterEachTestSubscriber.php
  • src/testing/src/PHPUnit/TestStateRegistrars.php
  • tests/Foundation/FoundationPackageManifestTest.php
  • tests/Testbench/Foundation/Fixtures/PackageManifest/composer.json
  • tests/Testbench/Foundation/PackageManifestTest.php
  • tests/Testing/PHPUnit/AfterEachTestCleanupTest.php
  • tests/Testing/PHPUnit/AfterEachTestSubscriberTest.php
  • tests/Testing/PHPUnit/TestStateRegistrarsTest.php

Comment thread src/boost/docs/testing.md
Handle malformed Composer installed package metadata without crashing package discovery during PHPUnit extension bootstrap. Invalid package lists now degrade to an empty manifest, malformed package entries are skipped before mapping, and non-array extra.hypervel metadata is treated as empty package metadata while preserving package versions.

Centralize root extra.hypervel reads through PackageManifest::rootHypervelExtra so root dont-discover and test-state metadata share the same composer.json parsing behavior.

Make TestStateRegistrars fail fast when Composer runtime metadata does not provide a valid root install path instead of guessing with getcwd. The root path validation is separated from the Composer lookup and covered through the test probe, including invalid values and realpath normalization.

Declare composer-runtime-api ^2.2 for split packages that use Composer\InstalledVersions directly: foundation, testbench, and testing. Permission remains unchanged because its usage is optional and guarded.

Document the first-exception-wins cleanup callback behavior and add focused tests for the malformed metadata and root path validation cases.
@binaryfire

binaryfire commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai @coderabbitai thanks for the review. Follow-up commit d2cefa9 addresses the actionable issues.

Fixed:

  • Hardened installed package discovery for malformed Composer metadata: scalar packages, malformed package entries, missing package names, and non-array extra.hypervel no longer crash discovery. Added focused coverage.
  • Removed the getcwd() fallback for Composer root discovery. Missing or invalid root install_path now fails fast instead of guessing. Added coverage for the validation path and realpath normalization.
  • Added composer-runtime-api: ^2.2 to the split package manifests that use Composer\InstalledVersions directly: testing, foundation, and testbench.
  • Extracted root extra.hypervel reads into PackageManifest::rootHypervelExtra() so root dont-discover and test-state metadata share parsing behavior.
  • Added a small comment documenting the intentional first-exception-wins cleanup callback behavior.

Intentionally unchanged:

  • Kept the app docs on tests/Support/TestState.php; this page documents generated application wiring and matches the skeleton, not the components repo fixture convention.
  • Did not add trigger_error() for later cleanup callback exceptions; cleanup keeps running and rethrows the first failure.
  • Did not move the registrar test helpers into another namespace; the helper names are feature-specific and not collision-prone.

Validation: composer fix is green: CS Fixer changed 0 files, PHPStan reported no errors, and Paratest passed.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

@binaryfire Thanks for the detailed follow-up — this all looks solid. Summary of what stands out:

  • Hardening discoverInstalledPackages() against malformed Composer metadata (scalar packages, malformed entries, missing names, non-array extra.hypervel) closes a real robustness gap.
  • Removing the getcwd() fallback in favor of fail-fast on missing/invalid install_path is the safer choice — silent guessing there could mask real installation problems.
  • Adding composer-runtime-api: ^2.2 to the split packages using Composer\InstalledVersions directly is a good catch for dependency correctness.
  • Extracting PackageManifest::rootHypervelExtra() for shared parsing between dont-discover and test-state reduces duplication nicely.

The rationale for the intentionally-unchanged items (skeleton docs matching generated app wiring, first-exception-wins over trigger_error(), and keeping feature-specific test helper names) all sound reasonable given the context.

Thanks for the thorough validation run as well. I'll take another pass over the diff to confirm everything lines up.

(=^・ω・^=)


✏️ Learnings added
Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/415

Timestamp: 2026-07-05T02:34:16.912Z
Learning: In the hypervel/components repository, `Hypervel\Testing\PHPUnit\AfterEachTestCleanup::runCallbacks()` intentionally implements first-exception-wins behavior: if multiple registered cleanup callbacks throw, only the first exception is rethrown after all callbacks have run; later exceptions are not reported via `trigger_error()` or otherwise. This is a deliberate design choice, not an oversight.

Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/415

Timestamp: 2026-07-05T02:34:16.912Z
Learning: In the hypervel/components repository, `PackageManifest::forRootInstall()`-style bootstrap discovery classes (e.g. `TestStateRegistrars`) validate the Composer root `install_path` from `Composer\InstalledVersions` and fail fast (throw) when it is missing or invalid, rather than falling back to `getcwd()`. This is intentional to avoid silently guessing the project root.

Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/415

Timestamp: 2026-07-05T02:34:16.912Z
Learning: In the hypervel/components repository, split packages (e.g. `src/testing`, `src/foundation`, `src/testbench`) that use `Composer\InstalledVersions` directly must declare `composer-runtime-api: ^2.2` as a dependency in their `composer.json`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@binaryfire Thanks — this looks like the same update already covered in my previous reply above (commit d2cefa9 addressing the malformed-metadata hardening, getcwd() fallback removal, composer-runtime-api additions, rootHypervelExtra() extraction, and the first-exception-wins documentation comment). I've already logged the relevant learnings from that summary, so no further action needed on my end. Let me know if you'd like another full pass over the diff.

(=^・ω・^=)

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown

The follow-up commit looks solid. Here's a quick verification of each fix:

Malformed metadata hardeningdiscoverInstalledPackages now guards on three separate conditions: non-array $installedPackages (scalar packages), non-array/missing-name package entries (.filter() before .mapWithKeys()), and non-array extra.hypervel (coerced to [] while keeping version). The test at testDiscoverInstalledPackagesSkipsMalformedPackageEntries covers all three paths in one shot, including the version-preservation requirement.

Fail-fast root pathresolveInstalledRootPath is a clean two-line guard. The TestStateRegistrarsProbe pattern for testing protected statics is the right call here — no need to restructure just to cover these paths.

rootHypervelExtra() extraction — The centralization is clean. packagesToIgnoreFromComposer now delegates through rootHypervelExtra rather than duplicating the composer.json parse, and rootRegistrars() in TestStateRegistrars uses the same method. Consistent null/non-array handling flows from one place.

composer-runtime-api: ^2.2 — Correct constraint for InstalledVersions availability. The decision to leave permission guarded-but-undeclared is reasonable given the optional usage pattern.

Test coverage — The new FoundationPackageManifestTest cases are focused and correctly scoped. The TestStateRegistrarsTest additions for resolveInstalledRootPath cover the three invalid input types (null, '', 123) plus the successful realpath normalization path.

No remaining concerns. This is ready.

Comment thread src/testing/src/PHPUnit/TestStateRegistrars.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/Testing/PHPUnit/TestStateRegistrarsTest.php (1)

211-225: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a #[DataProvider] instead of a manual loop.

Functionally correct, but a failed assertion inside the loop won't clearly indicate which of null/''/123 triggered it. A #[DataProvider] gives per-case isolation and clearer failure output, consistent with using PHPUnit attributes for parameterized cases.

♻️ Proposed refactor using a data provider
-    public function testResolveInstalledRootPathThrowsForInvalidComposerRootInstallPath(): void
-    {
-        foreach ([null, '', 123] as $installPath) {
-            try {
-                TestStateRegistrarsProbe::resolveInstalledRootPath($installPath);
-
-                $this->fail('Expected a runtime exception for invalid Composer root install path.');
-            } catch (RuntimeException $exception) {
-                $this->assertSame(
-                    'Composer runtime metadata is missing the root package install path.',
-                    $exception->getMessage()
-                );
-            }
-        }
-    }
+    #[DataProvider('invalidInstallPaths')]
+    public function testResolveInstalledRootPathThrowsForInvalidComposerRootInstallPath(mixed $installPath): void
+    {
+        $this->expectException(RuntimeException::class);
+        $this->expectExceptionMessage('Composer runtime metadata is missing the root package install path.');
+
+        TestStateRegistrarsProbe::resolveInstalledRootPath($installPath);
+    }
+
+    public static function invalidInstallPaths(): array
+    {
+        return [
+            'null' => [null],
+            'empty string' => [''],
+            'non-string' => [123],
+        ];
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Testing/PHPUnit/TestStateRegistrarsTest.php` around lines 211 - 225,
Refactor the
`testResolveInstalledRootPathThrowsForInvalidComposerRootInstallPath` test to
use a PHPUnit `#[DataProvider]` instead of the manual `foreach` loop. Move the
invalid inputs (`null`, `''`, `123`) into a dedicated provider method and have
the test method accept the install path as a parameter, keeping the same
`TestStateRegistrarsProbe::resolveInstalledRootPath` and `RuntimeException`
assertions so each case runs independently with clearer failure output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/Testing/PHPUnit/TestStateRegistrarsTest.php`:
- Around line 211-225: Refactor the
`testResolveInstalledRootPathThrowsForInvalidComposerRootInstallPath` test to
use a PHPUnit `#[DataProvider]` instead of the manual `foreach` loop. Move the
invalid inputs (`null`, `''`, `123`) into a dedicated provider method and have
the test method accept the install path as a parameter, keeping the same
`TestStateRegistrarsProbe::resolveInstalledRootPath` and `RuntimeException`
assertions so each case runs independently with clearer failure output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d2683a8a-4947-4540-a4b1-bb95b6ca14c6

📥 Commits

Reviewing files that changed from the base of the PR and between cc0ab1f and d2cefa9.

📒 Files selected for processing (8)
  • src/foundation/composer.json
  • src/foundation/src/PackageManifest.php
  • src/testbench/composer.json
  • src/testing/composer.json
  • src/testing/src/PHPUnit/AfterEachTestCleanup.php
  • src/testing/src/PHPUnit/TestStateRegistrars.php
  • tests/Foundation/FoundationPackageManifestTest.php
  • tests/Testing/PHPUnit/TestStateRegistrarsTest.php
✅ Files skipped from review due to trivial changes (2)
  • src/foundation/composer.json
  • src/testbench/composer.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/testing/src/PHPUnit/AfterEachTestCleanup.php
  • src/foundation/src/PackageManifest.php

Replace the manual invalid-install-path loop in TestStateRegistrarsTest with a PHPUnit DataProvider so each malformed Composer root install path is reported as its own dataset.

This keeps the same guard coverage for null, empty string, and non-string install_path values while matching the repository preference for PHPUnit attributes over inline parameter loops.

Validation run before commit:

- ./vendor/bin/phpunit --no-progress tests/Testing/PHPUnit/TestStateRegistrarsTest.php

- composer fix
@binaryfire

Copy link
Copy Markdown
Collaborator Author

Thanks @greptileai and @coderabbitai. Follow-up is pushed in 7ac0a83.

I took CodeRabbit's DataProvider nitpick. The invalid Composer root install path coverage now uses named PHPUnit datasets for null, empty string, and non-string values, so failures report per input instead of inside a loop.

I did not change the install_path fail-fast behavior from Greptile's latest note. Composer\InstalledVersions::getRootPackage() under composer-runtime-api: ^2.2 guarantees root install_path as a string. If that contract is missing, the Composer runtime metadata is broken. Falling back to framework-only cleanup would silently skip app/package test-state registrars, which is the same class of state-leak problem this PR is meant to prevent. Guessing a root path is also unsafe in monorepos and subdirectory runs.

Validation is green:

  • ./vendor/bin/phpunit --no-progress tests/Testing/PHPUnit/TestStateRegistrarsTest.php
  • composer fix

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@binaryfire Thanks for the update — this all checks out.

  • The named-dataset refactor for the invalid install_path cases (null, empty string, non-string) is a nice improvement; per-input failure reporting will make future regressions much easier to diagnose than a loop-based assertion.
  • Your rationale for keeping the fail-fast behavior is sound: given composer-runtime-api: ^2.2 guarantees install_path as a string, a violation there indicates broken Composer runtime metadata rather than a normal edge case worth silently degrading for. Falling back would risk masking exactly the kind of state-leak this PR sets out to prevent, and guessing a root path is indeed fragile for monorepos/subdirectory installs.

Nice work getting Paratest, CS Fixer, and PHPStan all green throughout these follow-ups. This looks ready from my end.

(=^・ω・^=)

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown

The DataProvider refactor is clean — each invalid path case (null, '', 123) now gets its own named dataset and will report individually on failure instead of collapsing into a loop failure at an opaque offset.

On the fail-fast decision: the reasoning is sound. composer-runtime-api: ^2.2 does guarantee install_path as a non-empty string in the root package entry, so a missing or non-string value there means the Composer runtime data itself is corrupt. A silent fallback would mask exactly the category of state-leak this PR exists to prevent, and path-guessing in monorepo/subdirectory setups is genuinely unsafe. Failing loudly with a clear message is the right call here.

Both points land well — no further changes needed from my side.

Comment thread src/foundation/src/PackageManifest.php Outdated
Normalize root composer extra.hypervel reads before returning keyed metadata so malformed root metadata is ignored consistently with installed package metadata.

This keeps root test-state and dont-discover discovery on the same defensive parsing path and prevents malformed root composer metadata from being treated as meaningful configuration.

Adds focused coverage for malformed root extra.hypervel metadata through both rootHypervelExtra and packagesToIgnoreFromComposer.

Validation run before commit:

- ./vendor/bin/phpunit --no-progress tests/Foundation/FoundationPackageManifestTest.php

- composer fix
@binaryfire binaryfire merged commit 4e56158 into 0.4 Jul 5, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants