From 9031e8f68c9b184b1d4daed1dc47bdb34e6d76c6 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 7 Jun 2026 08:36:39 -0400 Subject: [PATCH] fix: compact abandoned cleanup JSON blockers --- inc/Cli/Commands/WorkspaceCommand.php | 45 +++++++++++++++++++++ tests/smoke-worktree-cleanup-cli.php | 57 +++++++++++++++++++++------ 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/inc/Cli/Commands/WorkspaceCommand.php b/inc/Cli/Commands/WorkspaceCommand.php index 014b1d0..5e081b1 100644 --- a/inc/Cli/Commands/WorkspaceCommand.php +++ b/inc/Cli/Commands/WorkspaceCommand.php @@ -3316,6 +3316,9 @@ private function merge_worktree_abandoned_blockers( array $existing, array $inco */ private function render_worktree_abandoned_result( array $result, array $assoc_args ): void { if ( 'json' === (string) ( $assoc_args['format'] ?? '' ) ) { + if ( empty($assoc_args['verbose']) ) { + $result = $this->compact_worktree_abandoned_result($result); + } $json = wp_json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); WP_CLI::log(false === $json ? '{}' : $json); return; @@ -3390,6 +3393,48 @@ private function render_worktree_abandoned_result( array $result, array $assoc_a } } + /** + * Trim large abandoned cleanup JSON output while preserving summary counts. + * + * @param array $result Abandoned cleanup result. + * @return array + */ + private function compact_worktree_abandoned_result( array $result ): array { + $blocked = (array) ( $result['blocked'] ?? array() ); + if ( count($blocked) <= 25 ) { + return $result; + } + + $examples_by_reason = array(); + foreach ( $blocked as $row ) { + if ( ! is_array($row) ) { + continue; + } + + $reason = (string) ( $row['reason_code'] ?? 'unknown' ); + if ( count($examples_by_reason[ $reason ] ?? array()) >= 3 ) { + continue; + } + + $examples_by_reason[ $reason ][] = array( + 'handle' => (string) ( $row['handle'] ?? '' ), + 'repo' => (string) ( $row['repo'] ?? '' ), + 'branch' => (string) ( $row['branch'] ?? '' ), + 'reason_code' => $reason, + 'reason' => (string) ( $row['reason'] ?? '' ), + 'unpushed' => isset($row['unpushed']) ? (int) $row['unpushed'] : null, + ); + } + + $result['blocked_examples'] = $examples_by_reason; + $result['evidence']['blocked_truncated'] = true; + $result['evidence']['blocked_full_rows'] = count($blocked); + $result['evidence']['blocked_full_hint'] = 'Re-run with --verbose --format=json to include full blocked rows.'; + $result['blocked'] = array(); + + return $result; + } + /** * Render CLI output for worktree operations. * diff --git a/tests/smoke-worktree-cleanup-cli.php b/tests/smoke-worktree-cleanup-cli.php index ebb465a..05e5950 100644 --- a/tests/smoke-worktree-cleanup-cli.php +++ b/tests/smoke-worktree-cleanup-cli.php @@ -539,23 +539,13 @@ class FakeBoundedCleanupEligibleApplyAbility { public array $last_input = array(); public array $inputs = array(); + public int $extra_skipped = 0; public function execute( array $input ): array { $this->last_input = $input; $this->inputs[] = $input; - return array( - 'success' => true, - 'mode' => 'bounded_cleanup_eligible_apply', - 'dry_run' => ! empty($input['dry_run']), - 'summary' => array( - 'inspected' => 3, - 'would_remove' => ! empty($input['dry_run']) ? 1 : 0, - 'removed' => empty($input['dry_run']) ? 1 : 0, - 'skipped' => 2, - 'bytes_reclaimed' => empty($input['dry_run']) ? 4096 : 0, - ), - 'skipped' => array( + $skipped = array( array( 'handle' => 'repo@dirty', 'repo' => 'repo', @@ -576,7 +566,30 @@ public function execute( array $input ): array 'dirty' => 0, 'unpushed' => 2, ), + ); + for ( $i = 0; $i < $this->extra_skipped; ++$i ) { + $skipped[] = array( + 'handle' => 'repo@blocked-' . $i, + 'repo' => 'repo', + 'branch' => 'blocked-' . $i, + 'path' => '/workspace/repo@blocked-' . $i, + 'reason_code' => 0 === $i % 2 ? 'active_no_signal' : 'needs_metadata_reconcile', + 'reason' => 'large blocked output fixture', + ); + } + + return array( + 'success' => true, + 'mode' => 'bounded_cleanup_eligible_apply', + 'dry_run' => ! empty($input['dry_run']), + 'summary' => array( + 'inspected' => 3, + 'would_remove' => ! empty($input['dry_run']) ? 1 : 0, + 'removed' => empty($input['dry_run']) ? 1 : 0, + 'skipped' => 2, + 'bytes_reclaimed' => empty($input['dry_run']) ? 4096 : 0, ), + 'skipped' => $skipped, ); } } @@ -1145,6 +1158,26 @@ public function execute( array $input ): array datamachine_code_cleanup_assert(2 === (int) ( $abandoned_json['summary']['blocked'] ?? 0 ), 'abandoned summary reports blocked rows'); datamachine_code_cleanup_assert(1 === (int) ( $abandoned_json['summary']['blocked_by_reason']['unpushed_commits'] ?? 0 ), 'abandoned preserves unpushed-commit blocker evidence'); + $bounded_apply_ability->extra_skipped = 30; + WP_CLI::$logs = array(); + WP_CLI::$successes = array(); + $command->worktree(array( 'abandoned' ), array( 'apply' => true, 'force' => true, 'stage' => 'bounded', 'limit' => 10, 'passes' => 1, 'format' => 'json' )); + $abandoned_compact_json = json_decode(WP_CLI::$logs[0] ?? '', true); + datamachine_code_cleanup_assert(JSON_ERROR_NONE === json_last_error(), 'abandoned compact JSON output parses cleanly'); + datamachine_code_cleanup_assert(32 === (int) ( $abandoned_compact_json['summary']['blocked'] ?? 0 ), 'abandoned compact JSON keeps blocked summary count'); + datamachine_code_cleanup_assert(array() === ( $abandoned_compact_json['blocked'] ?? null ), 'abandoned compact JSON omits full blocked rows'); + datamachine_code_cleanup_assert(true === ( $abandoned_compact_json['evidence']['blocked_truncated'] ?? false ), 'abandoned compact JSON records blocked truncation evidence'); + datamachine_code_cleanup_assert(isset($abandoned_compact_json['blocked_examples']['active_no_signal'][0]['handle']), 'abandoned compact JSON includes grouped blocked examples'); + + WP_CLI::$logs = array(); + WP_CLI::$successes = array(); + $command->worktree(array( 'abandoned' ), array( 'apply' => true, 'force' => true, 'stage' => 'bounded', 'limit' => 10, 'passes' => 1, 'verbose' => true, 'format' => 'json' )); + $abandoned_verbose_json = json_decode(WP_CLI::$logs[0] ?? '', true); + datamachine_code_cleanup_assert(JSON_ERROR_NONE === json_last_error(), 'abandoned verbose JSON output parses cleanly'); + datamachine_code_cleanup_assert(32 === count($abandoned_verbose_json['blocked'] ?? array()), 'abandoned verbose JSON keeps full blocked rows'); + datamachine_code_cleanup_assert(! isset($abandoned_verbose_json['evidence']['blocked_truncated']), 'abandoned verbose JSON does not report truncation'); + $bounded_apply_ability->extra_skipped = 0; + $reconcile_call_count = count($reconcile_metadata_ability->inputs); WP_CLI::$logs = array(); WP_CLI::$successes = array();