Skip to content

Commit 37fc9be

Browse files
authored
Merge pull request #583 from Extra-Chill/fix-abandoned-cleanup-drain-marked
Drain marked worktrees during abandoned cleanup
2 parents 8d40799 + 6a6314c commit 37fc9be

2 files changed

Lines changed: 85 additions & 15 deletions

File tree

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2868,6 +2868,10 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
28682868
$result['summary']['would_reconcile'] = (int) ( $reconcile['summary']['proposed'] ?? 0 );
28692869

28702870
if ( $this->worktree_abandoned_stage_incomplete($reconcile) ) {
2871+
$bounded = $this->run_worktree_abandoned_bounded_apply($abilities['bounded_apply'], $result, $apply, $force, $limit, 'reconcile');
2872+
if ( is_wp_error($bounded) ) {
2873+
return $bounded;
2874+
}
28712875
$result['evidence']['budget_exhausted'] = $this->worktree_abandoned_budget_expired($deadline);
28722876
$result['continuation'] = $this->build_worktree_abandoned_continuation('reconcile', $reconcile, $limit, $passes, $force, $until_budget);
28732877
$result['next_commands'][] = (string) $result['continuation']['next_command'];
@@ -2930,32 +2934,22 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
29302934
$result['summary']['would_mark_cleanup_eligible'] += $planned;
29312935

29322936
if ( $this->worktree_abandoned_stage_incomplete($step) ) {
2937+
$bounded = $this->run_worktree_abandoned_bounded_apply($abilities['bounded_apply'], $result, $apply, $force, $limit, $step_stage);
2938+
if ( is_wp_error($bounded) ) {
2939+
return $bounded;
2940+
}
29332941
$result['evidence']['budget_exhausted'] = $this->worktree_abandoned_budget_expired($deadline);
29342942
$result['continuation'] = $this->build_worktree_abandoned_continuation($step_stage, $step, $limit, $passes, $force, $until_budget);
29352943
$result['next_commands'][] = (string) $result['continuation']['next_command'];
29362944
break 2;
29372945
}
29382946
}
29392947

2940-
$bounded_input = array(
2941-
'dry_run' => ! $apply,
2942-
'force' => $force,
2943-
'limit' => $limit,
2944-
'source' => self::CLEANUP_CLI_SOURCE,
2945-
);
2946-
$bounded = $abilities['bounded_apply']->execute($bounded_input);
2948+
$bounded = $this->run_worktree_abandoned_bounded_apply($abilities['bounded_apply'], $result, $apply, $force, $limit, sprintf('pass_%d', $pass));
29472949
if ( is_wp_error($bounded) ) {
29482950
return $bounded;
29492951
}
29502952

