feat(2meet-data-optimizer): set an entity bridge mode from the CLI under the Promotion Guard

- New `wp tmdo bridge-mode <entity> <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
This commit is contained in:
2026-09-26 23:36:55 +08:00
parent 2022cf1d08
commit 382786cb9d
3 changed files with 103 additions and 2 deletions
+10 -2
View File
@@ -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.' );
}
+36
View File
@@ -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
*
* <entity>
* : post | user | term | comment
*
* <mode>
* : disabled | dual_write | shadow_read | aeav_only
*
* ## EXAMPLES
*
* wp tmdo bridge-mode user dual_write
* wp tmdo bridge-mode post disabled
*
* @param array<int, string> $args Entity type and mode.
* @param array<string, string> $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' ) );
+57
View File
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* `wp tmdo bridge-mode <entity> <mode>` (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'] );
}
}