Files
wpdev f187ac5401 refactor(zones): 回填 Zone_Router + Routing_Predicate(PR-C)
A v3.0.1/v3.4.0 的架構深化,B 完全缺席:

新增
- includes/zones/class-tmdo-zone-router.php:module_name / read / write /
  delete_field / delete_post 五個 static 分派點
- includes/class-tmdo-routing-predicate.php:entity_bridge_owns(含
  request-level cache,B 原本沒有)+ should_{write,read,query}_from_zone
  + flush_cache

改寫
- Sync_Bridge 改用兩者,移除 5 個 private wrapper(get_zone_module /
  read_from_zone / write_to_zone / delete_from_zone / is_owned_by_entity_bridge)
  與 44 行 inline cleanup_post → Zone_Router::delete_post 一行(400→269 行)
- Query_Router 抽出 extract_hot_clauses() public static,pre_get_posts 變薄殼
- 收斂 5 處重複的 'hot_'/'cold_' . sanitize_key()(rest-api ×3、
  hot/cold-migration ×2)
- back-compat 補 WPDO_Zone_Router / WPDO_Routing_Predicate alias

測試
- 移植 ZoneRouterTest(250 行)+ RoutingPredicateTest(85 行)
- SyncBridgeEntityGuardTest 的 setUp/tearDownAfterClass 補 flush_cache(),
  否則 entity_bridge_cache 會跨測試污染(A v3.4.6 踩過同一個坑)

unit 409 / integration 398 GREEN

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:47:08 +08:00

270 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`' );
// Prevent this class's entity_bridge_cache entries leaking into later classes.
TMDO_Routing_Predicate::flush_cache();
// 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 . '`' );
// Routing_Predicate memoises entity_bridge_owns() per request — clear it so
// each case sees the mode/registry state it just set up.
TMDO_Routing_Predicate::flush_cache();
$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.'
);
}
}