From cd7b88b7d2195f26ae4886d70f44e635d1b83cdd Mon Sep 17 00:00:00 2001 From: "homeboy-ci[bot]" <266378653+homeboy-ci[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:29:48 -0400 Subject: [PATCH] fix: clarify active no-signal drain budget continuation --- .../WorkspaceAbandonedCleanupOrchestrator.php | 31 ++++++++++- .../smoke-abandoned-cleanup-orchestrator.php | 53 +++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php b/inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php index b20c17e..be6793c 100644 --- a/inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php +++ b/inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php @@ -142,6 +142,8 @@ public function run( array $input ): array|\WP_Error { if ( $this->budget_expired($deadline) ) { $result['evidence']['budget_exhausted'] = true; + $result['continuation'] = $this->build_budget_continuation($step_stage, 0, $limit, $passes, $force, $until_budget, $active_no_signal_drain, 'budget_exhausted_before_stage'); + $result['next_commands'][] = (string) $result['continuation']['next_command']; break 2; } @@ -182,6 +184,12 @@ public function run( array $input ): array|\WP_Error { if ( is_wp_error($bounded) ) { return $bounded; } + if ( $this->budget_expired($deadline) ) { + $result['evidence']['budget_exhausted'] = true; + $result['continuation'] = $this->build_budget_continuation($default_stage, 0, $limit, $passes, $force, $until_budget, $active_no_signal_drain, 'budget_exhausted_after_bounded_apply'); + $result['next_commands'][] = (string) $result['continuation']['next_command']; + break; + } $removed_or_would = (int) ( $bounded['summary']['removed'] ?? 0 ) + (int) ( $bounded['summary']['would_remove'] ?? 0 ); if ( 0 === $pass_marked && 0 === $removed_or_would ) { @@ -189,6 +197,10 @@ public function run( array $input ): array|\WP_Error { } } + if ( ! empty($result['continuation']) ) { + return $this->finalize_result($result, $apply, $force, $limit, $passes, $until_budget, $started_at, $active_no_signal_drain); + } + if ( $apply ) { $prune = $this->execute_ability($abilities['prune'], array()); if ( is_wp_error($prune) ) { @@ -507,8 +519,7 @@ private function run_bounded_apply( object $ability, array &$result, bool $apply private function build_continuation( string $stage, array $step, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain = false ): array { $pagination = (array) ( $step['pagination'] ?? $step['continuation'] ?? array() ); $next_offset = isset($pagination['next_offset']) ? max(0, (int) $pagination['next_offset']) : 0; - $operation = $active_no_signal_drain ? 'active-no-signal-drain' : 'abandoned'; - $command = sprintf('studio wp datamachine-code workspace worktree %s --apply%s --stage=%s --offset=%d --limit=%d --passes=%d%s --format=json', $operation, $force ? ' --force' : '', $stage, $next_offset, $limit, $passes, '' !== $until_budget ? ' --until-budget=' . $until_budget : ''); + $command = $this->build_continuation_command($stage, $next_offset, $limit, $passes, $force, $until_budget, $active_no_signal_drain); $current = (int) ( $pagination['offset'] ?? 0 ); $mutated = (int) ( $step['summary']['written'] ?? 0 ) + (int) ( $step['summary']['removed'] ?? 0 ); $restart = ! empty($pagination['partial']) && $next_offset <= $current && $mutated > 0; @@ -527,6 +538,22 @@ private function build_continuation( string $stage, array $step, int $limit, int return $continuation; } + private function build_budget_continuation( string $stage, int $offset, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain, string $reason ): array { + return array( + 'stage' => $stage, + 'offset' => max(0, $offset), + 'next_command' => $this->build_continuation_command($stage, max(0, $offset), $limit, $passes, $force, $until_budget, $active_no_signal_drain), + 'reason' => $reason, + 'budget_exhausted' => true, + 'hint' => 'Budget expired after safe progress. Re-run next_command to continue the drain from the next safe boundary.', + ); + } + + private function build_continuation_command( string $stage, int $offset, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain ): string { + $operation = $active_no_signal_drain ? 'active-no-signal-drain' : 'abandoned'; + return sprintf('studio wp datamachine-code workspace worktree %s --apply%s --stage=%s --offset=%d --limit=%d --passes=%d%s --format=json', $operation, $force ? ' --force' : '', $stage, $offset, $limit, $passes, '' !== $until_budget ? ' --until-budget=' . $until_budget : ''); + } + private function drain_pages( object $ability, array $base_input, bool $apply, ?float $deadline = null ): array|\WP_Error { $pages = array(); $summary = array(); diff --git a/tests/smoke-abandoned-cleanup-orchestrator.php b/tests/smoke-abandoned-cleanup-orchestrator.php index b80cbdb..2c8e0f3 100644 --- a/tests/smoke-abandoned-cleanup-orchestrator.php +++ b/tests/smoke-abandoned-cleanup-orchestrator.php @@ -263,4 +263,57 @@ static function () use ( &$clock_index, $clock_values ): float { abandoned_cleanup_assert(! empty($restart_result['continuation']['candidate_set_changed_restart_required']), 'restart continuation exposes candidate set changed evidence'); abandoned_cleanup_assert('active-no-signal-drain' === explode(' ', (string) $restart_result['continuation']['next_command'])[5], 'restart next command uses active/no-signal drain'); +$stage_budget_abilities = array( + 'datamachine-code/workspace-worktree-reconcile-metadata' => new AbandonedCleanupFakeAbility('reconcile_metadata', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-finalized-apply' => new AbandonedCleanupFakeAbility('finalized', array( 'inspected' => 1, 'written' => 1 ), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => new AbandonedCleanupFakeAbility('equivalent_clean', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-report' => new AbandonedCleanupFakeAbility('active_no_signal_report', array( 'total_active_no_signal' => 0, 'inspected' => 0, 'by_suggested_action' => array() ), array( 'complete' => true, 'total' => 0 )), + 'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 0, 'removed' => 0 ), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'), +); +$clock_index = 0; +$clock_values = array( 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1002.0, 1002.0 ); +$orchestrator = new DataMachineCode\Workspace\WorkspaceAbandonedCleanupOrchestrator( + static fn( string $name ) => $stage_budget_abilities[ $name ] ?? null, + static function () use ( &$clock_index, $clock_values ): float { + $value = $clock_values[ min($clock_index, count($clock_values) - 1) ]; + ++$clock_index; + return $value; + } +); +$stage_budget_result = $orchestrator->run(array( 'active_no_signal_drain' => true, 'apply' => true, 'limit' => 10, 'passes' => 1, 'until_budget' => '1s' )); +abandoned_cleanup_assert(! is_wp_error($stage_budget_result), 'active/no-signal stage budget result succeeds'); +abandoned_cleanup_assert('budget_exhausted_before_stage' === $stage_budget_result['continuation']['reason'], 'stage budget continuation explains boundary'); +abandoned_cleanup_assert('equivalent-clean' === $stage_budget_result['continuation']['stage'], 'stage budget continuation resumes at next safe stage'); +abandoned_cleanup_assert(0 === count($stage_budget_abilities['datamachine-code/workspace-worktree-prune']->calls), 'stage budget continuation skips prune after budget exhaustion'); +abandoned_cleanup_assert(str_contains((string) $stage_budget_result['continuation']['next_command'], '--stage=equivalent-clean --offset=0'), 'stage budget continuation command resumes exact safe boundary'); + +$bounded_budget_abilities = array( + 'datamachine-code/workspace-worktree-reconcile-metadata' => new AbandonedCleanupFakeAbility('reconcile_metadata', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-finalized-apply' => new AbandonedCleanupFakeAbility('finalized', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => new AbandonedCleanupFakeAbility('equivalent_clean', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-report' => new AbandonedCleanupFakeAbility('active_no_signal_report', array( 'total_active_no_signal' => 0, 'inspected' => 0, 'by_suggested_action' => array() ), array( 'complete' => true, 'total' => 0 )), + 'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 1, 'removed' => 1 ), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'), +); +$clock_index = 0; +$orchestrator = new DataMachineCode\Workspace\WorkspaceAbandonedCleanupOrchestrator( + static fn( string $name ) => $bounded_budget_abilities[ $name ] ?? null, + static function () use ( &$clock_index ): float { + $value = $clock_index < 14 ? 1000.0 : 1002.0; + ++$clock_index; + return $value; + } +); +$bounded_budget_result = $orchestrator->run(array( 'active_no_signal_drain' => true, 'apply' => true, 'limit' => 10, 'passes' => 2, 'until_budget' => '1s' )); +abandoned_cleanup_assert(! is_wp_error($bounded_budget_result), 'active/no-signal bounded budget result succeeds'); +abandoned_cleanup_assert('budget_exhausted_after_bounded_apply' === $bounded_budget_result['continuation']['reason'], 'bounded budget continuation explains boundary'); +abandoned_cleanup_assert('finalized' === $bounded_budget_result['continuation']['stage'], 'bounded budget continuation resumes as full safe drain'); +abandoned_cleanup_assert(0 === count($bounded_budget_abilities['datamachine-code/workspace-worktree-prune']->calls), 'bounded budget continuation skips prune after budget exhaustion'); +abandoned_cleanup_assert(str_contains((string) $bounded_budget_result['continuation']['hint'], 'Re-run next_command'), 'bounded budget continuation has operator hint'); + fwrite(STDOUT, 'abandoned cleanup orchestrator smoke passed' . PHP_EOL);