Skip to content

Commit 8f2978d

Browse files
jamesadevineCopilot
andcommitted
fix: harden area path matching and improve code clarity
- Replace expect() with unreachable!() for the impossible branch where allows_id returns None but area_path_prefix is also None - Require path separator boundary in area path prefix matching so prefix "4x4" does not accidentally match "4x4Production" - Add comments explaining why max budget is consumed before execution (prevents unbounded retries against failing endpoints) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 80cc20a commit 8f2978d

2 files changed

Lines changed: 23 additions & 8 deletions

File tree

src/execute.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ pub async fn execute_safe_outputs(
107107
entry_json
108108
);
109109

110-
// Enforce update-work-item max: skip excess entries rather than aborting the whole batch
110+
// Enforce update-work-item max: skip excess entries rather than aborting the whole batch.
111+
// Budget is consumed before execution so that failed attempts (target policy rejection,
112+
// network errors) still count — this prevents unbounded retries against a failing endpoint.
111113
if entry.get("name").and_then(|n| n.as_str()) == Some("update-work-item") {
112114
if update_wi_executed >= max_update_wi {
113115
let wi_id = entry
@@ -139,7 +141,8 @@ pub async fn execute_safe_outputs(
139141
update_wi_executed += 1;
140142
}
141143

142-
// Enforce comment-on-work-item max: skip excess entries rather than aborting the whole batch
144+
// Enforce comment-on-work-item max: same skip-and-continue pattern as update-work-item.
145+
// Budget is consumed before execution so that failed attempts still count.
143146
if entry.get("name").and_then(|n| n.as_str()) == Some("comment-on-work-item") {
144147
if comment_wi_executed >= max_comment_wi {
145148
let wi_id = entry

src/tools/comment_on_work_item.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,15 @@ impl Executor for CommentOnWorkItemResult {
220220
)));
221221
}
222222
None => {
223-
// Area path validation — need to fetch the work item
224-
let prefix = target.area_path_prefix().expect(
225-
"allows_id returned None but area_path_prefix is also None; this is a bug",
226-
);
223+
// Area path validation — need to fetch the work item.
224+
// Invariant: allows_id returns None only for StringTarget(s != "*"),
225+
// and area_path_prefix returns Some for exactly that case.
226+
let prefix = match target.area_path_prefix() {
227+
Some(p) => p,
228+
None => unreachable!(
229+
"allows_id returned None but area_path_prefix is also None"
230+
),
231+
};
227232
debug!(
228233
"Validating area path for work item #{} against prefix '{}'",
229234
self.work_item_id, prefix
@@ -232,8 +237,15 @@ impl Executor for CommentOnWorkItemResult {
232237
.await
233238
{
234239
Ok(area_path) => {
235-
// ADO area paths are case-insensitive
236-
if !area_path.to_lowercase().starts_with(&prefix.to_lowercase()) {
240+
// ADO area paths are case-insensitive and use backslash separators.
241+
// Require the match to land on a path boundary so that prefix "4x4"
242+
// doesn't accidentally match "4x4Production".
243+
let ap = area_path.to_lowercase();
244+
let pf = prefix.to_lowercase();
245+
let is_match = ap == pf
246+
|| (ap.starts_with(&pf)
247+
&& ap.as_bytes().get(pf.len()) == Some(&b'\\'));
248+
if !is_match {
237249
return Ok(ExecutionResult::failure(format!(
238250
"Work item #{} has area path '{}' which is not under allowed prefix '{}'",
239251
self.work_item_id, area_path, prefix

0 commit comments

Comments
 (0)