Commit 13a1d49593f for woocommerce

commit 13a1d49593fc3f7d3c2795ab2b930e08de14e9d4
Author: Barry Hughes <3594411+barryhughes@users.noreply.github.com>
Date:   Thu Oct 29 13:03:55 2026 -0700

    Probe the loaded Action Scheduler instead of the registry version (#70157)

    * Probe the loaded Action Scheduler instead of the registry version

    The download handler requires the UNIQUE_ARGS feature from the loaded copy before tracking partial downloads. PluginsHelper reads action status from the active store and SchedulerTraits checks the store for cancel_actions_by_group(). The inbox engine checks for its scheduled action through the util, and PTKPatternsStore defers its cleanup with when_ready().

    Split from #69847 (WOOPLUG-2435).

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    * Track partial downloads on every loaded copy and keep the inbox search fallback below 3.3.0

    * Drop the inbox engine guard now that the util sees running actions on every copy

    ---------

    Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/update-wooplug-2435-migrate-as-calls-probes-readiness b/plugins/woocommerce/changelog/update-wooplug-2435-migrate-as-calls-probes-readiness
new file mode 100644
index 00000000000..015a2bb07cd
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-wooplug-2435-migrate-as-calls-probes-readiness
@@ -0,0 +1,3 @@
+Significance: patch
+Type: update
+Comment: Covered by the base PR changelog entry (#69846).
diff --git a/plugins/woocommerce/includes/class-wc-download-handler.php b/plugins/woocommerce/includes/class-wc-download-handler.php
index b917442972f..a6920133931 100644
--- a/plugins/woocommerce/includes/class-wc-download-handler.php
+++ b/plugins/woocommerce/includes/class-wc-download-handler.php
@@ -10,6 +10,7 @@

 defined( 'ABSPATH' ) || exit;

+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;
 use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;

 /**
@@ -1043,7 +1044,6 @@ class WC_Download_Handler {
 	 * @param bool                     $defer           If tracking the download should be deferred.
 	 *
 	 * @return void
-	 * @throws Exception If the active version of Action Scheduler is less than 3.6.0.
 	 *
 	 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
 	 */
@@ -1074,13 +1074,7 @@ class WC_Download_Handler {
 			 */
 			$window = absint( apply_filters( 'woocommerce_partial_download_tracking_window', 30 * MINUTE_IN_SECONDS, $download->get_id() ) );

-			// If we do not have Action Scheduler 3.6.0+ (this would be an unexpected scenario) then we cannot
-			// track partial downloads, because we require support for unique actions.
-			if ( version_compare( ActionScheduler_Versions::instance()->latest_version(), '3.6.0', '<' ) ) {
-				throw new Exception( 'Support for unique scheduled actions is not currently available.' );
-			}
-
-			as_schedule_single_action(
+			ActionSchedulerUtil::schedule_single(
 				time() + $window,
 				self::TRACK_DOWNLOAD_CALLBACK,
 				array(
@@ -1099,7 +1093,7 @@ class WC_Download_Handler {
 					'id'       => $download->get_id(),
 					'user_id'  => $user_id,
 					'ip'       => $user_ip_address,
-					'deferred' => $defer ? 'yes' : 'no',
+					'deferred' => wc_bool_to_string( $defer ),
 				)
 			);
 		}
diff --git a/plugins/woocommerce/src/Admin/PluginsHelper.php b/plugins/woocommerce/src/Admin/PluginsHelper.php
index 43023b307f4..45b6d2b6218 100644
--- a/plugins/woocommerce/src/Admin/PluginsHelper.php
+++ b/plugins/woocommerce/src/Admin/PluginsHelper.php
@@ -8,7 +8,6 @@
 namespace Automattic\WooCommerce\Admin;

 use ActionScheduler;
-use ActionScheduler_DBStore;
 use ActionScheduler_QueueRunner;
 use Automatic_Upgrader_Skin;
 use Automattic\WooCommerce\Admin\PluginsInstallLoggers\AsyncPluginsInstallLogger;
@@ -756,12 +755,11 @@ class PluginsHelper {
 		$data = array();

 		foreach ( $actions as $action_id => $action ) {
-			$store  = new ActionScheduler_DBStore();
 			$args   = $action->get_args();
 			$data[] = array(
 				'job_id'  => $args[1],
 				'plugins' => $args[0],
-				'status'  => $store->get_status( $action_id ),
+				'status'  => ActionScheduler::store()->get_status( $action_id ),
 			);
 		}

diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php
index 2c08206be37..20914bcf11c 100644
--- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php
+++ b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php
@@ -14,6 +14,7 @@ use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
 use Automattic\WooCommerce\Admin\Notes\Note;
 use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
 use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\StoredStateSetupForProducts;
+use Automattic\WooCommerce\Internal\Utilities\ActionSchedulerUtil;

 /**
  * Remote Inbox Notifications engine.
@@ -61,8 +62,8 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
 			function () {
 				$queue = WC()->queue();
 				// Custom queues, including subclasses, must retain their search overrides.
-				if ( \WC_Action_Queue::class === get_class( $queue ) && function_exists( 'as_has_scheduled_action' ) ) {
-					if ( as_has_scheduled_action( 'woocommerce_run_on_woocommerce_admin_updated', array(), 'woocommerce-remote-inbox-engine' ) ) {
+				if ( \WC_Action_Queue::class === get_class( $queue ) ) {
+					if ( ActionSchedulerUtil::has_scheduled_action( 'woocommerce_run_on_woocommerce_admin_updated', array(), 'woocommerce-remote-inbox-engine' ) ) {
 						return;
 					}
 				} else {
diff --git a/plugins/woocommerce/src/Admin/Schedulers/SchedulerTraits.php b/plugins/woocommerce/src/Admin/Schedulers/SchedulerTraits.php
index e6ac4583f57..85f6879c28e 100644
--- a/plugins/woocommerce/src/Admin/Schedulers/SchedulerTraits.php
+++ b/plugins/woocommerce/src/Admin/Schedulers/SchedulerTraits.php
@@ -354,8 +354,11 @@ trait SchedulerTraits {
 	 * Clears all queued actions.
 	 */
 	public static function clear_queued_actions() {
-		if ( version_compare( \ActionScheduler_Versions::instance()->latest_version(), '3.0', '>=' ) ) {
-			\ActionScheduler::store()->cancel_actions_by_group( static::$group );
+		$store = \ActionScheduler::store();
+		// PHPStan sees the bundled copy, where the method exists; copies before 3.0.0 lack it.
+		// @phpstan-ignore-next-line function.alreadyNarrowedType -- see comment above.
+		if ( $store instanceof \ActionScheduler_Store && method_exists( $store, 'cancel_actions_by_group' ) ) {
+			$store->cancel_actions_by_group( static::$group ?? '' );
 		} else {
 			$actions = static::get_actions();
 			foreach ( $actions as $action ) {
diff --git a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php
index 1df05b99ac2..2bb6d0d3eb5 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;

 /**
@@ -75,16 +76,11 @@ class PTKPatternsStore {
 			return;
 		}

-		if ( did_action( 'action_scheduler_init' ) ) {
-			as_unschedule_all_actions( self::FETCH_PATTERNS_ACTION, array(), 'woocommerce' );
-		} else {
-			add_action(
-				'action_scheduler_init',
-				function () {
-					as_unschedule_all_actions( self::FETCH_PATTERNS_ACTION, array(), 'woocommerce' );
-				}
-			);
-		}
+		ActionSchedulerUtil::when_ready(
+			function () {
+				as_unschedule_all_actions( self::FETCH_PATTERNS_ACTION, array(), 'woocommerce' );
+			}
+		);
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKPatternsStoreTest.php b/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKPatternsStoreTest.php
index a21c9402051..7464d2d9912 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKPatternsStoreTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKPatternsStoreTest.php
@@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Tests\Blocks\Patterns;

 use Automattic\WooCommerce\Blocks\Patterns\PTKClient;
 use Automattic\WooCommerce\Blocks\Patterns\PTKPatternsStore;
+use Automattic\WooCommerce\Proxies\LegacyProxy;
 use WP_Error;

 /**
@@ -53,6 +54,9 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
 		delete_option( PTKPatternsStore::OPTION_NAME );
 		delete_option( 'woocommerce_allow_tracking' );

+		// A test that fails between registering and resetting a proxy mock must not leak it into later tests.
+		wc_get_container()->get( LegacyProxy::class )->reset();
+
 		parent::tearDown();
 	}

@@ -475,30 +479,26 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
 	}

 	/**
-	 * Test flush_cached_patterns defers unscheduling when called before action_scheduler_init.
+	 * Test flush_cached_patterns defers unscheduling when called before Action Scheduler is ready.
 	 *
-	 * This test simulates the scenario where flush_cached_patterns is called during early
-	 * initialization, before Action Scheduler has been initialized.
+	 * ActionSchedulerUtil::when_ready() decides readiness through the LegacyProxy, so the test makes
+	 * both `action_scheduler_init` and `init` look unfired there, which is how an early call sees them.
 	 */
 	public function test_flush_cached_patterns_defers_unscheduling_before_action_scheduler_init() {
-		global $wp_actions;
-
 		// Schedule an action.
 		as_schedule_single_action( time(), 'fetch_patterns', array(), 'woocommerce' );
 		$this->assertTrue( as_has_scheduled_action( 'fetch_patterns', array(), 'woocommerce' ), 'Action should be scheduled initially' );

-		// Save the original action_scheduler_init count.
-		$original_count = $wp_actions['action_scheduler_init'] ?? 0;
-
-		// Simulate that action_scheduler_init has not yet fired by unsetting it.
-		if ( isset( $wp_actions['action_scheduler_init'] ) ) {
-			unset( $wp_actions['action_scheduler_init'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
-		}
-
-		// Verify did_action returns 0 (not fired).
-		$this->assertEquals( 0, did_action( 'action_scheduler_init' ), 'action_scheduler_init should appear not fired' );
+		$proxy = wc_get_container()->get( LegacyProxy::class );
+		$proxy->register_function_mocks(
+			array(
+				'did_action' => function ( $hook ) {
+					return in_array( $hook, array( 'action_scheduler_init', 'init' ), true ) ? 0 : did_action( $hook );
+				},
+			)
+		);

-		// Call flush_cached_patterns before action_scheduler_init has fired.
+		// Call flush_cached_patterns before Action Scheduler is ready.
 		$this->pattern_store->flush_cached_patterns();

 		// The option should be deleted immediately.
@@ -508,6 +508,8 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
 		// The action should still be scheduled (unscheduling is deferred).
 		$this->assertTrue( as_has_scheduled_action( 'fetch_patterns', array(), 'woocommerce' ), 'Action should still be scheduled before action_scheduler_init fires' );

+		$proxy->reset();
+
 		/**
 		 * Simulate action_scheduler_init firing.
 		 *
@@ -519,7 +521,9 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
 		// After action_scheduler_init fires, the action should be unscheduled.
 		$this->assertFalse( as_has_scheduled_action( 'fetch_patterns', array(), 'woocommerce' ), 'Action should be unscheduled after action_scheduler_init fires' );

-		// Restore the original action count.
-		$wp_actions['action_scheduler_init'] = $original_count; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		// The deferred unscheduling runs once: a later firing must leave a newly scheduled action alone.
+		as_schedule_single_action( time(), 'fetch_patterns', array(), 'woocommerce' );
+		do_action( 'action_scheduler_init' );
+		$this->assertTrue( as_has_scheduled_action( 'fetch_patterns', array(), 'woocommerce' ), 'The deferred unscheduling should not run a second time' );
 	}
 }