@@ -111,30 +111,12 @@ pub async fn execute_safe_outputs(
111111 // Budget is consumed before execution so that failed attempts (target policy rejection,
112112 // network errors) still count — this prevents unbounded retries against a failing endpoint.
113113 if entry. get ( "name" ) . and_then ( |n| n. as_str ( ) ) == Some ( "update-work-item" ) {
114- if update_wi_executed >= max_update_wi {
115- let wi_id = entry
116- . get ( "id" )
117- . and_then ( |v| v. as_u64 ( ) )
118- . map ( |id| format ! ( " (work item #{})" , id) )
119- . unwrap_or_default ( ) ;
120- warn ! (
121- "[{}/{}] Skipping update-work-item{} entry: max ({}) already reached for this run" ,
122- i + 1 ,
123- entries. len( ) ,
124- wi_id,
125- max_update_wi
126- ) ;
127- let result = ExecutionResult :: failure ( format ! (
128- "Skipped{}: maximum update-work-item count ({}) already reached. \
129- Increase 'max' in safe-outputs.update-work-item to allow more updates.",
130- wi_id, max_update_wi
131- ) ) ;
132- println ! (
133- "[{}/{}] update-work-item - ✗ - {}" ,
134- i + 1 ,
135- entries. len( ) ,
136- result. message
137- ) ;
114+ let wi_id = entry
115+ . get ( "id" )
116+ . and_then ( |v| v. as_u64 ( ) )
117+ . map ( |id| format ! ( " (work item #{})" , id) )
118+ . unwrap_or_default ( ) ;
119+ if let Some ( result) = check_budget ( entries. len ( ) , i, "update-work-item" , & wi_id, update_wi_executed, max_update_wi) {
138120 results. push ( result) ;
139121 continue ;
140122 }
@@ -144,30 +126,12 @@ pub async fn execute_safe_outputs(
144126 // Enforce comment-on-work-item max: same skip-and-continue pattern as update-work-item.
145127 // Budget is consumed before execution so that failed attempts still count.
146128 if entry. get ( "name" ) . and_then ( |n| n. as_str ( ) ) == Some ( "comment-on-work-item" ) {
147- if comment_wi_executed >= max_comment_wi {
148- let wi_id = entry
149- . get ( "work_item_id" )
150- . and_then ( |v| v. as_i64 ( ) )
151- . map ( |id| format ! ( " (work item #{})" , id) )
152- . unwrap_or_default ( ) ;
153- warn ! (
154- "[{}/{}] Skipping comment-on-work-item{} entry: max ({}) already reached for this run" ,
155- i + 1 ,
156- entries. len( ) ,
157- wi_id,
158- max_comment_wi
159- ) ;
160- let result = ExecutionResult :: failure ( format ! (
161- "Skipped{}: maximum comment-on-work-item count ({}) already reached. \
162- Increase 'max' in safe-outputs.comment-on-work-item to allow more comments.",
163- wi_id, max_comment_wi
164- ) ) ;
165- println ! (
166- "[{}/{}] comment-on-work-item - ✗ - {}" ,
167- i + 1 ,
168- entries. len( ) ,
169- result. message
170- ) ;
129+ let wi_id = entry
130+ . get ( "work_item_id" )
131+ . and_then ( |v| v. as_i64 ( ) )
132+ . map ( |id| format ! ( " (work item #{})" , id) )
133+ . unwrap_or_default ( ) ;
134+ if let Some ( result) = check_budget ( entries. len ( ) , i, "comment-on-work-item" , & wi_id, comment_wi_executed, max_comment_wi) {
171135 results. push ( result) ;
172136 continue ;
173137 }
@@ -320,6 +284,47 @@ pub async fn execute_safe_output(
320284 Ok ( ( tool_name. to_string ( ) , result) )
321285}
322286
287+ /// Returns `Some(result)` when the budget for `tool_name` is exhausted so the caller can push the
288+ /// result and `continue` to the next entry. Returns `None` when a budget slot is still available
289+ /// and the caller should proceed with execution.
290+ ///
291+ /// `total` is the total number of entries (for the `[i/total]` log prefix), `i` is the
292+ /// zero-based index of the current entry, `wi_id` is a pre-formatted context string like
293+ /// `" (work item #42)"` or `""`.
294+ fn check_budget (
295+ total : usize ,
296+ i : usize ,
297+ tool_name : & str ,
298+ wi_id : & str ,
299+ executed : usize ,
300+ max : usize ,
301+ ) -> Option < ExecutionResult > {
302+ if executed < max {
303+ return None ;
304+ }
305+ warn ! (
306+ "[{}/{}] Skipping {}{} entry: max ({}) already reached for this run" ,
307+ i + 1 ,
308+ total,
309+ tool_name,
310+ wi_id,
311+ max
312+ ) ;
313+ let result = ExecutionResult :: failure ( format ! (
314+ "Skipped{}: maximum {} count ({}) already reached. \
315+ Increase 'max' in safe-outputs.{} to allow more.",
316+ wi_id, tool_name, max, tool_name
317+ ) ) ;
318+ println ! (
319+ "[{}/{}] {} - ✗ - {}" ,
320+ i + 1 ,
321+ total,
322+ tool_name,
323+ result. message
324+ ) ;
325+ Some ( result)
326+ }
327+
323328#[ cfg( test) ]
324329mod tests {
325330 use super :: * ;
@@ -679,4 +684,55 @@ mod tests {
679684 "noop should still succeed even when prior entries are skipped"
680685 ) ;
681686 }
687+
688+ // --- check_budget unit tests ---
689+
690+ #[ test]
691+ fn test_check_budget_returns_none_when_under_limit ( ) {
692+ let result = check_budget ( 5 , 0 , "update-work-item" , "" , 0 , 3 ) ;
693+ assert ! ( result. is_none( ) ) ;
694+ }
695+
696+ #[ test]
697+ fn test_check_budget_returns_none_at_exactly_one_below_limit ( ) {
698+ let result = check_budget ( 5 , 1 , "update-work-item" , "" , 2 , 3 ) ;
699+ assert ! ( result. is_none( ) ) ;
700+ }
701+
702+ #[ test]
703+ fn test_check_budget_returns_failure_when_at_limit ( ) {
704+ let result = check_budget ( 5 , 2 , "update-work-item" , "" , 3 , 3 ) ;
705+ assert ! ( result. is_some( ) ) ;
706+ let r = result. unwrap ( ) ;
707+ assert ! ( !r. success) ;
708+ assert ! ( r. message. contains( "maximum update-work-item count (3)" ) ) ;
709+ assert ! ( r. message. contains( "safe-outputs.update-work-item" ) ) ;
710+ }
711+
712+ #[ test]
713+ fn test_check_budget_returns_failure_when_over_limit ( ) {
714+ let result = check_budget ( 5 , 3 , "comment-on-work-item" , " (work item #99)" , 5 , 2 ) ;
715+ assert ! ( result. is_some( ) ) ;
716+ let r = result. unwrap ( ) ;
717+ assert ! ( !r. success) ;
718+ assert ! ( r. message. contains( "(work item #99)" ) ) ;
719+ assert ! ( r. message. contains( "maximum comment-on-work-item count (2)" ) ) ;
720+ }
721+
722+ #[ test]
723+ fn test_check_budget_zero_max_always_skips ( ) {
724+ let result = check_budget ( 3 , 0 , "update-work-item" , "" , 0 , 0 ) ;
725+ assert ! ( result. is_some( ) ) ;
726+ let r = result. unwrap ( ) ;
727+ assert ! ( !r. success) ;
728+ assert ! ( r. message. contains( "maximum update-work-item count (0)" ) ) ;
729+ }
730+
731+ #[ test]
732+ fn test_check_budget_wi_id_included_in_message ( ) {
733+ let result = check_budget ( 4 , 1 , "update-work-item" , " (work item #42)" , 1 , 1 ) ;
734+ assert ! ( result. is_some( ) ) ;
735+ let r = result. unwrap ( ) ;
736+ assert ! ( r. message. contains( "(work item #42)" ) ) ;
737+ }
682738}
0 commit comments