huskies: merge 493_bug_story_dependency_chain_not_firing_due_to_front_matter_format_issues

This commit is contained in:
dave
2026-04-07 13:28:38 +00:00
parent d158b05a1a
commit a3a3942b0a
3 changed files with 90 additions and 3 deletions
+81 -3
View File
@@ -14,6 +14,7 @@ pub fn create_story_file(
name: &str,
user_story: Option<&str>,
acceptance_criteria: Option<&[String]>,
depends_on: Option<&[u32]>,
commit: bool,
) -> Result<String, String> {
let story_number = next_item_number(root)?;
@@ -42,6 +43,10 @@ pub fn create_story_file(
let mut content = String::new();
content.push_str("---\n");
content.push_str(&format!("name: \"{}\"\n", name.replace('"', "\\\"")));
if let Some(deps) = depends_on.filter(|d| !d.is_empty()) {
let nums: Vec<String> = deps.iter().map(|n| n.to_string()).collect();
content.push_str(&format!("depends_on: [{}]\n", nums.join(", ")));
}
content.push_str("---\n\n");
content.push_str(&format!("# Story {story_number}: {name}\n\n"));
@@ -185,12 +190,16 @@ pub fn add_criterion_to_file(
/// Encode a string value as a YAML scalar.
///
/// Booleans (`true`/`false`) and integers are written as native YAML types (unquoted).
/// Everything else is written as a quoted string to avoid ambiguity.
/// Booleans (`true`/`false`), integers, and inline sequences (`[...]`) are
/// written as native YAML types (unquoted). Everything else is written as a
/// quoted string to avoid ambiguity.
fn yaml_encode_scalar(value: &str) -> String {
match value {
"true" | "false" => value.to_string(),
s if s.parse::<i64>().is_ok() => s.to_string(),
// YAML inline sequences like [490] or [490, 491] — write unquoted so
// serde_yaml can deserialise them as Vec<u32>.
s if s.starts_with('[') && s.ends_with(']') => s.to_string(),
s => format!("\"{}\"", s.replace('"', "\\\"").replace('\n', " ").replace('\r', "")),
}
}
@@ -321,7 +330,7 @@ mod tests {
fn create_story_with_colon_in_name_produces_valid_yaml() {
let tmp = tempfile::tempdir().unwrap();
let name = "Server-owned agent completion: remove report_completion dependency";
let result = create_story_file(tmp.path(), name, None, None, false);
let result = create_story_file(tmp.path(), name, None, None, None, false);
assert!(result.is_ok(), "create_story_file failed: {result:?}");
let backlog = tmp.path().join(".huskies/work/1_backlog");
@@ -652,4 +661,73 @@ mod tests {
let meta = parse_front_matter(&contents).expect("front matter should parse");
assert_eq!(meta.name.as_deref(), Some("My Story"), "name preserved after writing bool field");
}
// ── Bug 493 regression tests ──────────────────────────────────────────────
/// Bug 493 fix 1: update_story with depends_on as a string like "[490]" must
/// write the value unquoted so serde_yaml can deserialise it as Vec<u32>.
#[test]
fn update_story_depends_on_stored_as_yaml_array_not_quoted_string() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".huskies/work/2_current");
fs::create_dir_all(&current).unwrap();
let filepath = current.join("30_test.md");
fs::write(&filepath, "---\nname: T\n---\n\nNo sections.\n").unwrap();
let mut fields = HashMap::new();
fields.insert("depends_on".to_string(), "[490]".to_string());
update_story_in_file(tmp.path(), "30_test", None, None, Some(&fields)).unwrap();
let result = fs::read_to_string(&filepath).unwrap();
assert!(result.contains("depends_on: [490]"), "should be unquoted array: {result}");
assert!(!result.contains("depends_on: \"[490]\""), "must not be quoted: {result}");
// Must round-trip through the parser correctly.
let meta = parse_front_matter(&result).expect("front matter should parse");
assert_eq!(meta.depends_on, Some(vec![490]));
}
/// Bug 493 fix 2: create_story_file with depends_on must write it as a
/// YAML front matter array, not as an acceptance criterion checkbox.
#[test]
fn create_story_with_depends_on_writes_front_matter_array() {
let tmp = tempfile::tempdir().unwrap();
let story_id = create_story_file(
tmp.path(),
"Dependent Story",
None,
None,
Some(&[489]),
false,
)
.unwrap();
let backlog = tmp.path().join(".huskies/work/1_backlog");
let contents = fs::read_to_string(backlog.join(format!("{story_id}.md"))).unwrap();
// depends_on must be in front matter, not in AC text.
assert!(contents.contains("depends_on: [489]"), "missing front matter: {contents}");
assert!(!contents.contains("- [ ] depends_on"), "must not appear as checkbox: {contents}");
let meta = parse_front_matter(&contents).expect("front matter should parse");
assert_eq!(meta.depends_on, Some(vec![489]));
}
/// Multi-element depends_on array round-trips correctly.
#[test]
fn update_story_depends_on_multi_element_array() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".huskies/work/2_current");
fs::create_dir_all(&current).unwrap();
let filepath = current.join("31_test.md");
fs::write(&filepath, "---\nname: T\n---\n\nNo sections.\n").unwrap();
let mut fields = HashMap::new();
fields.insert("depends_on".to_string(), "[490, 491]".to_string());
update_story_in_file(tmp.path(), "31_test", None, None, Some(&fields)).unwrap();
let result = fs::read_to_string(&filepath).unwrap();
let meta = parse_front_matter(&result).expect("front matter should parse");
assert_eq!(meta.depends_on, Some(vec![490, 491]));
}
}