Commit 74af9d746f1 for woocommerce

commit 74af9d746f1e9b995281bbd087e86c827ea13bb7
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date:   Tue Aug 25 14:01:22 2026 +0100

    Add region-data, comment style, and testing guidance for AI agents (#67088)

    * Document CLDR policy and region-data migration guidance

    * Add comment style and test quality guidance for AI agents

    * Clarify that migrate_country_states only covers subdivision codes

    * Fix markdownlint error on example labels in unit-tests.md

    ---------

    Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>

diff --git a/.ai/skills/woocommerce-backend-dev/code-entities.md b/.ai/skills/woocommerce-backend-dev/code-entities.md
index 69c2a1c1c39..19e1a7fd673 100644
--- a/.ai/skills/woocommerce-backend-dev/code-entities.md
+++ b/.ai/skills/woocommerce-backend-dev/code-entities.md
@@ -88,7 +88,7 @@ public function calculate_with_tax( float $amount ) {

 ## Docblock Requirements

-Add concise docblocks to all hooks and methods. One line is ideal.
+Add concise docblocks to all hooks and methods. One line is ideal. The description should rarely need more than 3-4 lines.

 ### Public, Protected Methods, and Hooks

diff --git a/.ai/skills/woocommerce-backend-dev/coding-conventions.md b/.ai/skills/woocommerce-backend-dev/coding-conventions.md
index a82a8422c8d..74414177385 100644
--- a/.ai/skills/woocommerce-backend-dev/coding-conventions.md
+++ b/.ai/skills/woocommerce-backend-dev/coding-conventions.md
@@ -45,6 +45,26 @@ if ( $order->is_draft() ) {
 - Explaining what code does (code should be self-explanatory)
 - Restating the obvious

+Keep docblocks short and plain. See `AGENTS.md` ("Comments and Docblocks") for the full rule.
+
+**Avoid - Intricate phrasing:**
+
+```php
+/**
+ * Facilitates the orchestration of the underlying reconciliation
+ * process by which order totals are ultimately synchronized with
+ * their corresponding line item aggregates.
+ */
+```
+
+**Prefer - Plain and short:**
+
+```php
+/**
+ * Recalculate the order total from its line items.
+ */
+```
+
 ## WordPress Coding Standards

 Follow [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/):
diff --git a/.ai/skills/woocommerce-backend-dev/unit-tests.md b/.ai/skills/woocommerce-backend-dev/unit-tests.md
index acb9ffaf603..fb025e21fa7 100644
--- a/.ai/skills/woocommerce-backend-dev/unit-tests.md
+++ b/.ai/skills/woocommerce-backend-dev/unit-tests.md
@@ -7,6 +7,8 @@
 - [System Under Test Variable](#system-under-test-variable)
 - [Test Method Documentation](#test-method-documentation)
 - [Comments in Tests](#comments-in-tests)
+- [Avoid Performative Tests](#avoid-performative-tests)
+- [Group Similar Tests with @testWith or a Data Provider](#group-similar-tests-with-testwith-or-a-data-provider)
 - [Test Configuration](#test-configuration)
 - [Example: Payment Extension Suggestions Tests](#example-payment-extension-suggestions-tests)
 - [Mocking the WooCommerce Logger](#mocking-the-woocommerce-logger)
@@ -189,6 +191,57 @@ Use blank lines for visual separation instead. The test structure should be self
 - Documenting known issues: `// Workaround for WordPress core bug #12345`
 - Clarifying business rules: `// Payment processor requires 24h hold`

+## Avoid Performative Tests
+
+Tests exist to catch regressions, not to raise a coverage number. A test that passes no matter what the implementation does isn't providing value.
+
+**Signs a test is performative:**
+
+- Asserting something PHP or WordPress already guarantees (e.g. a setter stored the exact value passed in, with no transformation or validation involved)
+- Weak assertions (`assertNotNull`, unqualified `assertTrue`) where a specific expected value would actually catch a regression
+- Mocking so many of the SUT's related helpers or classes that you end up checking the mock's pre-set return value, not the SUT's own logic
+- A near-duplicate of another test, added just to "cover one more case," that doesn't touch any new code path
+
+**Before adding a test, ask:** if the implementation had a bug, would this test actually fail? If you can't think of a bug this test would catch, it isn't worth adding.
+
+## Group Similar Tests with `@testWith` or a Data Provider
+
+When several tests call the same method with different inputs but the same assertion logic, don't write many near-identical test methods with copy-pasted setup. Collapse them into one test.
+
+- Prefer **`@testWith`** when the inputs are simple values (strings, numbers, booleans) or arrays written directly in the annotation (see real examples in `wc-core-functions-test.php`).
+- Use a **`dataProvider`** method only when the dataset needs to be built with logic (loops, constants, fixtures) rather than written out literally.
+
+**Avoid - Repeated tests, same shape:**
+
+```php
+public function test_get_shipping_cost_us() {
+    $this->assertSame( 5.00, $this->sut->get_shipping_cost( 'US' ) );
+}
+
+public function test_get_shipping_cost_ca() {
+    $this->assertSame( 7.50, $this->sut->get_shipping_cost( 'CA' ) );
+}
+
+public function test_get_shipping_cost_mx() {
+    $this->assertSame( 12.00, $this->sut->get_shipping_cost( 'MX' ) );
+}
+```
+
+**Prefer - One test with `@testWith`:**
+
+```php
+/**
+ * @testWith ["US", 5.00]
+ *           ["CA", 7.50]
+ *           ["MX", 12.00]
+ */
+public function test_get_shipping_cost( string $country, float $expected ) {
+    $this->assertSame( $expected, $this->sut->get_shipping_cost( $country ) );
+}
+```
+
+Never recompute the expected value inside the test or provider by reimplementing the system under test's own logic. Hardcode the expected literal instead. A provider that recalculates the answer the same way the SUT does will pass even when both are wrong.
+
 ## Test Configuration

 Test configuration file: `phpunit.xml`
@@ -356,7 +409,8 @@ See `PaymentGatewayTest.php:create_fake_logger()` for a complete implementation.
 1. **Always run tests after making changes** to verify functionality
 2. **Use specific test filters** during development (see running-tests.md in the woocommerce-dev-cycle skill)
 3. **Write descriptive test names** that explain what is being tested
-4. **Use data providers** for testing multiple scenarios with the same logic
+4. **Use `@testWith` or a data provider** instead of several near-identical tests for the same logic
 5. **Include helpful assertion messages** for debugging when tests fail
 6. **Test both success and failure cases**
 7. **Mock external dependencies** (database, API calls, etc.)
+8. **Only add tests that can fail on a real bug**
diff --git a/AGENTS.md b/AGENTS.md
index a76cc94c221..547d0aa5ccd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -197,6 +197,16 @@ WordPress exposes more contracts than class and function signatures. The followi
 4. State the impact in the PR description: what changed, who could consume it, and why it is safe or what the deprecation path is.
 5. If you cannot establish the impact, stop and flag it to the user as needing review.

+## Country and State (Region) Data
+
+Country and state/province lists live in `plugins/woocommerce/i18n/countries.php` and `plugins/woocommerce/i18n/states.php` (see `plugins/woocommerce/i18n/README.md`).
+
+**Follow the CLDR standard.** Codes and names should match the [Unicode CLDR](https://cldr.unicode.org/) project. CLDR is the actively maintained, widely used source for this kind of data, so following it keeps WooCommerce consistent with the wider ecosystem and avoids the drift and upkeep of a homegrown list. If CLDR doesn't yet have the code or name a region needs, propose the change to CLDR first rather than diverging from it.
+
+**Adding new codes is safe. Renaming or removing existing ones is not.** Do that only when CLDR itself has changed, and expect it to need a migration. State/country codes are stored in orders, shipping zones, tax rates, and store settings. Editing `states.php`/`countries.php` only changes what new data looks like. Every already-stored old code is left behind, no longer matching the dropdown or validation that now expects the new one.
+
+To rename subdivision codes, use `Automattic\WooCommerce\Database\Migrations\MigrationHelper::migrate_country_states()` from a `wc_update_*` function in `wc-update-functions.php`, passing a map of old codes to new ones. See `wc_update_721_adjust_new_zealand_states()` for the pattern. Use the helper rather than writing your own partial migration, since it's easy to miss one of the places a code is stored. The helper only covers subdivision codes, so country or other changes might need a custom migration routine. Purely additive changes (new codes, no renames) don't need a migration.
+
 ## Database Migrations

 Database migrations live in `WC_Install::$db_updates`; read that class for the current mechanics before adding one. Two invariants have broken real releases when violated:
@@ -204,6 +214,17 @@ Database migrations live in `WC_Install::$db_updates`; read that class for the c
 - Migration keys are one-shot: sites that updated past a key never re-run it. A migration added after a prerelease of the same version has shipped needs a new suffixed key (see existing examples in `$db_updates`), and a key must never be ahead of the version it ships in.
 - Feature flag defaults are persisted, so changing `enabled_by_default` alone doesn't change behavior on existing sites; ship a migration or remove the flag.

+## Comments and Docblocks
+
+Docblocks are expected on methods, classes, and hooks (see the `woocommerce-backend-dev` skill for exact requirements). Inline comments are the exception, not the default: add one only when the code can't explain itself, for example a non-obvious "why", a hidden constraint, or a workaround for a specific bug. Either way, don't add a comment that just restates what the identifier names already say.
+
+When writing a comment or docblock description:
+
+- **Keep it short.** 3-4 lines is the target for a docblock description (`@param`/`@return`/`@since` lines are separate and don't count against this). If it's running longer, the comment is likely explaining something the code itself should make obvious. Simplify the code first.
+- **Use plain language.** Say what the code does or why in ordinary words. Avoid dense or clever phrasing, and avoid vague jargon for guard conditions (e.g. "gates", "gating"). Say "guard", "check", "only when" instead.
+- **Don't force-wrap at a fixed column.** This repo has no enforced 80- or 120-column limit on comment prose (`.markdownlint.json` disables `MD013`, and there's no PHPCS `LineLength` override), and plenty of existing docblocks already run past both. Match the wrap width already used in the surrounding file instead of imposing your own.
+- **Decorative comments are worse than none.** A comment that restates the next line, marks an obvious section (`// Loop over items`), or pads a docblock out to look thorough adds noise a future reader has to read past to find the comments that actually matter.
+
 ## Enum-Style Constants (`src/Enums/`)

 WooCommerce names its enumerated string vocabularies — order statuses, product types, stock statuses, settings option values, and more — as `final` classes of `public const` strings under `Automattic\WooCommerce\Enums` (`plugins/woocommerce/src/Enums/`, see its `README.md` for the full list). Native PHP enums are not an option: the minimum supported PHP version is 7.4, and the raw string values are the contract persisted in databases and consumed by extensions.
diff --git a/docs/contribution/contributing/string-localisation-guidelines.md b/docs/contribution/contributing/string-localisation-guidelines.md
index 5f3f3480f88..0ecd0d4ec03 100644
--- a/docs/contribution/contributing/string-localisation-guidelines.md
+++ b/docs/contribution/contributing/string-localisation-guidelines.md
@@ -170,3 +170,7 @@ This approach may not be suitable in all cases, because it can take time for CLD

 - [Snippet to add a country](/docs/code-snippets/add-a-country)
 - [Snippet to add or modify states](/docs/code-snippets/add-or-modify-states)
+
+### Updating an existing country or subdivision code
+
+Adding a new country or subdivision code is safe, and so is changing a display name, since names are just translatable strings. Renaming or removing an existing **code** is different: the old code may already be stored in customer orders, shipping zones, tax rates, and the store's default country setting, so it often needs a database migration too. For subdivision codes, use `MigrationHelper::migrate_country_states()` (`Automattic\WooCommerce\Database\Migrations\MigrationHelper`). There's an example in `wc_update_721_adjust_new_zealand_states()` (`wc-update-functions.php`). The helper only covers subdivision codes, so country or other changes might need a custom migration routine.
diff --git a/plugins/woocommerce/i18n/README.md b/plugins/woocommerce/i18n/README.md
index b5352efdbc1..c19ea6f96b3 100644
--- a/plugins/woocommerce/i18n/README.md
+++ b/plugins/woocommerce/i18n/README.md
@@ -1,3 +1,3 @@
 # Countries and Subdivisions

-If you are interested in making changes to the list of countries or subdivisions, please first read the guidance in our [developer documentation](../../../docs/localization-translation/countries-and-subdivisions.md).
+If you are interested in making changes to the list of countries or subdivisions, please first read the guidance in our [developer documentation](../../../docs/contribution/contributing/string-localisation-guidelines.md#countries-and-subdivisions). WooCommerce tries to follow the [Unicode CLDR](https://cldr.unicode.org/) standard for these codes and names.