Skip to content

Add WordPress integration test suite and CI - #2

Merged
henry-mosh merged 2 commits into
mainfrom
claude/testing-strategy-gw66fo
Aug 1, 2026
Merged

Add WordPress integration test suite and CI#2
henry-mosh merged 2 commits into
mainfrom
claude/testing-strategy-gw66fo

Conversation

@henry-mosh

Copy link
Copy Markdown
Collaborator

What

Adds automated tests to a plugin that had none, plus CI, and fixes two
outbound-call gaps found while writing them.

217 tests, WordPress integration rather than mock-heavy units — almost
every branch in this plugin touches WordPress state (transients, options,
template_redirect conditionals, REST, cron). The CiteCue delivery API is
faked at the wp_remote_get layer, the single choke point every outbound
call passes through, so no test touches the network. WordPress core and its
test library come from Composer: setup is composer install plus an empty
database.

Coverage

  • Crawler UA matching (longest-token-wins, robots.txt-only tokens excluded)
    and registry refresh (version floor, bundled-token floor, junk payloads)
  • Request eligibility — feeds, previews, robots, sitemaps, logged-in users,
    AJAX/cron, and the WooCommerce store pages
  • Every delivery outcome: 200/304/401/404/5xx/timeout, including stale-serve
    and the "404 evicts a previously cached body" rule
  • Circuit breaker and the per-minute lookup budget
  • llms.txt: freshness window, revalidation, eviction, outage behaviour
  • Ingest HMAC auth: replay rejection, ±300s window boundaries, rate limiting,
    and that unsigned junk cannot exhaust the budget
  • Ingest behaviour: status cap, local-edit protection, trashed pushes, type
    conflicts, sanitisation
  • Settings sanitation and the activation/cron/uninstall lifecycle

Production changes

Testability. Citecue_Proxy and Citecue_Llms_Txt each split into a
decide() returning what should happen and the existing serve() that emits
headers and calls exit. Behaviour-preserving — all branching moved into
decide(), which is what the tests drive.

Two fixes on the llms.txt path, the one endpoint served to every visitor
rather than only to bots:

  • A 404 (llms.txt switched off for the project) evicted the cached copy but
    recorded nothing, so every subsequent hit made a fresh outbound call. Now
    negative-cached for 60s, matching the page path.
  • The per-minute lookup budget only covered the crawler path, leaving llms.txt
    uncapped — and usable to sidestep the ceiling on the other path. The budget
    moved to Citecue_Cache alongside the circuit breaker, and both paths now
    share it. Same filter name and default, so no change for existing installs
    beyond the wider coverage.

CI

Static checks (php -l, WPCS, PHPCompatibility) plus the suite on PHP
7.4/8.2/8.4 against current WordPress, PHP 7.4/8.3 against WordPress 5.9/6.5,
and a separate job against a real WooCommerce.

WordPress 5.9 rather than the plugin's declared 5.8 floor: WordPress's own
test library only gained PHPUnit 9 support in 5.9, and 5.8 hard-fails with
"compatible with PHPUnit up to 7.x". 5.8 support rests on the
PHPCompatibility checks and manual verification.

Not covered

  • class-citecue-admin.php (564 lines) — nonce checks and the test-connection
    handler need the same decide()/emit split, since they are wp_die and
    redirect-and-exit throughout.
  • tests/cases/test-ingest-products.php is written but was never executed:
    it needs a real WooCommerce, and wordpress.org was unreachable from the
    environment it was written in. The test-woocommerce CI job is its first
    real run — expect to iterate there.

🤖 Generated with Claude Code

claude added 2 commits July 29, 2026 22:12
The plugin had no automated tests. Almost every interesting branch in it
touches WordPress state — transients, options, template_redirect query
conditionals, REST requests, cron — so the suite is integration tests
against a real WordPress rather than mock-heavy unit tests. The CiteCue
delivery API is faked at the wp_remote_get layer, which is the one place
every outbound call passes through, so no test touches the network.

WordPress core and its test library come from Composer, so setup is
`composer install` plus an empty database.

Testability change: Citecue_Proxy and Citecue_Llms_Txt each now split
into a decide() that returns what should happen and the existing serve()
that emits headers and exits. All the branching moved into decide(),
which is behaviour-preserving — serve() previously ended the request, so
nothing downstream could be asserted against it.

Covers: crawler UA matching and registry refresh, request eligibility
(feeds, previews, logged-in users, AJAX/cron, WooCommerce store pages),
every delivery API outcome including stale-serve and cache eviction, the
circuit breaker and the outbound lookup budget, llms.txt, ingest HMAC
auth (replay, clock skew, rate limiting), ingest post/page behaviour
(status cap, local-edit protection, trashed pushes), settings sanitation
and the activation/cron/uninstall lifecycle. 211 tests.

Product pushes need a real WooCommerce and run in a dedicated CI job;
store-page exclusion rules run against a minimal stand-in, since whether
WooCommerce exists is a process-wide fact rather than a per-test one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbkhCSV8HnNwmTce9qoNpV
/llms.txt had neither of the two protections the crawler path has, and it
is the one endpoint served to every visitor rather than only to bots:

- A 404 (llms.txt switched off for the project in CiteCue) evicted the
  cached copy but recorded nothing, so every subsequent hit on the URL
  made a fresh outbound call. It is now negative-cached for 60s, matching
  the page path.
- The per-minute lookup budget only covered the crawler path, so llms.txt
  traffic was uncapped and could also be used to sidestep the ceiling on
  the other path. The budget moved to Citecue_Cache — alongside the
  circuit breaker, the other transient-backed throttle — and both paths
  now share it. Same filter name, same default, so no behaviour change
  for existing installs beyond the wider coverage.

On an exhausted budget llms.txt degrades like the crawler path: a cached
body is still served, otherwise the request falls through to WordPress.

README gains a Performance section and an outage section covering what a
human visitor pays (nothing), what a crawler pays, and the recommendation
to run a persistent object cache so cached bodies stay out of wp_options.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbkhCSV8HnNwmTce9qoNpV
@henry-mosh
henry-mosh merged commit 393bab8 into main Aug 1, 2026
7 checks passed
@henry-mosh
henry-mosh deleted the claude/testing-strategy-gw66fo branch August 2, 2026 15:05
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.

2 participants