Skip to content

Latest commit

 

History

History
956 lines (651 loc) · 97.3 KB

AGENTS.md

File metadata and controls

956 lines (651 loc) · 97.3 KB

Hypervel Components Agent Guide

External contributions: When preparing an issue or pull request as an external contributor, read src/docs/contributions.md in full and follow its requirements. This does not apply to maintainer-directed work.

Agent workflow: This guide assumes two agents: an implementer and an independent peer providing second opinions and code review. Without a peer, replace each required second opinion with a concise, plain-language explanation of the issue and recommended solution to the owner, and wait for approval. Replace peer code review with a full self-review after implementation.

Background

Hypervel is a standalone Laravel-style Swoole framework. The public API should stay close to Laravel wherever possible, while the internals are adapted for long-lived Swoole workers, coroutine safety, and high performance.

Laravel is the main API reference. Hyperf is a historical and architectural reference for some lower-level Swoole/coroutine packages, but Hypervel code should follow current Hypervel patterns rather than copying Hyperf structure mechanically.

Most work in this repo today is framework bug fixes and enhancements, or porting Laravel packages and updates. docs/ai/porting-hyperf.md applies only to the rare Hyperf package or update port.

This file is intentionally detailed because agents trained on Laravel will otherwise assume Laravel's request lifecycle and miss Hypervel's Swoole/coroutine constraints.

Mental Model

When working on Hypervel, start from this frame:

  • Development monorepo: Applications install individual subtree-split packages, never hypervel/components. Root composer.json and package.json metadata is development-only. Determine installed-application behavior from the relevant package manifests and the application's dependencies and discovery settings.
  • Hypervel is Laravel-shaped at the API level.
  • Database connections are driver-neutral. ConnectionInterface and Connection support custom drivers registered through DB::extend(), including non-PDO transports.
  • Hypervel is not request-per-process PHP — workers are long-lived, and PHP state does not disappear after each request.
  • Singletons, static properties, manager registries, callbacks, config, and cached metadata can persist for the worker lifetime.
  • Per-request state must live in coroutine-scoped storage (CoroutineContext), not process-global state.
  • Laravel source is the default parity reference, but Laravel internals often assume per-request bootstrap and are not optimized to take advantage of static caching of immutable state.
  • Hyperf source can be useful for Swoole/coroutine behavior, but Hyperf container/config/listener patterns are not the target architecture.
  • Laravel's rate limiter lives under Illuminate\Cache; Hypervel's canonical implementation is the dedicated hypervel/rate-limiter package under Hypervel\RateLimiter. It uses typed policies and dedicated atomic stores and has no Hypervel\Cache alias or primitive counter API. Use this package directly when porting rate-limited Laravel code.

Repository Layout and Commands

Always run framework commands from the repository root. This is the directory that contains this AGENTS.md, the root composer.json, phpunit.xml.dist, phpstan.neon, src/, and tests/.

Key paths:

Path Description
src/docs/ Hypervel documentation and the source for the hypervel/docs package.
src/framework/ Composer metadata for hypervel/framework, the metapackage that installs the standard framework components. Published to its own read-only split repository.
src/testbench/ Hypervel's testbench package (port of orchestra/testbench). Contains TestCase, attributes (WithConfig, WithMigration), and bootstrap logic. Part of the monorepo, not a vendor dependency.
src/testbench/hypervel/ Committed Hypervel app skeleton. On bootstrap, testbench clones this to a disposable temp directory (/tmp/hypervel-components-testbench-{token}-{pid}/) and points BASE_PATH at the clone — tests that write files under BASE_PATH (generated providers, migrations, fixtures, etc.) hit the temp copy, not this committed path. The clone is deleted on shutdown and stale copies from crashed runs are cleaned up. Testbench also exports TESTBENCH_BASE_PATH so subprocesses can locate the active runtime.
src/testbench/workbench/ Committed shared test fixtures (NOT cloned). Subdirs are psr-4-mapped from the monorepo root as Workbench\App\*, Workbench\Database\Factories\*, Workbench\Database\Seeders\* so multiple tests can reuse the same models/factories/seeders without redefining them. Not the runtime app — that's the disposable clone of src/testbench/hypervel/.
skeletons/ Application skeletons and starter kits, each published to its own read-only split repository. hypervel/ is the base application skeleton; react-starter-kit/ is the React starter kit.
docs/ai/ Supplementary agent guides, including porting-hyperf.md (Hyperf conversion mechanics).
docs/todo.md Tracked gaps and improvements worth doing.

Running tests

Always use composer test:parallel to run the full components test suite. This runs raw ParaTest with Hypervel's parallel testing flag supplied by phpunit.xml.dist.

Always use composer test:testbench after Testbench changes. This runs the scoped Testbench package-mode contract suite through the real package:test command.

ParaTest defaults to the machine CPU count when no process count is specified. Redis integration runs need REDIS_TEST_DB_MIN / REDIS_TEST_DB_MAX to cover the chosen worker count, or an explicit --processes / -p value that fits the configured range.

TEST_TOKEN is runner-owned worker identity. Tests must use the assigned value unless they explicitly exercise token-dependent behavior; such tests must restore every environment source they mutate or run in an isolated subprocess.

To run a single test class: ./vendor/bin/phpunit --no-progress path/to/TestClass.php.

Change Workflow

Classify the change

Before editing, identify the work as one of:

  • A framework bug fix.
  • A framework enhancement.
  • An update from an existing package's upstream.
  • A new package port.

This determines which upstream source and tests to compare. Incremental updates from an existing upstream follow the workflow below together with the applicable Porting Policy and rules. New package ports follow the full workflow under Porting Packages. Bug fixes and enhancements follow the workflows below. In every case, read the package README for the upstream reference and the relevant Hypervel source and tests before editing.

Incremental upstream updates

When bringing an existing upstream feature, fix, or API change into Hypervel:

  1. Find the originating implementation pull request and any corresponding documentation pull request. Use them to understand the reason for the change and identify the complete set of files changed when it was introduced.
  2. Inspect every file changed by those pull requests, including source, contracts, tests, fixtures, configuration, package metadata, and documentation. Search the current upstream branch for the added symbols as well, since later changes may have introduced additional consumers or coverage.
  3. Treat the historical pull-request diffs as discovery and history only. Port the actual source, tests, and documentation from the current checked-out upstream default or development branch. Follow-up fixes and documentation improvements may have changed the final implementation or coverage.
  4. Compare that current upstream surface with the Hypervel implementation and apply the approved Hypervel adaptations under Porting Packages. If the upstream feature has no user-facing documentation, add proportionate Hypervel documentation at its natural public surface.

Audit changes during modification and code review

Every audit must explicitly check for overengineering, Laravel-style ergonomics, and avoidable performance or scalability costs, especially repeated hot-path work, excessive database or network round trips (e.g. Redis), inefficient query or index design, unnecessary allocation or serialization, unbounded work, and worker-lifetime memory growth.

Modifying code is an implicit assessment of it. Whenever you edit a method, move code, copy a file, or port from upstream, check what you touch for:

  • Lifetime classification for any container-buildable class you add, port, or whose mutable state you change — see Container.
  • Hardcoded values that should be derived (namespaces, defaults)
  • Defensive code that masks bugs
  • Conventions that diverge from how the framework actually does it
  • Deprecated APIs or dated patterns
  • Issues in the code right next to what you're changing

Anything found follows When to Stop and Report — "the task didn't ask me to fix that" and "I copied it verbatim" are not reasons to stay silent. The trigger is modification or code review; files read only for context don't need a line-by-line audit.

Framework bug fixes

  1. Reproduce the bug with the smallest useful test.
  2. Trace the exact failing code path and identify the root cause before changing source.
  3. Compare the matching Laravel or package-upstream behavior when it defines the contract.
  4. Reach consensus with your peer agent on the root cause and fix through a second-opinion loop before implementing.
  5. Fix the underlying defect — never add a workaround for incorrect framework code.
  6. Add a regression test for the bug and run that test file immediately.
  7. Check types, API parity, coroutine isolation, and worker-lifetime state around the changed code.
  8. Complete the verification workflow below.

Framework enhancements

  1. Read the related Hypervel APIs and tests. Check Laravel or the package upstream for an established public API and behavior.
  2. Decide which state is local, coroutine-scoped, or worker-scoped before writing code — see Coroutine and Worker-Lifetime State.
  3. Preserve Laravel API parity by default. If parity conflicts with Hypervel's architecture, preserves a verified defect or deprecated upstream API, or would require worse code or a workaround, reach consensus with your peer agent on the cleanest design through a second-opinion loop before planning or editing. Never make Hypervel code worse merely to preserve parity.
  4. Test the public behavior, failure paths, coroutine isolation, and cleanup the feature needs.
  5. Complete the verification workflow below.

Verification

Run verification commands sequentially — Never overlap formatting, static analysis or tests. Wait for each command to finish before starting another. A tool’s built-in parallelism, such as ParaTest workers, is fine.

During implementation, run each new or changed test file immediately with ./vendor/bin/phpunit --no-progress path/to/TestClass.php.

Do not routinely run formatting or PHPStan during edits. Run targeted PHPStan earlier only when investigating a typing issue or when its feedback is needed to guide implementation.

At the end of a coherent implementation slice, run composer lint:fix, composer analyse, and the affected tests, in that order. Use PHPUnit for one test file or composer test:parallel -- --processes=4 tests/PackageName for multiple files. Use --filter when needed to narrow the selection.

Run the full test suite only when the change has framework-wide impact. In that case, use composer fix, which includes formatting, analysis and all test suites; do not run those checks separately first. For focused changes, include separate Testbench or dogfood checks when the changed behavior affects those workflows.

After a failure or review correction, fix the cause, repeat only checks affected by the correction, and finish any outstanding verification. Do not restart the entire sequence merely because another review round occurred.

Development Conventions

