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
1 change: 1 addition & 0 deletions inc/Workspace/CleanupRunService.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ public function until_empty( array $opts = array() ): array|\WP_Error {
'include_resolvers' => false,
'force_artifact_cleanup' => ! empty($opts['force']),
'allow_active_artifact_cleanup' => ! empty($opts['allow_active_artifact_cleanup']),
'limit' => $limit,
'worktree_older_than' => isset($opts['older_than']) ? trim( (string) $opts['older_than']) : '',
)
);
Expand Down
121 changes: 103 additions & 18 deletions inc/Workspace/WorkspaceWorktreeLifecycle.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,9 @@ private function worktree_add_with_capacity_lock(
if ( $demand_plan instanceof \WP_Error ) {
return $demand_plan;
}
$disk_budget = WorktreeDiskBudget::inspect(
$this->workspace_path,
WorktreeDiskBudget::thresholds($repo, $branch),
$force,
array( 'include_workspace_usage' => true ),
$demand_plan
);
$disk_budget = $this->inspect_worktree_capacity($repo, $branch, $force, $demand_plan);
$capacity_reclaim = $this->reclaim_capacity_eligible_artifacts($repo, $branch, $force, $demand_plan, $disk_budget);
$disk_budget = $capacity_reclaim['after'];
if ( 'refused' === ( $disk_budget['status'] ?? '' ) ) {
$recommendations = array_map(
static function ( $row ): string {
Expand Down Expand Up @@ -289,8 +285,9 @@ static function ( $row ): string {
implode("\n", array_filter($recommendations))
),
array(
'status' => 507,
'disk_budget' => $disk_budget,
'status' => 507,
'disk_budget' => $disk_budget,
'capacity_reclaim' => $capacity_reclaim['evidence'],
)
);
}
Expand Down Expand Up @@ -337,22 +334,26 @@ static function ( $row ): string {
);
}

$response['disk_budget'] = $disk_budget;
$response['disk_budget'] = $disk_budget;
$response['capacity_reclaim'] = $capacity_reclaim['evidence'];
if ( ! empty($response['rebase_succeeded']) ) {
$post_rebase_demand = WorktreeBootstrapper::demand_plan_for_target($wt_path, 'HEAD', $bootstrap);
if ( $post_rebase_demand instanceof \WP_Error ) {
$this->rollback_rejected_worktree($primary_path, $wt_path, $branch, ! empty($response['created_branch']));
return $post_rebase_demand;
}
$post_rebase_demand = WorktreeBootstrapper::remaining_demand_after_materialization($post_rebase_demand);
$post_rebase_budget = WorktreeDiskBudget::inspect(
$this->workspace_path,
WorktreeDiskBudget::thresholds($repo, $branch),
$post_rebase_budget = $this->inspect_worktree_capacity($repo, $branch, $force, $post_rebase_demand);
$post_rebase_capacity_reclaim = $this->reclaim_capacity_eligible_artifacts(
$repo,
$branch,
$force,
array( 'include_workspace_usage' => true ),
$post_rebase_demand
$post_rebase_demand,
$post_rebase_budget
);
$post_rebase_budget = $post_rebase_capacity_reclaim['after'];
$response['post_rebase_disk_budget'] = $post_rebase_budget;
$response['post_rebase_capacity_reclaim'] = $post_rebase_capacity_reclaim['evidence'];
$response['disk_budget'] = $post_rebase_budget;
if ( 'refused' === ( $post_rebase_budget['status'] ?? '' ) ) {
$this->rollback_rejected_worktree($primary_path, $wt_path, $branch, ! empty($response['created_branch']));
Expand All @@ -361,9 +362,10 @@ static function ( $row ): string {
'worktree_disk_budget_exceeded',
'Refusing dependency bootstrap because the effective post-rebase target exceeds the workspace capacity budget.',
array(
'status' => 507,
'disk_budget' => $post_rebase_budget,
'phase' => 'post_rebase_admission',
'status' => 507,
'disk_budget' => $post_rebase_budget,
'capacity_reclaim' => $post_rebase_capacity_reclaim['evidence'],
'phase' => 'post_rebase_admission',
)
);
}
Expand Down Expand Up @@ -2113,6 +2115,89 @@ private function repair_cleanup_eligible_stale_worktree_marker( array $row, arra
);
}

/**
* Inspect capacity through a testable admission seam.
*
* @param array<string,mixed> $demand_plan
* @return array<string,mixed>
*/
protected function inspect_worktree_capacity( string $repo, string $branch, bool $force, array $demand_plan ): array {
return WorktreeDiskBudget::inspect(
$this->workspace_path,
WorktreeDiskBudget::thresholds($repo, $branch),
$force,
array( 'include_workspace_usage' => true ),
$demand_plan
);
}

/**
* Reclaim only already-eligible reconstructable artifacts before refusing
* admission. The caller holds the global capacity lock, so the follow-up
* measurement and admission decision cannot race another worktree creation.
*
* @param array<string,mixed> $demand_plan
* @param array<string,mixed> $before
* @return array{after:array<string,mixed>,evidence:array<string,mixed>}
*/
protected function reclaim_capacity_eligible_artifacts( string $repo, string $branch, bool $force, array $demand_plan, array $before ): array {
$evidence = array(
'attempted' => false,
'reclaimed_bytes' => 0,
'skipped' => array(),
'final_decision' => 'admitted_without_reclaim',
);
if ( $force || 'refused' !== ( $before['status'] ?? '' ) ) {
$evidence['skip_reason'] = $force ? 'force_override' : 'capacity_not_refused';
return array( 'after' => $before, 'evidence' => $evidence );
}

$evidence['attempted'] = true;
if ( ! class_exists(CleanupRunService::class) ) {
require_once __DIR__ . '/CleanupRunService.php';
}
$reclaim = $this->run_capacity_artifact_reclaim();
$after = $this->inspect_worktree_capacity($repo, $branch, false, $demand_plan);
if ( $reclaim instanceof \WP_Error ) {
$evidence['error_code'] = $reclaim->get_error_code();
$evidence['final_decision'] = 'refused_after_reclaim_error';
return array( 'after' => $after, 'evidence' => $evidence );
}

$evidence['state'] = (string) ( $reclaim['state'] ?? 'unknown' );
$evidence['applied'] = (int) ( $reclaim['applied'] ?? 0 );
$evidence['reclaimed_bytes'] = (int) ( $reclaim['bytes_reclaimed'] ?? 0 );
$evidence['skipped'] = $this->capacity_reclaim_skipped_categories($reclaim);
$evidence['final_decision'] = 'refused' === ( $after['status'] ?? '' ) ? 'refused_after_reclaim' : 'admitted_after_reclaim';

return array( 'after' => $after, 'evidence' => $evidence );
}

/** @return array<string,int> */
protected function capacity_reclaim_skipped_categories( array $reclaim ): array {
$categories = array();
foreach ( (array) ( $reclaim['remaining_blocked_reasons'] ?? array() ) as $reason => $bucket ) {
$count = is_array($bucket) ? (int) ( $bucket['count'] ?? 0 ) : (int) $bucket;
if ( $count > 0 ) {
$categories[ (string) $reason ] = $count;
}
}
return $categories;
}

/** @return array<string,mixed>|\WP_Error */
protected function run_capacity_artifact_reclaim(): array|\WP_Error {
return ( new CleanupRunService() )->until_empty(
array(
'mode' => 'artifacts',
'force' => false,
'limit' => 25,
'max_passes' => 3,
'budget_seconds' => 30,
)
);
}

/**
* Attach host-shell remediation commands to local-git-unavailable worktree errors.
*
Expand Down
7 changes: 6 additions & 1 deletion tests/cleanup-until-empty-blocked-only.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
require_once dirname(__DIR__) . '/inc/Workspace/CleanupRunService.php';

final class FakeUntilEmptyCleanupRunService extends CleanupRunService {
/** @var array<int,array<string,mixed>> */
public array $plan_options = array();

/** @var array<int,array<string,mixed>> */
private array $plans;

Expand All @@ -26,6 +29,7 @@ public function __construct( array $plans, array $applies ) {
}

public function plan( array $opts = array() ): array|\WP_Error {
$this->plan_options[] = $opts;
return array_shift($this->plans);
}

Expand Down Expand Up @@ -76,12 +80,13 @@ function cleanup_until_empty_plan(): array {
),
)
);
$result = $service->until_empty(array( 'mode' => 'artifacts' ));
$result = $service->until_empty(array( 'mode' => 'artifacts', 'limit' => 7 ));

