getProperty( 'cache' ); $prop->setAccessible( true ); $prop->setValue( null, null ); $this->setup_wpdb_mock(); } private function setup_wpdb_mock(): void { global $wpdb; $wpdb = new class { public string $prefix = 'wp_'; public string $posts = 'wp_posts'; public string $postmeta = 'wp_postmeta'; public string $options = 'wp_options'; public array $count_overrides = array(); public function prepare( string $sql, ...$args ): string { $i = 0; return preg_replace_callback( '/%[sd]/', function() use ( &$i, $args ) { return is_string( $args[ $i ] ?? null ) ? "'" . $args[ $i++ ] . "'" : (string) ( $args[ $i++ ] ?? '?' ); }, $sql ); } public function get_var( string $sql ) { // Mock by inspecting SQL pattern. if ( str_contains( $sql, "post_type =" ) ) { if ( preg_match( "/post_type = '([^']+)'/", $sql, $m ) ) { return (string) ( $this->count_overrides[ $m[1] ] ?? 0 ); } } if ( str_contains( $sql, "post_status = 'trash'" ) ) { return (string) ( $this->count_overrides['__trash__'] ?? 0 ); } return '0'; } public function get_results( string $sql, $output = ARRAY_A ): array { return array(); } }; } public function test_module_rules_known_modules_count(): void { $modules = WPDO_Module_Rules::known_modules(); $this->assertGreaterThanOrEqual( 14, count( $modules ), '預期 ≥ 14 個 module(9 HPCT + 6 zone - 1 dup)' ); $this->assertContains( 'reviews', $modules ); $this->assertContains( 'warm', $modules ); $this->assertContains( 'archive', $modules ); $this->assertContains( 'hot_hp_listing', $modules ); } public function test_module_rule_for_module_returns_array(): void { $rule = WPDO_Module_Rules::for_module( 'reviews' ); $this->assertIsArray( $rule ); $this->assertArrayHasKey( 'compat_required', $rule ); $this->assertArrayHasKey( 'description', $rule ); } public function test_module_rule_unknown_module_returns_null(): void { $this->assertNull( WPDO_Module_Rules::for_module( 'totally_fake_module_xyz' ) ); } public function test_detector_skip_when_module_not_idle(): void { // Set reviews to dual_write. WPDO_Feature_Flags::set( 'reviews', 'dual_write' ); $r = WPDO_Module_Detector::detect_one( 'reviews' ); $this->assertSame( 'skip', $r['recommendation'] ); $this->assertFalse( $r['available'] ); $this->assertNotEmpty( $r['blockers'] ); $this->assertSame( 'dual_write', $r['current_state'] ); } public function test_detector_warm_module_recommends_for_any_site(): void { // warm module's compat_required is empty + no post_type. $r = WPDO_Module_Detector::detect_one( 'warm' ); $this->assertTrue( $r['available'] ); $this->assertSame( 'enable', $r['recommendation'] ); $this->assertGreaterThan( 0, $r['confidence'] ); $this->assertNotEmpty( $r['suggested_action'] ); $this->assertSame( 'dual_write', $r['suggested_action']['to_state'] ); } public function test_detector_archive_blocked_when_no_trashed_posts(): void { // trashed = 0, threshold = 50. global $wpdb; $wpdb->count_overrides['__trash__'] = 0; $r = WPDO_Module_Detector::detect_one( 'archive' ); $this->assertSame( 'wait', $r['recommendation'] ); $this->assertFalse( $r['available'] ); } public function test_detector_archive_passes_when_enough_trashed(): void { global $wpdb; $wpdb->count_overrides['__trash__'] = 100; $r = WPDO_Module_Detector::detect_one( 'archive' ); $this->assertSame( 'enable', $r['recommendation'] ); $this->assertTrue( $r['available'] ); } public function test_detector_hivepress_required_blocks_when_inactive(): void { // reviews requires hivepress; Compatibility class isn't loaded in this test. // Without Compatibility class, the helper returns the full required list as missing. $r = WPDO_Module_Detector::detect_one( 'reviews' ); $this->assertSame( 'skip', $r['recommendation'] ); $this->assertNotEmpty( $r['blockers'] ); } public function test_get_actionable_filters_by_confidence(): void { $actionable = WPDO_Module_Detector::get_actionable( 0.5 ); $this->assertIsArray( $actionable ); // Each result should have available=true and recommendation=enable. foreach ( $actionable as $module => $r ) { $this->assertTrue( $r['available'], "$module should be available" ); $this->assertSame( 'enable', $r['recommendation'] ); $this->assertGreaterThanOrEqual( 0.5, $r['confidence'] ); } } public function test_get_actionable_sorts_by_confidence_desc(): void { $actionable = WPDO_Module_Detector::get_actionable( 0.0 ); $last_confidence = 1.0; foreach ( $actionable as $r ) { $this->assertLessThanOrEqual( $last_confidence, (float) $r['confidence'] ); $last_confidence = (float) $r['confidence']; } } public function test_detect_all_returns_entry_for_each_known_module(): void { $all = WPDO_Module_Detector::detect_all( true ); $known = WPDO_Module_Rules::known_modules(); foreach ( $known as $m ) { $this->assertArrayHasKey( $m, $all, "Detector should return entry for $m" ); } } }