Commit 94256708f85 for woocommerce
commit 94256708f851ea4280a5d552daf5a39822eec5e6
Author: Vasily Belolapotkov <vasily.belolapotkov@automattic.com>
Date: Tue Sep 15 15:58:29 2026 +0200
Guard bare as_has_scheduled_action() calls behind a version-tolerant helper (#68523)
Guard bare as_has_scheduled_action() calls behind a helper
- Add ActionSchedulerUtil::has_scheduled_action(), which falls back to as_next_scheduled_action() when an older Action Scheduler wins the load race
- Route the seven unguarded call sites and the four inline-ternary sites through it
- Bail out of the batch processing watchdog via can_check_scheduled_actions() instead of dropping processors when Action Scheduler is unavailable
- Raise a doing-it-wrong notice when neither function is loaded
diff --git a/plugins/woocommerce/changelog/fix-52745-guard-as-has-scheduled-action b/plugins/woocommerce/changelog/fix-52745-guard-as-has-scheduled-action
new file mode 100644
index 00000000000..9fa416fb8c3
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-52745-guard-as-has-scheduled-action
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Guard every `as_has_scheduled_action()` call behind a helper that falls back to `as_next_scheduled_action()`, preventing a fatal error when another plugin loads a pre-3.3.0 copy of Action Scheduler.
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-reports.php b/plugins/woocommerce/includes/admin/class-wc-admin-reports.php
index b926a3bdd96..b41721799c5 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-reports.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-reports.php
@@ -10,6 +10,8 @@
* @version 2.0.0
*/
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
+
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -58,7 +60,7 @@ class WC_Admin_Reports {
static $skip_consequent;
// Schedule the deletion, cap the execution to single pending event at any given time.
- $schedule = ! $skip_consequent && ! as_has_scheduled_action( 'woocommerce_delete_legacy_report_transients', null, 'woocommerce' );
+ $schedule = ! $skip_consequent && ! ActionSchedulerUtil::has_scheduled_action( 'woocommerce_delete_legacy_report_transients', null, 'woocommerce' );
if ( $schedule ) {
as_schedule_single_action( time() + MINUTE_IN_SECONDS, 'woocommerce_delete_legacy_report_transients', array( $order_id, false ), 'woocommerce' );
}
diff --git a/plugins/woocommerce/src/Blocks/BlockPatterns.php b/plugins/woocommerce/src/Blocks/BlockPatterns.php
index a68b561a5f5..93aae0eb089 100644
--- a/plugins/woocommerce/src/Blocks/BlockPatterns.php
+++ b/plugins/woocommerce/src/Blocks/BlockPatterns.php
@@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Domain\Package;
use Automattic\WooCommerce\Blocks\Patterns\PatternRegistry;
use Automattic\WooCommerce\Blocks\Patterns\PTKPatternsStore;
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
/**
* Registers patterns under the `./patterns/` directory and from the PTK API and updates their content.
@@ -206,10 +207,6 @@ class BlockPatterns {
return;
}
- // The most efficient way to check for an existing action is to use `as_has_scheduled_action`, but in unusual
- // cases where another plugin has loaded a very old version of Action Scheduler, it may not be available to us.
- $has_scheduled_action = function_exists( 'as_has_scheduled_action' ) ? 'as_has_scheduled_action' : 'as_next_scheduled_action';
-
$patterns = $this->ptk_patterns_store->get_patterns();
if ( empty( $patterns ) || ! is_array( $patterns ) ) {
// Only log once per day by using a transient.
@@ -217,7 +214,7 @@ class BlockPatterns {
// By only logging when patterns are empty and no fetch is scheduled,
// we ensure that warnings are only generated in genuinely problematic situations,
// such as when the pattern fetching mechanism has failed entirely.
- if ( ! get_transient( $transient_key ) && ! call_user_func( $has_scheduled_action, 'fetch_patterns' ) ) {
+ if ( ! get_transient( $transient_key ) && ! ActionSchedulerUtil::has_scheduled_action( 'fetch_patterns' ) ) {
wc_get_logger()->warning(
__( 'Empty patterns received from the PTK Pattern Store', 'woocommerce' ),
);
diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/DraftOrders.php b/plugins/woocommerce/src/Blocks/Domain/Services/DraftOrders.php
index 11431f1b822..49d609e122c 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Services/DraftOrders.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Services/DraftOrders.php
@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\Domain\Services;
use Automattic\WooCommerce\Blocks\Domain\Package;
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
use Exception;
use WC_Order;
@@ -77,8 +78,7 @@ class DraftOrders {
* Maybe create cron events.
*/
protected function maybe_create_cronjobs() {
- $has_scheduled_action = function_exists( 'as_has_scheduled_action' ) ? 'as_has_scheduled_action' : 'as_next_scheduled_action';
- if ( false === call_user_func( $has_scheduled_action, self::DRAFT_CLEANUP_EVENT_HOOK ) ) {
+ if ( ! ActionSchedulerUtil::has_scheduled_action( self::DRAFT_CLEANUP_EVENT_HOOK ) ) {
$midnight_tonight = strtotime( 'midnight tonight' );
if ( false !== $midnight_tonight ) {
as_schedule_recurring_action( $midnight_tonight, DAY_IN_SECONDS, self::DRAFT_CLEANUP_EVENT_HOOK );
diff --git a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php
index c76283cee9c..9cec87a9f66 100644
--- a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php
+++ b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php
@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\Patterns;
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
use WP_Upgrader;
/**
@@ -109,7 +110,7 @@ class PTKPatternsStore {
* @return void
*/
private function schedule_action_if_not_pending( $action ) {
- if ( as_has_scheduled_action( $action, array(), 'woocommerce' ) ) {
+ if ( ActionSchedulerUtil::has_scheduled_action( $action, array(), 'woocommerce' ) ) {
return;
}
diff --git a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
index 634ebaf6603..858c625fe31 100644
--- a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
+++ b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php
@@ -16,6 +16,7 @@ use Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataS
use Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore as TaxesDataStore;
use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;
/**
@@ -529,10 +530,7 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
if ( null === $action_hook ) {
return;
}
- // The most efficient way to check for an existing action is to use `as_has_scheduled_action`, but in unusual
- // cases where another plugin has loaded a very old version of Action Scheduler, it may not be available to us.
- $has_scheduled_action = function_exists( 'as_has_scheduled_action' ) ? 'as_has_scheduled_action' : 'as_next_scheduled_action';
- if ( call_user_func( $has_scheduled_action, $action_hook ) ) {
+ if ( ActionSchedulerUtil::has_scheduled_action( $action_hook ) ) {
return;
}
diff --git a/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php b/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php
index c09e608f52f..0f60d557fcc 100644
--- a/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php
+++ b/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php
@@ -21,6 +21,8 @@
namespace Automattic\WooCommerce\Internal\BatchProcessing;
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
+
/**
* Class BatchProcessingController
*
@@ -380,7 +382,7 @@ class BatchProcessingController {
$time += apply_filters( 'woocommerce_batch_processor_watchdog_delay_seconds', HOUR_IN_SECONDS );
}
- if ( ! as_has_scheduled_action( self::WATCHDOG_ACTION_NAME ) ) {
+ if ( ! ActionSchedulerUtil::has_scheduled_action( self::WATCHDOG_ACTION_NAME ) ) {
as_schedule_single_action(
$time,
self::WATCHDOG_ACTION_NAME,
@@ -559,14 +561,14 @@ class BatchProcessingController {
/**
* Check if a batch processing action is already scheduled for a given processor.
- * Differs from `as_has_scheduled_action` in that this excludes actions in progress.
+ * Pending and in-progress actions both count as scheduled.
*
* @param string $processor_class_name Fully qualified class name of the batch processor.
*
* @return bool True if a batch processing action is already scheduled for the processor.
*/
public function is_scheduled( string $processor_class_name ): bool {
- return as_has_scheduled_action( self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
+ return ActionSchedulerUtil::has_scheduled_action( self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
}
/**
@@ -846,11 +848,14 @@ class BatchProcessingController {
return;
}
- // The most efficient way to check for an existing action is to use `as_has_scheduled_action`, but in unusual
- // cases where another plugin has loaded a very old version of Action Scheduler, it may not be available to us.
- $has_scheduled_action = function_exists( 'as_has_scheduled_action') ? 'as_has_scheduled_action' : 'as_next_scheduled_action';
+ // Everything below reads "not scheduled" as grounds for recording a failure against a processor
+ // and eventually dropping it from the queue. Action Scheduler being unloaded also reads as
+ // "not scheduled", so bail rather than dismantle the queue over a missing dependency.
+ if ( ! ActionSchedulerUtil::can_check_scheduled_actions() ) {
+ return;
+ }
- if ( call_user_func( $has_scheduled_action, self::WATCHDOG_ACTION_NAME ) ) {
+ if ( ActionSchedulerUtil::has_scheduled_action( self::WATCHDOG_ACTION_NAME ) ) {
return;
}
diff --git a/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateAutoApplier.php b/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateAutoApplier.php
index 7a26f712ce2..af8ac86378e 100644
--- a/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateAutoApplier.php
+++ b/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateAutoApplier.php
@@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails;
use Automattic\WooCommerce\EmailEditor\Engine\Logger\Email_Editor_Logger_Interface;
use Automattic\WooCommerce\Internal\EmailEditor\Logger;
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
/**
* Auto-applies the current core block template to `woo_email` posts that have
@@ -227,14 +228,14 @@ class WCEmailTemplateAutoApplier {
* Enqueue the batched auto-apply runner as an Action Scheduler async action.
*
* Hooked to {@see 'woocommerce_email_template_divergence_sweep_complete'}. The
- * `as_has_scheduled_action()` short-circuit guards against double-enqueueing
+ * already-scheduled short-circuit guards against double-enqueueing
* when the detector sweep runs twice in one request — once on
* `woocommerce_updated`, once on `BACKFILL_COMPLETE_ACTION`.
*
* @since 10.8.0
*/
public static function schedule(): void {
- if ( as_has_scheduled_action( self::AUTO_APPLY_AS_HOOK, array(), self::AUTO_APPLY_AS_GROUP ) ) {
+ if ( ActionSchedulerUtil::has_scheduled_action( self::AUTO_APPLY_AS_HOOK, array(), self::AUTO_APPLY_AS_GROUP ) ) {
return;
}
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Services/PendingNotificationStore.php b/plugins/woocommerce/src/Internal/PushNotifications/Services/PendingNotificationStore.php
index f73ba5d372c..06879b909ba 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Services/PendingNotificationStore.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Services/PendingNotificationStore.php
@@ -9,6 +9,7 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\PushNotifications\Dispatchers\InternalNotificationDispatcher;
use Automattic\WooCommerce\Internal\PushNotifications\Notifications\Notification;
use Automattic\WooCommerce\Internal\PushNotifications\Services\NotificationProcessor;
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
/**
* Store that collects notifications during a request and dispatches them all on
@@ -125,7 +126,7 @@ class PendingNotificationStore {
// them from the same place; see Notification::get_safety_net_args().
$args = $notification->get_safety_net_args();
- if ( as_has_scheduled_action( NotificationProcessor::SAFETY_NET_HOOK, $args, NotificationProcessor::ACTION_SCHEDULER_GROUP ) ) {
+ if ( ActionSchedulerUtil::has_scheduled_action( NotificationProcessor::SAFETY_NET_HOOK, $args, NotificationProcessor::ACTION_SCHEDULER_GROUP ) ) {
return;
}
diff --git a/plugins/woocommerce/src/Internal/TransientFiles/TransientFilesEngine.php b/plugins/woocommerce/src/Internal/TransientFiles/TransientFilesEngine.php
index 5e48df2af7d..2ade0298284 100644
--- a/plugins/woocommerce/src/Internal/TransientFiles/TransientFilesEngine.php
+++ b/plugins/woocommerce/src/Internal/TransientFiles/TransientFilesEngine.php
@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Internal\TransientFiles;
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
use \DateTime;
use \Exception;
use \InvalidArgumentException;
@@ -370,7 +371,7 @@ class TransientFilesEngine implements RegisterHooksInterface {
* @return bool True if the expired files cleanup action is currently scheduled, false otherwise.
*/
public function expired_files_cleanup_is_scheduled(): bool {
- return as_has_scheduled_action( self::CLEANUP_ACTION_NAME, array(), self::CLEANUP_ACTION_GROUP );
+ return ActionSchedulerUtil::has_scheduled_action( self::CLEANUP_ACTION_NAME, array(), self::CLEANUP_ACTION_GROUP );
}
/**
diff --git a/plugins/woocommerce/src/Internal/Utilities/ActionSchedulerUtil.php b/plugins/woocommerce/src/Internal/Utilities/ActionSchedulerUtil.php
new file mode 100644
index 00000000000..3077ab1d8de
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Utilities/ActionSchedulerUtil.php
@@ -0,0 +1,63 @@
+<?php
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Utilities;
+
+/**
+ * A class of utilities for dealing with Action Scheduler across the versions of it that may be loaded.
+ */
+class ActionSchedulerUtil {
+
+ /**
+ * Is a matching action currently scheduled (pending or in-progress)?
+ *
+ * Prefers `as_has_scheduled_action`, which only exists since Action Scheduler 3.3.0: another plugin
+ * can load an older copy early enough to win the version race against the one bundled with
+ * WooCommerce, and a bare call is then a fatal. Falls back to the much older `as_next_scheduled_action`,
+ * which on such a copy may report only pending actions, not in-progress ones - an accepted trade.
+ *
+ * Reports false when Action Scheduler is not loaded at all, indistinguishable from "nothing is
+ * scheduled", and raises a doing-it-wrong notice so the condition is visible under debugging.
+ * A caller that treats a negative answer as licence to discard state should check
+ * {@see self::can_check_scheduled_actions()} first.
+ *
+ * @since 11.2.0
+ *
+ * @param string $hook The hook of the action.
+ * @param array|null $args Args that have been passed to the action. Null matches any args.
+ * @param string $group The group the action is assigned to.
+ *
+ * @return bool True if a matching action is scheduled, false otherwise.
+ */
+ public static function has_scheduled_action( string $hook, ?array $args = null, string $group = '' ): bool {
+ foreach ( array( 'as_has_scheduled_action', 'as_next_scheduled_action' ) as $function ) {
+ // PHPStan sees the Action Scheduler copy bundled with WooCommerce and concludes both functions
+ // always exist. The runtime case this guard exists for is precisely the one it cannot see.
+ // @phpstan-ignore-next-line function.alreadyNarrowedType -- see comment above.
+ if ( function_exists( $function ) ) {
+ // `as_next_scheduled_action` returns the timestamp of the next pending action, hence the cast.
+ return (bool) $function( $hook, $args, $group );
+ }
+ }
+
+ wc_doing_it_wrong(
+ __METHOD__,
+ 'Action Scheduler is not loaded, so scheduled actions cannot be checked. Call this after Action Scheduler has initialized.',
+ '11.2.0'
+ );
+
+ return false;
+ }
+
+ /**
+ * Can {@see self::has_scheduled_action()} actually query Action Scheduler, or would it report false
+ * only because neither function it relies on is loaded?
+ *
+ * @since 11.2.0
+ *
+ * @return bool True if a scheduled-action check can be answered, false otherwise.
+ */
+ public static function can_check_scheduled_actions(): bool {
+ return function_exists( 'as_has_scheduled_action' ) || function_exists( 'as_next_scheduled_action' );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Utilities/ActionSchedulerUtilTest.php b/plugins/woocommerce/tests/php/src/Internal/Utilities/ActionSchedulerUtilTest.php
new file mode 100644
index 00000000000..329527ebd64
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Utilities/ActionSchedulerUtilTest.php
@@ -0,0 +1,164 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Utilities;
+
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the Internal\Utilities\ActionSchedulerUtil class.
+ */
+class ActionSchedulerUtilTest extends WC_Unit_Test_Case {
+
+ private const HOOK = 'woocommerce_action_scheduler_util_test';
+ private const GROUP = 'woocommerce_action_scheduler_util_test_group';
+
+ /**
+ * Tear down test fixtures.
+ */
+ public function tearDown(): void {
+ as_unschedule_all_actions( self::HOOK );
+ parent::tearDown();
+ }
+
+ /**
+ * @testdox `has_scheduled_action` should return false when no matching action exists.
+ */
+ public function test_returns_false_when_nothing_is_scheduled(): void {
+ $this->assertFalse(
+ ActionSchedulerUtil::has_scheduled_action( self::HOOK ),
+ 'An unscheduled hook should not be reported as scheduled'
+ );
+ }
+
+ /**
+ * @testdox `has_scheduled_action` should return true for a pending action.
+ */
+ public function test_returns_true_for_a_pending_action(): void {
+ as_schedule_single_action( time() + HOUR_IN_SECONDS, self::HOOK, array(), self::GROUP );
+
+ $this->assertTrue(
+ ActionSchedulerUtil::has_scheduled_action( self::HOOK, array(), self::GROUP ),
+ 'A pending action should be reported as scheduled'
+ );
+ }
+
+ /**
+ * @testdox `has_scheduled_action` should return true for an in-progress action.
+ */
+ public function test_returns_true_for_an_in_progress_action(): void {
+ $action_id = as_schedule_single_action( time() + HOUR_IN_SECONDS, self::HOOK, array(), self::GROUP );
+ \ActionScheduler::store()->log_execution( $action_id );
+
+ $this->assertTrue(
+ ActionSchedulerUtil::has_scheduled_action( self::HOOK, array(), self::GROUP ),
+ 'An in-progress action should be reported as scheduled'
+ );
+ }
+
+ /**
+ * @testdox `has_scheduled_action` should return false when only the group differs.
+ */
+ public function test_returns_false_when_the_group_does_not_match(): void {
+ as_schedule_single_action( time() + HOUR_IN_SECONDS, self::HOOK, array(), self::GROUP );
+
+ $this->assertFalse(
+ ActionSchedulerUtil::has_scheduled_action( self::HOOK, array(), 'some_other_group' ),
+ 'An action in a different group should not match'
+ );
+ }
+
+ /**
+ * @testdox `has_scheduled_action` should return false when only the args differ.
+ */
+ public function test_returns_false_when_the_args_do_not_match(): void {
+ as_schedule_single_action( time() + HOUR_IN_SECONDS, self::HOOK, array( 'a' ), self::GROUP );
+
+ $this->assertFalse(
+ ActionSchedulerUtil::has_scheduled_action( self::HOOK, array( 'b' ), self::GROUP ),
+ 'An action scheduled with different args should not match'
+ );
+ }
+
+ /**
+ * @testdox `has_scheduled_action` should treat null args as a wildcard.
+ */
+ public function test_null_args_match_any_args(): void {
+ as_schedule_single_action( time() + HOUR_IN_SECONDS, self::HOOK, array( 'a' ), self::GROUP );
+
+ $this->assertTrue(
+ ActionSchedulerUtil::has_scheduled_action( self::HOOK, null, self::GROUP ),
+ 'Null args should match an action scheduled with any args'
+ );
+ }
+
+ /**
+ * @testdox `has_scheduled_action` should return false once the action has been unscheduled.
+ */
+ public function test_returns_false_after_the_action_is_unscheduled(): void {
+ as_schedule_single_action( time() + HOUR_IN_SECONDS, self::HOOK, array(), self::GROUP );
+ as_unschedule_all_actions( self::HOOK, array(), self::GROUP );
+
+ $this->assertFalse(
+ ActionSchedulerUtil::has_scheduled_action( self::HOOK, array(), self::GROUP ),
+ 'An unscheduled action should no longer be reported as scheduled'
+ );
+ }
+
+ /**
+ * @testdox `can_check_scheduled_actions` should be true when Action Scheduler is loaded.
+ */
+ public function test_can_check_scheduled_actions_when_action_scheduler_is_loaded(): void {
+ $this->assertTrue(
+ ActionSchedulerUtil::can_check_scheduled_actions(),
+ 'Action Scheduler is loaded in the test suite, so scheduled-action checks should be possible'
+ );
+ }
+
+ /**
+ * The helper falls back to `as_next_scheduled_action` when `as_has_scheduled_action` is missing.
+ * Neither that branch nor the false branch of `can_check_scheduled_actions` can be exercised here:
+ * Action Scheduler is always fully loaded in the test suite, and the function-mocking seam
+ * (CodeHacker) only rewrites files under `includes/`, not `src/`. So pin the property the fallback
+ * relies on instead: with the bundled Action Scheduler the two functions answer identically, so
+ * routing a call site through the fallback is a no-op for every store not running an ancient copy.
+ *
+ * @testdox The `as_next_scheduled_action` fallback agrees with `as_has_scheduled_action`.
+ * @dataProvider provider_fallback_equivalence
+ *
+ * @param array|null $scheduled_args Args to schedule the action with, or null to schedule nothing.
+ * @param array|null $queried_args Args to query with.
+ * @param bool $expected Expected result.
+ */
+ public function test_fallback_is_equivalent_to_the_preferred_function( ?array $scheduled_args, ?array $queried_args, bool $expected ): void {
+ if ( null !== $scheduled_args ) {
+ as_schedule_single_action( time() + HOUR_IN_SECONDS, self::HOOK, $scheduled_args, self::GROUP );
+ }
+
+ $this->assertSame(
+ $expected,
+ (bool) as_has_scheduled_action( self::HOOK, $queried_args, self::GROUP ),
+ 'as_has_scheduled_action should report the expected result'
+ );
+ $this->assertSame(
+ $expected,
+ (bool) as_next_scheduled_action( self::HOOK, $queried_args, self::GROUP ),
+ 'The as_next_scheduled_action fallback should report the same result'
+ );
+ }
+
+ /**
+ * Data provider for test_fallback_is_equivalent_to_the_preferred_function.
+ *
+ * @return array
+ */
+ public function provider_fallback_equivalence(): array {
+ return array(
+ 'nothing scheduled' => array( null, array(), false ),
+ 'exact args match' => array( array( 'a' ), array( 'a' ), true ),
+ 'args mismatch' => array( array( 'a' ), array( 'b' ), false ),
+ 'null args as wildcard' => array( array( 'a' ), null, true ),
+ );
+ }
+}