本文へ移動
cccskills
無料GitHub で公開

mcp-ai-wpoos-test-suite

Repair and triage guide for the NV oOS PHPUnit test suite — Docker test environment (incl. cross-worktree one-off runners), CI log triage, and 71 recurring root-cause patterns (hook resets, singleton interference, WP_Error envelope drift, coverage-manifest drift, preset-accounting gaps, and more — see the patterns section). Covers the cluster-by-cluster PR workflow against alpha-working and validation gates. Use when fixing failing PHPUnit tests, triaging CI logs, repairing test drift, deciding between a production fix and a test fix, or starting a new fix cluster.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md74.0 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

NV oOS Test Suite — Repair & Triage Guide

Operational playbook for keeping the single-process PHPUnit suite green, one cluster (suite) at a time. Distilled from ~70 cluster PRs (#6084–#6153) against the alpha-working branch. Complements .context/testing.md (how to write tests) — this skill covers how to fix a failing suite and how the repair loop runs.

When to use this skill

  • A PHPUnit suite is failing in CI, locally, or both
  • Triaging a fresh CI log zip (logs_*.zip)
  • "Continue the test-suite cluster" / "fix the next cluster" requests
  • Deciding whether a failure is test drift or a production bug
  • Opening the cluster PR (branch/commit/validation conventions)

For proactive per-toolkit hardening audits (shell-call guards, nonexistent client methods, provider array-content parsing, enum/service mismatches) use the mcp-ai-wpoos-toolkit-audit skill — it drives the audit loop and reuses this skill's cluster/PR/validation mechanics.

For SPA/JS tests (vitest + jsdom + Testing Library in the toolkit-shell, chat-spa, and schedule-anything-spa addons — Radix polyfills, jsdom gotchas, bundle gates) use the mcp-ai-wpoos-spa-ui skill. This skill covers the PHPUnit suite only; the two stacks are disjoint.

Test environment (Docker)

The test suite runs inside the oos-wp container (plugin bind-mounted at /var/www/html/wp-content/plugins/mcp-ai-wpoos, DB in oos-wp-db). Run from the repo root on the Windows host with MSYS_NO_PATHCONV=1.

WP 6.9 (primary):

MSYS_NO_PATHCONV=1 docker exec -e WP_CORE_DIR=/var/www/html -e WP_DB_HOST=db -e WP_DB_NAME=wordpress_test -e WP_DB_USER=wordpress -e WP_DB_PASSWORD=wordpress oos-wp sh -c 'cd /var/www/html/wp-content/plugins/mcp-ai-wpoos && php -d memory_limit=1G vendor/bin/phpunit <paths> --no-coverage 2>&1'

WP 7.1 (second validation): swap WP_CORE_DIR=/tmp/wp71 and WP_DB_NAME=wordpress_test_wp71.

phpcs (the CI gate counts ERRORS, not warnings):

MSYS_NO_PATHCONV=1 docker exec oos-wp sh -c 'cd /var/www/html/wp-content/plugins/mcp-ai-wpoos && php vendor/bin/phpcs --standard=phpcs.xml.dist --error-severity=1 --warning-severity=1 --report=summary <paths> 2>&1'

Rules:

  • Never run two docker exec phpunit concurrently — they collide on the shared wordpress_test DB.
  • Validate every cluster on both WP 6.9 and WP 7.1 before opening the PR.
  • The local bootstrap lacks JetEngine and Graphify; the CI bootstrap loads them. A suite skipped locally can still fail in CI — and vice versa.
  • phpcbf can auto-fix (e.g. array alignment): run it inside the container, it edits the bind-mounted file directly.

Cross-worktree runs (when oos-wp mounts a different worktree)

The oos-wp plugin path bind-mounts one worktree. Check which one:

docker inspect oos-wp --format "{{json .Mounts}}"

If the mounted path is not the worktree you are editing, docker exec oos-wp runs the wrong code. Instead run a one-off container on the same network sharing the WP-core volume, with your worktree mounted over the plugin path:

# 1. Build a Linux vendor into a named volume (the Windows-host vendor/
#    breaks inside Linux — classmap paths with backslashes →
#    'Class "PHPUnit\\TextUI\\Application" not found').
#    REUSE an existing complete volume when present (docker volume ls):
#    mcp-ai-wpoos-vendor ships with PHPUnit + wp-phpunit and needs no build.
docker volume create <worktree>-vendor
MSYS_NO_PATHCONV=1 docker run --rm \
  -v F:/GITHUB/worktrees/mcp-ai-wpoos/<worktree>/mcp-ai-wpoos:/app:ro \
  -v <worktree>-vendor:/app/vendor \
  composer:2 sh -c 'cd /app && composer install --no-interaction --prefer-dist --no-progress'

# 2. Run the tests (nested mounts: volume over the worktree's vendor/ works).
MSYS_NO_PATHCONV=1 docker run --rm \
  -e WP_CORE_DIR=/var/www/html -e WP_DB_HOST=db -e WP_DB_NAME=wordpress_test \
  -e WP_DB_USER=wordpress -e WP_DB_PASSWORD=wordpress \
  -v oos-wp_wp_core:/var/www/html \
  -v F:/GITHUB/worktrees/mcp-ai-wpoos/<worktree>/mcp-ai-wpoos:/var/www/html/wp-content/plugins/mcp-ai-wpoos \
  -v <worktree>-vendor:/var/www/html/wp-content/plugins/mcp-ai-wpoos/vendor \
  --network oos-wp_default \
  wordpress:6.9-php8.2-apache \
  sh -c 'cd /var/www/html/wp-content/plugins/mcp-ai-wpoos && php -d memory_limit=1G vendor/bin/phpunit <paths> --no-coverage'

WP 7.1 cross-worktree: the wp71 core lives in the oos-wp container's own /tmp/wp71 (NOT in the shared oos-wp_wp_core volume). Copy it out, mount it at /tmp/wp71 in the one-off run, and swap WP_CORE_DIR=/tmp/wp71 + WP_DB_NAME=wordpress_test_wp71:

docker cp oos-wp:/tmp/wp71 ./docker-tmp-wp71
# add: -v F:/GITHUB/worktrees/mcp-ai-wpoos/<worktree>/mcp-ai-wpoos/docker-tmp-wp71:/tmp/wp71

When /tmp/wp71 is gone (container restarts wipe it — docker cp errors with Could not find the file /tmp/wp71 in container oos-wp), rebuild a reusable named volume instead of polluting the repo:

# wordpress:cli runs as www-data — run as root to create the volume,
# and raise PHP memory or the extractor fatals at 128M:
docker volume create mf-wp71-core
docker run --rm --user 0:0 -v mf-wp71-core:/wp wordpress:cli-php8.3 sh -c \
  'php -d memory_limit=512M /usr/local/bin/wp --allow-root --path=/wp core download --version=7.1.3 --quiet && chown -R 33:33 /wp'
# then mount it: -v mf-wp71-core:/tmp/wp71 with WP_CORE_DIR=/tmp/wp71 + WP_DB_NAME=wordpress_test_wp71

(WP_CLI_PHP_ARGS env is not honoured by the phar shim — pass php -d memory_limit=512M /usr/local/bin/wp explicitly.)

Clean up afterwards: rm -rf docker-tmp-wp71 and docker volume rm <worktree>-vendor — never stage either artifact. The no-concurrent-phpunit rule applies to one-off runners too (same shared DB).

Pruned-vendor carry-over (hit on the 0.4.7 docs-hub pass): some worktrees carry a partial vendor/ whose vendor/composer/autoload_classmap.php has zero PHPUnit entries — php -r 'require "vendor/autoload.php"; var_dump(class_exists("PHPUnit\\TextUI\\Application"));' prints false on BOTH host and container, and vendor/bin/phpunit dies with Class "PHPUnit\TextUI\Application" not found. Quick check: grep -c PHPUnit vendor/composer/autoload_classmap.php (0 = pruned). The Linux vendor volume is the fix for container runs; for host-side runs, composer install on the host rebuilds a Windows vendor that then breaks INSIDE Linux — pick one environment per session.

docker-tmp-logs/ artifact: running the root suite writes an untracked docker-tmp-logs/ directory into the repo root (random .jpg files + chunk8.txt). Delete it before staging anything (git status --porcelain will show it as ??).

Pro addon dual-matrix runs (nvoos-content-graph-pro)

The PHPUnit Pro Addon CI workflow runs the addon suite twice (monolith + standalone) — mirror it locally with the same one-off runner:

# Standalone matrix (base plugin skipped; add the env var):
-e WP_MCP_AI_PRO_STANDALONE=1 \
# Config: the addon's own phpunit config (bootstrap loads the ecosystem
# plugins + honours the env var):
php -d memory_limit=1G vendor/bin/phpunit -c plugins/nvoos-content-graph-pro/phpunit.xml.dist <paths> --no-coverage

# Monolith matrix: same command WITHOUT the env var.

Standalone plugin suites (plugins/nvoos-content-graph, -ai, -pro)

plugins/nvoos-content-graph ships its own composer.json + phpunit.xml.dist (PHPUnit 9.6, wp-phpunit 7.0.2, PSR-4-only autoload — no Windows classmap hazard, so a host-side composer install in the plugin dir is safe and works inside the Linux container). No monorepo CI workflow runs its phpunit.xml.dist (phpunit-ai.yml only covers the AI addon's phpunit-ecosystem.xml.dist), so run it explicitly:

# Host: install the plugin's dev deps (creates
# plugins/nvoos-content-graph/vendor — gitignored):
cd plugins/nvoos-content-graph && composer install --no-interaction --prefer-dist

Mount-path caution: git rev-parse --show-toplevel BEFORE mounting — the MAIN checkout is F:/GITHUB/mcp-ai-wpoos while F:/GITHUB/worktrees/mcp-ai-wpoos is the PARENT of the git worktrees (worktree repos live at .../mcp-ai-wpoos/<worktree-name>/mcp-ai-wpoos). Mounting the parent silently runs the WRONG code — verify with git worktree list. The Zed file tools may report the worktrees path as the allowed directory even when the terminal's repo root is the main checkout; trust git rev-parse, not the tool metadata.

Write the wp-phpunit config the plugin bootstrap expects. Default lookup is

vendor/wp-phpunit/wp-phpunit/wp-tests-config.php (dirname of includes/).

vendor/ paths are excluded from repo file tools — use the shell:

cat > vendor/wp-phpunit/wp-phpunit/wp-tests-config.php <<'EOF'

<?php define( 'ABSPATH', '/var/www/html/' ); define( 'DB_NAME', 'wordpress_test' ); define( 'DB_USER', 'wordpress' ); define( 'DB_PASSWORD', 'wordpress' ); define( 'DB_HOST', 'db' ); define( 'DB_CHARSET', 'utf8' ); define( 'DB_COLLATE', '' ); define( 'WP_TESTS_DOMAIN', 'example.org' ); define( 'WP_TESTS_EMAIL', 'admin@example.org' ); define( 'WP_TESTS_TITLE', 'Test Blog' ); define( 'WP_PHP_BINARY', 'php' ); $table_prefix = 'wptests_'; EOF # Ensure the DB exists (bootstrap auto-runs install.php single-site unless # WP_TESTS_SKIP_INSTALL=1): docker exec oos-wp-db mysql -uroot -pwordpress -e \ "CREATE DATABASE IF NOT EXISTS wordpress_test; GRANT ALL PRIVILEGES ON wordpress_test.* TO 'wordpress'@'%'; FLUSH PRIVILEGES;" # Run (php:8.2-cli LACKS mysqli — wp_die "missing the MySQL extension"; use # wordpress:6.9-php8.2-apache with --entrypoint php, which ships mysqli): MSYS_NO_PATHCONV=1 docker run --rm --entrypoint php \ -v oos-wp_wp_core:/var/www/html \ -v F:/GITHUB/mcp-ai-wpoos:/var/www/html/wp-content/plugins/mcp-ai-wpoos \ --network oos-wp_default \ -e WP_TESTS_DIR=/var/www/html/wp-content/plugins/mcp-ai-wpoos/plugins/nvoos-content-graph/vendor/wp-phpunit/wp-phpunit \ wordpress:6.9-php8.2-apache \ /var/www/html/wp-content/plugins/mcp-ai-wpoos/plugins/nvoos-content-graph/vendor/bin/phpunit \ -c /var/www/html/wp-content/plugins/mcp-ai-wpoos/plugins/nvoos-content-graph/phpunit.xml.dist --testsuite=Unit ``` - "Full plugin run" = both suites: `--testsuite=Unit` then `--testsuite=Integration` (integration needs only WP + bundled JetEngine stubs). The no-concurrent-phpunit rule still applies (same DB). - JS contract checks need no browser: `node scripts/verify-commerce-fallback.js` (purchase-modal fallback redirect contract) and `node scripts/verify-theme-engine.js` run on the host from the plugin dir. - When only the client plugin is in scope this is faster than the root suite (which boots the full base+Pro plugin); the root suite covers `addons/checkout-api/tests` via the root phpunit.xml.dist. **Shared-DB isolation (cross-worktree clobbering):** every worktree's one-off runner hits the SAME `wordpress_test` DB by default — a concurrent worktree's `install.php` re-runs wipe your schema mid-suite (mass "Table doesn't exist" failures). Isolate per worktree with a dedicated DB: ```bash # One-time: create the isolated DB (db container name from docker ps; creds # in docker-compose.yml — oos-wp-db / wordpress:wordpress by default): docker exec oos-wp-db mysql -uroot -pwordpress -e \ "CREATE DATABASE IF NOT EXISTS wordpress_test_pearl; GRANT ALL PRIVILEGES ON wordpress_test_pearl.* TO 'wordpress'@'%'; FLUSH PRIVILEGES;" # Then point BOTH the schema install and every phpunit run at it: -e WP_DB_NAME=wordpress_test_pearl \ # Schema install (once, or after the DB gets dropped): php vendor/wp-phpunit/wp-phpunit/includes/install.php tests/wp-tests-config.php ``` The env var overrides `DB_NAME` in `tests/wp-tests-config.php` (which reads `getenv( 'WP_DB_NAME' )`) — never edit the tracked config file. - Run `--filter Test_X` first, then the full suite — the full run is ~10-20 min locally. - Sequential monolith + standalone runs may reuse `wordpress_test`; keep the no-concurrent rule. - The `<worktree>-vendor` volume is reusable across clusters: re-run the composer step (a no-op when current). - Expected skips: standalone ~1 skip; monolith skips every standalone-gated test (hundreds — not failures). A summary of "OK, but there were issues" with only warnings/deprecations/skips is a pass. - phpcs for the addon: `--standard=plugins/nvoos-content-graph-pro/phpcs.xml.dist` (phpcbf exit 1 = fixed files, not an error; re-run phpcs to confirm exit 0). - **content-graph session notes (1.0.9 pass):** the plugin's `vendor/` may be PRUNED of PHPUnit entirely (`vendor/phpunit` gone; `vendor/bin/phpunit` points at nothing) — a plain `composer install` in the plugin dir restores the locked set (PHPUnit 9.6.35 + wp-phpunit 7.0.2) and the recipe above then works as written. Point the plugin's gitignored `vendor/wp-phpunit/wp-phpunit/wp-tests-config.php` at an isolated DB (`wordpress_test_nvooscg` pattern) before the run. Current sizes: Unit 154 tests / 725 assertions (~7 s) and Integration 18 / 57 (~8 s) on `wordpress:6.9-php8.2-apache`. ## Cluster → PR workflow 1. `git fetch origin alpha-working` (auto-gc may make this time out — verify with `git --no-pager log -1 --oneline origin/alpha-working` and retry). 2. Create a branch per cluster from `origin/alpha-working`: `git switch -c fix/<cluster-slug> origin/alpha-working`. Never stack on a previous branch — uncommitted worktree edits carry over on switch. 3. Reproduce locally, fix, validate (both WP versions + phpcs). 4. Stage explicit paths only (`git add <file>` — **never `git add -A`**, and **never stage `vendor/`**; the worktree carries local vendor edits that must not be committed). On Windows Git Bash `2>nul` creates a literal `nul` file in the repo — delete stray artifacts before staging. 5. Commit with imperative subject ≤ 50 chars; PR base is `alpha-working`. 6. The user merges manually; move to the next candidate regardless. 7. **Verify `git branch --show-current` before every commit.** If the worktree gets switched under you (e.g. your PR merges and another agent switches branches), a commit can land directly on `alpha-working`. Recovery (done on the 1.0.9 pass): create the cluster branch from `origin/alpha-working`, `git cherry-pick <sha>`, push, then `git switch alpha-working && git reset --hard origin/alpha-working`. Open PRs with `gh pr create` from the terminal — the GitHub-MCP `create_pull_request` tool can fail with "Bad credentials" while the `gh` CLI (keyring-authenticated) works. ### Watching CI checks - Check-run viewers may return empty statuses while jobs run; the reliable watcher is `gh pr checks <n> --repo nvdigitalsolutions/mcp-ai-wpoos` from the terminal. - The `PHPUnit Pro Addon` matrix jobs finish in ~3 min, but the `PHP Linting` workflow (WPCS 3.0 + PHP Compatibility Check) is repo-wide and routinely takes 20–30 min — it finishes long after the matrix checks. Local phpcs on the changed files is the substantive gate; plan CI waits accordingly. ## CI log triage (`logs_*.zip`) 1. Extract to `logs_<id>/` and parse `0_test.txt`. Every line has a timestamp prefix; failures are numbered blocks `NN) Suite::method` followed by the assertion message. 2. Group by suite: ```bash sed -n '<first-failure-line>,<last-failure-line>p' logs_<id>/0_test.txt \ | grep -oE "[0-9]+\) [A-Za-z_0-9]+::[A-Za-z_0-9]+" \ | sed 's/^[0-9]*) //' | awk -F'::' '{print $1}' | sort | uniq -c | sort -rn ``` 3. Identify which code the run tested: grep `refs/remotes/pull/NNNN/merge` — the run is the merge of PR NNNN into its base at checkout time. 4. A suite with **zero occurrences** in the log passed. 5. Standalone-pass-but-full-run-fail ⇒ order-dependent interference (shared singletons, leaked filters) — see patterns below. Reproduce by running the suspect suites in **one** phpunit invocation. ## Recurring root-cause patterns (symptom → fix) 1. **Hook-table restore between tests.** wp-phpunit restores `$wp_filter` between tests, dropping hooks registered by other suites or by `is_admin()`-gated bootstrap code. Symptom: `$submenu` null, enqueue hooks dead, admin pages missing. Fix: re-register in `setUp()` — instantiate the class, `require` the admin file, or re-invoke private init hooks via `ReflectionClass` (see `Test_Admin_Hook_Suffixes` for the `init_hooks()`-re-invoke pattern). 2. **`do_action( 'init' )` in tests re-fires WooCommerce/block registrations** → "…is already registered" incorrect-usage notices fail the test. Fix: call the specific init function directly (e.g. `wp_mcp_ai_init_slash_commands()`, `$shortcode->register_assets()`). 3. **WP_Error vs array envelope drift.** Tools were swept from `array('success'=>false, …)` to `WP_Error`. Tests assert `assertWPError()` + `get_error_code()`. Coordination tools wrap results in nested envelopes (`result['team']`, `result['delegation']`, `result['aggregation']`) — check the tool's `execute()` return before asserting. The REST server converts a controller `WP_Error` into a **400 `WP_REST_Response`** (`error_to_response`): assert status + `$data['code']`, not `is_wp_error( $response )`. 4. **Nonces bind to the current user ID.** `wp_create_nonce()` must run *after* `wp_set_current_user()`. Symptom: save/metabox tests silently bail and meta stays empty. 5. **WooCommerce `get_current_page()` doing-it-wrong** when `admin_enqueue_scripts` fires before `current_screen`. Fix: `set_current_screen( '...' )` then `do_action( 'current_screen', get_current_screen() )` — pass the `WP_Screen` object, not nothing. 6. **Script/style queue leaks + WP 6.9 `all_queued_deps` memoization.** `wp_script_is( $h, 'enqueued' )` falls back to a cached recursive dep set that is NOT invalidated by direct array assignment. Reset via the public API so the memo invalidates: ```php global $wp_scripts; foreach ( (array) $wp_scripts->queue as $handle ) { wp_dequeue_script( $handle ); } foreach ( (array) wp_styles()->queue as $handle ) { wp_dequeue_style( $handle ); } ``` 7. **Shared singleton state.** `WP_MCP_AI_Tool_Registry` (and friends) persist across tests; suites like `test-tool-registry.php` and `test-hooks-registry.php` call `clear_tools()` in their teardown. A later suite's `init()` then no-ops on a partially bootstrapped state. Fix in the consuming suite's `setUp()`: ```php $registry = WP_MCP_AI_Tool_Registry::get_instance(); $registry->clear_tools(); $registry->init(); ``` Likewise, filters added at bootstrap (e.g. the `tools-init.php` side-loader on `wp_mcp_ai_default_tools`) pollute filter-contract tests — isolate with `remove_all_filters( $hook )` inside the test. 8. **Anonymous classes cannot access `protected` members of the enclosing class** (they do not inherit its scope). And production `catch ( Throwable )` swallows the resulting `Error`, so the request returns 200 with **zero side effects** — the failure shows up as "0 saved records", not an exception. Fix: make the captured property `public` (test-only), or store records on the mock itself (`public $records = array()`), like `test-chat-transcript-cct-author-id.php`. 9. **Message content is stored as segments.** The REST validator stores message `content` as `array( array( 'type' => 'text', 'text' => '…' ) )`. Tests asserting `$msg['content']` as a string must extract `$msg['content'][0]['text']`. 10. **Registered meta sanitization applies on `update_post_meta`.** `wp_kses_post` HTML-escapes (`&` → `&amp;`); `sanitize_text_field` strips `<script>` elements **together with their content** (so `'<script>alert(1)</script>'` → `''`, not `'alert(1)'`). Assert against the sanitized value. 11. **`absint()` flips negatives to positives** (`absint(-999)` → `999`). When the contract is "drop/clamp negatives", use `max( 0, (int) $value )` — the codebase documents this exact anti-pattern in `sanitize_associated_assistant_meta()`. 12. **`wp-admin/menu.php` never runs in PHPUnit**, so `add_submenu_page()` falls back to the generic `admin_page_*` hook suffix instead of the production `{post_type}_page_{slug}` suffix. Register the parent CPT in `setUp()` and, if needed, inject the production suffix via reflection on the page object's `page_hook`. 13. **Validated-tool auto-upgrade.** `WP_MCP_AI_Tool_Registry::get_tool( 'base_slug' )` resolves to the `_validated` variant when registered; validation rejects out-of-range args with `validation_failed` before execution. Expect that when asserting tool behavior. 14. **Risky tests (zero assertions).** Environment-dependent `if` branches skip all assertions when vendor packages/Node/JetEngine exist on the host. Risky tests don't fail CI (`phpunit.xml.dist` has no `failOnRisky`) but they mask dead tests. Fix by adding a production filter seam (e.g. `wp_mcp_ai_vendor_package_paths`) and forcing deterministic state in the test. 15. **Obsolete skip gates hide failures.** A suite that mocks its dependencies doesn't need the JetEngine skip gate — the gate only makes it pass locally and fail in CI. Remove it when the mock makes the suite self-contained. 16. **Test drift.** Handles renamed (`mcp-ai-*` → `wp-mcp-ai-*`), POST field names changed, per-metabox nonces added, REST response shapes changed. Check `git --no-pager log -1 -- <test-file>` and align the test with the *current* production contract — but verify the production behavior is intentional (see "Production fix vs test fix" below). 17. **Zombie mocks — PHPUnit clears mock stubs after each test.** A mock stored in a static/global registry (`WP_MCP_AI_Settings_Registry` sections) survives into later suites with its stubs wiped, so stubbed methods return null. Symptom: "Section should have an ID" / "Failed asserting that null is not null" while *iterating a later suite's* sections. Fix: add an `unregister_*()` API to production and have the registering test clean up after itself (#6139, #6144). 18. **`has_action()` / `has_filter()` return the priority (int), not bool.** Assert `assertSame( 10, has_action( ... ) )`. Reverse failure: a lazily-loaded self-instantiating class registers its hooks *after* the process-wide hook-table backup, so wp-phpunit's per-test restore wipes them — re-register in `setUp()` when the load happened out of order (#6143). 19. **Sub-tabbed / view-routed settings sanitizers return `array()` when the routing POST field is absent** (so nothing is saved). Set `$_POST['subtab_<section_id>']` for sub-tabbed pages (#6147) or `$_POST['view']` for orchestration views (#6148) before calling `sanitize()` (#6139). 20. **SSE blocking-emitter contract.** Streaming paths echo SSE frames directly during dispatch and return a response with no `Content-Type`/ CORS headers. Assert emitted frames (`retry:`, `event: …`, `data: {`, `data: [DONE]`) via an output-capture helper — never response headers (#6141, #6149). 21. **`rest_api_init` re-fire triggers third-party temp-table DDL** (Elementor `e_events`), and DDL implicitly commits the per-test transaction, leaking fixtures across tests. Bootstrap the REST controller *before* creating fixtures. Probe DDL with a temp-table create/drop; the DDL fires during `rest_api_init`, not during dispatch (#6141). 22. **`wp_next_scheduled()` defaults to empty `$args`**, so args-carrying jobs (`wp_schedule_single_event( $hook, $job_args )`) are invisible to it. Scan `_get_cron_array()` by hook name instead (#6153). 23. **Pro autoload gap.** CI runs `composer install` in `addons/pro/` so it executes Pro-dependent suites that local runs silently skip (the committed classmap does not map `includes/`). Close with a bootstrap fallback autoloader (slug → `class-wp-mcp-ai-<slug>.php` glob). Note the slug offset: `strlen( 'WP_MCP_AI_' )` is **10**, not 11 — an 11-char cut drops the first slug char (`ro-agent-command-center`) (#6143). 24. **`get_post_meta()` returns strings** — registered `integer` meta sanitizes on *write*, not read. Production itself `absint()`s on read; assert via `absint( get_post_meta( ... ) )` (#6150). 25. **Settings feature-flag splits.** `enable_federation` no longer implies `enable_federation_directory`; enable the specific flag and call the feature loader directly (`maybe_load_federation_features()`) because `init` already fired (#6150). After `update_option()` on `WP_MCP_AI_Admin_Settings::OPTION_NAME`, call `reset_settings_cache()` to drop the static settings cache (#6146). 26. **Addon custom tables never get installed under PHPUnit** — activation hooks don't run. Per-addon suite bootstraps (`tests/*/bootstrap.php`) must call the schema installer directly (`NV_oOS_Graphify_DB::install()`) so CI tables exist (#6145). 27. **Settings defaults live in three layers** — the section field `'default'` in `get_fields()` (drives checkbox render state via `WP_MCP_AI_Settings_Section::render_field()`), the base defaults in `WP_MCP_AI_Admin_Settings_Base::get_default_settings()` (merged into `get_settings()`), and runtime fallbacks like `isset( $settings[ $key ] ) ? $settings[ $key ] : false`. Fixing only one layer leaves fresh installs inconsistent: #6255 flipped the runtime fallback in `get_available_providers()` but the section fields still had `'default' => true`, so the OpenAI/Anthropic/Gemini enable checkboxes rendered checked on fresh installs and any Providers-page save persisted them. When changing a default, update all three layers and the test that codifies the field default (e.g. `test_provider_enable_field_defaults`). 28. **Capability-gated render paths.** The chat shortcode/block render enforces the chat capability (`wp_mcp_ai_chat_capability` filter, default `edit_posts`) — without a capable current user the render silently produces no chat markup. Fix: create an administrator and `wp_set_current_user()` in `setUp()` (reset to 0 in `tear_down()`), like `test-chat-template-selector.php`. 29. **Rate-limiter test contract: non-429 means allowed through.** Any status other than 429 means the limiter passed the request through to the handler; without a configured provider the handler returns 400 `wp_mcp_ai_missing_api_key`, which is unrelated to the limiter. Don't count 200/500 as "success". Also: `WP_MCP_AI_REST::check_rate_limit()` accepts the dispatching request's HTTP method so internal dispatches (`rest_do_request()`, WP-CLI) are classified by their real verb instead of the ambient `$_SERVER['REQUEST_METHOD']`; GET/HEAD stay exempt. 30. **Dual-shape action emitters.** `wp_mcp_ai_before_chat_request` has a canonical `( $assistant_id, $messages, $options, $request )` shape, but legacy/custom emitters (and unit tests) fire the 2-arg `( $messages, $request_data )` shape. Subscribers (nefarious monitor, OOS shadow runner, harness trace capture) must default every parameter and detect the legacy shape when the first argument is an array — never give a subscriber a strict signature. 31. **Shared rate-limit counters halve budgets.** The nefarious-usage monitor deliberately namespaces its transient (`wp_mcp_ai_nefarious_rate_limit_`) away from the chat REST limiter (`wp_mcp_ai_rate_limit_`) — sharing one counter halves the configured chat budget and entangles the two enforcement paths in tests. 32. **Transcript payload includes the assistant reply.** The chat service appends the final assistant response to the conversation before persisting the transcript, so `$request_payload['messages']` carries the user message plus the assistant reply. Assert both (role `user` then `assistant`). 33. **Docs Hub TOC anchors must mirror github-slugger** (the library behind rehype-slug): slug the Markdown-*stripped* text — not the raw line (`### [Core Architecture](core/)` otherwise gets an anchor that doesn't exist) — preserve Unicode letters, do NOT collapse hyphen runs, and dedupe repeats with `-1`, `-2`, … suffixes. 34. **Docs Hub link fixer: resolve-then-contain path guard.** Link targets must be plain relative paths — no URL schemes, absolute paths, backslash separators, or NUL bytes. Relative `../` is legitimate and NOT rejected; safety comes from `realpath()` containment of the resolved destination against the filterable `nvoos_docs_hub_fixer_allowed_roots` list (extend it with your temp dir in tests). Remote-sourced pages are never fixable server-side (flat content-hash cache) — the admin UI shows "Remote source" rows. Source resolution prefers the page `slug` because relative paths like `README.md` collide across addons. 35. **Docs Hub rebuild job contracts.** The sync rebuild honours the aggregate file cap (`nvoos_docs_hub_max_files_total`, default `NV_oOS_Docs_Hub_Rebuild_State::DEFAULT_MAX_FILES_TOTAL` = 5000). `promote_staging()` failure throws — never report "Rebuilt N pages" with nothing persisted. The search REST envelope is identical with or without an index (`results`, `total`, `query`). 36. **Core emoji loader corrupts React-managed SPAs.** `_print_emoji_detection_script()` installs a MutationObserver that replaces emoji text nodes with `<img>` elements anywhere in the document; React keeps direct text-node references, so the next commit throws `NotFoundError: Failed to execute 'removeChild'/'insertBefore' on 'Node'` and unmounts the app ("left panel links stopped working"). The Docs Hub shortcode render removes that action (plus the legacy detection and emoji styles) before output; tests assert the removals. No static done-guard — it would break cross-suite test isolation. 37. **Addon test bootstrap without activation.** PHPUnit never runs activation hooks, so addon suites define the addon's constants (`NVOOS_DOCS_HUB_VERSION/PATH/URL/FILE`) and `require` the classes they need directly in `setUp()`; filters like `nvoos_docs_hub_fixer_allowed_roots` provide the temp-dir seams. 38. **Graphify bridge load flips `wake_up_context` into graph mode.** Once any earlier suite `require`s `NV_oOS_Graphify_Memory_Bridge` (`tests/graphify/bootstrap.php` also installs the graph tables), the `auto` mode of `wake_up_context` switches to graph retrieval for the rest of the process. The graph anchors only *boost* wing/room scores — they never exclude — so cross-wing memories leak into a wing-scoped block. Symptom: MemPalace phase-2 wing test fails (`count` 3 vs 2) only when a graphify suite ran first. Fix (production, #6327): enforce `wing`/`room` in `WP_MCP_AI_Tool_Wake_Up_Context::matches_wake_filters()`. Reproduce with `tests/graphify/test-graphify-connectors.php tests/test-mempalace-phase2-memory-tools.php` in one invocation. 39. **`clear_tools()` on the SHARED registry instance destroys one-shot bootstrap tools.** Swapping in a fresh per-test registry is safe only if the ORIGINAL shared instance is restored untouched. `test-hooks-registry.php` used to `clear_tools()` the saved shared instance before restoring it; a later lazy `init()` re-registers file-scanned default tools but NOT tools registered by one-shot actions (`wp_mcp_ai_bootstrapped` — OKF Pro tools, composio, paper-store). Symptom: `is_tool_registered( 'okf_enrich_site_content' )` false in later suites while the class exists. Fix: restore the shared instance without clearing; the stub tools live on the fresh instance and die with it. 40. **Opt-in logging gates read the static settings cache.** `WP_MCP_AI_Logger::log_event()` early-returns on `is_logging_enabled()`, which reads the merged settings through the static cache. Tests asserting recent-activity/recent-errors entries must write `enable_logging => true` AND call `WP_MCP_AI_Admin_Settings::reset_settings_cache()` afterwards so the gate observes the test's write, independent of options left behind by earlier suites (house pattern from #6311's wp-cli fix, applied in #6327). Same idea for provider-connectivity assertions: isolate the provider key under test — `unset` the other provider keys before asserting `good`, or a leftover invalid key downgrades the status to `recommended`. 41. **Ported-constant drift in ecosystem-port characterization tests.** Port characterization tests must derive constants from the ported source — never from memory or class-name guesses. The Wave F2 comic data-layer test asserted `mcp_ai_comic_character`, but the byte-identical test `mcp_ai_comic_character`, but the byte-identical source declares `mcp_ai_comic_char` (the comic/panel/script slugs are all shorter than the class names suggest). Before running a new port test, grep the source's `const POST_TYPE` (and every other asserted constant) and align the test — a wrong slug turns a byte-identical port into a red-herring failure. Same discipline for module ordinals: count the registry test's own `$expected` list rather than trusting handoff notes (the comic handoff said "25th module"; the actual list has 24). 42. **Standalone-plugin vendor shadows the root PHPUnit.** Suites under `plugins/nvoos-content-graph/tests` (and similar standalone plugins) bootstrap through the plugin's own `vendor/autoload.php`, which pins PHPUnit 9 while `wp-phpunit` 7.x needs PHPUnit 10/11. Symptoms: every test errors with `Call to undefined method X::name()` or PHPUnit itself dies with `Cannot instantiate interface PHPUnit\Runner\TestSuiteLoader` (the plugin-vendor autoloader is registered last, so it wins lookups). Fix for local validation: run the suite with the ROOT vendor's phpunit binary (`php /path/to/root/vendor/bin/phpunit -c plugins/nvoos-content-graph/phpunit.xml.dist ...`) and scrub the plugin-vendor's PHPUnit mappings from its disposable vendor volume (`autoload_psr4.php`/`autoload_classmap.php`/`autoload_static.php` lines mentioning `PHPUnit` or `php-invoker`/`php-timer`, then `rm -rf <plugin>/vendor/phpunit`). Never commit the scrubbed vendor. 43. **`get_routes()` maps each path to a LIST of endpoint objects** (one per registered method), not a single `methods` map. Tests asserting route registration must iterate the list: `isset( $routes[$path] ) && is_array( $routes[$path] )` then check each endpoint's `$endpoint['methods'][$method]`. Asserting `$routes[$path]['methods']` directly silently fails. 44. **`register_rest_route()` outside `rest_api_init` fires `_doing_it_wrong`**, which wp-phpunit converts into a test failure. To register routes in a test: `add_action( 'rest_api_init', fn() => $controller->register_routes() )`, then force a fresh server (`$GLOBALS['wp_rest_server'] = null; rest_get_server();`) so `rest_api_init` re-fires and the hook runs on the correct action. 45. **`rest_url()` in tests uses the `?rest_route=` query form** (plain permalinks), e.g. `http://example.org/index.php?rest_route=/ns/route` — never assert a `/wp-json/` URL shape. Assert `assertStringContainsString( 'ns/route', $url )` to accept both forms. 46. **Pro CLI include-time fatal on base constants.** The Pro addon main file requires its CLI command files at include time under `WP_CLI`, and `class-wp-mcp-ai-pro-cli-base-command.php` reads `WP_MCP_AI_PATH` (a base-plugin constant) unguarded. When Pro is activated before the base plugin (active_plugins order), every `wp` command dies with `Undefined constant "WP_MCP_AI_PATH"`. Production fix (#6585): defer the CLI require loop to `plugins_loaded` (priority 30) when the constant is missing at include time, and bail cleanly in the CLI base command. Site workaround: `wp --skip-plugins eval '...reorder active_plugins...'` or run evals with `--skip-plugins` + manual `require_once` of the needed plugin file. 47. **Edit-tool non-ASCII mangling (CP1252 bytes).** The edit tool may write some non-ASCII characters (em-dashes etc.) as single CP1252 bytes (e.g. `0x97` instead of UTF-8 `E2 80 94`), silently corrupting file encoding — `php -l` and phpcs do NOT catch it. After editing files with non-ASCII content, validate with `mb_check_encoding($c, "UTF-8")` and repair byte-level with a PHP script (lone `0x97` -> `E2 80 94` etc.), or write new text in pure ASCII. When scanning for damage, a byte scanner must skip the continuation bytes of valid sequences (`$i += $len - 1` on the VALID branch too), or every continuation byte reports as a false-positive invalid sequence. 48. **Order-dependent `pro_schedules` cache interference (renderer suite).** `addons/pro/tests/test-pro-schedule-result-renderer.php` writes `SCHEDULES_OPTION`/`RESULTS_OPTION` via `update_option()` directly and expects `render()` to read them back, but `WP_MCP_AI_Pro_Schedule_Manager::load_schedules()` short-circuits through `WP_MCP_AI_Cache_Helper` (`pro_schedules`, 300 s TTL) when caching is enabled — so when a schedule-manager suite runs earlier in the same process, the renderer reads the stale cached copy and fails with `Schedule not found.` (proven pre-existing on a pristine worktree: `tests/test-pro-schedule-manager.php` + `addons/pro/tests/test-pro-schedule-result-renderer.php` in one invocation — not a regression of adjacent changes). Fix in the renderer test's `setUp()`: `WP_MCP_AI_Cache_Helper::delete( 'pro_schedules' )`, or run the suite in isolation. Related fixture note: `WP_MCP_AI_Pro_Remote_Site_Manager::save_connection()` validates `url` for EVERY connection type — an OAuth fixture (upwork, gmail) without a `url` fails with `wp_mcp_ai_pro_missing_url` before the type-specific checks; `save_connection()` encrypts `refresh_token`/`client_secret` and `get_access_token()` decrypts them, so plain fixture credentials round-trip fine. 49. **`WP_MCP_AI_Logger::log_activity()` does not exist — latent fatal.** Legacy tool code calls the nonexistent static `log_activity()`; the real API is `log_event( $type, $message, $context )` (plus `log_error()`). Because the dead call usually sits in an `is_available()`-false branch, it never fires in happy-path tests — L2-style drivers that exercise an unavailable branch (or a settings-gated tool without its flag) fatal with `Call to undefined method`. Production fix (#6738): route to `log_event( 'activity', ... )` — `'activity'` falls through `should_log_event_type()`'s default allow gate and lands in the recent activity buffer. Grep-replace ALL call sites in the touched family (five webchat tools carried it) — leaving siblings dead for later is the same bug waiting to fire. 50. **`$wpdb->prepare()` placeholder-count warnings from interpolated WHERE + array-spread args.** phpcs cannot statically resolve an interpolated `{$where_sql}` fragment, so `$wpdb->prepare( $query, $query_values )` (array form) warns "1 replacement, expected 2" and "no valid placeholders found" even though the runtime spread is valid. Fix by branching the query so every placeholder is literal: build the `message_type`-filtered and unfiltered variants as two explicit `prepare()` calls with scalar args, keeping the `phpcs:disable InterpolatedNotPrepared` block for the esc_sql()-escaped `{$table}`. Behavior-identical; also run phpcbf or align `=` spacing manually (WPCS alignment warnings follow). 51. **Addon tool classes fatal in standalone suites (trait/interface not found).** `Trait "WP_MCP_AI_Tool_Default_Capability" not found` / `Interface "WP_MCP_AI_Tool_Interface" not found` in the `test (standalone)` jobs (`plugins/nvoos-content-graph-{ai,ai-platform,pro}/phpunit.xml.dist`). Cause: an addon's `tests/bootstrap.php` — loaded from the shared root bootstrap — requires tool classes that `implements WP_MCP_AI_Tool_Interface` + `use WP_MCP_AI_Tool_Default_Capability`, contracts that only exist when the base plugin is loaded; the standalone matrixes boot through the root bootstrap WITHOUT the base plugin, so the require fatals before any test runs. Fix (three layers, as in `addons/nvoos-design-system`, PR #6810): - **Self-guard the production files** — after the ABSPATH guard: `if ( ! interface_exists( 'WP_MCP_AI_Tool_Interface' ) || ! trait_exists( 'WP_MCP_AI_Tool_Default_Capability' ) ) { return; }`. This also keeps the addon's autoloader safe standalone (class_exists returns false instead of fataling). - **Guard the addon's tests/bootstrap.php** requires with the same interface/trait check. - **`markTestSkipped()`** in the dependent test when the contracts are absent. Verify in two ISOLATED processes: no-contracts (require tools + entry → no fatal, tool classes undefined, core classes load) and with-contracts (tools load, capability correct). **Gotcha:** PHP binds unconditional top-level `interface`/`trait` declarations at COMPILE time — defining the contracts later in the same file/process makes them exist from line 1, silently invalidating a "contracts absent" harness. Always simulate the standalone state in a separate process (`php -r` or a second file). 52. **`Call to undefined method WP_CLI::add_hook()` when a test constructs real WooCommerce (stub constant leak).** A suite that instantiates the real `WooCommerce` class fatals because another test file's **file-scope** `WP_CLI` stub — loaded eagerly during PHPUnit discovery, before any test runs — defines the `WP_CLI` constant + a minimal class without `add_hook()` (the `addons/pro/tests/test-pro-cli-mcp-server-command.php` stub is the in-repo offender). WooCommerce's `WC_CLI` then passes its `defined('WP_CLI') && WP_CLI` guard and calls the missing method during `new WooCommerce()`. Fix (both layers, as in the v1.1.89 CI repair): - **Never construct a second real WooCommerce in a test.** When `class_exists( 'WooCommerce' )`, drive the code under test against the real singleton (`WC()->mailer()`) instead; snapshot `$GLOBALS['wp_filter']['woocommerce_init']` before any `remove_all_actions()` and restore it — plus the removed `WC_Emails` `email_header`/`email_footer` handlers — in tearDown (removing the real mailer's handlers without restoring them is order-dependent pollution for later WooCommerce suites). - **Defense in depth:** every file-scope `WP_CLI` stub must include a no-op `add_hook( $when, $callback )` — defining the `WP_CLI` constant alone makes real-WC boot paths assume the full WP-CLI API. **Gotcha:** `class_exists( 'WooCommerce' )` is true in CI (the root `tests/bootstrap.php` loads WC when `wp-content/plugins/woocommerce` exists) but false on minimal local copies — the same suite must exercise **both** branches, and only the stub branch may construct `WooCommerce` freely. 53. **Bundled asset reads via the WP Filesystem API silently no-op in Docker (template registry + merge-context assertions).** An addon's template registry (`NV_oOS_Design_System_Email_Template_Registry::get_builtin_html()`, the v1.1.90 letterhead fix) read bundled templates only through the WP Filesystem API, which falls back to `ftpsockets` when the `direct` transport is unavailable (e.g. Docker dev containers) — every read silently failed, and tests asserting rendered output went red in environment-dependent ways. Fix (both layers, as in PR #6816): - **Prefer the `direct` transport with a plain local-read fallback** — bundled files never need FTP/SSH; keep the WP Filesystem path only as the direct-transport branch. - **Assert on the merge context, not the rendered output, when the template never renders the token.** The wrapper/renderer supported `{{to_name}}` but the letterhead template never used it — the test asserted the recipient name in rendered output and failed; assert the assert the `nds_email_context` merge context instead (and cover the conditional block's both branches: name present / absent). 54. **Self-instantiating class + duplicate `new` in the bootstrap loader → double render (v1.1.91, PR #6830).** An admin class that self-instantiates at the bottom of its own file (`WP_MCP_AI_Admin_Orchestration_Dashboard` — the `WP_MCP_AI_Admin_Multi_Agent_Dashboard` pattern) gained a second `new` in `includes/bootstrap/loader.php`. `add_submenu_page()` registers the page hook via `add_action($page_hook, $callback)` — two distinct instances are two distinct callbacks, so firing the hook rendered the dashboard **twice** (duplicate DOM IDs breaking auto-refresh JS). Symptom in tests: `assertSame( 1, ... )` on the number of callbacks for the page hook fails with 2. Fix: remove the redundant `new` (keep the `require_once` outside the `is_admin()` gate so CLI/test contexts still get the registration); assert exactly one callback via `$GLOBALS['wp_filter'][ $page_hook ]` counting and fire the hook to assert the wrapper renders once. Regression-proof: re-adding the duplicate `new` makes the assertion fail — that is the test's job. 55. **`$wpdb` error HTML leaking into JSON responses (v1.1.91, PR #6827).** With WP_DEBUG + `show_errors` on, `$wpdb` prints a WordPress database error HTML block *before* `wp_send_json_success()` — the AJAX client fails with `parsererror: Unexpected token '<'` (the Pro orchestration monitor polled every 5 s). Root cause was gating JetEngine CCT reads on `is_available()` (content type *registered*) instead of the **physical table** existing with the full schema. Fix layers: - **Gate on storage readiness** — `table_exists()` probe (`SELECT 1 … LIMIT 1`) → `is_storage_ready()` (table + every required column derived from `get_meta_fields()`); consumers fall back to transients; schema drift trips the gate and logs missing columns; expose a `reset_storage_cache()` test seam for the per-request cache. - **Defense in depth** — the new `WP_MCP_AI_Db_Output_Guard::run()` suppresses `$wpdb` error output around a callback (logging the failure with the last query) and wraps the central dispatch (`WP_MCP_AI_Tool_Registry::execute_tool()` + both REST tool handlers); read AJAX handlers use `with_suppressed_db_errors()`. Test both halves: the gate suite (table present / absent / schema-drifted × consumer fallback) and the guard suite (error HTML suppressed, failure logged, response still valid JSON). Never assert raw `$wpdb->last_error` text — assert the envelope + the guard's log event. 56. **Provider returns array `message.content` → `preg_match(): Argument #2 ($subject) must be of type string, array given` (v1.1.92, PR #6845).** Gemini (and some OpenAI-compatible gateways, incl. DeepSeek endpoints) return `message.content` as an **array of parts**, not a string — any consumer feeding it straight into `preg_match()`/`json_decode()`/ string functions fatals. The six research tools all did (provider response, parse entry, and array content nested inside the JSON body). Fix layers: - **Normalize at every choke point** — the shared `WP_MCP_AI_Tool_Research_Content_Normalization` trait flattens content-part arrays to strings before any string operation; apply it on the provider response, the parsed entry, and nested array content. - **Normalize in the client** — `WP_MCP_AI_DeepSeek_Client::normalize_response()` flattens array `message.content` blocks so every DeepSeek consumer (chat, transcripts, tools) gets a string; leave `reasoning_content` and `tool_calls` untouched. - **Test both shapes** — assert the tool handles array content **and** plain-string content identically (the normalization suite feeds both shapes through each choke point); a regression is re-feeding the raw array into a string function. 57. **Pro tool coverage manifest CI failure — `test_tool_class_manifest_is_up_to_date` (v1.1.92, PR #6847; the #6828 precedent).** Every new Pro tool class must land in `addons/pro/tests/tools/.coverage-manifest.txt` — a PR that registers tools without regenerating the manifest fails the registry-coverage suite. Fix: run `bin/generate-tool-coverage-manifest.sh` in the same PR and commit the manifest diff with the tools. The manifest lists tool **classes** — a shared non-tool class in the same folder (e.g. the fashion transform) appears there without adding a slug to the registration count, so don't assert slug counts from the manifest alone. 58. **New registry tools missing from every assistant preset → `test_all_tools_accounted_for_in_presets` (v1.1.93, PR #6851).** When a PR registers new tools in `includes/class-wp-mcp-ai-tool-registry.php` (or the Pro map) without adding them to an assistant preset, the all-tools-accounted-for gate fails. Fix layers: - **Add the tools to a preset** — `get_fleet_status`/`get_site_uptime` joined the `site_management` preset (site-information block); pick the preset that matches the tool family. - **Both coverage manifests** — the **base** manifest lives at `tests/tools/.coverage-manifest.txt` (the Pro one at `addons/pro/tests/tools/.coverage-manifest.txt`, pattern 57); new base tool classes must land in both the preset(s) and the base manifest, or `test_tool_class_manifest_is_up_to_date` fails too. - **Image/attachment assertions follow the pipeline** — a test pinning an earlier behavior (e.g. `convert_image_files_to_image_url` asserting `http` URLs after #6846 switched to inline `data:image/…;base64`) must move to the new contract when the production path changes. 59. **Tool-rule validation failures rendered as HTTP 500 — a `WP_Error` without an HTTP status (v1.1.93, PR #6853).** `validate_tool_execution()` returned `tool_validation_failed` with no `status` key, and the WP REST layer converts a status-less `WP_Error` into a raw 500 (masked further by `api_error_verbosity: safe`). Fix: every validation `WP_Error` carries `status => 400` with the error list, so failures surface as readable 400s with the real reason. Regression-test the failure path asserting both the HTTP status **and** the error payload, and verify the credential gate: `validate_dependencies()` must resolve `required_settings` keys through `WP_MCP_AI_Credential_Resolver` (env vars, constants, WP 7.0 Connectors) so keys configured outside the settings array can't false-negative the gate. 60. **Registry auto-upgrade to the `_validated` variant rejects values the base tool advertises (v1.1.93, PR #6859).** `WP_MCP_AI_Tool_Registry::get_tool()` auto-upgrades `edit_gemini_image`/`generate_gemini_image` to their `_validated` wrappers whenever registered (PHP 8+ with Symfony — the default), so the validator's constraints are the real gate. The validated `aspect_ratio` `Choice` allowed only `1:1, 3:4, 4:3, 9:16, 16:9` while the base tools advertise `'auto'` in `get_allowed_aspect_ratios()` — callers passing the advertised default got a status-less `validation_failed` → REST 500. Fix: keep every validated `Choice`/constraint in sync with the base tool's advertised set, and regression-test both directions (the advertised value passes; an unknown value still fails). 61. **New severity-5 PHPCS sniffs gate previously-unchecked dispatch patterns (v1.1.94, PR #6866).** The `WPMCPAI.Decisions.ScopeDeclared` sniff requires every `->decide()` / `->create_decision()` call to sit in a method whose docblock declares `@decision-domain` + `@decision-authority` — an existing dispatch that predates the sniff (e.g. the verification cascade, `typesafe_*` tools) fails CI until the docblock + `WP_MCP_AI_Decision_Scope_Guard::gate()` call are added. Triage: run the sniff standalone over the tree first (`phpcs --sniffs= WPMCPAI.Decisions.ScopeDeclared`), fix the declarations before touching behavior (the guard must be a zero-behavior-change pass), and assert the banned-domain fail-closed path (the formula never runs — assert the deterministic fallback result, not an absence of calls). 62. **`wp_check_php_version()` polyfill declared before the admin-includes load → `Cannot redeclare` fatal (v1.1.94, PR #6870).** `get_site_health` pre-loaded `wp-admin/includes/misc.php` only when `WP_Site_Health` wasn't loaded — in REST/CLI/chat contexts the file was skipped, the polyfill declared, and core's later lazy require re-declared the function → critical-error page. Fix: `file_exists()`/`function_exists()` guard the **pre-load** (`require_once ABSPATH . 'wp-admin/includes/misc.php'` when the function is missing) *before* declaring the polyfill, and regression-test with `WP_Site_Health` already loaded (the exact condition that used to skip the require). 63. **A domain-event object fired through a legacy `wp_mcp_ai_*` hook crashes every subscriber with required parameters → HTTP 500 on the OOS path (v1.1.94, PR #6872).** The WordPress `EventDispatcher` fired the four mapped domain events as raw event objects; legacy subscribers like `WP_MCP_AI_Response_Attachments::handle_chat_response()` declared `( $content, $assistant_id, $request )` and threw `ArgumentCountError`. Fix: the dispatcher translates mapped events into the documented legacy argument tuples before firing (unknown events keep the single-event shape), and every subscriber must stay null-safe for the `$request` slot (it is null on the OOS path). Regression-test the end-to-end dispatch (REST request with the OOS engine enabled + intercepted provider HTTP) rather than the hook alone. 64. **Chat SSE streams reset by proxies during long agentic tool runs — `net::ERR_HTTP2_PROTOCOL_ERROR` / `SSE stream processing error`. Cloudflare (~100 s read timeout) and nginx (`proxy_read_timeout`, often 60 s on Cloudways) reset connections that sit idle while PHP is blocked inside a tool call or waiting on the model. PHP is single-threaded, so NO frames can be emitted during a blocking tool call — server-side headers alone cannot fix this. The shipped fix (v1.9.5) emits SSE keepalive comment frames (`send_sse_keepalive()` → `WP_MCP_AI_SSE_Handler::send_sse_comment()`) at every inter-step boundary in `handle_chat_request_with_streaming()`: after the SSE headers, before the initial LLM call, between `tool_start` and `execute_tool_call_internal()`, and before each in-loop LLM call. Comment frames are invisible to EventSource clients but flush through proxies and reset idle counters. Test via an anonymous recording `WP_MCP_AI_SSE_Handler` subclass injected through the REST constructor's `$sse_handler` param (don't delegate to the parent — it echoes and touches headers) + a mock router returning tool_calls then a final response; assert the ordered call log (`tests/rest/test-sse-stream-keepalive.php`). Limits: a single tool call longer than the proxy timeout still kills the stream — the durable fix is deadline-triggered offload to the job stream (proposal: `docs/project/proposals/sse-stream-hardening-long-run-offload-proposal.md`). 65. **A test context mysteriously lacks tools after v1.1.96 — the lazy registry, not a missing registration.** #6896 defers the ~350 default tool loads on plain front-end requests; `wp_mcp_ai_is_plugin_runtime_context()` returns true for REST/admin/AJAX/cron/CLI/WP-CLI/installing **and when `WP_TESTS_CONFIG_FILE_PATH` is defined**, so PHPUnit runs still get the eager defaults. A test that runs in a non-runtime context (e.g. a raw front-end `do_action('wp')` simulation or a hand-rolled bootstrap that unsets the config constant) will see third-party tools but no defaults until first access — assert through `ensure_default_tools_loaded()` (protected; exercise via a real execution or Reflection) or set the `wp_mcp_ai_is_plugin_runtime_context` filter to true in `setUp()`. Also from #6896: the `WP_MCP_AI_Inline_Async_Tick_Trait` double-declaration fatal — addons shipping a trait stub (docs-hub, graphify, saas-controller) crashed plugin activation in tests with addons loaded; the fix is the `trait_exists()` guard pattern — when a plugin trait may collide with an addon stub, guard every `require` of the trait file with `trait_exists()` (never rely on require_once ordering alone). 66. **The `parity-check` CI job fails after touching a chat/guardrail surface (v1.1.97+).** #6913 added a CI job that runs `bin/check-chat-parity.php`, which now tracks **all four** OOS/legacy guardrail surfaces (35 → 38 features, 100% parity): the gate-exception → envelope translation (428 confirmation / 429 limits via `translate_gate_exception()`), `apply_pre_response_render()` at the final payload (legacy non-streaming, legacy streaming's final SSE event, OOS handler), the `wp_mcp_ai_agentic_iteration_complete` domain-event bridge, and the `wp_mcp_ai_before_tool_execute` waterfall bridge. Any PR that adds, moves, or gates one of those surfaces without updating the script's feature assertions fails the `parity-check` job — extend the script in the same PR (the script is the regression net for guardrail symmetry between the legacy and OOS paths; its 100% parity is asserted by `tests/test-oos-parity-gaps.php`). 67. **Helper/stub files in a `suffix=".php"`-scanned test dir must be excluded in `phpunit.xml.dist` (v1.1.95+, PR #6886 follow-up).** The root config scans `addons/pro/tests` (and the other addon test dirs) with `suffix=".php"`, so every PHP file there is loaded as a test file. A helper whose declared class does not match the filename-derived name emits a PHPUnit runner warning: `Class class-wp-mcp-ai-media-toolkit-stub cannot be found in .../addons/pro/tests/class-wp-mcp-ai-media-toolkit-stub.php`. Fix: add an `<exclude>` entry next to the `class-wp-mcp-ai-workflow-log-context-recorder-tool.php` precedent (the stub stays `require_once`'d by its test, so the tests keep running). Every new `class-*.php` / `trait-*.php` helper added to a scanned dir needs the exclude in the same PR; the toolkit-audit loop generates these regularly. 68. **Standalone addon commerce/checkout suites (docs-hub 0.5.2, content-graph).** Call the REST controller methods **directly** (`NV_oOS_Docs_Hub_Checkout_REST::create_session( new WP_REST_Request() )`) so `WP_Error`s come back raw — `$server->dispatch()` wraps errors in `WP_REST_Response`, so through dispatch assert `get_status()` (403 for permission) and `get_data()['code']` (`rest_invalid_param`) instead of `instanceof WP_Error`. Mock the vendor with a `pre_http_request` filter that routes by URL substring (`/session`, `/verify`, `/health`) and counts calls for zero-HTTP assertions (already-licensed short-circuits and throttle blocks must never hit the wire). Stripe intent fixtures must be **alphanumeric-only** (`pi_1234567890abcdef`) — `pi_test_…` contains `_`, which fails the `^pi_[A-Za-z0-9]{8,}$` REST validator and silently 400s before the callback runs. Already-licensed state: license option + `update_option( 'active_plugins', array( BUNDLE_BASENAME ) )`. Install-conflict path: guarded `define( 'WP_MCP_AI_VERSION', … )` trips the base-plugin guard so verify records the license and returns the manual-download error without any download attempt. Upsell render tests capture `render_page()` via `ob_start()` (require `wp-admin/includes/template.php` for `submit_button()` first). Throttle tests: five direct session calls pass, the sixth returns 429 with zero extra HTTP. Full conventions: `.agents/skills/mcp-ai-wpoos-checkout-integration/SKILL.md`. 69. **A raw pass-through filter-chain assertion breaks when a global prompt-filter subscriber appends (v1.2.2, PR #6980).** The chat-profile system (#6977) registers `WP_MCP_AI_Chat_Profile_Prompt_Hint` at priority 20 on `wp_mcp_ai_resolved_system_prompt` — any test that asserts the resolver passes the prompt through **unmodified** fails with an appended "Read-only mode" cue because the test runs as guest (user 0) and guests resolve to the restrictive read-only profile. Production behaviour is correct by design (Proposal 015) — the test is simply not isolated from unrelated prompt-filter subscribers. Fix: pin the test's user to the `write` chat profile before exercising the chain (create an admin user, set the profile, then assert the pass-through), and guard the profile calls with `class_exists()` so standalone/addon test matrices without the chat-profile system remain unaffected. Rule: a pass-through assertion on any the `wp_mcp_ai_resolved_system_prompt`-family filter is fragile by definition — isolate the subscriber set first. 70. **`class_exists()`-guarded base-plugin calls must use the global namespace (v1.1.0 content-graph bearer auth, PR #6998).** Inside a namespaced class, an unqualified `WP_MCP_AI_Credentials::validate_token()` resolves to `Current\Namespace\WP_MCP_AI_Credentials` — the string form `class_exists( 'WP_MCP_AI_Credentials' )` passes (strings never resolve), then the call fatals with `Class "NvoosContentGraph\Rest\WP_MCP_AI_Credentials" not found` the moment a request presents a credential. Fix: leading backslash on every class reference (`\WP_MCP_AI_Credentials::...`). Tests that stub the base class via a no-namespace helper file (`tests/helpers/base-plugin-credential-stubs.php` pattern: seed tokens, `is_token_format()` + `validate_token()` static methods) catch this only if they exercise the credential path with a real REST request. 71. **Filesystem fixture accumulation in a fixed uploads path (v1.2.4, Model Foundry Phase 1, `test-model-foundry-exporters.php`).** A tool that scans a fixed uploads directory (`wp_upload_dir()['basedir'] . '/nvoos-docs-hub/content'`) picks up files left behind by other tests or earlier runs — the WP test framework's uploads dir persists across suites in the same run and across runs on a shared Docker core volume, so assertions on row counts or the first row flake (`rows == 3` instead of 2; preview text from a stale file). Fix: **wipe the fixed directory inside the test before writing fixtures** (a recursive `wipe_dir()` helper, tracked in `$cleanup_dirs` and re-wiped in `tearDown`), or pass a `uniqid()`-suffixed directory when the production API accepts an explicit path. Same rule for any corpus/exporter test asserting counts over a directory scan — never assume the directory is empty. ## CI workflow hardening — setup-php `tools: wp-cli` flakes `shivammathur/setup-php` `tools: wp-cli` fails intermittently (`✗ wp-cli Could not setup wp-cli`) WITHOUT failing the step, so the next `wp core download` dies with `wp: command not found` (exit 127) — same code, same workflow, alternating green/red runs. Fixed in PR #6998 for `.github/workflows/phpunit-content-graph.yml` + the plugin-check job of `.github/workflows/build-nvoos-content-graph.yml`: ```yaml - name: Ensure WP-CLI is available run: | if ! command -v wp >/dev/null 2>&1; then curl -sSLo wp-cli.phar https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar chmod +x wp-cli.phar sudo mv wp-cli.phar /usr/local/bin/wp fi wp --version ``` Diagnose via `gh run view <id> --log | grep -iE "wp-cli"` — look for the red ✗ tool line in the Set up PHP step, not the step that died. Any new workflow using setup-php `tools: wp-cli` should include this fallback step. ## Production fix vs test fix - **Fix production** when the test exposes a genuine bug: unsafe coercion (pattern 11), a latent fatal on a real code path (Graphify admin classes required only under bootstrap `is_admin()`, #6106), or a missing filter seam that blocks deterministic testing (#6097). Keep production edits minimal and behavior-compatible; document the contract change in the PR. - **Fix the test** when it documents an outdated contract or an environment assumption. Never weaken a security assertion (nonce/capability) to make a test pass; prefer structural fixes over `setExpectedIncorrectUsage()` (ineffective on WP 7.1). ## Debugging workflow Scratch test files under `tests/` with `fwrite( STDERR, … )` are the fastest instrument; `spl_object_hash( $this )` distinguishes instance identity when a mock seems to write to the "wrong" object. **Delete scratch files before committing.** ```php fwrite( STDERR, 'STATE: ' . wp_json_encode( $data ) . PHP_EOL ); ``` ### Order-dependent bisection pitfalls (session 2026-09-05) - **Chunk-only prefix runs produce false results.** Suites depend on side effects of earlier suites (registry state, generated skill knowledge, installed addon tables), so running an arbitrary subset can produce failures that don't exist in CI and mask the ones that do. Bisect with ever-larger prefixes of the full sorted order — never random subsets. - **Reset the DB between experiments.** A killed run leaves residue (open transactions, deadlocks, mid-run DDL) that corrupts the next run: `docker exec oos-wp-db sh -c 'mysql -uwordpress -pwordpress -e "DROP DATABASE wordpress_test; CREATE DATABASE wordpress_test;"'`. The fresh install then takes a few minutes on the first run. - **Scratch phpunit XML configs** must carry the `external-http` group exclusion (otherwise suites hit the real network and crawl) and list each `<file>` exactly once in sorted order — duplicates abort with "already added to test suite". Multiple `<directory>` entries append per-directory sorted files; do NOT merge directories into one sort. - **Local full-prefix runs are 5-10x slower than CI** (loopback HTTP per test). Always `--log-events-text /tmp/events.txt` with a host-mounted `/tmp` so the per-suite order survives even if the run is killed by `timeout`. - **Config/exclude changes: verify via `--list-tests`.** Loader warnings ("Class X cannot be found in Y") fire at suite construction, so `php -d memory_limit=1G vendor/bin/phpunit --list-tests --no-coverage` (no tests executed) reproduces them in minutes instead of a full run. Grep the output for `warning` and the suspect filename to confirm the fix before running any suite. - **CI-log archaeology.** Progress lines (`NNNN / 17245 (NN%)`) plus `[NV oOS]` debug lines locate suites; a suite whose logging gate is off emits no `[NV oOS]` lines while its neighbours do. Consecutive failing tests can straddle a progress-line boundary (`FF` at the end of one line plus `F` at the start of the next = three consecutive failures). ## Enumerating the suite Canonical scan dirs: `tests`, `addons/pro/tests`, `addons/canvas-toolkit/tests`, `addons/media-studio/tests`, `addons/saas-controller/tests`. Exclude: `tests/manual`, `tests/fixtures`, `tests/regression`, `tests/helpers`, all `bootstrap.php` / `wp-tests-config.php` / `wp-cli-smoke.php` files, the abilities mock tool and bootstrap trait, the paper-store helpers trait, the graphify and saas-controller bootstraps, `addons/pro/tests/class-wp-mcp-ai-workflow-log-context-recorder-tool.php`, and `addons/pro/tests/class-wp-mcp-ai-media-toolkit-stub.php`. Batched runs sort the remaining files alphabetically and resume from a suffix index (chunk manifests list files, they are not inputs). ## Cluster state board Completed clusters (merged or open against `alpha-working`): #6084 CRM toolkit, #6085 CRM data store, #6086 Huggingface, #6087 Veo, #6088 transcript mining, #6089 quiz admin pages, #6090 document template pages, #6091 multi-agent AJAX, #6092 chart JS enqueue, #6093 admin hook suffixes, #6094 orchestration modes, #6095 REST tools controller, #6096 multi-agent orchestration, #6097 NPM notice, #6098 multi-agent dashboard, #6099 slash command integration, #6100 profession media vector, #6101 base knowledge seeder, #6102 pro dashboard diagnostic, #6103 chat conversation CCT, #6104 admin test model, #6105 profession team CPT sanitization, #6106 Graphify admin classes, #6107 toolkit + hooks registry, #6108 test-suite skill, #6109 Shopify sync CCT manager, #6110 semantic compressor, #6111 usage tracker pricing drift, #6112 docs catch-up, #6113 Content Graph AI credential encryption, #6114 WP 7.1 Woo role resync, #6115 create post taxonomy application, #6116 site creator tools drift, #6117 toolkit registry singleton drift, #6118 PHPUnit loader excludes, #6119 credential resolver cache invalidation, #6120 capability flags model drift, #6121 settings suite test drift, #6122 slash command tool mode contract, #6124 crawler job contract, #6125 Crawl4AI tool failures, #6126 transcript mining job cluster, #6127 save post content drift, #6128 tool slug integrity drift, #6129 tool coverage manifest, #6130 bulk auto dispatch inline, #6131 TPM fallback model drift, #6132 async registry construct errors, #6133 base version polluters, #6134 cache helper cluster, #6135 transcript recorder repository, #6136 translation loading timing, #6137 profession playbook seeder, #6138 site health tool, #6139 settings dashboard, #6140 slash command sync docs, #6141 REST assistant directory, #6143 Pro test autoload, #6144 new regressions, #6145 Graphify connectors, #6146 rate limit backoff, #6147 provider subtab settings, #6148 orchestration slider settings, #6149 MCP client configuration, #6150 federation test, #6153 Google Chat fields (open). Newer merged clusters (post-#6153, discovered via the PR merge log): #6249 Hermes dashboard fleet extensions, #6251 Auth0 toggle, #6252 Auth0 menu, #6253 docs-hub emoji DOM crash, #6254 auto-categorize, #6255 provider defaults, #6256 Pro SPA v2 shortcode, #6257 chart tiers, #6258 theme sortable compat, #6259 chat attachments, #6260 count-tokens params, #6262 continuation seam, #6263 chat SSE handler, #6264 cache helper, #6265 nefarious rate limiter + chat hook tolerance (open). Session 2026-09-04/05 wave-5 clusters (runs 91006542428 + 91771001271, all merged): #6280 chat transcripts + attachments, #6281 charts + Pro dashboard, #6282 professions/teams, #6283 presets/remote connections, #6284 Cloudflare, #6285 mesh networking, #6286 memory auto-capture, #6287 AI CPT management integration, #6288 PayHere vs Remote Connections, #6289 Mubert music contract, #6290 OpenAI classic Images API, #6291/#6297 composer metadata drift, #6292 Content Graph AI ecosystem, #6293 pro schedule AJAX + curriculum exporter, #6294 cluster-board docs, #6295 exec-disabled host hardening, #6296 conversation-import CCT gate, #6298 model routing/complexity routing, #6299 Gemini, #6300 JetEngine/Graphify stub isolation + gates, #6301 OAuth/credentials encryption, #6302 REST + skill-pack registry, #6303 admin/settings drift, #6304 K10 Elementor, #6305 K11 permission gates (acting-user capability checks), #6306 K12 HTTP/provider clients, #6307 K13 orchestration budget, #6308 K14 logging/events (recent-activity allowlist + trace durations), #6311 K16 misc singles (webhook round-trip, settings-repository blob fallback, chart HTML attachment gating, site-builder heading tags, PSO keyword inflections, scheduled-post publish date reset + 16 suite contract updates), #6312 K15 skill-suite upload isolation (pack normalisation + install action landed via direct commit d452a125bf). Session 2026-09-05 cluster (run 91942465749): #6327 post-K16 singles (merged) — chat transcript session-key mock restore, MemPalace wing-scope graph leak (production fix in `wake_up_context`), OKF Pro tools lost to `clear_tools()` on the shared registry, Site Health provider-key isolation, transcript-mining logging cache reset. Remaining candidates change quickly; re-triage from the latest CI log rather than trusting an old list. ## References - Test-writing patterns & coverage policy: `.context/testing.md` - Remaining-fixes tracker: `docs/developer/testing-docs/TEST-SUITE-REMAINING-FIXES-PLAN.md` - Plugin operational guide: `.agents/skills/mcp-ai-wpoos-plugin/SKILL.md` - Per-toolkit hardening audit loop: `.agents/skills/mcp-ai-wpoos-toolkit-audit/SKILL.md` - **ID-handoff contract suites (permanent CI infrastructure):** `tests/test-tool-id-handoff-contract.php` (L1 honesty — checks `tests/fixtures/tool-contract-manifest.php` against the live registry in both directions; an unvetted annotation or a missing manifest entry fails CI), `tests/test-tool-id-handoff-round-trip.php` (L2 deterministic create→fetch→update→delete drivers, no LLM). Rollout semantics: `docs/project/proposals/P3-data-contract-rollout-plan-2026-09.md`; the manifest is the single source of truth — new tool annotations land in the same PR as their manifest entry (rollout complete via #6729–#6738; docs closure #6739).

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

Control a headless browser to navigate URLs, click elements, fill forms, extract content from JavaScript-rendered pages, take screenshots, and automate end-to-end web workflows.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

Review code for quality, simplicity, and maintainability. Identifies unnecessary abstractions, duplicated logic, oversized functions, missing error handling, and naming issues — then fixes them automatically before presenting the result.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

WooCommerce store operations through NV oOS Pro — product research, catalog cross-referencing, inventory lookups, and Shopify or Flowhub comparisons where integrations are configured.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

nvdigitalsolutions のスキルをすべて見る

このスキルの問題を報告する