diff --git a/inc/Cleanup/CleanupRunEvidenceStoreInterface.php b/inc/Cleanup/CleanupRunEvidenceStoreInterface.php index bd152959..23ee690a 100644 --- a/inc/Cleanup/CleanupRunEvidenceStoreInterface.php +++ b/inc/Cleanup/CleanupRunEvidenceStoreInterface.php @@ -21,7 +21,8 @@ interface CleanupRunEvidenceStoreInterface { * * @param string $run_id Stable cleanup run identifier. * @param bool $include_evidence Whether to include raw evidence records. + * @param bool $include_details Whether to include verbose diagnostic details. * @return array|\WP_Error */ - public function read( string $run_id, bool $include_evidence = false ): array|\WP_Error; + public function read( string $run_id, bool $include_evidence = false, bool $include_details = false ): array|\WP_Error; } diff --git a/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php b/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php index 0c94cfcc..5d6bf948 100644 --- a/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php +++ b/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php @@ -16,9 +16,10 @@ class DataMachineJobCleanupRunEvidenceStore implements CleanupRunEvidenceStoreIn * * @param string $run_id Stable cleanup run identifier. * @param bool $include_evidence Whether to include raw evidence records. + * @param bool $include_details Whether to include verbose diagnostic details. * @return array|\WP_Error */ - public function read( string $run_id, bool $include_evidence = false ): array|\WP_Error { + public function read( string $run_id, bool $include_evidence = false, bool $include_details = false ): array|\WP_Error { $job_id = $this->cleanup_run_job_id( $run_id ); if ( $job_id <= 0 ) { return new \WP_Error( 'invalid_cleanup_run_id', 'Cleanup run id must be a numeric job id or cleanup-run-.', array( 'status' => 400 ) ); @@ -34,21 +35,26 @@ public function read( string $run_id, bool $include_evidence = false ): array|\W $aggregate = $this->aggregate_cleanup_child_jobs( $child_jobs ); $children = $aggregate['children']; $state = $this->cleanup_run_state( (string) ( $job['status'] ?? '' ), $children ); - $output = array( - 'success' => true, - 'state' => $state, - 'run_id' => $this->cleanup_run_id( $job_id ), - 'job_id' => $job_id, - 'status' => in_array( $state, array( 'children_processing', 'partial_failed' ), true ) ? $state : ( $job['status'] ?? '' ), - 'created_at' => $job['created_at'] ?? '', - 'completed_at' => $job['completed_at'] ?? '', - 'artifact_cleanup' => $aggregate['artifact_cleanup'], - 'cleanup_items' => $aggregate['cleanup_items'], - 'children' => $children, + + $children_for_output = ( $include_evidence || $include_details ) ? $children : $this->summarize_cleanup_children( $children ); + $output = array( + 'success' => true, + 'state' => $state, + 'run_id' => $this->cleanup_run_id( $job_id ), + 'job_id' => $job_id, + 'status' => in_array( $state, array( 'children_processing', 'partial_failed' ), true ) ? $state : ( $job['status'] ?? '' ), + 'created_at' => $job['created_at'] ?? '', + 'parent_completed_at' => $job['completed_at'] ?? '', + 'artifact_cleanup' => $aggregate['artifact_cleanup'], + 'cleanup_items' => $aggregate['cleanup_items'], + 'children' => $children_for_output, ); + $output_aggregate = $aggregate; + $output_aggregate['children'] = $children_for_output; + if ( isset( $engine_data['system_task_result'] ) && is_array( $engine_data['system_task_result'] ) ) { - $engine_data['system_task_result'] = $this->with_cleanup_aggregate_report( $engine_data['system_task_result'], $aggregate ); + $engine_data['system_task_result'] = $this->with_cleanup_aggregate_report( $engine_data['system_task_result'], $output_aggregate ); } if ( $include_evidence ) { @@ -105,15 +111,18 @@ private function aggregate_cleanup_child_jobs( array $child_jobs ): array { 'failed_by_reason' => array(), ), 'children' => array( - 'batch_job_ids' => array(), - 'chunk_job_ids' => array(), - 'processing' => 0, - 'completed' => 0, - 'failed' => 0, - 'running' => 0, - 'total' => 0, - 'statuses' => array(), - 'job_ids' => array(), + 'batch_job_ids' => array(), + 'chunk_job_ids' => array(), + 'pending_job_ids' => array(), + 'processing_job_ids' => array(), + 'failed_job_ids' => array(), + 'processing' => 0, + 'completed' => 0, + 'failed' => 0, + 'running' => 0, + 'total' => 0, + 'statuses' => array(), + 'job_ids' => array(), ), ); @@ -126,6 +135,13 @@ private function aggregate_cleanup_child_jobs( array $child_jobs ): array { ++$summary['children']['total']; if ( $child_job_id > 0 ) { $summary['children']['job_ids'][] = $child_job_id; + if ( 'pending' === $status ) { + $summary['children']['pending_job_ids'][] = $child_job_id; + } elseif ( 'processing' === $status ) { + $summary['children']['processing_job_ids'][] = $child_job_id; + } elseif ( str_starts_with( $status, 'failed' ) ) { + $summary['children']['failed_job_ids'][] = $child_job_id; + } } $this->count_cleanup_child_status( $summary['children'], $status ); @@ -160,12 +176,46 @@ private function aggregate_cleanup_child_jobs( array $child_jobs ): array { $summary['cleanup_items']['freed_human'] = $this->format_bytes( $summary['cleanup_items']['bytes_reclaimed'] ); $summary['children']['batch_job_ids'] = array_values( array_unique( $summary['children']['batch_job_ids'] ) ); $summary['children']['chunk_job_ids'] = array_values( array_unique( $summary['children']['chunk_job_ids'] ) ); + $summary['children']['pending_job_ids'] = array_values( array_unique( $summary['children']['pending_job_ids'] ) ); + $summary['children']['processing_job_ids'] = array_values( array_unique( $summary['children']['processing_job_ids'] ) ); + $summary['children']['failed_job_ids'] = array_values( array_unique( $summary['children']['failed_job_ids'] ) ); $summary['children']['job_ids'] = array_values( array_unique( $summary['children']['job_ids'] ) ); $summary['children']['running'] = (int) $summary['children']['processing']; return $summary; } + /** + * Return operator-focused child status without unbounded diagnostic ID lists. + * + * @param array $children Full child aggregate. + * @return array + */ + private function summarize_cleanup_children( array $children ): array { + $limit = 10; + $batch_ids = (array) ( $children['batch_job_ids'] ?? array() ); + $chunk_ids = (array) ( $children['chunk_job_ids'] ?? array() ); + $pending = (array) ( $children['pending_job_ids'] ?? array() ); + $processing = (array) ( $children['processing_job_ids'] ?? array() ); + + return array( + 'processing' => (int) ( $children['processing'] ?? 0 ), + 'completed' => (int) ( $children['completed'] ?? 0 ), + 'failed' => (int) ( $children['failed'] ?? 0 ), + 'running' => (int) ( $children['running'] ?? 0 ), + 'total' => (int) ( $children['total'] ?? 0 ), + 'statuses' => (array) ( $children['statuses'] ?? array() ), + 'batch_total' => count( $batch_ids ), + 'chunk_total' => count( $chunk_ids ), + 'failed_job_ids' => (array) ( $children['failed_job_ids'] ?? array() ), + 'pending_job_ids' => array_slice( $pending, 0, $limit ), + 'processing_job_ids' => array_slice( $processing, 0, $limit ), + 'pending_truncated' => count( $pending ) > $limit, + 'processing_truncated' => count( $processing ) > $limit, + 'diagnostic_job_id_lists' => 'Re-run status with --verbose or use cleanup evidence for full child job IDs.', + ); + } + /** * Merge one chunk task result into aggregate cleanup item counters. * @@ -440,11 +490,11 @@ private function cleanup_run_job_id( string $run_id ): int { * @return string */ private function format_bytes( int $bytes ): string { - $bytes = max( 0, $bytes ); - $units = array( 'B', 'KiB', 'MiB', 'GiB', 'TiB' ); - $max_unit = count( $units ) - 1; - $value = (float) $bytes; - $unit = 0; + $bytes = max( 0, $bytes ); + $units = array( 'B', 'KiB', 'MiB', 'GiB', 'TiB' ); + $max_unit = count( $units ) - 1; + $value = (float) $bytes; + $unit = 0; while ( $value >= 1024 && $unit < $max_unit ) { $value /= 1024; ++$unit; diff --git a/inc/Cli/Commands/WorkspaceCommand.php b/inc/Cli/Commands/WorkspaceCommand.php index 63c1926d..034f7cc3 100644 --- a/inc/Cli/Commands/WorkspaceCommand.php +++ b/inc/Cli/Commands/WorkspaceCommand.php @@ -327,6 +327,9 @@ public function adopt_repo( array $args, array $assoc_args ): void { * without disabling the bounded scan. Useful when you want a small slice * audited with full safety information. * + * [--verbose] + * : Include full diagnostic child job ID lists in task-backed cleanup status output. + * * [--format=] * : Output format. * --- @@ -627,7 +630,7 @@ private function render_worktree_emergency_cleanup_result_from_ability( array|\W } private function render_cleanup_run_status( int $job_id, array $assoc_args, bool $evidence ): void { - $output = $this->cleanup_run_evidence_store()->read( $this->cleanup_run_id( $job_id ), $evidence ); + $output = $this->cleanup_run_evidence_store()->read( $this->cleanup_run_id( $job_id ), $evidence, ! empty( $assoc_args['verbose'] ) ); if ( $output instanceof \WP_Error ) { WP_CLI::error( $output->get_error_message() ); return; diff --git a/tests/smoke-worktree-cleanup-cli.php b/tests/smoke-worktree-cleanup-cli.php index 892bc650..9be2e738 100644 --- a/tests/smoke-worktree-cleanup-cli.php +++ b/tests/smoke-worktree-cleanup-cli.php @@ -666,7 +666,14 @@ public function execute( array $input ): array { $status_json = json_decode( WP_CLI::$logs[0] ?? '', true ); datamachine_code_cleanup_assert( 'children_processing' === ( $status_json['state'] ?? '' ), 'cleanup status stays active while child batch is processing' ); datamachine_code_cleanup_assert( 'children_processing' === ( $status_json['status'] ?? '' ), 'cleanup status does not report parent completed while children run' ); - datamachine_code_cleanup_assert( array( 125 ) === ( $status_json['children']['batch_job_ids'] ?? array() ), 'cleanup status reports child batch job ids' ); + datamachine_code_cleanup_assert( '2026-05-03 00:10:00' === ( $status_json['parent_completed_at'] ?? '' ), 'cleanup status labels parent scheduler completion separately' ); + datamachine_code_cleanup_assert( ! isset( $status_json['completed_at'] ), 'cleanup status does not expose parent completion as run completion' ); + datamachine_code_cleanup_assert( ! isset( $status_json['children']['job_ids'] ), 'cleanup status omits full child job ids by default' ); + datamachine_code_cleanup_assert( ! isset( $status_json['children']['chunk_job_ids'] ), 'cleanup status omits full child chunk job ids by default' ); + datamachine_code_cleanup_assert( 1 === (int) ( $status_json['children']['batch_total'] ?? 0 ), 'cleanup status reports child batch totals by default' ); + datamachine_code_cleanup_assert( 2 === (int) ( $status_json['children']['chunk_total'] ?? 0 ), 'cleanup status reports child chunk totals by default' ); + datamachine_code_cleanup_assert( array( 125 ) === ( $status_json['children']['processing_job_ids'] ?? array() ), 'cleanup status reports bounded processing job ids by default' ); + datamachine_code_cleanup_assert( array( 127 ) === ( $status_json['children']['failed_job_ids'] ?? array() ), 'cleanup status reports failed job ids by default' ); datamachine_code_cleanup_assert( 1 === (int) ( $status_json['children']['running'] ?? 0 ), 'cleanup status summarizes running child jobs' ); datamachine_code_cleanup_assert( ! isset( $status_json['flow_id'] ), 'cleanup status is not linked to a flow id' ); datamachine_code_cleanup_assert( 4 === (int) ( $status_json['artifact_cleanup']['planned_rows'] ?? 0 ), 'cleanup status aggregates artifact planned rows from child chunks' ); @@ -674,6 +681,15 @@ public function execute( array $input ): array { datamachine_code_cleanup_assert( 4 === (int) ( $status_json['cleanup_items']['planned_rows'] ?? 0 ), 'cleanup status aggregates planned rows from DB-backed cleanup item evidence' ); datamachine_code_cleanup_assert( 4096 === (int) ( $status_json['cleanup_items']['bytes_reclaimed'] ?? 0 ), 'cleanup status reconstructs reclaimed bytes from cleanup item evidence' ); datamachine_code_cleanup_assert( '4.0 KiB' === ( $status_json['system_task_result']['report']['freed_human'] ?? '' ), 'cleanup status replaces pending child job freed placeholder' ); + datamachine_code_cleanup_assert( ! isset( $status_json['system_task_result']['children']['job_ids'] ), 'cleanup status system task result omits full child job ids by default' ); + + WP_CLI::$logs = array(); + WP_CLI::$successes = array(); + $command->cleanup( array( 'status', 'cleanup-run-123' ), array( 'format' => 'json', 'verbose' => true ) ); + $verbose_status_json = json_decode( WP_CLI::$logs[0] ?? '', true ); + datamachine_code_cleanup_assert( array( 124, 125, 126, 127 ) === ( $verbose_status_json['children']['job_ids'] ?? array() ), 'cleanup status --verbose exposes full child job ids' ); + datamachine_code_cleanup_assert( array( 125 ) === ( $verbose_status_json['children']['batch_job_ids'] ?? array() ), 'cleanup status --verbose exposes child batch job ids' ); + datamachine_code_cleanup_assert( array( 126, 127 ) === ( $verbose_status_json['children']['chunk_job_ids'] ?? array() ), 'cleanup status --verbose exposes child chunk job ids' ); WP_CLI::$logs = array(); WP_CLI::$successes = array();