12: Conflict resolution - #12
Open
nikolaystrikhar wants to merge 1 commit into
Open
Conversation
nikolaystrikhar
force-pushed
the
12-conflict-resolver
branch
from
August 6, 2026 07:36
59ad699 to
f44d91b
Compare
nikolaystrikhar
force-pushed
the
11-loader-load-path
branch
from
August 6, 2026 14:22
7d4435a to
4d1c627
Compare
nikolaystrikhar
force-pushed
the
12-conflict-resolver
branch
from
August 6, 2026 14:22
f44d91b to
db11979
Compare
This was referenced Aug 10, 2026
nikolaystrikhar
force-pushed
the
11-loader-load-path
branch
from
August 11, 2026 10:47
4d1c627 to
fe01f47
Compare
nikolaystrikhar
force-pushed
the
12-conflict-resolver
branch
from
August 11, 2026 10:47
0e4b982 to
4aa12bd
Compare
nikolaystrikhar
force-pushed
the
11-loader-load-path
branch
from
August 11, 2026 15:39
fe01f47 to
4d56787
Compare
Conflict\Resolver detects a registered sub-plugin whose standalone is still active and applies the policy: DEFER stands down, NOTICE_ONLY explains, and DEACTIVATE turns the standalone off, queues the merge notice and redirects so the screen re-renders without it. The switch puts default: on the branch that only talks. Conflict_Policy is_valid() normalises an unrecognised policy to NOTICE_ONLY first, so default: is unreachable today -- but a fourth policy added later without a matching case must not be read as consent to turn off a plugin the site owner chose. Two gates, deliberately in different places. Resolution runs only on an interactive admin GET, in the Loader, where a host that rebinds the resolver cannot drop it: unguarded, a visitor's checkout POST comes back as a 302 that discards the order. Deactivation additionally needs activate_plugins, and that lives in the resolver, because a replacement owns its own policy -- plugins_loaded fires long before auth_redirect(), so an anonymous GET of an admin URL gets this far.
nikolaystrikhar
force-pushed
the
12-conflict-resolver
branch
from
August 11, 2026 15:50
4aa12bd to
cbf226e
Compare
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:
Conflict\Resolverfinds a registered sub-plugin whose standalone is still active and applies its policy — defer, notify, or deactivate.Usage:
The standalone is deactivated, the merge notice is queued, and the admin lands back on the screen they were on with it gone.
Why this way:
default:sits on the NOTICE_ONLY branch, not the DEACTIVATE one.Conflict_Policy::is_valid()normalises an unrecognised policy first, sodefault:is unreachable today — but it is one refactor from being live, and a fourth policy constant added without a matchingcasemust not be read as consent to turn off a plugin the site owner deliberately activated. The two safety gates sit in different places on purpose: request-level gating (interactive adminGETonly — not cron, not WP-CLI, not ajax, not a form POST) lives in theLoadertrampoline where rebinding the resolver cannot drop it, because ungated this turns a visitor's checkout POST into a 302 that discards the order; theactivate_pluginscheck lives in the resolver, because a host that binds its own resolver owns its own policy, andplugins_loadedfires long beforeauth_redirect()so an anonymous GET of an admin URL reaches this code. The redirect matches on the screen basename rather than a substring ofadmin_url():wp_get_referer()prefers the_wp_http_refererfield, which holds a bare path, so a URL comparison misses every admin form POST and the whole network admin.Verify:
slic run unitandslic run unit --env multisite— 298 tests green on both legs, pluscomposer test:analysis.exitis never mocked:wp_safe_redirectis stubbed to throw, and each test asserts both that it halted and the halt message, so "never redirected at all" cannot pass silently. Not covered here: the activation callback (#13) and the activation-error rewrite (#14). Rebased onto #11; the resolver interface now lives atConflict\Contracts\Resolver_Interfaceper the folder-scoped convention, and the notices collaborator is mergedmain'sNotices\Queue.