Commit 5ae90950a41 for woocommerce

commit 5ae90950a4142ee72c537c8eec86699582de66f7
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Wed Sep 2 16:46:16 2026 +0300

    Fix missing selected Any variation values in item names (#67527)

    * fix: include selected Any variation values in item names

    When a variation attribute is configured as "Any", the shopper-selected
    value exists only in cart and checkout context, so item names on the
    classic cart, mini-cart, checkout review and newly created order items
    showed it as a detached meta row instead of merging it into the name
    the way defined attribute values are merged.

    A new internal SelectedVariationName service appends the missing
    selected values to the stored variation name, following the same title
    policy as WC_Product_Variation_Data_Store_CPT::generate_product_title().
    Templates seed that contextual name through the new public
    WC_Cart::get_item_product_name() as the initial value of the existing
    woocommerce_cart_item_name filter, and pass it as the new optional
    third argument of wc_get_formatted_cart_item_data() so the value is not
    repeated as metadata. No global filters are added: outdated template
    overrides keep their previous display instead of risking duplicated
    values. Checkout persists the contextual name with raw attribute values
    so order-side meta dedup keeps matching, and that dedup now compares
    entity-decoded copies because wp_kses_post() normalizes special
    characters that item names store raw.

    Refs #36538

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

    * chore: add selected Any variation name changelog

    Refs #36538

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

    * docs: correct version annotations for selected Any variation names

    The branch was authored against 11.1.0-dev, but trunk has since moved
    to 11.2.0-dev and the PR targets trunk with no backport milestone, so
    the new symbols and the touched templates first ship in 11.2.0.

    The separator hook's docblock also claimed 3.0.0; the hook first
    appeared in 3.0.2 (it is absent from the 3.0.1 tag).

    Refs #36538

    * refactor: share the variation title policy through the data store

    SelectedVariationName carried a private copy of the "should the title
    include attributes" rules from
    WC_Product_Variation_Data_Store_CPT::generate_product_title(), with a
    comment asking future editors to keep the two in sync. Duplicated
    policy drifts, and reviewers found the copy hard to reason about.

    Move the rules into a public should_include_attributes_in_title()
    method on the data store, keeping core's explanatory comments and
    adding a (bool) cast on the filter result. generate_product_title()
    delegates to it, and SelectedVariationName calls it through the
    WC_Data_Store proxy guarded by has_callable(), falling back to the
    stored variation name for third-party stores without the method. The
    cast cannot change generate_product_title()'s outcome since the value
    only feeds a ternary.

    The new method is additive public surface on an overridable data
    store class; subclasses may override it to change the policy for
    stored and contextual names together.

    Refs #36538

    * fix: skip non-scalar cart variation values before term lookups

    wc_get_formatted_cart_item_data() validated each variation value only
    after get_term_by() and the woocommerce_variation_option_name filter
    had already received it. get_term_by() casts slug lookups to string,
    so an array value raised a warning and an object fataled before the
    guard could run. Cart contents can be reshaped by filters, and the
    checkout side already treats non-array variation data as empty.

    Check the raw value at the top of the loop and skip anything that is
    not a non-empty scalar. The post-filter check stays because the option
    filter can still return an unrenderable value.

    Refs #36538

    * test: simplify the selected Any variation tests

    WP_UnitTestCase backs up all hooks in set_up() and restores them in
    tear_down(), so the remove_filter() calls these tests wrapped in
    try/finally blocks were redundant and made the files inconsistent with
    each other. Several tests also registered filters their code path
    never reached, which implied coverage that did not exist, and one
    persisted a fixture it never cleaned up.

    Drop the hook cleanup and the inert filters, rename the affected tests
    after the behaviour they actually verify, build the swapped product in
    memory, and explain why a custom-attribute filter sees
    "pa_attribute_finish". The malformed-variation checkout test stays: it
    is what guards the array normalisation in front of
    SelectedVariationName::get_product_name(), which otherwise throws a
    TypeError on non-array cart data.

    Refs #36538

    * chore: note the order meta dedupe fix in the changelog

    get_formatted_meta_data() now compares entity-decoded values, which
    also stops attribute values containing characters such as "&" from
    being repeated as a meta row on existing orders. That is a visible
    change to historic orders and belongs in the release notes.

    Refs #36538

    * test: clear planted shipping session data after the cart session test

    test_setting_session_should_not_clear_shipping_data_when_cart_is_not_empty
    stores placeholder arrays under shipping_for_package_0/1 and never
    removes them. WC_Cart_Session only clears those keys through a
    persisted WC_Session_Handler, which the test session handlers never
    provide, so the placeholders outlive the test. Any later test that
    calculates shipping then fails inside WC_Shipping with
    "Undefined array key package_hash", which add_to_cart() swallows into a
    false return. Default order hides this because intermediate tests
    overwrite the keys; random order (as used by infection's initial run)
    exposes it.

    Null the five planted keys once the assertions are done.

    Refs #36538

    * chore: describe only user-visible changes in the changelog

    The entry listed the new WC_Cart::get_item_product_name() method, the
    added wc_get_formatted_cart_item_data() parameter, and the overridden
    template caveat. Changelog entries feed the release notes, where that
    implementation detail tells a merchant nothing about what changed for
    them.

    Describe the visible behaviour instead. Keep "classic" on the cart and
    checkout displays, since the blocks Cart and Checkout render names from
    Store API responses and are unchanged, and keep "on existing orders",
    since get_formatted_meta_data() runs at display time. The template
    override caveat stays in the PR description's backward compatibility
    section.

    Refs #36538

    * fix: decode URL-encoded variation values in cart item metadata

    wc_get_formatted_cart_item_data() compared and displayed the raw
    variation value while decoding only HTML entities. Variation titles
    come from wc_get_formatted_variation(), which rawurldecodes, so a
    stored attribute value carrying a percent escape never matched the
    title. It was rendered a second time as a metadata row, and rendered
    encoded: "Finish: Black%20White" sitting under a title reading
    "Black White".

    This reaches the cart when a fixed attribute holds such a value and
    the item is added by variation ID without posted attributes, so the
    value comes from the variation's own data rather than through
    wc_clean(), which strips percent escapes before validation.

    Decode the value after the term lookup and the option filter, before
    both the title comparison and the metadata row, so the cart normalises
    it the way the title already does. Callbacks on
    woocommerce_get_item_data now receive decoded values; only values
    containing percent escapes change.

    Refs #36538

    * chore: cover cart metadata dedupe in the changelog

    The entry described the duplicate metadata fix as applying to order
    item names only, but the same repeated row appears in the cart. Widen
    the wording so the release notes cover both.

    Refs #36538

    * test: clear planted shipping session data in teardown

    The cart session test nulled the shipping keys it planted only after
    its own assertions, so a failing assertion skipped the cleanup.

    Verified with a probe pair: shipping_for_package_0 does survive a
    failed test, because the parent teardown never removes it —
    WC_Shipping::reset_shipping() clears only chosen_shipping_methods. The
    planted placeholder rates then make later shipping calculations fail on
    a missing package hash. Two sibling tests in the file plant the same
    keys and share the gap.

    Move the cleanup into the class teardown, which runs whatever the
    assertions do, and drop the now-redundant inline block.

    Refs #36538

    ---------

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

diff --git a/plugins/woocommerce/changelog/36538-selected-any-variation-names b/plugins/woocommerce/changelog/36538-selected-any-variation-names
new file mode 100644
index 00000000000..29961f046ea
--- /dev/null
+++ b/plugins/woocommerce/changelog/36538-selected-any-variation-names
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Include selected "Any" variation values in classic cart and checkout displays and in new order item names, and stop variation values already shown in an item name from being repeated as cart or order metadata, including on existing orders.
diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php
index ac2f70399dc..4e8ff09e292 100644
--- a/plugins/woocommerce/includes/class-wc-cart.php
+++ b/plugins/woocommerce/includes/class-wc-cart.php
@@ -13,6 +13,7 @@ use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
 use Automattic\WooCommerce\Enums\ProductStatus;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Enums\TaxDisplayMode;
+use Automattic\WooCommerce\Internal\ProductVariations\SelectedVariationName;
 use Automattic\WooCommerce\Internal\Tax\TaxRateDataStore;
 use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
 use Automattic\WooCommerce\Utilities\DiscountsUtil;
@@ -939,6 +940,35 @@ class WC_Cart extends WC_Legacy_Cart {
 		return wc_get_formatted_cart_item_data( $cart_item, $flat );
 	}

+	/**
+	 * Gets the display name for a cart item.
+	 *
+	 * For variations, selected "Any" attribute values that are missing from the
+	 * stored variation name are appended so the name matches fully defined
+	 * variations. The stored product and variation names are not modified.
+	 *
+	 * @since 11.2.0
+	 * @param array           $cart_item Cart item.
+	 * @param WC_Product|null $product   Optional product object to use as the name source,
+	 *                                   e.g. the result of the `woocommerce_cart_item_product` filter.
+	 *                                   Defaults to the cart item's product.
+	 * @return string The product name including any selected "Any" attribute values,
+	 *                or an empty string when no product can be resolved from the arguments.
+	 */
+	public function get_item_product_name( $cart_item, $product = null ) {
+		if ( ! $product instanceof WC_Product ) {
+			$product = is_array( $cart_item ) && isset( $cart_item['data'] ) && $cart_item['data'] instanceof WC_Product ? $cart_item['data'] : null;
+		}
+
+		if ( ! $product instanceof WC_Product ) {
+			return '';
+		}
+
+		$variation = isset( $cart_item['variation'] ) && is_array( $cart_item['variation'] ) ? $cart_item['variation'] : array();
+
+		return wc_get_container()->get( SelectedVariationName::class )->get_product_name( $product, $variation, true );
+	}
+
 	/**
 	 * Gets cross sells based on the items in the cart.
 	 *
diff --git a/plugins/woocommerce/includes/class-wc-checkout.php b/plugins/woocommerce/includes/class-wc-checkout.php
index 142a1c32ee2..418d0e7d579 100644
--- a/plugins/woocommerce/includes/class-wc-checkout.php
+++ b/plugins/woocommerce/includes/class-wc-checkout.php
@@ -12,6 +12,7 @@ use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\WooCommerce\Enums\ProductTaxStatus;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareTrait;
+use Automattic\WooCommerce\Internal\ProductVariations\SelectedVariationName;
 use Automattic\WooCommerce\Internal\Tax\TaxRateDataStore;

 defined( 'ABSPATH' ) || exit;
@@ -564,6 +565,8 @@ class WC_Checkout {
 	 */
 	public function create_order_line_items( &$order, $cart ) {
 		foreach ( $cart->get_cart() as $cart_item_key => $values ) {
+			$variation = is_array( $values['variation'] ?? null ) ? $values['variation'] : array();
+
 			/**
 			 * Filter hook to get initial item object.
 			 *
@@ -576,7 +579,7 @@ class WC_Checkout {
 			$item->set_props(
 				array(
 					'quantity'     => $values['quantity'],
-					'variation'    => $values['variation'],
+					'variation'    => $variation,
 					'subtotal'     => $values['line_subtotal'],
 					'total'        => $values['line_total'],
 					'subtotal_tax' => $values['line_subtotal_tax'],
@@ -590,7 +593,7 @@ class WC_Checkout {
 			if ( $product ) {
 				$item->set_props(
 					array(
-						'name'         => $product->get_name(),
+						'name'         => wc_get_container()->get( SelectedVariationName::class )->get_product_name( $product, $variation ),
 						'tax_class'    => $product->get_tax_class(),
 						'product_id'   => $product->is_type( ProductType::VARIATION ) ? $product->get_parent_id() : $product->get_id(),
 						'variation_id' => $product->is_type( ProductType::VARIATION ) ? $product->get_id() : 0,
diff --git a/plugins/woocommerce/includes/class-wc-order-item.php b/plugins/woocommerce/includes/class-wc-order-item.php
index 22858fcd59a..62d3450793c 100644
--- a/plugins/woocommerce/includes/class-wc-order-item.php
+++ b/plugins/woocommerce/includes/class-wc-order-item.php
@@ -339,6 +339,10 @@ class WC_Order_Item extends WC_Data implements ArrayAccess {
 		$product           = is_callable( array( $this, 'get_product' ) ) ? $this->get_product() : false;
 		$order_item_name   = $this->get_name();

+		// Compare entity-decoded copies below: wp_kses_post() normalizes special characters
+		// in display values (e.g. "&" to "&amp;") while the item name stores them raw.
+		$decoded_item_name = ! $include_all && $product && $product->is_type( ProductType::VARIATION ) ? wp_specialchars_decode( $order_item_name, ENT_QUOTES ) : null;
+
 		foreach ( $meta_data as $meta ) {
 			if ( empty( $meta->id ) || '' === $meta->value || ! is_scalar( $meta->value ) || ( $hideprefix_length && substr( $meta->key, 0, $hideprefix_length ) === $hideprefix ) ) {
 				continue;
@@ -358,7 +362,7 @@ class WC_Order_Item extends WC_Data implements ArrayAccess {
 			}

 			// Skip items with values already in the product details area of the product name.
-			if ( ! $include_all && $product && $product->is_type( ProductType::VARIATION ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) {
+			if ( null !== $decoded_item_name && wc_is_attribute_in_product_name( wp_specialchars_decode( $display_value, ENT_QUOTES ), $decoded_item_name ) ) {
 				continue;
 			}

diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
index f9ec70e6246..e23406c1a56 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
@@ -294,6 +294,42 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
 	 * @return string
 	 */
 	protected function generate_product_title( $product ) {
+		$should_include_attributes = $this->should_include_attributes_in_title( $product );
+
+		/**
+		 * Filters the separator used between a variation product title and its attributes.
+		 *
+		 * @since 3.0.2
+		 * @param string     $separator Separator between the product title and attributes.
+		 * @param WC_Product $product Variation product object.
+		 */
+		$separator    = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product );
+		$title_base   = get_post_field( 'post_title', $product->get_parent_id() );
+		$title_suffix = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : '';
+
+		/**
+		 * Filters the generated variation product title.
+		 *
+		 * @since 3.0.0
+		 * @param string     $title Generated variation title.
+		 * @param WC_Product $product Variation product object.
+		 * @param string     $title_base Parent product title.
+		 * @param string     $title_suffix Formatted attribute values, or an empty string.
+		 */
+		return apply_filters( 'woocommerce_product_variation_title', $title_suffix ? $title_base . $separator . $title_suffix : $title_base, $product, $title_base, $title_suffix );
+	}
+
+	/**
+	 * Checks whether a variation title should include its attribute values.
+	 *
+	 * Shared by the stored variation title and the contextual names built for
+	 * selected "Any" attributes, so both follow the same rules.
+	 *
+	 * @since 11.2.0
+	 * @param WC_Product $product Variation product object.
+	 * @return bool
+	 */
+	public function should_include_attributes_in_title( $product ): bool {
 		$attributes = (array) $product->get_attributes();

 		// Do not include attributes if the product has 3+ attributes.
@@ -310,12 +346,14 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
 			}
 		}

-		$should_include_attributes = apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product );
-		$separator                 = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product );
-		$title_base                = get_post_field( 'post_title', $product->get_parent_id() );
-		$title_suffix              = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : '';
-
-		return apply_filters( 'woocommerce_product_variation_title', $title_suffix ? $title_base . $separator . $title_suffix : $title_base, $product, $title_base, $title_suffix );
+		/**
+		 * Filters whether variation product titles should include attributes.
+		 *
+		 * @since 3.0.2
+		 * @param bool       $should_include_attributes Whether attributes should be included.
+		 * @param WC_Product $product Variation product object.
+		 */
+		return (bool) apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product );
 	}

 	/**
diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php
index 0dd33771932..dfc7bfdca9a 100644
--- a/plugins/woocommerce/includes/wc-template-functions.php
+++ b/plugins/woocommerce/includes/wc-template-functions.php
@@ -4519,17 +4519,27 @@ function wc_get_theme_slug_for_templates() {
  * Gets and formats a list of cart item data + variations for display on the frontend.
  *
  * @since 3.3.0
- * @param array $cart_item Cart item object.
- * @param bool  $flat Should the data be returned flat or in a list.
+ * @since 11.2.0 Added the `$product_name` parameter.
+ * @param array       $cart_item   Cart item object.
+ * @param bool        $flat        Should the data be returned flat or in a list.
+ * @param string|null $product_name Product name displayed for the cart item, used to avoid
+ *                                  duplicating variation attributes. Defaults to the stored product name.
  * @return string
  */
