From c84d2409b7df0bb51834e66582169c2dd8f03629 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sat, 26 Sep 2026 00:36:02 +0800 Subject: [PATCH] feat(2meet-data-optimizer): warn when HivePress / WooCommerce run without their AddOn - TMDO_Compatibility::missing_integration_addons() reports active HivePress / WooCommerce whose AddOn bootstrap class is absent - Surface it as Site Health test wpdo_missing_integration_addons (also on the admin Doctor tab) and an administrator notice (TMDO_Missing_Addons_Notice) - Verify: MissingIntegrationAddonsTest (5, class stand-ins in separate processes); core unit 651 / integration 449, HP 148, WC 10 OK; read-only check: dev20/21 HivePress+WooCommerce, dev22 HivePress, others clean - PR: PR-035 --- 2meet-data-optimizer.php | 2 + admin/class-tmdo-admin.php | 21 +++-- admin/class-tmdo-help-tabs.php | 3 +- admin/class-tmdo-missing-addons-notice.php | 62 ++++++++++++ includes/class-tmdo-compatibility.php | 20 ++++ .../diagnostic/class-tmdo-site-health.php | 75 +++++++++++---- tests/unit/MissingIntegrationAddonsTest.php | 94 +++++++++++++++++++ 7 files changed, 246 insertions(+), 31 deletions(-) create mode 100644 admin/class-tmdo-missing-addons-notice.php create mode 100644 tests/unit/MissingIntegrationAddonsTest.php diff --git a/2meet-data-optimizer.php b/2meet-data-optimizer.php index 4fcafdf..e01978c 100644 --- a/2meet-data-optimizer.php +++ b/2meet-data-optimizer.php @@ -288,6 +288,8 @@ if ( is_admin() ) { TMDO_Dashboard_Widget::register(); require_once TMDO_PATH . 'admin/class-tmdo-deprecated-addons-notice.php'; TMDO_Deprecated_Addons_Notice::register(); + require_once TMDO_PATH . 'admin/class-tmdo-missing-addons-notice.php'; + TMDO_Missing_Addons_Notice::register(); } // ── WP-CLI ───────────────────────────────────────────────────────────────── diff --git a/admin/class-tmdo-admin.php b/admin/class-tmdo-admin.php index e11354d..f28da03 100644 --- a/admin/class-tmdo-admin.php +++ b/admin/class-tmdo-admin.php @@ -2895,7 +2895,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));' ?>

- +

@@ -2909,15 +2909,16 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));' } $tests = array( - 'wpdo_schema_drift' => array( 'check_schema_drift', __( 'Schema 完整性', '2meet-data-optimizer' ) ), - 'wpdo_error_budget' => array( 'check_error_budget', __( '錯誤預算(過去 7 天)', '2meet-data-optimizer' ) ), - 'wpdo_hook_conflicts' => array( 'check_hook_conflicts', __( 'Hook 衝突', '2meet-data-optimizer' ) ), - 'wpdo_autoload_bloat' => array( 'check_autoload_bloat', __( 'Autoload 大小', '2meet-data-optimizer' ) ), - 'wpdo_postmeta_explosion' => array( 'check_postmeta_explosion', __( 'wp_postmeta 爆量', '2meet-data-optimizer' ) ), - 'wpdo_orphan_zone_rows' => array( 'check_orphan_zone_rows', __( 'Orphan zone rows', '2meet-data-optimizer' ) ), - 'wpdo_missing_snapshot' => array( 'check_missing_snapshot', __( '缺少最近快照', '2meet-data-optimizer' ) ), - 'wpdo_entity_group_collisions' => array( 'check_entity_group_collisions', __( '實體群組名稱衝突', '2meet-data-optimizer' ) ), - 'wpdo_orphaned_entity_groups' => array( 'check_orphaned_entity_groups', __( '孤兒實體群組', '2meet-data-optimizer' ) ), + 'wpdo_schema_drift' => array( 'check_schema_drift', __( 'Schema 完整性', '2meet-data-optimizer' ) ), + 'wpdo_error_budget' => array( 'check_error_budget', __( '錯誤預算(過去 7 天)', '2meet-data-optimizer' ) ), + 'wpdo_hook_conflicts' => array( 'check_hook_conflicts', __( 'Hook 衝突', '2meet-data-optimizer' ) ), + 'wpdo_autoload_bloat' => array( 'check_autoload_bloat', __( 'Autoload 大小', '2meet-data-optimizer' ) ), + 'wpdo_postmeta_explosion' => array( 'check_postmeta_explosion', __( 'wp_postmeta 爆量', '2meet-data-optimizer' ) ), + 'wpdo_orphan_zone_rows' => array( 'check_orphan_zone_rows', __( 'Orphan zone rows', '2meet-data-optimizer' ) ), + 'wpdo_missing_snapshot' => array( 'check_missing_snapshot', __( '缺少最近快照', '2meet-data-optimizer' ) ), + 'wpdo_entity_group_collisions' => array( 'check_entity_group_collisions', __( '實體群組名稱衝突', '2meet-data-optimizer' ) ), + 'wpdo_orphaned_entity_groups' => array( 'check_orphaned_entity_groups', __( '孤兒實體群組', '2meet-data-optimizer' ) ), + 'wpdo_missing_integration_addons' => array( 'check_missing_integration_addons', __( '缺少對應 AddOn', '2meet-data-optimizer' ) ), ); ?> diff --git a/admin/class-tmdo-help-tabs.php b/admin/class-tmdo-help-tabs.php index 3da8749..57cb85b 100644 --- a/admin/class-tmdo-help-tabs.php +++ b/admin/class-tmdo-help-tabs.php @@ -183,7 +183,7 @@ class TMDO_Help_Tabs { * @return string */ private static function content_doctor(): string { - return '

