From 57772101849961da1f600721a56c7c47cd4634cb Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 25 Sep 2026 22:58:34 +0800 Subject: [PATCH] refactor(2meet-data-optimizer-hivepress-addon): remove comment mirror writes to never-created tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Favorites/Messages adapters reduced to detection only; Requests keeps hot_hp_request fields, drops hp_offer mirror - Delete TMDO_HivePress_Comment_Router (read path to wpdo_comment_hp_* tables that no installer creates) - Verify: addon unit 141 OK, integration 52 OK; phpcs 41E/57W → 35E/48W - PR: PR-013 --- includes/back-compat-aliases.php | 1 - includes/class-tmdo-hivepress-bootstrap.php | 1 - ...class-tmdo-hivepress-favorites-adapter.php | 181 +-------------- .../class-tmdo-hivepress-messages-adapter.php | 216 +----------------- .../class-tmdo-hivepress-requests-adapter.php | 173 ++------------ .../class-tmdo-hivepress-reviews-adapter.php | 12 +- .../class-tmdo-hivepress-bootstrap.php | 9 +- .../class-tmdo-hivepress-comment-router.php | 166 -------------- .../hivepress/interface-tmdo-hp-adapter.php | 2 +- tests/bootstrap.php | 1 - .../integration/HivepressIntegrationTest.php | 136 ----------- tests/integration/bootstrap.php | 1 - tests/unit/CommentRouterTest.php | 92 -------- tests/unit/FavoritesAdapterTest.php | 71 +----- tests/unit/MessagesAdapterTest.php | 74 ++---- tests/unit/RequestsAdapterTest.php | 47 +--- tests/unit/WpdbMockTrait.php | 2 +- 17 files changed, 77 insertions(+), 1108 deletions(-) delete mode 100644 includes/hivepress/class-tmdo-hivepress-comment-router.php delete mode 100644 tests/unit/CommentRouterTest.php diff --git a/includes/back-compat-aliases.php b/includes/back-compat-aliases.php index 6eb79a0..78f487f 100644 --- a/includes/back-compat-aliases.php +++ b/includes/back-compat-aliases.php @@ -29,7 +29,6 @@ add_action( 'TMDO_HivePress_Blocks_Adapter' => 'WPDO_HivePress_Blocks_Adapter', 'TMDO_HivePress_Bookings_Adapter' => 'WPDO_HivePress_Bookings_Adapter', 'TMDO_HivePress_Bootstrap' => 'WPDO_HivePress_Bootstrap', - 'TMDO_HivePress_Comment_Router' => 'WPDO_HivePress_Comment_Router', 'TMDO_HivePress_Conflict_Guard' => 'WPDO_HivePress_Conflict_Guard', 'TMDO_HivePress_Core_Adapter' => 'WPDO_HivePress_Core_Adapter', 'TMDO_HivePress_Cron_Optimizer' => 'WPDO_HivePress_Cron_Optimizer', diff --git a/includes/class-tmdo-hivepress-bootstrap.php b/includes/class-tmdo-hivepress-bootstrap.php index 4c3ea9c..c5f9c5c 100644 --- a/includes/class-tmdo-hivepress-bootstrap.php +++ b/includes/class-tmdo-hivepress-bootstrap.php @@ -44,7 +44,6 @@ final class TMDO_HP_Bootstrap { require_once $base . 'includes/hivepress/class-tmdo-hivepress-detector.php'; require_once $base . 'includes/hivepress/class-tmdo-hivepress-conflict-guard.php'; require_once $base . 'includes/hivepress/class-tmdo-hivepress-bootstrap.php'; - require_once $base . 'includes/hivepress/class-tmdo-hivepress-comment-router.php'; require_once $base . 'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php'; // 13 adapters diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-favorites-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-favorites-adapter.php index 9086044..d87bc84 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-favorites-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-favorites-adapter.php @@ -8,27 +8,9 @@ * - listing → comment_post_ID * - added_at → comment_date * - * Problem: `wp_comments` has no UNIQUE constraint on (user_id, comment_post_ID, comment_type). - * A double-tap on the favourite button can race and create two duplicate - * favourite rows. HivePress relies on application-level dedup (SELECT before - * INSERT) which is not race-safe. - * - * Fix: shadow table `wp_wpdo_comment_hp_favorite` with UNIQUE(user_id, listing_id). - * Adapter mirrors every favorite write into the shadow table; the comment - * router rewrites "is X favorited by user Y" reads to hit the shadow table - * (1 indexed lookup vs WP_Comment_Query meta scan). - * - * Schema (declared via expected_columns; created by TMDO_Schema_Manager - * when adapter mode flips to dual_write): - * - * CREATE TABLE wp_wpdo_comment_hp_favorite ( - * comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, - * user_id BIGINT UNSIGNED NOT NULL, - * listing_id BIGINT UNSIGNED NOT NULL, - * created_at DATETIME NOT NULL, - * UNIQUE KEY unique_user_listing (user_id, listing_id), - * KEY idx_listing (listing_id) - * ); + * Nothing is stored in comment meta, so there is no EAV surface to flatten: + * the adapter only reports detection and doctor status. (It used to mirror + * favorites into `wpdo_comment_hp_favorite`, a table nothing ever created.) * * @package WP_Data_Optimizer * @since 3.0.0 @@ -41,18 +23,15 @@ if ( ! defined( 'ABSPATH' ) ) { if ( ! class_exists( 'TMDO_HivePress_Favorites_Adapter' ) ) { /** - * Favorites addon adapter. + * Favorites addon adapter (detection and doctor only). */ final class TMDO_HivePress_Favorites_Adapter implements TMDO_HivePress_Adapter { use TMDO_HivePress_Adapter_Trait; - /** Comment_type owned by this adapter. */ + /** Comment_type of the Favorite model. */ public const COMMENT_TYPE = 'hp_favorite'; - /** Shadow table name (without wpdb prefix). */ - public const TABLE = 'wpdo_comment_hp_favorite'; - /** * Constructor — bind register hooks via inherited trait. */ @@ -79,156 +58,6 @@ if ( ! class_exists( 'TMDO_HivePress_Favorites_Adapter' ) ) { public function minimum_addon_version(): string { return '1.2.0'; } - - /** - * Declare the shadow table to the Custom_Table_Registry. - * - * @param TMDO_Custom_Table_Registry $registry Registry singleton. - */ - public function on_register_custom_tables( TMDO_Custom_Table_Registry $registry ): void { - $this->register_custom_table( - $registry, - array( - 'table_name' => self::TABLE, - 'primary_key' => 'comment_id', - 'post_type_link' => null, - 'expected_columns' => array( - 'comment_id' => 'BIGINT UNSIGNED NOT NULL PRIMARY KEY', - 'user_id' => 'BIGINT UNSIGNED NOT NULL', - 'listing_id' => 'BIGINT UNSIGNED NOT NULL', - 'created_at' => 'DATETIME NOT NULL', - ), - 'indexes' => array( - 'unique_user_listing' => 'UNIQUE (user_id, listing_id)', - 'idx_listing' => '(listing_id)', - ), - 'doctor_callback' => array( $this, 'doctor_check' ), - ) - ); - } - - /** - * Sync wp_comments → shadow table on insert. - * - * Bound during `on_register_event_hooks()`. The comment-router uses - * the shadow table for "is favorited" lookups; sync keeps it consistent. - */ - public function on_register_event_hooks(): void { - add_action( 'wp_insert_comment', array( $this, 'mirror_insert' ), 10, 2 ); - add_action( 'deleted_comment', array( $this, 'mirror_delete' ), 10, 2 ); - } - - /** - * Mirror a newly-inserted hp_favorite into the shadow table. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_insert( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { - return; - } - global $wpdb; - $user_id = (int) ( $comment->user_id ?? 0 ); - $listing_id = (int) ( $comment->comment_post_ID ?? 0 ); - if ( $user_id <= 0 || $listing_id <= 0 ) { - return; - } - $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->prepare( - 'INSERT IGNORE INTO `' . $wpdb->prefix . self::TABLE . '` (comment_id, user_id, listing_id, created_at) VALUES (%d, %d, %d, %s)', - $comment_id, - $user_id, - $listing_id, - $comment->comment_date ?? current_time( 'mysql' ) - ) - ); - } - - /** - * Remove the shadow row when a hp_favorite comment is deleted. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_delete( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { - return; - } - global $wpdb; - $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $wpdb->prefix . self::TABLE, - array( 'comment_id' => $comment_id ), - array( '%d' ) - ); - } - - /** - * Doctor probe — verify shadow table exists. - * - * @return array{ok:bool, message:string, details?:array} - */ - public function doctor_check(): array { - global $wpdb; - if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { - return array( - 'ok' => false, - 'message' => 'wpdb global not available', - ); - } - $table = $wpdb->prefix . self::TABLE; - try { - $exists = (bool) $wpdb->get_var( - $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) - ); - if ( ! $exists ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-favorites: shadow table %s missing', $table ), - 'details' => array( 'table' => $table ), - ); - } - $rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - return array( - 'ok' => true, - 'message' => sprintf( 'hivepress-favorites: %s rows=%d', $table, $rows ), - 'details' => array( - 'table' => $table, - 'rows' => $rows, - ), - ); - } catch ( \Throwable $e ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-favorites doctor failed: %s', $e->getMessage() ), - ); - } - } - - /** - * 8-D self-score. - * - * D1: 1.0 — adapter never calls *_meta() - * D2: 1.0 — uses wpdb prefix, no hardcoded table literals - * D3: 1.0 — shadow table covers the model - * D4: 1.0 — expected_columns + indexes registered - * D5: 1.0 — listens to wp_insert_comment for cross-write consistency - * D6: 1.0 — zero options/transients - * D7: 1.0 — only queries own shadow table - * D8: 1.0 — UNIQUE(user_id, listing_id) covering index - */ - public function suitability_score(): array { - return $this->compose_score( array() ); - } - - /** - * FSM modules this adapter contributes. - * - * @return array - */ - public function migrations(): array { - return array( 'comment_hp_favorite' ); - } } } // end if ( ! class_exists ) diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-messages-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-messages-adapter.php index cc044dc..6fc1d29 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-messages-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-messages-adapter.php @@ -3,28 +3,12 @@ * Messages adapter — `hivepress-messages`. * * The Message model extends Comment (`comment_type = hp_message`) and stores - * the recipient_id in `comment_karma` — a creative re-use of an unused column, - * but it leaves the field unindexed for "messages received by user N" - * queries. Result: every dashboard "unread count" badge does an O(N) - * `WHERE comment_type = 'hp_message' AND comment_karma = N` scan over - * the entire wp_comments table. + * the recipient in `comment_karma`, the sender in `user_id` and the read flag + * in `comment_approved`. * - * Fix: shadow table `wp_wpdo_comment_hp_message` with proper recipient_id - * column + index. Adapter mirrors every hp_message write into the shadow, - * and the comment-router rewrites recipient/unread queries to hit the index. - * - * Schema: - * - * CREATE TABLE wp_wpdo_comment_hp_message ( - * comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, - * sender_id BIGINT UNSIGNED NOT NULL, - * recipient_id BIGINT UNSIGNED NOT NULL, - * listing_id BIGINT UNSIGNED NOT NULL, - * is_read TINYINT(1) NOT NULL DEFAULT 0, - * sent_at DATETIME NOT NULL, - * KEY idx_recipient_unread (recipient_id, is_read), - * KEY idx_thread (listing_id, sent_at) - * ); + * Nothing is stored in comment meta, so there is no EAV surface to flatten: + * the adapter only reports detection and doctor status. (It used to mirror + * messages into `wpdo_comment_hp_message`, a table nothing ever created.) * * @package WP_Data_Optimizer * @since 3.0.0 @@ -37,18 +21,15 @@ if ( ! defined( 'ABSPATH' ) ) { if ( ! class_exists( 'TMDO_HivePress_Messages_Adapter' ) ) { /** - * Messages addon adapter. + * Messages addon adapter (detection and doctor only). */ final class TMDO_HivePress_Messages_Adapter implements TMDO_HivePress_Adapter { use TMDO_HivePress_Adapter_Trait; - /** Comment_type owned by this adapter. */ + /** Comment_type of the Message model. */ public const COMMENT_TYPE = 'hp_message'; - /** Shadow table name (without wpdb prefix). */ - public const TABLE = 'wpdo_comment_hp_message'; - /** * Constructor — bind register hooks via inherited trait. */ @@ -75,189 +56,6 @@ if ( ! class_exists( 'TMDO_HivePress_Messages_Adapter' ) ) { public function minimum_addon_version(): string { return '1.4.0'; } - - /** - * Declare the shadow table to the Custom_Table_Registry. - * - * @param TMDO_Custom_Table_Registry $registry Registry singleton. - */ - public function on_register_custom_tables( TMDO_Custom_Table_Registry $registry ): void { - $this->register_custom_table( - $registry, - array( - 'table_name' => self::TABLE, - 'primary_key' => 'comment_id', - 'post_type_link' => null, - 'expected_columns' => array( - 'comment_id' => 'BIGINT UNSIGNED NOT NULL PRIMARY KEY', - 'sender_id' => 'BIGINT UNSIGNED NOT NULL', - 'recipient_id' => 'BIGINT UNSIGNED NOT NULL', - 'listing_id' => 'BIGINT UNSIGNED NOT NULL', - 'is_read' => 'TINYINT(1) NOT NULL DEFAULT 0', - 'sent_at' => 'DATETIME NOT NULL', - ), - 'indexes' => array( - 'idx_recipient_unread' => '(recipient_id, is_read)', - 'idx_thread' => '(listing_id, sent_at)', - ), - 'doctor_callback' => array( $this, 'doctor_check' ), - ) - ); - } - - /** - * Bind sync hooks on hp_message comment insert / update / delete. - * - * The recipient_id is stored in comment_karma by HivePress; we mirror - * it into a properly-named indexed column. is_read tracks comment_approved - * (HP uses 0=unread / 1=read) — same column, mirrored. - */ - public function on_register_event_hooks(): void { - add_action( 'wp_insert_comment', array( $this, 'mirror_insert' ), 10, 2 ); - add_action( 'wp_set_comment_status', array( $this, 'mirror_status_change' ), 10, 2 ); - add_action( 'deleted_comment', array( $this, 'mirror_delete' ), 10, 2 ); - } - - /** - * Mirror a new hp_message into the shadow table. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_insert( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { - return; - } - global $wpdb; - $sender_id = (int) ( $comment->user_id ?? 0 ); - $recipient_id = (int) ( $comment->comment_karma ?? 0 ); - $listing_id = (int) ( $comment->comment_post_ID ?? 0 ); - $is_read = (int) ( $comment->comment_approved ?? 0 ); - if ( $sender_id <= 0 || $recipient_id <= 0 ) { - // Malformed message — nothing useful to mirror. - return; - } - $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->prepare( - 'INSERT INTO `' . $wpdb->prefix . self::TABLE . '` (comment_id, sender_id, recipient_id, listing_id, is_read, sent_at) VALUES (%d, %d, %d, %d, %d, %s) ON DUPLICATE KEY UPDATE is_read = VALUES(is_read)', - $comment_id, - $sender_id, - $recipient_id, - $listing_id, - $is_read, - $comment->comment_date ?? current_time( 'mysql' ) - ) - ); - } - - /** - * Update is_read when a message is marked read (comment_approved flip). - * - * @param int $comment_id Comment ID. - * @param string $status New status ('approve', 'hold', 'spam', 'trash'). - */ - public function mirror_status_change( int $comment_id, string $status ): void { - global $wpdb; - // Look up the comment to confirm it's hp_message before touching. - if ( ! function_exists( 'get_comment' ) ) { - return; - } - $comment = get_comment( $comment_id ); - if ( ! $comment || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { - return; - } - $is_read = ( 'approve' === $status ) ? 1 : 0; - $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $wpdb->prefix . self::TABLE, - array( 'is_read' => $is_read ), - array( 'comment_id' => $comment_id ), - array( '%d' ), - array( '%d' ) - ); - } - - /** - * Remove shadow row on hp_message deletion. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_delete( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { - return; - } - global $wpdb; - $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $wpdb->prefix . self::TABLE, - array( 'comment_id' => $comment_id ), - array( '%d' ) - ); - } - - /** - * Doctor probe — verify shadow table. - * - * @return array{ok:bool, message:string, details?:array} - */ - public function doctor_check(): array { - global $wpdb; - if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { - return array( - 'ok' => false, - 'message' => 'wpdb global not available', - ); - } - $table = $wpdb->prefix . self::TABLE; - try { - $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - if ( ! $exists ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-messages: shadow table %s missing', $table ), - 'details' => array( 'table' => $table ), - ); - } - $rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - return array( - 'ok' => true, - 'message' => sprintf( 'hivepress-messages: %s rows=%d', $table, $rows ), - 'details' => array( - 'table' => $table, - 'rows' => $rows, - ), - ); - } catch ( \Throwable $e ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-messages doctor failed: %s', $e->getMessage() ), - ); - } - } - - /** - * 8-D self-score. - * - * D1: 1.0 — adapter never calls *_meta() - * D2: 1.0 — uses wpdb prefix exclusively - * D3: 1.0 — shadow table covers recipient/unread access - * D4: 1.0 — expected_columns + indexes registered - * D5: 1.0 — listens to wp_insert_comment + status change for sync - * D6: 1.0 — zero options/transients - * D7: 1.0 — only queries own shadow table - * D8: 1.0 — idx_recipient_unread covers the hot dashboard query - */ - public function suitability_score(): array { - return $this->compose_score( array() ); - } - - /** - * FSM modules this adapter contributes. - * - * @return array - */ - public function migrations(): array { - return array( 'comment_hp_message' ); - } } } // end if ( ! class_exists ) diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php index 934f651..4ef0b7f 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php @@ -9,21 +9,9 @@ * — same access pattern as Listing (user dashboard + expiry cron) * * - Offer (comment_type=hp_offer, parent post_type=hp_request) - * comment-column based; benefit comes from comment-router using - * (comment_post_ID, comment_type) covering index + dedicated shadow - * table for "approved offers per request" aggregation. - * - * Shadow table for offers (analogous to favorites pattern): - * - * CREATE TABLE wp_wpdo_comment_hp_offer ( - * comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, - * request_id BIGINT UNSIGNED NOT NULL, - * bidder_id BIGINT UNSIGNED NOT NULL, - * approved TINYINT(1) NOT NULL DEFAULT 0, - * created_at DATETIME NOT NULL, - * KEY idx_request_approved (request_id, approved), - * KEY idx_bidder (bidder_id) - * ); + * stored in comment columns only — no comment meta to flatten. + * (The adapter used to mirror offers into `wpdo_comment_hp_offer`, + * a table nothing ever created.) * * @package WP_Data_Optimizer * @since 3.0.0 @@ -45,9 +33,6 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) { /** Comment_type for the Offer side of Requests. */ public const OFFER_COMMENT_TYPE = 'hp_offer'; - /** Shadow table for offers (without wpdb prefix). */ - public const OFFER_TABLE = 'wpdo_comment_hp_offer'; - /** * Constructor — bind register hooks via inherited trait. */ @@ -105,104 +90,7 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) { } /** - * Declare offer shadow table. - * - * @param TMDO_Custom_Table_Registry $registry Registry singleton. - */ - public function on_register_custom_tables( TMDO_Custom_Table_Registry $registry ): void { - $this->register_custom_table( - $registry, - array( - 'table_name' => self::OFFER_TABLE, - 'primary_key' => 'comment_id', - 'post_type_link' => 'hp_request', - 'expected_columns' => array( - 'comment_id' => 'BIGINT UNSIGNED NOT NULL PRIMARY KEY', - 'request_id' => 'BIGINT UNSIGNED NOT NULL', - 'bidder_id' => 'BIGINT UNSIGNED NOT NULL', - 'approved' => 'TINYINT(1) NOT NULL DEFAULT 0', - 'created_at' => 'DATETIME NOT NULL', - ), - 'indexes' => array( - 'idx_request_approved' => '(request_id, approved)', - 'idx_bidder' => '(bidder_id)', - ), - 'doctor_callback' => array( $this, 'doctor_check' ), - ) - ); - } - - /** - * Mirror new hp_offer comments into the shadow table. - */ - public function on_register_event_hooks(): void { - add_action( 'wp_insert_comment', array( $this, 'mirror_offer_insert' ), 10, 2 ); - add_action( 'edit_comment', array( $this, 'mirror_offer_update' ), 10, 2 ); - add_action( 'deleted_comment', array( $this, 'mirror_offer_delete' ), 10, 2 ); - } - - /** - * Insert a hp_offer mirror row. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_offer_insert( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::OFFER_COMMENT_TYPE ) { - return; - } - global $wpdb; - $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->prepare( - 'INSERT INTO `' . $wpdb->prefix . self::OFFER_TABLE . '` (comment_id, request_id, bidder_id, approved, created_at) VALUES (%d, %d, %d, %d, %s) ON DUPLICATE KEY UPDATE approved = VALUES(approved)', - $comment_id, - (int) ( $comment->comment_post_ID ?? 0 ), - (int) ( $comment->user_id ?? 0 ), - (int) ( $comment->comment_approved ?? 0 ), - $comment->comment_date ?? current_time( 'mysql' ) - ) - ); - } - - /** - * Update mirror row when comment is edited (approval flip is the hot path). - * - * @param int $comment_id Comment ID. - * @param array $data Comment data array passed by edit_comment. - */ - public function mirror_offer_update( int $comment_id, $data ): void { - unset( $data ); - global $wpdb; - // Re-fetch comment object for accurate state then re-mirror via insert path. - if ( function_exists( 'get_comment' ) ) { - $comment = get_comment( $comment_id ); - if ( $comment ) { - $this->mirror_offer_insert( $comment_id, $comment ); - } - } - unset( $wpdb ); - } - - /** - * Remove mirror row on hp_offer deletion. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_offer_delete( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::OFFER_COMMENT_TYPE ) { - return; - } - global $wpdb; - $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $wpdb->prefix . self::OFFER_TABLE, - array( 'comment_id' => $comment_id ), - array( '%d' ) - ); - } - - /** - * Doctor probe — request hot table + offer shadow table. + * Doctor probe — request hot table. * * @return array{ok:bool, message:string, details?:array} */ @@ -214,41 +102,21 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) { 'message' => 'wpdb global not available', ); } - $details = array(); - $ok = true; - foreach ( array( - 'hp_request_hot' => $wpdb->prefix . 'wpdo_hot_hp_request', - 'hp_offer_shadow' => $wpdb->prefix . self::OFFER_TABLE, - ) as $key => $table ) { - try { - $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - $details[ $key ] = array( - 'table' => $table, - 'exists' => $exists, - ); - if ( ! $exists ) { - $ok = false; - } - } catch ( \Throwable $e ) { - $details[ $key ] = array( - 'table' => $table, - 'error' => $e->getMessage(), - ); - $ok = false; - } + $table = $wpdb->prefix . 'wpdo_hot_hp_request'; + try { + $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); + return array( + 'ok' => $exists, + 'message' => $exists + ? sprintf( 'hivepress-requests: %s present', $table ) + : sprintf( 'hivepress-requests: %s missing', $table ), + ); + } catch ( \Throwable $e ) { + return array( + 'ok' => false, + 'message' => sprintf( 'hivepress-requests doctor failed: %s', $e->getMessage() ), + ); } - return array( - 'ok' => $ok, - 'message' => $ok ? 'hivepress-requests: tables OK' : 'hivepress-requests: one or more tables missing', - 'details' => $details, - ); - } - - /** - * 8-D self-score. - */ - public function suitability_score(): array { - return $this->compose_score( array() ); } /** @@ -257,10 +125,7 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) { * @return array */ public function migrations(): array { - return array( - 'hot_hp_request', - 'comment_hp_offer', - ); + return array( 'hot_hp_request' ); } } diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-reviews-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-reviews-adapter.php index 8dae0ad..a3ded1c 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-reviews-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-reviews-adapter.php @@ -10,14 +10,12 @@ * - approved → comment_approved * - listing → comment_post_ID * - * No postmeta is used; no entity-bridge group needed. Hot-path optimization - * comes from `TMDO_HivePress_Comment_Router` rewriting comment-query SQL to - * use `comment_post_ID + comment_type + comment_approved` index, and from - * the `wp_wpdo_comment_hp_review` flat table that WPDO Comment entity bridge - * already maintains (since v2.13.x). + * No postmeta is used; no entity-bridge group needed. Queries use the core + * `wp_comments` indexes on `comment_post_ID` / `comment_type`, and the + * `wp_wpdo_comment_hp_review` flat table is maintained by the WPDO Comment + * entity bridge (since v2.13.x). * * This adapter's role: - * - Declare the comment_type to the comment-router * - Doctor probe of the entity-bridge table * - Self-report 8-D suitability score * @@ -124,7 +122,7 @@ if ( ! class_exists( 'TMDO_HivePress_Reviews_Adapter' ) ) { * D5: 1.0 — no cross-adapter writes needed yet * D6: 1.0 — zero options/transients written * D7: 1.0 — only queries own entity-bridge table - * D8: 1.0 — comment-router uses (comment_post_ID, comment_type) covering index + * D8: 1.0 — reads use the core wp_comments (comment_post_ID, comment_type) indexes */ public function suitability_score(): array { return $this->compose_score( array() ); diff --git a/includes/hivepress/class-tmdo-hivepress-bootstrap.php b/includes/hivepress/class-tmdo-hivepress-bootstrap.php index d271b28..5654af6 100644 --- a/includes/hivepress/class-tmdo-hivepress-bootstrap.php +++ b/includes/hivepress/class-tmdo-hivepress-bootstrap.php @@ -118,12 +118,9 @@ if ( ! class_exists( 'TMDO_HivePress_Bootstrap' ) ) { self::bind_adapter( $slug, $version ); } - // Step 5: register cross-cutting components (router + cron optimizer). - // Both are gated by their own opt-in option and only activate when - // the operator flips it; safe to call here unconditionally. - if ( class_exists( 'TMDO_HivePress_Comment_Router' ) ) { - TMDO_HivePress_Comment_Router::register(); - } + // Step 5: register the cron optimizer. It is gated by its own opt-in + // option and only activates when the operator flips it; safe to call + // here unconditionally. if ( class_exists( 'TMDO_HivePress_Cron_Optimizer' ) ) { TMDO_HivePress_Cron_Optimizer::register(); } diff --git a/includes/hivepress/class-tmdo-hivepress-comment-router.php b/includes/hivepress/class-tmdo-hivepress-comment-router.php deleted file mode 100644 index cb37e5a..0000000 --- a/includes/hivepress/class-tmdo-hivepress-comment-router.php +++ /dev/null @@ -1,166 +0,0 @@ - - */ - private const SHADOW_MAP = array( - 'hp_message' => 'wpdo_comment_hp_message', - 'hp_favorite' => 'wpdo_comment_hp_favorite', - 'hp_offer' => 'wpdo_comment_hp_offer', - 'hp_review' => 'wpdo_comment_hp_review', - ); - - /** - * Whether the router is bound this request. - * - * @var bool - */ - private static bool $bound = false; - - /** - * Bind the comments_clauses filter (idempotent). - * - * Bootstrap calls this from each adapter's on_register_query_hooks() - * so router activation follows the same gate as adapter binding. - */ - public static function register(): void { - if ( self::$bound ) { - return; - } - if ( ! self::is_enabled() ) { - return; - } - add_filter( 'comments_clauses', array( __CLASS__, 'rewrite' ), 20, 2 ); - self::$bound = true; - } - - /** - * Reset internal state (test only). - * - * @internal - */ - public static function reset_for_tests(): void { - self::$bound = false; - } - - /** - * Whether router is enabled via the operator option. - */ - public static function is_enabled(): bool { - if ( ! function_exists( 'get_option' ) ) { - return false; - } - return (bool) (int) get_option( self::OPTION_ENABLED, 0 ); - } - - /** - * Filter callback: rewrite comments_clauses for known hp comment types. - * - * @param array $clauses SQL clauses (join, where, fields, ...). - * @param \WP_Comment_Query $query The query object. - * @return array Possibly-modified clauses. - */ - public static function rewrite( array $clauses, $query ): array { - $type = self::query_comment_type( $query ); - if ( '' === $type || ! isset( self::SHADOW_MAP[ $type ] ) ) { - return $clauses; - } - - global $wpdb; - $shadow = $wpdb->prefix . self::SHADOW_MAP[ $type ]; - $alias = 'wpdo_shadow'; - - // LEFT JOIN keeps row visibility when shadow lags (during backfill). - $join = ( $clauses['join'] ?? '' ) . " LEFT JOIN `{$shadow}` AS {$alias} ON {$alias}.comment_id = {$wpdb->comments}.comment_ID"; - - // Filter pushdown: hp_message recipient lookup uses comment_karma in HP. - // We rewrite WHERE clauses that filter by comment_karma (= recipient hack) - // to use the indexed shadow column. - $where = (string) ( $clauses['where'] ?? '' ); - if ( 'hp_message' === $type && '' !== $where ) { - $where = preg_replace( - '/' . preg_quote( $wpdb->comments, '/' ) . '\.comment_karma\s*=\s*(\d+)/', - $alias . '.recipient_id = $1', - $where - ); - } - - $clauses['join'] = $join; - $clauses['where'] = $where; - - /** - * Filter: wpdo_hivepress_comment_router_clauses - * - * Allows fine-grained tweaks per comment_type. Intended for adapter - * tests + future custom-column lookups. - * - * @param array $clauses Modified clauses. - * @param string $type Matched comment_type. - * @param string $alias Shadow table alias. - */ - return apply_filters( 'wpdo_hivepress_comment_router_clauses', $clauses, $type, $alias ); - } - - /** - * Extract the comment_type from a WP_Comment_Query. - * - * @param \WP_Comment_Query $query Query object. - */ - private static function query_comment_type( $query ): string { - if ( ! is_object( $query ) || ! isset( $query->query_vars ) ) { - return ''; - } - $type = $query->query_vars['type'] ?? ''; - if ( is_array( $type ) ) { - $type = reset( $type ); - } - return is_string( $type ) ? $type : ''; - } - } - -} // end if ( ! class_exists ) diff --git a/includes/hivepress/interface-tmdo-hp-adapter.php b/includes/hivepress/interface-tmdo-hp-adapter.php index acc2233..0cd52c4 100644 --- a/includes/hivepress/interface-tmdo-hp-adapter.php +++ b/includes/hivepress/interface-tmdo-hp-adapter.php @@ -119,7 +119,7 @@ if ( ! interface_exists( 'TMDO_HivePress_Adapter' ) ) { * * Adapters with hot zone fields used in search/filter MUST register a * `pre_get_posts` / `pre_get_comments` filter that defers to - * `TMDO_HivePress_Query_Router` / `..._Comment_Router`. + * `TMDO_HivePress_Query_Router`. * * Called on `init:5` after Schema_Registry is fully populated. */ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d041084..e358c4e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -38,7 +38,6 @@ $tmdo_hp_files = array( 'includes/hivepress/class-tmdo-hivepress-detector.php', 'includes/hivepress/class-tmdo-hivepress-conflict-guard.php', 'includes/hivepress/class-tmdo-hivepress-bootstrap.php', - 'includes/hivepress/class-tmdo-hivepress-comment-router.php', 'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php', 'includes/hivepress/class-tmdo-hivepress-attribute-bridge.php', 'includes/hivepress/class-tmdo-hivepress-suitability-scorer.php', diff --git a/tests/integration/HivepressIntegrationTest.php b/tests/integration/HivepressIntegrationTest.php index b1f6ce1..bfa2e4b 100644 --- a/tests/integration/HivepressIntegrationTest.php +++ b/tests/integration/HivepressIntegrationTest.php @@ -12,147 +12,17 @@ use PHPUnit\Framework\TestCase; * Exercises against real MariaDB (via tests/integration/bootstrap.php) the * scenarios that unit tests can only stub: * - * - Shadow table CREATE actually works with the declared expected_columns - * - UNIQUE constraint on (user_id, listing_id) really prevents dup favorites - * - mirror_insert() roundtrips through real $wpdb (SQL syntax valid) * - 7-state FSM lifecycle moves WPDO_Feature_Flags correctly * * @group integration */ class HivepressIntegrationTest extends TestCase { - /** Test-scope shadow table for favorites. */ - private const TABLE_FAVORITE = 'wp_itest_wpdo_comment_hp_favorite'; - - /** Test-scope shadow table for messages. */ - private const TABLE_MESSAGE = 'wp_itest_wpdo_comment_hp_message'; - - public static function setUpBeforeClass(): void { - // HivePress integration files are loaded at file-top level so the - // Testable* subclass definitions below can resolve their parents at - // parse time. Schema setup happens here. - global $wpdb; - $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_FAVORITE . '`' ); - $wpdb->query( "CREATE TABLE `" . self::TABLE_FAVORITE . "` ( - comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, - user_id BIGINT UNSIGNED NOT NULL, - listing_id BIGINT UNSIGNED NOT NULL, - created_at DATETIME NOT NULL, - UNIQUE KEY unique_user_listing (user_id, listing_id), - KEY idx_listing (listing_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ); - - $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_MESSAGE . '`' ); - $wpdb->query( "CREATE TABLE `" . self::TABLE_MESSAGE . "` ( - comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, - sender_id BIGINT UNSIGNED NOT NULL, - recipient_id BIGINT UNSIGNED NOT NULL, - listing_id BIGINT UNSIGNED NOT NULL, - is_read TINYINT(1) NOT NULL DEFAULT 0, - sent_at DATETIME NOT NULL, - KEY idx_recipient_unread (recipient_id, is_read), - KEY idx_thread (listing_id, sent_at) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ); - } - - public static function tearDownAfterClass(): void { - global $wpdb; - $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_FAVORITE . '`' ); - $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_MESSAGE . '`' ); - } - protected function setUp(): void { - global $wpdb; - $wpdb->query( 'TRUNCATE TABLE `' . self::TABLE_FAVORITE . '`' ); - $wpdb->query( 'TRUNCATE TABLE `' . self::TABLE_MESSAGE . '`' ); // Clear FSM module state between tests. $GLOBALS['_wp_options']['wpdo_features'] = array(); } - // ── Schema tests ──────────────────────────────────────────────────────── - - public function test_favorite_table_has_unique_user_listing_constraint(): void { - global $wpdb; - $row = $wpdb->get_row( 'SHOW CREATE TABLE `' . self::TABLE_FAVORITE . '`', 'ARRAY_A' ); - $this->assertNotNull( $row ); - $ddl = $row['Create Table'] ?? ''; - $this->assertStringContainsString( 'UNIQUE KEY', $ddl ); - $this->assertStringContainsString( 'unique_user_listing', $ddl ); - } - - public function test_message_table_has_recipient_unread_index(): void { - global $wpdb; - $row = $wpdb->get_row( 'SHOW CREATE TABLE `' . self::TABLE_MESSAGE . '`', 'ARRAY_A' ); - $this->assertNotNull( $row ); - $ddl = $row['Create Table'] ?? ''; - $this->assertStringContainsString( 'idx_recipient_unread', $ddl ); - $this->assertStringContainsString( 'recipient_id', $ddl ); - } - - // ── mirror_insert real roundtrip ──────────────────────────────────────── - - public function test_favorite_mirror_insert_writes_real_row(): void { - global $wpdb; - $adapter = new WPDO_HivePress_Favorites_Adapter(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_favorite'; - $comment->user_id = 11; - $comment->comment_post_ID = 222; - $comment->comment_date = '2026-05-04 12:00:00'; - - $adapter->mirror_insert( 1001, $comment ); - - $rows = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_FAVORITE . '`' ); - $this->assertSame( 1, $rows ); - - $saved = $wpdb->get_row( 'SELECT * FROM `' . self::TABLE_FAVORITE . '` WHERE comment_id = 1001', 'ARRAY_A' ); - $this->assertSame( '11', (string) $saved['user_id'] ); - $this->assertSame( '222', (string) $saved['listing_id'] ); - } - - public function test_favorite_unique_constraint_blocks_double_tap(): void { - global $wpdb; - $adapter = new WPDO_HivePress_Favorites_Adapter(); - - // Two different comment_ids but same user+listing — UNIQUE should win. - $comment = new stdClass(); - $comment->comment_type = 'hp_favorite'; - $comment->user_id = 7; - $comment->comment_post_ID = 99; - $comment->comment_date = '2026-05-04 12:00:00'; - - $adapter->mirror_insert( 2001, $comment ); - $adapter->mirror_insert( 2002, $comment ); // Race-style duplicate. - - $rows = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_FAVORITE . '` WHERE user_id = 7 AND listing_id = 99' ); - $this->assertSame( 1, $rows, 'UNIQUE(user_id, listing_id) must reject duplicate.' ); - } - - public function test_message_mirror_insert_promotes_recipient_id(): void { - global $wpdb; - $adapter = new WPDO_HivePress_Messages_Adapter(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_message'; - $comment->user_id = 5; // sender - $comment->comment_karma = 42; // recipient (HP hack) - $comment->comment_post_ID = 99; - $comment->comment_approved = 0; - $comment->comment_date = '2026-05-04 12:00:00'; - - $adapter->mirror_insert( 3001, $comment ); - - $saved = $wpdb->get_row( 'SELECT * FROM `' . self::TABLE_MESSAGE . '` WHERE comment_id = 3001', 'ARRAY_A' ); - $this->assertNotNull( $saved ); - $this->assertSame( '42', (string) $saved['recipient_id'] ); - $this->assertSame( '5', (string) $saved['sender_id'] ); - - // recipient_unread index supports the dashboard query. - $unread_count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_MESSAGE . '` WHERE recipient_id = 42 AND is_read = 0' ); - $this->assertSame( 1, $unread_count ); - } - // ── FSM lifecycle ─────────────────────────────────────────────────────── public function test_fsm_lifecycle_progression(): void { @@ -189,9 +59,3 @@ class HivepressIntegrationTest extends TestCase { $this->assertSame( 0, $count, 'Optimizer must short-circuit when module is idle.' ); } } - -// Production adapters use `$wpdb->prefix . self::TABLE` to compose the full -// table name. Integration test bootstrap sets prefix to `wp_itest_`, so the -// production mirror_insert() naturally writes to wp_itest_wpdo_comment_hp_*. -// No subclassing or test-double needed — the production code path is exercised -// directly against real MariaDB with isolated prefix. diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 2ce8c97..72f5713 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -39,7 +39,6 @@ $tmdo_hp_files = array( 'includes/hivepress/class-tmdo-hivepress-detector.php', 'includes/hivepress/class-tmdo-hivepress-conflict-guard.php', 'includes/hivepress/class-tmdo-hivepress-bootstrap.php', - 'includes/hivepress/class-tmdo-hivepress-comment-router.php', 'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php', 'includes/hivepress/class-tmdo-hivepress-attribute-bridge.php', 'includes/hivepress/class-tmdo-hivepress-suitability-scorer.php', diff --git a/tests/unit/CommentRouterTest.php b/tests/unit/CommentRouterTest.php deleted file mode 100644 index 7ce74f4..0000000 --- a/tests/unit/CommentRouterTest.php +++ /dev/null @@ -1,92 +0,0 @@ -install_wpdb_mock(); - WPDO_HivePress_Comment_Router::reset_for_tests(); - // Default disabled. - $GLOBALS['_wp_options'][ WPDO_HivePress_Comment_Router::OPTION_ENABLED ] = 0; - } - - public function test_disabled_by_default(): void { - $this->assertFalse( WPDO_HivePress_Comment_Router::is_enabled() ); - } - - public function test_enabled_when_option_set(): void { - $GLOBALS['_wp_options'][ WPDO_HivePress_Comment_Router::OPTION_ENABLED ] = 1; - $this->assertTrue( WPDO_HivePress_Comment_Router::is_enabled() ); - } - - public function test_register_is_idempotent(): void { - $GLOBALS['_wp_options'][ WPDO_HivePress_Comment_Router::OPTION_ENABLED ] = 1; - WPDO_HivePress_Comment_Router::register(); - WPDO_HivePress_Comment_Router::register(); // Second call must be no-op. - $this->assertTrue( true ); // No throw = pass - } - - public function test_rewrite_skips_unknown_comment_type(): void { - $query = new stdClass(); - $query->query_vars = array( 'type' => 'comment' ); // not in shadow map - - $clauses = array( 'join' => '', 'where' => '1=1' ); - $out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query ); - - $this->assertSame( $clauses, $out ); - } - - public function test_rewrite_adds_left_join_for_hp_favorite(): void { - $query = new stdClass(); - $query->query_vars = array( 'type' => 'hp_favorite' ); - - $clauses = array( 'join' => '', 'where' => '1=1' ); - $out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query ); - - $this->assertStringContainsString( 'LEFT JOIN', $out['join'] ); - $this->assertStringContainsString( 'wpdo_comment_hp_favorite', $out['join'] ); - $this->assertStringContainsString( 'AS wpdo_shadow', $out['join'] ); - } - - public function test_rewrite_promotes_recipient_for_hp_message(): void { - $query = new stdClass(); - $query->query_vars = array( 'type' => 'hp_message' ); - - $clauses = array( - 'join' => '', - 'where' => 'wp_comments.comment_karma = 42 AND comment_approved = 0', - ); - $out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query ); - - // recipient lookup should be rewritten to use shadow column. - $this->assertStringContainsString( 'wpdo_shadow.recipient_id = 42', $out['where'] ); - // Other predicates preserved. - $this->assertStringContainsString( 'comment_approved = 0', $out['where'] ); - } - - public function test_rewrite_handles_array_type(): void { - $query = new stdClass(); - $query->query_vars = array( 'type' => array( 'hp_review', 'comment' ) ); - - $clauses = array( 'join' => '', 'where' => '' ); - $out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query ); - - // First element of type array should match (hp_review). - $this->assertStringContainsString( 'wpdo_comment_hp_review', $out['join'] ); - } - - public function test_rewrite_returns_unchanged_when_query_invalid(): void { - $clauses = array( 'join' => '', 'where' => '1=1' ); - $out = WPDO_HivePress_Comment_Router::rewrite( $clauses, 'not an object' ); - $this->assertSame( $clauses, $out ); - } -} diff --git a/tests/unit/FavoritesAdapterTest.php b/tests/unit/FavoritesAdapterTest.php index 0b74b86..b6a3e23 100644 --- a/tests/unit/FavoritesAdapterTest.php +++ b/tests/unit/FavoritesAdapterTest.php @@ -6,6 +6,9 @@ use PHPUnit\Framework\TestCase; /** * Tests for WPDO_HivePress_Favorites_Adapter. * + * The adapter no longer mirrors favorites into a shadow table: that table was + * never created, so every mirror write failed. + * * @covers WPDO_HivePress_Favorites_Adapter */ class HivePressFavoritesAdapterTest extends TestCase { @@ -32,72 +35,22 @@ class HivePressFavoritesAdapterTest extends TestCase { public function test_constants_exposed(): void { $this->assertSame( 'hp_favorite', WPDO_HivePress_Favorites_Adapter::COMMENT_TYPE ); - $this->assertSame( 'wpdo_comment_hp_favorite', WPDO_HivePress_Favorites_Adapter::TABLE ); } - public function test_on_register_custom_tables_declares_shadow_table(): void { + public function test_registers_no_shadow_table(): void { $registry = WPDO_Custom_Table_Registry::instance(); $this->adapter->on_register_custom_tables( $registry ); - $tables = $registry->all(); - $this->assertCount( 1, $tables ); - - $cfg = reset( $tables ); - $this->assertSame( 'hivepress-favorites', $cfg['provider'] ); - $this->assertSame( 'wpdo_comment_hp_favorite', $cfg['table_name'] ); - $this->assertSame( 'comment_id', $cfg['primary_key'] ); - $this->assertArrayHasKey( 'unique_user_listing', $cfg['indexes'] ); - $this->assertStringContainsString( 'UNIQUE', $cfg['indexes']['unique_user_listing'] ); + $this->assertSame( array(), $registry->all() ); } - public function test_mirror_insert_skips_non_favorite_comments(): void { - // Stub $wpdb tracks queries. - global $wpdb; - $wpdb->queries = array(); + public function test_binds_no_comment_hooks(): void { + $GLOBALS['_wp_filter_callbacks'] = array(); - $comment = new stdClass(); - $comment->comment_type = 'comment'; // ← not hp_favorite - $comment->user_id = 1; - $comment->comment_post_ID = 99; + $this->adapter->on_register_event_hooks(); - $this->adapter->mirror_insert( 123, $comment ); - $this->assertSame( array(), $wpdb->queries ); - } - - public function test_mirror_insert_writes_for_hp_favorite(): void { - global $wpdb; - $wpdb->queries = array(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_favorite'; - $comment->user_id = 7; - $comment->comment_post_ID = 42; - $comment->comment_date = '2026-05-04 12:00:00'; - - $this->adapter->mirror_insert( 100, $comment ); - - $this->assertCount( 1, $wpdb->queries ); - $this->assertStringContainsString( 'INSERT IGNORE', $wpdb->queries[0] ); - $this->assertStringContainsString( 'wpdo_comment_hp_favorite', $wpdb->queries[0] ); - } - - public function test_mirror_insert_skips_when_user_or_listing_missing(): void { - global $wpdb; - $wpdb->queries = array(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_favorite'; - $comment->user_id = 0; - $comment->comment_post_ID = 99; - - $this->adapter->mirror_insert( 100, $comment ); - $this->assertSame( array(), $wpdb->queries ); - } - - public function test_doctor_check_reports_missing_table_in_stub_env(): void { - $result = $this->adapter->doctor_check(); - $this->assertFalse( $result['ok'] ); - $this->assertStringContainsString( 'wpdo_comment_hp_favorite', $result['message'] ); + $this->assertArrayNotHasKey( 'wp_insert_comment', $GLOBALS['_wp_filter_callbacks'] ); + $this->assertArrayNotHasKey( 'deleted_comment', $GLOBALS['_wp_filter_callbacks'] ); } public function test_score_aggregates_to_perfect_ten(): void { @@ -105,7 +58,7 @@ class HivePressFavoritesAdapterTest extends TestCase { $this->assertSame( 10.0, $score['aggregate'] ); } - public function test_migrations_lists_comment_module(): void { - $this->assertSame( array( 'comment_hp_favorite' ), $this->adapter->migrations() ); + public function test_migrations_is_empty(): void { + $this->assertSame( array(), $this->adapter->migrations() ); } } diff --git a/tests/unit/MessagesAdapterTest.php b/tests/unit/MessagesAdapterTest.php index ab59f5c..c9d6c33 100644 --- a/tests/unit/MessagesAdapterTest.php +++ b/tests/unit/MessagesAdapterTest.php @@ -6,6 +6,9 @@ use PHPUnit\Framework\TestCase; /** * Tests for WPDO_HivePress_Messages_Adapter. * + * The adapter no longer mirrors messages into a shadow table: that table was + * never created, so every mirror write failed. + * * @covers WPDO_HivePress_Messages_Adapter */ class HivePressMessagesAdapterTest extends TestCase { @@ -28,77 +31,26 @@ class HivePressMessagesAdapterTest extends TestCase { $this->assertSame( 'hivepress-messages', $this->adapter->plugin_slug() ); $this->assertSame( 'HivePress\\Messages\\Plugin', $this->adapter->detection_class() ); $this->assertSame( 'hp_message', WPDO_HivePress_Messages_Adapter::COMMENT_TYPE ); - $this->assertSame( 'wpdo_comment_hp_message', WPDO_HivePress_Messages_Adapter::TABLE ); } - public function test_on_register_custom_tables_declares_indexed_recipient(): void { + public function test_registers_no_shadow_table(): void { $registry = WPDO_Custom_Table_Registry::instance(); $this->adapter->on_register_custom_tables( $registry ); - $tables = $registry->all(); - $cfg = reset( $tables ); - $this->assertSame( 'wpdo_comment_hp_message', $cfg['table_name'] ); - $this->assertArrayHasKey( 'recipient_id', $cfg['expected_columns'] ); - $this->assertArrayHasKey( 'idx_recipient_unread', $cfg['indexes'] ); - $this->assertStringContainsString( 'recipient_id, is_read', $cfg['indexes']['idx_recipient_unread'] ); + $this->assertSame( array(), $registry->all() ); } - public function test_mirror_insert_skips_non_message_comments(): void { - global $wpdb; - $wpdb->queries = array(); + public function test_binds_no_comment_hooks(): void { + $GLOBALS['_wp_filter_callbacks'] = array(); - $comment = new stdClass(); - $comment->comment_type = 'comment'; - $comment->user_id = 1; - $comment->comment_karma = 5; + $this->adapter->on_register_event_hooks(); - $this->adapter->mirror_insert( 1, $comment ); - $this->assertSame( array(), $wpdb->queries ); + $this->assertArrayNotHasKey( 'wp_insert_comment', $GLOBALS['_wp_filter_callbacks'] ); + $this->assertArrayNotHasKey( 'wp_set_comment_status', $GLOBALS['_wp_filter_callbacks'] ); + $this->assertArrayNotHasKey( 'deleted_comment', $GLOBALS['_wp_filter_callbacks'] ); } - public function test_mirror_insert_promotes_recipient_from_comment_karma(): void { - global $wpdb; - $wpdb->queries = array(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_message'; - $comment->user_id = 7; // sender - $comment->comment_karma = 42; // ← recipient hack lives here - $comment->comment_post_ID = 99; - $comment->comment_approved = 0; - $comment->comment_date = '2026-05-04 12:00:00'; - - $this->adapter->mirror_insert( 100, $comment ); - - $this->assertCount( 1, $wpdb->queries ); - $sql = $wpdb->queries[0]; - $this->assertStringContainsString( 'INSERT INTO', $sql ); - $this->assertStringContainsString( 'wpdo_comment_hp_message', $sql ); - // The promoted recipient_id should appear in the bound SQL. - $this->assertStringContainsString( '42', $sql ); - } - - public function test_mirror_insert_skips_when_sender_or_recipient_zero(): void { - global $wpdb; - $wpdb->queries = array(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_message'; - $comment->user_id = 0; // bad sender - $comment->comment_karma = 42; - $comment->comment_post_ID = 99; - - $this->adapter->mirror_insert( 100, $comment ); - $this->assertSame( array(), $wpdb->queries ); - } - - public function test_doctor_check_reports_missing_table_in_stub_env(): void { - $result = $this->adapter->doctor_check(); - $this->assertFalse( $result['ok'] ); - $this->assertStringContainsString( 'wpdo_comment_hp_message', $result['message'] ); - } - - public function test_score_aggregates_to_perfect_ten(): void { - $this->assertSame( 10.0, $this->adapter->suitability_score()['aggregate'] ); + public function test_migrations_is_empty(): void { + $this->assertSame( array(), $this->adapter->migrations() ); } } diff --git a/tests/unit/RequestsAdapterTest.php b/tests/unit/RequestsAdapterTest.php index 276629e..0c850b1 100644 --- a/tests/unit/RequestsAdapterTest.php +++ b/tests/unit/RequestsAdapterTest.php @@ -48,54 +48,29 @@ class HivePressRequestsAdapterTest extends TestCase { $this->assertTrue( (bool) $expired['indexed'] ); } - public function test_on_register_custom_tables_declares_offer_shadow(): void { + public function test_registers_no_offer_shadow_table(): void { + // The offer shadow table was never created, so every mirror write failed. $registry = WPDO_Custom_Table_Registry::instance(); $this->adapter->on_register_custom_tables( $registry ); - $tables = $registry->all(); - $cfg = reset( $tables ); - $this->assertSame( 'wpdo_comment_hp_offer', $cfg['table_name'] ); - $this->assertSame( 'hp_request', $cfg['post_type_link'] ); - $this->assertArrayHasKey( 'idx_request_approved', $cfg['indexes'] ); + $this->assertSame( array(), $registry->all() ); } - public function test_mirror_offer_insert_skips_other_comment_types(): void { - global $wpdb; - $wpdb->queries = array(); + public function test_binds_no_comment_hooks(): void { + $GLOBALS['_wp_filter_callbacks'] = array(); - $comment = new stdClass(); - $comment->comment_type = 'comment'; // ← not hp_offer - $comment->comment_post_ID = 99; - $comment->user_id = 1; + $this->adapter->on_register_event_hooks(); - $this->adapter->mirror_offer_insert( 1, $comment ); - $this->assertSame( array(), $wpdb->queries ); - } - - public function test_mirror_offer_insert_writes_for_hp_offer(): void { - global $wpdb; - $wpdb->queries = array(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_offer'; - $comment->comment_post_ID = 42; - $comment->user_id = 7; - $comment->comment_approved = 0; - $comment->comment_date = '2026-05-04 12:00:00'; - - $this->adapter->mirror_offer_insert( 100, $comment ); - $this->assertCount( 1, $wpdb->queries ); - $this->assertStringContainsString( 'INSERT INTO', $wpdb->queries[0] ); - $this->assertStringContainsString( 'wpdo_comment_hp_offer', $wpdb->queries[0] ); + $this->assertArrayNotHasKey( 'wp_insert_comment', $GLOBALS['_wp_filter_callbacks'] ); + $this->assertArrayNotHasKey( 'edit_comment', $GLOBALS['_wp_filter_callbacks'] ); + $this->assertArrayNotHasKey( 'deleted_comment', $GLOBALS['_wp_filter_callbacks'] ); } public function test_score_aggregates_to_perfect_ten(): void { $this->assertSame( 10.0, $this->adapter->suitability_score()['aggregate'] ); } - public function test_migrations_lists_request_and_offer_modules(): void { - $mods = $this->adapter->migrations(); - $this->assertContains( 'hot_hp_request', $mods ); - $this->assertContains( 'comment_hp_offer', $mods ); + public function test_migrations_lists_only_request_hot_module(): void { + $this->assertSame( array( 'hot_hp_request' ), $this->adapter->migrations() ); } } diff --git a/tests/unit/WpdbMockTrait.php b/tests/unit/WpdbMockTrait.php index e5ec071..c8da95d 100644 --- a/tests/unit/WpdbMockTrait.php +++ b/tests/unit/WpdbMockTrait.php @@ -7,7 +7,7 @@ declare(strict_types=1); * Several existing wpdo tests (ZoneHotTest, SyncBridgeTest, etc.) replace the * global $wpdb with their own anonymous-class mock and never restore it. When * our adapter tests run after those, $wpdb lacks the query/get_col/delete - * methods our mirror_insert / cron-optimizer code paths call, causing + * methods our cron-optimizer code paths call, causing * cross-test pollution failures. * * Trait reinstalls a known $wpdb mock + clears the wpdo_features option so