2951-
$result['steps'][ sprintf('bounded_apply_pass_%d', $pass) ] = $this->summarize_worktree_abandoned_step($bounded);
2952-
2953-
$result['summary']['removed'] += (int) ( $bounded['summary']['removed'] ?? 0 );
2954-
$result['summary']['would_remove'] += (int) ( $bounded['summary']['would_remove'] ?? 0 );
2955-
$result['summary']['bytes_reclaimed'] += (int) ( $bounded['summary']['bytes_reclaimed'] ?? 0 );
2956-
2957-
$result['blocked'] = $this->merge_worktree_abandoned_blockers($result['blocked'], (array) ( $bounded['skipped'] ?? array() ));
2958-
29592953
$removed_or_would = (int) ( $bounded['summary']['removed'] ?? 0 ) + (int) ( $bounded['summary']['would_remove'] ?? 0 );
29602954
if ( 0 === $pass_marked && 0 === $removed_or_would ) {
29612955
break;
@@ -3040,6 +3034,45 @@ private function worktree_abandoned_stage_incomplete( array $step ): bool {
30403034
return $next_offset > $current;
30413035
}
30423036

3037+
/**
3038+
* Run bounded cleanup removal and merge its accounting into the abandoned result.
3039+
*
3040+
* @param object $ability Bounded cleanup ability.
3041+
* @param array<string,mixed> $result Abandoned cleanup result accumulator.
3042+
* @param bool $apply Whether apply mode is active.
3043+
* @param bool $force Whether force mode is active.
3044+
* @param int $limit Removal page size.
3045+
* @param string $step_label Step label suffix.
3046+
* @return array<string,mixed>|\WP_Error
3047+
*/
3048+
private function run_worktree_abandoned_bounded_apply( object $ability, array &$result, bool $apply, bool $force, int $limit, string $step_label ): array|\WP_Error {
3049+
$execute = array( $ability, 'execute' );
3050+
if ( ! is_callable($execute) ) {
3051+
return new \WP_Error('worktree_abandoned_ability_invalid', 'Worktree abandoned cleanup ability is not executable.', array( 'status' => 500 ));
3052+
}
3053+
3054+
$bounded = $execute(
3055+
array(
3056+
'dry_run' => ! $apply,
3057+
'force' => $force,
3058+
'limit' => $limit,
3059+
'source' => self::CLEANUP_CLI_SOURCE,
3060+
)
3061+
);
3062+
if ( is_wp_error($bounded) ) {
3063+
return $bounded;
3064+
}
3065+
3066+
$result['steps'][ sprintf('bounded_apply_%s', $step_label) ] = $this->summarize_worktree_abandoned_step($bounded);
3067+
3068+
$result['summary']['removed'] += (int) ( $bounded['summary']['removed'] ?? 0 );
3069+
$result['summary']['would_remove'] += (int) ( $bounded['summary']['would_remove'] ?? 0 );
3070+
$result['summary']['bytes_reclaimed'] += (int) ( $bounded['summary']['bytes_reclaimed'] ?? 0 );
3071+
$result['blocked'] = $this->merge_worktree_abandoned_blockers($result['blocked'], (array) ( $bounded['skipped'] ?? array() ));
3072+
3073+
return $bounded;
3074+
}
3075+
30433076
/**
30443077
* Build continuation evidence for a partially drained abandoned-cleanup stage.
30453078
*

tests/smoke-worktree-cleanup-cli.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ class FakeActiveNoSignalAbility
377377
{
378378
public array $last_input = array();
379379
public array $inputs = array();
380+
public ?int $stall_at_offset = null;
380381
private string $mode;
381382

382383
public function __construct( string $mode )
@@ -393,6 +394,28 @@ public function execute( array $input ): array
393394
$budget = isset($input['until_budget']) && '' !== trim((string) $input['until_budget']) ? ' --until-budget=' . trim((string) $input['until_budget']) : '';
394395
$dry_run = 'report' !== $this->mode && ! empty($input['dry_run']) ? ' --dry-run' : '';
395396
$next_command = sprintf('studio wp datamachine-code workspace worktree active-no-signal-%s%s --limit=%d --offset=%d%s --format=json', $this->mode, $dry_run, $limit, $offset + $limit, $budget);
397+
if ( null !== $this->stall_at_offset && $offset === $this->stall_at_offset ) {
398+
return array(
399+
'success' => true,
400+
'mode' => 'active_no_signal_' . str_replace('-', '_', $this->mode),
401+
'dry_run' => ! empty($input['dry_run']),
402+
'summary' => array(
403+
'inspected' => 0,
404+
'planned' => 0,
405+
'written' => 0,
406+
'skipped' => 0,
407+
),
408+
'pagination' => array(
409+
'total' => $offset + $limit,
410+
'offset' => $offset,
411+
'limit' => $limit,
412+
'scanned' => 0,
413+
'partial' => true,
414+
'complete' => false,
415+
'next_offset' => $offset,
416+
),
417+
);
418+
}
396419

397420
if ( 'report' === $this->mode ) {
398421
return array(
@@ -1140,6 +1163,20 @@ public function execute( array $input ): array
11401163
datamachine_code_cleanup_assert('remote-clean' === ( $abandoned_remote_clean_resume_json['stage'] ?? '' ), 'abandoned remote-clean resume reports requested stage');
11411164
datamachine_code_cleanup_assert(11 === (int) ( $active_remote_clean_ability->last_input['offset'] ?? -1 ), 'abandoned resume forwards offset to remote-clean stage');
11421165

1166+
$bounded_call_count_before_stalled_classifier = count($bounded_apply_ability->inputs);
1167+
$active_remote_clean_ability->stall_at_offset = 42;
1168+
WP_CLI::$logs = array();
1169+
WP_CLI::$successes = array();
1170+
$command->worktree(array( 'abandoned' ), array( 'apply' => true, 'force' => true, 'stage' => 'remote-clean', 'offset' => 42, 'limit' => 10, 'passes' => 1, 'until-budget' => '30s', 'format' => 'json' ));
1171+
$abandoned_remote_clean_stalled_json = json_decode(WP_CLI::$logs[0] ?? '', true);
1172+
datamachine_code_cleanup_assert(JSON_ERROR_NONE === json_last_error(), 'abandoned stalled remote-clean JSON output parses cleanly');
1173+
datamachine_code_cleanup_assert('remote-clean' === ( $abandoned_remote_clean_stalled_json['continuation']['stage'] ?? '' ), 'abandoned stalled remote-clean emits remote-clean continuation');
1174+
datamachine_code_cleanup_assert(42 === (int) ( $abandoned_remote_clean_stalled_json['continuation']['offset'] ?? -1 ), 'abandoned stalled remote-clean keeps current offset continuation');
1175+
datamachine_code_cleanup_assert(count($bounded_apply_ability->inputs) === $bounded_call_count_before_stalled_classifier + 1, 'abandoned drains bounded cleanup before returning stalled classifier continuation');
1176+
datamachine_code_cleanup_assert(isset($abandoned_remote_clean_stalled_json['steps']['bounded_apply_remote-clean']), 'abandoned stalled classifier output includes bounded cleanup step');
1177+
datamachine_code_cleanup_assert(1 === (int) ( $abandoned_remote_clean_stalled_json['summary']['removed'] ?? 0 ), 'abandoned stalled classifier summary includes bounded removals');
1178+
$active_remote_clean_ability->stall_at_offset = null;
1179+
11431180
$reconcile_metadata_ability->stall_at_offset = 90;
11441181
WP_CLI::$logs = array();
11451182
WP_CLI::$successes = array();

0 commit comments

Comments
 (0)