' . esc_html__( '9 項自動健康檢查:', '2meet-data-optimizer' ) . '

' + return '

' . esc_html__( '10 項自動健康檢查:', '2meet-data-optimizer' ) . '

' . '
    ' . '
  1. schema_drift — ' . esc_html__( '所有 v2 表是否存在', '2meet-data-optimizer' ) . '
  2. ' . '
  3. error_budget — ' . esc_html__( '7 天內 wp_wpdo_errors 行數', '2meet-data-optimizer' ) . '
  4. ' @@ -194,6 +194,7 @@ class TMDO_Help_Tabs { . '
  5. missing_snapshot — ' . esc_html__( '在 cutover/cleanup/complete 但 7 天沒 snapshot', '2meet-data-optimizer' ) . '
  6. ' . '
  7. entity_group_collisions — ' . esc_html__( '同名實體群組被不同外掛以不同欄位註冊(後者被忽略)', '2meet-data-optimizer' ) . '
  8. ' . '
  9. orphaned_entity_groups — ' . esc_html__( '扁平表還在但已無外掛註冊的群組(已依保存的定義還原以維持可讀)', '2meet-data-optimizer' ) . '
  10. ' + . '
  11. missing_integration_addons — ' . esc_html__( 'HivePress / WooCommerce 已啟用但對應 AddOn 未啟用', '2meet-data-optimizer' ) . '
  12. ' . '
' . '

' . esc_html__( '結果有 5 分鐘 transient cache,剛操作完想立刻看新值請等下個週期。', '2meet-data-optimizer' ) . '

'; } diff --git a/admin/class-tmdo-missing-addons-notice.php b/admin/class-tmdo-missing-addons-notice.php new file mode 100644 index 0000000..0803c63 --- /dev/null +++ b/admin/class-tmdo-missing-addons-notice.php @@ -0,0 +1,62 @@ + $addon ) { + $lines[] = sprintf( '%s → %s', $plugin, $addon ); + } + printf( + '

%s %s