-function wc_get_formatted_cart_item_data( $cart_item, $flat = false ) {
+function wc_get_formatted_cart_item_data( $cart_item, $flat = false, $product_name = null ) {
 	$item_data = array();

 	// Variation values are shown only if they are not found in the title as of 3.0.
 	// This is because variation titles display the attributes.
 	if ( $cart_item['data']->is_type( ProductType::VARIATION ) && is_array( $cart_item['variation'] ) ) {
+		$product_name = is_string( $product_name ) ? $product_name : $cart_item['data']->get_name();
+		$product_name = wp_specialchars_decode( wp_strip_all_tags( $product_name ), ENT_QUOTES );
+
 		foreach ( $cart_item['variation'] as $name => $value ) {
+			if ( ! is_scalar( $value ) || '' === (string) $value ) {
+				continue;
+			}
+			$value    = (string) $value;
 			$taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $name ) ) );

 			if ( taxonomy_exists( $taxonomy ) ) {
@@ -4545,8 +4555,18 @@ function wc_get_formatted_cart_item_data( $cart_item, $flat = false ) {
 				$label = wc_attribute_label( str_replace( 'attribute_', '', $name ), $cart_item['data'] );
 			}

-			// Check the nicename against the title.
-			if ( '' === $value || wc_is_attribute_in_product_name( $value, $cart_item['data']->get_name() ) ) {
+			// The option-name filter can return anything; skip values that cannot be rendered.
+			if ( ! is_scalar( $value ) || '' === (string) $value ) {
+				continue;
+			}
+
+			// Variation values can be stored URL-encoded, while variation titles are built
+			// from decoded values by wc_get_formatted_variation(). Decode here so an encoded
+			// value is matched against the title and displayed the same way the title shows it.
+			$value = rawurldecode( (string) $value );
+
+			// Check the display value against the title.
+			if ( wc_is_attribute_in_product_name( wp_specialchars_decode( $value, ENT_QUOTES ), $product_name ) ) {
 				continue;
 			}

diff --git a/plugins/woocommerce/src/Internal/ProductVariations/SelectedVariationName.php b/plugins/woocommerce/src/Internal/ProductVariations/SelectedVariationName.php
new file mode 100644
index 00000000000..51bffe55e65
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/ProductVariations/SelectedVariationName.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Contextual variation product names.
+ *
+ * @package WooCommerce\Internal\ProductVariations
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\ProductVariations;
+
+use Automattic\WooCommerce\Enums\ProductType;
+use WC_Data_Store;
+use WC_Product;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Builds variation names from selected cart and checkout attributes.
+ *
+ * @internal
+ *
+ * @since 11.2.0
+ */
+class SelectedVariationName {
+
+	/**
+	 * Gets a variation product name using selected variation attributes from cart or checkout context.
+	 *
+	 * @param WC_Product           $product              Variation product.
+	 * @param array<string, mixed> $variation_attributes Selected variation attributes.
+	 * @param bool                 $filter_custom_attributes Whether to filter custom attribute values for cart display.
+	 * @return string
+	 *
+	 * @since 11.2.0
+	 */
+	public function get_product_name( WC_Product $product, array $variation_attributes, bool $filter_custom_attributes = false ): string {
+		if ( ! $product->is_type( ProductType::VARIATION ) || empty( $variation_attributes ) ) {
+			return $product->get_name();
+		}
+
+		$product_name       = $product->get_name();
+		$product_attributes = (array) $product->get_attributes();
+
+		if ( ! in_array( '', $product_attributes, true ) ) {
+			return $product_name;
+		}
+
+		$data_store = $product->get_data_store();
+
+		// Stores without the shared title policy keep the stored variation name.
+		if ( ! $data_store instanceof WC_Data_Store || ! $data_store->has_callable( 'should_include_attributes_in_title' ) ) {
+			return $product_name;
+		}
+
+		// @phpstan-ignore method.notFound (the call is proxied by WC_Data_Store::__call() and guarded by has_callable() above)
+		if ( ! $data_store->should_include_attributes_in_title( $product ) ) {
+			return $product_name;
+		}
+
+		$selected_attributes = array();
+
+		foreach ( $variation_attributes as $name => $value ) {
+			$selected_attributes[ str_replace( 'attribute_', '', rawurldecode( (string) $name ) ) ] = $value;
+		}
+
+		$missing_attributes     = array();
+		$missing_display        = array();
+		$has_attributes_in_name = false;
+
+		foreach ( $product_attributes as $name => $value ) {
+			if ( ! array_key_exists( $name, $selected_attributes ) || ! is_scalar( $selected_attributes[ $name ] ) ) {
+				continue;
+			}
+
+			$selected_value = (string) $selected_attributes[ $name ];
+			$display_value  = wc_get_formatted_variation( array( $name => $selected_value ), true, false );
+
+			if ( '' === $display_value ) {
+				continue;
+			}
+
+			if ( wc_is_attribute_in_product_name( $display_value, $product_name ) ) {
+				$has_attributes_in_name = true;
+				continue;
+			}
+
+			if ( '' !== (string) $value ) {
+				continue;
+			}
+
+			$missing_attributes[ $name ] = $selected_value;
+
+			if ( $filter_custom_attributes && ! taxonomy_exists( $name ) ) {
+				/**
+				 * Filters the display name for a selected variation option.
+				 *
+				 * @since 3.4.0
+				 * @param string          $value Selected variation option value.
+				 * @param WP_Term|null    $term Term object when available.
+				 * @param string          $name Attribute taxonomy name.
+				 * @param WC_Product|null $product Product object.
+				 */
+				$display_value = apply_filters( 'woocommerce_variation_option_name', $selected_value, null, wc_attribute_taxonomy_name( 'attribute_' . $name ), $product );
+				$display_value = is_scalar( $display_value ) ? rawurldecode( (string) $display_value ) : '';
+			}
+
+			if ( '' !== $display_value ) {
+				$missing_display[] = $display_value;
+			}
+		}
+
+		if ( empty( $missing_attributes ) ) {
+			return $product_name;
+		}
+
+		if ( $has_attributes_in_name ) {
+			$separator = ', ';
+		} else {
+			/**
+			 * Filters the separator used between a variation product title and its attributes.
+			 *
+			 * @since 3.0.2
+			 * @param string     $separator Separator between the product title and attributes.
+			 * @param WC_Product $product Variation product object.
+			 */
+			$separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product );
+			$separator = is_scalar( $separator ) ? (string) $separator : ' - ';
+		}
+
+		$missing_values = implode( ', ', $missing_display );
+
+		if ( '' === $missing_values ) {
+			return $product_name;
+		}
+
+		return $product_name . $separator . $missing_values;
+	}
+}
diff --git a/plugins/woocommerce/templates/cart/cart.php b/plugins/woocommerce/templates/cart/cart.php
index 927ec42ee94..45bcd8f3cc7 100644
--- a/plugins/woocommerce/templates/cart/cart.php
+++ b/plugins/woocommerce/templates/cart/cart.php
@@ -12,7 +12,7 @@
  *
  * @see     https://woocommerce.com/document/template-structure/
  * @package WooCommerce\Templates
- * @version 11.0.0
+ * @version 11.2.0
  */

 defined( 'ABSPATH' ) || exit;
@@ -52,6 +52,8 @@ do_action( 'woocommerce_before_cart' ); ?>
 				$visible = apply_filters( 'woocommerce_cart_item_visible', true, $cart_item, $cart_item_key );

 				if ( $_product instanceof WC_Product && $_product->exists() && $cart_item['quantity'] > 0 && $visible ) {
+					$cart_item_name = WC()->cart->get_item_product_name( $cart_item, $_product );
+
 					/**
 					 * Filter the product name.
 					 *
@@ -60,7 +62,7 @@ do_action( 'woocommerce_before_cart' ); ?>
 					 * @param array $cart_item The product in the cart.
 					 * @param string $cart_item_key Key for the product in the cart.
 					 */
-					$product_name      = apply_filters( 'woocommerce_cart_item_name', $_product->get_name(), $cart_item, $cart_item_key );
+					$product_name      = apply_filters( 'woocommerce_cart_item_name', $cart_item_name, $cart_item, $cart_item_key );
 					$product_permalink = apply_filters( 'woocommerce_cart_item_permalink', $_product->is_visible() ? $_product->get_permalink( $cart_item ) : '', $cart_item, $cart_item_key );
 					?>
 					<tr class="woocommerce-cart-form__cart-item <?php echo esc_attr( apply_filters( 'woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key ) ); ?>">
@@ -117,13 +119,14 @@ do_action( 'woocommerce_before_cart' ); ?>
 							 *
 							 * @since 2.1.0
 							 */
-							echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', sprintf( '<a href="%s">%s</a>', esc_url( $product_permalink ), $_product->get_name() ), $cart_item, $cart_item_key ) );
+							echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', sprintf( '<a href="%s">%s</a>', esc_url( $product_permalink ), $cart_item_name ), $cart_item, $cart_item_key ) );
 						}

 						do_action( 'woocommerce_after_cart_item_name', $cart_item, $cart_item_key );

 						// Meta data.
-						echo wc_get_formatted_cart_item_data( $cart_item ); // PHPCS: XSS ok.
+						// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+						echo wc_get_formatted_cart_item_data( $cart_item, false, $cart_item_name );

 						// Backorder notification.
 						if ( $_product->backorders_require_notification() && $_product->is_on_backorder( $cart_item['quantity'] ) ) {
diff --git a/plugins/woocommerce/templates/cart/mini-cart.php b/plugins/woocommerce/templates/cart/mini-cart.php
index af1c260d2da..598cff58c31 100644
--- a/plugins/woocommerce/templates/cart/mini-cart.php
+++ b/plugins/woocommerce/templates/cart/mini-cart.php
@@ -14,7 +14,7 @@
  *
  * @see     https://woocommerce.com/document/template-structure/
  * @package WooCommerce\Templates
- * @version 11.0.0
+ * @version 11.2.0
  */

 defined( 'ABSPATH' ) || exit;
@@ -42,12 +42,13 @@ do_action( 'woocommerce_before_mini_cart' ); ?>
 			$visible = apply_filters( 'woocommerce_widget_cart_item_visible', true, $cart_item, $cart_item_key );

 			if ( $_product instanceof WC_Product && $_product->exists() && $cart_item['quantity'] > 0 && $visible ) {
+				$cart_item_name = WC()->cart->get_item_product_name( $cart_item, $_product );
 				/**
 				 * This filter is documented in woocommerce/templates/cart/cart.php.
 				 *
 				 * @since 2.1.0
 				 */
-				$product_name      = apply_filters( 'woocommerce_cart_item_name', $_product->get_name(), $cart_item, $cart_item_key );
+				$product_name      = apply_filters( 'woocommerce_cart_item_name', $cart_item_name, $cart_item, $cart_item_key );
 				$thumbnail         = apply_filters( 'woocommerce_cart_item_thumbnail', $_product->get_image(), $cart_item, $cart_item_key );
 				$product_price     = apply_filters( 'woocommerce_cart_item_price', WC()->cart->get_product_price( $_product ), $cart_item, $cart_item_key );
 				$product_permalink = apply_filters( 'woocommerce_cart_item_permalink', $_product->is_visible() ? $_product->get_permalink( $cart_item ) : '', $cart_item, $cart_item_key );
@@ -77,7 +78,7 @@ do_action( 'woocommerce_before_mini_cart' ); ?>
 							<?php echo $thumbnail . wp_kses_post( $product_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 						</a>
 					<?php endif; ?>
-					<?php echo wc_get_formatted_cart_item_data( $cart_item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
+					<?php echo wc_get_formatted_cart_item_data( $cart_item, false, $cart_item_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 					<?php echo apply_filters( 'woocommerce_widget_cart_item_quantity', '<span class="quantity">' . sprintf( '%s &times; %s', $cart_item['quantity'], $product_price ) . '</span>', $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 				</li>
 				<?php
diff --git a/plugins/woocommerce/templates/checkout/review-order.php b/plugins/woocommerce/templates/checkout/review-order.php
index 08a3459d057..c297b8f1a1f 100644
--- a/plugins/woocommerce/templates/checkout/review-order.php
+++ b/plugins/woocommerce/templates/checkout/review-order.php
@@ -12,7 +12,7 @@
  *
  * @see https://woocommerce.com/document/template-structure/
  * @package WooCommerce\Templates
- * @version 11.0.0
+ * @version 11.2.0
  */

 defined( 'ABSPATH' ) || exit;
@@ -45,9 +45,18 @@ defined( 'ABSPATH' ) || exit;
 				?>
 				<tr class="<?php echo esc_attr( apply_filters( 'woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key ) ); ?>">
 					<td class="product-name">
-						<?php echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', $_product->get_name(), $cart_item, $cart_item_key ) ) . '&nbsp;'; ?>
+						<?php
+						$cart_item_name = WC()->cart->get_item_product_name( $cart_item, $_product );
+						/**
+						 * This filter is documented in woocommerce/templates/cart/cart.php.
+						 *
+						 * @since 2.1.0
+						 */
+						$product_name = apply_filters( 'woocommerce_cart_item_name', $cart_item_name, $cart_item, $cart_item_key );
+						?>
+						<?php echo wp_kses_post( $product_name ) . '&nbsp;'; ?>
 						<?php echo apply_filters( 'woocommerce_checkout_cart_item_quantity', ' <strong class="product-quantity">' . sprintf( '&times;&nbsp;%s', $cart_item['quantity'] ) . '</strong>', $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
-						<?php echo wc_get_formatted_cart_item_data( $cart_item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
+						<?php echo wc_get_formatted_cart_item_data( $cart_item, false, $cart_item_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 					</td>
 					<td class="product-total">
 						<?php echo apply_filters( 'woocommerce_cart_item_subtotal', WC()->cart->get_product_subtotal( $_product, $cart_item['quantity'] ), $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
index d8c13e7df67..fbcfb8cb46c 100644
--- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
+++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
@@ -444,4 +444,40 @@ class WC_Helper_Product {
 	public static function save_post_test_update_meta_data_direct( $id ) {
 		update_post_meta( $id, '_test2', 'world' );
 	}
+
+	/**
+	 * Creates a variable product with global attributes and a single variation.
+	 *
+	 * Variation attribute values may be term slugs or empty strings ("Any" attributes).
+	 *
+	 * @param string $product_name         Product name.
+	 * @param array  $variation_attributes Variation attributes, e.g. array( 'pa_size' => 'huge', 'pa_number' => '' ).
+	 * @param array  $attribute_terms      Attribute terms keyed by raw attribute name (without 'pa_' prefix).
+	 *
+	 * @return array The variable product and its variation: array( WC_Product_Variable, WC_Product_Variation ).
+	 */
+	public static function create_variation_product_with_global_attributes( $product_name, $variation_attributes, $attribute_terms = array(
+		'size'   => array( 'small', 'huge' ),
+		'number' => array( '0', '1' ),
+	) ) {
+		$product = new WC_Product_Variable();
+		$product->set_name( $product_name );
+
+		$attributes = array();
+		foreach ( $attribute_terms as $attribute_name => $terms ) {
+			$attributes[] = self::create_product_attribute_object( $attribute_name, $terms );
+		}
+
+		$product->set_attributes( $attributes );
+		$product->save();
+
+		$variation = self::create_product_variation_object(
+			$product->get_id(),
+			$product_name . ' variation',
+			10,
+			$variation_attributes
+		);
+
+		return array( $product, wc_get_product( $variation->get_id() ) );
+	}
 }
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
index 5f519de19a6..8ad1c601490 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
@@ -39,6 +39,14 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
 		WC()->customer->set_is_vat_exempt( false );
 		WC()->session->set( 'wc_notices', null );

+		// The parent teardown only clears chosen_shipping_methods, through
+		// WC_Shipping::reset_shipping(). Planted shipping_for_package_* rates survive and
+		// make later shipping calculations fail on a missing package hash, so clear them
+		// here, where a failing assertion cannot skip it.
+		foreach ( array( 'shipping_method_counts', 'previous_shipping_methods', 'shipping_for_package_0', 'shipping_for_package_1', 'chosen_shipping_methods' ) as $key ) {
+			WC()->session->set( $key, null );
+		}
+
 		remove_filter( 'woocommerce_add_to_cart_quantity', array( $this, 'capture_add_to_cart_quantity_filter_args' ), 10 );
 	}

@@ -279,6 +287,370 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
 		$product->delete( true );
 	}

+	/**
+	 * @testdox Cart item product names include selected Any variation attributes.
+	 *
+	 * @dataProvider selected_any_variation_name_provider
+	 *
+	 * @param string                $product_name      Product name.
+	 * @param array<string, string> $stored_attributes Stored variation attributes.
+	 * @param string                $expected_name     Expected contextual cart item name.
+	 */
+	public function test_cart_item_product_name_includes_selected_any_variation_attributes( string $product_name, array $stored_attributes, string $expected_name ): void {
+		list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes( $product_name, $stored_attributes );
+		$option_filter_calls         = 0;
+		$option_filter               = function ( $value ) use ( &$option_filter_calls ) {
+			++$option_filter_calls;
+
+			return 'Filtered ' . $value;
+		};
+		add_filter( 'woocommerce_variation_option_name', $option_filter );
+
+		try {
+			list( , $cart_item ) = $this->add_variation_to_cart( $product, $variation );
+			$name                = WC()->cart->get_item_product_name( $cart_item );
+
+			$this->assertSame( $expected_name, $name );
+			$this->assertSame( '', trim( wc_get_formatted_cart_item_data( $cart_item, true, $name ) ) );
+			$this->assertSame( 0, $option_filter_calls );
+		} finally {
+			$variation->delete( true );
+			$product->delete( true );
+		}
+	}
+
+	/**
+	 * Provides stored variation attribute shapes for contextual name cases.
+	 *
+	 * @return array<string, array{string, array<string, string>, string}>
+	 */
+	public static function selected_any_variation_name_provider(): array {
+		return array(
+			'one fixed, one Any' => array(
+				'Cart Any Product',
+				array(
+					'pa_size'   => 'huge',
+					'pa_number' => '',
+				),
+				'Cart Any Product - huge, 1',
+			),
+			'all Any'            => array(
+				'shirt',
+				array(
+					'pa_size'   => '',
+					'pa_number' => '',
+				),
+				'shirt - huge, 1',
+			),
+		);
+	}
+
+	/**
+	 * @testdox Cart item product names honor swapped product objects and non-variation items.
+	 */
+	public function test_cart_item_product_name_honors_swapped_products_and_non_variations(): void {
+		$simple = WC_Helper_Product::create_simple_product();
+
+		try {
+			$cart_item = array(
+				'data'      => $simple,
+				'variation' => array(),
+			);
+
+			$this->assertSame( $simple->get_name(), WC()->cart->get_item_product_name( $cart_item ) );
+
+			$swapped = new WC_Product_Simple();
+			$swapped->set_name( 'Swapped Display Product' );
+
+			$this->assertSame( 'Swapped Display Product', WC()->cart->get_item_product_name( $cart_item, $swapped ) );
+			$this->assertSame( '', WC()->cart->get_item_product_name( array() ) );
+		} finally {
+			$simple->delete( true );
+		}
+	}
+
+	/**
+	 * @testdox Cart item names preserve filtered custom Any attribute labels without duplicate metadata.
+	 */
+	public function test_cart_item_name_preserves_filtered_custom_any_attribute_labels(): void {
+		$variation = new WC_Product_Variation();
+		$variation->set_name( 'Custom Any Product' );
+		$variation->set_attributes( array( 'finish' => '' ) );
+
+		// For custom attributes, core passes wc_attribute_taxonomy_name( 'attribute_finish' ) as the
+		// attribute name, so the filter sees "pa_attribute_finish" rather than "finish".
+		$filter_option_name = function ( $value, $term, $attribute_name ) {
+			unset( $term );
+
+			return 'pa_attribute_finish' === $attribute_name && 'gloss' === $value ? 'Polished' : $value;
+		};
+		add_filter( 'woocommerce_variation_option_name', $filter_option_name, 10, 3 );
+
+		$cart_item = array(
+			'data'      => $variation,
+			'variation' => array( 'attribute_finish' => 'gloss' ),
+		);
+
+		$rendered_name = WC()->cart->get_item_product_name( $cart_item );
+
+		$this->assertSame( 'Custom Any Product - Polished', $rendered_name );
+		$this->assertSame( '', trim( wc_get_formatted_cart_item_data( $cart_item, true, $rendered_name ) ) );
+
+		$cart_item['variation']['attribute_finish'] = 'Black & White';
+		$this->assertSame( '', trim( wc_get_formatted_cart_item_data( $cart_item, true, 'Custom Any Product - Black &amp; White' ) ) );
+
+		$variation->set_name( 'Custom Any Product - Black & White' );
+		$this->assertSame( '', trim( wc_get_formatted_cart_item_data( $cart_item, true, false ) ) );
+	}
+
+	/**
+	 * @testdox Two-argument cart item formatting preserves selected Any variation metadata.
+	 */
+	public function test_formatted_cart_item_data_preserves_selected_any_value_when_product_name_is_omitted(): void {
+		$variation = new WC_Product_Variation();
+		$variation->set_name( 'Legacy Any Product' );
+		$variation->set_attributes( array( 'finish' => '' ) );
+		$option_filter_calls    = 0;
+		$option_filter          = function () use ( &$option_filter_calls ) {
+			++$option_filter_calls;
+
+			return 'Filtered ' . $option_filter_calls;
+		};
+		$attribute_filter_calls = 0;
+		$attribute_filter       = function ( $is_in_name ) use ( &$attribute_filter_calls ) {
+			++$attribute_filter_calls;
+
+			return $is_in_name;
+		};
+		add_filter( 'woocommerce_variation_option_name', $option_filter );
+		add_filter( 'woocommerce_is_attribute_in_product_name', $attribute_filter );
+
+		$cart_item = array(
+			'data'      => $variation,
+			'variation' => array( 'attribute_finish' => 'Black%20White' ),
+		);
+
+		$this->assertSame( 'finish: Filtered 1', trim( wc_get_formatted_cart_item_data( $cart_item, true ) ) );
+		$this->assertSame( 1, $option_filter_calls );
+		$this->assertSame( 1, $attribute_filter_calls );
+	}
+
+	/**
+	 * @testdox Cart item formatting omits custom Any metadata when filtered display values cannot be rendered.
+	 * @dataProvider unrenderable_variation_option_label_provider
+	 *
+	 * @param mixed $filtered_value Filtered variation option label.
+	 */
+	public function test_formatted_cart_item_data_omits_unrenderable_custom_any_metadata( $filtered_value ): void {
+		$variation = new WC_Product_Variation();
+		$variation->set_name( 'Unrenderable Any Product' );
+		$variation->set_attributes( array( 'finish' => '' ) );
+
+		$option_filter = static function () use ( $filtered_value ) {
+			return $filtered_value;
+		};
+		add_filter( 'woocommerce_variation_option_name', $option_filter );
+
+		$cart_item = array(
+			'data'      => $variation,
+			'variation' => array( 'attribute_finish' => 'gloss' ),
+		);
+
+		$this->assertSame( '', trim( wc_get_formatted_cart_item_data( $cart_item, true ) ) );
+	}
+
+	/**
+	 * Provides filtered variation option labels that cannot be rendered.
+	 *
+	 * @return array<string, array{mixed}>
+	 */
+	public static function unrenderable_variation_option_label_provider(): array {
+		return array(
+			'false'            => array( false ),
+			'non-scalar array' => array( array( 'unexpected' ) ),
+		);
+	}
+
+	/**
+	 * @testdox Cart item formatting skips non-scalar variation values before term lookups and option filters.
+	 * @dataProvider non_scalar_variation_value_provider
+	 *
+	 * @param mixed $raw_value Raw cart variation value.
+	 */
+	public function test_formatted_cart_item_data_skips_non_scalar_variation_values( $raw_value ): void {
+		list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+			'Non-scalar Variation Product',
+			array( 'pa_size' => '' )
+		);
+
+		$option_filter_calls = 0;
+		$option_filter       = function ( $value ) use ( &$option_filter_calls ) {
+			++$option_filter_calls;
+
+			return $value;
+		};
+		add_filter( 'woocommerce_variation_option_name', $option_filter );
+
+		$cart_item = array(
+			'data'      => $variation,
+			'variation' => array(
+				'attribute_pa_size' => $raw_value,
+				'attribute_finish'  => $raw_value,
+			),
+		);
+
+		try {
+			$this->assertSame( '', trim( wc_get_formatted_cart_item_data( $cart_item, true ) ) );
+			$this->assertSame( 0, $option_filter_calls );
+		} finally {
+			$variation->delete( true );
+			$product->delete( true );
+		}
+	}
+
+	/**
+	 * Provides non-scalar cart variation values.
+	 *
+	 * @return array<string, array{mixed}>
+	 */
+	public static function non_scalar_variation_value_provider(): array {
+		return array(
+			'array'  => array( array( 'gloss' ) ),
+			'object' => array( new stdClass() ),
+		);
+	}
+
+	/**
+	 * @testdox Cart item formatting decodes taxonomy term entities when checking the rendered product name for duplicate metadata.
+	 */
+	public function test_formatted_cart_item_data_decodes_taxonomy_term_entities_for_name_comparison(): void {
+		$taxonomy = 'pa_encoded_finish';
+		$term     = false;
+
+		register_taxonomy( $taxonomy, array( 'product' ) );
+
+		try {
+			$term = wp_insert_term( 'Black & White', $taxonomy, array( 'slug' => 'black-white' ) );
+			$this->assertNotWPError( $term );
+
+			$variation = new WC_Product_Variation();
+			$variation->set_name( 'Encoded Any Product' );
+			$variation->set_attributes( array( $taxonomy => '' ) );
+
+			$cart_item = array(
+				'data'      => $variation,
+				'variation' => array( 'attribute_' . $taxonomy => 'black-white' ),
+			);
+
+			$this->assertSame( '', trim( wc_get_formatted_cart_item_data( $cart_item, true, 'Encoded Any Product - Black & White' ) ) );
+		} finally {
+			if ( is_array( $term ) ) {
+				wp_delete_term( $term['term_id'], $taxonomy );
+			}
+
+			unregister_taxonomy( $taxonomy );
+		}
+	}
+
+	/**
+	 * @testdox Cart item metadata decodes URL-encoded custom attribute values for the name comparison and for display.
+	 */
+	public function test_formatted_cart_item_data_decodes_url_encoded_custom_values(): void {
+		// A fixed attribute whose stored value carries a percent escape. wc_get_formatted_variation()
+		// decodes it when generating the variation title, so the cart value must be decoded to match.
+		$variation = new WC_Product_Variation();
+		$variation->set_name( 'Encoded Fixed Product - Black White' );
+		$variation->set_attributes( array( 'finish' => 'Black%20White' ) );
+
+		$cart_item = array(
+			'data'      => $variation,
+			'variation' => array( 'attribute_finish' => 'Black%20White' ),
+		);
+
+		$this->assertSame(
+			'',
+			trim( wc_get_formatted_cart_item_data( $cart_item, true, $variation->get_name() ) ),
+			'A decoded value already shown in the name must not be repeated as metadata.'
+		);
+
+		$this->assertSame(
+			'finish: Black White',
+			trim( wc_get_formatted_cart_item_data( $cart_item, true, 'Encoded Fixed Product' ) ),
+			'A value missing from the name must display decoded, matching how the name renders it.'
+		);
+
+		// The same normalisation applies to a selected "Any" value reaching the cart.
+		$any_variation = new WC_Product_Variation();
+		$any_variation->set_name( 'Encoded Any Product' );
+		$any_variation->set_attributes( array( 'finish' => '' ) );
+
+		$any_cart_item = array(
+			'data'      => $any_variation,
+			'variation' => array( 'attribute_finish' => 'Black%20White' ),
+		);
+
+		$this->assertSame( 'Encoded Any Product - Black White', WC()->cart->get_item_product_name( $any_cart_item ) );
+		$this->assertSame( '', trim( wc_get_formatted_cart_item_data( $any_cart_item, true, 'Encoded Any Product - Black White' ) ) );
+	}
+
+	/**
+	 * @testdox Cart item metadata omits fixed taxonomy attributes already shown in the variation name.
+	 */
+	public function test_formatted_cart_item_data_omits_metadata_for_fixed_taxonomy_attributes(): void {
+		list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+			'Cart Fixed Taxonomy Product',
+			array(
+				'pa_size'   => 'huge',
+				'pa_number' => '1',
+			)
+		);
+
+		try {
+			list( , $cart_item ) = $this->add_variation_to_cart( $product, $variation );
+			$this->assertSame( '', trim( wc_get_formatted_cart_item_data( $cart_item, true ) ) );
+		} finally {
+			$variation->delete( true );
+			$product->delete( true );
+		}
+	}
+
+	/**
+	 * @testdox Cart item metadata dedup keys on the template-provided name regardless of name filters.
+	 */
+	public function test_formatted_cart_item_data_dedupes_against_the_provided_name_regardless_of_name_filters(): void {
+		list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+			'Cart Replaced Name Product',
+			array(
+				'pa_size'   => 'huge',
+				'pa_number' => '',
+			)
+		);
+
+		$replace_name = function () {
+			return 'Custom cart label';
+		};
+		add_filter( 'woocommerce_cart_item_name', $replace_name, 20 );
+
+		try {
+			list( $cart_item_key, $cart_item ) = $this->add_variation_to_cart( $product, $variation );
+
+			$name = WC()->cart->get_item_product_name( $cart_item );
+			/**
+			 * This filter is documented in woocommerce/templates/cart/cart.php.
+			 *
+			 * @since 2.1.0
+			 */
+			$rendered_name = apply_filters( 'woocommerce_cart_item_name', $name, $cart_item, (string) $cart_item_key );
+
+			$this->assertSame( 'Custom cart label', $rendered_name );
+			$this->assertSame( 'Cart Replaced Name Product - huge, 1', $name );
+			$this->assertSame( '', trim( wc_get_formatted_cart_item_data( $cart_item, true, $name ) ), 'Dedup must key on the template-provided name, not on name-filter output.' );
+		} finally {
+			$variation->delete( true );
+			$product->delete( true );
+		}
+	}
+
 	/**
 	 * @testdox should throw a notice to the cart if using variation_id
 	 * that doesn't belong to specified variable product.
@@ -1850,4 +2222,49 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
 		$product->delete( true );
 		$coupon->delete( true );
 	}
+
+	/**
+	 * @testdox The mini-cart template renders selected Any values in the name exactly once.
+	 */
+	public function test_mini_cart_template_renders_selected_any_values_once(): void {
+		list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+			'Mini Cart Any Product',
+			array(
+				'pa_size'   => 'huge',
+				'pa_number' => '',
+			)
+		);
+
+		try {
+			$this->add_variation_to_cart( $product, $variation );
+
+			ob_start();
+			woocommerce_mini_cart();
+			$html = ob_get_clean();
+
+			$this->assertStringContainsString( 'Mini Cart Any Product - huge, 1', $html, 'The merged name must render.' );
+			$this->assertStringNotContainsString( '<dl class="variation"', $html, 'No variation meta list must render for values already in the name.' );
+		} finally {
+			$variation->delete( true );
+			$product->delete( true );
+		}
+	}
+
+	/**
+	 * Adds a variation to the cart, asserting success.
+	 *
+	 * @param WC_Product $product    Variable product.
+	 * @param WC_Product $variation  Variation to add.
+	 * @param array      $attributes Selected variation attributes.
+	 * @return array The cart item key and cart item: array( string, array ).
+	 */
+	private function add_variation_to_cart( $product, $variation, array $attributes = array(
+		'attribute_pa_size'   => 'huge',
+		'attribute_pa_number' => '1',
+	) ): array {
+		$cart_item_key = WC()->cart->add_to_cart( $product->get_id(), 1, $variation->get_id(), $attributes );
+		$this->assertNotFalse( $cart_item_key, 'The variation should be added to the cart.' );
+
+		return array( (string) $cart_item_key, WC()->cart->get_cart_item( (string) $cart_item_key ) );
+	}
 }
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
index f6f9f4f01fe..de2b3f92be1 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
@@ -783,4 +783,157 @@ class WC_Checkout_Test extends \WC_Unit_Test_Case {
 		$this->assertSame( 'checkout-error', $result->get_error_code(), 'Error code should come from the checkout try/catch path.' );
 		$this->assertStringContainsString( 'Order items could not be saved', $result->get_error_message(), 'Error message should surface the defense-in-depth guard message.' );
 	}
+
+	/**
+	 * @testdox Checkout tolerates non-array cart variation data when building contextual item names.
+	 */
+	public function test_create_order_line_items_tolerates_malformed_variation_data(): void {
+		list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+			'Checkout Malformed Variation Product',
+			array(
+				'pa_size'   => 'huge',
+				'pa_number' => '',
+			)
+		);
+
+		$order = wc_create_order();
+
+		$malformed_variation_filter = function ( $cart_contents ) {
+			foreach ( $cart_contents as &$cart_item ) {
+				$cart_item['variation'] = 'malformed variation data';
+			}
+			unset( $cart_item );
+
+			return $cart_contents;
+		};
+
+		try {
+			$this->add_variation_to_cart( $product, $variation );
+			add_filter( 'woocommerce_get_cart_contents', $malformed_variation_filter );
+
+			$this->sut->create_order_line_items( $order, WC()->cart );
+			$order->save();
+
+			$items = array_values( wc_get_order( $order->get_id() )->get_items() );
+			$this->assertCount( 1, $items );
+			$this->assertSame( $variation->get_name(), $items[0]->get_name() );
+			$this->assertCount( 0, $items[0]->get_meta_data(), 'Malformed variation data should not be stored as item meta.' );
+		} finally {
+			WC()->cart->empty_cart();
+			$order->delete( true );
+			$variation->delete( true );
+			$product->delete( true );
+		}
+	}
+
+	/**
+	 * @testdox Checkout merges selected taxonomy Any attributes into the item name without duplicating them as metadata.
+	 */
+	public function test_create_order_line_items_merges_taxonomy_any_attributes_and_dedupes_meta(): void {
+		list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+			'Checkout Taxonomy Any Product',
+			array(
+				'pa_size'   => 'huge',
+				'pa_number' => '',
+			)
+		);
+
+		$order = wc_create_order();
+
+		try {
+			$this->add_variation_to_cart( $product, $variation );
+			$this->sut->create_order_line_items( $order, WC()->cart );
+			$order->save();
+
+			$items = array_values( $order->get_items() );
+			$this->assertCount( 1, $items );
+			$this->assertSame( 'Checkout Taxonomy Any Product - huge, 1', $items[0]->get_name() );
+			$this->assertSame( '1', $items[0]->get_meta( 'pa_number' ), 'The selected Any value should remain stored as item meta.' );
+			$this->assertCount( 0, $items[0]->get_formatted_meta_data(), 'Selected Any values included in the item name should not be duplicated as metadata.' );
+		} finally {
+			WC()->cart->empty_cart();
+			$order->delete( true );
+			$variation->delete( true );
+			$product->delete( true );
+		}
+	}
+
+	/**
+	 * @testdox Persisted order item names use raw custom Any values so order meta dedup keeps working.
+	 *
+	 * @dataProvider custom_any_value_provider
+	 *
+	 * @param string $selected_value Selected custom attribute value.
+	 * @param string $expected_name  Expected persisted order item name.
+	 */
+	public function test_create_order_line_items_persists_raw_custom_any_values( string $selected_value, string $expected_name ): void {
+		$attribute = new WC_Product_Attribute();
+		$attribute->set_id( 0 );
+		$attribute->set_name( 'finish' );
+		$attribute->set_options( array( 'gloss', 'matte', 'Black & White' ) );
+		$attribute->set_visible( true );
+		$attribute->set_variation( true );
+
+		$product = new WC_Product_Variable();
+		$product->set_name( 'Canonical Name Product' );
+		$product->set_attributes( array( $attribute ) );
+		$product->save();
+
+		$variation = new WC_Product_Variation();
+		$variation->set_parent_id( $product->get_id() );
+		$variation->set_attributes( array( 'finish' => '' ) );
+		$variation->set_regular_price( '10' );
+		$variation->save();
+
+		$filter_option_name = static function ( $value ) {
+			return 'gloss' === $value ? 'Polished' : $value;
+		};
+		add_filter( 'woocommerce_variation_option_name', $filter_option_name );
+
+		$order = wc_create_order();
+
+		try {
+			$this->add_variation_to_cart( $product, $variation, array( 'attribute_finish' => $selected_value ) );
+			$this->sut->create_order_line_items( $order, WC()->cart );
+			$order->save();
+
+			$items = array_values( $order->get_items() );
+			$this->assertCount( 1, $items );
+			$this->assertSame( $expected_name, $items[0]->get_name(), 'Persisted names must use raw values, not woocommerce_variation_option_name output.' );
+			$this->assertCount( 0, $items[0]->get_formatted_meta_data(), 'The raw value in the name must keep order meta dedup working.' );
+		} finally {
+			WC()->cart->empty_cart();
+			$order->delete( true );
+			$variation->delete( true );
+			$product->delete( true );
+		}
+	}
+
+	/**
+	 * Provides selected custom Any values and their expected persisted names.
+	 *
+	 * @return array<string, array{string, string}>
+	 */
+	public static function custom_any_value_provider(): array {
+		return array(
+			'raw value wins over filtered label' => array( 'gloss', 'Canonical Name Product - gloss' ),
+			'entity-bearing value dedupes'       => array( 'Black & White', 'Canonical Name Product - Black & White' ),
+		);
+	}
+
+	/**
+	 * Adds a variation to the cart, asserting success, and calculates totals.
+	 *
+	 * @param WC_Product $product    Variable product.
+	 * @param WC_Product $variation  Variation to add.
+	 * @param array      $attributes Selected variation attributes.
+	 */
+	private function add_variation_to_cart( $product, $variation, array $attributes = array(
+		'attribute_pa_size'   => 'huge',
+		'attribute_pa_number' => '1',
+	) ): void {
+		$cart_item_key = WC()->cart->add_to_cart( $product->get_id(), 1, $variation->get_id(), $attributes );
+		$this->assertNotFalse( $cart_item_key, 'The variation should be added to the cart.' );
+		WC()->cart->calculate_totals();
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductVariations/SelectedVariationNameTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductVariations/SelectedVariationNameTest.php
new file mode 100644
index 00000000000..ea921e08f9d
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductVariations/SelectedVariationNameTest.php
@@ -0,0 +1,252 @@
+<?php
+/**
+ * SelectedVariationName tests.
+ *
+ * @package WooCommerce\Tests\Internal\ProductVariations
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\ProductVariations;
+
+use Automattic\WooCommerce\Internal\ProductVariations\SelectedVariationName;
+use WC_Data_Store;
+use WC_Helper_Product;
+use WC_Product_Variation;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for SelectedVariationName.
+ */
+class SelectedVariationNameTest extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var SelectedVariationName
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->sut = new SelectedVariationName();
+	}
+
+	/**
+	 * @testdox Selected Any attributes are included contextually without changing the stored variation name.
+	 */
+	public function test_get_product_name_includes_selected_any_attributes(): void {
+		list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+			'Selected Any Product',
+			array(
+				'pa_size'   => 'huge',
+				'pa_number' => '',
+			)
+		);
+		$filter_calls                = 0;
+		$option_filter               = function ( $value ) use ( &$filter_calls ) {
+			++$filter_calls;
+
+			return 'Filtered ' . $value;
+		};
+		add_filter( 'woocommerce_variation_option_name', $option_filter );
+
+		try {
+			$stored_name = $variation->get_name();
+			$name        = $this->sut->get_product_name(
+				$variation,
+				array(
+					'attribute_pa_size'   => 'huge',
+					'attribute_pa_number' => '1',
+				)
+			);
+
+			$this->assertSame( 'Selected Any Product - huge, 1', $name );
+			$this->assertSame( 0, $filter_calls, 'Canonical order values must not use cart option-label filters.' );
+			$this->assertSame( $stored_name, $variation->get_name(), 'The contextual name must not mutate the variation object.' );
+			$this->assertSame( $stored_name, wc_get_product( $variation->get_id() )->get_name(), 'The contextual name must not change the persisted variation.' );
+		} finally {
+			$variation->delete( true );
+			$product->delete( true );
+		}
+	}
+
+	/**
+	 * @testdox Data stores without the shared title policy keep the stored variation name.
+	 */
+	public function test_get_product_name_keeps_stored_name_without_data_store_title_policy(): void {
+		$data_store = $this->getMockBuilder( WC_Data_Store::class )->disableOriginalConstructor()->onlyMethods( array( 'has_callable' ) )->getMock();
+		$data_store->method( 'has_callable' )->with( 'should_include_attributes_in_title' )->willReturn( false );
+
+		$variation = $this->getMockBuilder( WC_Product_Variation::class )->onlyMethods( array( 'get_data_store' ) )->getMock();
+		$variation->method( 'get_data_store' )->willReturn( $data_store );
+		$variation->set_name( 'Legacy Store Product' );
+		$variation->set_attributes( array( 'finish' => '' ) );
+
+		$this->assertSame( 'Legacy Store Product', $this->sut->get_product_name( $variation, array( 'attribute_finish' => 'gloss' ) ) );
+	}
+
+	/**
+	 * @testdox Fixed variation names return without evaluating contextual title policy.
+	 */
+	public function test_get_product_name_returns_early_for_fixed_variations(): void {
+		$variation = new WC_Product_Variation();
+		$variation->set_name( 'Fixed Product - huge, 1' );
+		$variation->set_attributes(
+			array(
+				'pa_size'   => 'huge',
+				'pa_number' => '1',
+			)
+		);
+
+		$title_policy_calls  = 0;
+		$title_policy_filter = function ( $should_include ) use ( &$title_policy_calls ) {
+			++$title_policy_calls;
+
+			return $should_include;
+		};
+		add_filter( 'woocommerce_product_variation_title_include_attributes', $title_policy_filter );
+
+		$name = $this->sut->get_product_name(
+			$variation,
+			array(
+				'attribute_pa_size'   => 'huge',
+				'attribute_pa_number' => '1',
+			)
+		);
+
+		$this->assertSame( 'Fixed Product - huge, 1', $name );
+		$this->assertSame( 0, $title_policy_calls );
+	}
+
+	/**
+	 * @testdox Contextual variation names retain parent-only titles when title policy omits attributes.
+	 *
+	 * @dataProvider title_policy_provider
+	 *
+	 * @param string                $product_name Product name.
+	 * @param array<string, string> $stored_attributes Stored variation attributes.
+	 * @param array<string, string> $selected_attributes Selected cart attributes.
+	 * @param bool                  $use_exclude_filter Whether to force exclusion via the include-attributes filter.
+	 */
+	public function test_get_product_name_respects_title_policy( string $product_name, array $stored_attributes, array $selected_attributes, bool $use_exclude_filter = false ): void {
+		$variation = new WC_Product_Variation();
+		$variation->set_name( $product_name );
+		$variation->set_attributes( $stored_attributes );
+
+		if ( $use_exclude_filter ) {
+			add_filter( 'woocommerce_product_variation_title_include_attributes', '__return_false' );
+		}
+
+		$this->assertSame( $product_name, $this->sut->get_product_name( $variation, $selected_attributes ) );
+	}
+
+	/**
+	 * Provides variation-title policy cases.
+	 *
+	 * @return array<string, array{0: string, 1: array<string, string>, 2: array<string, string>, 3?: bool}>
+	 */
+	public static function title_policy_provider(): array {
+		return array(
+			'three attributes'             => array(
+				'Three Attribute Product',
+				array(
+					'pa_size'   => 'huge',
+					'pa_colour' => 'blue',
+					'pa_number' => '',
+				),
+				array(
+					'attribute_pa_size'   => 'huge',
+					'attribute_pa_colour' => 'blue',
+					'attribute_pa_number' => '1',
+				),
+			),
+			'multi-word attribute'         => array(
+				'Multi Word Attribute Product',
+				array(
+					'pa_mount-colour' => '',
+					'pa_size'         => 'large',
+				),
+				array(
+					'attribute_pa_mount-colour' => 'black',
+					'attribute_pa_size'         => 'large',
+				),
+			),
+			'include filter returns false' => array(
+				'Filtered Include Product',
+				array(
+					'pa_size'   => 'huge',
+					'pa_number' => '',
+				),
+				array(
+					'attribute_pa_size'   => 'huge',
+					'attribute_pa_number' => '1',
+				),
+				true,
+			),
+		);
+	}
+
+	/**
+	 * @testdox Filtered Any variation labels that cannot be displayed are omitted.
+	 * @dataProvider filtered_any_variation_label_provider
+	 *
+	 * @param mixed $filtered_value Filtered variation option label.
+	 */
+	public function test_get_product_name_omits_filtered_any_variation_labels_that_cannot_be_displayed( $filtered_value ): void {
+		$variation = new WC_Product_Variation();
+		$variation->set_name( 'Filtered Any Product' );
+		$variation->set_attributes( array( 'finish' => '' ) );
+
+		$option_filter          = static function () use ( $filtered_value ) {
+			return $filtered_value;
+		};
+		$separator_filter_calls = 0;
+		$separator_filter       = static function ( $separator ) use ( &$separator_filter_calls ) {
+			++$separator_filter_calls;
+
+			return $separator;
+		};
+		add_filter( 'woocommerce_variation_option_name', $option_filter );
+		add_filter( 'woocommerce_product_variation_title_attributes_separator', $separator_filter );
+
+		$name = $this->sut->get_product_name( $variation, array( 'attribute_finish' => 'gloss' ), true );
+
+		$this->assertSame( 'Filtered Any Product', $name );
+		$this->assertSame( 1, $separator_filter_calls, 'The title separator filter must retain its existing timing.' );
+	}
+
+	/**
+	 * Provides filtered Any variation labels that cannot be displayed.
+	 *
+	 * @return array<string, array{mixed}>
+	 */
+	public static function filtered_any_variation_label_provider(): array {
+		return array(
+			'false'            => array( false ),
+			'empty string'     => array( '' ),
+			'non-scalar array' => array( array( 'unexpected' ) ),
+		);
+	}
+
+	/**
+	 * @testdox Non-string separator filter values fall back to the default separator.
+	 */
+	public function test_get_product_name_tolerates_non_string_separator_filter_values(): void {
+		$variation = new WC_Product_Variation();
+		$variation->set_name( 'Guarded Separator Product' );
+		$variation->set_attributes( array( 'finish' => '' ) );
+
+		add_filter(
+			'woocommerce_product_variation_title_attributes_separator',
+			static function () {
+				return array( 'not', 'a', 'string' );
+			}
+		);
+
+		$this->assertSame( 'Guarded Separator Product - gloss', $this->sut->get_product_name( $variation, array( 'attribute_finish' => 'gloss' ) ) );
+	}
+}