15: End-to-end suite - #16
Closed
nikolaystrikhar wants to merge 4 commits into
Closed
Conversation
nikolaystrikhar
force-pushed
the
14-activation-error-rewrite
branch
from
August 12, 2026 10:57
4b98893 to
55bc54a
Compare
nikolaystrikhar
force-pushed
the
15-e2e-suite
branch
from
August 12, 2026 10:57
01b6153 to
efbd136
Compare
Conflict\Resolver takes its four collaborators as required constructor arguments -- the checker, the deactivator, the notice queue and the redirector -- because the container is now mandatory and nothing has to be constructible without one. The nullable peers and the accessors that fell back to a static are gone with the class they fell back to. Conflict\Gatekeeper owns who may resolve: an interactive admin GET, the activate_plugins capability, and a hook prefix. plugins_loaded runs on every request and fires before auth_redirect(), so an unauthenticated GET of an admin URL reaches this code, and the capability gate covers every policy rather than only the destructive one -- the other branches queue a notice the same user could not render anyway. The priority-1 step asks the gatekeeper before it resolves Resolver_Interface at all, so a host binding its own resolver cannot drop either gate by omission. Conflict\Destination becomes Conflict\Redirector. It decides where to send the user and never goes there; wp_safe_redirect() and the exit after it stay in the resolver, so the policy action and the admin-URL knowledge change for separate reasons. A class that returns a URL from a filter still earns the agent noun. The boot barrier now measures from the lowest priority in the sequence rather than from the load priority, or a host booting between conflict resolution and the load would be told nothing while half its wiring silently failed.
nikolaystrikhar
force-pushed
the
14-activation-error-rewrite
branch
from
August 12, 2026 13:30
55bc54a to
d2daabd
Compare
nikolaystrikhar
force-pushed
the
15-e2e-suite
branch
from
August 12, 2026 13:43
efbd136 to
0baf6e8
Compare
nikolaystrikhar
marked this pull request as draft
August 12, 2026 13:47
Activation becomes Activator. An abstract -ion noun names a directory in this ecosystem and never a class -- Activation/ holds an Activator and a Deactivator -- and the class does something rather than being something. Load\Runner takes it injected and calls it immediately after a successful require_once, so a callback never runs for a file that was not loaded. The bookkeeping is an option keyed by slug, because a callback that ran and a callback that failed have to be told apart across requests, and a sub-plugin absorbed into a host has no activation hook of its own to hang this on. There is no Loader::activation() accessor. The activator has exactly one caller and a host that wants different once-ever bookkeeping binds Activator_Interface, which is the same line the accessor would have been -- and public API is forever.
An admin who still has the standalone installed and clicks Activate gets WordPress's own fatal-error screen, because the bundled copy already defined everything the standalone is about to. wp_admin_notice_markup lets us replace that wording with an explanation of the merge, on the one screen where the site owner is actively trying to do the thing we have made impossible. The rewrite lives on Notices\Queue rather than in a class of its own: it is notice wording for a sub-plugin, keyed by the same standalone basename the queue already reasons about, and a separate class would have to be handed the registrar to find out which sub-plugin the screen is even about. The filter is a named static trampoline rather than a closure over the container, unlike the plugins_loaded steps. This is the one hook that rewrites a screen WordPress drew instead of adding one of ours, so a host that wants core's wording back needs a callback it can remove_filter() -- and a closure cannot be removed. The plugins_loaded steps are closures because that sequence has to run inline when boot came too late, which is a constraint these two admin hooks do not have.
Fifteen requests, each one dispatched through the hooks a host would fire rather than by calling the steps directly, so the priorities, the gatekeeper and the boot barrier are exercised instead of bypassed. The container being mandatory is what makes the last two tests possible: a host binds its own registrar, checker, deactivator, notice queue, activator, gatekeeper and resolver before boot, and every one is asserted to be the object the request actually used -- with the defaults asserted not to have run beside it, since active_plugins untouched and both options unwritten is the only way to tell "the binding was used" from "the binding was ignored and nothing happened". Each load-path test writes its own bundled fixture, because require_once caches by resolved path for the whole process and a shared file would make every later test pass without loading anything. The trait that writes them gained the old suite's @after cleanup and kept ours, so a fixture is removed whether or not the test remembers.
nikolaystrikhar
force-pushed
the
14-activation-error-rewrite
branch
from
August 12, 2026 14:01
d2daabd to
66eba85
Compare
nikolaystrikhar
force-pushed
the
15-e2e-suite
branch
from
August 12, 2026 14:01
0baf6e8 to
3d5d17c
Compare
nikolaystrikhar
force-pushed
the
14-activation-error-rewrite
branch
4 times, most recently
from
August 13, 2026 11:01
1aaffc3 to
f7875e9
Compare
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What: 16 scenarios driving the library through the bootstrap a host runs —
Config::set_hook_prefix(),Config::set_container(),Loader::register(),Loader::boot()— and then through the hooks boot() wired, dispatchingplugins_loadedfor the conflict step at priority 1 and the load pass at 2,all_admin_noticesfor the queue andwp_admin_notice_markupfor the re-activation rewrite, against realactive_plugins, core's owndeactivate_plugins()and real site options; no source files, all tests.Usage: the shape of a scenario — a bootstrap, a request, and assertions against what WordPress actually holds afterwards.
Why this way:
The hooks are dispatched, not the steps called. The earlier draft ran a request as
Loader::run_conflict_resolution()followed byLoader::load_all(); a request is nowdo_action( 'plugins_loaded' )and an admin page loaddo_action( 'all_admin_notices' ), so what runs is whatever boot() wired, in the order and at the priorities it wired it. That is the half where the bugs are — an admin-onlyadd_action()that never ran, a step wired into a dispatch window that had already closed, a resolution ordered behind the load pass — and calling the steps directly is exactly the arrangement that cannot see any of it. The container is handed over bare rather than pre-registered throughWithContainer, for the same reason: boot() running the provider over it is one of the steps under test.Each load-path test writes its own fixture file and its own guard constant.
require_oncededupes by resolved path for the lifetime of the process anddefine()lasts just as long, so a fixture shared between two tests lets the second pass without loading anything — including if the load logic were deleted outright — and a reused constant makes a later sub-plugin read as already loaded.WithBundledPluginsnow cleans up on its own@afterhook rather than trusting a caller's tearDown: a failed assertion aborts the test where it stands, so a cleanup line at the end of a body is the one that does not run on the day it matters.The host-binding tests assert the defaults did not run. Across the two of them a host binds seven of the library's registrations — registrar, plugin checker, deactivator, notice queue, activator, gatekeeper and resolver — and each is asserted to be the object the request reached. On their own those positive assertions would hold just as well if the library had also resolved a second, default copy behind the host's back, so the standalone is left really active, and the test asserts
active_pluginsstill contains it, the default queue is empty and the activation record is empty.exitis never mocked, even end to end.wp_safe_redirectthrows instead, which stops the request where production stops it:run_halted_request()returns where the user was sent and asserts the destination, whilerun_request()installs the same throwing stub for every request that must not redirect and fails the test if one does.preventExit()would let a request carry on past the line production never returns from, which turns a failure into a pass.Four preconditions decide whether any of this means anything, so setUp establishes all four. An interactive admin GET and a user who can
activate_plugins, orConflict\Gatekeeperturns every policy scenario away and they pass while resolving nothing; the hook prefix, which bothplugins_loadedsteps and both option names derive from; and a rewoundplugins_loadedcounter, because the harness dispatched that hook before any test ran and boot() would rightly report it is too late to wire. All four are process-global, and all four are restored in tearDown rather than at the end of a test body.