test: shared hand-off + mutation infrastructure for feature PRs#7083
test: shared hand-off + mutation infrastructure for feature PRs#7083somethingwithproof wants to merge 6 commits into
Conversation
Consolidates infection/infection dev dep, allow-plugins entry, baseline infection.json5, tests/HandOff/HandOffHelpers.php with reusable stubs (cacti_log capture, temp-file fixtures, minimal-zip builder), HandOff testsuite registration, and the documented pattern. Feature PRs that add hand-off coverage rebase onto this and contribute only ONE HandOffTest file plus an optional infection.json5 filter override. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
There was a problem hiding this comment.
Pull request overview
Adds shared “hand-off” test infrastructure and baseline mutation-testing configuration so multiple feature PRs can rebase and drop duplicated scaffolding.
Changes:
- Add shared hand-off test helpers under
tests/HandOff/and track the directory in git. - Register a
HandOfftestsuite inphpunit.xmland document the hand-off test pattern. - Introduce a baseline
infection.json5and addinfection/infectiontorequire-dev(plus Composer allow-plugin entry).
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
tests/HandOff/HandOffHelpers.php |
Adds shared helpers for log stubbing, temp files, and ZIP fixture creation used by HandOff tests. |
tests/HandOff/.gitkeep |
Ensures tests/HandOff/ is tracked before tests land. |
phpunit.xml |
Adds HandOff testsuite pointing to ./tests/HandOff. |
infection.json5 |
Adds baseline Infection mutation-testing config and thresholds. |
docs/security/handoff-tests.md |
Documents the hand-off test pattern and how to run HandOff/mutation locally. |
composer.json |
Adds Infection dev dependency and allows Infection’s Composer plugin. |
| function &cacti_handoff_stub_cacti_log(): array { | ||
| static $buffer = array(); | ||
|
|
||
| if (!function_exists('cacti_log')) { | ||
| eval('function cacti_log($string, $output = false, $environ = "CMDPHP", $level = 0) { ' | ||
| . '\cacti_handoff_record_log_line($string, $environ, $level); ' | ||
| . '}'); | ||
| } | ||
|
|
||
| return $buffer; | ||
| } | ||
|
|
||
| /* | ||
| * Internal sink used by the eval'd cacti_log shim. Kept separate so | ||
| * the stored shape stays a single hashmap per call regardless of how | ||
| * cacti_log was invoked. | ||
| */ | ||
| function cacti_handoff_record_log_line(string $string, string $environ = 'CMDPHP', int $level = 0): void { | ||
| $buffer = &cacti_handoff_log_buffer_ref(); | ||
| $buffer[] = array( | ||
| 'string' => $string, | ||
| 'environ' => $environ, | ||
| 'level' => $level, | ||
| ); | ||
| } | ||
|
|
||
| /* | ||
| * Single static buffer shared by the stub, the getter, and the | ||
| * clearer. A by-reference accessor avoids duplicating the static. | ||
| */ | ||
| function &cacti_handoff_log_buffer_ref(): array { | ||
| static $buffer = array(); | ||
| return $buffer; | ||
| } |
There was a problem hiding this comment.
cacti_handoff_stub_cacti_log() returns a by-ref static $buffer, but the shim actually records log lines into the separate static buffer inside cacti_handoff_log_buffer_ref(). As a result, callers using the return value from cacti_handoff_stub_cacti_log() will never see recorded entries. Make the stub return cacti_handoff_log_buffer_ref() by reference (and remove the extra static), or have cacti_handoff_record_log_line() append to the same static used by the stub.
There was a problem hiding this comment.
Fixed — stub now returns cacti_handoff_log_buffer_ref() by reference.
| eval('function cacti_log($string, $output = false, $environ = "CMDPHP", $level = 0) { ' | ||
| . '\cacti_handoff_record_log_line($string, $environ, $level); ' | ||
| . '}'); | ||
| } | ||
|
|
||
| return $buffer; | ||
| } | ||
|
|
||
| /* | ||
| * Internal sink used by the eval'd cacti_log shim. Kept separate so |
There was a problem hiding this comment.
Defining cacti_log() via eval() is avoidable here and makes the helper harder to audit/debug. PHP supports conditional function declarations without eval (e.g., define function cacti_log(...) { ... } inside the if (!function_exists('cacti_log')) block). Also, the stub signature diverges from the real cacti_log() in lib/functions.php:1448 (types/defaults/return bool), which can cause surprising behavior when hand-off tests hit real call sites—consider matching the real signature and returning true.
| eval('function cacti_log($string, $output = false, $environ = "CMDPHP", $level = 0) { ' | |
| . '\cacti_handoff_record_log_line($string, $environ, $level); ' | |
| . '}'); | |
| } | |
| return $buffer; | |
| } | |
| /* | |
| * Internal sink used by the eval'd cacti_log shim. Kept separate so | |
| function cacti_log(string $string, bool $output = false, string $environ = 'CMDPHP', int $level = 0): bool { | |
| cacti_handoff_record_log_line($string, $environ, $level); | |
| return true; | |
| } | |
| } | |
| return $buffer; | |
| } | |
| /* | |
| * Internal sink used by the cacti_log shim. Kept separate so |
There was a problem hiding this comment.
Fixed — eval removed; cacti_log is declared inline inside function_exists guard with the correct signature.
| } | ||
|
|
||
| foreach ($entries as $name => $contents) { | ||
| $zip->addFromString((string) $name, (string) $contents); |
There was a problem hiding this comment.
ZipArchive::addFromString() can return false (e.g., invalid entry name). Right now failures are silently ignored, which can lead to tests operating on an incomplete ZIP. Check the return value and throw a RuntimeException when adding an entry fails.
| $zip->addFromString((string) $name, (string) $contents); | |
| if ($zip->addFromString((string) $name, (string) $contents) === false) { | |
| $zip->close(); | |
| throw new RuntimeException('cacti_handoff_make_zip: failed to add ZIP entry "' . (string) $name . '"'); | |
| } |
There was a problem hiding this comment.
Fixed — addFromString return value checked; failure closes the archive and throws RuntimeException.
| // on the Infection CLI. The thresholds below (minMsi / minCoveredMsi) are | ||
| // the project floor; feature PRs may raise them but should not lower them. | ||
| { | ||
| $schema: "vendor/infection/infection/resources/schema.json", |
There was a problem hiding this comment.
$schema points at vendor/infection/..., but this repo’s Composer vendor-dir is ./include/vendor (composer.json config). Updating the schema path to include/vendor/infection/infection/resources/schema.json keeps editor/IDE schema validation working with the project’s vendor layout.
| $schema: "vendor/infection/infection/resources/schema.json", | |
| $schema: "include/vendor/infection/infection/resources/schema.json", |
Each test guards on its feature file and skips when not present on develop. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
- phpunit.xml: add Integration testsuite alongside existing Unit/HandOff - tests/Integration/: DbDumpIntegrationTest, PingIntegrationTest (from develop), CactiProcessIntegrationTest, SqlScriptsIntegrationTest - tests/HandOff/RegressionGuardTest: guards against backsliding on shell_exec, password-in-argv, RLIKE concat, and open-redirect patterns Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> # Conflicts: # phpunit.xml
Guards integration tests and regression guards from Cacti#7083 against absence of CactiProcess and other feature-PR hardening, so the consolidated foundation PR is green on develop. Suites activate automatically once their feature PRs merge. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…7124) * test: shared hand-off + mutation infrastructure for feature PRs Consolidates infection/infection dev dep, allow-plugins entry, baseline infection.json5, tests/HandOff/HandOffHelpers.php with reusable stubs (cacti_log capture, temp-file fixtures, minimal-zip builder), HandOff testsuite registration, and the documented pattern. Feature PRs that add hand-off coverage rebase onto this and contribute only ONE HandOffTest file plus an optional infection.json5 filter override. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: consolidate per-feature hand-off suites into shared infra Each test guards on its feature file and skips when not present on develop. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(test): replace eval stub, unify log buffer ref, guard ZipArchive add Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(ci): correct infection vendor path in infection.json5 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci(phpstan): baseline upstream undefined-variable and isset errors Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: add HandOff and Integration testsuites; add regression guards - phpunit.xml: add Integration testsuite alongside existing Unit/HandOff - tests/Integration/: DbDumpIntegrationTest, PingIntegrationTest (from develop), CactiProcessIntegrationTest, SqlScriptsIntegrationTest - tests/HandOff/RegressionGuardTest: guards against backsliding on shell_exec, password-in-argv, RLIKE concat, and open-redirect patterns Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * chore(test): pest test infrastructure and PSR-4 autoload Add Pest 2 dev dependency, regenerate composer.lock under PHP 8.1, declare PSR-4 autoload for the Cacti namespace, and seed the test bootstrap with three unit tests plus an orb integration check. Pest stays on the v2 line because v3 transitively requires PHP 8.2 and breaks the 8.1 matrix entry. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * chore(phpstan): catch up baseline with 11 upstream-detected entries Cacti Commit Audit fails on develop with 11 PHPStan errors that are not yet in phpstan-baseline.neon. Append the matching entries so the PR-A branch passes Run PHPStan at Level 6. Same set of entries already exists on PR #7077; this is a transient catch-up that upstream will absorb when those entries land on develop. Files: aggregate_graphs.php, color_templates.php, graph_templates.php, graphs.php, lib/html.php (3 entries). Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * chore(test): drop composer.lock; let CI resolve per PHP version The lock file pinned brianium/paratest v7.3.1 which only supports PHP 8.1-8.3, so the 8.4 matrix entry rejected the lock with "Your lock file does not contain a compatible set of packages". A single lock cannot satisfy 8.1 (paratest 7.3.x) and 8.4 (paratest 7.4.x) at the same time because pestphp/pest 2.36.0 is the last 2.x release that supports 8.1 and the 8.1-compatible paratest tag predates 8.4 support. Match upstream develop's behaviour: no committed lock; let `composer install` in CI resolve per matrix entry. Pest stays on the v2 line in composer.json so 8.1 still gets a compatible Pest. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(test): apply Copilot review feedback for test infra Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * chore(test): drop psr-4 autoload and align deps with canonical PRs Match the flat un-namespaced lib/CactiX.php convention used by #7088, #7073, #7077, #7075. Drop symfony/process (added by #7073) and symfony/validator (added by #7077) to avoid composer.json conflicts at merge time. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: skip integration suites and regression guards pending feature PRs Guards integration tests and regression guards from #7083 against absence of CactiProcess and other feature-PR hardening, so the consolidated foundation PR is green on develop. Suites activate automatically once their feature PRs merge. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(ci): drop infection/infection to unbreak PHP 8.4 matrix infection/infection ^0.27 transitively pins thecodingmachine/safe ^2.1.2, resolving to v2.5.0. Its generated stubs use implicit-nullable parameter declarations, which PHP 8.4 emits as deprecations into cacti.log and trips the log-quiet assertion. infection has no config or test wiring in this repo, so removing the dev dep clears the transitive pin without losing functionality. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net>
…#6989) * fix(import): harden package path traversal and absolute-path rejection Reject absolute paths (Unix /... and Windows \... or C:\...) before any boundary math runs. Extend realpath boundary checks to data_query xml_path and package find_paths(). Exclude binary-extension files from path resolution. cs-fixer style fixups included. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: align import path traversal coverage Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(style): align ping.php match arms; add PHPStan baseline for pre-existing Level 6 errors Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * chore: ignore local Claude Code tooling state and add contributor notes Add .claude/, .omc/, .worktrees/, and notepad.md to .gitignore so local AI-assistant session state, oh-my-claudecode orchestration files, and scratch worktrees never leak into a PR. Add CLAUDE.md at the repo root documenting the house conventions (PHP 7.4 target on 1.2.x branches, cacti_sizeof/htmle/gfrv wrappers, db_qstr_rlike, the vendor-pollution pitfall) so future AI contributions match the review feedback pattern from TheWitness instead of repeating it on every PR. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * chore: remove accidentally tracked .worktrees and .claude/worktrees gitlinks 31 submodule-style gitlinks pointing at local git worktrees had been staged into the branch. They showed up in the PR diff stat as +1-line entries and were what @TheWitness asked to be removed. The .gitignore entry from the previous commit covers new ones; these needed an explicit git rm --cached to leave the index. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(import): tighten Copilot review feedback Hoist allowed-base realpath out of per-file loop; early-continue when $resolved_dir is false; whitespace-tolerant regex assertions in tests; unique random temp base per test. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(import): hand-off coverage + infection mutation config Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci(infection): allow infection/extension-installer composer plugin Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(import): drop hand-off test, consolidated into #7083 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci(phpstan): baseline upstream undefined-variable and isset errors Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: retrigger after transient packagist network failure Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * chore: remove dev tooling artifacts from index Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: retrigger after transient PPA failure --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net>
…7032) * fix(snmp): strip trailing .0 padding in OID indexes SNMP walks on some devices return OID indexes with one or more trailing zero octets that are structural padding rather than meaningful data (e.g. .1.3.6.1.2.1.2.2.1.2.1.0 where the final .0 is padding). When Cacti uses those raw indexes as array keys they never match the parsed index values, so rows silently disappear from data query results. Detection heuristic: strip a trailing .0 segment only when (a) the OID has more than two dot-separated components, (b) the candidate key without the suffix already exists in the index map, and (c) the row count after stripping is not reduced below 1. This avoids false-positive stripping on legitimate single-component or two-component indexes. Single-row OID indexes are preserved unchanged to prevent over-stripping on devices where .0 is the real index value. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * chore: ignore local Claude Code tooling state and add contributor notes Add .claude/, .omc/, .worktrees/, and notepad.md to .gitignore so local AI-assistant session state, oh-my-claudecode orchestration files, and scratch worktrees never leak into a PR. Add CLAUDE.md at the repo root documenting the house conventions (PHP 7.4 target on 1.2.x branches, cacti_sizeof/htmle/gfrv wrappers, db_qstr_rlike, the vendor-pollution pitfall) so future AI contributions match the review feedback pattern from TheWitness instead of repeating it on every PR. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(oid): tighten Copilot review feedback Defense-in-depth size check after stripping; renamed dataset cases for clarity; strengthened single-index guard test. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(snmp): detect single zero OID padding safely * ci(phpstan): baseline new errors from upstream PHPStan version bump 11 errors surfaced after PHPStan re-analyzed the tree on the latest PR runs. None are in code this PR touches; they are pre-existing issues in aggregate_graphs.php, color_templates.php, graph_templates.php, graphs.php, and lib/html.php. Suppress them in the baseline so this PR can land. A separate cleanup PR on develop should retire entries. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(oid): hand-off coverage + infection mutation config Pest tests walk the full pipeline (raw indexes -> detect -> strip -> consumer regex parse) end-to-end and pin the collision-safe + single-.0 boundary behaviour. infection.json5 scopes mutation to lib/data_query helpers with a 70% MSI floor. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci(infection): allow infection/extension-installer composer plugin Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(oid): drop hand-off test, consolidated into #7083 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(oid): cover mixed .0 padding negative case Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(oid): guard strip function against mixed-padding sets Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * chore: remove dev tooling artifacts from index Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: re-trigger after transient PPA failure Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: retrigger after transient PPA failure --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net>
…ion (#7063) * feat(security): add cacti_dispatch helper for action table authorisation cacti_dispatch() wraps the repeated switch/case on get_request_var('action') that every Cacti controller writes. Each action is declared with the HTTP method it accepts, the realm id that must be allowed, and an optional object-level ACL callback. The guards run in method then realm then object-ACL order and each one logs with a WEBUI category before returning a 405 or 403, so review across controllers no longer depends on every author remembering to add the same three checks in the same order. The test file source-scans the helper for each guard and for the guard ordering, so a future refactor that silently drops the realm or ACL branch fails CI rather than regressing authorisation for every controller that opts in. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * security(dispatch): address Copilot review on cacti_dispatch Reject non-string action inputs before offset access so ?action[]=x cannot produce a TypeError. Default REQUEST_METHOD to GET for CLI and tests, and normalise both sides with cacti_strtoupper so the comparison is locale-independent. Treat a declared-but-non-callable object_acl as a misdeclaration and deny (log ERROR + 403) rather than silently skipping the check. Split the denial emitter into cacti_dispatch_deny() under a prefixed name to avoid colliding with the existing raise_ajax_permission_denied() in lib/functions.php, and always follow the AJAX helper with an explicit http_response_code() so a denied non-AJAX request cannot fall through to a 200 render. Reject non-callable callbacks with a 500 and ERROR log. Drop the redundant docblock on the function and tighten the parameter types. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * style(dispatch): apply php-cs-fixer to cacti_dispatch Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(dispatch): drop tautological is_string checks flagged by phpstan level 6 PHPStan level 6 flagged two is_string() calls as always true: - $default was already type-hinted as string via PHPDoc; the runtime cast to '' was redundant. - The second is_string($action) at the unknown-action log happens after the earlier is_string guard already narrowed the type. Tighten the signature to string $default so the first cast drops, and remove the second is_string from the log interpolation since $action is always a string by the time that branch runs. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(dispatch): tighten Copilot review feedback strspn whitelist on action; explicit AJAX detection for 403 vs redirect; file_get_contents guard in tests. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(dispatch): hand-off coverage + infection mutation config Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci(infection): allow infection/extension-installer composer plugin Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(dispatch): drop hand-off test, consolidated into #7083 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * feat(dispatch): reject mis-declared method, document CSRF scope Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci(phpstan): baseline upstream undefined-variable and isset errors Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net>
Shared hand-off testing + mutation infrastructure consolidated from 8 in-flight feature PRs.
What this adds
composer.json:infection/infection ^0.27inrequire-devandinfection/extension-installer: trueinconfig.allow-plugins(the plugin would otherwise be blocked by Composer 2.x).infection.json5at repo root: baseline mutation config — Pest, MSI thresholds 70 / covered 80, 30s timeout, log underbuild/infection/. Per-feature filters live in the feature PRs.tests/HandOff/HandOffHelpers.php: shared helpers reused by every hand-off test:cacti_handoff_stub_cacti_log()recordscacti_logcalls into a static buffercacti_handoff_get_log_buffer()/cacti_handoff_clear_log_buffer()for assertions and resetcacti_handoff_temp_file(contents, extension)— auto-cleaned tempnam wrappercacti_handoff_make_zip(entries)— minimal valid ZIP builder for upload-validation teststests/HandOff/<Feature>HandOffTest.php(8 files): boundary-crossing test suites consolidated from each feature PR. Each suite begins with afile_exists()/function_exists()guard against the feature's source file; on develop without the feature merged, the suite registers a single skipped test and returns. Once a feature PR merges, its suite activates automatically.phpunit.xml: registers aHandOfftestsuite alongsideUnitandIntegration.docs/security/handoff-tests.md: explains the boundary-crossing assertion pattern and how to run mutation per feature.Source PRs that contributed each suite
CactiProcessHandOffTest.php— from feat(security): Symfony Process and Mime helpers #7073 (CactiProcess wrapper)CactiMimeHandOffTest.php— from feat(mime): add CactiMime helpers and validate package_import uploads #7074 (CactiMime wrapper)CliInvocationHandOffTest.php— from feat(cli): add Symfony Console plumbing and migrate two pilot scripts #7075 (Symfony Console plumbing)CactiSettingsHandOffTest.php— from feat(validator): add settings constraint validation pilot #7077 (Symfony Validator settings)ImportPackagePathHandOffTest.php— from fix(import): consolidate path hardening for import and package writes #6989 (import path-traversal hardening)OidStripHandOffTest.php— from fix: auto-detect and strip trailing .0 padding in SNMP OID indexes #7032 (OID stripping)CactiDispatchHandOffTest.php— from feat(security): add cacti_dispatch helper for action table authorisation #7063 (cacti_dispatch helper)HtmxLoaderHandOffTest.php— from feat(htmx): vendored htmx 2.0.6 loader + tests (tranche A of filter pilot) #7066 (htmx loader)Each feature PR has been rebased to drop its local copy. Skip guards keep this PR green on develop today; the suites activate as feature PRs land.
What this does NOT change
infection/infectiontransitively pulls Symfony components, and the project's policy is no Symfony on 1.2.x. Hand-off tests on 1.2.x branches still work via the same helpers but skip the mutation config.Rollback
Two commits. Revert if there's pushback on the infection dev-dep.