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
71 changes: 69 additions & 2 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2829,7 +2829,7 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
'apply' => $apply,
)
);
$reconcile = $abilities['reconcile_metadata']->execute($reconcile_input);
$reconcile = $this->drain_worktree_abandoned_pages($abilities['reconcile_metadata'], $reconcile_input, $apply);
if ( is_wp_error($reconcile) ) {
return $reconcile;
}
Expand All @@ -2855,7 +2855,7 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
'offset' => 0,
)
);
$step = $ability->execute($step_input);
$step = $this->drain_worktree_abandoned_pages($ability, $step_input, $apply);
if ( is_wp_error($step) ) {
return $step;
}
Expand Down Expand Up @@ -2928,6 +2928,72 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
return $result;
}

/**
* Drain paginated abandoned-cleanup classifier pages.
*
* Preview intentionally stays bounded to one page. Apply mode follows
* pagination.next_offset so stale rows beyond page zero can be reconciled or
* marked cleanup-eligible before bounded removal runs.
*
* @param object $ability Ability object with execute().
* @param array<string,mixed> $base_input Base ability input.
* @param bool $apply Whether the orchestration is applying changes.
* @return array<string,mixed>|\WP_Error
*/
private function drain_worktree_abandoned_pages( object $ability, array $base_input, bool $apply ): array|\WP_Error {
$pages = array();
$summary = array();
$pagination = array();
$offset = isset($base_input['offset']) ? max(0, (int) $base_input['offset']) : 0;
$max_pages = $apply ? 100 : 1;
$last_result = array();

for ( $page = 1; $page <= $max_pages; ++$page ) {
$input = $base_input;
if ( isset($base_input['offset']) || $page > 1 ) {
$input['offset'] = $offset;
}

$result = $ability->execute($input);
if ( is_wp_error($result) ) {
return $result;
}

$last_result = $result;
$pages[] = $this->summarize_worktree_abandoned_step($result);

foreach ( (array) ( $result['summary'] ?? array() ) as $key => $value ) {
if ( is_numeric($value) ) {
$summary[ $key ] = (int) ( $summary[ $key ] ?? 0 ) + (int) $value;
}
}

$pagination = (array) ( $result['pagination'] ?? $result['continuation'] ?? array() );
if ( ! $apply || empty($pagination) || ! empty($pagination['complete']) ) {
break;
}

$next_offset = isset($pagination['next_offset']) ? (int) $pagination['next_offset'] : null;
if ( null === $next_offset || $next_offset <= $offset ) {
break;
}

$total = isset($pagination['total']) ? (int) $pagination['total'] : null;
if ( null !== $total && $next_offset >= $total ) {
break;
}

$offset = $next_offset;
}

$last_result['summary'] = $summary;
$last_result['pagination'] = $pagination;
$last_result['pages'] = $pages;
$last_result['page_count'] = count($pages);

return $last_result;
}

