hardening 2.2: 2meet-data-optimizer 1.2.0 (drift detection, scope, CLI and diagnostics) #6

Merged
wpdev merged 19 commits from hardening/2.2 into main 2026-09-27 10:56:49 +08:00
5 changed files with 62 additions and 7 deletions
Showing only changes of commit 1b21ee9add - Show all commits
+1 -1
View File
@@ -240,7 +240,7 @@ class TMDO_CLI_V2 {
} else { } else {
$report = TMDO_Drift_Check::run( $entity, $group, $sample ); $report = TMDO_Drift_Check::run( $entity, $group, $sample );
} }
} catch ( InvalidArgumentException $e ) { } catch ( InvalidArgumentException | RuntimeException $e ) {
WP_CLI::error( $e->getMessage() ); WP_CLI::error( $e->getMessage() );
return; return;
} }
+12 -5
View File
@@ -103,13 +103,15 @@ final class TMDO_Drift_Check {
} }
/** /**
* Daily check (wpdo_daily_health_check): sample every entity type whose * Daily check (wpdo_daily_health_check): sample every entity type written
* flat tables are written (mode dual_write or higher). The outcome replaces * to both native and flat (dual_write, shadow_read). In aeav_only flat is
* the recorded status, so a group found drifting leaves the router at once. * the source of truth and native is stale by design, so drift means nothing
* there. The outcome replaces the recorded status, so a group found
* drifting leaves the router at once.
*/ */
public static function cron_tick(): void { public static function cron_tick(): void {
foreach ( TMDO_Entity_Registry::VALID_ENTITY_TYPES as $entity_type ) { foreach ( TMDO_Entity_Registry::VALID_ENTITY_TYPES as $entity_type ) {
if ( ! TMDO_Mode_Manager::writes_to_flat( $entity_type ) ) { if ( ! TMDO_Mode_Manager::writes_to_flat( $entity_type ) || ! TMDO_Mode_Manager::writes_to_eav( $entity_type ) ) {
continue; continue;
} }
try { try {
@@ -132,11 +134,16 @@ final class TMDO_Drift_Check {
* @param string $entity_type post | user | term | comment. * @param string $entity_type post | user | term | comment.
* @param string $group One group, or '' for every backfillable group. * @param string $group One group, or '' for every backfillable group.
* @return array{fixed:array{rebuilt:int, cleared:int, deleted_rows:int}, after:array<string, mixed>} * @return array{fixed:array{rebuilt:int, cleared:int, deleted_rows:int}, after:array<string, mixed>}
* @throws InvalidArgumentException Unknown entity type or group. * @throws RuntimeException In aeav_only, where flat is the source of truth
* (and InvalidArgumentException via resolve() for an
* unknown entity type or group).
*/ */
public static function fix( string $entity_type, string $group = '' ): array { public static function fix( string $entity_type, string $group = '' ): array {
global $wpdb; global $wpdb;
list( $adapter, $groups ) = self::resolve( $entity_type, $group ); list( $adapter, $groups ) = self::resolve( $entity_type, $group );
if ( TMDO_Mode_Manager::MODE_AEAV_ONLY === TMDO_Mode_Manager::get( $entity_type ) ) {
throw new RuntimeException( esc_html( $entity_type ) . ' is in aeav_only: flat is the source of truth and native meta is stale, so it cannot be rebuilt from native. Write flat back first (wp tmdo bridge-restore-native).' );
}
$id = $adapter->get_entity_id_column(); $id = $adapter->get_entity_id_column();
$fixed = array( $fixed = array(
@@ -525,12 +525,16 @@ class TMDO_Post_Query_Router {
/** /**
* The last `wp tmdo drift-check` found the group out of step with native * The last `wp tmdo drift-check` found the group out of step with native
* meta. A group never checked keeps routing. * meta. A group never checked keeps routing, and so does every group in
* aeav_only, where native is stale by design and no fallback.
* *
* @param string $group Entity group name. * @param string $group Entity group name.
* @return bool * @return bool
*/ */
private static function drifting( string $group ): bool { private static function drifting( string $group ): bool {
if ( TMDO_Mode_Manager::MODE_AEAV_ONLY === TMDO_Mode_Manager::get( 'post' ) ) {
return false;
}
$status = get_option( TMDO_Drift_Check::OPTION, array() ); $status = get_option( TMDO_Drift_Check::OPTION, array() );
$last = is_array( $status ) ? ( $status['post'][ $group ] ?? null ) : null; $last = is_array( $status ) ? ( $status['post'][ $group ] ?? null ) : null;
return is_array( $last ) && (int) ( $last['miss'] ?? 0 ) + (int) ( $last['diff'] ?? 0 ) + (int) ( $last['extra'] ?? 0 ) > 0; return is_array( $last ) && (int) ( $last['miss'] ?? 0 ) + (int) ( $last['diff'] ?? 0 ) + (int) ( $last['extra'] ?? 0 ) > 0;
+27
View File
@@ -214,6 +214,33 @@ class DriftCheckTest extends TestCase {
$this->assertFalse( get_option( TMDO_Drift_Check::OPTION ) ); $this->assertFalse( get_option( TMDO_Drift_Check::OPTION ) );
} }
public function test_the_daily_tick_skips_aeav_only_where_flat_is_the_source_of_truth(): void {
self::set_modes( array( 'post' => 'disabled', 'user' => 'aeav_only', 'term' => 'disabled', 'comment' => 'disabled' ) );
try {
TMDO_Drift_Check::cron_tick();
} finally {
self::set_modes( null );
}
$this->assertFalse( get_option( TMDO_Drift_Check::OPTION ) );
}
public function test_fix_refuses_aeav_only_and_leaves_flat_alone(): void {
global $wpdb;
$flat = md5( wp_json_encode( $wpdb->get_results( 'SELECT * FROM `' . self::TABLE . '` ORDER BY id', ARRAY_A ) ) );
self::set_modes( array( 'post' => 'disabled', 'user' => 'aeav_only', 'term' => 'disabled', 'comment' => 'disabled' ) );
try {
TMDO_Drift_Check::fix( 'user' );
$this->fail( 'fix() must refuse aeav_only' );
} catch ( RuntimeException $e ) {
$this->assertStringContainsString( 'aeav_only', $e->getMessage() );
} finally {
self::set_modes( null );
}
$this->assertSame( $flat, md5( wp_json_encode( $wpdb->get_results( 'SELECT * FROM `' . self::TABLE . '` ORDER BY id', ARRAY_A ) ) ) );
}
public function test_the_daily_tick_runs_on_the_daily_health_check_hook(): void { public function test_the_daily_tick_runs_on_the_daily_health_check_hook(): void {
$source = (string) file_get_contents( WPDO_PLUGIN_DIR . 'includes/class-tmdo-core.php' ); $source = (string) file_get_contents( WPDO_PLUGIN_DIR . 'includes/class-tmdo-core.php' );
@@ -100,6 +100,23 @@ class PostQueryRouterDriftTest extends TestCase {
$this->assertNotEmpty( $query->get( 'wpdo_post_clauses' ) ); $this->assertNotEmpty( $query->get( 'wpdo_post_clauses' ) );
} }
public function test_aeav_only_ignores_drift_because_native_is_stale_by_design(): void {
self::set_modes(
array(
'post' => 'aeav_only',
'user' => 'disabled',
'term' => 'disabled',
'comment' => 'disabled',
)
);
$this->drift( 'wc_product', 3 );
$query = $this->price_query();
( new WPDO_Post_Query_Router() )->pre_get_posts( $query );
$this->assertNotEmpty( $query->get( 'wpdo_post_clauses' ) );
}
public function test_sorting_by_a_drifting_key_stays_native(): void { public function test_sorting_by_a_drifting_key_stays_native(): void {
$this->drift( 'wc_product', 1 ); $this->drift( 'wc_product', 1 );
$query = new WP_Query(); $query = new WP_Query();