huskies: merge 573_story_remove_criterion_mcp_tool_to_delete_an_acceptance_criterion

This commit is contained in:
dave
2026-04-15 13:19:17 +00:00
parent 483489cc44
commit 7fa31c03a3
4 changed files with 204 additions and 4 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ pub use bug_ops::{
};
pub use story_ops::{
add_criterion_to_file, check_criterion_in_file, create_story_file, edit_criterion_in_file,
update_story_in_file,
remove_criterion_from_file, update_story_in_file,
};
pub use test_results::{
read_test_results_from_story_file, write_coverage_baseline_to_story_file,
+102
View File
@@ -126,6 +126,53 @@ pub fn check_criterion_in_file(
Ok(())
}
/// Remove an acceptance criterion from a story by its 0-based index (counting all criteria,
/// both checked and unchecked). Returns an error if the index is out of range.
pub fn remove_criterion_from_file(
project_root: &Path,
story_id: &str,
criterion_index: usize,
) -> Result<(), String> {
let contents = read_story_content(project_root, story_id)?;
let mut count: usize = 0;
let mut found = false;
let new_lines: Vec<String> = contents
.lines()
.filter(|line| {
let trimmed = line.trim();
if trimmed.starts_with("- [ ] ") || trimmed.starts_with("- [x] ") {
if count == criterion_index {
count += 1;
found = true;
return false;
}
count += 1;
}
true
})
.map(|s| s.to_string())
.collect();
if !found {
return Err(format!(
"Criterion index {criterion_index} out of range. Story '{story_id}' has \
{count} criteria (indices 0..{}).",
count.saturating_sub(1)
));
}
let mut new_str = new_lines.join("\n");
if contents.ends_with('\n') {
new_str.push('\n');
}
let stage = story_stage(story_id).unwrap_or_else(|| "2_current".to_string());
write_story_content(project_root, story_id, &stage, &new_str);
Ok(())
}
/// Edit the text of an existing acceptance criterion without changing its checked state.
///
/// Finds the criterion at `criterion_index` (0-based, counting all criteria regardless
@@ -578,6 +625,61 @@ mod tests {
assert!(result.unwrap_err().contains("Acceptance Criteria"));
}
// ── remove_criterion_from_file tests ──────────────────────────────────────
#[test]
fn remove_criterion_removes_by_index() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(
tmp.path(),
"20_remove_test",
&story_with_ac_section(&["First", "Second", "Third"]),
);
remove_criterion_from_file(tmp.path(), "20_remove_test", 1).unwrap();
let contents = read_story_content(tmp.path(), "20_remove_test").unwrap();
assert!(contents.contains("- [ ] First"), "First should remain");
assert!(!contents.contains("Second"), "Second should be removed");
assert!(contents.contains("- [ ] Third"), "Third should remain");
}
#[test]
fn remove_criterion_shifts_indices() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(
tmp.path(),
"21_remove_test",
&story_with_ac_section(&["A", "B", "C"]),
);
remove_criterion_from_file(tmp.path(), "21_remove_test", 0).unwrap();
let contents = read_story_content(tmp.path(), "21_remove_test").unwrap();
assert!(!contents.contains("- [ ] A"), "A should be removed");
assert!(contents.contains("- [ ] B"), "B should remain");
assert!(contents.contains("- [ ] C"), "C should remain");
// B is now at index 0, C at index 1 — verify by removing B next
remove_criterion_from_file(tmp.path(), "21_remove_test", 0).unwrap();
let contents2 = read_story_content(tmp.path(), "21_remove_test").unwrap();
assert!(!contents2.contains("- [ ] B"), "B should now be removed");
assert!(contents2.contains("- [ ] C"), "C should still remain");
}
#[test]
fn remove_criterion_out_of_range_returns_error() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(
tmp.path(),
"22_remove_test",
&story_with_ac_section(&["Only"]),
);
let result = remove_criterion_from_file(tmp.path(), "22_remove_test", 5);
assert!(result.is_err(), "should fail for out-of-range index");
assert!(result.unwrap_err().contains("out of range"));
}
// ── update_story_in_file tests ─────────────────────────────────────────────
#[test]