Skip to content

Commit 039bd50

Browse files
authored
Fix cleanup planning triage (#634)
* Fix cleanup planning triage * Fix cleanup triage lint
1 parent 4b0c630 commit 039bd50

2 files changed

Lines changed: 253 additions & 30 deletions

File tree

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ public function path( array $args, array $assoc_args ): void {
113113
* - worktree
114114
* ---
115115
*
116+
* [--summary]
117+
* : Show compact workspace triage counts instead of one row per checkout.
118+
*
116119
* [--format=<format>]
117120
* : Output format.
118121
* ---
@@ -173,6 +176,11 @@ public function list_repos( array $args, array $assoc_args ): void {
173176
return;
174177
}
175178

179+
if ( ! empty($assoc_args['summary']) ) {
180+
$this->render_workspace_list_summary($result, $assoc_args);
181+
return;
182+
}
183+
176184
$items = array_map(
177185
function ( $repo ) {
178186
$freshness = is_array($repo['primary_freshness'] ?? null) ? $repo['primary_freshness'] : null;
@@ -199,6 +207,92 @@ function ( $repo ) {
199207
);
200208
}
201209

210+
/**
211+
* Render compact workspace list counts for cleanup triage.
212+
*
213+
* @param array<string,mixed> $result Workspace list ability result.
214+
* @param array<string,mixed> $assoc_args CLI assoc args.
215+
* @return void
216+
*/
217+
private function render_workspace_list_summary( array $result, array $assoc_args ): void {
218+
$repos = (array) ( $result['repos'] ?? array() );
219+
$summary = array(
220+
'total' => count($repos),
221+
'primary' => 0,
222+
'worktree' => 0,
223+
'context' => 0,
224+
'non_git' => 0,
225+
'repos' => array(),
226+
'workspace' => (string) ( $result['path'] ?? '' ),
227+
);
228+
229+
foreach ( $repos as $row ) {
230+
if ( ! is_array($row) ) {
231+
continue;
232+
}
233+
$kind = ! empty($row['is_context']) ? 'context' : ( ! empty($row['is_worktree']) ? 'worktree' : 'primary' );
234+
++$summary[ $kind ];
235+
if ( empty($row['git']) ) {
236+
++$summary['non_git'];
237+
}
238+
$repo = (string) ( $row['repo'] ?? $row['name'] ?? 'unknown' );
239+
if ( ! isset($summary['repos'][ $repo ]) ) {
240+
$summary['repos'][ $repo ] = array(
241+
'repo' => $repo,
242+
'primary' => 0,
243+
'worktree' => 0,
244+
'context' => 0,
245+
'total' => 0,
246+
);
247+
}
248+
++$summary['repos'][ $repo ][ $kind ];
249+
++$summary['repos'][ $repo ]['total'];
250+
}
251+
252+
ksort($summary['repos']);
253+
$summary['repos'] = array_values($summary['repos']);
254+
255+
$format = (string) ( $assoc_args['format'] ?? 'table' );
256+
if ( 'json' === $format ) {
257+
WP_CLI::log( (string) wp_json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
258+
return;
259+
}
260+
261+
WP_CLI::log(sprintf('Workspace: %s', $summary['workspace']));
262+
$this->format_items(
263+
array(
264+
array(
265+
'metric' => 'total',
266+
'count' => $summary['total'],
267+
),
268+
array(
269+
'metric' => 'primary',
270+
'count' => $summary['primary'],
271+
),
272+
array(
273+
'metric' => 'worktree',
274+
'count' => $summary['worktree'],
275+
),
276+
array(
277+
'metric' => 'context',
278+
'count' => $summary['context'],
279+
),
280+
array(
281+
'metric' => 'non_git',
282+
'count' => $summary['non_git'],
283+
),
284+
),
285+
array( 'metric', 'count' ),
286+
array( 'format' => 'table' ),
287+
'metric'
288+
);
289+
290+
if ( array() !== $summary['repos'] ) {
291+
WP_CLI::log('Repos:');
292+
$this->format_items($summary['repos'], array( 'repo', 'primary', 'worktree', 'context', 'total' ), array( 'format' => 'table' ), 'repo');
293+
}
294+
}
295+
202296
/**
203297
* Clone a git repository into the workspace.
204298
*
@@ -340,6 +434,11 @@ public function adopt_repo( array $args, array $assoc_args ): void {
340434
* [--force]
341435
* : Pass force=true into the cleanup task params for modes that support it.
342436
*
437+
* [--include-artifacts]
438+
* : For `plan --mode=retention`, also include the exhaustive artifact cleanup
439+
* scan. Omitted by default so safe retention planning stays bounded on large
440+
* workspaces; use `--mode=artifacts` for an artifact-only plan.
441+
*
343442
* [--older-than=<duration>]
344443
* : Pass an age gate such as 7d or 24h into cleanup task params.
345444
*
@@ -529,8 +628,18 @@ private function run_cleanup_plan( array $assoc_args ): void {
529628
return;
530629
}
531630

532-
$input = array(
533-
'mode' => strtolower(preg_replace('/[^a-z0-9_\-]/', '', (string) ( $assoc_args['mode'] ?? 'retention' ))),
631+
$mode = strtolower(preg_replace('/[^a-z0-9_\-]/', '', (string) ( $assoc_args['mode'] ?? 'retention' )));
632+
if ( ! in_array($mode, self::CLEANUP_MODES, true) ) {
633+
WP_CLI::error(sprintf('Unknown cleanup mode: %s. Expected one of: %s.', $mode, implode(', ', self::CLEANUP_MODES)));
634+
return;
635+
}
636+
637+
$include_artifacts = 'artifacts' === $mode || ! empty($assoc_args['include-artifacts']);
638+
$include_worktrees = 'artifacts' !== $mode;
639+
$input = array(
640+
'mode' => $mode,
641+
'include_artifacts' => $include_artifacts,
642+
'include_worktrees' => $include_worktrees,
534643
'include_resolvers' => true,
535644
);
536645
if ( isset($assoc_args['older-than']) && '' !== trim( (string) $assoc_args['older-than']) ) {
@@ -539,6 +648,10 @@ private function run_cleanup_plan( array $assoc_args ): void {
539648
if ( isset($assoc_args['force']) ) {
540649
$input['force_artifact_cleanup'] = (bool) $assoc_args['force'];
541650
}
651+
if ( 'json' !== (string) ( $assoc_args['format'] ?? 'table' ) ) {
652+
$profile = $include_artifacts ? 'includes artifact scan' : 'worktree inventory only';
653+
WP_CLI::log(sprintf('Planning cleanup (%s; %s)...', $mode, $profile));
654+
}
542655

543656
$result = $ability->execute($input);
544657
if ( is_wp_error($result) ) {
@@ -912,6 +1025,10 @@ private function render_cleanup_plan_result( array $result, array $assoc_args ):
9121025
WP_CLI::log(sprintf('Rows: %d', (int) ( $summary['total_rows'] ?? 0 )));
9131026
WP_CLI::log(sprintf('Bytes: %s', $this->format_bytes($summary['total_size_bytes'] ?? 0)));
9141027
WP_CLI::log(sprintf('Apply: wp datamachine-code workspace cleanup apply %s', (string) ( $result['run_id'] ?? '' )));
1028+
$inputs = (array) ( $result['inputs'] ?? array() );
1029+
if ( empty($inputs['include_artifacts']) ) {
1030+
WP_CLI::log('Artifacts: skipped for bounded retention planning; run `wp datamachine-code workspace cleanup plan --mode=artifacts` when you want artifact rows.');
1031+
}
9151032
}
9161033

9171034
private function cleanup_run_id( int $job_id ): string {

tests/smoke-worktree-cleanup-cli.php

Lines changed: 134 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -626,8 +626,8 @@ public function execute( array $input ): array // phpcs:ignore Generic.CodeAnal
626626
}
627627
}
628628

629-
class FakeListAbility
630-
{
629+
class FakeListAbility
630+
{
631631
public function execute( array $input ): array // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
632632
{
633633
return array(
@@ -673,12 +673,57 @@ public function execute( array $input ): array // phpcs:ignore Generic.CodeAnal
673673
),
674674
),
675675
);
676-
}
677-
}
678-
679-
class FakeCleanupRunAbility
680-
{
681-
public array $last_input = array();
676+
}
677+
}
678+
679+
class FakeWorkspaceListAbility
680+
{
681+
public function execute( array $input ): array // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
682+
{
683+
return array(
684+
'success' => true,
685+
'path' => '/workspace',
686+
'repos' => array(
687+
array(
688+
'name' => 'repo',
689+
'repo' => 'repo',
690+
'branch' => 'main',
691+
'remote' => 'https://example.com/repo.git',
692+
'git' => true,
693+
'is_worktree' => false,
694+
'path' => '/workspace/repo',
695+
),
696+
array(
697+
'name' => 'repo@feature-one',
698+
'repo' => 'repo',
699+
'branch' => 'feature/one',
700+
'git' => true,
701+
'is_worktree' => true,
702+
'path' => '/workspace/repo@feature-one',
703+
),
704+
array(
705+
'name' => 'docs@cleanup',
706+
'repo' => 'docs',
707+
'branch' => 'cleanup',
708+
'git' => false,
709+
'is_worktree' => true,
710+
'path' => '/workspace/docs@cleanup',
711+
),
712+
array(
713+
'name' => 'context-docs',
714+
'repo' => 'docs',
715+
'git' => true,
716+
'is_context' => true,
717+
'path' => '/workspace/docs',
718+
),
719+
),
720+
);
721+
}
722+
}
723+
724+
class FakeCleanupRunAbility
725+
{
726+
public array $last_input = array();
682727

683728
public function execute( array $input ): array
684729
{
@@ -691,11 +736,31 @@ public function execute( array $input ): array
691736
'mode' => (string) ( $input['mode'] ?? '' ),
692737
'task_type' => 'workspace_retention_cleanup',
693738
);
694-
}
695-
}
696-
697-
class FakeCleanupStatusAbility
698-
{
739+
}
740+
}
741+
742+
class FakeCleanupPlanAbility
743+
{
744+
public array $last_input = array();
745+
746+
public function execute( array $input ): array
747+
{
748+
$this->last_input = $input;
749+
return array(
750+
'success' => true,
751+
'run_id' => 'cleanup-run-20260612000000-test',
752+
'plan_id' => 'cleanup-plan-test',
753+
'inputs' => $input,
754+
'summary' => array(
755+
'total_rows' => 2,
756+
'total_size_bytes' => 4096,
757+
),
758+
);
759+
}
760+
}
761+
762+
class FakeCleanupStatusAbility
763+
{
699764
public array $last_input = array();
700765

701766
public function execute( array $input ): array
@@ -887,17 +952,21 @@ public function execute( array $input ): array
887952
$active_merged_ability = new FakeActiveNoSignalAbility('merged-apply');
888953
$active_remote_clean_ability = new FakeActiveNoSignalAbility('remote-clean-apply');
889954
$reconcile_metadata_ability = new FakeReconcileMetadataAbility();
890-
$bounded_apply_ability = new FakeBoundedCleanupEligibleApplyAbility();
891-
$prune_ability = new FakePruneAbility();
892-
$list_ability = new FakeListAbility();
893-
$cleanup_run_ability = new FakeCleanupRunAbility();
894-
$cleanup_status_ability = new FakeCleanupStatusAbility();
955+
$bounded_apply_ability = new FakeBoundedCleanupEligibleApplyAbility();
956+
$prune_ability = new FakePruneAbility();
957+
$list_ability = new FakeListAbility();
958+
$workspace_list_ability = new FakeWorkspaceListAbility();
959+
$cleanup_run_ability = new FakeCleanupRunAbility();
960+
$cleanup_plan_ability = new FakeCleanupPlanAbility();
961+
$cleanup_status_ability = new FakeCleanupStatusAbility();
895962
$hygiene_ability = new FakeHygieneAbility();
896963
$get_jobs_ability = new FakeGetJobsAbility();
897964
$retry_job_ability = new FakeRetryJobAbility();
898965
$fail_job_ability = new FakeFailJobAbility();
899-
$GLOBALS['__abilities'] = array(
900-
'datamachine-code/workspace-cleanup-run' => $cleanup_run_ability,
966+
$GLOBALS['__abilities'] = array(
967+
'datamachine-code/workspace-list' => $workspace_list_ability,
968+
'datamachine-code/workspace-cleanup-plan' => $cleanup_plan_ability,
969+
'datamachine-code/workspace-cleanup-run' => $cleanup_run_ability,
901970
'datamachine-code/workspace-cleanup-apply' => $cleanup_status_ability,
902971
'datamachine-code/workspace-cleanup-status' => $cleanup_status_ability,
903972
'datamachine-code/workspace-cleanup-resume' => $cleanup_status_ability,
@@ -943,14 +1012,51 @@ public function execute( array $input ): array
9431012
datamachine_code_cleanup_assert(str_contains($doc_comment, 'workspace cleanup plan --mode=retention'), 'worktree examples include DB-backed cleanup plan');
9441013
datamachine_code_cleanup_assert(str_contains($doc_comment, 'workspace cleanup run --mode=retention'), 'worktree examples include task-backed cleanup run');
9451014
datamachine_code_cleanup_assert(! str_contains($doc_comment, '> cleanup-plan.json'), 'worktree examples do not normalize cleanup-plan file redirection');
946-
datamachine_code_cleanup_assert(! str_contains($doc_comment, '> artifact-plan.json'), 'worktree examples do not normalize artifact-plan file redirection');
947-
datamachine_code_cleanup_assert(! str_contains($doc_comment, '> emergency-plan.json'), 'worktree examples do not normalize emergency-plan file redirection');
948-
datamachine_code_cleanup_assert(! str_contains($doc_comment, '> reconcile-plan.json'), 'worktree examples do not normalize reconcile-plan file redirection');
949-
950-
echo "\n[0b] task-backed workspace cleanup run/status/control output\n";
951-
WP_CLI::$logs = array();
952-
WP_CLI::$successes = array();
953-
$command->cleanup(array( 'run' ), array( 'mode' => 'retention', 'format' => 'json' ));
1015+
datamachine_code_cleanup_assert(! str_contains($doc_comment, '> artifact-plan.json'), 'worktree examples do not normalize artifact-plan file redirection');
1016+
datamachine_code_cleanup_assert(! str_contains($doc_comment, '> emergency-plan.json'), 'worktree examples do not normalize emergency-plan file redirection');
1017+
datamachine_code_cleanup_assert(! str_contains($doc_comment, '> reconcile-plan.json'), 'worktree examples do not normalize reconcile-plan file redirection');
1018+
1019+
echo "\n[0a2] workspace list compact triage output\n";
1020+
WP_CLI::$logs = array();
1021+
WP_CLI::$successes = array();
1022+
$command->list_repos(array(), array( 'summary' => true ));
1023+
datamachine_code_cleanup_assert(in_array('Workspace: /workspace', WP_CLI::$logs, true), 'workspace list --summary prints workspace path');
1024+
datamachine_code_cleanup_assert(in_array('table:5:metric,count', WP_CLI::$logs, true), 'workspace list --summary prints compact metric counts');
1025+
datamachine_code_cleanup_assert(in_array('table:2:repo,primary,worktree,context,total', WP_CLI::$logs, true), 'workspace list --summary groups counts by repo');
1026+
1027+
WP_CLI::$logs = array();
1028+
WP_CLI::$successes = array();
1029+
$command->list_repos(array(), array( 'summary' => true, 'format' => 'json' ));
1030+
$list_summary_json = json_decode(WP_CLI::$logs[0] ?? '', true);
1031+
datamachine_code_cleanup_assert(4 === (int) ( $list_summary_json['total'] ?? 0 ), 'workspace list --summary JSON keeps total count');
1032+
datamachine_code_cleanup_assert(2 === (int) ( $list_summary_json['worktree'] ?? 0 ), 'workspace list --summary JSON counts worktrees');
1033+
datamachine_code_cleanup_assert(1 === (int) ( $list_summary_json['non_git'] ?? 0 ), 'workspace list --summary JSON counts non-git rows');
1034+
1035+
echo "\n[0b] task-backed workspace cleanup run/status/control output\n";
1036+
WP_CLI::$logs = array();
1037+
WP_CLI::$successes = array();
1038+
$command->cleanup(array( 'plan' ), array( 'mode' => 'retention' ));
1039+
datamachine_code_cleanup_assert('retention' === ( $cleanup_plan_ability->last_input['mode'] ?? '' ), 'cleanup plan receives retention mode');
1040+
datamachine_code_cleanup_assert(false === ( $cleanup_plan_ability->last_input['include_artifacts'] ?? true ), 'retention cleanup plan skips exhaustive artifact scan by default');
1041+
datamachine_code_cleanup_assert(true === ( $cleanup_plan_ability->last_input['include_worktrees'] ?? false ), 'retention cleanup plan keeps inventory worktree planning enabled');
1042+
datamachine_code_cleanup_assert(in_array('Planning cleanup (retention; worktree inventory only)...', WP_CLI::$logs, true), 'human cleanup plan reports bounded scan profile before planning');
1043+
datamachine_code_cleanup_assert(in_array('Artifacts: skipped for bounded retention planning; run `wp datamachine-code workspace cleanup plan --mode=artifacts` when you want artifact rows.', WP_CLI::$logs, true), 'human cleanup plan shows explicit artifact follow-up command');
1044+
1045+
WP_CLI::$logs = array();
1046+
WP_CLI::$successes = array();
1047+
$command->cleanup(array( 'plan' ), array( 'mode' => 'retention', 'include-artifacts' => true, 'format' => 'json' ));
1048+
datamachine_code_cleanup_assert(true === ( $cleanup_plan_ability->last_input['include_artifacts'] ?? false ), 'retention cleanup plan can explicitly include artifacts');
1049+
datamachine_code_cleanup_assert('{' === substr(WP_CLI::$logs[0] ?? '', 0, 1), 'json cleanup plan output is not prefixed by progress text');
1050+
1051+
WP_CLI::$logs = array();
1052+
WP_CLI::$successes = array();
1053+
$command->cleanup(array( 'plan' ), array( 'mode' => 'artifacts', 'format' => 'json' ));
1054+
datamachine_code_cleanup_assert(true === ( $cleanup_plan_ability->last_input['include_artifacts'] ?? false ), 'artifact cleanup plan includes artifact scan');
1055+
datamachine_code_cleanup_assert(false === ( $cleanup_plan_ability->last_input['include_worktrees'] ?? true ), 'artifact cleanup plan skips worktree removal rows');
1056+
1057+
WP_CLI::$logs = array();
1058+
WP_CLI::$successes = array();
1059+
$command->cleanup(array( 'run' ), array( 'mode' => 'retention', 'format' => 'json' ));
9541060
$run_json = json_decode(WP_CLI::$logs[0] ?? '', true);
9551061
datamachine_code_cleanup_assert('jobs_queued' === ( $run_json['state'] ?? '' ), 'cleanup run queues a system task');
9561062
datamachine_code_cleanup_assert('cleanup-run-123' === ( $run_json['run_id'] ?? '' ), 'cleanup run returns stable run id');

0 commit comments

Comments
 (0)