cleanup_until_empty_assert_same(true, $result['success'] ?? null, 'Blocked-only artifact cleanup should be a successful terminal result.');
cleanup_until_empty_assert_same('complete_with_blockers', $result['state'] ?? null, 'Blocked-only artifact cleanup should report a distinct terminal state.');
cleanup_until_empty_assert_same(2, $result['remaining_blocked_count'] ?? null, 'Blocked-only result should include the blocker count.');
cleanup_until_empty_assert_same($blocked_summary, $result['remaining_blocked_reasons'] ?? null, 'Blocked-only result should include blocker reasons.');
cleanup_until_empty_assert_same(7, $service->plan_options[0]['limit'] ?? null, 'Bounded cleanup must apply its requested limit while building the DB-backed plan.');

$service = new FakeUntilEmptyCleanupRunService(
array( cleanup_until_empty_plan() ),
Expand Down
7 changes: 7 additions & 0 deletions tests/worktree-add-lifecycle.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ function wp_json_encode( mixed $value, int $flags = 0, int $depth = 512 ): strin
return json_encode($value, $flags, $depth);
}

function wp_generate_password( int $length = 12, bool $special_chars = true, bool $extra_special_chars = false ): string {
return str_repeat('a', $length);
}

$GLOBALS['datamachine_code_test_options'] = array();
function get_option( string $name, mixed $default = false ): mixed {
return $GLOBALS['datamachine_code_test_options'][ $name ] ?? $default;
Expand Down Expand Up @@ -325,8 +329,11 @@ function create_primary_checkout( string $workspace_root ): void {
assert_true('worktree_disk_budget_exceeded' === $refused->get_error_code(), 'unexpected disk pressure refusal error code');
$refusal_data = (array) $refused->get_error_data();
$disk_budget = (array) ( $refusal_data['disk_budget'] ?? array() );
$reclaim = (array) ( $refusal_data['capacity_reclaim'] ?? array() );
assert_true('refused' === ( $disk_budget['status'] ?? '' ), 'disk pressure refusal did not include refused budget status');
assert_true(isset($disk_budget['free_bytes'], $disk_budget['effective_refuse_bytes']), 'disk pressure refusal must include exact free and required bytes');
assert_true(true === ( $reclaim['attempted'] ?? false ), 'capacity refusal did not attempt bounded safe artifact reclaim');
assert_true('refused_after_reclaim' === ( $reclaim['final_decision'] ?? '' ), 'capacity refusal did not report the final reclaim decision');
assert_true(str_contains($refused->get_error_message(), 'studio wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --dry-run --limit=25'), 'disk pressure refusal must include the next cleanup command');
assert_true(! is_dir($workspace_root . '/homeboy@audit-primitives-disk-refused'), 'disk pressure refusal left a worktree directory behind');

Expand Down
70 changes: 70 additions & 0 deletions tests/worktree-capacity-auto-reclaim.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

if ( ! defined('ABSPATH') ) {
define('ABSPATH', __DIR__ . '/fixtures/');
}

if ( ! class_exists('WP_Error') ) {
class WP_Error {
public function __construct( private string $code = '' ) {}

public function get_error_code(): string {
return $this->code;
}
}
}

require_once dirname(__DIR__) . '/inc/Workspace/WorkspaceWorktreeLifecycle.php';

use DataMachineCode\Workspace\WorkspaceWorktreeLifecycle;

function capacity_auto_reclaim_assert_same( mixed $expected, mixed $actual, string $message ): void {
if ( $expected !== $actual ) {
throw new RuntimeException(sprintf('%s Expected %s, got %s.', $message, var_export($expected, true), var_export($actual, true)));
}
}

final class CapacityAutoReclaimHarness {
use WorkspaceWorktreeLifecycle;

/** @var array<int,array<string,mixed>> */
private array $budgets;

public function __construct( array $budgets ) {
$this->budgets = $budgets;
}

public function reclaim( array $before ): array {
return $this->reclaim_capacity_eligible_artifacts('repo', 'branch', false, array( 'bytes' => 10 ), $before);
}

protected function inspect_worktree_capacity( string $repo, string $branch, bool $force, array $demand_plan ): array {
return array_shift($this->budgets) ?? array( 'status' => 'refused' );
}

protected function run_capacity_artifact_reclaim(): array|\WP_Error {
return array(
'state' => 'completed_with_skips',
'applied' => 2,
'bytes_reclaimed' => 60,
'remaining_blocked_reasons' => array(
'dirty_worktree' => array( 'count' => 1 ),
'unpushed_commits' => array( 'count' => 2 ),
),
);
}
}

$before = array( 'status' => 'refused', 'projected_free_bytes' => 90, 'effective_refuse_bytes' => 100 );
$harness = new CapacityAutoReclaimHarness(array( array( 'status' => 'ok', 'projected_free_bytes' => 150, 'effective_refuse_bytes' => 100 ) ));
$result = $harness->reclaim($before);

capacity_auto_reclaim_assert_same('ok', $result['after']['status'] ?? null, 'Reclaim crossing the capacity floor must use the recomputed capacity.');
capacity_auto_reclaim_assert_same(true, $result['evidence']['attempted'] ?? null, 'Refused admission must attempt artifact-only reclaim.');
capacity_auto_reclaim_assert_same(60, $result['evidence']['reclaimed_bytes'] ?? null, 'Reclaim evidence must retain measured bytes.');
capacity_auto_reclaim_assert_same(array( 'dirty_worktree' => 1, 'unpushed_commits' => 2 ), $result['evidence']['skipped'] ?? null, 'Reclaim evidence must retain protected skip categories.');
capacity_auto_reclaim_assert_same('admitted_after_reclaim', $result['evidence']['final_decision'] ?? null, 'Crossing the floor must admit after safe reclaim.');

echo "worktree-capacity-auto-reclaim: ok\n";