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