Commit f0720e34345 for woocommerce

commit f0720e343459fae8fb807793e391703e50f5779a
Author: Luis Felipe Zaguini <26530524+zaguiini@users.noreply.github.com>
Date:   Fri Oct 16 18:41:19 2026 -0300

    Write the extensions input schema one time (#69682)

    * Write the extensions input schema one time

    The full extensions schema is at the top level of the input schema, and each
    oneOf branch only accepts the key. The product create and update schemas no
    longer grow with the number of product types.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Point each oneOf branch to the top-level extensions property, and assert the error text

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

    * Read the enum field schema from the top-level extensions property

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

    * Accept the extensions key in anyOf and allOf branches too

    JSON Schema checks additionalProperties inside each branch, so a branch
    that refuses other properties rejected the top-level extensions key.
    Every oneOf, anyOf and allOf branch now gets the small entry.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Rename AbilityExtensions to ExtensibleAbilities after the rebase

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Check the allOf branch in the published schema, since WordPress does not validate allOf

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/tweak-ability-extensions-input-schema-once b/plugins/woocommerce/changelog/tweak-ability-extensions-input-schema-once
new file mode 100644
index 00000000000..86c1d676ecf
--- /dev/null
+++ b/plugins/woocommerce/changelog/tweak-ability-extensions-input-schema-once
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Write the extensions input schema of the product abilities one time, so its size does not grow with the number of product types.
diff --git a/plugins/woocommerce/src/Abilities/ActionableAbility.php b/plugins/woocommerce/src/Abilities/ActionableAbility.php
index 0a13efc65d3..244a907ea4c 100644
--- a/plugins/woocommerce/src/Abilities/ActionableAbility.php
+++ b/plugins/woocommerce/src/Abilities/ActionableAbility.php
@@ -227,9 +227,11 @@ abstract class ActionableAbility extends \WP_Ability {
 	}

 	/**
-	 * Add the `extensions` property to the input schema. It lists the fields
-	 * that have an update_callback, and allows null for each one. A schema with
-	 * `oneOf` gets the property in each branch.
+	 * Add the `extensions` property one time, at the top level of the input
+	 * schema. It lists the fields that have an update_callback, and allows null
+	 * for each one. Each `oneOf`, `anyOf` and `allOf` branch only accepts the
+	 * key, so a branch with `additionalProperties` set to false does not reject
+	 * it, and the schema does not grow with the number of branches.
 	 *
 	 * @param array<string, mixed> $schema Input schema.
 	 * @return array<string, mixed>
@@ -269,12 +271,14 @@ abstract class ActionableAbility extends \WP_Ability {
 			);
 		}

-		if ( isset( $schema['oneOf'] ) ) {
-			foreach ( array_keys( $schema['oneOf'] ) as $index ) {
-				$schema['oneOf'][ $index ]['properties']['extensions'] = $extensions;
+		$schema['properties']['extensions'] = $extensions;
+		foreach ( array( 'oneOf', 'anyOf', 'allOf' ) as $keyword ) {
+			foreach ( array_keys( $schema[ $keyword ] ?? array() ) as $index ) {
+				$schema[ $keyword ][ $index ]['properties']['extensions'] = array(
+					'type'        => 'object',
+					'description' => __( 'The fields are listed in the top-level extensions property.', 'woocommerce' ),
+				);
 			}
-		} else {
-			$schema['properties']['extensions'] = $extensions;
 		}
 		return $schema;
 	}
diff --git a/plugins/woocommerce/tests/php/src/Internal/AbilitiesApi/ExtensibleAbilitiesTest.php b/plugins/woocommerce/tests/php/src/Internal/AbilitiesApi/ExtensibleAbilitiesTest.php
index 3981e3796fb..5fa811d38da 100644
--- a/plugins/woocommerce/tests/php/src/Internal/AbilitiesApi/ExtensibleAbilitiesTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/AbilitiesApi/ExtensibleAbilitiesTest.php
@@ -694,6 +694,9 @@ class ExtensibleAbilitiesTest extends \WC_REST_Unit_Test_Case {
 			);

 			$this->assertSame( 400, $response->get_status(), wp_json_encode( $extensions ) );
+			if ( isset( $extensions['other_ext']['count'] ) && 'many' === $extensions['other_ext']['count'] ) {
+				$this->assertStringContainsString( 'input[extensions][other_ext][count] is not of type integer,null.', $response->get_data()['message'] );
+			}
 		}
 		$saved = wc_get_product( $product->get_id() );
 		$this->assertSame( 'Pen', $saved->get_name() );
@@ -830,7 +833,7 @@ class ExtensibleAbilitiesTest extends \WC_REST_Unit_Test_Case {
 		$product->update_meta_data( '_test_size', 'small' );
 		$product->save();

-		$schema   = $this->get_ability( 'woocommerce/product-update' )['input_schema']['oneOf'][0]['properties']['extensions']['properties']['other_ext']['properties']['size'];
+		$schema   = $this->get_ability( 'woocommerce/product-update' )['input_schema']['properties']['extensions']['properties']['other_ext']['properties']['size'];
 		$rejected = $this->run_ability_response(
 			'woocommerce/product-update',
 			array(
@@ -945,6 +948,82 @@ class ExtensibleAbilitiesTest extends \WC_REST_Unit_Test_Case {
 		$this->assertSame( 'Pen', wc_get_product( $product->get_id() )->get_name() );
 	}

+	/**
+	 * @testdox Should list the extensions key in anyOf and allOf branches that refuse other properties, and accept extension fields through an anyOf branch.
+	 */
+	public function test_combinator_branches_accept_the_extensions_key(): void {
+		$product = \WC_Helper_Product::create_simple_product( true, array( 'name' => 'Pen' ) );
+		$branch  = array(
+			'type'                 => 'object',
+			'properties'           => array(
+				'id'   => array( 'type' => 'integer' ),
+				'name' => array( 'type' => 'string' ),
+			),
+			'additionalProperties' => false,
+		);
+		foreach ( array( 'anyOf', 'allOf' ) as $keyword ) {
+			$name     = 'test-ext/product-rename-' . strtolower( $keyword );
+			$register = static function () use ( $name, $keyword, $branch ) {
+				wp_register_ability(
+					$name,
+					array(
+						'label'               => 'Rename product',
+						'description'         => 'Renames a product.',
+						'category'            => 'woocommerce',
+						'input_schema'        => array(
+							'type'   => 'object',
+							$keyword => array( $branch ),
+						),
+						'output_schema'       => ExtensibleAbilities::add_fields_schema( array( 'type' => 'object' ), 'product' ),
+						'permission_callback' => '__return_true',
+						'ability_class'       => TestProductRenameAbility::class,
+						'meta'                => array( 'show_in_rest' => true ),
+					)
+				);
+			};
+			add_action( 'wp_abilities_api_init', $register, 20 );
+			$this->reset_registries();
+
+			$schema = $this->get_ability( $name )['input_schema'];
+			$this->assertSame( 'object', $schema[ $keyword ][0]['properties']['extensions']['type'] ?? null, $keyword );
+
+			if ( 'anyOf' === $keyword ) {
+				$output = $this->run_ability(
+					$name,
+					array(
+						'id'         => $product->get_id(),
+						'name'       => 'Pencil',
+						'extensions' => array( 'test-ext' => array( 'color' => 'blue' ) ),
+					)
+				);
+				$this->assertSame( array( 'color' => 'blue' ), $output['extensions']['test-ext'] );
+			}
+
+			remove_action( 'wp_abilities_api_init', $register, 20 );
+		}
+	}
+
+	/**
+	 * @testdox Should list the writable fields one time at the top level of the product input schemas.
+	 */
+	public function test_input_schemas_list_writable_fields_one_time(): void {
+		foreach ( array( 'woocommerce/product-create', 'woocommerce/product-update' ) as $name ) {
+			$schema     = $this->get_ability( $name )['input_schema'];
+			$extensions = $schema['properties']['extensions']['properties'];
+
+			$this->assertSame( array( 'string', 'null' ), $extensions['test-ext']['properties']['color']['type'] );
+			$this->assertSame( array( 'integer', 'null' ), $extensions['other_ext']['properties']['count']['type'] );
+			$this->assertArrayNotHasKey( 'code', $extensions['test-ext']['properties'] );
+			$this->assertSame( 1, substr_count( wp_json_encode( $schema ), '"color"' ) );
+			foreach ( $schema['oneOf'] as $branch ) {
+				$this->assertSame( 'object', $branch['properties']['extensions']['type'] );
+				$this->assertArrayNotHasKey( 'properties', $branch['properties']['extensions'] );
+			}
+		}
+		$order = $this->get_ability( 'woocommerce/order-update-status' )['input_schema'];
+		$this->assertArrayNotHasKey( 'extensions', $order['properties'] );
+	}
+
 	/**
 	 * @testdox Should not accept extension fields in the write abilities with the feature off.
 	 */
@@ -969,6 +1048,7 @@ class ExtensibleAbilitiesTest extends \WC_REST_Unit_Test_Case {
 		);

 		$this->assertInstanceOf( ActionableAbility::class, wp_get_ability( 'woocommerce/product-update' ) );
+		$this->assertArrayNotHasKey( 'extensions', $schema['properties'] ?? array() );
 		$this->assertArrayNotHasKey( 'extensions', $schema['oneOf'][0]['properties'] );
 		$this->assertSame( 400, $rejected->get_status() );
 		$this->assertSame( 'Pencil', $output['product']['name'] );