This package boots a local WordPress Playground site, mounts a plugin, captures front-end output, and compares before and after states.
In your open plugin repository, tell your AI agent:
Implement https://github.com/theseoframework/wp-plugin-regression for this repo.For live implementation examples, see the-seo-framework and troy.
- Import this Git's files to a private folder location.
- Open Terminal.
cdto folder.- Enter:
npm install.
- Open Terminal.
cdto folder.- Enter:
git pull. - Enter:
npm install.
- Open Terminal.
cdto folder.- Enter:
node run.js <command> --root <consumer> --plugin-json <plugin.json>.
Commands:
node run.js launch --root C:\path\to\plugin --plugin-json C:\path\to\plugin.json
node run.js stop --root C:\path\to\plugin
node run.js stop --all
node run.js capture --root C:\path\to\plugin --plugin-json C:\path\to\plugin.json --label before
node run.js compare --root C:\path\to\plugin --plugin-json C:\path\to\plugin.json --before before --after after
node run.js harness --root C:\path\to\plugin --action ping
node run.js harness --root C:\path\to\plugin --json-file C:\path\to\payload.json
node run.js surfaces --root C:\path\to\plugin --plugin-json C:\path\to\plugin.json
Optional launch flags: --wp, --php, --site, --plugin=working|wporg, --port, --pair, --keep, --workspace, --workers=<n|auto>, --extra-plugin-zip=<path>. Repeat --extra-plugin-zip or pass comma-separated paths. The engine extracts each zip into ~/.wordpress-playground/extra-plugins/ (same volume as persist, never the consumer repo), discovers Plugin Name, and activates it. Use this for off-site / commercial plugins. Do not unpack those zips into the consumer tree. --workers defaults to max(6, os.cpus().length - 1). Playground auto is cpus-1 with no floor; their unset default is min(6, that).
Launch passes --php-extension lib/php-ini/manifest.json so PHP-WASM reads lib/php-ini/php.ini before php_wasm_init. PHP resource limits stay at compiled defaults (memory_limit=128M, upload_max_filesize=2M, post_max_size=8M; max_execution_time=0 is the CLI SAPI default) so Playground's 256M / 2000M padding cannot hide faults. Error logging stays on. OPcache is in-memory (file_cache_only=0, memory_consumption=256, max_accelerated_files=7963, interned_strings_buffer=32). Launch passes --workers as max(6, cpus-1) (15 on 8 cores / 16 threads). Each worker has its own SHM. Playground's own OPcache defaults are 1000 files, file_cache_only=1, and 64M SHM (fills with large plugin trees). setPhpIniEntry after boot cannot change PHP_INI_SYSTEM values. JIT stays off: Zend JIT emits native x86_64 / i386 / arm64 and PHP-WASM has no backend. The dummy .so is not loaded. Restart the site after engine changes to this ini.
--wp is a Playground build slug (latest, beta, trunk / nightly, 7.0, 6.9.1, 6.8-RC1, or a zip URL), not a path to Core. resolveWordPressRelease() turns that slug into { version, releaseUrl } (for example latest → 7.1 and the zip URL). Playground caches the zip as ~/.wordpress-playground/<version>.zip. The engine unpacks a slim copy at ~/.wordpress-playground/wp/<version>/ (same version token), keeps WP_DEFAULT_THEME, and mounts it with install-from-existing-files. Official Core zips still ship several Twenty* themes; there is no one-theme bundle.
Site persist is not in the consumer repo. It lives at ~/.wordpress-playground/tests/<plugin.slug>/<version>/<site>/. Launch wipes that folder unless you pass --keep. Live runs are listed in ~/.wordpress-playground/tests/runs.json. Each run is keyed with workspace (explicit --workspace, else the folder name of --root). Legacy runs without that field still match by the folder name of root. stop with no --port stops every run for this workspace and drops its stale entries. stop --all stops every run in runs.json (all consumers) and removes stale entries; use it only when the operator asks to purge every workspace. --root is not required with --all. trunk is the prebuilt WordPress/WordPress nightly. It is not a local wordpress-develop tree. --wp=7.2 only works if Playground hosts that release. PHP is --php (7.4–8.5).
Launch sets the site title to <plugin name> Playground (plugin.json name, else the Plugin Name header, else slug). Agents may change blogname afterward.
A/B is two capture labels and compare --before <prev> --after <cur>. Launch takes the next free port in 9001–9099. launch --pair takes the next two consecutive ports (wordpress.org --site=before, working tree --site=after). capture --label before / --label after pick those sites. compare diffs the JSON bundles and does not need a live server; each bundle's origin is rewritten independently. Pin ports with --port or --port-before / --port-after. Do not reuse one persist folder across --wp versions.
This engine does not drive a browser. Logged-out HTTP capture covers front-end artifacts. Admin UI and REST-from-the-browser A/B is a Playwright MCP consumer of the live site URL after launch.
plugin.json may list entries (id, type, path, optional frame), surfaces (feature → page types), surfaceLines (feature → substrings), and headTags (regexes). capture writes one captures/<label>.json bundle. HTML captures store the plugin head-marker block, then prepend headTags matches from <head> that are not already in that block. Those extras are ambiguous (theme, core, or the plugin). capture / compare accept --feature=<name> or --types=post,page and print that feature’s page list first. compare --feature then diffs only matching lines (plus status/location). surfaces prints the whole map.
--root is the consumer repo (where .local/playground is written). Optional dir is a package folder relative to --root; mounts resolve from there. activate (default true) controls the blueprint activatePlugin step. extraPlugins is more { slug, dir, mainFile, mounts, activate } entries mounted and optionally activated the same way. An entry may set zip instead of dir / mounts; the engine extracts that archive the same way as --extra-plugin-zip. extraMounts is [ hostRel, vfs ] directory pairs resolved from --root (trees that must not land under wp-content/plugins/). When vfs is under /wordpress/wp-content/, the engine also hardlinks that tree into the persisted site so a parent wp-content mount cannot hide the files. Omit these fields for a single-plugin repo whose --root is the plugin itself. Pass off-site zips on the CLI so a single-plugin plugin.json stays clean.
harness action frame asks the consumer to switch a reading frame (for example blog-on-front vs a static front page).
Captures are logged-out. The server is not started with --login. Same-path redirects are followed; a redirect to a different path is recorded as-is.
capture also accepts --path / --paths (comma-separated) and appends those URLs to the bundle.
Harness post and term accept slug, meta (object), and optional id to update. Replies include url and path. meta writes one key (type is post or term). Consumer shims may handle plugin-specific meta via wp_plugin_regression_update_meta.
Pretty permalinks are set in the blueprint, but rewrite rules are not flushed there. Playground must flush them on a later init after post types exist. A flush_rewrite_rules() during boot writes incomplete rules and makes post permalinks and 404s fall through to the homepage.
Workaround until WordPress/wordpress-playground#4325 is patched: Playground ships a mu-plugin that 301s /sitemap.xml to /wp-sitemap.xml. Launch overwrites that file with a valid PHP no-op so the mounted plugin can own the endpoint. Drop the overwrite when that issue lands. Do not write '<?php\n' in a single-quoted PHP string; that is a parse error and every request 500s.
Captures stay under the consumer .local/playground/captures/. Sites, slim Core trees, wporg zips, and runs.json stay under ~/.wordpress-playground/.
Node.js 20.18 or higher must be on your PATH.
stop sends a graceful tree kill first, then force, and waits until the site SQLite file is unlocked. If a hard kill still leaves that file unreadable, retry stop, then launch (default is a fresh wipe).
PowerShell strips quotes from --json "{...}". Use --json-file <path> (resolved from --root when relative).