Commit 4d8bfde8f6f for woocommerce

commit 4d8bfde8f6f58ed9ad1fb4872dc1cf82766d0fd5
Author: Lucio Giannotta <lucio.giannotta@a8c.com>
Date:   Wed Sep 9 20:49:35 2026 +0200

    Fix variation save persisting the inherited parent image (#68389)

diff --git a/plugins/woocommerce/changelog/fix-wooplug-7653-variation-inherited-image b/plugins/woocommerce/changelog/fix-wooplug-7653-variation-inherited-image
new file mode 100644
index 00000000000..acc571df393
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-7653-variation-inherited-image
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Variation gallery: stop persisting the inherited parent image on variation save, prefer variation-owned galleries on the storefront, and clean up previously frozen variation thumbnails.
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index a27be7d4b16..c621ba870c0 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -354,6 +354,7 @@ class WC_Install {
 			'wc_update_1120_remove_abandoned_cart_recovery',
 			'wc_update_1120_migrate_stock_notifications_alpha_constant',
 			'wc_update_1120_delete_surface_cart_checkout_note',
+			'wc_update_1120_cleanup_inherited_variation_images',
 			'wc_update_11201_migrate_tax_lookup_order_items',
 			'wc_update_11201_invalidate_analytics_reports_cache',
 			'wc_update_11202_reset_refund_returning_customer_markers',
diff --git a/plugins/woocommerce/includes/class-wc-product-variable.php b/plugins/woocommerce/includes/class-wc-product-variable.php
index f03445fecc5..2f5361dfc48 100644
--- a/plugins/woocommerce/includes/class-wc-product-variable.php
+++ b/plugins/woocommerce/includes/class-wc-product-variable.php
@@ -598,10 +598,11 @@ class WC_Product_Variable extends WC_Product {
 			return false;
 		}

-		$variation_featured_id    = (int) $variation->get_image_id();
-		$variation_featured_valid = $variation_featured_id && wp_attachment_is_image( $variation_featured_id );
-		$parent_featured_id       = (int) $this->get_image_id();
-		$parent_featured_valid    = $parent_featured_id && wp_attachment_is_image( $parent_featured_id );
+		$variation_featured_id           = (int) $variation->get_image_id();
+		$variation_featured_is_inherited = ! $variation->get_image_id( 'edit' ) && (int) ( $variation->get_parent_data()['image_id'] ?? 0 ) === $variation_featured_id;
+		$variation_featured_valid        = ! $variation_featured_is_inherited && $variation_featured_id && wp_attachment_is_image( $variation_featured_id );
+		$parent_featured_id              = (int) $this->get_image_id();
+		$parent_featured_valid           = $parent_featured_id && wp_attachment_is_image( $parent_featured_id );

 		$variation_gallery_image_ids = array_values(
 			array_filter(
@@ -611,7 +612,6 @@ class WC_Product_Variable extends WC_Product {
 		);
 		$variation_gallery_html      = '';

-		// Prefer variation-owned images over the parent fallback.
 		if ( $variation_featured_valid ) {
 			$selected_image_id = $variation_featured_id;
 		} elseif ( ! empty( $variation_gallery_image_ids ) ) {
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index 37811b0b6de..bc2886f0e5b 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -40,6 +40,7 @@ use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Registe
 use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize as Download_Directories_Sync;
 use Automattic\WooCommerce\Internal\StockNotifications\StockNotifications;
 use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
+use Automattic\WooCommerce\Internal\VariationGallery\Telemetry as VariationGalleryTelemetry;
 use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
 use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
 use Automattic\WooCommerce\Internal\VariationGallery\Package as VariationGalleryPackage;
@@ -3717,6 +3718,121 @@ function wc_update_1120_delete_surface_cart_checkout_note(): void {
 	InboxNotifications::delete_surface_cart_checkout_blocks_notification();
 }

+/**
+ * Remove variation featured images that duplicate the parent product's featured image.
+ *
+ * The classic editor used to persist the inherited parent image onto variations on save,
+ * freezing dynamic inheritance. Removing values that still equal the parent's canonical
+ * thumbnail is display-neutral; diverged values may be deliberate and are kept. Skipped
+ * for stores upgrading from before 10.9.0, which predates the variation gallery.
+ * A database error stops the cleanup and is logged.
+ *
+ * @since 11.2.0
+ *
+ * @return bool True to run again for the next batch, false when completed.
+ */
+function wc_update_1120_cleanup_inherited_variation_images() {
+	global $wpdb;
+
+	$state_option     = 'woocommerce_update_1120_cleanup_state';
+	$completed_option = 'woocommerce_update_1120_completed_at';
+	$batch_size       = 250;
+
+	// A manual db-version rollback replays all update callbacks; this one deletes data, so it must not run twice.
+	if ( get_option( $completed_option ) ) {
+		return false;
+	}
+
+	// Still the pre-update version here: the option is only bumped by the final update callback.
+	if ( version_compare( (string) get_option( 'woocommerce_db_version' ), '10.9.0', '<' ) ) {
+		return false;
+	}
+
+	$state = get_option( $state_option, array() );
+	$state = is_array( $state ) ? $state : array();
+
+	// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names cannot be prepared.
+	$matching_variations = (array) $wpdb->get_results(
+		$wpdb->prepare(
+			"SELECT variation.ID AS variation_id,
+				variation.post_parent AS parent_id,
+				GROUP_CONCAT(DISTINCT variation_thumb.meta_value) AS inherited_image_ids
+			FROM {$wpdb->posts} AS variation
+			INNER JOIN {$wpdb->postmeta} AS variation_thumb
+				ON variation_thumb.post_id = variation.ID
+				AND variation_thumb.meta_key = '_thumbnail_id'
+			INNER JOIN {$wpdb->postmeta} AS parent_thumb
+				ON parent_thumb.post_id = variation.post_parent
+				AND parent_thumb.meta_key = '_thumbnail_id'
+			WHERE variation.ID > %d
+				AND variation.post_type = 'product_variation'
+				AND variation_thumb.meta_value <> ''
+				AND variation_thumb.meta_value = parent_thumb.meta_value
+			GROUP BY variation.ID
+			ORDER BY variation.ID ASC
+			LIMIT %d",
+			(int) ( $state['last_processed_id'] ?? 0 ),
+			$batch_size + 1
+		),
+		ARRAY_A
+	);
+	// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+
+	if ( '' !== $wpdb->last_error ) {
+		wc_get_logger()->error(
+			sprintf( 'Stopped cleaning up inherited variation images: %s', $wpdb->last_error ),
+			array( 'source' => 'wc_update_1120_cleanup_inherited_variation_images' )
+		);
+		delete_option( $state_option );
+
+		return false;
+	}
+
+	$has_more            = count( $matching_variations ) > $batch_size;
+	$matching_variations = array_slice( $matching_variations, 0, $batch_size );
+
+	$cleaned_count = (int) ( $state['cleaned_count'] ?? 0 );
+
+	foreach ( $matching_variations as $matching_variation ) {
+		// The join matches any parent thumbnail row; only the canonical value is safe to delete.
+		$canonical_parent_thumbnail = (int) get_post_meta( (int) $matching_variation['parent_id'], '_thumbnail_id', true );
+		$deleted_any                = false;
+
+		foreach ( wp_parse_id_list( $matching_variation['inherited_image_ids'] ) as $inherited_image_id ) {
+			if ( $inherited_image_id === $canonical_parent_thumbnail && delete_post_meta( (int) $matching_variation['variation_id'], '_thumbnail_id', $inherited_image_id ) ) {
+				$deleted_any = true;
+			}
+		}
+
+		if ( $deleted_any ) {
+			++$cleaned_count;
+		}
+	}
+
+	if ( $has_more ) {
+		$last_processed = end( $matching_variations );
+		update_option(
+			$state_option,
+			array(
+				'last_processed_id' => (int) $last_processed['variation_id'],
+				'cleaned_count'     => $cleaned_count,
+			),
+			false
+		);
+
+		return true;
+	}
+
+	delete_option( $state_option );
+	update_option( $completed_option, time(), false );
+	VariationGalleryTelemetry::record_event(
+		VariationGalleryTelemetry::EVENT_INHERITED_IMAGE_CLEANUP_COMPLETED,
+		array( 'cleaned_count' => $cleaned_count )
+	);
+
+	return false;
+}
+
 /**
  * Invalidate the Analytics report cache.
  *
diff --git a/plugins/woocommerce/src/Internal/VariationGallery/ClassicVariationGalleryAdmin.php b/plugins/woocommerce/src/Internal/VariationGallery/ClassicVariationGalleryAdmin.php
index 68f0ac81361..a56af0656f7 100644
--- a/plugins/woocommerce/src/Internal/VariationGallery/ClassicVariationGalleryAdmin.php
+++ b/plugins/woocommerce/src/Internal/VariationGallery/ClassicVariationGalleryAdmin.php
@@ -235,8 +235,9 @@ class ClassicVariationGalleryAdmin implements RegisterHooksInterface {
 	 * @return array<int>
 	 */
 	private function get_display_image_ids( WC_Product_Variation $variation ): array {
-		$image_ids   = array_values( array_map( 'intval', $variation->get_gallery_image_ids() ) );
-		$featured_id = (int) $variation->get_image_id();
+		$image_ids = array_values( array_map( 'intval', $variation->get_gallery_image_ids() ) );
+		// Read the stored value so an inherited parent image is not saved as the variation's own image.
+		$featured_id = (int) $variation->get_image_id( 'edit' );

 		if ( $featured_id > 0 && ! in_array( $featured_id, $image_ids, true ) ) {
 			array_unshift( $image_ids, $featured_id );
diff --git a/plugins/woocommerce/src/Internal/VariationGallery/Telemetry.php b/plugins/woocommerce/src/Internal/VariationGallery/Telemetry.php
index 7d5523c1af4..57c2b48e072 100644
--- a/plugins/woocommerce/src/Internal/VariationGallery/Telemetry.php
+++ b/plugins/woocommerce/src/Internal/VariationGallery/Telemetry.php
@@ -16,9 +16,10 @@ defined( 'ABSPATH' ) || exit;
  */
 class Telemetry implements RegisterHooksInterface {

-	public const EVENT_SAVE_SUCCEEDED      = 'variation_gallery_save_succeeded';
-	public const EVENT_SAVE_FAILED         = 'variation_gallery_save_failed';
-	public const EVENT_MIGRATION_COMPLETED = 'variation_gallery_migration_completed';
+	public const EVENT_SAVE_SUCCEEDED                    = 'variation_gallery_save_succeeded';
+	public const EVENT_SAVE_FAILED                       = 'variation_gallery_save_failed';
+	public const EVENT_MIGRATION_COMPLETED               = 'variation_gallery_migration_completed';
+	public const EVENT_INHERITED_IMAGE_CLEANUP_COMPLETED = 'variation_gallery_inherited_image_cleanup_completed';

 	private const LEGACY_PLUGIN_FILE = 'woocommerce-additional-variation-images/woocommerce-additional-variation-images.php';

diff --git a/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php b/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
index f95ac444f89..f7f051c7f68 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
@@ -287,6 +287,53 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case {
 		$this->assertSame( '', $available_variation['gallery_images_html'] );
 	}

+	/**
+	 * @testdox 'get_available_variation' prefers the variation's own gallery over the inherited parent featured image.
+	 */
+	public function test_get_available_variation_prefers_own_gallery_over_inherited_parent_image(): void {
+		list( $product, $variation, $variation_gallery_id ) = $this->create_variation_gallery_fixture();
+
+		$available_variation = $product->get_available_variation( $variation );
+
+		$this->assertSame(
+			$variation_gallery_id,
+			$available_variation['image_id'],
+			'A variation that owns a gallery but no featured image should open on gallery[0], not on the inherited parent image.'
+		);
+	}
+
+	/**
+	 * @testdox 'get_available_variation' treats a filtered image equal to the parent featured image as inheritance, so the variation gallery still wins.
+	 */
+	public function test_get_available_variation_treats_filtered_parent_image_as_inherited(): void {
+		list( $product, $variation, $variation_gallery_id ) = $this->create_variation_gallery_fixture();
+		$parent_featured_id                                 = $product->get_image_id();
+
+		// A filter returning exactly the parent's featured image is indistinguishable from
+		// plain inheritance, so the variation-owned gallery takes priority by design.
+		$filter              = static fn() => $parent_featured_id;
+		add_filter( 'woocommerce_product_variation_get_image_id', $filter );
+		$available_variation = $product->get_available_variation( $variation );
+		remove_filter( 'woocommerce_product_variation_get_image_id', $filter );
+
+		$this->assertSame( $variation_gallery_id, $available_variation['image_id'] );
+	}
+
+	/**
+	 * @testdox 'get_available_variation' preserves a filtered variation image when no featured image is stored.
+	 */
+	public function test_get_available_variation_preserves_filtered_image(): void {
+		list( $product, $variation ) = $this->create_variation_gallery_fixture();
+		$filtered_image_id           = $this->create_image_attachment( 'Filtered Variation Image', 'filtered-variation.jpg' );
+
+		$filter              = static fn() => $filtered_image_id;
+		add_filter( 'woocommerce_product_variation_get_image_id', $filter );
+		$available_variation = $product->get_available_variation( $variation );
+		remove_filter( 'woocommerce_product_variation_get_image_id', $filter );
+
+		$this->assertSame( $filtered_image_id, $available_variation['image_id'] );
+	}
+
 	/**
 	 * @testdox get_variation_prices sorts on first call, skips re-sorting on repeat calls, and treats float and string prices as equal via loose comparison.
 	 */
@@ -389,6 +436,26 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * Create a product with a parent image and an image-less variation with a gallery.
+	 *
+	 * @return array{0: WC_Product_Variable, 1: WC_Product_Variation, 2: int}
+	 */
+	private function create_variation_gallery_fixture(): array {
+		$product              = WC_Helper_Product::create_variation_product();
+		$parent_featured_id   = $this->create_image_attachment( 'Parent Featured Image', 'parent-featured.jpg' );
+		$variation_gallery_id = $this->create_image_attachment( 'Variation Gallery Image', 'variation-gallery.jpg' );
+		$product->set_image_id( $parent_featured_id );
+		$product->save();
+
+		wc_get_container()->get( Automattic\WooCommerce\Internal\Caches\ProductCache::class )->flush();
+		$variation = wc_get_product( $product->get_children()[0] );
+		$variation->set_gallery_image_ids( array( $variation_gallery_id ) );
+		$variation->save();
+
+		return array( $product, $variation, $variation_gallery_id );
+	}
+
 	/**
 	 * Create a real image attachment that passes `wp_attachment_is_image()`.
 	 *
diff --git a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
index 8d24d758f87..4fc24f442df 100644
--- a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
@@ -545,4 +545,99 @@ class WC_Update_Functions_Test extends \WC_Unit_Test_Case {
 		$this->assertNull( $get_marker( $refund->get_id() ), 'The refund row marker should be reset to NULL.' );
 		$this->assertSame( '0', $get_marker( $order->get_id() ), 'The order row marker should be left unchanged.' );
 	}
+
+	/**
+	 * @testdox wc_update_1120_cleanup_inherited_variation_images removes a variation thumbnail that duplicates the parent's featured image.
+	 */
+	public function test_wc_update_1120_removes_variation_thumbnail_duplicating_parent() {
+		include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+		update_option( 'woocommerce_db_version', '11.1.0' );
+		$variation_id = $this->create_variation_with_thumbnails( '77', '77' );
+
+		$this->assertFalse( wc_update_1120_cleanup_inherited_variation_images(), 'A batch smaller than the limit should complete in one run.' );
+		$this->assertSame( '', get_post_meta( $variation_id, '_thumbnail_id', true ), 'The duplicated thumbnail should be removed so the variation inherits again.' );
+	}
+
+	/**
+	 * @testdox wc_update_1120_cleanup_inherited_variation_images keeps a variation thumbnail that diverged from the parent's featured image.
+	 */
+	public function test_wc_update_1120_keeps_diverged_variation_thumbnail() {
+		include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+		update_option( 'woocommerce_db_version', '11.1.0' );
+		$variation_id = $this->create_variation_with_thumbnails( '77', '88' );
+
+		$this->assertFalse( wc_update_1120_cleanup_inherited_variation_images() );
+		$this->assertSame( '88', get_post_meta( $variation_id, '_thumbnail_id', true ), 'A diverged value may be a deliberate merchant choice and must survive the cleanup.' );
+	}
+
+	/**
+	 * @testdox wc_update_1120_cleanup_inherited_variation_images removes only metadata values that duplicate the parent image.
+	 */
+	public function test_wc_update_1120_keeps_other_thumbnail_metadata_values() {
+		include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+		update_option( 'woocommerce_db_version', '11.1.0' );
+		$variation_id = $this->create_variation_with_thumbnails( '77', '77' );
+		add_post_meta( $variation_id, '_thumbnail_id', '88' );
+
+		wc_update_1120_cleanup_inherited_variation_images();
+
+		$this->assertSame( array( '88' ), get_post_meta( $variation_id, '_thumbnail_id', false ), 'Only the value duplicating the parent image should be removed.' );
+	}
+
+	/**
+	 * @testdox wc_update_1120_cleanup_inherited_variation_images never runs again once completed, even if update callbacks are replayed.
+	 */
+	public function test_wc_update_1120_does_not_run_again_after_completion() {
+		include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+		update_option( 'woocommerce_db_version', '11.1.0' );
+		update_option( 'woocommerce_update_1120_completed_at', time() );
+		$variation_id = $this->create_variation_with_thumbnails( '77', '77' );
+
+		$this->assertFalse( wc_update_1120_cleanup_inherited_variation_images() );
+		$this->assertSame( '77', get_post_meta( $variation_id, '_thumbnail_id', true ), 'A value matching the parent after the one-time cleanup may be deliberate and must survive a replay.' );
+	}
+
+	/**
+	 * @testdox wc_update_1120_cleanup_inherited_variation_images matches only the parent's canonical thumbnail, not stale duplicate rows.
+	 */
+	public function test_wc_update_1120_ignores_stale_duplicate_parent_thumbnail_rows() {
+		include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+		update_option( 'woocommerce_db_version', '11.1.0' );
+		$variation_id = $this->create_variation_with_thumbnails( '88', '77' );
+		$parent_id    = wp_get_post_parent_id( $variation_id );
+		add_post_meta( $parent_id, '_thumbnail_id', '77' );
+
+		wc_update_1120_cleanup_inherited_variation_images();
+
+		$this->assertSame( '77', get_post_meta( $variation_id, '_thumbnail_id', true ), 'A value matching only a stale duplicate parent row may be deliberate and must survive.' );
+	}
+
+	/**
+	 * @testdox wc_update_1120_cleanup_inherited_variation_images skips stores upgrading from before the variation gallery existed.
+	 */
+	public function test_wc_update_1120_skips_stores_upgrading_from_before_10_9() {
+		include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+		update_option( 'woocommerce_db_version', '10.8.0' );
+		$variation_id = $this->create_variation_with_thumbnails( '77', '77' );
+
+		$this->assertFalse( wc_update_1120_cleanup_inherited_variation_images() );
+		$this->assertSame( '77', get_post_meta( $variation_id, '_thumbnail_id', true ), 'Stores never exposed to the bug must not be touched.' );
+	}
+
+	/**
+	 * Create a variable product and return its first variation's ID, with the given thumbnail meta on parent and variation.
+	 *
+	 * @param string $parent_thumbnail_id    Meta value for the parent's _thumbnail_id.
+	 * @param string $variation_thumbnail_id Meta value for the variation's _thumbnail_id.
+	 * @return int
+	 */
+	private function create_variation_with_thumbnails( string $parent_thumbnail_id, string $variation_thumbnail_id ): int {
+		$product      = WC_Helper_Product::create_variation_product();
+		$variation_id = $product->get_children()[0];
+
+		update_post_meta( $product->get_id(), '_thumbnail_id', $parent_thumbnail_id );
+		update_post_meta( $variation_id, '_thumbnail_id', $variation_thumbnail_id );
+
+		return $variation_id;
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/VariationGallery/ClassicVariationGalleryAdminTest.php b/plugins/woocommerce/tests/php/src/Internal/VariationGallery/ClassicVariationGalleryAdminTest.php
index d13f130b9e1..009875601ef 100644
--- a/plugins/woocommerce/tests/php/src/Internal/VariationGallery/ClassicVariationGalleryAdminTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/VariationGallery/ClassicVariationGalleryAdminTest.php
@@ -64,9 +64,7 @@ class ClassicVariationGalleryAdminTest extends \WC_Unit_Test_Case {
 			implode( ',', $image_ids )
 		);

-		ob_start();
-		$this->sut->render_variation_gallery_field( 0, array(), get_post( $variation->get_id() ) );
-		$output = (string) ob_get_clean();
+		$output = $this->render_variation_gallery_field( $variation );

 		$this->assertStringContainsString(
 			'value="' . implode( ',', $image_ids ) . '"',
@@ -86,9 +84,7 @@ class ClassicVariationGalleryAdminTest extends \WC_Unit_Test_Case {
 		$variation->set_gallery_image_ids( array( $gallery ) );
 		$variation->save();

-		ob_start();
-		$this->sut->render_variation_gallery_field( 0, array(), get_post( $variation->get_id() ) );
-		$output = (string) ob_get_clean();
+		$output = $this->render_variation_gallery_field( $variation );

 		// Featured image must be prepended to the list (value) AND get the active class.
 		$this->assertStringContainsString( 'value="' . $featured . ',' . $gallery . '"', $output );
@@ -109,9 +105,7 @@ class ClassicVariationGalleryAdminTest extends \WC_Unit_Test_Case {
 		$variation->set_gallery_image_ids( array( $missing, $good ) );
 		$variation->save();

-		ob_start();
-		$this->sut->render_variation_gallery_field( 0, array(), get_post( $variation->get_id() ) );
-		$output = (string) ob_get_clean();
+		$output = $this->render_variation_gallery_field( $variation );

 		$this->assertStringContainsString(
 			'data-attachment_id="' . $missing . '"',
@@ -119,20 +113,6 @@ class ClassicVariationGalleryAdminTest extends \WC_Unit_Test_Case {
 		);
 	}

-	/**
-	 * @testdox Empty variation rows render the hidden gallery input needed for the first save.
-	 */
-	public function test_render_variation_gallery_field_renders_hidden_input_for_empty_gallery() {
-		$variation = $this->create_variation();
-
-		ob_start();
-		$this->sut->render_variation_gallery_field( 0, array(), get_post( $variation->get_id() ) );
-		$output = (string) ob_get_clean();
-
-		$this->assertStringContainsString( 'name="variable_gallery_image_ids[0]"', $output );
-		$this->assertStringContainsString( 'value=""', $output );
-	}
-
 	/**
 	 * @testdox Saving an empty variation gallery clears featured + gallery and disables the legacy fallback.
 	 */
@@ -198,6 +178,44 @@ class ClassicVariationGalleryAdminTest extends \WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * @testdox An image-less variation does not render its inherited parent image as variation data.
+	 */
+	public function test_render_variation_gallery_field_excludes_inherited_parent_image(): void {
+		$variation = $this->create_variation_inheriting_parent_image();
+		$output    = $this->render_variation_gallery_field( $variation );
+
+		$this->assertStringContainsString( 'value=""', $output, 'The inherited parent image must not be submitted as variation data.' );
+	}
+
+	/**
+	 * Render the gallery field for a variation.
+	 *
+	 * @param WC_Product_Variation $variation Variation object.
+	 * @return string
+	 */
+	private function render_variation_gallery_field( WC_Product_Variation $variation ): string {
+		ob_start();
+		$this->sut->render_variation_gallery_field( 0, array(), get_post( $variation->get_id() ) );
+
+		return (string) ob_get_clean();
+	}
+
+	/**
+	 * Create a variation that inherits its parent's image.
+	 *
+	 * @return WC_Product_Variation
+	 */
+	private function create_variation_inheriting_parent_image(): WC_Product_Variation {
+		$variation = $this->create_variation();
+		$parent    = wc_get_product( $variation->get_parent_id() );
+		$parent->set_image_id( $this->create_attachment( 'Parent featured image' ) );
+		$parent->save();
+		wc_get_container()->get( \Automattic\WooCommerce\Internal\Caches\ProductCache::class )->flush();
+
+		return $variation;
+	}
+
 	/**
 	 * Create a variation for testing.
 	 *