Commit 05db210c08f for woocommerce
commit 05db210c08f795c75c83db6f4ee30f61f2feba48
Author: AnnaMag <AnnaMag@users.noreply.github.com>
Date: Fri Nov 13 21:57:32 2026 +0100
Agent readiness: show the findings on the Agentic commerce settings page (#70456)
* Agent readiness: show the findings on the Agentic commerce settings page
Part of #70456.
* Agent readiness: mark the changelog entry as a patch
A Comment-only changelog entry must be a patch; changelogger validate rejects it as minor.
diff --git a/plugins/woocommerce/changelog/add-agent-readiness-settings-findings b/plugins/woocommerce/changelog/add-agent-readiness-settings-findings
new file mode 100644
index 00000000000..a66e8bd7cf1
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-agent-readiness-settings-findings
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Agent readiness: move the check from the Status tab to Agentic commerce settings, behind an experimental flag.
diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss
index 46d6d8f2003..d1549b4ce67 100644
--- a/plugins/woocommerce/client/legacy/css/admin.scss
+++ b/plugins/woocommerce/client/legacy/css/admin.scss
@@ -9960,6 +9960,7 @@ body.woocommerce-settings-payments-section_legacy {
.wc-agentic-commerce-badge {
display: inline-block;
+ white-space: nowrap;
padding: 0 8px;
border-radius: 2px;
font-weight: 600;
@@ -9982,6 +9983,19 @@ body.woocommerce-settings-payments-section_legacy {
}
}
+.wc-agentic-commerce-card__section {
+ padding: 16px 24px 24px;
+ border-block-start: 1px solid #f0f0f1;
+
+ h3 {
+ margin-block-end: 8px;
+ }
+}
+
+.wc-agentic-commerce-channels {
+ margin-block-end: 16px;
+}
+
.wc-agentic-commerce-ucp-switch {
flex: none;
margin-block-start: 2px;
diff --git a/plugins/woocommerce/client/legacy/js/admin/wc-agentic-commerce-settings.js b/plugins/woocommerce/client/legacy/js/admin/wc-agentic-commerce-settings.js
index b929ee32e8e..69c072ad547 100644
--- a/plugins/woocommerce/client/legacy/js/admin/wc-agentic-commerce-settings.js
+++ b/plugins/woocommerce/client/legacy/js/admin/wc-agentic-commerce-settings.js
@@ -55,6 +55,24 @@
} );
}
+ const run = document.querySelector( '.wc-agentic-commerce-run' );
+ const runMessage = document.querySelector( '.wc-agentic-commerce-run-message' );
+ if ( run && runMessage ) {
+ run.addEventListener( 'click', async () => {
+ run.disabled = true;
+ try {
+ await window.wp.apiFetch( {
+ path: settings.scanPath,
+ method: 'POST',
+ } );
+ runMessage.textContent = settings.i18n.running;
+ } catch ( error ) {
+ run.disabled = false;
+ runMessage.textContent = ( error && error.message ) || settings.i18n.error;
+ }
+ } );
+ }
+
document.querySelectorAll( '.wc-agentic-commerce-copy' ).forEach( ( button ) => {
button.addEventListener( 'click', async () => {
const input = document.getElementById( button.dataset.copyTarget );
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 73a00a5bc38..93d7da70728 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -461,7 +461,6 @@ final class WooCommerce {
$container->get( Automattic\WooCommerce\Internal\ScheduledSalePriceReconciler::class )->register();
$container->get( Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalController::class )->register();
$container->get( Automattic\WooCommerce\Internal\Ucp\UcpDiscovery::class )->register();
- $container->get( Automattic\WooCommerce\Internal\AgentReadiness\StatusTab::class )->register();
$container->get( Automattic\WooCommerce\Internal\AgentReadiness\ScheduledScan::class )->register();
$container->get( Automattic\WooCommerce\Internal\AgentReadiness\ReadinessRestController::class )->register();
$container->get( Automattic\WooCommerce\Internal\AgentReadiness\AgenticCommerceSettings::class )->register();
diff --git a/plugins/woocommerce/src/Internal/AgentReadiness/AgenticCommerceSettings.php b/plugins/woocommerce/src/Internal/AgentReadiness/AgenticCommerceSettings.php
index 6b244891a6b..71aa8127d0e 100644
--- a/plugins/woocommerce/src/Internal/AgentReadiness/AgenticCommerceSettings.php
+++ b/plugins/woocommerce/src/Internal/AgentReadiness/AgenticCommerceSettings.php
@@ -16,6 +16,8 @@ defined( 'ABSPATH' ) || exit;
*/
class AgenticCommerceSettings implements RegisterHooksInterface {
+ public const FEATURE_ID = 'agent_readiness';
+
public const TAB = 'agentic-commerce';
public const PRIVACY_PAGE_OPTION = 'wp_page_for_privacy_policy';
@@ -61,6 +63,13 @@ class AgenticCommerceSettings implements RegisterHooksInterface {
*/
private UcpSwitch $ucp_switch;
+ /**
+ * Background scan.
+ *
+ * @var ScheduledScan
+ */
+ private ScheduledScan $scheduled_scan;
+
/**
* Initialize dependencies.
*
@@ -70,12 +79,14 @@ class AgenticCommerceSettings implements RegisterHooksInterface {
* @param StoreConditions $store_conditions Store conditions.
* @param AgentReadiness $agent_readiness Readiness service.
* @param UcpSwitch $ucp_switch UCP switch.
+ * @param ScheduledScan $scheduled_scan Background scan.
*/
- final public function init( FeaturesController $features_controller, StoreConditions $store_conditions, AgentReadiness $agent_readiness, UcpSwitch $ucp_switch ): void {
+ final public function init( FeaturesController $features_controller, StoreConditions $store_conditions, AgentReadiness $agent_readiness, UcpSwitch $ucp_switch, ScheduledScan $scheduled_scan ): void {
$this->features_controller = $features_controller;
$this->store_conditions = $store_conditions;
$this->agent_readiness = $agent_readiness;
$this->ucp_switch = $ucp_switch;
+ $this->scheduled_scan = $scheduled_scan;
}
/**
@@ -91,7 +102,7 @@ class AgenticCommerceSettings implements RegisterHooksInterface {
* @internal
*/
public function handle_init(): void {
- if ( ! is_admin() || ! $this->features_controller->feature_is_enabled( StatusTab::FEATURE_ID ) ) {
+ if ( ! is_admin() || ! $this->features_controller->feature_is_enabled( self::FEATURE_ID ) ) {
return;
}
add_filter( 'woocommerce_get_settings_pages', array( $this, 'handle_woocommerce_get_settings_pages' ) );
@@ -109,7 +120,7 @@ class AgenticCommerceSettings implements RegisterHooksInterface {
if ( ! is_array( $pages ) || ! class_exists( \WC_Settings_Page::class, false ) ) {
return $pages;
}
- $pages[] = new AgenticCommerceSettingsPage( $this->store_conditions, $this->agent_readiness, $this->ucp_switch );
+ $pages[] = new AgenticCommerceSettingsPage( $this->store_conditions, $this->agent_readiness, $this->ucp_switch, $this->scheduled_scan );
return $pages;
}
}
diff --git a/plugins/woocommerce/src/Internal/AgentReadiness/AgenticCommerceSettingsPage.php b/plugins/woocommerce/src/Internal/AgentReadiness/AgenticCommerceSettingsPage.php
index 4188bfda4a3..8e437571b4d 100644
--- a/plugins/woocommerce/src/Internal/AgentReadiness/AgenticCommerceSettingsPage.php
+++ b/plugins/woocommerce/src/Internal/AgentReadiness/AgenticCommerceSettingsPage.php
@@ -51,19 +51,28 @@ class AgenticCommerceSettingsPage extends \WC_Settings_Page {
*/
private UcpSwitch $ucp_switch;
+ /**
+ * Background scan.
+ *
+ * @var ScheduledScan
+ */
+ private ScheduledScan $scheduled_scan;
+
/**
* Constructor.
*
* @param StoreConditions $store_conditions Store conditions.
* @param AgentReadiness $agent_readiness Readiness service.
* @param UcpSwitch $ucp_switch UCP switch.
+ * @param ScheduledScan $scheduled_scan Background scan.
*/
- public function __construct( StoreConditions $store_conditions, AgentReadiness $agent_readiness, UcpSwitch $ucp_switch ) {
+ public function __construct( StoreConditions $store_conditions, AgentReadiness $agent_readiness, UcpSwitch $ucp_switch, ScheduledScan $scheduled_scan ) {
$this->id = AgenticCommerceSettings::TAB;
$this->label = __( 'Agentic commerce', 'woocommerce' );
$this->store_conditions = $store_conditions;
$this->agent_readiness = $agent_readiness;
$this->ucp_switch = $ucp_switch;
+ $this->scheduled_scan = $scheduled_scan;
parent::__construct();
@@ -148,7 +157,7 @@ class AgenticCommerceSettingsPage extends \WC_Settings_Page {
$this->render_switch( $checks, count( $failing ) );
$this->render_visibility( $report );
$this->render_status( $checks, $this->ucp_switch->status( $failing ), count( $failing ) );
- $this->render_readiness( array_slice( $this->agent_readiness->get_store_checks( $report, $checks ), count( $checks ) ), (bool) ( array_column( $checks, 'ok', 'id' )['launched'] ?? true ) );
+ $this->render_readiness( array_slice( $this->agent_readiness->get_store_checks( $report, $checks ), count( $checks ) ), (bool) ( array_column( $checks, 'ok', 'id' )['launched'] ?? true ), $report );
echo '<div class="wc-agentic-commerce-card wc-agentic-commerce-policies">';
parent::output();
@@ -205,12 +214,7 @@ class AgenticCommerceSettingsPage extends \WC_Settings_Page {
echo '<p>' . esc_html__( 'Agents see the same products shoppers see in your shop. To keep a product away from agents, set its catalog visibility to Hidden.', 'woocommerce' ) . '</p></div>';
if ( null === $report ) {
- printf(
- '<p class="wc-agentic-commerce-card__body">%1$s <a href="%2$s">%3$s</a></p></div>',
- esc_html__( 'Product counts appear after the first agent readiness check.', 'woocommerce' ),
- esc_url( admin_url( 'admin.php?page=wc-status&tab=' . StatusTab::TAB ) ),
- esc_html__( 'Run the check', 'woocommerce' )
- );
+ echo '<p class="wc-agentic-commerce-card__body">' . esc_html__( 'Product counts appear after the first agent readiness check.', 'woocommerce' ) . '</p></div>';
return;
}
@@ -263,6 +267,148 @@ class AgenticCommerceSettingsPage extends \WC_Settings_Page {
echo '</div>';
}
+ /**
+ * "Run check" queues the background scan through wc/v3/agent-readiness/scan. While a scan
+ * waits the button is disabled. When Action Scheduler looks stalled, the page says so;
+ * the scan never runs in the request (WOOAI-1661).
+ *
+ * @param array<string, mixed>|null $report Stored report, or null before the first scan.
+ */
+ private function render_run( ?array $report ): void {
+ $pending = $this->scheduled_scan->is_pending();
+ $stalled = $pending && $this->scheduled_scan->is_stalled();
+ $message = '';
+ if ( $stalled ) {
+ $message = __( 'The check has not started because scheduled actions do not seem to be running on this site. It runs once they do. See WooCommerce > Status > Scheduled Actions.', 'woocommerce' );
+ } elseif ( $pending ) {
+ $message = __( 'Checking your store in the background. Reload this page in a minute to see the result.', 'woocommerce' );
+ } elseif ( null !== $report && ! empty( $report['generated_at'] ) ) {
+ /* translators: %s: how long ago the check ran, e.g. "5 mins". */
+ $message = sprintf( __( 'Last checked %s ago.', 'woocommerce' ), human_time_diff( (int) strtotime( (string) $report['generated_at'] ) ) );
+ }
+ ?>
+ <p>
+ <button type="button" class="button wc-agentic-commerce-run" <?php disabled( $pending ); ?>><?php esc_html_e( 'Run check', 'woocommerce' ); ?></button>
+ <span class="description wc-agentic-commerce-run-message" role="status"><?php echo esc_html( $message ); ?></span>
+ </p>
+ <?php
+ }
+
+ /**
+ * Per-channel product counts and each failing product check with example products.
+ *
+ * @param array<string, mixed> $report Stored report.
+ */
+ private function render_product_checks( array $report ): void {
+ $channels = is_array( $report['channels'] ?? null ) ? $report['channels'] : array();
+ $failing = array_filter(
+ is_array( $report['checks'] ?? null ) ? $report['checks'] : array(),
+ static fn( $check ): bool => is_array( $check ) && (int) ( $check['count'] ?? 0 ) > 0
+ );
+
+ echo '<div class="wc-agentic-commerce-card__section"><h3>' . esc_html__( 'Product checks', 'woocommerce' ) . '</h3>';
+ echo '<p>' . esc_html__( 'Checks your products against the product data specs of AI shopping channels, so agents such as ChatGPT, Google and Copilot can list and recommend them.', 'woocommerce' ) . '</p>';
+ if ( ! empty( $report['truncated'] ) ) {
+ /* translators: %d: number of products checked. */
+ echo '<div class="notice notice-info inline"><p>' . esc_html( sprintf( __( 'Only the %d most recently updated products were checked.', 'woocommerce' ), ProductScan::PRODUCT_LIMIT ) ) . '</p></div>';
+ }
+ ?>
+ <table class="widefat striped wc-agentic-commerce-channels">
+ <thead>
+ <tr>
+ <th><?php esc_html_e( 'Channel', 'woocommerce' ); ?></th>
+ <th><?php esc_html_e( 'Can be listed', 'woocommerce' ); ?></th>
+ <th><?php esc_html_e( 'Missing a required field', 'woocommerce' ); ?></th>
+ <th><?php esc_html_e( 'Listed with gaps', 'woocommerce' ); ?></th>
+ <th><?php esc_html_e( 'Complete', 'woocommerce' ); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ( $channels as $channel ) : ?>
+ <tr>
+ <td><a href="<?php echo esc_url( (string) $channel['url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( (string) $channel['name'] ); ?></a><br /><span class="description"><?php echo esc_html( (string) $channel['spec'] ); ?></span></td>
+ <td><?php echo esc_html( number_format_i18n( (int) $channel['listable'] ) ); ?></td>
+ <td><?php echo esc_html( number_format_i18n( (int) $channel['not_listable'] ) ); ?></td>
+ <td><?php echo esc_html( number_format_i18n( (int) $channel['with_gaps'] ) ); ?></td>
+ <td><?php echo esc_html( number_format_i18n( (int) $channel['complete'] ) ); ?></td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+ <?php
+ if ( empty( $failing ) ) {
+ echo '<p>' . esc_html__( 'Every product passed every check.', 'woocommerce' ) . '</p></div>';
+ return;
+ }
+ ?>
+ <table class="widefat striped wc-agentic-commerce-product-checks">
+ <thead>
+ <tr>
+ <th><?php esc_html_e( 'Problem', 'woocommerce' ); ?></th>
+ <th><?php esc_html_e( 'Channels', 'woocommerce' ); ?></th>
+ <th><?php esc_html_e( 'Products', 'woocommerce' ); ?></th>
+ <th><?php esc_html_e( 'Examples', 'woocommerce' ); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ( $failing as $check ) : ?>
+ <tr>
+ <td>
+ <strong><?php echo esc_html( (string) $check['label'] ); ?></strong>
+ <p><?php echo esc_html( (string) $check['why'] ); ?></p>
+ <p><a href="<?php echo esc_url( (string) $check['fix_url'] ); ?>"><?php echo esc_html( (string) $check['fix'] ); ?></a></p>
+ </td>
+ <td><?php echo empty( $check['channels'] ) ? esc_html__( 'Product page only', 'woocommerce' ) : wp_kses_post( implode( '<br />', $this->channel_tags( (array) $check['channels'], $channels ) ) ); ?></td>
+ <td><?php echo esc_html( number_format_i18n( (int) $check['count'] ) ); ?></td>
+ <td><?php echo wp_kses_post( implode( '<br />', $this->product_links( (array) ( $check['product_ids'] ?? array() ) ) ) ); ?></td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+ </div>
+ <?php
+ }
+
+ /**
+ * "Required by X" or "Harder to find on X" per channel.
+ *
+ * @param array $check_channels Field and effect by channel id.
+ * @param array $channels Channels from the report.
+ * @return array<int, string> Escaped HTML.
+ */
+ private function channel_tags( array $check_channels, array $channels ): array {
+ $tags = array();
+ foreach ( $check_channels as $channel_id => $spec ) {
+ $name = (string) ( $channels[ $channel_id ]['name'] ?? $channel_id );
+ if ( ReadinessRule::EFFECT_NOT_LISTABLE === ( $spec['effect'] ?? '' ) ) {
+ /* translators: %s: channel name, e.g. ChatGPT. */
+ $tags[] = '<mark class="wc-agentic-commerce-badge wc-agentic-commerce-badge--fix">' . esc_html( sprintf( __( 'Required by %s', 'woocommerce' ), $name ) ) . '</mark>';
+ } else {
+ /* translators: %s: channel name, e.g. ChatGPT. */
+ $tags[] = esc_html( sprintf( __( 'Harder to find on %s', 'woocommerce' ), $name ) );
+ }
+ }
+ return $tags;
+ }
+
+ /**
+ * Edit links for the example products that still exist.
+ *
+ * @param array $product_ids Product ids.
+ * @return array<int, string> Escaped HTML.
+ */
+ private function product_links( array $product_ids ): array {
+ $links = array();
+ foreach ( array_slice( $product_ids, 0, AgentReadiness::PRODUCT_IDS_LIMIT ) as $product_id ) {
+ $url = get_edit_post_link( (int) $product_id, 'raw' );
+ if ( ! $url ) {
+ continue;
+ }
+ $links[] = '<a href="' . esc_url( $url ) . '">' . esc_html( get_the_title( (int) $product_id ) ) . '</a>';
+ }
+ return $links;
+ }
+
/**
* The UCP switch card. The switch saves right away, apart from Save changes. While the
* store is in coming soon mode, the card says so: agents can't shop a store that isn't live.
@@ -388,19 +534,37 @@ class AgenticCommerceSettingsPage extends \WC_Settings_Page {
<?php
}
+ /**
+ * What a failing store check stops, from its `blocks` value.
+ *
+ * @param string $blocks listing, ucp or checkout.
+ */
+ private static function blocks_text( string $blocks ): string {
+ $texts = array(
+ 'listing' => __( 'Affects every product on every channel.', 'woocommerce' ),
+ 'ucp' => __( 'Affects agents that use UCP.', 'woocommerce' ),
+ 'checkout' => __( 'Affects agent checkout.', 'woocommerce' ),
+ );
+ return $texts[ $blocks ] ?? '';
+ }
+
/**
* Agent readiness: the checks the last scan ran on what the store returns. They never
* affect the switch, so they sit apart from the setup checks in Status.
*
- * @param array<int, array{id:string,level:string,ok:bool|null,name:string,fix:string,fix_url:string}> $checks Scanned checks.
- * @param bool $launched Whether the store is launched.
+ * Run check sits at the top, and the product checks from the same scan at the bottom.
+ *
+ * @param array<int, array{id:string,level:string,ok:bool|null,blocks:string,name:string,why:string,fix:string,fix_url:string}> $checks Scanned checks.
+ * @param bool $launched Whether the store is launched.
+ * @param array<string, mixed>|null $report Stored report, or null before the first scan.
*/
- private function render_readiness( array $checks, bool $launched ): void {
+ private function render_readiness( array $checks, bool $launched, ?array $report ): void {
?>
<div class="wc-agentic-commerce-card wc-agentic-commerce-readiness">
<div class="wc-agentic-commerce-card__header">
<h2><?php esc_html_e( 'Agent readiness', 'woocommerce' ); ?></h2>
<p><?php esc_html_e( 'What agents get when they visit your store, from the last check.', 'woocommerce' ); ?></p>
+ <?php $this->render_run( $report ); ?>
</div>
<?php if ( array() === $checks ) : ?>
<p class="wc-agentic-commerce-card__body"><?php esc_html_e( 'Results appear after the first agent readiness check.', 'woocommerce' ); ?></p>
@@ -418,7 +582,12 @@ class AgenticCommerceSettingsPage extends \WC_Settings_Page {
?>
<li class="wc-agentic-commerce-check">
<mark class="wc-agentic-commerce-badge wc-agentic-commerce-badge--<?php echo esc_attr( $badge[0] ); ?>"><?php echo esc_html( $badge[1] ); ?></mark>
- <span class="wc-agentic-commerce-check__text"><?php echo esc_html( $check['name'] ); ?></span>
+ <span class="wc-agentic-commerce-check__text">
+ <?php echo esc_html( $check['name'] ); ?>
+ <?php if ( false === $check['ok'] ) : ?>
+ <span class="description"><?php echo esc_html( trim( $check['why'] . ' ' . self::blocks_text( $check['blocks'] ) ) ); ?></span>
+ <?php endif; ?>
+ </span>
<?php if ( null === $check['ok'] ) : ?>
<span class="description"><?php echo $launched ? esc_html__( 'Your store could not load its own page. The next check tries again.', 'woocommerce' ) : esc_html__( 'Launch your store to run this check.', 'woocommerce' ); ?></span>
<?php elseif ( ! $check['ok'] && '' === $check['fix_url'] ) : ?>
@@ -430,6 +599,11 @@ class AgenticCommerceSettingsPage extends \WC_Settings_Page {
<?php endforeach; ?>
</ul>
<?php endif; ?>
+ <?php
+ if ( null !== $report ) {
+ $this->render_product_checks( $report );
+ }
+ ?>
</div>
<?php
}
@@ -533,7 +707,7 @@ class AgenticCommerceSettingsPage extends \WC_Settings_Page {
}
/**
- * Load the switch and copy button script.
+ * Load the script for the switch, Run check and the copy button.
*
* @param int $failing_required Number of failing required setup checks, for the message shown after turning UCP on.
*/
@@ -549,11 +723,13 @@ class AgenticCommerceSettingsPage extends \WC_Settings_Page {
self::SCRIPT_HANDLE,
'wcAgenticCommerceSettings',
array(
- 'path' => '/wc/v3/agent-readiness/ucp',
- 'i18n' => array(
+ 'path' => '/wc/v3/agent-readiness/ucp',
+ 'scanPath' => '/wc/v3/agent-readiness/scan',
+ 'i18n' => array(
'on' => self::on_message( $failing_required ),
'error' => __( 'The setting could not be changed. Reload the page and try again.', 'woocommerce' ),
'copied' => __( 'Copied', 'woocommerce' ),
+ 'running' => __( 'Checking your store in the background. Reload this page in a minute to see the result.', 'woocommerce' ),
'statusLabel' => self::status_labels(),
),
)
diff --git a/plugins/woocommerce/src/Internal/AgentReadiness/README.md b/plugins/woocommerce/src/Internal/AgentReadiness/README.md
index c8a30fabe91..415cda63135 100644
--- a/plugins/woocommerce/src/Internal/AgentReadiness/README.md
+++ b/plugins/woocommerce/src/Internal/AgentReadiness/README.md
@@ -143,13 +143,13 @@ Per check, `channels` maps a channel id to the feed field and the effect, the sa
## Background scan and REST
-"Run check" on the tab, or `POST wc/v3/agent-readiness/scan`, queues the scan in Action Scheduler (`ScheduledScan`). The report is stored in the `woocommerce_agent_readiness_report` option (not autoloaded). Each run schedules the next one a day later, before scanning, so a failed scan doesn't stop the daily refresh; a run that finds the feature disabled does. If a queued scan still hasn't started 10 minutes after it was due and no other scheduled action has finished, failed or started in that time (Action Scheduler isn't running), the tab says so and the scan stays queued. The scan never runs in a page or REST request; splitting the product pass into one action per batch is WOOAI-1661.
+"Run check" on the Agentic commerce settings page, or `POST wc/v3/agent-readiness/scan`, queues the scan in Action Scheduler (`ScheduledScan`). The report is stored in the `woocommerce_agent_readiness_report` option (not autoloaded). Each run schedules the next one a day later, before scanning, so a failed scan doesn't stop the daily refresh; a run that finds the feature disabled does. If a queued scan still hasn't started 10 minutes after it was due and no other scheduled action has finished, failed or started in that time (Action Scheduler isn't running), the settings page says so and the scan stays queued. The scan never runs in a page or REST request; splitting the product pass into one action per batch is WOOAI-1661.
Product changes don't update the stored report: a fixed product leaves it at the next daily scan or "Run check". On a store with 11,000 products, a scan (stopping at 5,000) took about 4 seconds and 144 MB.
`GET wc/v3/agent-readiness` returns `{ "report": <report or null>, "scan_pending": <bool>, "scan_stalled": <bool> }`, with `store_checks` read now; `scan_stalled` is true while a queued scan waits on a stalled Action Scheduler. Both routes need `manage_woocommerce` and exist only while the feature is enabled.
-The same shape is meant for every consumer: this Status tab, an outside scan of a store, and extensions that rank the findings by sales or offer fixes. Check and channel ids are stable keys.
+The same shape is meant for every consumer: the Agentic commerce settings page, an outside scan of a store, and extensions that rank the findings by sales or offer fixes. Check and channel ids are stable keys.
The scan walks published simple and variable products shown in the shop catalog in batches of 200, most recently modified first, primes meta, terms, images and variations per batch, and stops at 5,000 products.
@@ -167,7 +167,7 @@ WooCommerce > Settings > Agentic commerce (`AgenticCommerceSettings`, `AgenticCo
- The UCP switch, first. It saves through `POST wc/v3/agent-readiness/ucp` (`{ "enabled": true }`). A failing required check keeps it from being turned on, and that POST answers 409; turning it off still works. In coming soon mode the card says agents can't shop the store yet and links to Site visibility.
- Product counts from the stored report (`products`, `hidden_from_catalog`, `unsupported_types`), each linking to a filtered product list.
- Status: the store conditions from `StoreConditions`, the same list the readiness report shows, in three groups. Required lists failing `required` conditions (coming soon mode and HTTPS) and puts the status at Action needed; Recommended lists failing `recommended` ones, such as a missing privacy, terms, refund and returns, or shipping policy page (policy pages are not required by the UCP spec); Done lists the passing ones by their passing state (`name`). Each open condition says what to do and links to it; a chosen policy page that isn't published links to its editor.
-- Agent readiness: the store output checks from the stored report, since they only run in the scan. They are never required, so they don't affect the switch or Status.
+- Agent readiness: the store output checks from the stored report, since they only run in the scan. They are never required, so they don't affect the switch or Status. A failing row adds why it matters and what it affects (`blocks`). The card has "Run check" with the time of the last run, and the product checks from the stored report: per-channel counts, and each failing check with its channels and up to 10 example products.
- Store policy pages. Refund and returns, privacy and terms are the existing options, so they stay in sync with Advanced > Page setup and Settings > Privacy; shipping policy, FAQ and warranty are new. They are for the policy links a UCP checkout can carry (`links`); the UCP endpoints don't send them yet. The dropdowns list published pages other than the cart, checkout, shop and My account pages, and saving leaves a stored page in place even when it's a draft or one of those store pages, which then fails its check.
- The store address agents start from (`/.well-known/ucp`), with Copy.
- Placeholders for parts that aren't built yet: Test your setup (WOOAI-1619) and Security (WOOAI-1620).
@@ -190,4 +190,4 @@ Core's UCP catalog mapper (`src/Internal/ProductFeed/Mapping/UcpProductMapper.ph
wp option update woocommerce_feature_agent_readiness_enabled yes
```
-Then open WooCommerce > Status > Agent readiness and select "Run check".
+Then open WooCommerce > Settings > Agentic commerce and select "Run check".
diff --git a/plugins/woocommerce/src/Internal/AgentReadiness/ReadinessAbility.php b/plugins/woocommerce/src/Internal/AgentReadiness/ReadinessAbility.php
index d7a65f9369e..4bda9ccf641 100644
--- a/plugins/woocommerce/src/Internal/AgentReadiness/ReadinessAbility.php
+++ b/plugins/woocommerce/src/Internal/AgentReadiness/ReadinessAbility.php
@@ -22,7 +22,7 @@ trait ReadinessAbility {
*/
protected static function name_when_enabled( string $name ): string {
$features = wc_get_container()->get( FeaturesController::class );
- return $features->feature_is_enabled( StatusTab::FEATURE_ID ) ? $name : '';
+ return $features->feature_is_enabled( AgenticCommerceSettings::FEATURE_ID ) ? $name : '';
}
/**
diff --git a/plugins/woocommerce/src/Internal/AgentReadiness/ReadinessRestController.php b/plugins/woocommerce/src/Internal/AgentReadiness/ReadinessRestController.php
index fdd552e3b5b..ba4847036a7 100644
--- a/plugins/woocommerce/src/Internal/AgentReadiness/ReadinessRestController.php
+++ b/plugins/woocommerce/src/Internal/AgentReadiness/ReadinessRestController.php
@@ -79,7 +79,7 @@ class ReadinessRestController extends RestApiControllerBase {
* Register the routes.
*/
public function register_routes(): void {
- if ( ! $this->features_controller->feature_is_enabled( StatusTab::FEATURE_ID ) ) {
+ if ( ! $this->features_controller->feature_is_enabled( AgenticCommerceSettings::FEATURE_ID ) ) {
return;
}
register_rest_route(
diff --git a/plugins/woocommerce/src/Internal/AgentReadiness/ScheduledScan.php b/plugins/woocommerce/src/Internal/AgentReadiness/ScheduledScan.php
index 4845fdf82e4..f6519088a09 100644
--- a/plugins/woocommerce/src/Internal/AgentReadiness/ScheduledScan.php
+++ b/plugins/woocommerce/src/Internal/AgentReadiness/ScheduledScan.php
@@ -154,7 +154,7 @@ class ScheduledScan implements RegisterHooksInterface {
* @internal
*/
public function run(): void {
- if ( ! $this->features_controller->feature_is_enabled( StatusTab::FEATURE_ID ) ) {
+ if ( ! $this->features_controller->feature_is_enabled( AgenticCommerceSettings::FEATURE_ID ) ) {
return;
}
as_unschedule_all_actions( self::DAILY_HOOK, array(), self::GROUP );
diff --git a/plugins/woocommerce/src/Internal/AgentReadiness/StatusTab.php b/plugins/woocommerce/src/Internal/AgentReadiness/StatusTab.php
deleted file mode 100644
index 6bbbc21a3a6..00000000000
--- a/plugins/woocommerce/src/Internal/AgentReadiness/StatusTab.php
+++ /dev/null
@@ -1,358 +0,0 @@
-<?php
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Internal\AgentReadiness;
-
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
-use Automattic\WooCommerce\Internal\RegisterHooksInterface;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * The "Agent readiness" tab under WooCommerce > Status, shown only when the
- * agent_readiness feature is enabled. "Run check" queues a background scan;
- * the tab shows the last stored report.
- *
- * @since 11.4.0
- */
-class StatusTab implements RegisterHooksInterface {
-
- public const FEATURE_ID = 'agent_readiness';
-
- public const TAB = 'agent-readiness';
-
- private const NONCE_ACTION = 'wc_agent_readiness_run';
-
- /**
- * Features controller.
- *
- * @var FeaturesController
- */
- private FeaturesController $features_controller;
-
- /**
- * Readiness service.
- *
- * @var AgentReadiness
- */
- private AgentReadiness $agent_readiness;
-
- /**
- * Background scan.
- *
- * @var ScheduledScan
- */
- private ScheduledScan $scheduled_scan;
-
- /**
- * Initialize dependencies.
- *
- * @internal
- *
- * @param FeaturesController $features_controller Features controller.
- * @param AgentReadiness $agent_readiness Readiness service.
- * @param ScheduledScan $scheduled_scan Background scan.
- */
- final public function init( FeaturesController $features_controller, AgentReadiness $agent_readiness, ScheduledScan $scheduled_scan ): void {
- $this->features_controller = $features_controller;
- $this->agent_readiness = $agent_readiness;
- $this->scheduled_scan = $scheduled_scan;
- }
-
- /**
- * Register hooks. The feature check waits for init, since feature definitions load translations.
- */
- public function register(): void {
- add_action( 'init', array( $this, 'handle_init' ) );
- }
-
- /**
- * Add the tab when the feature is enabled.
- *
- * @internal
- */
- public function handle_init(): void {
- if ( ! is_admin() || ! $this->features_controller->feature_is_enabled( self::FEATURE_ID ) ) {
- return;
- }
- add_filter( 'woocommerce_admin_status_tabs', array( $this, 'handle_woocommerce_admin_status_tabs' ) );
- add_action( 'woocommerce_admin_status_content_' . self::TAB, array( $this, 'render' ) );
- add_action( 'load-woocommerce_page_wc-status', array( $this, 'handle_run_request' ) );
- }
-
- /**
- * Add the tab to the status page.
- *
- * @internal
- *
- * @param mixed $tabs Tab labels by slug.
- * @return mixed
- */
- public function handle_woocommerce_admin_status_tabs( $tabs ) {
- if ( ! is_array( $tabs ) ) {
- return $tabs;
- }
- $tabs[ self::TAB ] = __( 'Agent readiness', 'woocommerce' );
- return $tabs;
- }
-
- /**
- * Queue a scan when the "Run check" form is submitted, then redirect back to the tab.
- *
- * @internal
- */
- public function handle_run_request(): void {
- if ( ! isset( $_POST['wc_agent_readiness_run'] ) ) {
- return;
- }
- check_admin_referer( self::NONCE_ACTION );
- if ( ! current_user_can( 'manage_woocommerce' ) ) {
- wp_die( esc_html__( 'You do not have permission to run this check.', 'woocommerce' ), 403 );
- }
- $this->scheduled_scan->queue();
- wp_safe_redirect( $this->tab_url() );
- exit;
- }
-
- /**
- * Render the tab.
- *
- * @internal
- */
- public function render(): void {
- $report = $this->agent_readiness->get_cached_report();
- $pending = $this->scheduled_scan->is_pending();
- $stalled = $pending && $this->scheduled_scan->is_stalled();
- ?>
- <h2><?php esc_html_e( 'Agent readiness', 'woocommerce' ); ?></h2>
- <p><?php esc_html_e( 'Checks your products against the product data specs of AI shopping channels, so agents such as ChatGPT, Google and Copilot can list and recommend them.', 'woocommerce' ); ?></p>
- <form method="post" action="<?php echo esc_url( $this->tab_url() ); ?>">
- <?php wp_nonce_field( self::NONCE_ACTION ); ?>
- <p>
- <button type="submit" name="wc_agent_readiness_run" value="1" class="button button-primary" <?php disabled( $pending ); ?>><?php esc_html_e( 'Run check', 'woocommerce' ); ?></button>
- <?php if ( null !== $report && ! empty( $report['generated_at'] ) ) : ?>
- <span class="description">
- <?php
- /* translators: %s: how long ago the check ran, e.g. "5 mins". */
- echo esc_html( sprintf( __( 'Last run %s ago.', 'woocommerce' ), human_time_diff( (int) strtotime( (string) $report['generated_at'] ) ) ) );
- ?>
- </span>
- <?php endif; ?>
- </p>
- </form>
- <?php
- if ( $stalled ) {
- echo '<div class="notice notice-warning inline"><p>' . esc_html__( 'The check has not started because scheduled actions do not seem to be running on this site. It runs once they do. See WooCommerce > Status > Scheduled Actions.', 'woocommerce' ) . '</p></div>';
- } elseif ( $pending ) {
- echo '<div class="notice notice-info inline"><p>' . esc_html__( 'Checking your products in the background. Reload this page in a minute to see the result.', 'woocommerce' ) . '</p></div>';
- }
- if ( null === $report ) {
- if ( ! $pending ) {
- echo '<p>' . esc_html__( 'No check has run yet.', 'woocommerce' ) . '</p>';
- }
- return;
- }
-
- $this->render_store_checks( $report['store_checks'] ?? array() );
- if ( ! empty( $report['truncated'] ) ) {
- echo '<div class="notice notice-info inline"><p>';
- /* translators: %d: number of products checked. */
- echo esc_html( sprintf( __( 'Only the %d most recently updated products were checked.', 'woocommerce' ), ProductScan::PRODUCT_LIMIT ) );
- echo '</p></div>';
- }
- $this->render_channels( $report['channels'] ?? array(), (int) ( $report['products'] ?? 0 ) );
- $this->render_not_checked( (int) ( $report['hidden_from_catalog'] ?? 0 ), (int) ( $report['unsupported_types'] ?? 0 ) );
- $this->render_checks( $report['checks'] ?? array(), $report['channels'] ?? array() );
- }
-
- /**
- * Store-wide problems, shown first because they affect every product.
- *
- * @param array $store_checks Store checks from the report.
- */
- private function render_store_checks( array $store_checks ): void {
- $failing = array();
- $unchecked = array();
- foreach ( $store_checks as $check ) {
- if ( array_key_exists( 'ok', $check ) && null === $check['ok'] ) {
- $unchecked[] = (string) $check['name'];
- } elseif ( empty( $check['ok'] ) ) {
- $failing[] = $check;
- }
- }
- if ( ! empty( $unchecked ) ) {
- printf(
- '<div class="notice notice-warning inline"><p><strong>%1$s</strong> %2$s</p></div>',
- esc_html__( 'Your store could not load its own pages, so these were not checked:', 'woocommerce' ),
- esc_html( implode( ', ', $unchecked ) )
- );
- }
- if ( empty( $failing ) ) {
- return;
- }
- echo '<h3>' . esc_html__( 'Store-wide problems', 'woocommerce' ) . '</h3>';
- foreach ( $failing as $check ) {
- printf(
- '<div class="notice notice-error inline"><p><strong>%1$s</strong> %2$s %3$s</p></div>',
- esc_html( (string) $check['label'] ),
- esc_html( (string) $check['why'] ),
- esc_html( (string) $check['fix'] )
- );
- }
- }
-
- /**
- * Per-channel product counts.
- *
- * @param array $channels Channels from the report.
- * @param int $products Products checked.
- */
- private function render_channels( array $channels, int $products ): void {
- echo '<h3>' . esc_html__( 'Channels', 'woocommerce' ) . '</h3>';
- /* translators: %d: number of products checked. */
- echo '<p>' . esc_html( sprintf( _n( '%d published product checked.', '%d published products checked.', $products, 'woocommerce' ), $products ) ) . '</p>';
- ?>
- <table class="widefat striped">
- <thead>
- <tr>
- <th><?php esc_html_e( 'Channel', 'woocommerce' ); ?></th>
- <th><?php esc_html_e( 'Can be listed', 'woocommerce' ); ?></th>
- <th><?php esc_html_e( 'Missing a required field', 'woocommerce' ); ?></th>
- <th><?php esc_html_e( 'Listed with gaps', 'woocommerce' ); ?></th>
- <th><?php esc_html_e( 'Complete', 'woocommerce' ); ?></th>
- </tr>
- </thead>
- <tbody>
- <?php foreach ( $channels as $channel ) : ?>
- <tr>
- <td><a href="<?php echo esc_url( (string) $channel['url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( (string) $channel['name'] ); ?></a><br /><span class="description"><?php echo esc_html( (string) $channel['spec'] ); ?></span></td>
- <td><?php echo esc_html( (string) (int) $channel['listable'] ); ?></td>
- <td><?php echo esc_html( (string) (int) $channel['not_listable'] ); ?></td>
- <td><?php echo esc_html( (string) (int) $channel['with_gaps'] ); ?></td>
- <td><?php echo esc_html( (string) (int) $channel['complete'] ); ?></td>
- </tr>
- <?php endforeach; ?>
- </tbody>
- </table>
- <?php
- }
-
- /**
- * Published products the scan counts but doesn't check.
- *
- * @param int $hidden Products hidden from the shop catalog.
- * @param int $unsupported Products of a type agents don't support yet.
- */
- private function render_not_checked( int $hidden, int $unsupported ): void {
- $lines = array();
- if ( $hidden > 0 ) {
- /* translators: %d: number of products. */
- $lines[] = sprintf( __( 'Hidden from your catalog: %d', 'woocommerce' ), $hidden );
- }
- if ( $unsupported > 0 ) {
- /* translators: %d: number of products. */
- $lines[] = sprintf( __( 'Product type not supported yet: %d', 'woocommerce' ), $unsupported );
- }
- if ( empty( $lines ) ) {
- return;
- }
- echo '<p class="description">' . esc_html__( 'Not checked, because agents don\'t see these products:', 'woocommerce' ) . '</p><ul>';
- foreach ( $lines as $line ) {
- echo '<li>' . esc_html( $line ) . '</li>';
- }
- echo '</ul>';
- }
-
- /**
- * Each failing check with its channel tags and example products.
- *
- * @param array $checks Checks from the report.
- * @param array $channels Channels from the report.
- */
- private function render_checks( array $checks, array $channels ): void {
- $failing = array_filter(
- $checks,
- static function ( $check ) {
- return (int) ( $check['count'] ?? 0 ) > 0;
- }
- );
- echo '<h3>' . esc_html__( 'Product checks', 'woocommerce' ) . '</h3>';
- if ( empty( $failing ) ) {
- echo '<p>' . esc_html__( 'Every product passed every check.', 'woocommerce' ) . '</p>';
- return;
- }
- ?>
- <table class="widefat striped">
- <thead>
- <tr>
- <th><?php esc_html_e( 'Problem', 'woocommerce' ); ?></th>
- <th><?php esc_html_e( 'Channels', 'woocommerce' ); ?></th>
- <th><?php esc_html_e( 'Products', 'woocommerce' ); ?></th>
- <th><?php esc_html_e( 'Examples', 'woocommerce' ); ?></th>
- </tr>
- </thead>
- <tbody>
- <?php foreach ( $failing as $check ) : ?>
- <tr>
- <td>
- <strong><?php echo esc_html( (string) $check['label'] ); ?></strong>
- <p><?php echo esc_html( (string) $check['why'] ); ?></p>
- <p><a href="<?php echo esc_url( (string) $check['fix_url'] ); ?>"><?php echo esc_html( (string) $check['fix'] ); ?></a></p>
- </td>
- <td><?php echo empty( $check['channels'] ) ? esc_html__( 'Product page only', 'woocommerce' ) : wp_kses_post( implode( '<br />', $this->channel_tags( (array) $check['channels'], $channels ) ) ); ?></td>
- <td><?php echo esc_html( (string) (int) $check['count'] ); ?></td>
- <td><?php echo wp_kses_post( implode( '<br />', $this->product_links( (array) $check['product_ids'] ) ) ); ?></td>
- </tr>
- <?php endforeach; ?>
- </tbody>
- </table>
- <?php
- }
-
- /**
- * "Required by X" or "Harder to find on X" per channel.
- *
- * @param array $check_channels Field and effect by channel id.
- * @param array $channels Channels from the report.
- * @return array<int, string> Escaped HTML.
- */
- private function channel_tags( array $check_channels, array $channels ): array {
- $tags = array();
- foreach ( $check_channels as $channel_id => $spec ) {
- $name = (string) ( $channels[ $channel_id ]['name'] ?? $channel_id );
- if ( ReadinessRule::EFFECT_NOT_LISTABLE === ( $spec['effect'] ?? '' ) ) {
- /* translators: %s: channel name, e.g. ChatGPT. */
- $tags[] = '<mark class="error">' . esc_html( sprintf( __( 'Required by %s', 'woocommerce' ), $name ) ) . '</mark>';
- } else {
- /* translators: %s: channel name, e.g. ChatGPT. */
- $tags[] = esc_html( sprintf( __( 'Harder to find on %s', 'woocommerce' ), $name ) );
- }
- }
- return $tags;
- }
-
- /**
- * Edit links for the example products that still exist.
- *
- * @param array $product_ids Product ids.
- * @return array<int, string> Escaped HTML.
- */
- private function product_links( array $product_ids ): array {
- $links = array();
- foreach ( array_slice( $product_ids, 0, AgentReadiness::PRODUCT_IDS_LIMIT ) as $product_id ) {
- $url = get_edit_post_link( (int) $product_id, 'raw' );
- if ( ! $url ) {
- continue;
- }
- $links[] = '<a href="' . esc_url( $url ) . '">' . esc_html( get_the_title( (int) $product_id ) ) . '</a>';
- }
- return $links;
- }
-
- /**
- * URL of this tab.
- */
- private function tab_url(): string {
- return admin_url( 'admin.php?page=wc-status&tab=' . self::TAB );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index 443a02f69d6..737f9205ff5 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -665,7 +665,7 @@ class FeaturesController {
'agent_readiness' => array(
'name' => __( 'Agent readiness', 'woocommerce' ),
'description' => __(
- 'Check products against the product data specs of AI shopping channels under WooCommerce > Status, and set up agentic commerce under WooCommerce > Settings.',
+ 'Check products against the product data specs of AI shopping channels, and set up agentic commerce, under WooCommerce > Settings > Agentic commerce.',
'woocommerce'
),
'enabled_by_default' => false,
diff --git a/plugins/woocommerce/tests/php/src/Internal/AgentReadiness/AgenticCommerceSettingsTest.php b/plugins/woocommerce/tests/php/src/Internal/AgentReadiness/AgenticCommerceSettingsTest.php
index a150c467a8c..e999da90bb2 100644
--- a/plugins/woocommerce/tests/php/src/Internal/AgentReadiness/AgenticCommerceSettingsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/AgentReadiness/AgenticCommerceSettingsTest.php
@@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Tests\Internal\AgentReadiness;
use Automattic\WooCommerce\Internal\AgentReadiness\AgenticCommerceSettings;
use Automattic\WooCommerce\Internal\AgentReadiness\AgenticCommerceSettingsPage;
use Automattic\WooCommerce\Internal\AgentReadiness\AgentReadiness;
+use Automattic\WooCommerce\Internal\AgentReadiness\ScheduledScan;
use WC_Admin_Settings;
use WC_Helper_Product;
use WC_Unit_Test_Case;
@@ -283,6 +284,58 @@ class AgenticCommerceSettingsTest extends WC_Unit_Test_Case {
$this->assertStringNotContainsString( 'AI agents can now find and shop your store.</p>', $html );
}
+ /**
+ * @testdox The page shows each failing product check with its channel tags and example products, and a failing setup check says what to do.
+ */
+ public function test_output_product_checks_and_store_check_detail(): void {
+ $product = WC_Helper_Product::create_simple_product();
+ update_option( 'blog_public', '0' );
+ wc_get_container()->get( AgentReadiness::class )->refresh_report();
+
+ $html = $this->render_page();
+
+ $this->assertStringContainsString( 'Product checks', $html );
+ $this->assertStringContainsString( 'Required by ChatGPT', $html );
+ $this->assertStringContainsString( 'Harder to find on Google', $html );
+ $this->assertStringContainsString( 'post=' . $product->get_id(), $html );
+ $this->assertStringContainsString( 'Let search engines index your store', $html, 'Hidden from search engines is a Recommended setup step' );
+ $this->assertStringContainsString( 'Last checked', $html );
+ }
+
+ /**
+ * @testdox While a scan waits, Run check is disabled and the page says so; once it stalls, the button stays disabled and the page says scheduled actions aren't running.
+ */
+ public function test_output_run_pending_and_stalled(): void {
+ delete_option( AgentReadiness::REPORT_OPTION );
+ as_unschedule_all_actions( ScheduledScan::RUN_NOW_HOOK );
+
+ $html = $this->render_page();
+ $this->assertDoesNotMatchRegularExpression( '/wc-agentic-commerce-run"[^>]*disabled/', $html );
+ $this->assertStringNotContainsString( 'Product checks', $html, 'No product checks before the first scan' );
+
+ wc_get_container()->get( ScheduledScan::class )->queue();
+ $html = $this->render_page();
+ $this->assertStringContainsString( 'Checking your store in the background.', $html );
+ $this->assertMatchesRegularExpression( '/wc-agentic-commerce-run"[^>]*disabled/', $html );
+
+ as_unschedule_all_actions( ScheduledScan::RUN_NOW_HOOK );
+ $this->age_finished_actions();
+ as_schedule_single_action( time() - ScheduledScan::STALLED_AFTER - 60, ScheduledScan::RUN_NOW_HOOK, array(), ScheduledScan::GROUP );
+ $html = $this->render_page();
+ $this->assertStringContainsString( 'The check has not started because scheduled actions', $html );
+ $this->assertMatchesRegularExpression( '/wc-agentic-commerce-run"[^>]*disabled/', $html );
+ as_unschedule_all_actions( ScheduledScan::RUN_NOW_HOOK );
+ }
+
+ /**
+ * Date every finished action an hour back, so only what the test does next
+ * counts as recent queue activity.
+ */
+ private function age_finished_actions(): void {
+ global $wpdb;
+ $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}actionscheduler_actions SET last_attempt_gmt = %s WHERE status IN ( 'complete', 'failed', 'in-progress' )", gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS ) ) );
+ }
+
/**
* @testdox A stored draft page is listed in its own dropdown and kept when resubmitted, an empty choice clears a stored draft, and a new draft is not saved.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/AgentReadiness/StatusTabTest.php b/plugins/woocommerce/tests/php/src/Internal/AgentReadiness/StatusTabTest.php
deleted file mode 100644
index de6b319f6d6..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/AgentReadiness/StatusTabTest.php
+++ /dev/null
@@ -1,111 +0,0 @@
-<?php
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\AgentReadiness;
-
-use Automattic\WooCommerce\Internal\AgentReadiness\AgentReadiness;
-use Automattic\WooCommerce\Internal\AgentReadiness\ScheduledScan;
-use Automattic\WooCommerce\Internal\AgentReadiness\StatusTab;
-use WC_Helper_Product;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for the Agent readiness status tab.
- */
-class StatusTabTest extends WC_Unit_Test_Case {
-
- /**
- * @testdox The tab renders store-wide problems, channel tags and product links from the cached report.
- */
- public function test_render_from_cached_report(): void {
- wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
- $product = WC_Helper_Product::create_simple_product();
- update_option( 'blog_public', '0' );
- StoreOutputChecksTest::block_requests();
- wc_get_container()->get( AgentReadiness::class )->refresh_report();
-
- ob_start();
- wc_get_container()->get( StatusTab::class )->render();
- $html = (string) ob_get_clean();
-
- $this->assertStringContainsString( 'Store is hidden from search engines', $html );
- $this->assertStringContainsString( 'Required by ChatGPT', $html );
- $this->assertStringContainsString( 'Harder to find on Google', $html );
- $this->assertStringContainsString( 'post=' . $product->get_id(), $html );
- }
-
- /**
- * @testdox The tab lists the products it counts but doesn't check.
- */
- public function test_render_not_checked_counts(): void {
- wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
- $hidden = WC_Helper_Product::create_simple_product();
- $hidden->set_catalog_visibility( 'hidden' );
- $hidden->save();
- WC_Helper_Product::create_external_product();
- StoreOutputChecksTest::block_requests();
- wc_get_container()->get( AgentReadiness::class )->refresh_report();
-
- ob_start();
- wc_get_container()->get( StatusTab::class )->render();
- $html = (string) ob_get_clean();
-
- $this->assertStringContainsString( 'Hidden from your catalog: 1', $html );
- $this->assertStringContainsString( 'Product type not supported yet: 1', $html );
- }
-
- /**
- * @testdox The tab asks for a first run when no report is cached.
- */
- public function test_render_without_report(): void {
- delete_option( AgentReadiness::REPORT_OPTION );
-
- ob_start();
- wc_get_container()->get( StatusTab::class )->render();
- $html = (string) ob_get_clean();
-
- $this->assertStringContainsString( 'No check has run yet.', $html );
- $this->assertStringContainsString( 'name="wc_agent_readiness_run"', $html );
- }
-
- /**
- * @testdox While a scan waits, the button is disabled and the tab says so; once it stalls, the button stays disabled and the tab says scheduled actions aren't running.
- */
- public function test_render_pending_and_stalled(): void {
- delete_option( AgentReadiness::REPORT_OPTION );
- as_unschedule_all_actions( ScheduledScan::RUN_NOW_HOOK );
- wc_get_container()->get( ScheduledScan::class )->queue();
-
- $html = $this->render();
- $this->assertStringContainsString( 'Checking your products in the background.', $html );
- $this->assertStringNotContainsString( 'No check has run yet.', $html, 'A first scan is already on its way' );
- $this->assertMatchesRegularExpression( '/name="wc_agent_readiness_run"[^>]*disabled/', $html );
-
- as_unschedule_all_actions( ScheduledScan::RUN_NOW_HOOK );
- $this->age_finished_actions();
- as_schedule_single_action( time() - ScheduledScan::STALLED_AFTER - 60, ScheduledScan::RUN_NOW_HOOK, array(), ScheduledScan::GROUP );
-
- $html = $this->render();
- $this->assertStringContainsString( 'The check has not started because scheduled actions', $html );
- $this->assertMatchesRegularExpression( '/name="wc_agent_readiness_run"[^>]*disabled/', $html );
- as_unschedule_all_actions( ScheduledScan::RUN_NOW_HOOK );
- }
-
- /**
- * The tab's HTML.
- */
- private function render(): string {
- ob_start();
- wc_get_container()->get( StatusTab::class )->render();
- return (string) ob_get_clean();
- }
-
- /**
- * Date every finished action an hour back, so only what the test does next
- * counts as recent queue activity.
- */
- private function age_finished_actions(): void {
- global $wpdb;
- $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}actionscheduler_actions SET last_attempt_gmt = %s WHERE status IN ( 'complete', 'failed', 'in-progress' )", gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS ) ) );
- }
-}