Files
2meet-data-optimizer/tests/integration/ShadowDiffThrottleTest.php
wpdev 9d1a7c5591 feat(2meet-data-optimizer): throttle shadow_read diffs per key and promote on clean drift checks
- The shadow diff logger records at most 20 rows per entity type + key per hour across all objects, on top of the existing per-object 5-minute window: a key stale on every object (WooCommerce total_sales, written by direct SQL) no longer logs a row per product read.
- The auto-promoter's third condition is now TMDO_Drift_Check::clean_since(): every backfillable group of the entity checked within 2 days (sampled or full) with no drift; no record, no promotion. The shadow diff count no longer gates it (the filter's 4th argument is passed as 0). The Promotion Guard still refuses HivePress / WooCommerce sites.
- Reason classification was dropped (decision 2026-09-27): it needs a schema change and drift-check already reports miss / diff / extra per key.
- Verify: ShadowDiffThrottleTest, AutoPromoterDriftTest (RED before); dev36 simulated user shadow_read: clean_since yes, promotion still refused by the guard, mode unchanged.
- PR: PR-075
2026-09-27 00:48:18 +08:00

90 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* TMDO_Shadow_Diff_Logger throttling (2.2 PR-075).
*
* The per-object window (one row per entity + key per 5 minutes) did not stop
* a key that is stale on every object — WooCommerce's total_sales, written by
* direct SQL — from logging a row for each product read. A key now records at
* most KEY_LIMIT rows per KEY_WINDOW across all objects.
*/
class ShadowDiffThrottleTest extends TestCase {
private const DIFFS = 'wp_itest_wpdo_shadow_diffs';
private const ERRORS = 'wp_itest_wpdo_errors';
public static function setUpBeforeClass(): void {
foreach ( array(
'includes/adapters/interface-entity-adapter.php',
'includes/adapters/class-tmdo-adapter-user.php',
'includes/engine/class-tmdo-shadow-diff-logger.php',
) as $file ) {
require_once WPDO_PLUGIN_DIR . $file;
}
}
protected function setUp(): void {
global $wpdb;
self::drop();
$wpdb->query( "CREATE TABLE `{$wpdb->usermeta}` ( umeta_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, meta_key VARCHAR(255) DEFAULT NULL, meta_value LONGTEXT, PRIMARY KEY (umeta_id), KEY user_id (user_id) ) ENGINE=InnoDB" );
$wpdb->query( 'CREATE TABLE `' . self::DIFFS . "` ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, ts DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', entity_type VARCHAR(20) NOT NULL DEFAULT '', entity_id BIGINT UNSIGNED NOT NULL DEFAULT 0, meta_key VARCHAR(255) NOT NULL DEFAULT '', postmeta_value LONGTEXT, zone_value LONGTEXT, diff_hash VARCHAR(40) NOT NULL DEFAULT '', PRIMARY KEY (id) ) ENGINE=InnoDB" );
$wpdb->query( 'CREATE TABLE `' . self::ERRORS . "` ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, module VARCHAR(100) NOT NULL DEFAULT '', zone VARCHAR(20) NOT NULL DEFAULT '', hook VARCHAR(255) NOT NULL DEFAULT '', message TEXT NOT NULL, context LONGTEXT, created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (id) ) ENGINE=InnoDB" );
for ( $id = 1; $id <= 30; $id++ ) {
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => $id, 'meta_key' => 'total_sales', 'meta_value' => '5' ) );
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => $id, 'meta_key' => 'other_key', 'meta_value' => '5' ) );
}
$GLOBALS['_wp_transients'] = array();
WPDO_Entity_Registry::init();
WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() );
}
protected function tearDown(): void {
self::drop();
WPDO_Entity_Registry::init();
}
private static function drop(): void {
global $wpdb;
foreach ( array( $wpdb->usermeta, self::DIFFS, self::ERRORS ) as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
}
}
private function logged( string $key ): int {
global $wpdb;
return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM `' . self::DIFFS . '` WHERE meta_key = %s', $key ) );
}
private function stale_read( int $user_id, string $key ): void {
TMDO_Shadow_Diff_Logger::compare_and_log( 'user', $user_id, $key, 4, array( 'type' => 'integer' ) );
}
public function test_a_key_stale_on_every_object_logs_at_most_the_key_limit(): void {
for ( $id = 1; $id <= 30; $id++ ) {
$this->stale_read( $id, 'total_sales' );
}
$this->assertSame( TMDO_Shadow_Diff_Logger::KEY_LIMIT, $this->logged( 'total_sales' ) );
}
public function test_the_limit_is_per_key(): void {
for ( $id = 1; $id <= 30; $id++ ) {
$this->stale_read( $id, 'total_sales' );
}
$this->stale_read( 1, 'other_key' );
$this->assertSame( 1, $this->logged( 'other_key' ) );
}
public function test_the_same_object_and_key_still_log_once_per_window(): void {
$this->stale_read( 1, 'total_sales' );
$this->stale_read( 1, 'total_sales' );
$this->assertSame( 1, $this->logged( 'total_sales' ) );
}
}