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
23 changes: 12 additions & 11 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,15 @@ public function list_repos( array $args, array $assoc_args ): void {
function ( $repo ) {
$freshness = is_array($repo['primary_freshness'] ?? null) ? $repo['primary_freshness'] : null;
return array(
'name' => $repo['name'],
'kind' => ! empty($repo['is_worktree']) ? 'worktree' : 'primary',
'repo' => $repo['repo'] ?? $repo['name'],
'branch' => $repo['branch'] ?? '-',
'freshness' => is_array($freshness) ? (string) ( $freshness['status'] ?? '-' ) : '-',
'behind' => is_array($freshness) && null !== ( $freshness['behind'] ?? null ) ? (string) $freshness['behind'] : '-',
'remote' => $repo['remote'] ?? '-',
'git' => $repo['git'] ? 'yes' : 'no',
'path' => $repo['path'],
'name' => $repo['name'],
'kind' => ! empty($repo['is_worktree']) ? 'worktree' : 'primary',
'repo' => $repo['repo'] ?? $repo['name'],
'branch' => $repo['branch'] ?? '-',
'freshness' => is_array($freshness) ? (string) ( $freshness['status'] ?? '-' ) : '-',
'behind' => is_array($freshness) && null !== ( $freshness['behind'] ?? null ) ? (string) $freshness['behind'] : '-',
'remote' => $repo['remote'] ?? '-',
'git' => $repo['git'] ? 'yes' : 'no',
'path' => $repo['path'],
);
},
$result['repos']
Expand Down Expand Up @@ -230,7 +230,7 @@ public function clone_repo( array $args, array $assoc_args ): void {
array(
'full' => isset($assoc_args['full']),
'allow_duplicate_remote' => isset($assoc_args['allow-duplicate-remote']),
'progress_callback' => static function ( array $event ): void {
'progress_callback' => static function ( array $event ): void {
$elapsed = number_format( (float) ( $event['elapsed'] ?? 0 ), 1);
WP_CLI::log(sprintf('[clone %ss] %s', $elapsed, (string) ( $event['message'] ?? '' )));
},
Expand Down Expand Up @@ -5106,7 +5106,8 @@ private function render_worktree_artifact_cleanup_result( array $result, array $
}

if ( $dry_run ) {
WP_CLI::success(sprintf('%d artifact(s) would be removed. Prefer `workspace cleanup run --mode=artifacts`; --apply-plan remains a low-level escape hatch until DB-backed cleanup runs land.', (int) ( $summary['would_remove_artifacts'] ?? 0 )));
$apply_command = (string) ( $result['apply_command'] ?? $summary['apply_command'] ?? 'studio wp datamachine-code workspace cleanup run --mode=artifacts --format=json' );
WP_CLI::success(sprintf('%d artifact(s) would be removed. Apply this page with `%s`; --apply-plan remains a low-level escape hatch.', (int) ( $summary['would_remove_artifacts'] ?? 0 ), $apply_command));
return;
}
WP_CLI::success(sprintf('Removed %d artifact(s); %d worktree(s) skipped.', (int) ( $summary['removed_artifacts'] ?? 0 ), count($skipped)));
Expand Down
40 changes: 33 additions & 7 deletions inc/Workspace/WorkspaceArtifactCleanup.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E
if ( $exhaustive ) {
$limit = 0;
}
$apply_command = $this->build_artifact_cleanup_apply_command($limit, $offset, $exhaustive);
// Apply paths default to safety probing (small subset). Dry-run defaults
// to skipping the per-worktree git probes unless explicitly requested or
// the caller asked for exhaustive mode.
Expand All @@ -66,7 +67,7 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E
}

if ( ! $dry_run && null === $apply_plan ) {
return new \WP_Error('artifact_cleanup_plan_required', 'Artifact cleanup applies through reviewed JSON only on this low-level command. Prefer workspace cleanup run --mode=artifacts for daily cleanup; use --dry-run first and --apply-plan=<file> only as an escape hatch.', array( 'status' => 400 ));
return new \WP_Error('artifact_cleanup_plan_required', sprintf('Artifact cleanup applies through the high-level cleanup runner for daily cleanup. Run `%s` to apply the same bounded page, or use --dry-run first and --apply-plan=<file> only as a low-level escape hatch.', $apply_command), array( 'status' => 400 ));
}

$only_handles = null;
Expand Down Expand Up @@ -116,12 +117,13 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E

if ( $dry_run ) {
$response = array(
'success' => true,
'dry_run' => true,
'candidates' => $candidates,
'removed' => array(),
'skipped' => $skipped,
'summary' => $summary,
'success' => true,
'dry_run' => true,
'apply_command' => $apply_command,
'candidates' => $candidates,
'removed' => array(),
'skipped' => $skipped,
'summary' => array( 'apply_command' => $apply_command ) + $summary,
);
if ( null !== $pagination ) {
$response['pagination'] = $pagination;
Expand Down Expand Up @@ -178,6 +180,30 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E
return $response;
}

/**
* Build the high-level command that applies the same artifact cleanup page.
*
* @param int $limit Effective bounded scan limit.
* @param int $offset Bounded inventory offset.
* @param bool $exhaustive Whether the dry-run used exhaustive mode.
* @return string
*/
private function build_artifact_cleanup_apply_command( int $limit, int $offset, bool $exhaustive ): string {
$parts = array(
'studio wp datamachine-code workspace cleanup run',
'--mode=artifacts',
);
if ( $exhaustive ) {
$parts[] = '--exhaustive';
} else {
$parts[] = sprintf('--limit=%d', $limit);
$parts[] = sprintf('--offset=%d', $offset);
}
$parts[] = '--format=json';

return implode(' ', $parts);
}

/**
* Build current artifact cleanup candidates and safety skips.
*
Expand Down
5 changes: 5 additions & 0 deletions tests/smoke-worktree-cleanup-artifacts.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ function apply_filters( string $hook_name, $value ) // phpcs:ignore Generic.Cod
$assert('bounded_inventory', $plan['pagination']['mode'] ?? '', 'bounded dry-run advertises bounded_inventory mode');
$assert(false, (bool) ( $plan['pagination']['safety_probes'] ?? true ), 'bounded dry-run reports safety_probes=false');
$assert(true, (bool) ( $plan['pagination']['complete'] ?? false ), 'bounded dry-run completes when total <= limit');
$assert('studio wp datamachine-code workspace cleanup run --mode=artifacts --limit=100 --offset=0 --format=json', $plan['apply_command'] ?? '', 'bounded dry-run exposes matching high-level apply command');
$assert($plan['apply_command'] ?? '', $plan['summary']['apply_command'] ?? '', 'bounded dry-run summary repeats apply command');

$bounded_skip_reasons = array_column($plan['skipped'] ?? array(), 'reason_code', 'handle');
$assert('active_symlink_target', $bounded_skip_reasons['demo@active'] ?? '', 'active plugin symlink target is protected even in bounded mode');
Expand All @@ -230,6 +232,7 @@ function apply_filters( string $hook_name, $value ) // phpcs:ignore Generic.Cod
$assert(1, count($exhaustive_plan['candidates'] ?? array()), 'exhaustive dry-run skips dirty/unpushed worktrees');
$assert('demo@clean', $exhaustive_plan['candidates'][0]['handle'] ?? '', 'exhaustive clean worktree is candidate');
$assert('target', $exhaustive_plan['candidates'][0]['artifacts'][0]['path'] ?? '', 'exhaustive candidate artifact path comes from profile');
$assert('studio wp datamachine-code workspace cleanup run --mode=artifacts --exhaustive --format=json', $exhaustive_plan['apply_command'] ?? '', 'exhaustive dry-run exposes matching high-level apply command');

$skip_reasons = array_column($exhaustive_plan['skipped'] ?? array(), 'reason_code', 'handle');
$assert('dirty_worktree', $skip_reasons['demo@dirty'] ?? '', 'exhaustive dirty worktree is protected');
Expand All @@ -252,6 +255,7 @@ function apply_filters( string $hook_name, $value ) // phpcs:ignore Generic.Cod
$page_one = $workspace->worktree_cleanup_artifacts(array( 'dry_run' => true, 'limit' => 1, 'offset' => 0 ));
$assert(false, is_wp_error($page_one), 'page-1 dry-run succeeds');
$assert(1, (int) ( $page_one['pagination']['scanned'] ?? 0 ), 'page-1 scanned exactly one worktree');
$assert('studio wp datamachine-code workspace cleanup run --mode=artifacts --limit=1 --offset=0 --format=json', $page_one['apply_command'] ?? '', 'page-1 dry-run apply command preserves page scope');
$assert(true, (bool) ( $page_one['pagination']['partial'] ?? false ), 'page-1 reports partial=true');
$assert(false, (bool) ( $page_one['pagination']['complete'] ?? true ), 'page-1 reports complete=false');
$assert(1, (int) ( $page_one['pagination']['next_offset'] ?? 0 ), 'page-1 next_offset advances by limit');
Expand All @@ -263,6 +267,7 @@ function apply_filters( string $hook_name, $value ) // phpcs:ignore Generic.Cod
$direct_apply = $workspace->worktree_cleanup_artifacts(array());
$assert(true, is_wp_error($direct_apply), 'direct apply without plan is rejected');
$assert('artifact_cleanup_plan_required', $direct_apply->code ?? '', 'direct apply error is explicit');
$assert(true, str_contains($direct_apply->get_error_message(), 'workspace cleanup run --mode=artifacts --limit=100 --offset=0 --format=json'), 'direct apply error points to matching high-level apply command');

// Build a stricter plan from the exhaustive scan for precise apply-shape
// assertions. This keeps the source-file-mismatch test deterministic
Expand Down
20 changes: 16 additions & 4 deletions tests/smoke-worktree-cleanup-cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,19 @@ class FakeArtifactCleanupAbility
public function execute( array $input ): array
{
$this->last_input = $input;
$apply_command = 'studio wp datamachine-code workspace cleanup run --mode=artifacts';
if ( ! empty($input['exhaustive']) ) {
$apply_command .= ' --exhaustive';
} else {
$apply_command .= ' --limit=' . (int) ( $input['limit'] ?? 100 );
$apply_command .= ' --offset=' . (int) ( $input['offset'] ?? 0 );
}
$apply_command .= ' --format=json';
return array(
'success' => true,
'dry_run' => ! empty($input['dry_run']),
'candidates' => array(
'success' => true,
'dry_run' => ! empty($input['dry_run']),
'apply_command' => $apply_command,
'candidates' => array(
array(
'handle' => 'repo@old',
'repo' => 'repo',
Expand All @@ -316,6 +325,7 @@ public function execute( array $input ): array
),
),
'summary' => array(
'apply_command' => $apply_command,
'would_remove_artifacts' => 1,
'removed_artifacts' => 0,
'skipped' => 1,
Expand Down Expand Up @@ -1376,11 +1386,13 @@ public function execute( array $input ): array
datamachine_code_cleanup_assert(array( 'dry_run' => true, 'force' => false ) === $artifact_ability->last_input, 'cleanup-artifacts dry-run flags forwarded to ability');
$artifact_json = json_decode(WP_CLI::$logs[0] ?? '', true);
datamachine_code_cleanup_assert('target' === ( $artifact_json['candidates'][0]['artifacts'][0]['path'] ?? '' ), 'cleanup-artifacts JSON includes artifact paths');
datamachine_code_cleanup_assert('studio wp datamachine-code workspace cleanup run --mode=artifacts --limit=100 --offset=0 --format=json' === ( $artifact_json['apply_command'] ?? '' ), 'cleanup-artifacts JSON includes matching high-level apply command');
datamachine_code_cleanup_assert(( $artifact_json['apply_command'] ?? '' ) === ( $artifact_json['summary']['apply_command'] ?? null ), 'cleanup-artifacts summary repeats matching apply command');

WP_CLI::$logs = array();
WP_CLI::$successes = array();
$command->worktree(array( 'cleanup-artifacts' ), array( 'dry-run' => true ));
datamachine_code_cleanup_assert(str_contains(WP_CLI::$successes[0] ?? '', 'workspace cleanup run --mode=artifacts'), 'cleanup-artifacts dry-run points daily apply path to task-backed cleanup');
datamachine_code_cleanup_assert(str_contains(WP_CLI::$successes[0] ?? '', 'workspace cleanup run --mode=artifacts --limit=100 --offset=0 --format=json'), 'cleanup-artifacts dry-run points daily apply path to same task-backed page');
datamachine_code_cleanup_assert(str_contains(WP_CLI::$successes[0] ?? '', 'low-level escape hatch'), 'cleanup-artifacts dry-run demotes apply-plan wording');
datamachine_code_cleanup_assert(! str_contains(WP_CLI::$successes[0] ?? '', 'Save JSON'), 'cleanup-artifacts dry-run does not normalize saving plan files');

Expand Down
Loading