The Working rules and the Avoid overengineering rules apply to all work in this repo. The Code conventions apply to newly written code. Laravel package ports preserve upstream naming, structure, and style except for the approved adaptations under Porting Packages. Hyperf ports follow docs/ai/porting-hyperf.md.

Apply approved adaptations when porting or modifying code. Do not expand a change solely to apply style or modernization conventions elsewhere, unless that cleanup was explicitly requested.

Working rules

  • Never use subagents without explicit user consent — Do not spawn or delegate work to subagents unless the user explicitly requests or approves their use.
  • Avoid bulk modification tools — tools like sed and replace_all often have unwanted side effects. Never use bulk modification tools without explicit user approval; prefer manual edits. When approved, run them in multiple passes that each target long, exact, case-sensitive strings to avoid accidental changes.
  • One file at a time — never work on multiple files simultaneously. This governs manual editing; package-manager and formatter runs may touch multiple files.
  • Never use Write to overwrite files — always use Edit for targeted updates.
  • Always use cp to copy files and mv to move/rename — never read → write new version → delete old version.
  • Copy before splitting — When a new file or class is primarily extracted from existing code, use cp to copy the primary source first and then update the copy rather than rebuilding it from individual pieces. Copy any additional blocks, including comments and docblocks, into the destination before removing them from their source.
  • Grep broadly — never assume a subdir — when searching for any symbol, class, method, or pattern, grep across the whole src/ (or tests/) tree, not a specific package subdir. Assumptions about where something lives produce false negatives.
  • Read the source before describing behavior — never state how code behaves from memory or Laravel assumptions. Hypervel's coroutine runtime breaks many Laravel assumptions; if you haven't read the relevant source, read it first.
  • Treat past owner decisions as context, not constraints — Previous owner approvals and completed plans explain history but do not determine the best design today. Never retain or reject a design merely because it was previously approved; decide from current requirements, code, and evidence.
  • Revert failed attempts immediately — when a fix doesn't work, revert it before trying another approach. Don't leave experimental code in place.
  • Check dependency versions before adding them — Before adding a package dependency to the root composer.json, check Packagist for the latest compatible stable version. The root composer.lock is intentionally untracked; run composer update after adding or merging dependency changes, do not treat an outdated local lock as a repository defect, and never commit it.
  • Declare only real dependencies — Declare packages used directly by the code or needed for a supported installation. Do not add dependencies merely to complete the list. Add a polyfill only when neither the minimum PHP version nor an existing requirement provides the feature.
  • Keep Composer metadata functional — Declare an ext-* requirement only when the extension is not guaranteed by Hypervel's minimum PHP version. Add a suggest entry only when installing that package enables a concrete, documented feature; conditional interoperability, class-string references, tests, or metadata completeness do not qualify.

Documentation

  • Use one source of truth — Put all user documentation in src/docs/. Package READMEs are intentionally minimal, not a second documentation surface, and must not duplicate user documentation.
  • Write user documentation in Laravel-docs prose — Use the simple, direct, human-friendly style of first-party Laravel documentation. Before drafting, read and match the surrounding prose; for new pages, use src/docs/processes.md as a style reference. Prefer natural explanations and examples over implementation language; avoid internal jargon, stiff wording, and needless detail.
  • Keep the Laravel porting guide current and focused — Whenever a framework change means existing Laravel application or package code, configuration, integrations, or data require adaptation or a compatibility check, update src/docs/porting-from-laravel.md in the same change. Before adding an entry, ask: “What existing Laravel usage would someone port incorrectly without this entry?” Purely additive, opt-in enhancements belong in feature documentation, not this guide. An enhancement that also changes compatibility qualifies because of that difference, not because it adds functionality. Do not document bug fixes, internal implementation differences, performance work, incidental source drift, or narrow edge cases unless they change what a porter needs to do. Briefly identify the affected usage and required action or risk, then link to the relevant canonical documentation instead of repeating its detail. The guide is a high-signal starting context for humans and LLMs, not an exhaustive framework diff or feature catalogue. Treat its context size as a design constraint, and remove stale or duplicated guidance whenever editing it.

Package READMEs

