Skip to content

Commit 7417ae9

Browse files
refactor(execute): extract check_budget helper to DRY up per-tool budget checks (#89)
* Initial plan * refactor(execute): extract check_budget helper to DRY up budget checks Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/892058b8-8269-4769-acf3-676555b60e2b Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
1 parent 513f7fe commit 7417ae9

1 file changed

Lines changed: 104 additions & 48 deletions

File tree

src/execute.rs

Lines changed: 104 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
324329
mod 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

Comments
 (0)