] * : Which garbage class to address. Default: all. * --- * default: all * options: * - all * - wxr_import * - demo_data * - transients * --- * * [--dry-run] * : Show row counts without deleting. * * [--confirm] * : Required to actually DELETE rows. Mutually exclusive with --dry-run. * * ## EXAMPLES * * wp wpdo termmeta-cleanup --dry-run * wp wpdo termmeta-cleanup --target=wxr_import --dry-run * wp wpdo termmeta-cleanup --confirm * * @param array $args Positional arguments (unused). * @param array $assoc_args Named arguments. */ public function termmeta_cleanup( $args, $assoc_args ): void { $target = (string) ( $assoc_args['target'] ?? TMDO_Termmeta_Cleaner::TARGET_ALL ); $dry_run = isset( $assoc_args['dry-run'] ); $confirm = isset( $assoc_args['confirm'] ); if ( ! in_array( $target, TMDO_Termmeta_Cleaner::VALID_TARGETS, true ) ) { WP_CLI::error( 'Invalid --target. Choose: ' . implode( ', ', TMDO_Termmeta_Cleaner::VALID_TARGETS ) ); } if ( $dry_run && $confirm ) { WP_CLI::error( '--dry-run and --confirm are mutually exclusive.' ); } if ( ! $dry_run && ! $confirm ) { WP_CLI::error( "Refusing to run without an explicit choice. Pass --dry-run to preview, or --confirm to delete.\n" . 'Example: wp wpdo termmeta-cleanup --dry-run' ); } if ( $dry_run ) { $counts = TMDO_Termmeta_Cleaner::count_garbage( $target ); self::render_term_table( $counts, 'would delete' ); WP_CLI::success( sprintf( '[dry-run] %d row(s) would be deleted. Re-run with --confirm to apply.', $counts['total'] ) ); return; } $deleted = TMDO_Termmeta_Cleaner::delete_garbage( $target ); self::render_term_table( $deleted, 'deleted' ); if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::info( 'termmeta_cleanup', array( 'target' => $target, 'wxr_import' => $deleted['wxr_import'], 'demo_data' => $deleted['demo_data'], 'transients' => $deleted['transients'], 'total' => $deleted['total'], ) ); } WP_CLI::success( sprintf( 'Deleted %d row(s) from wp_termmeta.', $deleted['total'] ) ); } /** * Render a table for termmeta cleanup output. * * @param array $counts Output from TMDO_Termmeta_Cleaner::count_garbage / delete_garbage. * @param string $verb Column header verb. */ private static function render_term_table( array $counts, string $verb ): void { $rows = array( array( 'bucket' => 'wxr_import', $verb => $counts['wxr_import'], 'rule' => '_wxr_import_*', ), array( 'bucket' => 'demo_data', $verb => $counts['demo_data'], 'rule' => '_2meet_demo_*', ), array( 'bucket' => 'transients', $verb => $counts['transients'], 'rule' => '_transient_% OR _transient_timeout_%', ), array( 'bucket' => 'TOTAL', $verb => $counts['total'], 'rule' => '', ), ); WP_CLI\Utils\format_items( 'table', $rows, array( 'bucket', $verb, 'rule' ) ); } // ── commentmeta-cleanup (v2.12.0 Phase 0) ───────────────────────────────── /** * Clean wp_commentmeta garbage rows. * * Targets: * - wxr_import — `_wxr_import_*` rows (often dominant — dev10 had 211) * - demo_data — `_2meet_demo_*` rows * - transients — `_transient_*` cache rows * - orphan_post_meta — Stray post-domain keys (`_hp_price`, `_thumbnail_id`, * etc.) mistakenly written to commentmeta * * SAFETY: by default refuses to run. Pass --dry-run to preview, or * --confirm to actually delete. * * ## OPTIONS * * [--target=] * : Which garbage class to address. Default: all. * --- * default: all * options: * - all * - wxr_import * - demo_data * - transients * - orphan_post_meta * --- * * [--dry-run] * : Show row counts without deleting. * * [--confirm] * : Required to actually DELETE rows. * * ## EXAMPLES * * wp wpdo commentmeta-cleanup --dry-run * wp wpdo commentmeta-cleanup --target=wxr_import --confirm * wp wpdo commentmeta-cleanup --confirm * * @param array $args Positional arguments (unused). * @param array $assoc_args Named arguments. */ public function commentmeta_cleanup( $args, $assoc_args ): void { $target = (string) ( $assoc_args['target'] ?? TMDO_Commentmeta_Cleaner::TARGET_ALL ); $dry_run = isset( $assoc_args['dry-run'] ); $confirm = isset( $assoc_args['confirm'] ); if ( ! in_array( $target, TMDO_Commentmeta_Cleaner::VALID_TARGETS, true ) ) { WP_CLI::error( 'Invalid --target. Choose: ' . implode( ', ', TMDO_Commentmeta_Cleaner::VALID_TARGETS ) ); } if ( $dry_run && $confirm ) { WP_CLI::error( '--dry-run and --confirm are mutually exclusive.' ); } if ( ! $dry_run && ! $confirm ) { WP_CLI::error( "Refusing to run without an explicit choice. Pass --dry-run to preview, or --confirm to delete.\n" . 'Example: wp wpdo commentmeta-cleanup --dry-run' ); } if ( $dry_run ) { $counts = TMDO_Commentmeta_Cleaner::count_garbage( $target ); self::render_comment_table( $counts, 'would delete' ); WP_CLI::success( sprintf( '[dry-run] %d row(s) would be deleted. Re-run with --confirm to apply.', $counts['total'] ) ); return; } $deleted = TMDO_Commentmeta_Cleaner::delete_garbage( $target ); self::render_comment_table( $deleted, 'deleted' ); if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::info( 'commentmeta_cleanup', array( 'target' => $target, 'wxr_import' => $deleted['wxr_import'], 'demo_data' => $deleted['demo_data'], 'transients' => $deleted['transients'], 'orphan_post_meta' => $deleted['orphan_post_meta'], 'total' => $deleted['total'], ) ); } WP_CLI::success( sprintf( 'Deleted %d row(s) from wp_commentmeta.', $deleted['total'] ) ); } /** * Render a table for commentmeta cleanup output. * * @param array $counts Output from TMDO_Commentmeta_Cleaner::count_garbage / delete_garbage. * @param string $verb Column header verb. */ private static function render_comment_table( array $counts, string $verb ): void { $rows = array( array( 'bucket' => 'wxr_import', $verb => $counts['wxr_import'], 'rule' => '_wxr_import_*', ), array( 'bucket' => 'demo_data', $verb => $counts['demo_data'], 'rule' => '_2meet_demo_*', ), array( 'bucket' => 'transients', $verb => $counts['transients'], 'rule' => '_transient_% OR _transient_timeout_%', ), array( 'bucket' => 'orphan_post_meta', $verb => $counts['orphan_post_meta'], 'rule' => '_hp_price / _hp_status / _thumbnail_id / etc.', ), array( 'bucket' => 'TOTAL', $verb => $counts['total'], 'rule' => '', ), ); WP_CLI\Utils\format_items( 'table', $rows, array( 'bucket', $verb, 'rule' ) ); } // ── term-promote-mode (v2.12.5 Phase 5) ─────────────────────────────────── /** * Promote term entity mode through the FSM safe path. * * Wraps TMDO_Mode_Manager::set('term', $new_mode). Mode_Manager enforces * one-step-at-a-time promotion (disabled → dual_write → shadow_read → * aeav_only); this CLI provides friendly UX + validation. * * ## OPTIONS * * * : Target mode. One of: disabled, dual_write, shadow_read, aeav_only. * * ## EXAMPLES * * wp wpdo term-promote-mode dual_write * wp wpdo term-promote-mode shadow_read * wp wpdo term-promote-mode aeav_only * * @param array $args Positional arguments — [new_mode]. * @param array $assoc_args Named arguments (unused). */ public function term_promote_mode( $args, $assoc_args ): void { self::promote_mode( 'term', $args, $assoc_args ); } // ── comment-promote-mode (v2.12.5 Phase 5) ──────────────────────────────── /** * Promote comment entity mode through the FSM safe path. * * ## OPTIONS * * * : Target mode. One of: disabled, dual_write, shadow_read, aeav_only. * * ## EXAMPLES * * wp wpdo comment-promote-mode dual_write * wp wpdo comment-promote-mode shadow_read * * @param array $args Positional arguments — [new_mode]. * @param array $assoc_args Named arguments (unused). */ public function comment_promote_mode( $args, $assoc_args ): void { self::promote_mode( 'comment', $args, $assoc_args ); } /** * Shared promote-mode implementation for both term and comment. * * @param string $entity_type 'term' or 'comment'. * @param array $args Positional arguments — [new_mode]. * @param array $assoc_args Named arguments (unused). * @return void */ private static function promote_mode( string $entity_type, array $args, array $assoc_args ): void { unset( $assoc_args ); if ( empty( $args[0] ) ) { WP_CLI::error( "Missing new_mode argument. Usage: wp wpdo {$entity_type}-promote-mode " ); } $new_mode = (string) $args[0]; if ( ! class_exists( 'TMDO_Mode_Manager' ) ) { WP_CLI::error( 'TMDO_Mode_Manager not loaded.' ); } $current = TMDO_Mode_Manager::get( $entity_type ); WP_CLI::log( "Current {$entity_type} mode: {$current}" ); WP_CLI::log( "Target {$entity_type} mode: {$new_mode}" ); $result = TMDO_Mode_Manager::set( $entity_type, $new_mode ); if ( true !== $result ) { $msg = $result instanceof \WP_Error ? $result->get_error_message() : 'Unknown error'; WP_CLI::error( "Mode promotion failed: {$msg}" ); } WP_CLI::success( "Promoted {$entity_type} mode {$current} → {$new_mode}" ); // Hint next step. $advice = self::next_promotion_hint( $new_mode ); if ( '' !== $advice ) { WP_CLI::log( '' ); WP_CLI::log( $advice ); } } /** * Suggest the next manual step after a mode promotion. * * @param string $new_mode Just-set mode. * @return string */ private static function next_promotion_hint( string $new_mode ): string { switch ( $new_mode ) { case 'dual_write': return 'Next: observe write paths for ≥ 24h, then promote to shadow_read with `-promote-mode shadow_read`.'; case 'shadow_read': return 'Next: run `wp wpdo term-comment-shadow-report` periodically; verify drift_total = 0 over 24h, then promote to aeav_only.'; case 'aeav_only': return 'Next: monitor wp_termmeta / wp_commentmeta row growth — should be 0 for managed keys. Cleanup historical rows with `termmeta-cleanup` / `commentmeta-cleanup`.'; default: return ''; } } // ── term-comment-diagnose (v2.12.5 Phase 5) ─────────────────────────────── /** * Read-only diagnostic report of term + comment entity bridge state. * * Shows current modes, ratio, registered group keys, flat table row counts. * * ## EXAMPLES * * wp wpdo term-comment-diagnose * * @param array $args Positional arguments (unused). * @param array $assoc_args Named arguments (unused). */ public function term_comment_diagnose( $args, $assoc_args ): void { unset( $args, $assoc_args ); global $wpdb; $term_mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'term' ) : 'unavailable'; $comment_mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'comment' ) : 'unavailable'; $terms_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->terms}" ); $termmeta_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta}" ); $comments_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comments}" ); $commentmeta_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->commentmeta}" ); WP_CLI::log( '── Term entity ──' ); WP_CLI::log( sprintf( 'Mode: %s', $term_mode ) ); WP_CLI::log( sprintf( 'wp_terms: %s', number_format_i18n( $terms_count ) ) ); WP_CLI::log( sprintf( 'wp_termmeta: %s (1:%s)', number_format_i18n( $termmeta_count ), $terms_count > 0 ? round( $termmeta_count / $terms_count, 2 ) : 'n/a' ) ); self::print_flat_row( $wpdb->prefix . 'wpdo_term_hp_taxonomy', 'wpdo_term_hp_taxonomy' ); self::print_flat_row( $wpdb->prefix . 'wpdo_term_misc', 'wpdo_term_misc' ); WP_CLI::log( '' ); WP_CLI::log( '── Comment entity ──' ); WP_CLI::log( sprintf( 'Mode: %s', $comment_mode ) ); WP_CLI::log( sprintf( 'wp_comments: %s', number_format_i18n( $comments_count ) ) ); WP_CLI::log( sprintf( 'wp_commentmeta: %s (1:%s)', number_format_i18n( $commentmeta_count ), $comments_count > 0 ? round( $commentmeta_count / $comments_count, 2 ) : 'n/a' ) ); self::print_flat_row( $wpdb->prefix . 'wpdo_comment_hp_review', 'wpdo_comment_hp_review' ); self::print_flat_row( $wpdb->prefix . 'wpdo_comment_misc', 'wpdo_comment_misc' ); WP_CLI::log( '' ); WP_CLI::log( '── Filter chain status ──' ); self::print_filter_status( 'wpdo_term_comment_garbage_filter_enabled', 'Garbage filter (Phase 1)' ); self::print_filter_status( 'wpdo_wc_term_count_filter_enabled', 'WC term count filter (Phase 3)' ); self::print_filter_status( 'wpdo_term_comment_misc_bucket_enabled', 'Misc bucket (Phase 4)' ); WP_CLI::log( '' ); WP_CLI::log( 'Recommendation: ' . self::recommend_next_action( $term_mode, $comment_mode ) ); } /** * Recommend next ops action based on current modes. * * @param string $term_mode Term entity mode. * @param string $comment_mode Comment entity mode. * @return string */ private static function recommend_next_action( string $term_mode, string $comment_mode ): string { if ( 'aeav_only' === $term_mode && 'aeav_only' === $comment_mode ) { return 'Both entities at aeav_only ✓. Run `termmeta-cleanup` / `commentmeta-cleanup` to drop historical wp_*meta rows; v3.0.0 will DROP the tables.'; } if ( 'shadow_read' === $term_mode || 'shadow_read' === $comment_mode ) { return 'Run `term-comment-shadow-report` to verify drift = 0 over 24h before promoting to aeav_only.'; } if ( 'dual_write' === $term_mode || 'dual_write' === $comment_mode ) { return 'Promote to shadow_read when ready: `term-promote-mode shadow_read` / `comment-promote-mode shadow_read`.'; } return 'Both entities at disabled. Promote to dual_write first to enable Hook Bus interception.'; } /** * Print a flat table's row count (or "(missing)" when not present). * * @param string $table Fully-qualified table name. * @param string $label Human-readable name. * @return void */ private static function print_flat_row( string $table, string $label ): void { global $wpdb; $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); if ( ! $exists ) { WP_CLI::log( sprintf( '%-22s%s', $label . ':', '(missing)' ) ); return; } $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared WP_CLI::log( sprintf( '%-22s%s rows', $label . ':', number_format_i18n( $count ) ) ); } /** * Print a filter's enabled/disabled status. * * @param string $option_key Option storing the toggle value. * @param string $label Human-readable name. * @return void */ private static function print_filter_status( string $option_key, string $label ): void { $enabled = (bool) get_option( $option_key, '1' ); WP_CLI::log( sprintf( '%-32s%s', $label . ':', $enabled ? '✓ enabled' : '✗ disabled' ) ); } // ── term-comment-shadow-report (v2.12.5 Phase 5) ────────────────────────── /** * Run the shadow verifier on demand and print a report. * * Uses a configurable sample size (default 100). Each registered term + * comment entity group is sampled; per-group results are tabulated. * * ## OPTIONS * * [--samples=] * : Number of entities to sample per group. Default: 100. * * ## EXAMPLES * * wp wpdo term-comment-shadow-report * wp wpdo term-comment-shadow-report --samples=500 * * @param array $args Positional arguments (unused). * @param array $assoc_args Named arguments. */ public function term_comment_shadow_report( $args, $assoc_args ): void { unset( $args ); if ( ! class_exists( 'TMDO_Term_Comment_Shadow_Verifier' ) ) { WP_CLI::error( 'TMDO_Term_Comment_Shadow_Verifier not loaded.' ); } $samples = isset( $assoc_args['samples'] ) ? max( 1, (int) $assoc_args['samples'] ) : 100; WP_CLI::log( "Running shadow verifier (sample_size={$samples} per group)…" ); WP_CLI::log( '' ); $results = TMDO_Term_Comment_Shadow_Verifier::run_all( $samples ); if ( empty( $results ) ) { WP_CLI::warning( 'No groups registered or registry unavailable.' ); return; } $rows = array(); foreach ( $results as $group => $r ) { if ( isset( $r['error'] ) ) { $rows[] = array( 'group' => $group, 'entity' => '?', 'sampled' => 0, 'matched' => 0, 'diffs' => 0, 'missing_flat' => 0, 'missing_meta' => 0, 'note' => $r['error'], ); continue; } $rows[] = array( 'group' => $r['group'], 'entity' => $r['entity_type'], 'sampled' => $r['sampled'], 'matched' => $r['matched'], 'diffs' => $r['diffs'], 'missing_flat' => $r['missing_flat'], 'missing_meta' => $r['missing_meta'], 'note' => 0 === $r['diffs'] && 0 === $r['missing_flat'] && 0 === $r['missing_meta'] ? '✓ clean' : '⚠ drift', ); } WP_CLI\Utils\format_items( 'table', $rows, array( 'group', 'entity', 'sampled', 'matched', 'diffs', 'missing_flat', 'missing_meta', 'note' ) ); // Total drift summary. $total_drift = 0; foreach ( $results as $r ) { if ( isset( $r['error'] ) ) { continue; } $total_drift += $r['diffs'] + $r['missing_flat'] + $r['missing_meta']; } WP_CLI::log( '' ); if ( 0 === $total_drift ) { WP_CLI::success( 'Shadow verifier: 0 drift. Safe to promote shadow_read → aeav_only.' ); } else { WP_CLI::warning( "Shadow verifier: {$total_drift} drift. Investigate before promoting." ); } } // ── term-comment-backfill (v2.12.6 Phase 6) ─────────────────────────────── /** * Backfill historical wp_termmeta / wp_commentmeta into flat tables. * * Pivot SQL one-shot per registered group. After v2.12.x activation, * write-time interception covers new updates — but legacy rows that * predate v2.12.x are still in wp_*meta only. This command reads them * and INSERTs into the corresponding flat table (ON DUPLICATE KEY UPDATE * with COALESCE to preserve any flat-only values). * * Safe to re-run: idempotent thanks to ON DUPLICATE KEY UPDATE. * * SAFETY: --dry-run / --confirm required (default refuses). * * ## OPTIONS * * [--group=] * : Restrict to one group. Default: all. * --- * default: all * options: * - all * - hp_taxonomy * - hp_review * --- * * [--dry-run] * : Show candidate counts without writing. * * [--confirm] * : Required to actually backfill. Mutually exclusive with --dry-run. * * ## EXAMPLES * * wp wpdo term-comment-backfill --dry-run * wp wpdo term-comment-backfill --confirm * wp wpdo term-comment-backfill --group=hp_taxonomy --confirm * * @param array $args Positional arguments (unused). * @param array $assoc_args Named arguments. */ public function term_comment_backfill( $args, $assoc_args ): void { unset( $args ); $group = isset( $assoc_args['group'] ) ? (string) $assoc_args['group'] : 'all'; $dry_run = isset( $assoc_args['dry-run'] ); $confirm = isset( $assoc_args['confirm'] ); if ( ! class_exists( 'TMDO_Term_Comment_Backfill' ) ) { WP_CLI::error( 'TMDO_Term_Comment_Backfill not loaded.' ); } if ( $dry_run && $confirm ) { WP_CLI::error( '--dry-run and --confirm are mutually exclusive.' ); } if ( ! $dry_run && ! $confirm ) { WP_CLI::error( "Refusing to run without explicit choice. Pass --dry-run or --confirm.\n" . 'Example: wp wpdo term-comment-backfill --dry-run' ); } $valid_groups = array( 'all', 'hp_taxonomy', 'hp_review' ); if ( ! in_array( $group, $valid_groups, true ) ) { WP_CLI::error( 'Invalid --group. Choose: ' . implode( ', ', $valid_groups ) ); } // Run backfill. if ( 'all' === $group ) { $results = TMDO_Term_Comment_Backfill::backfill_all( $dry_run ); } else { $entity_type = 'hp_review' === $group ? 'comment' : 'term'; $results = array( $group => TMDO_Term_Comment_Backfill::backfill_group( $entity_type, $group, $dry_run ) ); } // Render table. $rows = array(); $total_candidates = 0; $total_written = 0; foreach ( $results as $r ) { $rows[] = array( 'group' => $r['group'], 'entity' => $r['entity_type'], 'candidates' => $r['candidates'], 'written' => $r['written'], 'note' => $r['error'] ?? ( $r['dry_run'] ? '[dry-run]' : '✓' ), ); $total_candidates += (int) $r['candidates']; $total_written += (int) $r['written']; } WP_CLI\Utils\format_items( 'table', $rows, array( 'group', 'entity', 'candidates', 'written', 'note' ) ); if ( class_exists( 'TMDO_Logger' ) && ! $dry_run ) { TMDO_Logger::info( 'term_comment_backfill', array( 'group' => $group, 'total_candidates' => $total_candidates, 'total_written' => $total_written, ) ); } if ( $dry_run ) { WP_CLI::success( sprintf( '[dry-run] %d candidate row(s) across %d group(s). Re-run with --confirm to apply.', $total_candidates, count( $results ) ) ); } else { WP_CLI::success( sprintf( 'Backfilled %d row(s) across %d group(s). Run `wp wpdo term-comment-shadow-report` to verify drift = 0.', $total_written, count( $results ) ) ); } } // ── comment-stress-test (v2.13.2) ───────────────────────────────────────── /** * Run / inspect the Comment Stress Tester (CLI equivalent of admin tab). * * Wraps TMDO_Comment_Stress_Tester static API. Supports five subcommands: * status / start / cancel / cleanup / benchmark. * * `start` runs synchronously: it loops `run_batch()` until the state machine * reports `completed` / `failed` / `cancelled` (mirroring how the cron + REST * polling drives the same state machine). For very large targets, prefer the * admin tab so the work happens via WP-Cron and you can watch progress. * * ## OPTIONS * * * : One of: status, start, cancel, cleanup, benchmark. * * [--post-id=] * : Target post ID for `start` (comments will be attached to this post). * * [--target=] * : Number of comments to create for `start`. Default: 100. * * [--mode=] * : Write mode for `start`. Default: fast. * --- * default: fast * options: * - fast * - realistic * --- * * [--batch-size=] * : Batch size for `start`. Default: 200 (fast) / 20 (realistic recommended). * * [--yes] * : Skip cleanup confirmation prompt. * * ## EXAMPLES * * wp wpdo comment-stress-test status * wp wpdo comment-stress-test start --post-id=50 --target=100 --mode=fast * wp wpdo comment-stress-test start --post-id=50 --target=5 --mode=realistic --batch-size=5 * wp wpdo comment-stress-test cancel * wp wpdo comment-stress-test cleanup --yes * wp wpdo comment-stress-test benchmark * * @param array $args Positional arguments — [subcommand]. * @param array $assoc_args Named arguments. */ public function comment_stress_test( $args, $assoc_args ): void { if ( ! class_exists( 'TMDO_Comment_Stress_Tester' ) ) { WP_CLI::error( 'TMDO_Comment_Stress_Tester not loaded.' ); } if ( empty( $args[0] ) ) { WP_CLI::error( 'Missing . One of: status, start, cancel, cleanup, benchmark.' ); } $sub = strtolower( (string) $args[0] ); switch ( $sub ) { case 'status': self::cst_status(); return; case 'start': self::cst_start( $assoc_args ); return; case 'cancel': self::cst_cancel(); return; case 'cleanup': self::cst_cleanup( $assoc_args ); return; case 'benchmark': self::cst_benchmark(); return; default: WP_CLI::error( "Unknown subcommand '{$sub}'. One of: status, start, cancel, cleanup, benchmark." ); } } /** * Print current state machine snapshot. * * @return void */ private static function cst_status(): void { $progress = TMDO_Comment_Stress_Tester::get_progress( false ); $rows = array( array( 'field' => 'status', 'value' => (string) ( $progress['status'] ?? 'idle' ), ), array( 'field' => 'post_id', 'value' => (string) ( $progress['post_id'] ?? '—' ), ), array( 'field' => 'mode', 'value' => (string) ( $progress['mode'] ?? '—' ), ), array( 'field' => 'processed', 'value' => (string) ( $progress['processed'] ?? 0 ), ), array( 'field' => 'target', 'value' => (string) ( $progress['target'] ?? 0 ), ), array( 'field' => 'pct', 'value' => ( (string) ( $progress['pct'] ?? 0 ) ) . '%', ), array( 'field' => 'rate_per_sec', 'value' => (string) ( $progress['rate_per_sec'] ?? 0 ), ), array( 'field' => 'elapsed_sec', 'value' => (string) ( $progress['elapsed_sec'] ?? 0 ), ), array( 'field' => 'eta_sec', 'value' => (string) ( $progress['eta_sec'] ?? 0 ), ), array( 'field' => 'batches_done', 'value' => (string) ( $progress['batches_done'] ?? 0 ), ), array( 'field' => 'test_comment_count', 'value' => (string) ( $progress['test_comment_count'] ?? 0 ), ), ); WP_CLI\Utils\format_items( 'table', $rows, array( 'field', 'value' ) ); } /** * Start a stress test run synchronously (loops run_batch until terminal). * * @param array $assoc_args CLI args (post-id / target / mode / batch-size). * @return void */ private static function cst_start( array $assoc_args ): void { $post_id = (int) ( $assoc_args['post-id'] ?? 0 ); $target = (int) ( $assoc_args['target'] ?? 100 ); $mode = (string) ( $assoc_args['mode'] ?? 'fast' ); $batch = isset( $assoc_args['batch-size'] ) ? (int) $assoc_args['batch-size'] : ( 'realistic' === $mode ? 20 : TMDO_Comment_Stress_Tester::DEFAULT_BATCH_SIZE ); if ( $post_id < 1 ) { WP_CLI::error( 'Missing or invalid --post-id (must be >= 1).' ); } if ( $target < 1 || $target > TMDO_Comment_Stress_Tester::MAX_COUNT ) { WP_CLI::error( '--target must be between 1 and ' . TMDO_Comment_Stress_Tester::MAX_COUNT . '.' ); } $result = TMDO_Comment_Stress_Tester::start( $post_id, $target, $mode, $batch ); if ( empty( $result['ok'] ) ) { WP_CLI::error( 'Failed to start: ' . ( $result['error'] ?? 'unknown' ) ); } WP_CLI::log( "Started: post_id={$post_id} target={$target} mode={$mode} batch_size={$batch}" ); // Drain the state machine synchronously (CLI equivalent of cron pump). $max_loops = 5000; // Safety cap to avoid runaway loop on bad state. $loops = 0; do { TMDO_Comment_Stress_Tester::run_batch(); $progress = TMDO_Comment_Stress_Tester::get_progress( false ); $status = (string) ( $progress['status'] ?? 'idle' ); if ( in_array( $status, array( 'completed', 'failed', 'cancelled' ), true ) ) { break; } WP_CLI::log( sprintf( ' batch %d done / processed %d / %d (%s%%) — %s', (int) ( $progress['batches_done'] ?? 0 ), (int) ( $progress['processed'] ?? 0 ), (int) ( $progress['target'] ?? 0 ), (string) ( $progress['pct'] ?? 0 ), $status ) ); ++$loops; } while ( $loops < $max_loops ); $final = TMDO_Comment_Stress_Tester::get_progress( false ); WP_CLI::success( sprintf( 'Run %s — created %d / %d comment(s) in %d batch(es), %ss elapsed.', (string) ( $final['status'] ?? 'unknown' ), (int) ( $final['processed'] ?? 0 ), (int) ( $final['target'] ?? 0 ), (int) ( $final['batches_done'] ?? 0 ), (string) ( $final['elapsed_sec'] ?? 0 ) ) ); } /** * Cancel an in-flight run. * * @return void */ private static function cst_cancel(): void { $result = TMDO_Comment_Stress_Tester::cancel(); if ( empty( $result['ok'] ) ) { WP_CLI::error( 'Cancel failed: ' . ( $result['error'] ?? 'unknown' ) ); } if ( isset( $result['message'] ) && 'no_active_job' === $result['message'] ) { WP_CLI::warning( 'No active job to cancel.' ); return; } WP_CLI::success( 'Cancellation flagged. The next batch tick will mark state as cancelled.' ); } /** * Delete all stress-test comments + cascade. * * @param array $assoc_args CLI args (--yes to skip prompt). * @return void */ private static function cst_cleanup( array $assoc_args ): void { $count = TMDO_Comment_Stress_Tester::count_test_comments(); if ( 0 === $count ) { WP_CLI::success( 'No stress-test comments to clean up.' ); return; } if ( empty( $assoc_args['yes'] ) ) { WP_CLI::confirm( sprintf( 'About to DELETE %d test comment(s) (email LIKE %%@%s) and their wp_commentmeta + flat-table rows. Continue?', $count, TMDO_Comment_Stress_Tester::TEST_EMAIL_DOMAIN ) ); } $result = TMDO_Comment_Stress_Tester::cleanup(); delete_option( TMDO_Comment_Stress_Tester::OPT_STATE ); delete_transient( TMDO_Comment_Stress_Tester::CANCEL_FLAG ); WP_CLI::success( sprintf( 'Deleted %d comment(s), %d commentmeta row(s), %d flat row(s).', (int) ( $result['deleted_comments'] ?? 0 ), (int) ( $result['deleted_meta'] ?? 0 ), (int) ( $result['deleted_flat'] ?? 0 ) ) ); } /** * Run benchmark report (write metrics + DB sizes + query perf). * * @return void */ private static function cst_benchmark(): void { $state = TMDO_Comment_Stress_Tester::get_state(); $report = TMDO_Comment_Stress_Tester::run_benchmark( $state ?: null ); $query = $report['query'] ?? array(); $db_sizes = $report['db_sizes'] ?? array(); WP_CLI::log( '▍ Query performance' ); $rows = array(); foreach ( $query as $key => $v ) { $rows[] = array( 'probe' => (string) $key, 'duration_ms' => (string) ( $v['duration_ms'] ?? '—' ), ); } WP_CLI\Utils\format_items( 'table', $rows, array( 'probe', 'duration_ms' ) ); WP_CLI::log( '▍ DB sizes (comment-related)' ); $db_rows = array(); foreach ( $db_sizes as $r ) { $db_rows[] = array( 'table' => (string) ( $r['table'] ?? '' ), 'rows' => (string) ( $r['rows'] ?? 0 ), 'data_mb' => (string) ( $r['data_mb'] ?? '—' ), 'index_mb' => (string) ( $r['index_mb'] ?? '—' ), 'total_mb' => (string) ( $r['total_mb'] ?? '—' ), ); } WP_CLI\Utils\format_items( 'table', $db_rows, array( 'table', 'rows', 'data_mb', 'index_mb', 'total_mb' ) ); WP_CLI::success( 'Benchmark complete.' ); } } // ── Register subcommands ────────────────────────────────────────────────────── // (Originally class closed here in v2.12.0; v2.12.5+ adds new methods inside.) WP_CLI::add_command( 'wpdo termmeta-cleanup', array( 'TMDO_CLI_Term_Comment', 'termmeta_cleanup' ) ); WP_CLI::add_command( 'tmdo termmeta-cleanup', array( 'TMDO_CLI_Term_Comment', 'termmeta_cleanup' ) ); WP_CLI::add_command( 'wpdo commentmeta-cleanup', array( 'TMDO_CLI_Term_Comment', 'commentmeta_cleanup' ) ); WP_CLI::add_command( 'tmdo commentmeta-cleanup', array( 'TMDO_CLI_Term_Comment', 'commentmeta_cleanup' ) ); WP_CLI::add_command( 'wpdo term-promote-mode', array( 'TMDO_CLI_Term_Comment', 'term_promote_mode' ) ); WP_CLI::add_command( 'tmdo term-promote-mode', array( 'TMDO_CLI_Term_Comment', 'term_promote_mode' ) ); WP_CLI::add_command( 'wpdo comment-promote-mode', array( 'TMDO_CLI_Term_Comment', 'comment_promote_mode' ) ); WP_CLI::add_command( 'tmdo comment-promote-mode', array( 'TMDO_CLI_Term_Comment', 'comment_promote_mode' ) ); WP_CLI::add_command( 'wpdo term-comment-diagnose', array( 'TMDO_CLI_Term_Comment', 'term_comment_diagnose' ) ); WP_CLI::add_command( 'tmdo term-comment-diagnose', array( 'TMDO_CLI_Term_Comment', 'term_comment_diagnose' ) ); WP_CLI::add_command( 'wpdo term-comment-shadow-report', array( 'TMDO_CLI_Term_Comment', 'term_comment_shadow_report' ) ); WP_CLI::add_command( 'tmdo term-comment-shadow-report', array( 'TMDO_CLI_Term_Comment', 'term_comment_shadow_report' ) ); WP_CLI::add_command( 'wpdo term-comment-backfill', array( 'TMDO_CLI_Term_Comment', 'term_comment_backfill' ) ); WP_CLI::add_command( 'tmdo term-comment-backfill', array( 'TMDO_CLI_Term_Comment', 'term_comment_backfill' ) ); WP_CLI::add_command( 'wpdo comment-stress-test', array( 'TMDO_CLI_Term_Comment', 'comment_stress_test' ) ); WP_CLI::add_command( 'tmdo comment-stress-test', array( 'TMDO_CLI_Term_Comment', 'comment_stress_test' ) );