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, `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' ); // Create the errors log table so WPDO_Logger::error() can write to it. $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 and populate the Schema Registry singleton. $ref = new ReflectionClass( WPDO_Schema_Registry::class ); $inst = $ref->getProperty( 'instance' ); $inst->setAccessible( true ); $inst->setValue( null, null ); WPDO_Schema_Registry::instance()->register( 'test_provider', [ 'post_type' => self::POST_TYPE, 'meta_key' => self::FIELD, 'zone' => 'hot', 'data_type' => 'decimal(10,2) NOT NULL DEFAULT 0', 'column' => self::FIELD, 'indexed' => false, ] ); } 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`' ); } protected function setUp(): void { global $wpdb; // Wipe data before each test. $wpdb->query( 'TRUNCATE TABLE `' . self::TABLE . '`' ); // Reset FeatureFlags (clears option + static cache). $GLOBALS['_wp_options'] = []; WPDO_Feature_Flags::set( self::MODULE, 'idle' ); // Reset SyncBridge request-level field cache. $ref = new ReflectionClass( WPDO_Sync_Bridge::class ); $cache = $ref->getProperty( 'field_cache' ); $cache->setAccessible( true ); $cache->setValue( null, [] ); // Reset $bypassing flag. $bypass = $ref->getProperty( 'bypassing' ); $bypass->setAccessible( true ); $bypass->setValue( null, false ); // Seed post-type lookup. $GLOBALS['_wp_post_types'] = []; for ( $i = 1; $i <= 20; $i++ ) { $GLOBALS['_wp_post_types'][ $i ] = self::POST_TYPE; } // Object cache reset. $GLOBALS['_wp_cache'] = []; $this->bridge = new WPDO_Sync_Bridge(); } // ── intercept_update ───────────────────────────────────────────────────── public function test_update_dual_write_writes_value_to_hot_table(): void { WPDO_Feature_Flags::set( self::MODULE, 'dual_write' ); $this->bridge->intercept_update( null, 1, self::FIELD, '199.99', '' ); $val = WPDO_Zone_Hot::get( 1, self::POST_TYPE, self::FIELD ); $this->assertSame( '199.99', $val ); } public function test_update_idle_does_not_write_to_hot_table(): void { // Module stays in 'idle' — is_write_active() returns false. $this->bridge->intercept_update( null, 2, self::FIELD, '50.00', '' ); $val = WPDO_Zone_Hot::get( 2, self::POST_TYPE, self::FIELD ); $this->assertNull( $val ); } public function test_update_always_returns_null_to_allow_native_write(): void { WPDO_Feature_Flags::set( self::MODULE, 'dual_write' ); $result = $this->bridge->intercept_update( null, 3, self::FIELD, '100.00', '' ); // Must return null (not short-circuit) so WordPress still writes postmeta. $this->assertNull( $result ); } public function test_update_skips_unregistered_meta_key(): void { WPDO_Feature_Flags::set( self::MODULE, 'dual_write' ); // 'hp_unregistered' is not in Schema Registry. $this->bridge->intercept_update( null, 4, 'hp_unregistered', '42.00', '' ); // Hot table for test_post should still be empty. $val = WPDO_Zone_Hot::get( 4, self::POST_TYPE, self::FIELD ); $this->assertNull( $val ); } public function test_update_skips_when_post_type_unknown(): void { WPDO_Feature_Flags::set( self::MODULE, 'dual_write' ); // post_id 999 not seeded in _wp_post_types → get_post_type() returns false. $this->bridge->intercept_update( null, 999, self::FIELD, '77.00', '' ); // Nothing should have been written (table doesn't have post 999). $val = WPDO_Zone_Hot::get( 999, self::POST_TYPE, self::FIELD ); $this->assertNull( $val ); } // ── intercept_add ──────────────────────────────────────────────────────── public function test_add_dual_write_writes_value_to_hot_table(): void { WPDO_Feature_Flags::set( self::MODULE, 'dual_write' ); $this->bridge->intercept_add( null, 5, self::FIELD, '299.00', true ); $val = WPDO_Zone_Hot::get( 5, self::POST_TYPE, self::FIELD ); $this->assertSame( '299.00', $val ); } public function test_add_returns_null_to_allow_native_write(): void { WPDO_Feature_Flags::set( self::MODULE, 'dual_write' ); $result = $this->bridge->intercept_add( null, 6, self::FIELD, '10.00', false ); $this->assertNull( $result ); } // ── intercept_get ──────────────────────────────────────────────────────── public function test_get_cutover_returns_zone_value_wrapped_in_array(): void { // Write directly to hot table, then verify intercept_get reads it back. WPDO_Zone_Hot::set( 7, self::POST_TYPE, self::FIELD, '500.00' ); WPDO_Feature_Flags::set( self::MODULE, 'cutover' ); $result = $this->bridge->intercept_get( null, 7, self::FIELD, true ); // SyncBridge wraps value in array so WP can unwrap correctly. $this->assertIsArray( $result ); $this->assertSame( '500.00', $result[0] ); } public function test_get_dual_write_returns_null_passthrough(): void { WPDO_Zone_Hot::set( 8, self::POST_TYPE, self::FIELD, '123.00' ); // dual_write is NOT a read-custom state. WPDO_Feature_Flags::set( self::MODULE, 'dual_write' ); $result = $this->bridge->intercept_get( null, 8, self::FIELD, true ); // Should pass through (return null) so WP reads from postmeta. $this->assertNull( $result ); } public function test_get_returns_null_when_no_zone_row(): void { // cutover state but no row in hot table. WPDO_Feature_Flags::set( self::MODULE, 'cutover' ); $result = $this->bridge->intercept_get( null, 9, self::FIELD, true ); $this->assertNull( $result ); } public function test_get_returns_null_for_empty_meta_key(): void { WPDO_Feature_Flags::set( self::MODULE, 'cutover' ); // Empty meta_key means "get all meta" — bridge should pass through. $result = $this->bridge->intercept_get( null, 10, '', true ); $this->assertNull( $result ); } // ── intercept_delete ───────────────────────────────────────────────────── public function test_delete_zeros_out_hot_column(): void { WPDO_Zone_Hot::set( 11, self::POST_TYPE, self::FIELD, '999.00' ); $this->assertSame( '999.00', WPDO_Zone_Hot::get( 11, self::POST_TYPE, self::FIELD ) ); WPDO_Feature_Flags::set( self::MODULE, 'dual_write' ); $this->bridge->intercept_delete( [ 1 ], 11, self::FIELD, '999.00' ); // delete_from_zone calls Zone_Hot::set(post_id, post_type, column, null). $val = WPDO_Zone_Hot::get( 11, self::POST_TYPE, self::FIELD ); $this->assertNull( $val ); } // ── $bypassing flag ─────────────────────────────────────────────────────── public function test_bypass_flag_prevents_intercept_get(): void { WPDO_Zone_Hot::set( 12, self::POST_TYPE, self::FIELD, '777.00' ); WPDO_Feature_Flags::set( self::MODULE, 'cutover' ); // Simulate internal call (e.g. migration reading postmeta). $ref = new ReflectionClass( WPDO_Sync_Bridge::class ); $bypass = $ref->getProperty( 'bypassing' ); $bypass->setAccessible( true ); $bypass->setValue( null, true ); $result = $this->bridge->intercept_get( null, 12, self::FIELD, true ); // Should pass through immediately, ignoring zone. $this->assertNull( $result ); } public function test_bypass_flag_prevents_intercept_update(): void { WPDO_Feature_Flags::set( self::MODULE, 'dual_write' ); $ref = new ReflectionClass( WPDO_Sync_Bridge::class ); $bypass = $ref->getProperty( 'bypassing' ); $bypass->setAccessible( true ); $bypass->setValue( null, true ); $this->bridge->intercept_update( null, 13, self::FIELD, '888.00', '' ); // bypassing = true → no write to hot table. $val = WPDO_Zone_Hot::get( 13, self::POST_TYPE, self::FIELD ); $this->assertNull( $val ); } }