/**
* Build a compact step summary for abandoned cleanup output.
*
Expand All @@ -2938,6 +3004,7 @@ private function summarize_worktree_abandoned_step( array $step ): array {
$summary = (array) ( $step['summary'] ?? array() );
return array(
'mode' => (string) ( $step['mode'] ?? '' ),
'page_count' => (int) ( $step['page_count'] ?? 1 ),
'dry_run' => ! empty($step['dry_run']),
'applied' => ! empty($step['applied']) || ! empty($step['destructive']),
'inspected' => (int) ( $summary['inspected'] ?? $summary['processed'] ?? 0 ),
Expand Down
27 changes: 22 additions & 5 deletions tests/smoke-worktree-cleanup-cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ public function execute( array $input ): array
class FakeActiveNoSignalAbility
{
public array $last_input = array();
public array $inputs = array();
private string $mode;

public function __construct( string $mode )
Expand All @@ -386,6 +387,7 @@ public function __construct( string $mode )
public function execute( array $input ): array
{
$this->last_input = $input;
$this->inputs[] = $input;
$limit = (int) ( $input['limit'] ?? 25 );
$offset = (int) ( $input['offset'] ?? 0 );
$budget = isset($input['until_budget']) && '' !== trim((string) $input['until_budget']) ? ' --until-budget=' . trim((string) $input['until_budget']) : '';
Expand Down Expand Up @@ -458,18 +460,31 @@ public function execute( array $input ): array
class FakeReconcileMetadataAbility
{
public array $last_input = array();
public array $inputs = array();

public function execute( array $input ): array
{
$this->last_input = $input;
$this->inputs[] = $input;
$limit = (int) ( $input['limit'] ?? 25 );
$offset = (int) ( $input['offset'] ?? 0 );
return array(
'success' => true,
'mode' => 'metadata_reconcile',
'dry_run' => ! empty($input['dry_run']),
'summary' => array(
'inspected' => 2,
'proposed' => 2,
'written' => empty($input['dry_run']) ? 2 : 0,
'inspected' => 1,
'proposed' => 1,
'written' => empty($input['dry_run']) ? 1 : 0,
),
'pagination' => array(
'total' => 2,
'offset' => $offset,
'limit' => $limit,
'scanned' => 1,
'partial' => $offset + $limit < 2,
'complete' => $offset + $limit >= 2,
'next_offset' => $offset + $limit,
),
);
}
Expand Down Expand Up @@ -1055,14 +1070,16 @@ public function execute( array $input ): array
echo "\n[1a] abandoned cleanup orchestrates safe DMC abilities\n";
WP_CLI::$logs = array();
WP_CLI::$successes = array();
$command->worktree(array( 'abandoned' ), array( 'apply' => true, 'force' => true, 'limit' => 5, 'passes' => 1, 'until-budget' => '30s', 'format' => 'json' ));
$command->worktree(array( 'abandoned' ), array( 'apply' => true, 'force' => true, 'limit' => 1, 'passes' => 1, 'until-budget' => '30s', 'format' => 'json' ));
$abandoned_json = json_decode(WP_CLI::$logs[0] ?? '', true);
datamachine_code_cleanup_assert(JSON_ERROR_NONE === json_last_error(), 'abandoned JSON output parses cleanly');
datamachine_code_cleanup_assert(true === ( $abandoned_json['applied'] ?? null ), 'abandoned apply mode is explicit in JSON');
datamachine_code_cleanup_assert(true === ( $abandoned_json['force'] ?? null ), 'abandoned force mode is explicit in JSON');
datamachine_code_cleanup_assert(5 === (int) ( $reconcile_metadata_ability->last_input['limit'] ?? 0 ), 'abandoned forwards limit to metadata reconciliation');
datamachine_code_cleanup_assert(1 === (int) ( $reconcile_metadata_ability->last_input['limit'] ?? 0 ), 'abandoned forwards limit to metadata reconciliation');
datamachine_code_cleanup_assert(false === ( $reconcile_metadata_ability->last_input['dry_run'] ?? null ), 'abandoned --apply applies metadata reconciliation');
datamachine_code_cleanup_assert(array( 0, 1 ) === array_map(fn( $input ) => (int) ( $input['offset'] ?? 0 ), array_slice($reconcile_metadata_ability->inputs, -2)), 'abandoned drains metadata reconciliation pages in apply mode');
datamachine_code_cleanup_assert('30s' === ( $active_finalized_ability->last_input['until_budget'] ?? '' ), 'abandoned forwards time budget to active/no-signal marking');
datamachine_code_cleanup_assert(array( 0, 1 ) === array_map(fn( $input ) => (int) ( $input['offset'] ?? 0 ), array_slice($active_finalized_ability->inputs, -2)), 'abandoned drains active/no-signal classifier pages in apply mode');
datamachine_code_cleanup_assert(true === ( $bounded_apply_ability->last_input['force'] ?? null ), 'abandoned forwards force only to bounded cleanup removal');
datamachine_code_cleanup_assert(false === ( $bounded_apply_ability->last_input['dry_run'] ?? null ), 'abandoned --apply removes eligible rows');
datamachine_code_cleanup_assert(1 === $prune_ability->calls, 'abandoned prunes stale git metadata after cleanup pass');
Expand Down
Loading