', + esc_html__( '2meet Data Optimizer:', '2meet-data-optimizer' ), + esc_html( + sprintf( + /* translators: %s: "Plugin → AddOn" pairs */ + __( '以下外掛已啟用,但對應的 AddOn 沒有啟用,它們的欄位不會被優化:%s。請啟用對應的 AddOn。', '2meet-data-optimizer' ), + implode( ';', $lines ) + ) + ) + ); + } +} diff --git a/includes/class-tmdo-compatibility.php b/includes/class-tmdo-compatibility.php index cb4ac92..c8891eb 100644 --- a/includes/class-tmdo-compatibility.php +++ b/includes/class-tmdo-compatibility.php @@ -68,6 +68,26 @@ class TMDO_Compatibility { return class_exists( 'WooCommerce' ); } + /** + * Integrations that are active while their AddOn is not. + * + * Core does not know these plugins; their fields are only optimised by the + * matching AddOn. The AddOn is detected by its bootstrap class, which its + * main file declares as soon as the plugin loads. + * + * @return array Plugin name → AddOn plugin slug. + */ + public static function missing_integration_addons(): array { + $missing = array(); + if ( self::is_hivepress_active() && ! class_exists( 'TMDO_HP_Bootstrap' ) ) { + $missing['HivePress'] = '2meet-data-optimizer-hivepress-addon'; + } + if ( self::is_woocommerce_active() && ! class_exists( 'TMDO_Woocommerce_Bootstrap' ) ) { + $missing['WooCommerce'] = '2meet-data-optimizer-woocommerce-addon'; + } + return $missing; + } + /** * Is LatePoint plugin active? (v2.5.0 M16 polish — for module detector.) * diff --git a/includes/diagnostic/class-tmdo-site-health.php b/includes/diagnostic/class-tmdo-site-health.php index e2a8722..053d784 100644 --- a/includes/diagnostic/class-tmdo-site-health.php +++ b/includes/diagnostic/class-tmdo-site-health.php @@ -2,7 +2,7 @@ /** * TMDO_Site_Health — WordPress Site Health integration (v2.2.0 M3). * - * Registers 9 tests under Tools → Site Health → Status: + * Registers 10 tests under Tools → Site Health → Status: * 1. wpdo_schema_drift (critical) * 2. wpdo_error_budget (recommended) * 3. wpdo_hook_conflicts (recommended) @@ -12,6 +12,7 @@ * 7. wpdo_missing_snapshot (critical) * 8. wpdo_entity_group_collisions (recommended) * 9. wpdo_orphaned_entity_groups (recommended) + * 10. wpdo_missing_integration_addons (recommended) * * Each test result is cached for 5 minutes to keep Site Health responsive. * @@ -36,15 +37,16 @@ class TMDO_Site_Health { /** Test name → callable suffix mapping. */ private const TESTS = array( - 'wpdo_schema_drift' => 'check_schema_drift', - 'wpdo_error_budget' => 'check_error_budget', - 'wpdo_hook_conflicts' => 'check_hook_conflicts', - 'wpdo_autoload_bloat' => 'check_autoload_bloat', - 'wpdo_postmeta_explosion' => 'check_postmeta_explosion', - 'wpdo_orphan_zone_rows' => 'check_orphan_zone_rows', - 'wpdo_missing_snapshot' => 'check_missing_snapshot', - 'wpdo_entity_group_collisions' => 'check_entity_group_collisions', - 'wpdo_orphaned_entity_groups' => 'check_orphaned_entity_groups', + 'wpdo_schema_drift' => 'check_schema_drift', + 'wpdo_error_budget' => 'check_error_budget', + 'wpdo_hook_conflicts' => 'check_hook_conflicts', + 'wpdo_autoload_bloat' => 'check_autoload_bloat', + 'wpdo_postmeta_explosion' => 'check_postmeta_explosion', + 'wpdo_orphan_zone_rows' => 'check_orphan_zone_rows', + 'wpdo_missing_snapshot' => 'check_missing_snapshot', + 'wpdo_entity_group_collisions' => 'check_entity_group_collisions', + 'wpdo_orphaned_entity_groups' => 'check_orphaned_entity_groups', + 'wpdo_missing_integration_addons' => 'check_missing_integration_addons', ); /** @@ -57,7 +59,7 @@ class TMDO_Site_Health { } /** - * Register the 9 tests with WP Site Health. + * Register the 10 tests with WP Site Health. * * @param array $tests Existing tests. * @return array @@ -80,15 +82,16 @@ class TMDO_Site_Health { */ private static function label_for( string $key ): string { $map = array( - 'wpdo_schema_drift' => __( 'WPDO schema drift', '2meet-data-optimizer' ), - 'wpdo_error_budget' => __( 'WPDO error budget', '2meet-data-optimizer' ), - 'wpdo_hook_conflicts' => __( 'WPDO hook conflicts', '2meet-data-optimizer' ), - 'wpdo_autoload_bloat' => __( 'WPDO autoload bloat', '2meet-data-optimizer' ), - 'wpdo_postmeta_explosion' => __( 'WPDO postmeta explosion', '2meet-data-optimizer' ), - 'wpdo_orphan_zone_rows' => __( 'WPDO orphan zone rows', '2meet-data-optimizer' ), - 'wpdo_missing_snapshot' => __( 'WPDO missing snapshot', '2meet-data-optimizer' ), - 'wpdo_entity_group_collisions' => __( 'WPDO entity group collisions', '2meet-data-optimizer' ), - 'wpdo_orphaned_entity_groups' => __( 'WPDO orphaned entity groups', '2meet-data-optimizer' ), + 'wpdo_schema_drift' => __( 'WPDO schema drift', '2meet-data-optimizer' ), + 'wpdo_error_budget' => __( 'WPDO error budget', '2meet-data-optimizer' ), + 'wpdo_hook_conflicts' => __( 'WPDO hook conflicts', '2meet-data-optimizer' ), + 'wpdo_autoload_bloat' => __( 'WPDO autoload bloat', '2meet-data-optimizer' ), + 'wpdo_postmeta_explosion' => __( 'WPDO postmeta explosion', '2meet-data-optimizer' ), + 'wpdo_orphan_zone_rows' => __( 'WPDO orphan zone rows', '2meet-data-optimizer' ), + 'wpdo_missing_snapshot' => __( 'WPDO missing snapshot', '2meet-data-optimizer' ), + 'wpdo_entity_group_collisions' => __( 'WPDO entity group collisions', '2meet-data-optimizer' ), + 'wpdo_orphaned_entity_groups' => __( 'WPDO orphaned entity groups', '2meet-data-optimizer' ), + 'wpdo_missing_integration_addons' => __( 'WPDO integrations without their AddOn', '2meet-data-optimizer' ), ); return $map[ $key ] ?? $key; } @@ -511,6 +514,38 @@ class TMDO_Site_Health { ); } + // ─── Test 10: Integrations without their AddOn ────────────────────── + + /** + * Report HivePress / WooCommerce running while their AddOn is inactive. + * + * Not cached: two class_exists() probes. + * + * @return array Site Health result. + */ + public static function check_missing_integration_addons(): array { + $missing = class_exists( 'TMDO_Compatibility' ) ? TMDO_Compatibility::missing_integration_addons() : array(); + if ( empty( $missing ) ) { + return self::wrap( 'wpdo_missing_integration_addons', self::pass( __( 'Every detected integration has its AddOn active.', '2meet-data-optimizer' ) ) ); + } + $lines = array(); + foreach ( $missing as $plugin => $addon ) { + $lines[] = "{$plugin} → {$addon}"; + } + return self::wrap( + 'wpdo_missing_integration_addons', + self::fail( + __( 'WPDO integrations running without their AddOn', '2meet-data-optimizer' ), + sprintf( + /* translators: %s: "Plugin → AddOn" pairs */ + __( 'These plugins are active but their AddOn is not, so their fields are not optimised: %s. Activate the AddOn.', '2meet-data-optimizer' ), + implode( '; ', $lines ) + ), + 'recommended' + ) + ); + } + /** * Wrap a raw check result with required Site Health envelope fields. * diff --git a/tests/unit/MissingIntegrationAddonsTest.php b/tests/unit/MissingIntegrationAddonsTest.php new file mode 100644 index 0000000..9237221 --- /dev/null +++ b/tests/unit/MissingIntegrationAddonsTest.php @@ -0,0 +1,94 @@ + true ); + } + + public function test_nothing_missing_without_the_integrations(): void { + $this->assertSame( array(), TMDO_Compatibility::missing_integration_addons() ); + $this->assertSame( 'good', TMDO_Site_Health::check_missing_integration_addons()['status'] ); + $this->assertArrayHasKey( 'wpdo_missing_integration_addons', TMDO_Site_Health::register_tests( array() )['direct'] ); + + ob_start(); + TMDO_Missing_Addons_Notice::render(); + $this->assertSame( '', ob_get_clean() ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_hivepress_without_its_addon_is_reported(): void { + eval( 'namespace HivePress; class Core {}' ); + + $this->assertSame( array( 'HivePress' => '2meet-data-optimizer-hivepress-addon' ), TMDO_Compatibility::missing_integration_addons() ); + + $result = TMDO_Site_Health::check_missing_integration_addons(); + $this->assertSame( 'recommended', $result['status'] ); + $this->assertStringContainsString( '2meet-data-optimizer-hivepress-addon', $result['description'] ); + + ob_start(); + TMDO_Missing_Addons_Notice::render(); + $this->assertStringContainsString( '2meet-data-optimizer-hivepress-addon', (string) ob_get_clean() ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_hivepress_with_its_addon_is_fine(): void { + eval( 'namespace HivePress; class Core {}' ); + eval( 'final class TMDO_HP_Bootstrap {}' ); + + $this->assertSame( array(), TMDO_Compatibility::missing_integration_addons() ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_woocommerce_without_its_addon_is_reported(): void { + eval( 'class WooCommerce {}' ); + + $this->assertSame( array( 'WooCommerce' => '2meet-data-optimizer-woocommerce-addon' ), TMDO_Compatibility::missing_integration_addons() ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_notice_is_for_administrators_only(): void { + eval( 'class WooCommerce {}' ); + $GLOBALS['_wp_current_user_can'] = array(); + + ob_start(); + TMDO_Missing_Addons_Notice::render(); + $this->assertSame( '', ob_get_clean() ); + } +}