setup_wpdb_mock(); } private function setup_wpdb_mock(): void { global $wpdb; $wpdb = new class { public string $prefix = 'wp_'; public string $postmeta = 'wp_postmeta'; public string $options = 'wp_options'; public function prepare( string $sql, ...$args ): string { $i = 0; return preg_replace_callback( '/%[sd]/', function() use ( &$i, $args ) { return (string) ( $args[ $i++ ] ?? '?' ); }, $sql ); } public function get_var( string $sql ) { return '0'; } // shadow_diffs table absent public function get_results( string $sql, $output = ARRAY_A ): array { return array(); } }; } private function set_module_state( string $module, string $state, ?int $days_ago = null ): void { // Set FSM state. $flags = (array) get_option( 'wpdo_features', array() ); $flags[ $module ] = $state; update_option( 'wpdo_features', $flags, false ); // Reset Feature_Flags request cache via reflection. $ref = new ReflectionClass( WPDO_Feature_Flags::class ); $prop = $ref->getProperty( 'cache' ); $prop->setAccessible( true ); $prop->setValue( null, null ); if ( null !== $days_ago ) { $entered = (array) get_option( 'wpdo_fsm_state_entered', array() ); $entered[ $module ] = array( 'state' => $state, 'entered_at' => gmdate( 'Y-m-d H:i:s', time() - $days_ago * 86400 ), ); update_option( 'wpdo_fsm_state_entered', $entered, false ); } } public function test_idle_state_returns_HOLD(): void { $this->set_module_state( 'reviews', 'idle' ); $advice = WPDO_FSM_Advisor::advise( 'reviews' ); $this->assertSame( 'HOLD', $advice['action'] ); $this->assertSame( 'info', $advice['level'] ); } public function test_complete_state_returns_HOLD(): void { $this->set_module_state( 'reviews', 'complete' ); $advice = WPDO_FSM_Advisor::advise( 'reviews' ); $this->assertSame( 'HOLD', $advice['action'] ); } public function test_dual_write_with_short_soak_returns_WAIT(): void { // In dual_write 0 days < 1 day min soak. $this->set_module_state( 'reviews', 'dual_write', 0 ); $advice = WPDO_FSM_Advisor::advise( 'reviews' ); $this->assertSame( 'WAIT', $advice['action'] ); $this->assertGreaterThanOrEqual( 1, $advice['days_remaining'] ); } public function test_dual_write_after_min_soak_returns_PROMOTE(): void { $this->set_module_state( 'reviews', 'dual_write', 2 ); $advice = WPDO_FSM_Advisor::advise( 'reviews' ); $this->assertSame( 'PROMOTE', $advice['action'] ); $this->assertSame( 'backfill', $advice['next_state'] ); } public function test_verify_with_long_soak_returns_PROMOTE(): void { // verify needs 7 days; 10 days = should promote. $this->set_module_state( 'reviews', 'verify', 10 ); $advice = WPDO_FSM_Advisor::advise( 'reviews' ); $this->assertSame( 'PROMOTE', $advice['action'] ); $this->assertSame( 'cutover', $advice['next_state'] ); } public function test_verify_with_short_soak_returns_WAIT(): void { $this->set_module_state( 'reviews', 'verify', 3 ); $advice = WPDO_FSM_Advisor::advise( 'reviews' ); $this->assertSame( 'WAIT', $advice['action'] ); $this->assertSame( 4, $advice['days_remaining'] ); } public function test_cleanup_with_short_wash_returns_WAIT(): void { $this->set_module_state( 'reviews', 'cleanup', 1 ); $advice = WPDO_FSM_Advisor::advise( 'reviews' ); $this->assertSame( 'WAIT', $advice['action'] ); $this->assertSame( 2, $advice['days_remaining'] ); } public function test_cleanup_after_wash_returns_PROMOTE(): void { $this->set_module_state( 'reviews', 'cleanup', 5 ); $advice = WPDO_FSM_Advisor::advise( 'reviews' ); $this->assertSame( 'PROMOTE', $advice['action'] ); $this->assertSame( 'complete', $advice['next_state'] ); } public function test_advice_metrics_include_state_and_soak(): void { $this->set_module_state( 'reviews', 'dual_write', 5 ); $advice = WPDO_FSM_Advisor::advise( 'reviews' ); $this->assertSame( 'dual_write', $advice['metrics']['state'] ); $this->assertSame( 5, $advice['metrics']['days_in_state'] ); $this->assertSame( 1, $advice['metrics']['min_soak_days'] ); } public function test_advise_all_returns_map_for_all_modules(): void { $advice = WPDO_FSM_Advisor::advise_all(); $this->assertNotEmpty( $advice ); // All known HPCT + zone modules should be present (default idle). foreach ( WPDO_Feature_Flags::HPCT_MODULES as $m ) { $this->assertArrayHasKey( $m, $advice ); } } }