Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 48 additions & 15 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2868,6 +2868,10 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
$result['summary']['would_reconcile'] = (int) ( $reconcile['summary']['proposed'] ?? 0 );

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

if ( $this->worktree_abandoned_stage_incomplete($step) ) {
$bounded = $this->run_worktree_abandoned_bounded_apply($abilities['bounded_apply'], $result, $apply, $force, $limit, $step_stage);
if ( is_wp_error($bounded) ) {
return $bounded;
}
$result['evidence']['budget_exhausted'] = $this->worktree_abandoned_budget_expired($deadline);
$result['continuation'] = $this->build_worktree_abandoned_continuation($step_stage, $step, $limit, $passes, $force, $until_budget);
$result['next_commands'][] = (string) $result['continuation']['next_command'];
break 2;
}
}

$bounded_input = array(
'dry_run' => ! $apply,
'force' => $force,
'limit' => $limit,
'source' => self::CLEANUP_CLI_SOURCE,
);
$bounded = $abilities['bounded_apply']->execute($bounded_input);
$bounded = $this->run_worktree_abandoned_bounded_apply($abilities['bounded_apply'], $result, $apply, $force, $limit, sprintf('pass_%d', $pass));
if ( is_wp_error($bounded) ) {
return $bounded;
}

$result['steps'][ sprintf('bounded_apply_pass_%d', $pass) ] = $this->summarize_worktree_abandoned_step($bounded);

$result['summary']['removed'] += (int) ( $bounded['summary']['removed'] ?? 0 );
$result['summary']['would_remove'] += (int) ( $bounded['summary']['would_remove'] ?? 0 );
$result['summary']['bytes_reclaimed'] += (int) ( $bounded['summary']['bytes_reclaimed'] ?? 0 );

$result['blocked'] = $this->merge_worktree_abandoned_blockers($result['blocked'], (array) ( $bounded['skipped'] ?? array() ));

$removed_or_would = (int) ( $bounded['summary']['removed'] ?? 0 ) + (int) ( $bounded['summary']['would_remove'] ?? 0 );
if ( 0 === $pass_marked && 0 === $removed_or_would ) {
break;
Expand Down Expand Up @@ -3040,6 +3034,45 @@ private function worktree_abandoned_stage_incomplete( array $step ): bool {
return $next_offset > $current;
}

/**
* Run bounded cleanup removal and merge its accounting into the abandoned result.
*
* @param object $ability Bounded cleanup ability.
* @param array<string,mixed> $result Abandoned cleanup result accumulator.
* @param bool $apply Whether apply mode is active.
* @param bool $force Whether force mode is active.
* @param int $limit Removal page size.
* @param string $step_label Step label suffix.
* @return array<string,mixed>|\WP_Error
*/
private function run_worktree_abandoned_bounded_apply( object $ability, array &$result, bool $apply, bool $force, int $limit, string $step_label ): array|\WP_Error {
$execute = array( $ability, 'execute' );
if ( ! is_callable($execute) ) {
return new \WP_Error('worktree_abandoned_ability_invalid', 'Worktree abandoned cleanup ability is not executable.', array( 'status' => 500 ));
}

$bounded = $execute(
array(
'dry_run' => ! $apply,
'force' => $force,
'limit' => $limit,
'source' => self::CLEANUP_CLI_SOURCE,
)
);
if ( is_wp_error($bounded) ) {
return $bounded;
}

$result['steps'][ sprintf('bounded_apply_%s', $step_label) ] = $this->summarize_worktree_abandoned_step($bounded);

$result['summary']['removed'] += (int) ( $bounded['summary']['removed'] ?? 0 );
$result['summary']['would_remove'] += (int) ( $bounded['summary']['would_remove'] ?? 0 );
$result['summary']['bytes_reclaimed'] += (int) ( $bounded['summary']['bytes_reclaimed'] ?? 0 );
$result['blocked'] = $this->merge_worktree_abandoned_blockers($result['blocked'], (array) ( $bounded['skipped'] ?? array() ));

return $bounded;
}

/**
* Build continuation evidence for a partially drained abandoned-cleanup stage.
*
Expand Down
37 changes: 37 additions & 0 deletions tests/smoke-worktree-cleanup-cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ class FakeActiveNoSignalAbility
{
public array $last_input = array();
public array $inputs = array();
public ?int $stall_at_offset = null;
private string $mode;

public function __construct( string $mode )
Expand All @@ -393,6 +394,28 @@ public function execute( array $input ): array
$budget = isset($input['until_budget']) && '' !== trim((string) $input['until_budget']) ? ' --until-budget=' . trim((string) $input['until_budget']) : '';
$dry_run = 'report' !== $this->mode && ! empty($input['dry_run']) ? ' --dry-run' : '';
$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);
if ( null !== $this->stall_at_offset && $offset === $this->stall_at_offset ) {
return array(
'success' => true,
'mode' => 'active_no_signal_' . str_replace('-', '_', $this->mode),
'dry_run' => ! empty($input['dry_run']),
'summary' => array(
'inspected' => 0,
'planned' => 0,
'written' => 0,
'skipped' => 0,
),
'pagination' => array(
'total' => $offset + $limit,
'offset' => $offset,
'limit' => $limit,
'scanned' => 0,
'partial' => true,
'complete' => false,
'next_offset' => $offset,
),
);
}

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

$bounded_call_count_before_stalled_classifier = count($bounded_apply_ability->inputs);
$active_remote_clean_ability->stall_at_offset = 42;
WP_CLI::$logs = array();
WP_CLI::$successes = array();
$command->worktree(array( 'abandoned' ), array( 'apply' => true, 'force' => true, 'stage' => 'remote-clean', 'offset' => 42, 'limit' => 10, 'passes' => 1, 'until-budget' => '30s', 'format' => 'json' ));
$abandoned_remote_clean_stalled_json = json_decode(WP_CLI::$logs[0] ?? '', true);
datamachine_code_cleanup_assert(JSON_ERROR_NONE === json_last_error(), 'abandoned stalled remote-clean JSON output parses cleanly');
datamachine_code_cleanup_assert('remote-clean' === ( $abandoned_remote_clean_stalled_json['continuation']['stage'] ?? '' ), 'abandoned stalled remote-clean emits remote-clean continuation');
datamachine_code_cleanup_assert(42 === (int) ( $abandoned_remote_clean_stalled_json['continuation']['offset'] ?? -1 ), 'abandoned stalled remote-clean keeps current offset continuation');
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');
datamachine_code_cleanup_assert(isset($abandoned_remote_clean_stalled_json['steps']['bounded_apply_remote-clean']), 'abandoned stalled classifier output includes bounded cleanup step');
datamachine_code_cleanup_assert(1 === (int) ( $abandoned_remote_clean_stalled_json['summary']['removed'] ?? 0 ), 'abandoned stalled classifier summary includes bounded removals');
$active_remote_clean_ability->stall_at_offset = null;

$reconcile_metadata_ability->stall_at_offset = 90;
WP_CLI::$logs = array();
WP_CLI::$successes = array();
Expand Down
Loading