d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
263 lines
10 KiB
PHP
263 lines
10 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Integration test: WPDO_Sync_Bridge entity-bridge guard (v2.9.2).
|
|
*
|
|
* Verifies that when WPDO_Entity_Registry has registered a meta_key for
|
|
* entity_type=post AND post mode is dual_write or higher, Sync_Bridge
|
|
* skips its zone write so the same value isn't written to two flat tables.
|
|
*
|
|
* This is the central correctness guarantee for v2.9.2 — without it, the
|
|
* 5 keys overlapping between Schema_Registry hot zone and the new post
|
|
* Entity Registry (hp_price, hp_featured, hp_verified, _price, _stock)
|
|
* would receive triple writes (zone + entity flat + wp_postmeta).
|
|
*
|
|
* Behavior matrix:
|
|
* post mode = disabled → Sync_Bridge writes zone (legacy unchanged)
|
|
* post mode = dual_write+ + key in Entity_Registry → Sync_Bridge skips zone
|
|
* post mode = dual_write+ + key NOT in Entity_Registry → Sync_Bridge writes zone (back-compat)
|
|
*/
|
|
class SyncBridgeEntityGuardTest extends TestCase {
|
|
|
|
private const POST_TYPE = 'hp_listing';
|
|
private const TABLE = 'wp_itest_wpdo_hot_hp_listing';
|
|
private const MODULE = 'hot_hp_listing';
|
|
private const ENTITY_KEY = 'hp_price'; // overlaps Entity_Registry hp_listing_core
|
|
private const ZONE_ONLY_KEY = 'hp_legacy_only'; // only in Schema_Registry, not Entity_Registry
|
|
|
|
private WPDO_Sync_Bridge $bridge;
|
|
|
|
// ── Fixture lifecycle ─────────────────────────────────────────────────────
|
|
|
|
public static function setUpBeforeClass(): void {
|
|
global $wpdb;
|
|
|
|
// Load Entity_Registry chain (interface → adapter → registry → mode-manager).
|
|
if ( ! interface_exists( 'WPDO_Entity_Adapter_Interface' ) ) {
|
|
require_once WPDO_PLUGIN_DIR . 'includes/adapters/interface-entity-adapter.php';
|
|
}
|
|
if ( ! class_exists( 'WPDO_Entity_Registry' ) ) {
|
|
require_once WPDO_PLUGIN_DIR . 'includes/engine/class-tmdo-entity-registry.php';
|
|
}
|
|
if ( ! class_exists( 'WPDO_Mode_Manager' ) ) {
|
|
require_once WPDO_PLUGIN_DIR . 'includes/engine/class-tmdo-mode-manager.php';
|
|
}
|
|
if ( ! class_exists( 'WPDO_Adapter_Post' ) ) {
|
|
require_once WPDO_PLUGIN_DIR . 'includes/adapters/class-tmdo-adapter-post.php';
|
|
}
|
|
if ( ! class_exists( 'WPDO_Post_Fields' ) ) {
|
|
require_once WPDO_PLUGIN_DIR . 'includes/integrations/class-tmdo-post-fields.php';
|
|
}
|
|
|
|
// Hot zone test table (legacy Sync_Bridge target).
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE . '`' );
|
|
$wpdb->query(
|
|
'CREATE TABLE `' . self::TABLE . '` (
|
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
`post_id` bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
`hp_price` decimal(10,2) DEFAULT NULL,
|
|
`hp_legacy_only` varchar(255) DEFAULT NULL,
|
|
`updated_at` datetime NOT NULL DEFAULT \'0000-00-00 00:00:00\',
|
|
PRIMARY KEY (`id`),
|
|
UNIQUE KEY `post_id` (`post_id`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
|
);
|
|
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `wp_itest_wpdo_errors`' );
|
|
$wpdb->query(
|
|
'CREATE TABLE `wp_itest_wpdo_errors` (
|
|
`id` bigint(20) 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 DEFAULT CHARSET=utf8mb4'
|
|
);
|
|
|
|
// Reset Schema_Registry singleton + register both keys (one will overlap with Entity_Registry).
|
|
$ref = new ReflectionClass( WPDO_Schema_Registry::class );
|
|
$inst = $ref->getProperty( 'instance' );
|
|
$inst->setAccessible( true );
|
|
$inst->setValue( null, null );
|
|
|
|
WPDO_Schema_Registry::instance()->register( 'test', array(
|
|
'post_type' => self::POST_TYPE,
|
|
'meta_key' => self::ENTITY_KEY,
|
|
'zone' => 'hot',
|
|
'data_type' => 'decimal(10,2) NOT NULL DEFAULT 0',
|
|
'column' => self::ENTITY_KEY,
|
|
'indexed' => false,
|
|
) );
|
|
WPDO_Schema_Registry::instance()->register( 'test', array(
|
|
'post_type' => self::POST_TYPE,
|
|
'meta_key' => self::ZONE_ONLY_KEY,
|
|
'zone' => 'hot',
|
|
'data_type' => 'varchar(255) DEFAULT NULL',
|
|
'column' => self::ZONE_ONLY_KEY,
|
|
'indexed' => false,
|
|
) );
|
|
|
|
// Register post adapter + post-fields groups (puts hp_price into Entity_Registry).
|
|
WPDO_Entity_Registry::init();
|
|
WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() );
|
|
WPDO_Post_Fields::register_entity_fields();
|
|
}
|
|
|
|
public static function tearDownAfterClass(): void {
|
|
global $wpdb;
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE . '`' );
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `wp_itest_wpdo_errors`' );
|
|
|
|
// Reset Mode_Manager cache to prevent post=dual_write leaking into
|
|
// later tests that share the same PHP process (e.g. SyncBridgeIntegrationTest
|
|
// which uses 'hp_price' as a generic test field — that key is in the post
|
|
// Entity_Registry once we've registered it here, so the guard would fire
|
|
// in those tests' assertions if mode is still cached as dual_write).
|
|
$ref = new ReflectionClass( WPDO_Mode_Manager::class );
|
|
$cache = $ref->getProperty( 'cache' );
|
|
$cache->setAccessible( true );
|
|
$cache->setValue( null, null );
|
|
|
|
// Also reset Entity_Registry so the registered post groups don't leak.
|
|
WPDO_Entity_Registry::init();
|
|
}
|
|
|
|
protected function setUp(): void {
|
|
global $wpdb;
|
|
$wpdb->query( 'TRUNCATE TABLE `' . self::TABLE . '`' );
|
|
|
|
$GLOBALS['_wp_options'] = array();
|
|
WPDO_Feature_Flags::set( self::MODULE, 'dual_write' );
|
|
|
|
// Reset Sync_Bridge state.
|
|
$ref = new ReflectionClass( WPDO_Sync_Bridge::class );
|
|
$cache = $ref->getProperty( 'field_cache' );
|
|
$cache->setAccessible( true );
|
|
$cache->setValue( null, array() );
|
|
$bypass = $ref->getProperty( 'bypassing' );
|
|
$bypass->setAccessible( true );
|
|
$bypass->setValue( null, false );
|
|
|
|
// Reset Mode_Manager cache to default (post=disabled).
|
|
// Tests that need dual_write override via set_post_mode() helper below,
|
|
// which writes the cache directly (avoiding Cache_Orchestrator dep).
|
|
self::set_post_mode( 'disabled' );
|
|
|
|
// Seed post-type lookup.
|
|
$GLOBALS['_wp_post_types'] = array();
|
|
for ( $i = 1; $i <= 20; $i++ ) {
|
|
$GLOBALS['_wp_post_types'][ $i ] = self::POST_TYPE;
|
|
}
|
|
|
|
$GLOBALS['_wp_cache'] = array();
|
|
|
|
$this->bridge = new WPDO_Sync_Bridge();
|
|
}
|
|
|
|
/**
|
|
* Set Mode_Manager post mode by writing the static cache directly,
|
|
* bypassing set() which has a hard dep on WPDO_Cache_Orchestrator
|
|
* (out of scope for this guard test).
|
|
*/
|
|
private static function set_post_mode( string $mode ): void {
|
|
$ref = new ReflectionClass( WPDO_Mode_Manager::class );
|
|
$cache = $ref->getProperty( 'cache' );
|
|
$cache->setAccessible( true );
|
|
$cache->setValue( null, array(
|
|
'post' => $mode,
|
|
'user' => 'aeav_only', // user mode frozen — must not change
|
|
'term' => 'dual_write',
|
|
'comment' => 'dual_write',
|
|
) );
|
|
}
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Baseline: post mode = disabled (default) — Sync_Bridge MUST still write zone.
|
|
* This guarantees v2.9.1 → v2.9.2 upgrade is zero-impact for users who
|
|
* haven't opted in to Entity Bridge post mode.
|
|
*/
|
|
public function test_zone_write_unchanged_when_post_mode_disabled(): void {
|
|
// post mode defaults to disabled — Mode_Manager reads from option.
|
|
$this->bridge->intercept_update( null, 1, self::ENTITY_KEY, '199.99', '' );
|
|
|
|
$val = WPDO_Zone_Hot::get( 1, self::POST_TYPE, self::ENTITY_KEY );
|
|
$this->assertSame(
|
|
'199.99',
|
|
$val,
|
|
'mode=disabled: Sync_Bridge must continue writing zone (legacy back-compat).'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Guard: post mode = dual_write + key registered in Entity_Registry
|
|
* → Sync_Bridge skips zone write (Entity Bridge will handle it).
|
|
*/
|
|
public function test_zone_skipped_when_post_mode_dual_write_and_key_in_entity_registry(): void {
|
|
self::set_post_mode( 'dual_write' );
|
|
|
|
$this->bridge->intercept_update( null, 2, self::ENTITY_KEY, '299.99', '' );
|
|
|
|
$val = WPDO_Zone_Hot::get( 2, self::POST_TYPE, self::ENTITY_KEY );
|
|
$this->assertNull(
|
|
$val,
|
|
'mode=dual_write + Entity_Registry has key: Sync_Bridge MUST skip zone write to avoid double-write.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Back-compat: post mode = dual_write + key NOT in Entity_Registry
|
|
* → Sync_Bridge still writes zone (only Entity_Registry-managed keys are skipped).
|
|
*/
|
|
public function test_zone_write_continues_for_zone_only_key_when_post_mode_dual_write(): void {
|
|
self::set_post_mode( 'dual_write' );
|
|
|
|
// hp_legacy_only is in Schema_Registry only — not in Entity_Registry.
|
|
$this->bridge->intercept_update( null, 3, self::ZONE_ONLY_KEY, 'legacy_value', '' );
|
|
|
|
$val = WPDO_Zone_Hot::get( 3, self::POST_TYPE, self::ZONE_ONLY_KEY );
|
|
$this->assertSame(
|
|
'legacy_value',
|
|
$val,
|
|
'mode=dual_write but key not in Entity_Registry: Sync_Bridge must keep writing zone (back-compat).'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The intercept_update return value must remain null in all branches —
|
|
* we never short-circuit WP native postmeta in v2.9.2 (still dual_write
|
|
* w.r.t. wp_postmeta; cutover comes in v2.9.5).
|
|
*/
|
|
public function test_intercept_returns_null_regardless_of_guard(): void {
|
|
self::set_post_mode( 'dual_write' );
|
|
|
|
$result_skipped = $this->bridge->intercept_update( null, 4, self::ENTITY_KEY, '50.00', '' );
|
|
$result_written = $this->bridge->intercept_update( null, 5, self::ZONE_ONLY_KEY, 'x', '' );
|
|
|
|
$this->assertNull( $result_skipped, 'Guard branch must still return null.' );
|
|
$this->assertNull( $result_written, 'Non-guard branch must still return null.' );
|
|
}
|
|
|
|
/**
|
|
* intercept_add must apply the same guard.
|
|
*/
|
|
public function test_add_zone_skipped_when_entity_registry_owns_key(): void {
|
|
self::set_post_mode( 'dual_write' );
|
|
|
|
$this->bridge->intercept_add( null, 6, self::ENTITY_KEY, '99.99', true );
|
|
|
|
$val = WPDO_Zone_Hot::get( 6, self::POST_TYPE, self::ENTITY_KEY );
|
|
$this->assertNull(
|
|
$val,
|
|
'intercept_add must apply the Entity_Registry guard symmetrically with intercept_update.'
|
|
);
|
|
}
|
|
}
|