Commit 2fa660b3b99 for woocommerce

commit 2fa660b3b99e12f33ae99abe9ba52439d445cc9a
Author: Patrick Zielinski <patrick.zielinski@a8c.com>
Date:   Mon Apr 27 08:47:02 2026 -0400

    [Email Editor] Add server-side reset endpoint that stamps sync meta (#64355)

    * Stamp sync meta on woo_email post when REST update matches core render

    * Add server-side reset endpoint for woo_email posts

    Adds POST /woocommerce-email-editor/v1/emails/{id}/reset that atomically
    rewrites the post_content to the canonical core template render and stamps
    sync meta (_wc_email_template_version, _wc_email_template_source_hash,
    _wc_email_last_synced_at, _wc_email_template_status = in_sync).

    Renders content via WCTransactionalEmailPostsGenerator::compute_canonical_post_content()
    so the byte sequence is identical to a fresh recreate, eliminating the hash
    mismatch that drove the pivot away from client-side reset detection.

    Gated on WCEmailTemplateSyncRegistry: emails not opted in are rejected with
    a 403 rather than silently no-op'd. The post update + 4 meta writes are
    wrapped in a single SQL transaction so a partial failure rolls back cleanly.

    Initialise the previously-typed-but-uninitialised $post_manager property to
    null so the existing 500 guard in get_default_content_response actually
    returns the WP_Error instead of throwing on PHP 8.1+.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Switch reset action to call server-side reset endpoint

    The Reset action in the email editor now POSTs to
    /woocommerce-email-editor/v1/emails/{id}/reset, which atomically rewrites
    post_content and stamps sync meta server-side. The client still updates the
    in-memory editor state with the returned content so the user sees the reset
    content without a page reload.

    Replaces the previous GET /default-content + saveEditedEntityRecord round-trip,
    which produced post_content that the divergence detector could not reliably
    match against the core render due to JS/PHP serialization differences.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Remove Option A REST-update stamper

    Drops WCEmailTemplateSyncRestStamper and its hook on rest_after_insert_woo_email,
    along with the corresponding test. Reset detection no longer happens via hash
    comparison on standard REST writes — the dedicated /reset endpoint introduced
    in the previous commit handles it server-authoritatively.

    Per Slack discussion (RSM-148 comment 2026-04-25), client-side reset producing
    content that the server then hashes was unreliable due to JS/PHP serialization
    differences. The server-side endpoint sidesteps the comparison entirely.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    ---------

    Co-authored-by: PZ01 <patrick.zielinski@automattic.com>
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/rsm-148-stamp-sync-meta-on-email-post-reset b/plugins/woocommerce/changelog/rsm-148-stamp-sync-meta-on-email-post-reset
new file mode 100644
index 00000000000..a333bef552e
--- /dev/null
+++ b/plugins/woocommerce/changelog/rsm-148-stamp-sync-meta-on-email-post-reset
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Add server-side POST /woocommerce-email-editor/v1/emails/{id}/reset endpoint that atomically rewrites a woo_email post to its current core template render and stamps sync meta (version, source hash, synced-at, status=in_sync).
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/reset-notification-email-content.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/reset-notification-email-content.tsx
index 74deef4a27d..2a4682f46dd 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/reset-notification-email-content.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/reset-notification-email-content.tsx
@@ -104,9 +104,16 @@ const getResetNotificationEmailContentAction = () => {

 								try {
 									const response = ( await apiFetch( {
-										path: `/woocommerce-email-editor/v1/emails/${ item.id }/default-content`,
+										path: `/woocommerce-email-editor/v1/emails/${ item.id }/reset`,
+										method: 'POST',
 									} ) ) as { content: string };

+									// Server has already persisted post_content + sync meta.
+									// Sync the editor's in-memory state so the user sees the
+									// reset content without a page reload. The trailing
+									// saveEditedEntityRecord is a content no-op (matches what
+									// the server just wrote) but keeps core-data's dirty
+									// tracking in a consistent state.
 									const blocks = parse(
 										response.content || ''
 									);
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 1eb3e83066a..884312d580a 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -64782,12 +64782,6 @@ parameters:
 			count: 2
 			path: src/Internal/EmailEditor/EmailApiController.php

-		-
-			message: '#^Property Automattic\\WooCommerce\\Internal\\EmailEditor\\EmailApiController\:\:\$post_manager \(Automattic\\WooCommerce\\Internal\\EmailEditor\\WCTransactionalEmails\\WCTransactionalEmailPostsManager\|null\) is never assigned null so it can be removed from the property type\.$#'
-			identifier: property.unusedType
-			count: 1
-			path: src/Internal/EmailEditor/EmailApiController.php
-
 		-
 			message: '#^PHPDoc type array of property Automattic\\WooCommerce\\Internal\\EmailEditor\\EmailPatterns\\WooEmailContentPattern\:\:\$template_types is not covariant with PHPDoc type array\<string\> of overridden property Automattic\\WooCommerce\\EmailEditor\\Engine\\Patterns\\Abstract_Pattern\:\:\$template_types\.$#'
 			identifier: property.phpDocType
diff --git a/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php b/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php
index 3e0e71957fa..0c16132de39 100644
--- a/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php
+++ b/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php
@@ -5,6 +5,8 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Internal\EmailEditor;

 use Automattic\WooCommerce\EmailEditor\Validator\Builder;
+use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateDivergenceDetector;
+use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateSyncRegistry;
 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsManager;
 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsGenerator;
 use WC_Email;
@@ -26,7 +28,7 @@ class EmailApiController {
 	 *
 	 * @var WCTransactionalEmailPostsManager|null
 	 */
-	private ?WCTransactionalEmailPostsManager $post_manager;
+	private ?WCTransactionalEmailPostsManager $post_manager = null;

 	/**
 	 * The WooCommerce transactional email posts generator.
@@ -283,6 +285,27 @@ class EmailApiController {
 				'schema'              => array( $this, 'get_default_content_schema' ),
 			)
 		);
+
+		register_rest_route(
+			'woocommerce-email-editor/v1',
+			'/emails/(?P<id>\d+)/reset',
+			array(
+				'methods'             => \WP_REST_Server::CREATABLE,
+				'callback'            => array( $this, 'reset_response' ),
+				'permission_callback' => function () {
+					return current_user_can( 'manage_woocommerce' );
+				},
+				'args'                => array(
+					'id' => array(
+						'description'       => __( 'The ID of the woo_email post.', 'woocommerce' ),
+						'type'              => 'integer',
+						'required'          => true,
+						'sanitize_callback' => 'absint',
+					),
+				),
+				'schema'              => array( $this, 'get_reset_schema' ),
+			)
+		);
 	}

 	/**
@@ -338,4 +361,133 @@ class EmailApiController {
 			200
 		);
 	}
+
+	/**
+	 * Get the schema for the reset endpoint response.
+	 *
+	 * @return array
+	 */
+	public function get_reset_schema(): array {
+		return array(
+			'$schema'    => 'http://json-schema.org/draft-04/schema#',
+			'title'      => 'woo_email_reset',
+			'type'       => 'object',
+			'properties' => array(
+				'content'     => array(
+					'description' => __( 'The canonical block content written to the post.', 'woocommerce' ),
+					'type'        => 'string',
+					'readonly'    => true,
+				),
+				'version'     => array(
+					'description' => __( 'The core block template @version stamped on the post, or null when the email is not sync-enabled.', 'woocommerce' ),
+					'type'        => array( 'string', 'null' ),
+					'readonly'    => true,
+				),
+				'source_hash' => array(
+					'description' => __( 'sha1 of the canonical block content stamped on the post, or null when the email is not sync-enabled.', 'woocommerce' ),
+					'type'        => array( 'string', 'null' ),
+					'readonly'    => true,
+				),
+				'synced_at'   => array(
+					'description' => __( 'UTC timestamp when the post was stamped (Y-m-d H:i:s), or null when the email is not sync-enabled.', 'woocommerce' ),
+					'type'        => array( 'string', 'null' ),
+					'readonly'    => true,
+				),
+				'status'      => array(
+					'description' => __( 'The post-reset sync status (in_sync on success for sync-enabled emails, null otherwise).', 'woocommerce' ),
+					'type'        => array( 'string', 'null' ),
+					'readonly'    => true,
+				),
+			),
+		);
+	}
+
+	/**
+	 * Reset a `woo_email` post to its current core template render and (when sync-enabled) stamp sync meta.
+	 *
+	 * Writes the canonical post content (byte-identical to what
+	 * {@see WCTransactionalEmailPostsGenerator} would produce on a fresh recreate). For emails
+	 * that are opted in to template sync (registered in {@see WCEmailTemplateSyncRegistry}),
+	 * also stamps `_wc_email_template_version`, `_wc_email_template_source_hash`,
+	 * `_wc_email_last_synced_at`, and `_wc_email_template_status = in_sync`. Meta writes are
+	 * conditional on the post update succeeding, so a `wp_update_post` failure leaves the
+	 * post — and any pre-existing meta — untouched.
+	 *
+	 * Non-sync-enabled emails (e.g. third-party templates without an `@version` header)
+	 * still receive a successful content reset, just without the meta stamp. This mirrors
+	 * the pre-RSM-148 behaviour where the standalone REST PUT performed the content reset
+	 * and stamping was a separate side effect, preserving backward compatibility.
+	 *
+	 * @param WP_REST_Request $request The REST request.
+	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
+	 * @return WP_REST_Response|WP_Error
+	 *
+	 * @since 10.8.0
+	 */
+	public function reset_response( WP_REST_Request $request ) {
+		if ( ! ( $this->post_manager && $this->posts_generator ) ) {
+			return new WP_Error(
+				'woocommerce_email_editor_not_initialized',
+				__( 'Email editor is not initialized.', 'woocommerce' ),
+				array( 'status' => 500 )
+			);
+		}
+
+		$post_id    = (int) $request->get_param( 'id' );
+		$email_type = $this->post_manager->get_email_type_from_post_id( $post_id );
+		$email      = $this->get_email_by_type( $email_type ?? '' );
+
+		if ( ! $email ) {
+			return new WP_Error(
+				'woocommerce_email_not_found',
+				__( 'No email found for the given post ID.', 'woocommerce' ),
+				array( 'status' => 404 )
+			);
+		}
+
+		$canonical   = WCTransactionalEmailPostsGenerator::compute_canonical_post_content( $email );
+		$sync_config = WCEmailTemplateSyncRegistry::get_email_sync_config( (string) $email_type );
+
+		$update_result = wp_update_post(
+			array(
+				'ID'           => $post_id,
+				'post_content' => $canonical,
+			),
+			true
+		);
+
+		if ( is_wp_error( $update_result ) ) {
+			return new WP_Error(
+				'woocommerce_email_reset_failed',
+				sprintf(
+					/* translators: %s: underlying error message */
+					__( 'Failed to reset email content: %s', 'woocommerce' ),
+					$update_result->get_error_message()
+				),
+				array( 'status' => 500 )
+			);
+		}
+
+		$source_hash = null;
+		$synced_at   = null;
+		if ( null !== $sync_config ) {
+			$source_hash = sha1( $canonical );
+			$synced_at   = gmdate( 'Y-m-d H:i:s' );
+			update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::VERSION_META_KEY, (string) $sync_config['version'] );
+			update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::SOURCE_HASH_META_KEY, $source_hash );
+			update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::LAST_SYNCED_AT_META_KEY, $synced_at );
+			update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::STATUS_META_KEY, WCEmailTemplateDivergenceDetector::STATUS_IN_SYNC );
+		}
+
+		return new WP_REST_Response(
+			array(
+				'content'     => $canonical,
+				'version'     => null !== $sync_config ? (string) $sync_config['version'] : null,
+				'source_hash' => $source_hash,
+				'synced_at'   => $synced_at,
+				'status'      => null !== $sync_config ? WCEmailTemplateDivergenceDetector::STATUS_IN_SYNC : null,
+			),
+			200
+		);
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php
index b410a1967e0..fd592a6de1f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php
@@ -6,6 +6,8 @@ namespace Automattic\WooCommerce\Tests\Internal\EmailEditor;

 use Automattic\WooCommerce\Internal\EmailEditor\EmailApiController;
 use Automattic\WooCommerce\Internal\EmailEditor\Integration;
+use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateDivergenceDetector;
+use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateSyncRegistry;
 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsGenerator;
 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsManager;

@@ -63,6 +65,9 @@ class EmailApiControllerTest extends \WC_Unit_Test_Case {
 		parent::tearDown();
 		update_option( 'woocommerce_feature_block_email_editor_enabled', 'no' );
 		delete_option( 'woocommerce_' . $this->email_type . '_settings' );
+		remove_all_filters( 'woocommerce_transactional_emails_for_block_editor' );
+		WCEmailTemplateSyncRegistry::reset_cache();
+		delete_transient( 'wc_email_editor_initial_templates_generated' );
 	}

 	/**
@@ -338,4 +343,238 @@ class EmailApiControllerTest extends \WC_Unit_Test_Case {
 		$this->assertArrayHasKey( 'content', $result->get_data() );
 		$this->assertSame( '<!-- wp:paragraph --><p>Default content</p><!-- /wp:paragraph -->', $result->get_data()['content'] );
 	}
+
+	/**
+	 * @testdox Should reset post content to canonical core render and refresh sync meta.
+	 */
+	public function test_reset_response_overwrites_post_content_and_stamps_sync_meta(): void {
+		$email_type = 'customer_new_account';
+
+		$generator = new WCTransactionalEmailPostsGenerator();
+		$generator->init_default_transactional_emails();
+
+		$post_manager = WCTransactionalEmailPostsManager::get_instance();
+		$post_manager->clear_caches();
+		$post_manager->delete_email_template( $email_type );
+		WCEmailTemplateSyncRegistry::reset_cache();
+
+		$post_id = $generator->generate_email_template_if_not_exists( $email_type );
+		$this->assertIsInt( $post_id );
+
+		// Simulate merchant customisation that diverges from the core render.
+		wp_update_post(
+			array(
+				'ID'           => $post_id,
+				'post_content' => '<!-- wp:paragraph --><p>Customized by merchant</p><!-- /wp:paragraph -->',
+			)
+		);
+
+		// Backdate the synced_at stamp so we can assert the endpoint refreshes it.
+		update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::LAST_SYNCED_AT_META_KEY, '2000-01-01 00:00:00' );
+		// Force a non-in_sync status so we can assert the endpoint resets it.
+		update_post_meta(
+			$post_id,
+			WCEmailTemplateDivergenceDetector::STATUS_META_KEY,
+			WCEmailTemplateDivergenceDetector::STATUS_CORE_UPDATED_CUSTOMIZED
+		);
+
+		$request = new \WP_REST_Request( 'POST', '/woocommerce-email-editor/v1/emails/' . $post_id . '/reset' );
+		$request->set_param( 'id', $post_id );
+
+		$result = $this->email_api_controller->reset_response( $request );
+
+		$this->assertInstanceOf( \WP_REST_Response::class, $result );
+		$this->assertSame( 200, $result->get_status() );
+
+		$email = $this->resolve_wc_email( $email_type );
+		$this->assertNotNull( $email );
+		$expected_canonical = WCTransactionalEmailPostsGenerator::compute_canonical_post_content( $email );
+
+		$response_data = $result->get_data();
+		$this->assertSame( $expected_canonical, $response_data['content'], 'Response content must equal canonical core render.' );
+		$this->assertSame( sha1( $expected_canonical ), $response_data['source_hash'], 'Response source_hash must equal sha1(canonical).' );
+		$this->assertSame( WCEmailTemplateDivergenceDetector::STATUS_IN_SYNC, $response_data['status'], 'Response status must be in_sync.' );
+		$this->assertNotEmpty( $response_data['version'], 'Response version must be populated.' );
+		$this->assertMatchesRegularExpression( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $response_data['synced_at'], 'Response synced_at must be a GMT timestamp.' );
+
+		$persisted_post = get_post( $post_id );
+		$this->assertSame( $expected_canonical, $persisted_post->post_content, 'Persisted post_content must equal canonical core render.' );
+
+		$this->assertSame(
+			$response_data['version'],
+			(string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::VERSION_META_KEY, true ),
+			'Persisted version meta must match response.'
+		);
+		$this->assertSame(
+			$response_data['source_hash'],
+			(string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::SOURCE_HASH_META_KEY, true ),
+			'Persisted source_hash meta must match response.'
+		);
+		$this->assertSame(
+			$response_data['synced_at'],
+			(string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::LAST_SYNCED_AT_META_KEY, true ),
+			'Persisted synced_at meta must match response.'
+		);
+		$this->assertSame(
+			WCEmailTemplateDivergenceDetector::STATUS_IN_SYNC,
+			(string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::STATUS_META_KEY, true ),
+			'Persisted status meta must be set to in_sync.'
+		);
+	}
+
+	/**
+	 * @testdox Should return 404 when reset post ID has no associated email type.
+	 */
+	public function test_reset_response_returns_404_for_unknown_post(): void {
+		$unassociated_post = $this->factory()->post->create_and_get(
+			array(
+				'post_title'  => 'Unknown Email',
+				'post_name'   => 'unknown_email_for_reset',
+				'post_type'   => Integration::EMAIL_POST_TYPE,
+				'post_status' => 'draft',
+			)
+		);
+
+		$request = new \WP_REST_Request( 'POST', '/woocommerce-email-editor/v1/emails/' . $unassociated_post->ID . '/reset' );
+		$request->set_param( 'id', $unassociated_post->ID );
+
+		$result = $this->email_api_controller->reset_response( $request );
+
+		$this->assertInstanceOf( \WP_Error::class, $result );
+		$this->assertSame( 'woocommerce_email_not_found', $result->get_error_code() );
+		$this->assertSame( 404, $result->get_error_data()['status'] );
+	}
+
+	/**
+	 * @testdox Should reset content but skip meta stamping for emails absent from the sync registry.
+	 */
+	public function test_reset_response_resets_content_without_meta_for_non_sync_enabled_email(): void {
+		$email_type = 'customer_new_account';
+
+		$generator = new WCTransactionalEmailPostsGenerator();
+		$generator->init_default_transactional_emails();
+
+		$post_manager = WCTransactionalEmailPostsManager::get_instance();
+		$post_manager->clear_caches();
+		$post_manager->delete_email_template( $email_type );
+
+		$post_id = $generator->generate_email_template_if_not_exists( $email_type );
+		$this->assertIsInt( $post_id );
+
+		// Capture meta stamped at generation time so we can assert it is unchanged after reset.
+		$baseline_version     = (string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::VERSION_META_KEY, true );
+		$baseline_source_hash = (string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::SOURCE_HASH_META_KEY, true );
+		$baseline_synced_at   = (string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::LAST_SYNCED_AT_META_KEY, true );
+
+		// Simulate a customised post so the content reset is observable.
+		wp_update_post(
+			array(
+				'ID'           => $post_id,
+				'post_content' => '<!-- wp:paragraph --><p>Customised by merchant</p><!-- /wp:paragraph -->',
+			)
+		);
+
+		// Forcibly empty the registry so the email is not sync-enabled.
+		WCEmailTemplateSyncRegistry::reset_cache();
+		add_filter( 'woocommerce_transactional_emails_for_block_editor', '__return_empty_array' );
+
+		$request = new \WP_REST_Request( 'POST', '/woocommerce-email-editor/v1/emails/' . $post_id . '/reset' );
+		$request->set_param( 'id', $post_id );
+
+		$result = $this->email_api_controller->reset_response( $request );
+
+		$this->assertInstanceOf( \WP_REST_Response::class, $result );
+		$this->assertSame( 200, $result->get_status() );
+
+		$email = $this->resolve_wc_email( $email_type );
+		$this->assertNotNull( $email );
+		$expected_canonical = WCTransactionalEmailPostsGenerator::compute_canonical_post_content( $email );
+
+		$response_data = $result->get_data();
+		$this->assertSame( $expected_canonical, $response_data['content'], 'Response content must equal canonical core render.' );
+		$this->assertNull( $response_data['version'], 'Response version must be null for non-sync-enabled emails.' );
+		$this->assertNull( $response_data['source_hash'], 'Response source_hash must be null for non-sync-enabled emails.' );
+		$this->assertNull( $response_data['synced_at'], 'Response synced_at must be null for non-sync-enabled emails.' );
+		$this->assertNull( $response_data['status'], 'Response status must be null for non-sync-enabled emails.' );
+
+		$this->assertSame(
+			$expected_canonical,
+			(string) get_post_field( 'post_content', $post_id ),
+			'post_content must be reset to canonical render even when the email is not sync-enabled.'
+		);
+
+		// Stamping must NOT have run. Meta values stay at whatever the generator wrote at creation time.
+		$this->assertSame(
+			$baseline_version,
+			(string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::VERSION_META_KEY, true ),
+			'_wc_email_template_version must not be touched when the email is not sync-enabled.'
+		);
+		$this->assertSame(
+			$baseline_source_hash,
+			(string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::SOURCE_HASH_META_KEY, true ),
+			'_wc_email_template_source_hash must not be touched when the email is not sync-enabled.'
+		);
+		$this->assertSame(
+			$baseline_synced_at,
+			(string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::LAST_SYNCED_AT_META_KEY, true ),
+			'_wc_email_last_synced_at must not be touched when the email is not sync-enabled.'
+		);
+		$this->assertSame(
+			'',
+			(string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::STATUS_META_KEY, true ),
+			'_wc_email_template_status must not be set when the email is not sync-enabled.'
+		);
+	}
+
+	/**
+	 * @testdox Should return 500 when controller has not been initialized.
+	 */
+	public function test_reset_response_returns_500_when_uninitialized(): void {
+		$controller = new EmailApiController();
+		// Intentionally skip init() to leave dependencies null.
+
+		$request = new \WP_REST_Request( 'POST', '/woocommerce-email-editor/v1/emails/0/reset' );
+		$request->set_param( 'id', 0 );
+
+		$result = $controller->reset_response( $request );
+
+		$this->assertInstanceOf( \WP_Error::class, $result );
+		$this->assertSame( 'woocommerce_email_editor_not_initialized', $result->get_error_code() );
+		$this->assertSame( 500, $result->get_error_data()['status'] );
+	}
+
+	/**
+	 * @testdox Should register a POST /reset route alongside the existing default-content route.
+	 */
+	public function test_register_routes_registers_reset_endpoint(): void {
+		$rest_server = rest_get_server();
+		$this->email_api_controller->register_routes();
+
+		$routes = $rest_server->get_routes();
+		$this->assertArrayHasKey( '/woocommerce-email-editor/v1/emails/(?P<id>\d+)/reset', $routes );
+
+		$reset_route_handlers = $routes['/woocommerce-email-editor/v1/emails/(?P<id>\d+)/reset'];
+		$methods              = array();
+		foreach ( $reset_route_handlers as $handler ) {
+			foreach ( array_keys( $handler['methods'] ) as $method ) {
+				$methods[ $method ] = true;
+			}
+		}
+		$this->assertArrayHasKey( 'POST', $methods, 'Reset endpoint must accept POST.' );
+	}
+
+	/**
+	 * Helper: resolve a WC_Email instance by email type ID.
+	 *
+	 * @param string $email_type Email type ID.
+	 * @return \WC_Email|null
+	 */
+	private function resolve_wc_email( string $email_type ): ?\WC_Email {
+		foreach ( WC()->mailer()->get_emails() as $email ) {
+			if ( $email->id === $email_type ) {
+				return $email;
+			}
+		}
+		return null;
+	}
 }