Skip to content

feat: introduce CLI commands with integration test infrastructure - #197

Closed
GaryJones wants to merge 22 commits into
3.xfrom
refactor/cli-test-strategy
Closed

GaryJones wants to merge 22 commits into
3.xfrom
refactor/cli-test-strategy

Conversation

@GaryJones

Copy link
Copy Markdown
Contributor

Summary

  • CLI commands (push-post, pull-site, push-all-posts, pull-sitegroup, sites-list, sitegroups-list) wired into the DI container via PluginBootstrapper, each as a thin adapter delegating to PushService or PullService
  • 22 PHPUnit integration tests exercise commands against a real WordPress database, following the testing pyramid from plugin-standards/TESTING.md
  • WpCliStub replaces real WP_CLI during tests, capturing output for assertion without fatal exits on error calls
  • CliTestCase and WpCliOutputCapture provide concise assertion helpers for CLI tests
  • Application contracts (PullServiceInterface, PushServiceInterface) give the CLI layer a stable boundary
  • Behat README documents the CLI testing pyramid strategy

Test plan

  • 22 CLI integration tests pass (composer test:integration -- --group=cli)
  • 246 existing unit tests still pass (composer test:unit)
  • Existing Behat tests still pass (composer behat)
  • CI passes

🤖 Generated with Claude Code

GaryJones and others added 15 commits January 13, 2026 12:32
Add PushService and PullService to orchestrate syndication workflows:
- PushService: handles push syndication with locking, state tracking,
  and support for create/update/delete operations
- PullService: handles pull syndication with import mode, GUID-based
  deduplication, and batch processing

Add immutable DTOs for operation results:
- PushResult: success/failure/skipped states with remote ID tracking
- PullResult: includes partial status for multi-post operations

Create TransportFactoryInterface to enable proper unit testing through
dependency inversion. Services now depend on the interface rather than
the concrete factory class.

All 204 unit tests pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Create Bootstrapper class as the main entry point for the new DDD
architecture. The Bootstrapper:
- Initialises the DI container as a singleton
- Registers PushService and PullService in the container
- Provides hook manager access for WordPress integration
- Includes reset() method for testing support

Add syn_get_container action hook to provide container access
to legacy code during the migration period.

All 215 unit tests pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ation

Wire up the new DDD architecture in the main plugin file:
- Initialise Bootstrapper after autoloader registration
- Add syndication_container() helper function for legacy code access

Add HookRegistrar to mirror WP_Push_Syndication_Server hook registration:
- Registers WordPress core hooks (init, admin_init, save_post, etc.)
- Empty placeholder handlers with @todo comments for implementation
- on_cron_schedules implemented (adds syn_pull_time_interval)

When ready to migrate:
1. Implement placeholder handlers using new services
2. Remove legacy handlers from WP_Push_Syndication_Server
3. Delete legacy code

All 227 unit tests pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract post type (syn_site) and taxonomy (syn_sitegroup) registration
into a dedicated PostTypeRegistrar class. This encapsulates the
registration logic previously in WP_Push_Syndication_Server::init()
and makes it available through the DI container.

Key changes:
- PostTypeRegistrar handles all registration with idempotent checks
- HookRegistrar.on_init() now uses PostTypeRegistrar via the container
- Supports syn_syndicate_cap filter for custom capability requirements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive tests for the DDD architecture components:

Unit tests:
- ContainerTest: Tests DI container registration, resolution, caching,
  and get_services_by_interface functionality

Integration tests:
- BootstrapperIntegrationTest: Verifies plugin bootstrap, container
  availability, services, and global helper function
- PostTypeRegistrationTest: Confirms syn_site post type and syn_sitegroup
  taxonomy are registered with correct properties
- HookRegistrationTest: Validates all WordPress hooks are registered
  and functioning correctly

Test counts: 250 unit tests, 31 new integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Wire up the HookRegistrar handlers to use the new DDD services:

- on_transition_post_status: Saves selected site groups and schedules
  push content via the syn_schedule_push_content action
- on_trash_post: Deletes syndicated content from remote sites using
  PushService when delete_pushed_posts setting is enabled
