Skip to content

Tests: register core ability categories once in the bootstrap#854

Merged
dkotter merged 4 commits into
developfrom
fix/ability-category-test-isolation
Jul 10, 2026
Merged

Tests: register core ability categories once in the bootstrap#854
dkotter merged 4 commits into
developfrom
fix/ability-category-test-isolation

Conversation

@gziolo

@gziolo gziolo commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

Register the core ability categories once in the PHPUnit bootstrap, and delete the three copies of ensure_*_category() that test classes carried to work around their absence.

Why

The WordPress test suite unhooks wp_register_core_ability_categories(), so the site and user categories do not exist during tests. Plugin abilities declare those categories, and wp_register_ability() refuses a category that is not registered.

The catch is that booting the abilities registry registers every plugin ability at once. So one missing category breaks whichever test happens to touch abilities first, even a test that has nothing to do with settings or users. That is why UsersTest had to register site as well as user. It was paying for core/read-settings.

This gets worse with every ability we add. Each new ability test class copied the same setup, and each new core category meant editing all the copies. We are at three copies on this branch, and #739 adds a fourth while it waits for review.

It also makes the suite order dependent. Run everything and an earlier class has already registered the categories, so a later class passes without its own copy. Run one class alone and it fails. On develop today:

$ phpunit --filter 'Abilities\Settings\SettingsTest'
ERRORS! Tests: 9, Errors: 8, Failures: 1.
   Ability category 'site' is not registered ... core/read-settings

$ phpunit --filter 'Abilities_Explorer\Ability_HandlerTest'
ERRORS!
   Ability category 'site' is not registered ... core/read-settings

How

tests/bootstrap.php registers the categories on wp_abilities_api_categories_init at priority 5. That runs after the suite has unhooked core, and before the plugin registers the categories it owns. Categories the plugin owns, such as ai-experiments, keep arriving through the plugin's own callbacks, so they need nothing here.

The filter stands down if core's callback is still hooked, so it turns into a no-op the day the suite stops unhooking it.

Deleting the three helpers is what makes CI protect this. With the helpers gone and the bootstrap filter reverted, the full suite reports 50 errors. Before this change it passed either way.

Testing

Each class now passes on its own, and the full suite still passes with Tests: 986, Assertions: 2706.

Class Before (alone) After (alone)
Abilities\Settings\SettingsTest ERRORS OK (9 tests)
Abilities\Users\UsersTest OK (43 tests) OK (43 tests)
Abilities_Explorer\Ability_HandlerTest ERRORS OK (17 tests)

Two more checks. Simulating a future WordPress that no longer unhooks core, the filter stands down and registers nothing twice. Stacking this branch on #739 and deleting all three of ContentTest's stubs, including the one for the plugin-owned content category, gives OK (89 tests, 312 assertions) alone and 1082 tests green for the full suite.

PHPCS and PHPStan are clean.

Follow-up

Once this lands, ContentTest::ensure_ability_category() and its three setUp() calls can be removed from #739.

A test that wants a category it owns still has to fake the hook context, because the categories hook fires only once, when the registry boots. No test needs that today. If one does, it deserves a shared helper rather than a fourth copy.

Open WordPress Playground Preview

The WordPress test suite unhooks `wp_register_core_ability_categories()` at
priority 1, so the `site` and `user` categories never exist during tests. The
plugin's own abilities declare those categories, and `wp_register_ability()`
refuses a category that is not registered. Every plugin ability therefore
emitted an incorrect-usage notice as soon as the abilities registry booted.

Three test classes worked around this with their own copy of an
`ensure_*_category()` helper. The copies hid the problem instead of solving
it. Whether a class passed depended on test order: run the whole suite and an
earlier class had already registered the categories, so a later class passed
even without its own helper. Run one class alone and it failed. On the default
branch today, `SettingsTest` and `Ability_HandlerTest` both fail in isolation.

Register the categories once in the test bootstrap, after the suite has
unhooked core and before the plugin registers the categories it owns, then
delete the three helpers. Removing them is what makes CI protect this: with
the helpers gone and the bootstrap filter reverted, the full suite reports 50
errors, so a regression can no longer hide behind test ordering.

Each of the three classes now passes on its own, and the full suite still
passes with 986 tests.
@gziolo
gziolo requested a review from a team July 10, 2026 11:56
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: gziolo <gziolo@git.wordpress.org>
Co-authored-by: justlevine <justlevine@git.wordpress.org>
Co-authored-by: dkotter <dkotter@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.67%. Comparing base (cf1bf71) to head (62be73e).

Additional details and impacted files
@@            Coverage Diff             @@
##             develop     #854   +/-   ##
==========================================
  Coverage      76.67%   76.67%           
  Complexity      2198     2198           
==========================================
  Files            100      100           
  Lines           9078     9078           
==========================================
  Hits            6961     6961           
  Misses          2117     2117           
Flag Coverage Δ
unit 76.67% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gziolo gziolo self-assigned this Jul 10, 2026
gziolo added 3 commits July 10, 2026 14:13
The bootstrap filter runs at priority 5 and core's own callback sits at
priority 10. Today the test suite unhooks core's callback, so only ours runs.
If a future WordPress stops unhooking it, both would run, and the second one
would report "Ability category site is already registered" and fail the suite.

Skip the registration when core's callback is still hooked, so this filter does
nothing on the day the suite no longer needs it.
`has_action()` returns the priority of the hooked callback, or `false` when it
is not hooked. A callback hooked at priority 0 therefore reads as falsy, and the
guard would register the core categories a second time.

Core hooks its callback at the default priority 10 today, so this is latent. The
comparison should still follow the documented contract.
Say why one missing category breaks unrelated tests, and why the setup grew a
new copy with every ability. Drop the parts the code already shows.

@justlevine justlevine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me both in concept and approach.

(Internally, we usually do this in the suite's TestCase, but that's definitely premature for the AI plugin )

@dkotter dkotter added this to the 1.2.0 milestone Jul 10, 2026
@dkotter
dkotter merged commit 7660347 into develop Jul 10, 2026
25 checks passed
@dkotter
dkotter deleted the fix/ability-category-test-isolation branch July 10, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants