From 382786cb9dc8bc3928bfb5470de799974c3e0fe2 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sat, 26 Sep 2026 23:36:55 +0800 Subject: [PATCH] feat(2meet-data-optimizer): set an entity bridge mode from the CLI under the Promotion Guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New `wp tmdo bridge-mode ` goes through TMDO_Mode_Manager::set(): unsafe jumps and aeav_only promotions refused by the Promotion Guard exit with the reason and leave the mode unchanged. Named apart from the Feature Flags `mode-set`. - member-sso-sync and member-force-logout warn that they are deprecated: the user sso group they work on is no longer registered. - Verify: CliBridgeModeTest (RED: method missing); dev36 bridge-mode user aeav_only refused (unsafe jump), term dual_write → shadow_read → aeav_only refused by the HivePress guard → back to disabled, wpdo_bridge_modes identical to before. - PR: PR-069 --- cli/class-tmdo-cli-member.php | 12 +++++-- cli/class-tmdo-cli-v2.php | 36 ++++++++++++++++++++ tests/unit/CliBridgeModeTest.php | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 tests/unit/CliBridgeModeTest.php diff --git a/cli/class-tmdo-cli-member.php b/cli/class-tmdo-cli-member.php index 66c3e7c..a3641b6 100644 --- a/cli/class-tmdo-cli-member.php +++ b/cli/class-tmdo-cli-member.php @@ -9,8 +9,8 @@ * wp wpdo member-shadow-report — Recent entity_type=user diff records. * wp wpdo member-cutover — Switch user mode to aeav_only. * wp wpdo member-points-check — Show balance + recent ledger for a user. - * wp wpdo member-sso-sync — Sync Hub membership claims to flat table. - * wp wpdo member-force-logout — Set token_expires_at to past → re-auth. + * wp wpdo member-sso-sync — (deprecated) Sync Hub membership claims to flat table. + * wp wpdo member-force-logout — (deprecated) Set token_expires_at to past → re-auth. * * @package WP_Data_Optimizer * @since 2.5.5 @@ -358,6 +358,9 @@ class TMDO_CLI_Member { /** * Sync Hub membership claims into the user flat table. * + * Deprecated since 1.2.0: the user `sso` group is no longer registered, so + * its flat table is never created on new installs. Will be removed. + * * Reads _tmso_refresh_token / _tmso_picture_url / _tmso_last_id_token * from usermeta and writes them into the sso group flat table. * Intended as a one-shot sync until 2meet-spoke-sso v1.14+ starts writing @@ -383,6 +386,7 @@ class TMDO_CLI_Member { * @param array $assoc_args Named arguments. */ public function member_sso_sync( $args, $assoc_args ): void { + WP_CLI::warning( 'member-sso-sync is deprecated since 1.2.0 (the user sso group is no longer registered) and will be removed.' ); global $wpdb; $user_id = isset( $assoc_args['user-id'] ) ? (int) $assoc_args['user-id'] : 0; @@ -468,6 +472,9 @@ class TMDO_CLI_Member { /** * Force re-authentication by setting token_expires_at to a past datetime. * + * Deprecated since 1.2.0: works on the user `sso` flat table, which is no + * longer registered. Will be removed. + * * The Spoke silent-refresh logic detects an expired token_expires_at and * redirects the user to re-authenticate against the Hub. If the Hub has * the account blocked, the user is logged out from all Spokes. @@ -492,6 +499,7 @@ class TMDO_CLI_Member { * @param array $assoc_args Named arguments. */ public function member_force_logout( $args, $assoc_args ): void { + WP_CLI::warning( 'member-force-logout is deprecated since 1.2.0 (the user sso group is no longer registered) and will be removed.' ); if ( ! isset( $assoc_args['confirm'] ) ) { WP_CLI::error( 'This invalidates live SSO tokens. Add --confirm to proceed.' ); } diff --git a/cli/class-tmdo-cli-v2.php b/cli/class-tmdo-cli-v2.php index fe6c07e..9da5a69 100644 --- a/cli/class-tmdo-cli-v2.php +++ b/cli/class-tmdo-cli-v2.php @@ -152,6 +152,40 @@ class TMDO_CLI_V2 { WP_CLI::success( "Hook Bus: {$state}" ); } + /** + * Set an entity's bridge mode. Goes through TMDO_Mode_Manager::set(): unsafe + * jumps and promotions refused by the Promotion Guard (aeav_only on + * HivePress / WooCommerce sites) fail and leave the mode unchanged. + * + * ## OPTIONS + * + * + * : post | user | term | comment + * + * + * : disabled | dual_write | shadow_read | aeav_only + * + * ## EXAMPLES + * + * wp tmdo bridge-mode user dual_write + * wp tmdo bridge-mode post disabled + * + * @param array $args Entity type and mode. + * @param array $assoc_args Unused. + */ + public function bridge_mode( $args, $assoc_args ): void { + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed + $entity = (string) ( $args[0] ?? '' ); + $mode = (string) ( $args[1] ?? '' ); + $from = TMDO_Mode_Manager::get( $entity ); + + $result = TMDO_Mode_Manager::set( $entity, $mode ); + if ( is_wp_error( $result ) ) { + WP_CLI::error( $result->get_error_message() ); + } + WP_CLI::success( "{$entity}: {$from} → {$mode}" ); + } + /** * Write an entity's flat-table values back to native meta — the way out of * aeav_only. Resumable; re-run after an interruption. @@ -918,6 +952,8 @@ WP_CLI::add_command( 'wpdo bridge-status', array( 'TMDO_CLI_V2', 'bridge_status' WP_CLI::add_command( 'tmdo bridge-status', array( 'TMDO_CLI_V2', 'bridge_status' ) ); WP_CLI::add_command( 'wpdo bridge-set', array( 'TMDO_CLI_V2', 'bridge_set' ) ); WP_CLI::add_command( 'tmdo bridge-set', array( 'TMDO_CLI_V2', 'bridge_set' ) ); +WP_CLI::add_command( 'wpdo bridge-mode', array( new TMDO_CLI_V2(), 'bridge_mode' ) ); +WP_CLI::add_command( 'tmdo bridge-mode', array( new TMDO_CLI_V2(), 'bridge_mode' ) ); WP_CLI::add_command( 'wpdo bridge-restore-native', array( new TMDO_CLI_V2(), 'bridge_restore_native' ) ); WP_CLI::add_command( 'tmdo bridge-restore-native', array( new TMDO_CLI_V2(), 'bridge_restore_native' ) ); WP_CLI::add_command( 'wpdo repair-array-literals', array( new TMDO_CLI_V2(), 'repair_array_literals' ) ); diff --git a/tests/unit/CliBridgeModeTest.php b/tests/unit/CliBridgeModeTest.php new file mode 100644 index 0000000..4c7dc80 --- /dev/null +++ b/tests/unit/CliBridgeModeTest.php @@ -0,0 +1,57 @@ + ` (2.2 PR-069): the CLI way to change an + * entity's bridge mode. It must go through TMDO_Mode_Manager::set(), so unsafe + * jumps and promotions refused by the Promotion Guard never reach the option. + * (The Guard matrix itself is covered by PromotionGuardTest.) + */ +class CliBridgeModeTest extends TestCase { + + protected function setUp(): void { + ini_set( 'error_log', '/dev/null' ); + $GLOBALS['_wp_options'] = array(); + $GLOBALS['_wp_filter_callbacks'] = array(); + $GLOBALS['_wp_current_user_can'] = array( 'manage_options' => true ); + TMDO_Mode_Manager::reset_cache(); + } + + private function modes( array $modes ): void { + $GLOBALS['_wp_options']['wpdo_bridge_modes'] = $modes; + TMDO_Mode_Manager::reset_cache(); + } + + public function test_sets_the_mode_of_one_entity(): void { + $this->modes( array( 'user' => 'disabled', 'post' => 'dual_write' ) ); + + ( new TMDO_CLI_V2() )->bridge_mode( array( 'user', 'dual_write' ), array() ); + + $this->assertSame( 'dual_write', TMDO_Mode_Manager::get( 'user' ) ); + $this->assertSame( 'dual_write', TMDO_Mode_Manager::get( 'post' ), 'other entities untouched' ); + } + + public function test_an_unsafe_jump_is_refused(): void { + $this->modes( array( 'user' => 'disabled' ) ); + + ( new TMDO_CLI_V2() )->bridge_mode( array( 'user', 'aeav_only' ), array() ); + + $this->assertSame( 'disabled', TMDO_Mode_Manager::get( 'user' ) ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_a_promotion_refused_by_the_guard_leaves_the_mode_unchanged(): void { + eval( 'namespace HivePress; class Core {}' ); + $this->modes( array( 'user' => 'shadow_read' ) ); + + ( new TMDO_CLI_V2() )->bridge_mode( array( 'user', 'aeav_only' ), array() ); + + $this->assertSame( 'shadow_read', TMDO_Mode_Manager::get( 'user' ) ); + $this->assertSame( array( 'user' => 'shadow_read' ), $GLOBALS['_wp_options']['wpdo_bridge_modes'] ); + } +}