'test-job', 'log' => array(), 'state' => 'running', 'overall_progress' => 0, 'metrics' => array( 'ratio_start' => $ratio_start, 'ratio_now' => $ratio_now, ), ); } // ── WPDO_Phase_Completed ────────────────────────────────────────────────── public function test_completed_returns_done_status(): void { $phase = new WPDO_Phase_Completed( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'done', $result['status'] ); } public function test_completed_sets_state_to_completed(): void { $phase = new WPDO_Phase_Completed( 'user' ); $job = $this->make_job(); $phase->execute( $job ); $this->assertSame( 'completed', $job['state'] ); } public function test_completed_sets_overall_progress_to_100(): void { $phase = new WPDO_Phase_Completed( 'user' ); $job = $this->make_job(); $phase->execute( $job ); $this->assertSame( 100, $job['overall_progress'] ); } public function test_completed_sets_completed_at_timestamp(): void { $before = time(); $phase = new WPDO_Phase_Completed( 'user' ); $job = $this->make_job(); $phase->execute( $job ); $this->assertGreaterThanOrEqual( $before, $job['completed_at'] ); $this->assertLessThanOrEqual( time(), $job['completed_at'] ); } public function test_completed_appends_log_entry(): void { $phase = new WPDO_Phase_Completed( 'user' ); $job = $this->make_job( 1.5, 0.75 ); $phase->execute( $job ); $this->assertNotEmpty( $job['log'] ); $last_line = end( $job['log'] ); $this->assertStringContainsString( 'Migration complete', $last_line ); } public function test_completed_name_is_completed(): void { $phase = new WPDO_Phase_Completed( 'user' ); $this->assertSame( 'completed', $phase->name() ); } // ── WPDO_Migration_Phase_Base log truncation ────────────────────────────── public function test_log_truncates_at_max_log_lines(): void { $phase = new WPDO_Phase_Completed( 'user' ); $job = $this->make_job(); // Each execute() appends exactly one log entry — call it MAX+5 times. $max = WPDO_Migration_Phase_Base::MAX_LOG_LINES; for ( $i = 0; $i < $max + 5; $i++ ) { $phase->execute( $job ); } $this->assertCount( $max, $job['log'] ); } public function test_log_keeps_most_recent_lines_when_truncating(): void { $phase = new WPDO_Phase_Completed( 'user' ); $job = $this->make_job(); $max = WPDO_Migration_Phase_Base::MAX_LOG_LINES; for ( $i = 0; $i < $max + 10; $i++ ) { $phase->execute( $job ); } // Log contains exactly MAX lines with truncation keeping the most recent. $this->assertCount( $max, $job['log'] ); // First kept line was written after at least 10 entries were discarded. $first_line = $job['log'][0]; $this->assertStringContainsString( 'Migration complete', $first_line ); } // ── Phase interface contract ─────────────────────────────────────────────── public function test_phase_implements_interface(): void { $phase = new WPDO_Phase_Completed( 'user' ); // Interfaces cannot be class_alias()'d; the phase contract is core-internal // (only the 11 built-in phases implement it), so assert the canonical name. $this->assertInstanceOf( TMDO_Migration_Phase_Interface::class, $phase ); } public function test_phase_entity_type_injectable(): void { $phase_user = new WPDO_Phase_Completed( 'user' ); $phase_term = new WPDO_Phase_Completed( 'term' ); // Both are independent instances — no shared state. $job_a = $this->make_job(); $job_b = $this->make_job(); $phase_user->execute( $job_a ); $phase_term->execute( $job_b ); $this->assertSame( 'completed', $job_a['state'] ); $this->assertSame( 'completed', $job_b['state'] ); } // ── Phase_Cleanup — RuntimeException guards (P1-11, highest priority) ──────── protected function setUp(): void { $GLOBALS['_wp_options'] = []; WPDO_Mode_Manager::reset_cache(); } private function make_mode_option( string $entity_type, string $mode ): void { $GLOBALS['_wp_options']['wpdo_bridge_modes'] = array( $entity_type => $mode ); WPDO_Mode_Manager::reset_cache(); } public function test_cleanup_throws_when_mode_not_aeav_only(): void { $this->make_mode_option( 'user', 'dual_write' ); $phase = new WPDO_Phase_Cleanup( 'user' ); $job = $this->make_job(); $this->expectException( \RuntimeException::class ); $this->expectExceptionMessageMatches( '/must be aeav_only/' ); $phase->execute( $job ); } public function test_cleanup_throws_when_auto_backup_requested_but_no_path(): void { $this->make_mode_option( 'user', 'aeav_only' ); $phase = new WPDO_Phase_Cleanup( 'user' ); $job = array_merge( $this->make_job(), array( 'options' => array( 'auto_backup' => true ), 'backup_path' => '', // empty — simulates backup that never ran ) ); $this->expectException( \RuntimeException::class ); $this->expectExceptionMessageMatches( '/no backup_path on record/' ); $phase->execute( $job ); } public function test_cleanup_skips_when_dry_run(): void { $this->make_mode_option( 'user', 'dual_write' ); // wrong mode, but dry_run should short-circuit first $phase = new WPDO_Phase_Cleanup( 'user' ); $job = array_merge( $this->make_job(), array( 'options' => array( 'dry_run' => true ) ) ); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); $this->assertStringContainsString( 'dry_run', $job['log'][0] ?? '' ); } // ── Phase_Demote ────────────────────────────────────────────────────────────── public function test_demote_skips_when_not_aeav_only(): void { $this->make_mode_option( 'user', 'shadow_read' ); $phase = new WPDO_Phase_Demote( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); $this->assertStringContainsString( 'skipped', $job['log'][0] ?? '' ); } public function test_demote_transitions_aeav_only_to_dual_write(): void { $this->make_mode_option( 'user', 'aeav_only' ); $phase = new WPDO_Phase_Demote( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); $this->assertSame( 'dual_write', WPDO_Mode_Manager::get( 'user' ) ); } // ── Phase_Backup — skip when auto_backup=false ──────────────────────────────── public function test_backup_skips_when_auto_backup_false(): void { $phase = new WPDO_Phase_Backup( 'user' ); $job = array_merge( $this->make_job(), array( 'options' => array( 'auto_backup' => false ) ) ); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); $this->assertStringContainsString( 'skipped', $job['log'][0] ?? '' ); } // ── Phase_Promote_Aeav ──────────────────────────────────────────────────────── public function test_promote_aeav_skips_when_already_aeav_only(): void { $this->make_mode_option( 'user', 'aeav_only' ); $phase = new WPDO_Phase_Promote_Aeav( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); $this->assertStringContainsString( 'Already', $job['log'][0] ?? '' ); } public function test_promote_aeav_transitions_from_shadow_read(): void { $this->make_mode_option( 'user', 'shadow_read' ); $phase = new WPDO_Phase_Promote_Aeav( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); $this->assertSame( 'aeav_only', WPDO_Mode_Manager::get( 'user' ) ); } // ── Phase_Promote_Shadow ────────────────────────────────────────────────────── public function test_promote_shadow_transitions_from_dual_write(): void { $this->make_mode_option( 'user', 'dual_write' ); $phase = new WPDO_Phase_Promote_Shadow( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); $this->assertSame( 'shadow_read', WPDO_Mode_Manager::get( 'user' ) ); } public function test_promote_shadow_skips_when_already_at_shadow_or_higher(): void { $this->make_mode_option( 'user', 'aeav_only' ); $phase = new WPDO_Phase_Promote_Shadow( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); // Mode should be unchanged (already past shadow_read). $this->assertSame( 'aeav_only', WPDO_Mode_Manager::get( 'user' ) ); } // ── Phase name() contract ───────────────────────────────────────────────────── public function test_all_phases_return_expected_names(): void { $cases = array( array( new WPDO_Phase_Backup( 'user' ), 'backup' ), array( new WPDO_Phase_Cleanup( 'user' ), 'cleanup' ), array( new WPDO_Phase_Demote( 'user' ), 'demote' ), array( new WPDO_Phase_Promote_Aeav( 'user' ), 'promote_aeav' ), array( new WPDO_Phase_Promote_Shadow( 'user' ), 'promote_shadow' ), ); foreach ( $cases as [ $phase, $expected ] ) { $this->assertSame( $expected, $phase->name() ); } } }