- on_save_post: Triggers pull job refresh when syn_site posts change
- on_delete_post: Triggers pull job refresh when syn_site posts are deleted
- on_create_term/on_delete_term: Trigger pull job refresh when
  syn_sitegroup terms change

Includes debounced scheduling for pull job refresh to prevent
timeout issues when many sites are configured.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add background processing for push operations to handle large networks:

- on_schedule_push_content: Schedules cron event for immediate background
  execution and spawns cron to minimize delay
- on_push_content: Executes push via PushService in cron context,
  handling both selected sites (push) and removed sites (delete)
- extract_site_ids: Helper to handle both legacy WP_Post objects and
  new integer site IDs for backward compatibility

This ensures pushing to many sites (50+) doesn't block the editor
experience. The push runs as a background task via WordPress cron.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements async pull handlers in HookRegistrar to mirror the existing push architecture. The legacy plugin manages pull operations through scheduled cron jobs that need migration to the new service-based approach.

Adds syn_pull_content hook handler that delegates to PullService for async content pulling from remote sites, with support for configurable update behaviour. Adds syn_refresh_pull_jobs handler to reschedule all pull jobs when site configuration changes, ensuring the cron schedule stays synchronised with site and sitegroup modifications.

Includes helper methods for retrieving configured pull sites from sitegroups and scheduling individual cron jobs per site, replacing the legacy batch scheduling approach.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replaces granular per-post logging with consolidated per-sync-run entries
to reduce noise and improve log clarity. The previous system logged every
single post operation separately, making it difficult to understand overall
sync outcomes and cluttering the interface with redundant information when
nothing had actually changed.

Key improvements:

- Consolidated logging: One log entry per pull sync run (per site) and one
  entry per push operation (covering all target sites), rather than
  individual entries for every post
- Content hash detection: Skips logging "updated" actions when post content
  hasn't actually changed, eliminating false positives from identical syncs
- Separate admin interfaces: Dedicated Pull Logs and Push Logs pages with
  filtering, search, and visual status badges for easier troubleshooting
- Per-site configuration: Configure default post status for pulled content
  (draft/pending/publish) and customisable log retention limits (1-1000
  entries per site)
- Remote post links: Links now point to front-end URLs rather than admin
  URLs, as syndicated users may not have admin access on remote sites
- Smart legacy handling: Legacy logs page only appears when old logs exist
  (moved to menu priority 20), with clear messaging directing users to new
  log pages

Technical changes:

- Adds SyndicationLog singleton with structured logging methods and status
  constants (success/partial/error/skipped)
- Pull logs stored in post meta per-site, push logs stored in options table
- Updates PullService and PushService to integrate consolidated logging
  throughout sync workflows
- Implements PullLogViewer and PushLogViewer admin interfaces with proper
  list tables, pagination, and filtering
- Updates tests to account for new PullServiceInterface

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The legacy failure handling system used three interconnected classes:
- Syndication_Event_Counter (counting failures)
- Syndication_Site_Failure_Monitor (disabling sites after max failures)
- Failed_Syndication_Auto_Retry (immediate retries before scheduled pulls)

This consolidates all functionality into a single SiteHealthMonitor class
that listens to the same legacy hooks fired by transports. The new class
provides clearer admin notices with dismiss functionality and uses
simpler option-based storage for failure counts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove duplicate syn_pull_content, syn_push_content, syn_schedule_push_content
  handlers from server class (now handled by PluginBootstrapper)
- Remove Syndication_Logger::init() call (no longer needed for writing)
- Delete Syndication_Admin_Notices class (was only used by legacy logger)
- Keep Syndication_Logger for reading existing legacy logs
- Legacy log viewer instantiated directly, only shows when legacy logs exist

This eliminates double processing of cron events and removes unused
admin notice infrastructure while preserving backward compatibility
for viewing existing legacy logs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This class was not referenced anywhere in the codebase.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Eliminates 335 lines of code that duplicated functionality already implemented in the new service-oriented architecture. The removed hook registrations and methods (schedule_push_content, push_content, pull_content, refresh_pull_jobs, and related handlers) were superseded by PluginBootstrapper's event handling and the PushService/PullService implementations.

