Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/fix-cli-cache-invalidation
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Register the cache-invalidation hooks under WP-CLI so publishing a scheduled post from the command line clears the stale cache.
52 changes: 52 additions & 0 deletions tests/php/bootstrap-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,58 @@ function remove_all_filters( $hook_name ) {
unset( $GLOBALS['wpsc_test_filters'][ $hook_name ] );
return true;
}

/*
* Actions are filters in WordPress, so the action doubles delegate to the same
* registry. These let a test assert that a function under test wired a hook —
* e.g. the #1007 guard that wp_cache_postload() registers the cache-invalidation
* hooks even when there is no request URI (WP-CLI).
*/

/**
* Register an action callback (test double for WordPress add_action()).
*
* @param string $hook_name Action hook name.
* @param callable $callback Callback to run.
* @param int $priority Lower runs earlier. Default 10.
* @param int $accepted_args Number of args passed to the callback. Default 1.
*/
function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
return add_filter( $hook_name, $callback, $priority, $accepted_args );
}

/**
* Whether a callback is registered for an action (test double for has_action()).
*
* @param string $hook_name Action hook name.
* @param callable|false $callback Specific callback to look for, or false for any.
* @return int|bool Registered priority for a specific callback, true for "any", false if none.
*/
function has_action( $hook_name, $callback = false ) {
if ( empty( $GLOBALS['wpsc_test_filters'][ $hook_name ] ) ) {
return false;
}
if ( false === $callback ) {
return true;
}
foreach ( $GLOBALS['wpsc_test_filters'][ $hook_name ] as $priority => $callbacks ) {
foreach ( $callbacks as $registered ) {
if ( $registered['callback'] === $callback ) {
return $priority;
}
}
}
return false;
}

/**
* Remove every callback registered for an action (test cleanup helper).
*
* @param string $hook_name Action hook name.
*/
function remove_all_actions( $hook_name ) {
return remove_all_filters( $hook_name );
}
}

/*
Expand Down
73 changes: 73 additions & 0 deletions tests/php/smoke/WpCachePostloadTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
/**
* Smoke tests for wp_cache_postload() hook registration.
*
* Regression guard for #1007: when $wp_cache_request_uri is empty — which is the
* case under WP-CLI and some cron runners — wp_cache_postload() must still register
* the cache-invalidation hooks, or publishing a scheduled post from the CLI leaves
* the stale page in the cache. The serving path is correctly skipped in that case;
* only the content-change hooks need to run.
*
* Runs in the WordPress-free smoke tier; add_action()/has_action() are provided as
* test doubles by tests/php/bootstrap-smoke.php.
*
* @package automattic/wp-super-cache
*/

use PHPUnit\Framework\TestCase;

/**
* @covers ::wp_cache_postload
*/
class WpCachePostloadTest extends TestCase {

/**
* With no request URI and caching enabled, postload still wires the
* content-change hooks. Runs in a separate process so the static guard inside
* wpsc_register_post_hooks() and the hook registry start clean.
*
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function test_registers_invalidation_hooks_when_request_uri_empty(): void {
$GLOBALS['wp_cache_request_uri'] = '';
$GLOBALS['cache_enabled'] = true;
$GLOBALS['wp_super_cache_late_init'] = false;

// Precondition: nothing is hooked yet, so a pass below is the fix's doing.
$this->assertFalse(
has_action( 'transition_post_status', 'wpsc_post_transition' ),
'transition_post_status must not be hooked before postload runs'
);

wp_cache_postload();

$this->assertNotFalse(
has_action( 'transition_post_status', 'wpsc_post_transition' ),
'postload must register the post-transition invalidation hook without a request URI (WP-CLI)'
);
$this->assertNotFalse(
has_action( 'publish_post', 'wp_cache_post_edit' ),
'postload must register the publish_post invalidation hook without a request URI (WP-CLI)'
);
}

/**
* Boundary: with caching disabled there is nothing to invalidate, so no hooks
* register even without a request URI.
*
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function test_registers_no_hooks_when_cache_disabled(): void {
$GLOBALS['wp_cache_request_uri'] = '';
$GLOBALS['cache_enabled'] = false;

wp_cache_postload();

$this->assertFalse(
has_action( 'transition_post_status', 'wpsc_post_transition' ),
'no invalidation hooks should register while caching is disabled'
);
}
}
14 changes: 13 additions & 1 deletion wp-cache-phase2.php
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,19 @@ function wp_cache_postload() {
global $wp_cache_request_uri;

if ( empty( $wp_cache_request_uri ) ) {
wp_cache_debug( 'wp_cache_postload: no request uri configured. Not running.' );
wp_cache_debug( 'wp_cache_postload: no request uri configured. Not serving cached files.' );

// A request with no URI (WP-CLI, and some cron runners) never serves a
// cached page, so the serving path below is correctly skipped. The
// cache-invalidation hooks are a separate concern: without them, publishing
// a scheduled post from WP-CLI leaves the stale page in the cache. Register
// them here so content changes still clear the cache. wpsc_register_post_hooks()
// wires only the content-change hooks (not the serving ones) and is
// idempotent, so this is safe alongside the normal request path.
Comment on lines +425 to +427
if ( $cache_enabled ) {
wpsc_register_post_hooks();
}

return false;
}

Expand Down
Loading