Use this order, omitting items that do not apply:

  1. Package header
  2. Documentation link, when the package has a meaningful user-facing documentation page (Documentation: https://hypervel.org/docs/{documentation-slug})
  3. Differences From Laravel, for deliberate differences only
  4. Upstream link, when the package is a port or deliberately tracks an upstream package (Ported from: https://github.com/{vendor}/{package})

Do not add a documentation link merely for completeness. Omit it when there is no meaningful user-facing documentation page for the package.

Do not add upstream links for inspiration or historical lineage. Omit them when the Hypervel package is maintained independently and is not expected to track upstream changes.

Differences From Laravel is only for deliberate, lasting public contract differences that developers must account for. Bug fixes, correctness or safety fixes, performance improvements, and internal implementation differences do not qualify merely because they make Hypervel behave differently from Laravel. Omit the section when none apply.

When porting upstream updates, keep the package’s differences section accurate for the code being examined: add missing public-contract differences, correct stale descriptions, and remove entries that no longer qualify. Briefly explain the affected usage, why it differs, and the Hypervel equivalent where applicable. Apply this equally to first-party and third-party upstreams; link to detailed documentation rather than repeating it.

Documented differences record current decisions, not permanent requirements. Preserve supported enhancements, but reassess limitations when upstream changes or a clean, performant implementation make them unnecessary; follow the existing approval rules for API changes.

Avoid overengineering

Build complete, long-term solutions, not MVPs or local workarounds. A broad change is correct when the root cause is in shared code, but every added mechanism must solve a real problem.

  • Do not add behavior to guard against unsupported API use. Before adding defensive code, show that normal supported use actually behaves incorrectly and causes meaningful harm. Behavior that is only possible or surprising when an API is used outside its contract is not a bug.
  • Prefer the simplest existing Laravel or Hypervel API, PHP feature, or database constraint. Do not duplicate framework behavior with package-owned machinery.
  • Do not add a new mechanism merely because it sounds robust, flexible, or potentially useful — for example, a registry, retry loop, configuration option, or extension point. It must solve a verified problem, meet a clear approved requirement, support a clearly likely need whose shape is understood, or remove greater complexity elsewhere.
  • Do not add machinery to preserve invariants across deliberate Laravel-style escape hatches such as withoutEvents(), quiet methods, raw builders, raw SQL, disabled middleware, or direct transport access unless the public contract explicitly promises that behavior.
  • For coroutine or worker-lifetime concerns, identify the concrete shared state, a realistic interleaving, and the resulting leak or failure before adding isolation, locking, cleanup, or caching machinery.
  • Fix related symptoms in the lowest shared layer that owns the behavior instead of adding separate defensive paths. The size of the resulting change is not a reason to patch around the defect.
  • A capability may be worth adding before its first use when the need is clearly likely and its design is understood. Do not add generic flexibility for hypothetical consumers.
  • Avoiding overengineering never justifies an incomplete fix, weaker correctness, missing security or isolation safeguards, or deferring a worthwhile improvement.

Code conventions

  • New Hypervel-owned code and packages must be Laravel-style — Design new packages and public surfaces as if they were first-party Laravel packages ported to and enhanced for Hypervel. APIs, naming, class responsibilities, code patterns, and directory structure must be ergonomic, intuitive, and immediately familiar to Laravel developers, while internals remain coroutine-safe and optimized for Hypervel's long-lived Swoole runtime and high-performance requirements. Apply the requirements under Audit changes during modification and code review from initial design onward.
  • Reserve assert* methods for testing APIs — Use prefixes such as ensure* or validate* for runtime guards.
  • Modern PHP 8.4+ with full typing — use constructor property promotion, readonly properties, enums, match expressions, named arguments, and attributes where they fit. Every file declares strict_types=1; parameters, return types, properties, and class constants are natively typed wherever PHP and the inherited API permit (e.g. resource cannot be represented as a native PHP type). PHP does not allow return types on __construct() or __destruct().
  • Contract signature dependencies are lazy — contract signatures may natively reference types from optional split packages without a reverse Composer dependency. Do not remove these types or add cyclic dependencies solely for split-package isolation.
  • Dependency access in new packages — Inject framework service contracts through constructors or methods rather than using helpers, facades, or direct instantiation. Where injection is impractical, such as static contexts or framework-constructed objects, use Container::getInstance()->make(...). Helpers are fine in route and config files.
  • Dependency access within existing packages — Follow the established style of surrounding classes with the same role, including when adding new classes to a ported package. Preserve upstream’s helper, facade and instantiation style where it keeps updates straightforward. A class being newly written is not, by itself, a reason to use a different style. Change dependency access only for a concrete correctness, lifetime, testability or maintainability benefit, and keep related code internally consistent without rewriting unrelated classes.
  • Use imported short class names — applies to new and ported code, including PHPDoc annotations. Replace fully or partially qualified class references with imported short names; use aliases for naming collisions. Classes in the current namespace need no import. Keep fully qualified names where genuinely clearer, such as middleware arrays and similar config-style identifier lists.
  • Group traits in Concerns/ — follow the package's existing convention if it already has a Concerns/ or Traits/ directory; never mix both in one package. New Hypervel-original packages always use Concerns/; a newly ported package keeps its upstream directory name.
  • Use Laravel observer conventions — place Eloquent observers in a top-level Observers/ directory. Register model-specific observers with #[ObservedBy(...)]; use observe() only for dynamic registration or observers supplied automatically by a reusable concern.
  • Use attributed local scopes — define local Eloquent query scopes as protected methods marked with #[Scope], rather than legacy public scopeFoo() methods. Use separate scope classes only for genuine global scopes.
  • No class docblocks unless warranted — only add a class-level docblock if something unusual or complex needs explanation: purpose, architectural role, usage patterns. Never write one that inventories the class — trait lists, method summaries, "registers X, configures Y" — that duplicates the members' own docblocks and goes stale. Method docblocks (title only, Laravel-style, imperative mood: "Return", not "Returns") are always added, except on test methods, whether identified by a test name prefix or PHPUnit’s #[Test] attribute. A body can accompany the title for complex methods that need further explanation.
  • Add comments only where they're genuinely useful (required Laravel-style method title docblocks and config section comment blocks still apply) — a short WHY for logic that isn't obvious from reading the code, the reason behind a bug fix, or logic that's coupled to code in other files and hard to understand in isolation. Avoid unnecessary code comments; don't comment what the code does, and don't annotate framework divergences, routine casts, or type normalizations. When reconciling upstream changes, add missing explanations for non-obvious Hypervel behavior being retained, so future syncs can understand why it exists.
  • Don't make classes final by default — keep classes open; add final only when it protects a real invariant or avoids a concrete framework/API problem, e.g. immutability, coroutine-safety, or a security guarantee.
  • Place methods logically, not at the end — group new methods with related ones (getters with getters, setters with setters). Two exceptions: preserve upstream order when merging ported code (see Porting rules), and flushState() has its own placement rule (see Static state and test cleanup).
  • Only extract methods when justified — extract only when the logic is complex enough to benefit from a name, it's likely to be reused, or two or more methods call it. Don't extract a simple one-liner with a single caller.
  • Never abbreviate variable names — $attributes not $attrs, $connection not $conn.
  • Enum cases use PascalCase by default — case Pending not case pending, case OauthToken not case OAUTH_TOKEN. Applies to both backed and unit enums. Exception: when ->name is used as an external identifier (cache keys, cookie names, filesystem disks, rate limiter names, timezone strings) or appears in serialized output (e.g., toArray() returning 'name' => $this->name), match the consuming system's convention (typically lowercase or snake_case).
  • Strict comparisons by default — Use === and !== in newly written code unless the supported API requires loose comparison. When converting an upstream comparison, verify operand types ($value === 0.0 for a float, not === 0) and preserve its behavior, including coercion and object equality. Keep loose comparisons where their semantics are intentional and needed to preserve supported behavior; do not replace them solely for consistency.
  • Prefer union types over mixed when all types are known — mixed is only for truly unconstrained values or cases that cannot be safely narrowed after control-flow analysis.
  • Type decisions must be evidence-based — check corresponding Laravel/Hyperf signatures and docblocks as a reference, then trace the real control flow through method bodies across all callers and callees to confirm the types are correct.
  • Fail fast with framework and PHP exceptions — don't add guards, wrapping, or defensive checks unless they handle a reachable invalid case or materially improve the error, and never swallow exceptions. If code would fail anyway (e.g. null passed to a typed parameter), let it fail naturally instead of adding a check that throws a custom exception — the stack trace is enough to diagnose.
  • Use semantic column types in migrations — uuid() / foreignUuid() / uuidMorphs() for UUIDs, ipAddress() / macAddress() for network addresses, and jsonb() for JSON the database queries, indexes or validates. Use text() for opaque serialized values that are only read back whole. PostgreSQL gets the real types (jsonb, uuid, inet, macaddr); the other supported databases fall back automatically to compatible types.
  • No arbitrary string lengths — use ->string('name'), not ->string('name', 100). Don't invent limits; specify a length only when the domain or protocol defines one — exact (UUID: 36, ULID: 26, sha-256 hex token: 64) or a defined maximum (IPv6 address: 45).
  • No database enums — use string columns plus PHP enums. Adding a value to a database enum requires a migration.
  • Use dateTime for arbitrary dates — use dateTime instead of timestamp for scheduled dates, expiry dates, historical dates, and similar arbitrary dates. Keep timestamp for dates recorded when an action happens, such as created_at, updated_at, and deleted_at. On MySQL and MariaDB, timestamp only supports 1970–2038.
  • Prefer timestamp over timestampTz in migrations — store times in UTC with plain timestamp columns, matching the normal convention in Laravel and Hypervel first-party migrations. Reserve timestampTz for columns that genuinely need database-level timezone semantics, such as existing timezone-aware schemas or columns written under different session timezones. The schema API supports both — this is a column-choice convention, not an API restriction.
  • Guard optional event dispatches with hasListeners() — before constructing and dispatching framework events, guard them with hasListeners() so hot paths skip event overhead when nobody is listening. Bare * listeners are passive observers and do not count; targeted wildcards do. Do not guard dispatches where dispatching is the side effect, such as jobs, broadcasts, webhooks, or command bus calls.
  • Use Sleep::usleep() / Sleep::sleep() for delays in source code — Sleep is fakeable in tests. Use raw sleep() / usleep() only where real time must pass, such as test harnesses and external-process polling.
  • Use xxh128 for internal non-cryptographic hashing — cache and context keys, content checksums, and change detection. It is faster than sha256, which is reserved for trust boundaries: stored credential digests, signatures, and anything an attacker gains by forging. Seed it when the hashed value comes from user input, as SwooleStore does for its physical table keys.
  • Use immutable dates by default — Hypervel defaults to Hypervel\Support\CarbonImmutable, including where Laravel uses mutable Carbon. Create public or application-configurable dates through the Date facade or date helpers, and use exact CarbonImmutable for framework-owned internal or held values. Type configurable Carbon boundaries as CarbonInterface and native or third-party boundaries as DateTimeInterface. Capture the return value of every date modifier whose result must persist. Use Hypervel\Support\Carbon only for explicit mutable opt-out or conversion behavior.
  • Always use American English spelling — E.g., "behavior" vs "behaviour", "utilize" vs "utilise".

Configuration

These rules apply to all code, including ported code.

  • Laravel-style config files — Group related settings under Laravel's standard section comment blocks, with concise, user-facing explanations.
  • Always use typed getters for values with one non-null type. Only use get() when null, union, or mixed values are meaningful. Add a test for any supported null behavior.
  • Cast environment-backed booleans and numbers in config files; if null is supported, cast only non-null values. Consumers must not repeat those casts. When a factory accepts raw configuration records, normalize types and documented optional defaults once at that boundary; never supply missing required members.
  • Required settings live in shipped config and must not have a code-level fallback, so missing or misspelled keys fail loudly.
  • Intentionally optional settings keep one owning fallback and remain discoverable in config or feature documentation. Document what omission or null means.
  • Framework and package defaults are shallow-merged with application config. mergeableOptions() is only for named groups such as connections or stores: application entries replace matching defaults, while other default entries remain. Other nested arrays are replaced as a whole.
  • Replaceable named and nested records must apply documented optional defaults at their owning boundary; missing required members must still fail.
  • A non-obvious fallback value shared by multiple source paths must have an owning constant. If user-facing config exposes the same default, keep its concrete value readable and add a focused test asserting that it matches the constant. Do not create constants for obvious source fallbacks such as true, false, null, [], or 'default'.
  • Env var naming — Ported config keeps upstream names. New Hypervel-specific settings should use the established prefix for the package or subsystem that owns the value (SERVER_, CACHE_, REDIS_, etc.). Determine ownership semantically, not from the config filename: aggregate files such as app.php contain multiple domains, and APP_ is for genuinely application-wide settings. If a value mirrors another config key, reuse that key's environment variable instead of defining a duplicate.
  • Use resolve...Using for Hypervel-owned config resolvers — prefer this naming for callbacks that resolve config-derived values, unless an established Laravel domain convention already exists, such as redirectUsing().

Container

Hypervel's container keeps Laravel's named API surface — bind(), singleton(), scoped(), instance(), aliases, contextual bindings — with resolution adapted for long-lived Swoole workers. Container ArrayAccess and dynamic service properties are intentionally unsupported. make() and get() resolve identically; get() is the PSR-compliant exception wrapper. Container::getInstance() auto-creates via ??= new static(), so it always returns a container.

Resolution semantics vs Laravel

The critical difference: unbound concrete classes are auto-singletoned. In Laravel, make() on a class with no binding builds a fresh instance every call. In Hypervel, the first resolution caches the instance (in $autoSingletons) for the worker lifetime. Worker-safe services can reuse initialized state, avoiding repeated construction cost and allocation. Explicit bindings override this (bound classes follow their binding type), and SelfBuilding and Transient classes are excluded.

Registration Laravel Hypervel
Unbound concrete class Fresh instance every make() Auto-singletoned on first make(); cached for the worker lifetime
singleton() / #[Singleton] Cached in $instances Same — but the worker serves many requests, so the instance is shared across all of them
scoped() / #[Scoped] A worker-lifetime singleton the runtime flushes between requests (forgetScopedInstances()) Cached per coroutine via CoroutineContext — isolated between the concurrent requests in one worker
bind() Fresh instance every make() Same
make($abstract, $params) / makeWith() Contextual build — never cached Same — parameters bypass the singleton, scoped, and auto-singleton caches
build($concrete) Constructs the concrete directly, bypassing bindings and caches for the top-level class Same; Hypervel adds buildWith($concrete, $params). Nested constructor dependencies still resolve through the container
implements SelfBuilding Container calls the class's static newInstance() Same, and it also skips auto-singletoning
implements Transient No equivalent marker Unbound resolutions are always fresh; explicit bindings still determine the lifetime. Eloquent models inherit this marker from Model

Rules that follow from this:

  • Ownership of mutable state decides the lifetime. Most services, middleware, listeners, factories, and formatters are safe to auto-singleton because they are stateless or hold only worker-owned state. State a service computes and keeps for reuse — caches, registries, resolved configuration, callbacks, or connections — is intended worker-lifetime state, so those classes remain auto-singletons. State owned by a caller, request, or operation must not be stored on a worker-shared object: keep invocation state in CoroutineContext, use scoped() for one instance per coroutine/request, bind() for a fresh bound instance, or Transient only when freshness is intrinsic to the whole hierarchy.
  • A class that captures request state in its constructor and is neither scoped nor execution-scoped is a coroutine-safety bug — reach consensus on the fix through a second-opinion loop with your peer agent before implementing. Contextual attributes such as RouteParameter and CurrentUser already make their containing class execution-scoped.
  • Do not infer Transient from mutable properties, setters, or a no-argument constructor. Remove incidental mutation instead of marking the class as Transient, and verify every subclass before marking a base class. For example, Collection holds caller-owned values and is Transient, while Manager::$drivers is a service-owned cache and remains auto-singletoned.

Choosing a resolution strategy

  • Unbound concrete service whose state is safe to share for the worker lifetime → do not bind it; auto-singletoning is the default.
  • Interface or canonical service key shared for the worker lifetime → singleton().
  • Existing object intentionally shared for the worker lifetime → instance() during boot, or in tests for a swap.
  • Unbound class whose constructor uses an ExecutionScopedAttribute → do not bind it; the container scopes it automatically to the current execution.
  • One instance per coroutine / request → scoped().
  • Fresh instance required by a specific binding → bind().
  • Freshness intrinsic to the whole class hierarchy → Transient.
  • One resolution should ignore the implicit auto-singleton and constructor-derived execution scope while retaining aliases, bindings, extenders, and resolving callbacks → makeTransient().
  • Class-controlled construction through newInstance() → SelfBuilding.
  • One contextual resolution with explicit constructor parameters → make() / makeWith() with parameters. Aliases, binding definitions, and resolving callbacks still apply, but cached lifetimes are bypassed.
  • Direct construction that intentionally bypasses top-level bindings and caches → build() / buildWith(). Do not use these when aliases, test swaps, or resolving callbacks must be honored.

Binding patterns

1. Canonical string key — use a closure with new:

// The concrete (AuthManager) is listed as an alias for 'auth', so
// singleton('auth', AuthManager::class) would create a circular resolution cycle:
// 'auth' -> build AuthManager -> getAlias(AuthManager) -> 'auth' -> infinite loop
$this->app->singleton('auth', fn ($app) => new AuthManager($app));

2. Abstract is not in the alias table — use string concrete:

// Neither FormatterInterface nor DefaultFormatter are aliases for anything,
// so the container can resolve this directly without cycles.
$this->app->singleton(FormatterInterface::class, DefaultFormatter::class);

3. Abstract and concrete are the same class — do not bind merely to share it. Hypervel's container auto-singletons unbound concrete classes on first resolution. An explicit singleton(Foo::class) is redundant unless the class declares an intrinsic fresh lifetime through Transient or SelfBuilding:

// Wrong: redundant; auto-singleton handles this.
$this->app->singleton(BroadcastManager::class);

// Correct: do not bind it. The first make(BroadcastManager::class) auto-singletons it.

Aliases and canonical keys

Before binding core framework services, check Application::registerCoreContainerAliases().

If the abstract or concrete participates in the alias table, choose the binding key carefully. bind() and singleton() store bindings under the exact abstract key passed; resolve() resolves aliases before lookup. Binding an alias instead of the canonical key can orphan the binding.

When adding a core alias, use the string key as the canonical abstract:

// Wrong: contract is canonical.
\Hypervel\Contracts\Auth\Factory::class => [
    'auth',
    \Hypervel\Auth\AuthManager::class,
],

// Correct: string key is canonical.
'auth' => [
    \Hypervel\Auth\AuthManager::class,
    \Hypervel\Contracts\Auth\Factory::class,
],

For canonical string keys such as 'auth', 'cache', 'db', 'request', and similar framework services, prefer closure bindings:

$this->app->singleton('auth', fn ($app) => new AuthManager($app));

Facades for these services should resolve the canonical container key instead of the concrete manager class.

Do not add new core aliases without need. Only add an alias when Hypervel needs it as part of its public container surface.

Providers and Listeners

Provider registration

When adding a package provider, register the provider and aliases through the package Composer metadata and the root Composer metadata, as described in the package skeleton workflow under Porting Packages. Add a provider to DefaultProviders only if every Hypervel application needs it at framework startup, such as auth, cache, database, session, validation, view, or low-level Swoole infrastructure. Optional packages such as Reverb, Scout, Telescope, Sentry, and Watcher should rely on package discovery instead.

Before changing registerBaseServiceProviders(), reach consensus with your peer agent through a second-opinion loop that the service is needed during bootstrap, before normal providers are registered. Reserve this method for framework infrastructure needed by the boot process itself.

It is safe to have the same provider listed in both registerBaseServiceProviders() and extra.hypervel.providers when early loading is genuinely needed. Application::register() deduplicates providers by class name, and the discovery entry ensures standalone installs still load the provider.

Boot-time callbacks and resolvers

For callbacks and resolvers registered during provider boot, inject worker-safe dependencies into boot() and capture them in the closure. Do not repeatedly resolve them from the container when the callback runs. Event listeners are the exception described below: resolve listeners at event time so application and test rebindings are honored.

Listener registration

Register listeners in the service provider's boot() method using closures that resolve the listener from the container:

public function boot(): void
{
    $events = $this->app->make('events');

    $events->listen(AfterWorkerStart::class, function (AfterWorkerStart $event) {
        $this->app->make(AfterWorkerStartListener::class)->handle($event);
    });
}

Resolve from the container ($this->app->make(...)) rather than injecting or instantiating directly — this ensures constructor dependencies are resolved and the listener benefits from auto-singleton caching.

Coroutine and Worker-Lifetime State

Decide where state lives before writing code:

State Storage
Immutable metadata shared by all requests Static property cache or worker-lifetime singleton
Stateless service shared by all requests singleton() or auto-singleton (see Container)
Mutable state for one request, operation, or coroutine CoroutineContext or a scoped() binding
Fresh mutable object per resolution bind(), contextual parameters, or Transient for an intrinsically fresh class hierarchy
  • Use Hypervel\Context\CoroutineContext for invocation-scoped state — anything that must not be visible to other concurrent coroutines in the same worker. Static properties and singleton fields leak across coroutines: whatever one coroutine sets becomes visible to all others in the worker. Use the established key-naming convention: __<package>.<key> value prefix, _CONTEXT_KEY / _CONTEXT_KEY_PREFIX constant suffixes, public only when other classes or tests reference the constant. Do not use Hypervel\Support\Facades\Context as the low-level coroutine store; it provides Laravel-style application context instead.

  • Group related coroutine state in …State classes when useful. Store the object in CoroutineContext so callers can retrieve it once and read or update several properties without repeated context lookups or array copying. Use named properties for known fields and arrays for keyed collections. When copied child coroutines need independent state, implement ReplicableContext with the appropriate copying behavior. See InertiaState and ContainerResolutionState.

  • Configure process-global values only during worker boot — config is a process-global singleton, so Config::set() during request handling changes behavior for every concurrent request in the worker. Never mutate config for request-specific behavior; use CoroutineContext or middleware instead. Provider boot-time configuration is fine — it runs once per worker.

  • Create Swoole tables from a BeforeServerStart listener, never in register() — register() runs in every process, including ones that never use the table. Managers that create tables by name also seal() there. Swoole frees table memory only on destroy(), which only the creating process may call — never from a destructor, since forked workers run those too.

  • Name static cache properties for what they store — not with a Cache suffix; static properties in Swoole workers are caches by nature. Exception: matching an existing Laravel-ported pattern in the same class (e.g. $classCastCache, $attributeCastCache on HasAttributes).

  • Bound worker-lifetime lookup caches — any internal lookup cache retained across requests in a worker—for example, in static properties or singleton instances—must have a naturally limited set of keys or discard entries that are safe to recompute. Do not add a size limit merely to hide growth from request- or user-derived keys. This governs framework-derived caches, not application-owned cache stores such as the worker-array driver.

  • Review worker-lifetime state explicitly — before introducing or modifying static properties/caches, singletons or other long-lived state, assess the Swoole persistence impact (memory leaks, cross-request behavior) and reach consensus on the design through a second-opinion loop with your peer agent.

  • Document worker-lifetime mutators — when adding or touching a public method that mutates static state, singleton-held configuration, manager registries, cached drivers, global callbacks, or other worker-lifetime state, add a short warning to the method docblock if the method is intended only for boot-time configuration or tests. Use the tag-first format so humans and LLMs can recognize it quickly:

    • Boot-only. — for startup configuration methods
    • Tests only. — for test fakes, swaps, and resolver overrides
    • Boot or tests only. — for cache/registry clearing methods used during boot reconfiguration or test cleanup

    The second sentence should name the concrete failure mode, e.g. "The callback persists in a static property for the worker lifetime and affects every subsequent request." Do not add these warnings to methods that are genuinely safe for normal runtime/per-request use. If a method is commonly expected to be used dynamically but mutates shared worker-lifetime state, treat that as a coroutine-safety bug and reach consensus on the fix through a second-opinion loop with your peer agent. Never just document it.

  • Assess static caching opportunities — if a path repeatedly computes expensive stable metadata and worker-lifetime static caching would be a clear win, reach consensus with your peer agent through a second-opinion loop on what to cache, the expected benefit, bounded memory use, and correctness across requests before implementing.

Classes that use static caching need a flushState() method for test cleanup — see "Static state and test cleanup" under Writing Tests.

  • Release idle pooled connections before unrelated long waits — Holding connections while waiting for external services or child coroutines can exhaust pools and limit concurrency. Release them at the appropriate shared boundary, preserving active operations and session ownership; consider reacquisition costs rather than adding release calls indiscriminately. For database sessions, reuse ConnectionResolver::releaseIdleConnections(), which preserves transactions and explicit pins. See Mailer::sendSymfonyMessage() and ConcurrentImportRunner::wait() for examples.
  • Treat coroutine cancellation separately from errors — Code that owns coroutine cancellation must pass CanceledException through unchanged and clean up the children or resources it owns. Add this only where cancellation can actually happen and mishandling it causes a real bug—not to every catch (Throwable).
  • Contain cancellation in direct engine coroutines — A callback passed directly to Engine\Coroutine must catch cancellation after cleanup so it cannot terminate the worker. The higher-level Coroutine class already provides this protection.

When to Stop and Report

Resolve the technical issues below through evidence-based second-opinion loops with your peer agent, then proceed on consensus. Ask the owner only for unapproved public API departures or capability removals; database schema changes; meaningful performance regressions or weakened guarantees; consequential tradeoffs the requirements do not settle; unresolved peer disagreements; or actions requiring owner authorization.

  • Investigate anything unusual — missing dependencies, logic needing special consideration, things that don't make sense for Hypervel, etc. Investigate and reach consensus on the findings and recommended action through a second-opinion loop with your peer agent before implementing. Do not dismiss concerns as theoretical without investigation. If no supported, realistic path or meaningful harm is found, do not present it as a defect or add machinery to address it.
  • Do not defer work without owner authorization — do not remove functionality or substitute stubs or TODOs for implementation unless the owner authorizes it. Preserve existing TODO comments unless their underlying work is verified complete or the owner directs otherwise. If blocked, reach consensus on a complete solution through a second-opinion loop with your peer agent.
  • Investigate source code bugs — for bugs found in Hypervel or upstream code being ported, including through PHPStan or tests, reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing. Upstream bugs must be fixed, not ported as-is.
  • Trace upstream differences before calling them bugs — a difference from Laravel or an upstream package is not proof that Hypervel is wrong, and matching upstream is not proof that Hypervel is correct. A verified Hypervel defect remains a defect when upstream has the same problem.
  • Do not work around incorrect existing code to avoid churn — if work exposes incorrect types, wrong logic, missing methods/classes, or other real defects in existing Hypervel code, fix the underlying code instead of adding compatibility hacks or local workarounds to sidestep the problem. Prioritize correctness and code quality over keeping the change small. For any non-trivial fix, reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing.
  • Never weaken or drop tests to work around source issues — if a test exposes source-side problems (wrong types, broken logic, missing classes/methods, signatures that diverge from Laravel, missing API parity, etc.), reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing. Never delete, skip, loosen assertions, or alter tests to make them pass against flawed source code. The test is the spec; the source gets fixed. For type errors specifically, "When tests expose source code type errors" under Writing Tests covers how to identify the correct type.
  • Never dismiss issues as "out of scope" or "pre-existing" — when any issue surfaces (bugs, unexplained divergences or API gaps, incorrect visibility, type inconsistencies, naming mismatches, etc.), investigate and reach consensus on the findings and solution through a second-opinion loop with your peer agent before implementing. Never use phrases like "out of scope", "pre-existing", "not part of this work", "separate concern", or "unrelated" to justify ignoring an issue. Only the owner may authorize leaving a confirmed issue unresolved.

Worker-lifetime state has additional second-opinion requirements — static/singleton state changes, unsafe public mutators, static caching opportunities, and per-request state captured by auto-singletons — listed under Container and Coroutine and Worker-Lifetime State.

Handling failing tests

  • Investigate tests broken by your changes before updating them. When an implementation change causes a previously passing behavioral test to fail, treat the test as evidence of a contract or missed edge case. Trace what it protects through the tested code, callers, upstream source/tests, and relevant history. Before changing the test, reach consensus with your peer agent through a second-opinion loop that the behavior change is correct, including its compatibility and edge-case consequences.
  • Easy fixes (namespace typos, missing return types, a missed namespace update, etc.) — fix and continue.
  • Non-trivial failures (behavioral changes, test logic issues, unclear root causes) — investigate and reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing. Investigate all failures thoroughly — don't assume a failure is caused by your change without confirming it.
  • Do not skip or remove tests except under the rules below. Never comment out, skip, or avoid porting a test because the required functionality is missing. If Hypervel should support it, investigate the root cause and reach consensus on the fix through a second-opinion loop with your peer agent. Keep every applicable upstream test, even if redundant or seemingly unnecessary; exclusions require justified Hypervel architectural or API differences. Remove tests for the approved unsupported features listed under Porting Laravel Tests without asking. For any other exclusion or removal, reach consensus through a second-opinion loop on what the test covers and why removal is justified.

Writing Tests

These rules apply to all tests — new tests for framework work and ported tests alike.

Avoid overengineered tests

Test supported public behavior, meaningful branches, verified regressions, and realistic coroutine or worker-lifetime failures. Do not add production APIs, branches, or defensive machinery solely to make speculative states testable. Do not require invariants to survive deliberate framework escape hatches unless the public contract promises that behavior.

Exceptions in tests

In any test, PHPUnit assertion failures, skips, and incomplete markers extend AssertionFailedError, which extends RuntimeException; a catch must not swallow one and let the test pass. An exception test must fail unless its intended behavior and exception path occurred. Before rewriting an existing or ported exception test, demonstrate a concrete violation; a broad catch alone is not a defect. When writing a new test that controls the exception, prefer prebuilding it and asserting its identity after the catch.

Directory layout

Laravel-ported tests follow upstream placement; the rules below apply to Hypervel-only test files. All tests live in tests/{PackageName}/ (PascalCase). Tests that require external services go in tests/Integration/{PackageName}/ — see Integration tests below. When only some integration tests for a package require one service, group them in tests/Integration/{PackageName}/{ServiceName}/. When every integration test for the package requires that service, keep them directly in the package directory.

Package-specific tests that require one database driver go in tests/Integration/{PackageName}/Database/{Postgres|MySql|MariaDb|Sqlite}/. The database workflows discover these directories by convention.

Base classes

Never extend PHPUnit\Framework\TestCase directly. Always use one of these:

Class Use When
Hypervel\Tests\TestCase Unit tests, mocks only, no container needed. Tests that only need an isolated scratch directory stay here — use ParallelTesting::tempDir() (see Temp directories below).
Hypervel\Testbench\TestCase Integration tests (needs container for facades, config, DB, etc.) or any test that needs a full app skeleton / writes through BASE_PATH — testbench clones a disposable runtime skeleton per run and exposes its path via BASE_PATH (and TESTBENCH_BASE_PATH for subprocesses), deleted on shutdown. Committed source is never mutated.

Always call parent::setUp() in your setUp method.

Configuration

Put application environment configuration in defineEnvironment() or a #[DefineEnvironment] method. Do not mutate application config in setUp(), because providers or resolved services may already have consumed it.

Within test methods, use config() directly to read or mutate configuration values. Do not use no-argument config() as a dependency source; resolve the Repository contract from the container when constructing a test subject manually.

Test support files

All standalone test support files — PHP classes, non-class PHP files, and non-PHP files (JSON, SQL, images, templates, etc.) — go in a single Fixtures/ directory (capital F). This matches Laravel's predominant convention. PHP classes in Fixtures/ are PSR-4 autoloaded like any other test file.

Put independently reusable fixture classes and enums in separate PSR-4 files. Keep single-test helpers inline; never share inline helpers between test files. Keep Integration-only fixtures under Integration, and share fixtures across unit and Integration tests only when it makes the tests clearer. Preserve combined files when their structure is itself part of what is being tested.

Helper class namespacing

Tests often define helper classes (models, stubs) with generic names like User, Post, or Comment. When multiple test files use the same namespace and define classes with the same name, PHP throws "Cannot redeclare class" errors.

Use test-specific namespaces only for collision-prone helper classes (matching Laravel's pattern):

// WRONG - shared namespace causes conflicts for generic helper names
namespace Hypervel\Tests\Integration\Database;

class EloquentDeleteTest extends DatabaseTestCase { ... }
class Comment extends Model {}  // Conflicts with Comment in other files!

// CORRECT - test-specific namespace isolates generic helper names
namespace Hypervel\Tests\Integration\Database\EloquentDeleteTest;

class EloquentDeleteTest extends DatabaseTestCase { ... }
class Comment extends Model {}  // No conflict - different namespace

Use this when helper classes have generic names likely to appear in other test files. Do not add extra namespaces for helper classes whose names already include the tested feature or package context, such as FailingHorizonInstallCommand or MissingProviderTelescopeInstallCommand.

When a test-specific namespace is needed, the namespace includes the test class name as the final segment. This means:

  • Each affected test file has its own namespace
  • Generic helper classes can use simple names (Comment, Post, User)
  • No $table properties needed (Eloquent derives comments from Comment)
  • No explicit foreign keys needed (Eloquent derives user_id from User)

PHPUnit loads test files directly (not via autoloading), so the namespace doesn't need to match the directory structure.

Temp directories for file I/O

Tests that write files to disk must never write to the committed tests/ directory. For tests needing a full app skeleton, Testbench\TestCase handles this automatically (see testbench entry in the paths table above). For any test that only needs an isolated scratch directory, use ParallelTesting::tempDir('TestName') — store it as a property, delete any leftover copy and create it fresh in setUp, then delete it again via Filesystem::deleteDirectory() in tearDown. Use sys_get_temp_dir() directly only when the system temporary path itself is the behavior under test. See FoundationViteTest or OptionTest for the pattern.

The Testbench skeleton clone is shared for the whole worker, so tests that write under BASE_PATH must restore or delete the exact files they touch in tearDown(). For .env files, prefer useEnvironmentPath() with an isolated ParallelTesting::tempDir() directory.

Coroutine support

All tests run inside coroutines by default. The RunTestsInCoroutine trait is on both base test cases (Hypervel\Tests\TestCase and Hypervel\Foundation\Testing\TestCase / Testbench), so each test method automatically runs in a fresh coroutine. Context is destroyed when the coroutine ends — no manual cleanup needed.

Never add use RunTestsInCoroutine; to individual test classes. It's inherited from the base class. If you encounter a test extending raw PHPUnit\Framework\TestCase, change it to extend Hypervel\Tests\TestCase instead.

Opting out of coroutines: Set protected bool $runTestsInCoroutine = false; on the test class. This is needed when:

  • Tests call run() directly to create their own coroutines (e.g., pool management tests, parallel HTTP tests)
  • Tests explicitly verify non-coroutine → coroutine transitions

PHPUnit constraint: setUp() and tearDown() run outside the test method's coroutine (PHPUnit 13's runBare() is final). For DB operations in setUp/tearDown, Foundation TestCase provides runInCoroutine() which creates temporary coroutines and bridges transaction state via preserveTransactionContext().

Optional hooks for code that must run inside the test's coroutine:

  • setUpInCoroutine() — runs inside the coroutine before the test method
  • tearDownInCoroutine() — runs inside the coroutine after the test method

These are primarily useful for DB operations or external service setup that needs coroutine context. Most ported Laravel tests won't need them.

Testing coroutine state isolation

To prove state is per-coroutine (not shared on a worker-lifetime singleton), spawn concurrent coroutines via parallel() from Hypervel\Coroutine and usleep() between mutation and read — the sleep forces the runtime to interleave them; without it tasks may complete sequentially and the leak won't reproduce.

use function Hypervel\Coroutine\parallel;

[$a, $b] = parallel([
    function () use ($service) { $service->set('A'); usleep(5000); return $service->get(); },
    function () use ($service) { $service->set('B'); usleep(5000); return $service->get(); },
]);

Examples: tests/Inertia/CoroutineIsolationTest.php, tests/Container/CoroutineSafetyTest.php. Name new tests CoroutineIsolationTest / CoroutineSafetyTest for discoverability.

Request context in tests

request() resolves from RequestContext — when no request exists in context (tests that don't make HTTP requests), each request() call creates a throwaway fallback instance. This means request()->merge() has no effect on subsequent request() calls. Replace request()->merge(['key' => 'value']) with RequestContext::set(Request::create('/?key=value')) to seed a stable request in context.

Seed application requests with RequestContext::set(); replacing the 'request' binding with instance() bypasses coroutine-local behavior.

Static state and test cleanup

AfterEachTestSubscriber handles framework-global cleanup between tests. It calls flushState() on framework classes that hold static state, and resets the container itself — Container::flushState() + setInstance(null) — plus CoroutineContext::flush(), with each test in a fresh coroutine. So container singleton/auto-singleton instance state and coroutine context don't leak between tests; only static/process-global state and live external resources need package cleanup, not mutable state on a container-cached instance. Do not duplicate framework-static resets in tearDown(); AfterEachTestSubscriber is their one authoritative registry. Tests still own resources they create, such as child coroutines, subscribers, processes, sockets, and temporary files, and must close or join them through exception-safe cleanup.

Tests that create a Swoole\Table pass it to trackSwooleTable() from InteractsWithSwooleTables; otherwise its memory stays allocated until the PHPUnit worker exits.

When writing or porting source classes that use static properties for caching (e.g., $booted, $globalScopes, resolved config values, compiled formats):

  1. Add a public static function flushState(): void method that resets the static properties to their initial values
  2. Check whether the subscriber (src/testing/src/PHPUnit/AfterEachTestSubscriber.php) should call it — if the cached state could leak between tests and cause failures, add the call

Framework-owned classes go in AfterEachTestSubscriber. First-party optional framework packages must stay in grouped optional methods at the bottom of that subscriber and must be invoked through callIfExists(). Third-party packages, private packages, and applications should register cleanup for process-local state that survives application teardown through extra.hypervel.test-state and a TestState registrar instead of hardcoding their classes into the framework subscriber. These callbacks run after the test application is destroyed, so they must not resolve container services; external resources remain owned by their test traits.

Do not add Hypervel\Testing\PHPUnit\AfterEachTestCleanup itself to AfterEachTestSubscriber. Its callbacks are suite-level registrations that must persist for the PHPUnit worker lifetime.

Place flushState() at the end of the class. The only exception is when the class has trailing magic dispatch/lifecycle methods (__call, __callStatic, __get, __set, __isset, __unset, __destruct) at the end; in that case, place flushState() immediately before that trailing magic-method block. __invoke() is not a placement anchor.

Use the standard title docblock for flushState() methods:

/**
 * Flush all static state.
 */

Do not add Boot-only., Tests only., or Boot or tests only. warning paragraphs to flushState() docblocks. Those warnings belong on public mutators and registrars that userland might call incorrectly, not on this test cleanup hook that is only registered in AfterEachTestSubscriber.

Keep the docblock to the title only — no extra paragraphs. If the method body has a non-obvious WHY worth explaining (ordering constraints, late-static-binding subtleties, etc.), put it as an inline comment above the relevant line inside the method, not as an extra paragraph under the title.

When the property's initial value and flushState()'s reset value share a literal (a number, string, class name, etc.), extract it to a DEFAULT_* class constant and reference it from both sides — this prevents drift if the default ever changes. Make the constant public only if tests or external callers reference it; otherwise protected. Nullable lazy caches and callback slots are exempt: null there is the structural "not yet computed" sentinel required by the ??= pattern, not a configurable default — initialize and reset with a literal null, no constant.

public const DEFAULT_TRUNCATE_AT = 120;

public static false|int $truncateAt = self::DEFAULT_TRUNCATE_AT;

public static function flushState(): void
{
    static::$truncateAt = self::DEFAULT_TRUNCATE_AT;
}

Per-package base test cases

Do not create per-package abstract test case classes (e.g., EngineTestCase, CoroutineTestCase) just for coroutine support — it's already on the base class.

A per-package base class is only justified when there is shared setUp logic — e.g., shared container mock setup, shared helpers, or shared test fixtures that multiple test classes in the package need.

Mockery

Always import as m: Use use Mockery as m; and call m::mock(), m::spy(), etc. Never use the full Mockery:: prefix.

Framework base test cases own Mockery verification in tearDown() so unmet expectations are attributed to the test that created them. The global AfterEachTestSubscriber remains a fallback for tests using another base case and always resets framework state even when Mockery verification fails. Tests must not add their own Mockery::close() calls.

Stricter typing

Hypervel uses stricter types than Laravel. Laravel-trained habits produce incomplete test mocks that loose typing silently accepts — Hypervel's types reject them. This applies to new tests and ported tests alike.

Model properties require type declarations:

// Laravel
protected $table = 'users';
protected $fillable = ['name'];
public $timestamps = false;

// Hypervel
protected ?string $table = 'users';
protected array $fillable = ['name'];
public bool $timestamps = false;

Mock return types must match:

// Laravel (loose - stdClass works)
$connection = m::mock(stdClass::class);

// Hypervel (strict - use correct type)
$connection = m::mock(PDO::class);
$query = m::mock(QueryBuilder::class);

Fluent methods need return values:

// Laravel (null return silently accepted)
$builder->shouldReceive('where')->with(...);

// Hypervel (must return for chaining)
$builder->shouldReceive('where')->with(...)->andReturnSelf();

Mocking methods with static return type:

Methods like newInstance() have static return type, meaning they must return the same class (or subclass) as the object they're called on. Mockery creates proxy subclasses, so returning the parent class fails:

// FAILS - mock is Mockery_1_MyModel, returning MyModel fails static type
$this->related = m::mock(MyModel::class);
$this->related->shouldReceive('newInstance')->andReturn(new MyModel);

// WORKS - use partial mock and andReturnSelf()
$this->related = m::mock(MyModel::class)->makePartial();
$this->related->shouldReceive('newInstance')->andReturnSelf();

// Test attributes on the mock itself (partial mock has real Model behavior)
$result = $relation->getResults();
$this->assertSame('taylor', $result->username);

This is a testing-only issue — the strict types are correct and an improvement. In production code, you never mock Models and call newInstance().

When andReturnSelf() isn't enough:

If a test needs to verify distinct instances (e.g., makeMany() returns different objects), use a concrete test stub instead of mocks:

class EloquentHasManyRelatedStub extends Model
{
    public static bool $saveCalled = false;

    public function newInstance(mixed $attributes = [], mixed $exists = false): static
    {
        $instance = new static;
        $instance->setRawAttributes((array) $attributes, true);
        return $instance;
    }

    public function save(array $options = []): bool
    {
        static::$saveCalled = true;
        return true;
    }
}

// Test verifies real behavior, not mock expectations
$this->assertNotSame($instances[0], $instances[1]);
$this->assertFalse(EloquentHasManyRelatedStub::$saveCalled);

Concrete stubs are the correct approach here — they test actual behavior rather than just verifying mocks were called correctly.

When tests expose source code type errors

If a test fails with a type error, the source code type may be wrong — not the test. Types should be correct, not just strict. A narrow type that doesn't cover all valid cases is incorrect.

How to identify:

  • Test returns/passes a type that the source code should accept but doesn't
  • The type is a parent class of what's currently declared (e.g., Support\Collection vs Eloquent\Collection)

How to fix:

  1. Identify all valid types the method can accept/return
  2. Use the common base type that covers all cases without being unnecessarily loose
  3. Fix the source code, not the test

Example: A method returns Eloquent\Collection normally, but an afterQuery callback can return Support\Collection. Since Eloquent\Collection extends Support\Collection, the correct return type is Support\Collection — it covers both cases precisely.

Wrong approach: Removing types, using mixed, or modifying tests to avoid the type check. These hide the real issue.

Docblocks and types

  • Add declare(strict_types=1); at the top of every file
  • Add : void return types to test methods. This keeps tests consistent with the repo's full-typing rule.
  • Use PHPUnit attributes instead of docblock annotations — prefer #[DataProvider('...')], #[Depends('...')], etc. over their @dataProvider/@depends docblock equivalents. Do not add @internal/@coversNothing docblocks or #[CoversNothing]/#[CoversClass(…)] attributes — Hyperf uses both forms but Laravel doesn't, and they serve no purpose outside strict coverage modes

Integration tests

Tests requiring separately configured services or public APIs go in tests/Integration/{PackageName}/. Self-contained loopback server fixtures may stay in tests/{PackageName}/ if they need no external setup and use automatically assigned ports, bounded waits, unconditional cleanup, and parallel isolation.

Optimize integration tests for parallel testing — ParaTest runs tests concurrently. Use RefreshDatabase by default and DatabaseTruncation when transaction depth zero is required; reserve DatabaseMigrations for migration behavior or a genuinely fresh migrated schema/baseline. Set up only the tables and services each test needs. Keep database cases in one data-provider test when splitting them would make schema setup compete. Load the full schema only when testing migrations or schema parity; do not hide avoidable slowness with longer timeouts.

Service workflows enumerate their test directories explicitly. Adding a service-specific directory requires adding it to the matching workflow; using the service trait provides isolation and skip behavior but does not make CI discover the test.

External service test traits

Integration tests that use an external service must use that service's test trait.

Trait Service Key Env Vars
InteractsWithRedis Redis / Redis Cluster / Valkey REDIS_HOST, REDIS_PORT, REDIS_CLUSTER_HOSTS_AND_PORTS
InteractsWithMeilisearch Meilisearch MEILISEARCH_HOST, MEILISEARCH_PORT, MEILISEARCH_KEY
InteractsWithTypesense Typesense TYPESENSE_HOST, TYPESENSE_PORT, TYPESENSE_API_KEY, TYPESENSE_PROTOCOL
InteractsWithAlgolia Algolia ALGOLIA_APP_ID, ALGOLIA_SECRET
InteractsWithServer Engine test servers (HTTP, TCP, WebSocket, HTTP/2) TEST_SERVER_HOST

This applies whether the test calls the service directly or reaches it through the package code under test.

These traits are required for external-service tests to work under ParaTest. Parallel workers share external services unless the trait isolates them. Tests that bypass the trait will leak state across workers and fail depending on timing.

The traits handle service-specific setup and cleanup. For example, InteractsWithRedis assigns each ParaTest worker its own Redis database and flushes it before and after each test. Redis Cluster only supports database zero, so Cluster suites run serially and flush that database between tests. Both approaches isolate the test keyspace without changing the Redis behavior being tested.

If a service is not configured, the trait skips the test before connecting. If the service is configured but unreachable or misconfigured, the test fails.

When adding integration tests for a new service type that has no trait yet, create one following this same pattern (per-worker isolation, skip-when-unconfigured, fail-when-unreachable).

phpunit.xml.dist

tests/Integration/ is not excluded from phpunit.xml.dist. The skip traits handle graceful skipping when service env vars are not configured. When services are explicitly enabled (CI or local with .env), the tests run normally.

GH workflows

Check .github/workflows/ to determine which workflow runs the affected integration tests. When adding a test directory, update the appropriate workflow so CI actually runs it.

When adding integration tests that need a new service, either add them to an existing workflow or create a new one. The workflow must start the service and set the appropriate env vars.

Environment files

Add env vars for new integration tests to both:

  • .env.example — commented out, as reference for what's available
  • .env — with sensible local defaults so developers can uncomment and run locally

See the existing entries for database, Redis, Meilisearch, and Typesense as examples.

Static Analysis

The tests/ directory is excluded from phpstan. Do not run phpstan on tests.

Run full PHPStan checks with composer analyse. During implementation, use targeted PHPStan only when investigating or validating a specific type issue.

phpstan.types.neon.dist validates only the committed types/ fixtures. Never pass source or test paths to it.

When fixing phpstan errors:

  1. Investigate before coding. For each error: read the code, check the Laravel equivalent's types (native and docblock), trace through callers and dependents. Report findings with the single, most correct fix.
  2. Don't make the code worse or more convoluted just to satisfy PHPStan. Fix real issues in the code, but don't add awkward wrappers, fake branches, casts, or wider types just to silence PHPStan. A phpstan fix is a typing change: it must not change runtime behavior, add overhead, or introduce new edge cases. If you cannot satisfy PHPStan within those constraints, reach consensus on a compliant solution through a second-opinion loop with your peer agent. If the code is correct and PHPStan cannot understand it, follow the narrowing / suppression order below.
  3. Native types vs docblocks determine what's dead code. If a native return type makes a guard unreachable, the guard is dead code — remove it. If only a docblock suggests always-true, the guard is legitimate runtime defense — leave it.
  4. Don't change contract/concrete boundaries to fix phpstan. Swapping a contract for a concrete (or vice versa) to satisfy a type check diverges from Laravel's API. Only do this when Laravel's typing is genuinely incorrect.
  5. Methods can be added to contracts only if they represent behavior any conforming implementation must provide. Implementation-specific methods, internal helpers, or driver-specific features don't belong on contracts — find another fix even if adding them would satisfy phpstan.
  6. Wrong docblock types should be fixed, not suppressed. Check the actual runtime behavior (extension docs, reflection, tests) to determine the correct type.
  7. Type decisions must be evidence-based. See Development Conventions — check Laravel/Hyperf signatures and docblocks, then trace real control flow. Don't guess.
  8. Narrowing / suppression order. When the code is correct but PHPStan can't follow it, in order: (1) fix the type signature or docblock; (2) @var to narrow to the correct runtime type; (3) a line- or identifier-scoped @phpstan-ignore (e.g. magic __call/__get forwarding). Never use assert() to narrow types, and never add a neon-wide rule on your own (see #9). When a container string key is also a PHP class name, keep the canonical service key and use @var for its actual runtime type; do not change service resolution solely for PHPStan.
  9. Don't add patterns to phpstan.neon.dist on your own. The neon file's global ignores cover fundamental framework patterns (Eloquent magic, generics, new static). Fix new phpstan errors at the source, not by masking them with new neon rules. Global suppression is a rare exception. First, reach consensus through a second-opinion loop with your peer agent that the error cannot be fixed at the source or narrowed locally, and that the pattern is frequent enough to justify global suppression. Then STOP, explain the reason and recommended suppression to the owner, and wait for approval before adding it.

Porting Packages

Policy

When porting Laravel packages, whether first-party or third-party, keep them as close to 1:1 with upstream as possible so future changes are easy to merge. The exceptions are:

  • Modernizing PHP types, including native parameter, return, property, and class-constant types, plus other appropriate PHP 8.4+ features, strict types, and strict comparisons
  • Applying the class-import convention, including its exceptions
  • Converting mutable Laravel date construction to Hypervel's immutable date conventions, typing configurable factory output as CarbonInterface, and capturing date-modifier return values
  • Converting container array access ($app['events']) and dynamic service-property access ($app->events) in ported code to named container methods, and applying the Configuration rules to ported config and its consumers
  • Adding Laravel-style title docblocks to methods (not classes — see Development Conventions)
  • For ported Laravel packages: making them coroutine-safe, adding Swoole performance enhancements (e.g., static property caching), making them pass PHPStan
  • Not porting upstream framework-specific integrations that only make sense in the source framework (for example packages, drivers) unless Hypervel intentionally has an equivalent surface
  • Not porting upstream mechanisms that do not make sense in Hypervel's stateful Swoole architecture (for example Laravel's deferred service provider machinery, where the upstream optimization only matters in a per-request bootstrap model)
  • Not porting deprecated upstream code or backwards-compatibility shims for versions/features Hypervel does not support — Hypervel is a new framework without Laravel's backwards-compatibility burden, so deprecated APIs and compatibility code that exist only to support older versions should be omitted rather than ported. Before changing or removing a deprecated public Laravel API, reach consensus on the recommended approach through a second-opinion loop with your peer agent. Then STOP, explain the proposed difference and its rationale to the owner, and wait for approval. Here, "upstream" means the framework or package being ported, not one of its dependencies — a Symfony deprecation does not make a Laravel API deprecated while Laravel still retains it. If a deprecated upstream surface still contains behavior that Hypervel actively needs, keep the behavior but move it onto the correct non-deprecated Hypervel-owned surface instead of porting the deprecated alias/wrapper as-is.
  • General performance improvements — reach consensus on the expected benefit, correctness, and complexity tradeoffs through a second-opinion loop with your peer agent before implementing.

Hypervel has no obligation to preserve Hypervel-specific behavior from earlier versions, but supported Laravel APIs—including named arguments and protected extension points—must remain compatible unless the user approves a difference. If a Laravel API is unsuitable for Hypervel or preserving it would make the code worse, reach consensus on the cleanest design through a second-opinion loop with your peer agent. Then STOP, explain the proposed difference and its rationale to the owner, and wait for approval before changing it.

When Laravel or a tracked upstream introduces an equivalent to a Hypervel-specific enhancement, compare their capabilities, APIs, coroutine safety and performance, then reach consensus on how to reconcile them through a second-opinion loop with your peer agent. Normally prefer the upstream API when it meets Hypervel’s requirements. Before deprecating or removing the Hypervel-specific API, STOP, explain the recommendation to the owner, and wait for approval.

Approved adaptations take precedence over upstream fidelity. Preserve Laravel upstream naming, structure, and style everywhere else.

Hyperf is a historical reference rather than an ongoing merge target. For the rare Hyperf port, follow docs/ai/porting-hyperf.md.

When working on a package, check its README for the upstream reference before making changes. Most Hypervel packages are ports of Laravel first-party or third-party ecosystem packages, such as Spatie packages. Most low-level Swoole infrastructure packages were originally ported from Hyperf, and a few packages are Hypervel-specific.

Read docs/ai/porting-hyperf.md only when porting a Hyperf package or update.

Global upstream exclusions

Owner-approved exclusions that apply across packages. Package-specific differences belong in the package README.

  • Laravel Boost and package-provided Boost resources, including skills and guidelines.

Source workflow

1. Package skeleton

If the Hypervel version of the package doesn't exist yet, create the skeleton using an existing package as a template:

  • Porting a Laravel first-party package: Use the cache package as reference
  • Porting a Hyperf package: Use the websocket-server package as reference
  • Porting a Laravel-ecosystem third-party package: Use the permission package as a reference

Read the reference package's composer.json, LICENSE.md, and README.md and create equivalents for the new package. Every package must be wired in both places: its own src/{package}/composer.json for the subtree split, and the root composer.json for monorepo development. Update autoloading, replace, and Hypervel provider / alias discovery metadata as needed, and add root dependencies with composer require — see Providers and Listeners for where providers should be registered. Create the README using the Package READMEs format under Development Conventions.

2. Port the files one at a time, alphabetically

Check the source package to see what classes exist. Create a comprehensive todo list with a separate entry for each file to port. The porting process is:

  1. Copy the file using cp — never read the source first and write a new version. Copying first then reading the copy avoids reading the file twice, which wastes context.
  2. Read the ENTIRE copied file to understand context. For large files, read them in chunks.
  3. Update namespaces and apply the Porting Policy's approved adaptations (modernized types, method docblocks, etc.). For Laravel ports, do not make unrelated naming, structural, or style changes; preserve upstream naming, structure, and style. For Hyperf ports, follow docs/ai/porting-hyperf.md.

For very large files where even reading in chunks is impractical Update the file in chunks from top to bottom — read a chunk, update, read next chunk, update. Do NOT try to search for patterns and update scattered bits.

3. Update consumers

Search both src/ and tests/ for any use statements or references to the old namespace (e.g., Illuminate\Database\) and update them to the new Hypervel namespace. Verify zero remaining references before proceeding.

4. Run phpstan

After porting is complete, run phpstan on the newly ported package and fix errors. Investigate each error properly — don't reach for ignores without thinking it through. See the Static Analysis section.

5. Complete verification

Complete the verification workflow under Change Workflow. For failures, follow When to Stop and Report: make straightforward fixes directly; for non-trivial failures, reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing.

Provider and listener reminder

When ported code adds a provider or listener, wire providers and aliases in both Composer metadata locations and register listeners in the service provider's boot() method through closures that resolve them from the container. Follow the full rules under Providers and Listeners.

Porting rules

  • Preserve source constant/property/method order in Laravel ports — when porting or merging methods into an existing Hypervel class, insert them in the same relative order as upstream. This keeps diffs meaningful and makes future merges easier.
  • Preserve existing comments — use the following rules for upstream code comments and docblocks: Do not remove or modify upstream code comments unless they are incorrect. Only remove @param and @return annotations where the description adds nothing beyond what the native type hint and parameter/method name already convey. Examples of removable: @param string $name The name of the cookie (just restates string $name), @param int $offset Stream offset (just restates int $offset). Examples to keep: @param bool $secure Whether the cookie should only be transmitted over a secure HTTPS connection, @param int $whence Specifies how the cursor position will be calculated..., @return resource|null (when the native type is mixed because resource isn't a valid PHP type hint). Keep everything else: behavioral descriptions, @see links, @throws annotations, warnings, contract explanations, usage notes. Modernize the title line to imperative form ("Returns" → "Return", "Retrieves" → "Retrieve") but do not remove or rewrite the body content beneath it. Translate non-English comments to English and fix grammar errors.
  • Record intentional Laravel differences where future ports will look — When a Laravel feature is intentionally not ported because it does not fit Hypervel's Swoole/coroutine architecture, or because Hypervel has a better native equivalent, record it in three places so a future port cannot miss it: (1) the package README under Differences From Laravel, following the Package READMEs rules above and explaining what to use instead; (2) a concise source comment at the natural insertion point where the skipped method/class would otherwise sit; (3) a concise REMOVED: comment at the matching upstream test location when tests are skipped. This is a narrow exception to the "don't annotate divergences" rule: it applies only to intentionally omitted methods or features, never to ordinary ported-and-adapted code. Closed decisions only — real gaps still worth doing go in docs/todo.md.
  • Replace framework names in code — any occurrence of the word laravel or hyperf in ported code (string literals, comments, prefixes, identifiers, etc.) must be replaced with hypervel, preserving the original casing. For example: laravel_reserved_ → hypervel_reserved_, LaravelExcelExporter → HypervelExcelExporter, HYPERF_VERSION → HYPERVEL_VERSION. This does not apply to namespaces (which have their own conversion rules) or to references that describe the upstream source (e.g., docblock @see links to Laravel/Hyperf source).
  • Don't copy Laravel/Hyperf-specific framework details just to stay 1:1 — preserve public APIs and behavior while translating upstream-specific packages, providers, bootstrap mechanisms, and architecture to Hypervel equivalents. If no equivalent exists, reach consensus on the appropriate adaptation through a second-opinion loop with your peer agent before implementing.

Test porting workflow

Follow the same cp-then-edit process as source files. This workflow applies to both Hyperf and Laravel test porting. Laravel-specific conversions are covered in Porting Laravel Tests below; Hyperf-specific conversions (namespaces, license headers, container and error-handler mocking, NonCoroutine tests) are covered in docs/ai/porting-hyperf.md.

Test file names and directory structure should mirror the source for both Laravel and Hyperf ports, providing a 1:1 class-to-test mapping. For Laravel ports, this also enables automated porting of upstream PRs. When both Hyperf and Laravel have tests covering the same class, merge them into one file — take the more comprehensive version as the base and add unique tests from the other. Consolidate overlapping Hypervel-specific tests under upstream test names and placement, preserving stronger assertions and distinct coverage. Keep every applicable test from the tracked upstream suite, even if redundant.

1. Audit source tests

List all test files in the source package's tests/ directory. For Laravel packages, also check tests/Integration/{PackageName}/ — that's where Laravel puts its integration tests for each package. Note what each file covers.

2. Audit existing Hypervel tests (if any)

Read all files in the existing Hypervel test directory for this package. Categorise them:

  • Custom tests (Hypervel-specific, no Hyperf/Laravel equivalent): Keep as-is
  • Ported tests (already ported from source): Keep — new source tests must be merged in

3. Create the todo list

One entry per test file. Note the strategy:

  • Copy and update — no existing Hypervel test for this
  • Merge — Merge upstream tests into the existing Hypervel test file, preserving distinct Hypervel-specific coverage
  • Integration — needs external service, goes in tests/Integration/{PackageName}/
  • Investigate — exposes missing functionality, an unsupported feature, or an architectural difference. Reach consensus through a second-opinion loop with your peer agent on what the test covers, whether Hypervel should support it, and the appropriate fix or justified exclusion.

4. Port test files one at a time

For newly copied files (copy and update):

  1. Copy the file using cp to the correct location
  2. Read the ENTIRE copied file to understand context
  3. Update namespaces, base class, imports, types, docblocks, etc.

For merged files:

  1. Read BOTH the source file AND the existing Hypervel file
  2. Merge source tests into the Hypervel file, preserving distinct Hypervel-specific coverage
  3. Update namespaces, types, docblocks, etc.

For stub/helper files: Copy Stub/ directory files the same way.

5. Run tests after each file

Use this exact cadence for each test class:

  1. Port the test class.
  2. Run that test class immediately (./vendor/bin/phpunit --no-progress path/to/TestClass.php).
  3. Fix all straightforward failures.
  4. If any failure exposes a source code bug, missing functionality, or unclear behavioral difference, investigate and reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing.
  5. Once the test class is green, move to the next test class. Work serially on one test class at a time.

6. Complete verification

After all test files are ported, complete the verification workflow under Change Workflow.

Porting Laravel Tests

Most conversion work is applying general rules from Writing Tests — especially "Stricter typing", "When tests expose source code type errors", and "Helper class namespacing", since Laravel's loose typing and shared test namespaces hide problems that Hypervel exposes. The subsections below cover what is specific to Laravel ports.

Namespace changes

  • Change Illuminate\Tests\{Package} to Hypervel\Tests\{Package}
  • Change all Illuminate\ source imports to Hypervel\

If Laravel's namespace includes the test class name, keep it. Stripping it causes "Cannot redeclare class" errors.

Missing dependencies

Some test files reference classes defined in other test files. Laravel gets away with this due to test suite load order. Make tests self-contained by defining required classes locally.

Workbench fixtures from upstream packages

Laravel packages sometimes ship a workbench/ directory with controllers, models, middleware, and a routes/web.php. Hypervel's testbench workbench is shared across every package's tests, so port these into the package-scoped pattern:

  • Controllers, models, middleware → tests/{Package}/Fixtures/..., namespace Hypervel\Tests\{Package}\Fixtures\....
  • Routes → tests/{Package}/Fixtures/routes.php. Load only from tests that need them (test setUp, or a small bootstrap script for CLI subprocesses). Never always-load.

Update upstream test imports to point at the new Fixtures namespace.

Approved unsupported features

Tests for these features should be removed (not commented out) without asking — they will never be supported:

  • Built-in database drivers: SQL Server, MongoDB, DynamoDB are not provided. Hypervel ships MySQL, MariaDB, PostgreSQL, and SQLite drivers.
  • Cache drivers: Memcached, APC / APCu, DynamoDB, MongoDB
  • Dynamic connections: DB::build(), DB::connectUsing() — incompatible with Swoole connection pooling
  • Container access: ArrayAccess and dynamic service properties

Laravel porting quick checklist

  1. Update namespace to Hypervel\Tests\{Package}
  2. Add declare(strict_types=1);
  3. Change Illuminate\ imports to Hypervel\
  4. Extend correct base TestCase (Hypervel\Tests\TestCase or Hypervel\Testbench\TestCase)
  5. Ensure parent::setUp() is called
  6. Add type declarations to model properties
  7. Fix mock types (PDO, QueryBuilder, Grammar, etc.)
  8. Add ->andReturnSelf() to chained method mocks
  9. Use a test-specific namespace only when helper classes have generic, collision-prone names — already-specific helper names do not need extra namespace ceremony.
  10. Remove tests only under the rules in Handling failing tests
  11. Run tests and fix any remaining type errors