@@ -269,4 +269,57 @@ static function () use ( &$clock_index, $clock_values ): float {
269269abandoned_cleanup_assert (str_contains ((string ) $ restart_result ['continuation ' ]['next_command_label ' ], 'candidate set changed ' ), 'restart continuation labels the restart command ' );
270270abandoned_cleanup_assert ('active-no-signal-drain ' === explode (' ' , (string ) $ restart_result ['continuation ' ]['next_command ' ])[5 ], 'restart next command uses active/no-signal drain ' );
271271
272+ $ stage_budget_abilities = array (
273+ 'datamachine-code/workspace-worktree-reconcile-metadata ' => new AbandonedCleanupFakeAbility ('reconcile_metadata ' , array (), array ( 'complete ' => true )),
274+ 'datamachine-code/workspace-worktree-active-no-signal-finalized-apply ' => new AbandonedCleanupFakeAbility ('finalized ' , array ( 'inspected ' => 1 , 'written ' => 1 ), array ( 'complete ' => true )),
275+ 'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply ' => new AbandonedCleanupFakeAbility ('equivalent_clean ' , array (), array ( 'complete ' => true )),
276+ 'datamachine-code/workspace-worktree-active-no-signal-merged-apply ' => new AbandonedCleanupFakeAbility ('merged ' , array (), array ( 'complete ' => true )),
277+ 'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply ' => new AbandonedCleanupFakeAbility ('remote_clean ' , array (), array ( 'complete ' => true )),
278+ '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 )),
279+ 'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply ' => new AbandonedCleanupFakeAbility ('bounded ' , array ( 'processed ' => 0 , 'removed ' => 0 ), array ( 'complete ' => true )),
280+ 'datamachine-code/workspace-worktree-prune ' => new AbandonedCleanupFakeAbility ('prune ' ),
281+ );
282+ $ clock_index = 0 ;
283+ $ clock_values = array ( 1000.0 , 1000.0 , 1000.0 , 1000.0 , 1000.0 , 1002.0 , 1002.0 );
284+ $ orchestrator = new DataMachineCode \Workspace \WorkspaceAbandonedCleanupOrchestrator (
285+ static fn ( string $ name ) => $ stage_budget_abilities [ $ name ] ?? null ,
286+ static function () use ( &$ clock_index , $ clock_values ): float {
287+ $ value = $ clock_values [ min ($ clock_index , count ($ clock_values ) - 1 ) ];
288+ ++$ clock_index ;
289+ return $ value ;
290+ }
291+ );
292+ $ stage_budget_result = $ orchestrator ->run (array ( 'active_no_signal_drain ' => true , 'apply ' => true , 'limit ' => 10 , 'passes ' => 1 , 'until_budget ' => '1s ' ));
293+ abandoned_cleanup_assert (! is_wp_error ($ stage_budget_result ), 'active/no-signal stage budget result succeeds ' );
294+ abandoned_cleanup_assert ('budget_exhausted_before_stage ' === $ stage_budget_result ['continuation ' ]['reason ' ], 'stage budget continuation explains boundary ' );
295+ abandoned_cleanup_assert ('equivalent-clean ' === $ stage_budget_result ['continuation ' ]['stage ' ], 'stage budget continuation resumes at next safe stage ' );
296+ abandoned_cleanup_assert (0 === count ($ stage_budget_abilities ['datamachine-code/workspace-worktree-prune ' ]->calls ), 'stage budget continuation skips prune after budget exhaustion ' );
297+ 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 ' );
298+
299+ $ bounded_budget_abilities = array (
300+ 'datamachine-code/workspace-worktree-reconcile-metadata ' => new AbandonedCleanupFakeAbility ('reconcile_metadata ' , array (), array ( 'complete ' => true )),
301+ 'datamachine-code/workspace-worktree-active-no-signal-finalized-apply ' => new AbandonedCleanupFakeAbility ('finalized ' , array (), array ( 'complete ' => true )),
302+ 'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply ' => new AbandonedCleanupFakeAbility ('equivalent_clean ' , array (), array ( 'complete ' => true )),
303+ 'datamachine-code/workspace-worktree-active-no-signal-merged-apply ' => new AbandonedCleanupFakeAbility ('merged ' , array (), array ( 'complete ' => true )),
304+ 'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply ' => new AbandonedCleanupFakeAbility ('remote_clean ' , array (), array ( 'complete ' => true )),
305+ '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 )),
306+ 'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply ' => new AbandonedCleanupFakeAbility ('bounded ' , array ( 'processed ' => 1 , 'removed ' => 1 ), array ( 'complete ' => true )),
307+ 'datamachine-code/workspace-worktree-prune ' => new AbandonedCleanupFakeAbility ('prune ' ),
308+ );
309+ $ clock_index = 0 ;
310+ $ orchestrator = new DataMachineCode \Workspace \WorkspaceAbandonedCleanupOrchestrator (
311+ static fn ( string $ name ) => $ bounded_budget_abilities [ $ name ] ?? null ,
312+ static function () use ( &$ clock_index ): float {
313+ $ value = $ clock_index < 14 ? 1000.0 : 1002.0 ;
314+ ++$ clock_index ;
315+ return $ value ;
316+ }
317+ );
318+ $ bounded_budget_result = $ orchestrator ->run (array ( 'active_no_signal_drain ' => true , 'apply ' => true , 'limit ' => 10 , 'passes ' => 2 , 'until_budget ' => '1s ' ));
319+ abandoned_cleanup_assert (! is_wp_error ($ bounded_budget_result ), 'active/no-signal bounded budget result succeeds ' );
320+ abandoned_cleanup_assert ('budget_exhausted_after_bounded_apply ' === $ bounded_budget_result ['continuation ' ]['reason ' ], 'bounded budget continuation explains boundary ' );
321+ abandoned_cleanup_assert ('finalized ' === $ bounded_budget_result ['continuation ' ]['stage ' ], 'bounded budget continuation resumes as full safe drain ' );
322+ abandoned_cleanup_assert (0 === count ($ bounded_budget_abilities ['datamachine-code/workspace-worktree-prune ' ]->calls ), 'bounded budget continuation skips prune after budget exhaustion ' );
323+ abandoned_cleanup_assert (str_contains ((string ) $ bounded_budget_result ['continuation ' ]['hint ' ], 'Re-run next_command ' ), 'bounded budget continuation has operator hint ' );
324+
272325fwrite (STDOUT , 'abandoned cleanup orchestrator smoke passed ' . PHP_EOL );
0 commit comments