- PHP 8.1+
- Composer
- MySQL 8.0 server plus client tools (
mysql,mysqladmin) (for integration tests only) - SVN + unzip (for integration test setup only β
apt install subversion unzipon Ubuntu,brew install subversionon macOS)
composer installInstall the pre-commit hook to enforce reviewer agent approval before every AI-generated commit:
cp .githooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commitFor your own (non-AI) commits, bypass the hook with USER_COMMIT=1:
USER_COMMIT=1 git commit -m "message"composer test:unitRuns in ~0.3s. No external dependencies β all WordPress functions are mocked with Brain\Monkey.
# Recommended: pin WP test paths to the repo-local .tmp directory.
# This avoids accidentally using stale global /tmp test configs with mismatched DB credentials.
export WP_TESTS_DIR="$PWD/.tmp/wordpress-tests-lib"
export WP_CORE_DIR="$PWD/.tmp/wordpress"
# One-time setup: installs the WordPress 7.0 (GA) test library and creates test DB.
# 7.0 resolves to the branches/7.0 test library (the same lane CI's WP 7.0 matrix uses)
# and the latest 7.0.x core. Pass "latest" to track the newest stable line instead.
bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 7.0
# Run tests
composer test:integrationSet WP_MULTISITE=1 to run the multisite test suite:
WP_MULTISITE=1 composer test:integrationIf the generated host-side WordPress test config points at a stale or unreachable
Docker-published MySQL port after a wp-env rebuild, composer test:integration
now falls back automatically to the running wp-env tests-cli container and
executes the same suite there against the tests-mysql service.
If 127.0.0.1 is not serving MySQL but Local by Flywheel is running, bin/install-wp-tests.sh now auto-detects a single Local MySQL socket under ~/Library/Application Support/Local/run/*/mysql/mysqld.sock and rewrites the generated DB_HOST to localhost:/path/to/mysqld.sock.
If you have multiple Local sites running and want to choose a specific socket, pass it explicitly:
WP_TESTS_DIR="$PWD/.tmp/wordpress-tests-lib" \
WP_CORE_DIR="$PWD/.tmp/wordpress" \
bash bin/install-wp-tests.sh \
wordpress_test \
root \
root \
"localhost:$HOME/Library/Application Support/Local/run/<site-id>/mysql/mysqld.sock" \
7.0To discover the available Local sockets:
find "$HOME/Library/Application Support/Local/run" -name mysqld.sockIf you do not want to export variables in your shell, prefix each command:
WP_TESTS_DIR="$PWD/.tmp/wordpress-tests-lib" \
WP_CORE_DIR="$PWD/.tmp/wordpress" \
composer test:integration
WP_TESTS_DIR="$PWD/.tmp/wordpress-tests-lib" \
WP_CORE_DIR="$PWD/.tmp/wordpress" \
WP_MULTISITE=1 composer test:integrationYou can run Playwright against an existing local WordPress site (Studio or Local by Flywheel) instead of wp-env:
WP_BASE_URL="http://your-local-site.test" \
WP_USERNAME="admin" \
WP_PASSWORD="your-password" \
npm run test:e2e:localUse the local site's real admin credentials. WP_BASE_URL can be http:// or https:// depending on your local environment.
For WordPress Studio, use the Studio site's localhost port directly. Studio is the
recommended local path for SQLite-focused verification because it avoids pretending
that the default wp-env Docker stack exercises SQLite:
WP_BASE_URL="http://localhost:8881" \
WP_USERNAME="admin" \
WP_PASSWORD="password" \
npm run test:e2e:localIf you point Playwright at a Studio site, keep WP_BASE_URL and WP_REQUEST_BASE_URL
on the same localhost:<port> origin unless you intentionally configured a custom
domain for that Studio instance.
To run the multisite network-admin regression against the multisite-subdomains Local site:
WP_BASE_URL="https://multisite-subdomains.local" \
WP_USERNAME="test" \
WP_PASSWORD="test" \
npm run test:e2e:local:multisiteThis spec is local-only. It skips automatically under the default single-site wp-env configuration used in CI.
Some Local sites use a copied wp-content/plugins/wp-sudo directory instead of
the live repo checkout. In that case the site can drift behind main even when
this repo is up to date.
The canonical helper for compare/sync/symlink operations is:
npm run local:plugin -- status
npm run local:plugin -- sync
npm run local:plugin -- linkBy default it targets the active Local multisite environment used for multisite-subdomains.local:
/Users/danknauss/Development/Local Sites/multisite-subdomains/app/public/wp-content/plugins/wp-sudoOverride that path with SITE_PLUGIN=/path/to/site/wp-content/plugins/wp-sudo.
Before debugging unexpected Local-site behaviour, run:
npm run local:plugin -- statusIf the site copy differs, sync it from the repo before continuing:
npm run local:plugin -- syncIf you want zero drift risk, replace the copied plugin directory with a symlink to this repo checkout:
npm run local:plugin -- linkThe link command preserves the previous copied plugin directory as a timestamped
backup before replacing it with a symlink.
The Playwright suite is split into two projects:
chromiumβ required functional E2E coverage. This is whatnpm run test:e2eruns locally and what the requiredE2E TestsGitHub workflow runs in CI.chromium-visualβ visual regression baselines only. This is whatnpm run test:e2e:visualruns locally and what the separate non-blockingE2E Visual Baselinesworkflow runs.
Run the functional suite locally:
npm run test:e2eIf wp-env gets stuck after a partial Docker startup, clear the generated stack
and retry:
npm run env:stop
npm run env:clean
npm run env:start
npm run env:assert-wp-versionThe last command verifies that both wp-env environments are actually running
WordPress 7.0-RC1, not just that the containers started.
Run both Playwright projects locally:
npm run test:e2e:allRun visual baselines only:
npm run test:e2e:visualRefresh visual snapshots intentionally:
npm run test:e2e:visual -- --update-snapshotsDo not update visual snapshots just to make required CI pass. Review the uploaded Playwright artifacts from the E2E Visual Baselines workflow first, then refresh snapshots only when the UI change is intentional.
WP Sudo now has two different kinds of browser/compatibility workflows:
- Primary release gates:
PHPUnit,Psalm,CodeQL,E2E Tests, andE2E Nginx Smoke. These should all be green before tagging a release. - Breadth workflows:
WordPress Compat SweepandE2E SQLite Smoke. These expand compatibility signal across older WordPress minors and SQLite, but they are not the fast-feedback path for every push or PR.
The nginx smoke workflow is treated as a first-class release gate because it is fast, stable, and catches stack-sensitive issues around routing, cookies, redirects, replay, and AJAX behavior that the default Apache stack can miss.
The scheduled compatibility sweep and SQLite smoke workflow should still be checked before release when the touched code could plausibly affect:
- WordPress version compatibility (
6.3through6.6) - MariaDB-specific behavior outside the main integration matrix
- SQLite request handling, bootstrap timing, or alternate persistence behavior
- stack-sensitive flows such as redirects, cookies, request replay, and admin/AJAX request handling
When SQLite-specific release assurance matters, run the Studio checklist in
docs/studio-sqlite-release-runbook.md
instead of treating the CI smoke lane as a full substitute for local SQLite verification.
When expanding the alternate-stack smoke pack, keep it focused on stack-sensitive behaviors only. Do not clone the full Playwright suite onto every environment. Prefer adding or extending smoke cases when a flow depends on:
- cookies or session persistence
- redirects or replay after auth
- admin-bar or AJAX request handling
- settings POST replay
- challenge rendering or browser-visible auth transitions
If mysql or mysqladmin is not found, install client tooling first:
# macOS (Homebrew)
brew install mysql-client
# Ubuntu/Debian
sudo apt install mysql-clientHomebrew may install client binaries without linking them on PATH. If needed:
# Apple Silicon
export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"
# Intel macOS
export PATH="/usr/local/opt/mysql-client/bin:$PATH"composer analyse:phpstan # PHPStan
composer analyse:psalm # Psalm + WordPress plugin/stubs
composer analyse # Runs both
composer lint # PHPCS (WordPress-Extra + WordPress-Docs + VIP rulesets)
composer lint:fix # Auto-fix PHPCS violationsCurrent live counts are centralized in docs/current-metrics.md.
composer verify:metricsIf it reports drift, update docs/current-metrics.md first, then re-run composer verify:metrics until it passes.
- Update
docs/current-metrics.mdfirst whenever counts change. - Update
docs/release-status.mdfirst whenever release state changes (stable tag, unreleasedmainwork, latest supported WordPress release, forward-lane pin, or delayed release date assumptions). - Prefer linking to
docs/current-metrics.mdanddocs/release-status.mdinstead of copying volatile counts or dates into prose. - When WordPress release timing changes, grep for stale fixed-date references (for example
April 9, 2026,7.0-RC1, orGA) acrossdocs/, readmes, and maintainer instruction files. - Treat
.planning/as historical working material unless a file explicitly says it is current.
Two environments are used deliberately β choose based on what you are testing:
Unit tests (tests/Unit/) use Brain\Monkey to mock all WordPress functions. Use for:
- Request matching logic (Gate surfaces, action registry)
- Session state machine and policy enforcement
- Hook registration
- Settings sanitization and defaults
- Upgrader migration logic
Integration tests (tests/Integration/) load real WordPress against a MySQL database. Use for:
- Full reauth flows (Gate β Challenge β Session β Stash)
- Real bcrypt password verification (
wp_check_password) - Transient TTL and cookie behavior
- REST and AJAX gating
- Two Factor plugin interaction
- Multisite session isolation
- Upgrader migrations against real DB
When in doubt: if the test needs a real database, real crypto, or calls that cross class boundaries in production, write an integration test.
Do not optimize for a single global coverage percentage. WP Sudo is a security plugin, so coverage targets are risk-weighted by test category:
- Unit tests: target
90%+line coverage on business logic inincludes/, and near-100%branch coverage on security-critical state machines and policy code such asSudo_Session,Gate,Challenge,Action_Registry, and request replay logic. - Integration tests: target
100%scenario coverage for security-critical WordPress flows at least once in a real environment. That includes session activation/expiry, grace-window behavior, password failure/throttle/lockout/expiry recovery, 2FA pending/validation/resend/expiry, stash save/get/delete/replay, AJAX/REST/admin exit behavior, and multisite isolation. - E2E/browser tests: target
100%coverage of every distinct user-visible challenge/replay branch. Each visible password, 2FA, stale-session, resend, throttle, lockout, expiry-recovery, and replay path should have at least one Playwright case. - Bridge files: target
100%unit coverage for first-party integration bridges. These adapters are thin and should be fully covered directly. - Lifecycle code: target
100%integration coverage for activation, deactivation, uninstall, and upgrade routines.
Static analysis is part of the assurance target, not optional polish. composer analyse:phpstan, composer analyse:psalm, composer lint, and the required GitHub workflows should remain green at all times.
Current live counts and matrix details are centralized in docs/current-metrics.md. Current stable-vs-forward release posture is centralized in docs/release-status.md. Keep this section qualitative to avoid stale duplication.
In broad terms, the strongest covered area is the challenge flow itself: password and 2FA auth, stale tabs, resend behavior, throttle/lockout UX, expiry recovery, and request replay all have automated coverage. The main remaining gaps are matrix depth on alternate stacks rather than core flow depth: the full browser suite still runs only on the default Apache + MariaDB lane, while nginx, multisite nginx, and SQLite currently run focused stack-smoke subsets.
Keep tests/e2e/specs/stack-smoke.spec.ts single-site only. Its job is fast stack-sensitivity coverage around cookies, redirects, replay, AJAX, and REST, not full environment matrix depth.
Multisite alternate-stack coverage now lives in its own lane:
- workflow:
e2e-nginx-multisite.yml - compose stack:
nginx-mariadb-multisite.compose.yml - spec:
multisite-stack-smoke.spec.ts
That lane intentionally starts small:
- network-admin challenge cancel/return behavior
- one gated network-admin POST replay
It is a breadth workflow, not a merge gate. Keep single-site and multisite alternate-stack signals separate so failures stay easy to interpret. Keep SQLite multisite out of CI until the nginx + MariaDB multisite lane proves stable over time.
Every PR automatically gets a "Try in WordPress Playground" comment with a
link that installs the plugin from that PR's commit and lands you in the admin
logged in as admin / password.
Current Playground previews are pinned to WordPress 7.0-RC1. See docs/release-status.md for the current forward-lane posture and latest stable WordPress release.
For WordPress 7.0 release signoff, do not treat the green RC-era CI matrix as a substitute for the remaining RC/GA manual passes. RC1 is recorded in the 15.0 Release Signoff Log table in tests/MANUAL-TESTING.md; repeat that signoff for the scheduled RC3/RC4 checkpoints and again for the final 7.0 release before claiming final readiness. See docs/release-status.md for the current dates.
The repo ships several Playground blueprints. All are source-only artifacts
and are excluded from the distributed plugin ZIP (see .pressshipignore).
| File | Tier | Purpose | Install source | Documented in |
|---|---|---|---|---|
blueprint.json |
Public demo | "Try latest release" β stable-tag demo with the standard seed | archive/refs/tags/vX.Y.Z.zip (bumped at tag time) |
readme.md / readme.txt badges |
blueprint-main.json |
Public demo | "Try main" β tracks the main branch |
archive/refs/heads/main.zip |
readme.md / readme.txt badges |
blueprint-editor-reauth.json |
Public/reviewer scenario | In-editor reauthentication demo β opens the block editor without an active sudo session so Block Directory plugin install/activate can trigger the modal | release branch or main while the feature is unreleased; tag-pinned copy after release if kept as a public demo |
readme.md while featured; docs/ui-ux-testing-prompts.md Β§6c |
blueprint-editor-2fa.json |
Public/reviewer scenario | In-editor two-factor demo (Milestone B) β installs Two Factor, enrolls the admin in TOTP (public sandbox secret), and shows the current code in an editor notice so the in-modal 2FA step can be completed after the password | main while unreleased; tag-pinned copy after release if kept as a public demo |
readme.md while featured |
blueprint-recovery-mode.json |
Reviewer scenario | Break-glass WP_SUDO_RECOVERY_MODE demo |
main |
docs/ui-ux-testing-prompts.md Β§6a |
blueprint-user-switching.json |
Reviewer scenario | Session-theft demo via the User Switching plugin | main |
docs/ui-ux-testing-prompts.md Β§6b |
blueprint-multisite.json |
Public/reviewer scenario | Multisite/network-admin demo β converts the standard-seed single site to a network (enableMultisite), network-activates WP Sudo, and lands the super admin on the network Sudo settings page (/wp-admin/network/settings.php?page=wp-sudo-settings) |
main while featured; tag-pinned copy after release if kept as a public demo |
readme.md while featured; docs/ui-ux-testing-prompts.md Β§6d |
.github/playground/sqlite-stack-smoke.blueprint.json |
CI smoke | SQLite drop-in stack smoke used by the E2E-SQLite workflow | (CI-driven) | .github/workflows/e2e-sqlite.yml |
The two standard public blueprints are surfaced via the readme "Try in
Playground" badges. Feature-specific public demos, such as the in-editor
reauthentication blueprint, may be temporarily surfaced while the feature is new;
before tagging a release, either tag-pin them like blueprint.json or clearly
move them back to reviewer-only status. The reviewer blueprints stage
hard-to-reach states for manual review (not linked from the readme by default);
the CI smoke blueprint is owned by its workflow. When adding a new blueprint,
add it to .pressshipignore and to this table.
Use this checklist for each later RC and again at GA:
- Repin the forward WordPress lane references if the build string changed:
- Run section
15intests/MANUAL-TESTING.mdand record the result in the15.0 Release Signoff Log. - Verify the standard local checks:
composer test:integrationWP_MULTISITE=1 composer test:integrationcomposer analyse:phpstancomposer analyse:psalmcomposer lint
- At GA only, update
Tested up toand release metadata surfaces:readme.txtreadme.md- any release notes or changelog entry being prepared for the next tag
After WordPress 7.0 GA lands, remove the temporaryβ done;handle_err_admin_role()workaroundrewrite_role_error()andrender_role_error_notice()removed in v3.4.0 after Trac #64690 confirmed in WP 7.0 GA.
| Feature | Notes |
|---|---|
| Plugin activation & settings page | β |
| Gate fires on dangerous actions (plugin activate/delete, user delete, etc.) | β |
| Challenge / reauthentication page | β |
| Password verification & session cookie | β |
| Admin bar countdown timer | β |
| Request stash & replay after auth | β |
| Rate limiting / 5-attempt lockout | β within session |
| Session expiry by time | β wait out the configured duration (1β15 min) |
| Two Factor plugin (TOTP) | β installed automatically via blueprint |
unfiltered_html removed from Editor role |
β |
| Block-editor reauthentication UI | β
use blueprint-editor-reauth.json; in the editor, open the inserter, search a Block Directory block, and install/activate it to trigger the password modal |
The block/site editor middleware handles any cookie-authenticated REST response
with code: "sudo_required", but the default WordPress UI only exposes a small
set of dangerous actions from inside editor screens β chiefly Block Directory
install/activate (/wp/v2/plugins), which is the primary Playground demo path.
Ordinary content/design saves (posts, pages, templates, global styles,
navigation, media, widgets, fonts, reusable blocks) are deliberately not
gated.
The canonical, per-route inventory of which editor actions can trip a sudo
challenge lives in
docs/ui-ux-testing-prompts.md Β§6c (the
Editor-triggerable gated-action inventory). Keep it as the single source; if a
future release adds a first-party visible editor control for another gated REST
route, update that inventory, this release checklist, and the Playground
instructions together.
| Feature | Why |
|---|---|
| WP-CLI / Cron entry point policies | No CLI in browser |
| REST / XML-RPC entry point policies | Network disabled in Playground |
| Two Factor email / magic-link providers | PHP outbound network is off |
| WebAuthn bridge (security key gating) | Browser WebAuthn API unavailable in Playground sandbox |
| Multisite behaviour in the standard public demos | The default public blueprints are single-site; add a dedicated multisite blueprint/link when a release needs a network-admin demo |
| State after refreshing Playground | Full reset on page reload |
Transients and user meta persist across normal WP navigation within a session,
but are wiped if you reload the Playground page itself. Use the integration test
suite (composer test:integration) to verify transient TTL, real bcrypt, and
multisite isolation β those require a real MySQL database and can't be covered by
Playground.
To test the current main branch interactively without opening a PR, use
blueprint.json directly:
https://playground.wordpress.net/# + the URL-encoded contents of that file.
- Write a failing test first β commit or show it before writing production code
- Write the minimum production code to pass
composer test:unitmust pass before every commitcomposer analysemust pass before every commit (PHPStan + Psalm)
The one-click installer on the Settings page copies the shim via file_put_contents. If it fails due to file permissions in your local environment, install manually:
- Copy
mu-plugin/wp-sudo-gate.phpfrom the plugin directory - Paste it into
wp-content/mu-plugins/wp-sudo-gate.php(create the directory if needed) - The mu-plugin will be active on the next page load
Use conventional commit format. Run tests and static analysis before committing.
The tests/MANUAL-TESTING.md checklist covers 20 test areas across all surfaces (admin UI, AJAX, REST, CLI, Cron, XML-RPC, WPGraphQL, multisite). Run through it before tagging a release. The file includes expected results, curl commands, and cleanup steps. For structured UI/UX-focused testing scenarios, see docs/ui-ux-testing-prompts.md.
Use the wp_sudo_gated_actions filter. See docs/developer-reference.md for the rule structure and examples.
The bridges/ directory contains drop-in mu-plugins that gate third-party plugin actions:
| Bridge | Target Plugin | What It Gates |
|---|---|---|
wp-sudo-wp2fa-bridge.php |
WP 2FA (Melapress) | Connects WP 2FA's TOTP/email/backup code methods to the sudo challenge page |
wp-sudo-webauthn-bridge.php |
Two Factor Provider for WebAuthn | Gates security key registration and deletion via AJAX |
To create a new bridge:
- Copy an existing bridge as a template
- Add a class-existence guard so rules are only injected when the target plugin is active
- Add AJAX and/or REST rules via
wp_sudo_gated_actions - Add unit tests in
tests/Unit/and manual test steps intests/MANUAL-TESTING.md - Document in
docs/developer-reference.mdanddocs/two-factor-integration.md(if 2FA-related)
See docs/developer-reference.md for the full worked example.
Nine action hooks are available for external logging. See docs/developer-reference.md.