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.' ); } }