huskies: merge 516_story_update_story_description_should_create_the_description_section_if_it_doesn_t_exist_instead_of_erroring
This commit is contained in:
@@ -517,6 +517,53 @@ pub(super) fn replace_section_content(content: &str, section_name: &str, new_tex
|
|||||||
Ok(new_str)
|
Ok(new_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert a new `## {section_name}` section into `content`.
|
||||||
|
///
|
||||||
|
/// The new section is placed immediately before the first occurrence of
|
||||||
|
/// `## {before_section}`. If `before_section` is `None` or not found in the
|
||||||
|
/// document, the section is appended at the end.
|
||||||
|
pub(super) fn create_section_content(
|
||||||
|
content: &str,
|
||||||
|
section_name: &str,
|
||||||
|
new_text: &str,
|
||||||
|
before_section: Option<&str>,
|
||||||
|
) -> String {
|
||||||
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
|
|
||||||
|
let insert_at = before_section
|
||||||
|
.and_then(|before| {
|
||||||
|
let heading = format!("## {before}");
|
||||||
|
lines.iter().position(|l| l.trim() == heading)
|
||||||
|
})
|
||||||
|
.unwrap_or(lines.len());
|
||||||
|
|
||||||
|
let mut new_lines: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for line in lines.iter().take(insert_at) {
|
||||||
|
new_lines.push(line.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure a blank line before the new heading.
|
||||||
|
if new_lines.last().map(|l| !l.is_empty()).unwrap_or(false) {
|
||||||
|
new_lines.push(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
new_lines.push(format!("## {section_name}"));
|
||||||
|
new_lines.push(String::new());
|
||||||
|
new_lines.push(new_text.to_string());
|
||||||
|
new_lines.push(String::new());
|
||||||
|
|
||||||
|
for line in lines.iter().skip(insert_at) {
|
||||||
|
new_lines.push(line.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_str = new_lines.join("\n");
|
||||||
|
if content.ends_with('\n') {
|
||||||
|
new_str.push('\n');
|
||||||
|
}
|
||||||
|
new_str
|
||||||
|
}
|
||||||
|
|
||||||
/// Replace the `## Test Results` section in `contents` with `new_section`,
|
/// Replace the `## Test Results` section in `contents` with `new_section`,
|
||||||
/// or append it if not present.
|
/// or append it if not present.
|
||||||
pub(super) fn replace_or_append_section(contents: &str, header: &str, new_section: &str) -> String {
|
pub(super) fn replace_or_append_section(contents: &str, header: &str, new_section: &str) -> String {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use serde_json::Value;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::{next_item_number, read_story_content, replace_section_content, slugify_name, story_stage, write_story_content_with_fs};
|
use super::{create_section_content, next_item_number, read_story_content, replace_section_content, slugify_name, story_stage, write_story_content_with_fs};
|
||||||
|
|
||||||
/// Shared create-story logic used by both the OpenApi and MCP handlers.
|
/// Shared create-story logic used by both the OpenApi and MCP handlers.
|
||||||
///
|
///
|
||||||
@@ -249,10 +249,16 @@ pub fn update_story_in_file(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(us) = user_story {
|
if let Some(us) = user_story {
|
||||||
contents = replace_section_content(&contents, "User Story", us)?;
|
contents = match replace_section_content(&contents, "User Story", us) {
|
||||||
|
Ok(updated) => updated,
|
||||||
|
Err(_) => create_section_content(&contents, "User Story", us, Some("Acceptance Criteria")),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if let Some(desc) = description {
|
if let Some(desc) = description {
|
||||||
contents = replace_section_content(&contents, "Description", desc)?;
|
contents = match replace_section_content(&contents, "Description", desc) {
|
||||||
|
Ok(updated) => updated,
|
||||||
|
Err(_) => create_section_content(&contents, "Description", desc, Some("Acceptance Criteria")),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write back to content store.
|
// Write back to content store.
|
||||||
@@ -505,13 +511,56 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn update_story_missing_section_returns_error() {
|
fn update_story_creates_user_story_section_if_missing() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
setup_story_in_fs(tmp.path(), "23_test", "---\nname: T\n---\n\nNo sections here.\n");
|
// Story with no ## User Story section but has ## Acceptance Criteria.
|
||||||
|
let content = "---\nname: T\n---\n\n## Acceptance Criteria\n\n- [ ] AC\n";
|
||||||
|
setup_story_in_fs(tmp.path(), "23_test", content);
|
||||||
|
|
||||||
let result = update_story_in_file(tmp.path(), "23_test", Some("new text"), None, None);
|
let result = update_story_in_file(tmp.path(), "23_test", Some("New user story"), None, None);
|
||||||
assert!(result.is_err());
|
assert!(result.is_ok(), "should succeed when section is missing: {result:?}");
|
||||||
assert!(result.unwrap_err().contains("User Story"));
|
|
||||||
|
let updated = read_story_content(tmp.path(), "23_test").unwrap();
|
||||||
|
assert!(updated.contains("## User Story"), "section should be created");
|
||||||
|
assert!(updated.contains("New user story"), "text should be present");
|
||||||
|
// Section should appear before Acceptance Criteria.
|
||||||
|
let pos_us = updated.find("## User Story").unwrap();
|
||||||
|
let pos_ac = updated.find("## Acceptance Criteria").unwrap();
|
||||||
|
assert!(pos_us < pos_ac, "User Story should be before Acceptance Criteria");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_story_creates_description_section_if_missing() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
// Story with no ## Description section but has ## Acceptance Criteria.
|
||||||
|
let content = "---\nname: T\n---\n\n## User Story\n\nAs a user...\n\n## Acceptance Criteria\n\n- [ ] AC\n";
|
||||||
|
setup_story_in_fs(tmp.path(), "32_test", content);
|
||||||
|
|
||||||
|
let result = update_story_in_file(tmp.path(), "32_test", None, Some("New description text"), None);
|
||||||
|
assert!(result.is_ok(), "should succeed when section is missing: {result:?}");
|
||||||
|
|
||||||
|
let updated = read_story_content(tmp.path(), "32_test").unwrap();
|
||||||
|
assert!(updated.contains("## Description"), "section should be created");
|
||||||
|
assert!(updated.contains("New description text"), "text should be present");
|
||||||
|
// Section should appear before Acceptance Criteria.
|
||||||
|
let pos_desc = updated.find("## Description").unwrap();
|
||||||
|
let pos_ac = updated.find("## Acceptance Criteria").unwrap();
|
||||||
|
assert!(pos_desc < pos_ac, "Description should be before Acceptance Criteria");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_story_creates_description_section_no_ac_section() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
// Story with no ## Description and no ## Acceptance Criteria.
|
||||||
|
let content = "---\nname: T\n---\n\nSome content here.\n";
|
||||||
|
setup_story_in_fs(tmp.path(), "33_test", content);
|
||||||
|
|
||||||
|
let result = update_story_in_file(tmp.path(), "33_test", None, Some("Appended description"), None);
|
||||||
|
assert!(result.is_ok(), "should succeed even with no Acceptance Criteria: {result:?}");
|
||||||
|
|
||||||
|
let updated = read_story_content(tmp.path(), "33_test").unwrap();
|
||||||
|
assert!(updated.contains("## Description"), "section should be created");
|
||||||
|
assert!(updated.contains("Appended description"), "text should be present");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user