chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
if ( ! function_exists( '__' ) ) {
|
||||
function __( string $text, string $domain = 'default' ): string {
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-logger.php';
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-feature-flags.php';
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/advisor/class-tmdo-fsm-advisor.php';
|
||||
|
||||
/**
|
||||
* Unit tests for WPDO_FSM_Advisor (v2.4.0 M12).
|
||||
*
|
||||
* Pure-logic tests — uses option-driven state injection (no real DB).
|
||||
*/
|
||||
class FSMAdvisorTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['_wp_options'] = array();
|
||||
$this->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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
if ( ! function_exists( '__' ) ) {
|
||||
function __( string $text, string $domain = 'default' ): string { return $text; }
|
||||
}
|
||||
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-logger.php';
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-feature-flags.php';
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/advisor/class-tmdo-fsm-advisor.php';
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/diagnostic/class-tmdo-health-cron.php';
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/advisor/class-tmdo-fsm-automator.php';
|
||||
|
||||
/**
|
||||
* Unit tests for WPDO_FSM_Automator (v2.5.0 M13).
|
||||
*/
|
||||
class FSMAutomatorTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['_wp_options'] = array();
|
||||
// Reset Feature_Flags cache.
|
||||
$ref = new ReflectionClass( WPDO_Feature_Flags::class );
|
||||
$prop = $ref->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 $postmeta = 'wp_postmeta';
|
||||
public string $options = 'wp_options';
|
||||
public function prepare( string $sql, ...$args ): string { return $sql; }
|
||||
public function get_var( string $sql ) { return '0'; }
|
||||
public function get_results( string $sql, $output = ARRAY_A ): array { return array(); }
|
||||
};
|
||||
}
|
||||
|
||||
public function test_default_disabled(): void {
|
||||
$this->assertFalse( WPDO_FSM_Automator::is_enabled() );
|
||||
}
|
||||
|
||||
public function test_run_returns_disabled_when_off(): void {
|
||||
$result = WPDO_FSM_Automator::run();
|
||||
$this->assertFalse( $result['ok'] );
|
||||
$this->assertContains( 'automator disabled', $result['errors'] );
|
||||
}
|
||||
|
||||
public function test_run_blocked_by_critical_cool_off(): void {
|
||||
// Enable + simulate critical alert in last run.
|
||||
update_option( 'wpdo_automator_enabled', '1', false );
|
||||
update_option( WPDO_Health_Cron::OPTION_LAST_RUN, array(
|
||||
'critical_count' => 2,
|
||||
'ran_at' => gmdate( 'Y-m-d H:i:s' ),
|
||||
), false );
|
||||
|
||||
$result = WPDO_FSM_Automator::run();
|
||||
$this->assertFalse( $result['ok'] );
|
||||
$this->assertStringContainsString( 'cool-off', $result['errors'][0] );
|
||||
}
|
||||
|
||||
public function test_run_proceeds_when_no_critical(): void {
|
||||
update_option( 'wpdo_automator_enabled', '1', false );
|
||||
update_option( WPDO_Health_Cron::OPTION_LAST_RUN, array(
|
||||
'critical_count' => 0,
|
||||
'ran_at' => gmdate( 'Y-m-d H:i:s' ),
|
||||
), false );
|
||||
|
||||
$result = WPDO_FSM_Automator::run();
|
||||
$this->assertTrue( $result['ok'] );
|
||||
// Most modules will be in idle and Advisor recommends WAIT (soak time);
|
||||
// expected that 0 actions are executed in baseline test.
|
||||
$this->assertGreaterThanOrEqual( 0, $result['executed'] );
|
||||
}
|
||||
|
||||
public function test_blacklist_skips_module(): void {
|
||||
update_option( 'wpdo_automator_enabled', '1', false );
|
||||
update_option( 'wpdo_automator_blacklist', array( 'reviews', 'wc_orders' ), false );
|
||||
update_option( WPDO_Health_Cron::OPTION_LAST_RUN, array(
|
||||
'critical_count' => 0,
|
||||
'ran_at' => gmdate( 'Y-m-d H:i:s' ),
|
||||
), false );
|
||||
$blacklist = WPDO_FSM_Automator::blacklist();
|
||||
$this->assertContains( 'reviews', $blacklist );
|
||||
$this->assertContains( 'wc_orders', $blacklist );
|
||||
|
||||
$result = WPDO_FSM_Automator::run();
|
||||
$this->assertTrue( $result['ok'] );
|
||||
// Skipped at least the 2 blacklisted ones.
|
||||
$this->assertGreaterThanOrEqual( 2, $result['skipped'] );
|
||||
}
|
||||
|
||||
public function test_forbidden_transitions_constant(): void {
|
||||
$ref = new ReflectionClass( WPDO_FSM_Automator::class );
|
||||
$constant = $ref->getReflectionConstant( 'FORBIDDEN_TRANSITIONS' );
|
||||
$this->assertNotNull( $constant );
|
||||
$value = $constant->getValue();
|
||||
$this->assertContains( array( 'verify', 'cutover' ), $value );
|
||||
$this->assertContains( array( 'cutover', 'cleanup' ), $value );
|
||||
$this->assertContains( array( 'cleanup', 'complete' ), $value );
|
||||
}
|
||||
|
||||
public function test_options_keys_match_admin_form(): void {
|
||||
$this->assertSame( 'wpdo_automator_enabled', WPDO_FSM_Automator::OPT_ENABLED );
|
||||
$this->assertSame( 'wpdo_automator_blacklist', WPDO_FSM_Automator::OPT_BLACKLIST );
|
||||
$this->assertSame( 'wpdo_automator_last_action', WPDO_FSM_Automator::OPT_LAST_ACTION );
|
||||
}
|
||||
|
||||
public function test_last_actions_returns_array(): void {
|
||||
$this->assertSame( array(), WPDO_FSM_Automator::last_actions() );
|
||||
update_option( 'wpdo_automator_last_action', array( 'reviews' => '2026-04-28 04:30:00' ), false );
|
||||
$last = WPDO_FSM_Automator::last_actions();
|
||||
$this->assertSame( '2026-04-28 04:30:00', $last['reviews'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
if ( ! function_exists( '__' ) ) {
|
||||
function __( string $text, string $domain = 'default' ): string { return $text; }
|
||||
}
|
||||
if ( ! function_exists( 'number_format_i18n' ) ) {
|
||||
function number_format_i18n( $n, int $decimals = 0 ): string {
|
||||
return number_format( (float) $n, $decimals );
|
||||
}
|
||||
}
|
||||
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-logger.php';
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-feature-flags.php';
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/advisor/class-tmdo-module-rules.php';
|
||||
require_once dirname( __DIR__, 3 ) . '/includes/advisor/class-tmdo-module-detector.php';
|
||||
|
||||
/**
|
||||
* Unit tests for WPDO_Module_Rules + WPDO_Module_Detector (v2.5.0 M16).
|
||||
*
|
||||
* Pure-logic tests using mocked WPDO_Compatibility (via dynamic class
|
||||
* substitution where needed) and option-driven post counts.
|
||||
*/
|
||||
class ModuleDetectorTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['_wp_options'] = array();
|
||||
// Reset Feature_Flags cache.
|
||||
$ref = new ReflectionClass( WPDO_Feature_Flags::class );
|
||||
$prop = $ref->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" );
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user