Commit b0a9a70d08d for woocommerce

commit b0a9a70d08dc89ac97dd2757b3d175a45d352341
Author: Mike Jolley <mike.jolley@me.com>
Date:   Mon Oct 12 17:09:50 2026 +0100

    Clear the system status plugin cache when a plugin is deleted (#69601)

    The system status plugin lists are cached in hour-long transients that
    were cleared on activate, deactivate and upgrader events, but not on
    deletion, so deleted plugins stayed listed for up to an hour. Hook
    clean_plugin_cache() to deleted_plugin, and add REST tests covering
    the delete and install paths.

    Closes #39986.

    Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-39986-system-status-deleted-plugins b/plugins/woocommerce/changelog/fix-39986-system-status-deleted-plugins
new file mode 100644
index 00000000000..a8aaea51811
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-39986-system-status-deleted-plugins
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Stop listing deleted plugins in the system status report and REST API for up to an hour after they are removed.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php
index c0c0c845478..2b6fd7dcc72 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php
@@ -52,6 +52,7 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
 		add_action( 'switch_theme', array( __CLASS__, 'clean_theme_cache' ) );
 		add_action( 'activate_plugin', array( __CLASS__, 'clean_plugin_cache' ) );
 		add_action( 'deactivate_plugin', array( __CLASS__, 'clean_plugin_cache' ) );
+		add_action( 'deleted_plugin', array( __CLASS__, 'clean_plugin_cache' ) );
 		add_action(
 			'upgrader_process_complete',
 			function ( $upgrader, $extra ) {
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller-test.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller-test.php
index 90bde4ffc9d..5b32a4fe917 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller-test.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller-test.php
@@ -17,6 +17,13 @@ class WC_REST_System_Status_V2_Controller_Test extends WC_REST_Unit_Test_Case {
 	 */
 	private $sut;

+	/**
+	 * Plugin installed from the sample-woo-plugin.zip test fixture.
+	 *
+	 * @var string
+	 */
+	private const TEST_PLUGIN_FILE = 'sample-woo-plugin/sample-woo-plugin.php';
+
 	/**
 	 * Set up test fixtures.
 	 */
@@ -30,9 +37,16 @@ class WC_REST_System_Status_V2_Controller_Test extends WC_REST_Unit_Test_Case {
 	 * Tear down test fixtures.
 	 */
 	public function tearDown(): void {
-		parent::tearDown();
-		remove_all_filters( 'wc_get_template' );
-		delete_transient( 'wc_system_status_theme_info' );
+		try {
+			remove_all_filters( 'wc_get_template' );
+			delete_transient( 'wc_system_status_theme_info' );
+
+			if ( file_exists( WP_PLUGIN_DIR . '/' . self::TEST_PLUGIN_FILE ) ) {
+				delete_plugins( array( self::TEST_PLUGIN_FILE ) );
+			}
+		} finally {
+			parent::tearDown();
+		}
 	}

 	/**
@@ -123,4 +137,73 @@ class WC_REST_System_Status_V2_Controller_Test extends WC_REST_Unit_Test_Case {
 		$this->assertSame( 'MH', $settings['store_base_state'] );
 		$this->assertSame( DefaultCustomerAddress::GEOLOCATION_AJAX, $settings['default_customer_location'] );
 	}
+
+	/**
+	 * @testdox Should not list a plugin in inactive_plugins after it has been deleted.
+	 */
+	public function test_get_inactive_plugins_excludes_deleted_plugin(): void {
+		$this->login_as_administrator();
+		$this->install_test_plugin();
+
+		$this->assertContains(
+			self::TEST_PLUGIN_FILE,
+			$this->get_inactive_plugin_files_from_endpoint(),
+			'Precondition: the installed but inactive plugin should be listed'
+		);
+
+		$this->assertTrue( delete_plugins( array( self::TEST_PLUGIN_FILE ) ), 'Precondition: the plugin should be deleted' );
+		// The non-persistent `plugins` cache group would be empty on the next real request.
+		wp_clean_plugins_cache( false );
+
+		$this->assertNotContains(
+			self::TEST_PLUGIN_FILE,
+			$this->get_inactive_plugin_files_from_endpoint(),
+			'A deleted plugin should not be served from the cached inactive plugins list'
+		);
+	}
+
+	/**
+	 * @testdox Should list a plugin in inactive_plugins after it has been installed.
+	 */
+	public function test_get_inactive_plugins_includes_newly_installed_plugin(): void {
+		$this->login_as_administrator();
+
+		$this->assertNotContains(
+			self::TEST_PLUGIN_FILE,
+			$this->get_inactive_plugin_files_from_endpoint(),
+			'Precondition: the plugin should not be listed before it is installed'
+		);
+
+		$this->install_test_plugin();
+
+		$this->assertContains(
+			self::TEST_PLUGIN_FILE,
+			$this->get_inactive_plugin_files_from_endpoint(),
+			'A newly installed plugin should not be missing from the cached inactive plugins list'
+		);
+	}
+
+	/**
+	 * Install the sample-woo-plugin.zip fixture through the plugin upgrader.
+	 */
+	private function install_test_plugin(): void {
+		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+
+		$upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
+		$result   = $upgrader->install( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/sample-woo-plugin.zip' );
+
+		$this->assertTrue( $result, 'Precondition: the test plugin should be installed' );
+	}
+
+	/**
+	 * Fetch the inactive plugin files reported by the system status endpoint.
+	 *
+	 * @return string[]
+	 */
+	private function get_inactive_plugin_files_from_endpoint(): array {
+		$response = $this->do_rest_get_request( 'system_status', array( '_fields' => 'inactive_plugins' ) );
+		$this->assertSame( 200, $response->get_status() );
+
+		return array_column( $response->get_data()['inactive_plugins'], 'plugin' );
+	}
 }