From 62f9b46b0b7889af5ee8ad53a893b5c354a0a297 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Tue, 19 May 2026 10:54:29 -0400 Subject: [PATCH] fix: aggregate cleanup chunk evidence from job data --- .../DataMachineJobCleanupRunEvidenceStore.php | 42 ++++++++++--- tests/smoke-worktree-cleanup-cli.php | 63 +++++++++---------- 2 files changed, 63 insertions(+), 42 deletions(-) diff --git a/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php b/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php index 5d6bf948..85528c49 100644 --- a/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php +++ b/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php @@ -30,11 +30,12 @@ public function read( string $run_id, bool $include_evidence = false, bool $incl return new \WP_Error( 'cleanup_run_not_found', sprintf( 'Cleanup run not found: %s', $this->cleanup_run_id( $job_id ) ), array( 'status' => 404 ) ); } - $engine_data = $this->normalize_engine_data( $job['engine_data'] ?? array() ); - $child_jobs = $this->get_cleanup_run_descendant_jobs( $job_id ); - $aggregate = $this->aggregate_cleanup_child_jobs( $child_jobs ); - $children = $aggregate['children']; - $state = $this->cleanup_run_state( (string) ( $job['status'] ?? '' ), $children ); + $engine_data = $this->normalize_engine_data( $job['engine_data'] ?? array() ); + $parent_result = $this->extract_system_task_result( $engine_data ); + $child_jobs = $this->get_cleanup_run_descendant_jobs( $job_id ); + $aggregate = $this->aggregate_cleanup_child_jobs( $child_jobs ); + $children = $aggregate['children']; + $state = $this->cleanup_run_state( (string) ( $job['status'] ?? '' ), $children ); $children_for_output = ( $include_evidence || $include_details ) ? $children : $this->summarize_cleanup_children( $children ); $output = array( @@ -53,8 +54,8 @@ public function read( string $run_id, bool $include_evidence = false, bool $incl $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'], $output_aggregate ); + if ( array() !== $parent_result ) { + $engine_data['system_task_result'] = $this->with_cleanup_aggregate_report( $parent_result, $output_aggregate ); } if ( $include_evidence ) { @@ -130,7 +131,7 @@ private function aggregate_cleanup_child_jobs( array $child_jobs ): array { $child_job_id = (int) ( $child['job_id'] ?? 0 ); $status = (string) ( $child['status'] ?? '' ); $engine_data = $this->normalize_engine_data( $child['engine_data'] ?? array() ); - $result = is_array( $engine_data['system_task_result'] ?? null ) ? $engine_data['system_task_result'] : array(); + $result = $this->extract_system_task_result( $engine_data ); ++$summary['children']['total']; if ( $child_job_id > 0 ) { @@ -161,13 +162,16 @@ private function aggregate_cleanup_child_jobs( array $child_jobs ): array { if ( 'worktree_cleanup_chunk' !== (string) ( $engine_data['task_type'] ?? '' ) && ! isset( $result['chunk_type'] ) ) { continue; } + if ( array() === $result ) { + continue; + } if ( $child_job_id > 0 ) { $summary['children']['chunk_job_ids'][] = $child_job_id; } $this->merge_cleanup_item_result( $summary['cleanup_items'], $result ); - if ( 'artifacts' === (string) ( $result['chunk_type'] ?? '' ) ) { + if ( in_array( (string) ( $result['chunk_type'] ?? '' ), array( 'artifacts', 'artifact_discovery' ), true ) ) { $this->merge_cleanup_item_result( $summary['artifact_cleanup'], $result ); } } @@ -373,6 +377,26 @@ private function normalize_engine_data( mixed $engine_data ): array { return array(); } + /** + * Extract a task result from either nested packets or direct task engine data. + * + * @param array $engine_data Job engine data. + * @return array + */ + private function extract_system_task_result( array $engine_data ): array { + if ( isset( $engine_data['system_task_result'] ) && is_array( $engine_data['system_task_result'] ) ) { + return $engine_data['system_task_result']; + } + + foreach ( array( 'chunk_type', 'planned_count', 'applied_count', 'skipped_count', 'failed_count', 'bytes_reclaimed', 'report', 'job_backed' ) as $key ) { + if ( array_key_exists( $key, $engine_data ) ) { + return $engine_data; + } + } + + return array(); + } + /** * Count a child job status into stable status buckets. * diff --git a/tests/smoke-worktree-cleanup-cli.php b/tests/smoke-worktree-cleanup-cli.php index 9be2e738..dcb6fc1a 100644 --- a/tests/smoke-worktree-cleanup-cli.php +++ b/tests/smoke-worktree-cleanup-cli.php @@ -472,20 +472,18 @@ public function execute( array $input ): array { 'source' => 'system', 'status' => 'completed', 'engine_data' => array( - 'task_type' => 'worktree_cleanup_chunk', - 'system_task_result' => array( - 'success' => true, - 'chunk_type' => 'artifacts', - 'planned_count' => 3, - 'applied_count' => 2, - 'skipped_count' => 1, - 'failed_count' => 0, - 'bytes_reclaimed' => 4096, - 'skipped' => array( - array( 'handle' => 'repo@dirty', 'reason_code' => 'dirty_worktree' ), - ), - 'failed' => array(), + 'task_type' => 'worktree_cleanup_chunk', + 'success' => true, + 'chunk_type' => 'artifact_discovery', + 'planned_count' => 3, + 'applied_count' => 2, + 'skipped_count' => 1, + 'failed_count' => 0, + 'bytes_reclaimed' => 4096, + 'skipped' => array( + array( 'handle' => 'repo@dirty', 'reason_code' => 'dirty_worktree' ), ), + 'failed' => array(), ), ), array( @@ -494,19 +492,17 @@ public function execute( array $input ): array { 'source' => 'system', 'status' => 'failed - apply_failed', 'engine_data' => array( - 'task_type' => 'worktree_cleanup_chunk', - 'system_task_result' => array( - 'success' => false, - 'chunk_type' => 'artifacts', - 'planned_count' => 1, - 'applied_count' => 0, - 'skipped_count' => 0, - 'failed_count' => 1, - 'bytes_reclaimed' => 0, - 'skipped' => array(), - 'failed' => array( - array( 'handle' => 'repo@failed', 'reason_code' => 'apply_failed' ), - ), + 'task_type' => 'worktree_cleanup_chunk', + 'success' => false, + 'chunk_type' => 'artifacts', + 'planned_count' => 1, + 'applied_count' => 0, + 'skipped_count' => 0, + 'failed_count' => 1, + 'bytes_reclaimed' => 0, + 'skipped' => array(), + 'failed' => array( + array( 'handle' => 'repo@failed', 'reason_code' => 'apply_failed' ), ), ), ), @@ -542,13 +538,11 @@ public function execute( array $input ): array { 'mode' => 'retention', 'source' => 'workspace_cleanup_cli', ), - 'system_task_result' => array( - 'success' => true, - 'job_backed' => true, - 'report' => array( - 'bytes_reclaimed' => 0, - 'freed_human' => 'pending child jobs', - ), + 'success' => true, + 'job_backed' => true, + 'report' => array( + 'bytes_reclaimed' => 0, + 'freed_human' => 'pending child jobs', ), ), ), @@ -680,6 +674,9 @@ public function execute( array $input ): array { datamachine_code_cleanup_assert( 4096 === (int) ( $status_json['artifact_cleanup']['bytes_reclaimed'] ?? 0 ), 'cleanup status aggregates artifact bytes from child chunks' ); 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( 3 === (int) ( $status_json['cleanup_items']['by_type']['artifact_discovery']['planned_rows'] ?? 0 ), 'cleanup status preserves artifact discovery chunk type aggregation' ); + datamachine_code_cleanup_assert( 1 === (int) ( $status_json['cleanup_items']['by_type']['artifacts']['planned_rows'] ?? 0 ), 'cleanup status preserves artifact apply chunk type aggregation' ); + datamachine_code_cleanup_assert( ! isset( $status_json['cleanup_items']['by_type']['unknown'] ), 'cleanup status does not aggregate completed chunks under unknown type' ); 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' );