This continues the architectural migration from the monolithic legacy class towards proper separation of concerns through dependency injection and dedicated service classes. The removed code was entirely unreachable, as the new architecture takes precedence in hook execution order.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The cron_add_pull_time_interval method in WP_Push_Syndication_Server duplicated functionality now provided by PluginBootstrapper::on_cron_schedules(). Removed the duplicate method and filter registration to centralise cron schedule management in the bootstrapper, eliminating 20 lines of redundant code and ensuring a single source of truth for custom cron intervals.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
CLI commands (push-post, pull-site, push-all-posts, pull-sitegroup,
sites-list, sitegroups-list) are wired into the DI container via
PluginBootstrapper. Each command is a thin adapter that delegates to
PushService or PullService, keeping business logic in the application
layer where it is already tested.

The test strategy follows the pyramid from TESTING.md: 22 PHPUnit
integration tests exercise commands against a real WordPress database,
while Behat is reserved for CLI contract verification (argument
parsing, output format). A WpCliStub replaces the real WP_CLI class
during integration tests, preventing the fatal exit on error calls
and capturing output for assertion. CliTestCase and WpCliOutputCapture
provide the assertion helpers that keep individual test bodies concise.

Application contracts (PullServiceInterface, PushServiceInterface)
give the CLI layer a stable boundary to programme against,
independent of transport implementation details.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@GaryJones
GaryJones requested a review from a team as a code owner January 29, 2026 11:31
@GaryJones GaryJones self-assigned this Jan 29, 2026
GaryJones and others added 2 commits January 29, 2026 12:44
The WP_Error stub used the `mixed` type hint which is only available
in PHP 8.0+, causing a fatal error on PHP 7.4 CI runs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The 3.x plugin header has declared "Requires PHP: 8.2" since the
architecture rewrite, and the rewritten code relies on PHP 8 features
such as readonly properties and union types. The supporting
configuration was never updated to match, so composer.json, README.md,
.wp-env.json and the CI matrices still advertised PHP 7.4. The unit test
job consequently ran on 7.4 and died with a parse error before a single
test executed.

Aligning every declaration with the 8.2 minimum removes the impossible
job rather than papering over the syntax it cannot parse. The unit
matrix now covers 8.2 through 8.4, and the wp-env development
environment matches the version the plugin actually supports.
The earlier commit on this branch fixed the failing PHP 7.4 job by
bumping the matrix here, but the same problem affects every pull request
into 3.x, so the fix belongs on the base branch. It now lives there, in
a wider tidy that also makes the integration suite run for 3.x at all.

Taking those files verbatim rather than keeping a near-identical variant
means the two branches merge without a conflict over a five-line matrix.
Removing this class as unused was a misreading. The XML client requires
it at the top of the file and instantiates it when rendering the
category selector on its settings screen, so with the file gone the
plugin fataled the moment WordPress loaded it, taking activation down
with it.

Nothing caught this at the time because the integration suite never ran
for pull requests into 3.x. It now does, and failed on the first
attempt to activate the plugin.
SyndicationNotifier is instantiated on every request but no such class
exists, nor has one ever been committed. The plugin therefore fataled
the moment WordPress loaded it, which the newly enabled integration
suite caught on its second run.

Nothing is lost by removing the call. The settings screen has long
offered email and Slack notification fields, but neither this branch nor
develop contains any code that sends them, so the class was aspirational
rather than a replacement for working behaviour. Wiring those fields up
to something real remains outstanding.
Six call sites reach for Container::instance(), including the plugin
bootstrap on plugins_loaded, the legacy server class, and the base class
every integration test extends, but the method itself was never written.
The plugin therefore fataled as soon as WordPress finished loading
plugins, and no integration test could have run.

The container was always designed to be shared: its docblock directs
internal code to the static accessor, and it already caches resolved
services. Adding the accessor completes that intent rather than
introducing a new pattern. Unit tests construct the container directly
and are unaffected.
Nothing autoloads the test directory, so every shared test class has to
be required explicitly. Two were not.

The base class CliTestCase extends was never loaded at all, which killed
PHPUnit in the bootstrap before a single test ran. The mock client was
loaded until the CLI requires were added in its place rather than
alongside it, dropping it by accident.
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.

1 participant