fix: add --all to cargo fmt in script/test and autoformat codebase

cargo fmt without --all fails with "Failed to find targets" in
workspace repos. This was blocking every story's gates. Also ran
cargo fmt --all to fix all existing formatting issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dave
2026-04-13 14:07:08 +00:00
parent ed2526ce41
commit 845b85e7a7
128 changed files with 3566 additions and 2395 deletions
+105 -35
View File
@@ -165,12 +165,16 @@ fn is_bug_item(stem: &str) -> bool {
/// Extract bug name from content (heading or front matter).
fn extract_bug_name_from_content(content: &str) -> Option<String> {
// Try front matter first.
if let Ok(meta) = parse_front_matter(content) && let Some(name) = meta.name {
if let Ok(meta) = parse_front_matter(content)
&& let Some(name) = meta.name
{
return Some(name);
}
// Fallback: heading.
for line in content.lines() {
if let Some(rest) = line.strip_prefix("# Bug ") && let Some(colon_pos) = rest.find(": ") {
if let Some(rest) = line.strip_prefix("# Bug ")
&& let Some(colon_pos) = rest.find(": ")
{
return Some(rest[colon_pos + 2..].to_string());
}
}
@@ -184,16 +188,19 @@ pub fn list_bug_files(_root: &Path) -> Result<Vec<(String, String)>, String> {
let mut bugs = Vec::new();
for item in crate::pipeline_state::read_all_typed() {
if !matches!(item.stage, crate::pipeline_state::Stage::Backlog) || !is_bug_item(&item.story_id.0) {
if !matches!(item.stage, crate::pipeline_state::Stage::Backlog)
|| !is_bug_item(&item.story_id.0)
{
continue;
}
let sid = item.story_id.0;
let name = if item.name.is_empty() { None } else { Some(item.name) }
.or_else(|| {
crate::db::read_content(&sid)
.and_then(|c| extract_bug_name_from_content(&c))
})
.unwrap_or_else(|| sid.clone());
let name = if item.name.is_empty() {
None
} else {
Some(item.name)
}
.or_else(|| crate::db::read_content(&sid).and_then(|c| extract_bug_name_from_content(&c)))
.unwrap_or_else(|| sid.clone());
bugs.push((sid, name));
}
@@ -214,17 +221,23 @@ pub fn list_refactor_files(_root: &Path) -> Result<Vec<(String, String)>, String
let mut refactors = Vec::new();
for item in crate::pipeline_state::read_all_typed() {
if !matches!(item.stage, crate::pipeline_state::Stage::Backlog) || !is_refactor_item(&item.story_id.0) {
if !matches!(item.stage, crate::pipeline_state::Stage::Backlog)
|| !is_refactor_item(&item.story_id.0)
{
continue;
}
let sid = item.story_id.0;
let name = if item.name.is_empty() { None } else { Some(item.name) }
.or_else(|| {
crate::db::read_content(&sid)
.and_then(|c| parse_front_matter(&c).ok())
.and_then(|m| m.name)
})
.unwrap_or_else(|| sid.clone());
let name = if item.name.is_empty() {
None
} else {
Some(item.name)
}
.or_else(|| {
crate::db::read_content(&sid)
.and_then(|c| parse_front_matter(&c).ok())
.and_then(|m| m.name)
})
.unwrap_or_else(|| sid.clone());
refactors.push((sid, name));
}
@@ -278,7 +291,11 @@ mod tests {
fs::write(backlog.join("3_bug_another.md"), "").unwrap();
// Also write to content store so next_item_number sees them.
crate::db::write_item_with_content("1_bug_crash", "1_backlog", "---\nname: Crash\n---\n");
crate::db::write_item_with_content("3_bug_another", "1_backlog", "---\nname: Another\n---\n");
crate::db::write_item_with_content(
"3_bug_another",
"1_backlog",
"---\nname: Another\n---\n",
);
assert!(super::super::next_item_number(tmp.path()).unwrap() >= 4);
}
@@ -323,7 +340,11 @@ mod tests {
);
let result = list_bug_files(tmp.path()).unwrap();
assert!(result.iter().any(|(id, name)| id == "7001_bug_open" && name == "Open Bug"));
assert!(
result
.iter()
.any(|(id, name)| id == "7001_bug_open" && name == "Open Bug")
);
assert!(!result.iter().any(|(id, _)| id == "7002_bug_closed"));
}
@@ -349,9 +370,18 @@ mod tests {
let result = list_bug_files(tmp.path()).unwrap();
// Find positions of our three bugs in the sorted result.
let pos_first = result.iter().position(|(id, _)| id == "7011_bug_first").unwrap();
let pos_second = result.iter().position(|(id, _)| id == "7012_bug_second").unwrap();
let pos_third = result.iter().position(|(id, _)| id == "7013_bug_third").unwrap();
let pos_first = result
.iter()
.position(|(id, _)| id == "7011_bug_first")
.unwrap();
let pos_second = result
.iter()
.position(|(id, _)| id == "7012_bug_second")
.unwrap();
let pos_third = result
.iter()
.position(|(id, _)| id == "7013_bug_third")
.unwrap();
assert!(pos_first < pos_second);
assert!(pos_second < pos_third);
}
@@ -379,12 +409,17 @@ mod tests {
)
.unwrap();
assert!(bug_id.ends_with("_bug_login_crash"), "expected ID to end with _bug_login_crash, got: {bug_id}");
assert!(
bug_id.ends_with("_bug_login_crash"),
"expected ID to end with _bug_login_crash, got: {bug_id}"
);
// Check content exists (either in DB or filesystem).
let contents = crate::db::read_content(&bug_id)
.or_else(|| {
let filepath = tmp.path().join(format!(".huskies/work/1_backlog/{bug_id}.md"));
let filepath = tmp
.path()
.join(format!(".huskies/work/1_backlog/{bug_id}.md"));
fs::read_to_string(filepath).ok()
})
.expect("bug content should exist");
@@ -393,7 +428,10 @@ mod tests {
contents.starts_with("---\nname: \"Login Crash\"\n---"),
"bug file must start with YAML front matter"
);
assert!(contents.contains("Login Crash"), "content should mention bug name");
assert!(
contents.contains("Login Crash"),
"content should mention bug name"
);
assert!(contents.contains("## Description"));
assert!(contents.contains("The login page crashes on submit."));
assert!(contents.contains("## How to Reproduce"));
@@ -409,7 +447,15 @@ mod tests {
#[test]
fn create_bug_file_rejects_empty_name() {
let tmp = tempfile::tempdir().unwrap();
let result = create_bug_file(tmp.path(), "!!!", "desc", "steps", "actual", "expected", None);
let result = create_bug_file(
tmp.path(),
"!!!",
"desc",
"steps",
"actual",
"expected",
None,
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("alphanumeric"));
}
@@ -453,11 +499,16 @@ mod tests {
let spike_id =
create_spike_file(tmp.path(), "Filesystem Watcher Architecture", None).unwrap();
assert!(spike_id.ends_with("_spike_filesystem_watcher_architecture"), "expected ID to end with _spike_filesystem_watcher_architecture, got: {spike_id}");
assert!(
spike_id.ends_with("_spike_filesystem_watcher_architecture"),
"expected ID to end with _spike_filesystem_watcher_architecture, got: {spike_id}"
);
let contents = crate::db::read_content(&spike_id)
.or_else(|| {
let filepath = tmp.path().join(format!(".huskies/work/1_backlog/{spike_id}.md"));
let filepath = tmp
.path()
.join(format!(".huskies/work/1_backlog/{spike_id}.md"));
fs::read_to_string(filepath).ok()
})
.expect("spike content should exist");
@@ -466,7 +517,10 @@ mod tests {
contents.starts_with("---\nname: \"Filesystem Watcher Architecture\"\n---"),
"spike file must start with YAML front matter"
);
assert!(contents.contains("Filesystem Watcher Architecture"), "content should mention spike name");
assert!(
contents.contains("Filesystem Watcher Architecture"),
"content should mention spike name"
);
assert!(contents.contains("## Question"));
assert!(contents.contains("## Hypothesis"));
assert!(contents.contains("## Timebox"));
@@ -480,11 +534,14 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let description = "What is the best approach for watching filesystem events?";
let spike_id = create_spike_file(tmp.path(), "FS Watcher Spike", Some(description)).unwrap();
let spike_id =
create_spike_file(tmp.path(), "FS Watcher Spike", Some(description)).unwrap();
let contents = crate::db::read_content(&spike_id)
.or_else(|| {
let filepath = tmp.path().join(format!(".huskies/work/1_backlog/{spike_id}.md"));
let filepath = tmp
.path()
.join(format!(".huskies/work/1_backlog/{spike_id}.md"));
fs::read_to_string(filepath).ok()
})
.expect("spike content should exist");
@@ -498,7 +555,9 @@ mod tests {
let contents = crate::db::read_content(&spike_id)
.or_else(|| {
let filepath = tmp.path().join(format!(".huskies/work/1_backlog/{spike_id}.md"));
let filepath = tmp
.path()
.join(format!(".huskies/work/1_backlog/{spike_id}.md"));
fs::read_to_string(filepath).ok()
})
.expect("spike content should exist");
@@ -544,8 +603,19 @@ mod tests {
);
let spike_id = create_spike_file(tmp.path(), "My Spike", None).unwrap();
assert!(spike_id.ends_with("_spike_my_spike"), "expected ID to end with _spike_my_spike, got: {spike_id}");
let num: u32 = spike_id.chars().take_while(|c| c.is_ascii_digit()).collect::<String>().parse().unwrap();
assert!(num >= 7051, "expected spike number >= 7051, got: {spike_id}");
assert!(
spike_id.ends_with("_spike_my_spike"),
"expected ID to end with _spike_my_spike, got: {spike_id}"
);
let num: u32 = spike_id
.chars()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.parse()
.unwrap();
assert!(
num >= 7051,
"expected spike number >= 7051, got: {spike_id}"
);
}
}
+106 -43
View File
@@ -161,7 +161,6 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
Ok(state)
}
/// Build a map from story_id → AgentAssignment for all pending/running agents.
fn build_active_agent_map(ctx: &AppContext) -> HashMap<String, AgentAssignment> {
let agents = match ctx.agents.list_agents() {
@@ -196,7 +195,6 @@ fn build_active_agent_map(ctx: &AppContext) -> HashMap<String, AgentAssignment>
map
}
pub fn load_upcoming_stories(_ctx: &AppContext) -> Result<Vec<UpcomingStory>, String> {
use crate::pipeline_state::Stage;
@@ -244,9 +242,7 @@ pub fn load_upcoming_stories(_ctx: &AppContext) -> Result<Vec<UpcomingStory>, St
Ok(stories)
}
pub fn validate_story_dirs(
_root: &std::path::Path,
) -> Result<Vec<StoryValidationResult>, String> {
pub fn validate_story_dirs(_root: &std::path::Path) -> Result<Vec<StoryValidationResult>, String> {
use crate::pipeline_state::Stage;
let mut results = Vec::new();
@@ -309,7 +305,12 @@ pub(super) fn read_story_content(_project_root: &Path, story_id: &str) -> Result
}
/// Write story content to the DB content store and CRDT.
pub(super) fn write_story_content(_project_root: &Path, story_id: &str, stage: &str, content: &str) {
pub(super) fn write_story_content(
_project_root: &Path,
story_id: &str,
stage: &str,
content: &str,
) {
crate::db::write_item_with_content(story_id, stage, content);
}
@@ -321,13 +322,16 @@ pub(super) fn story_stage(story_id: &str) -> Option<String> {
.map(|item| item.stage.dir_name().to_string())
}
/// Replace the content of a named `## Section` in a story file.
///
/// Finds the first occurrence of `## {section_name}` and replaces everything
/// until the next `##` heading (or end of file) with the provided text.
/// Returns an error if the section is not found.
pub(super) fn replace_section_content(content: &str, section_name: &str, new_text: &str) -> Result<String, String> {
pub(super) fn replace_section_content(
content: &str,
section_name: &str,
new_text: &str,
) -> Result<String, String> {
let lines: Vec<&str> = content.lines().collect();
let heading = format!("## {section_name}");
@@ -517,18 +521,24 @@ mod tests {
("4_merge", "9840_story_merge"),
("5_done", "9850_story_done"),
] {
crate::db::write_item_with_content(
id,
stage,
&format!("---\nname: {id}\n---\n"),
);
crate::db::write_item_with_content(id, stage, &format!("---\nname: {id}\n---\n"));
}
let ctx = crate::http::context::AppContext::new_test(root);
let state = load_pipeline_state(&ctx).unwrap();
assert!(state.backlog.iter().any(|s| s.story_id == "9810_story_upcoming"));
assert!(state.current.iter().any(|s| s.story_id == "9820_story_current"));
assert!(
state
.backlog
.iter()
.any(|s| s.story_id == "9810_story_upcoming")
);
assert!(
state
.current
.iter()
.any(|s| s.story_id == "9820_story_current")
);
assert!(state.qa.iter().any(|s| s.story_id == "9830_story_qa"));
assert!(state.merge.iter().any(|s| s.story_id == "9840_story_merge"));
assert!(state.done.iter().any(|s| s.story_id == "9850_story_done"));
@@ -558,12 +568,23 @@ mod tests {
);
let ctx = crate::http::context::AppContext::new_test(root);
ctx.agents.inject_test_agent("9860_story_test", "coder-1", crate::agents::AgentStatus::Running);
ctx.agents.inject_test_agent(
"9860_story_test",
"coder-1",
crate::agents::AgentStatus::Running,
);
let state = load_pipeline_state(&ctx).unwrap();
let item = state.current.iter().find(|s| s.story_id == "9860_story_test").unwrap();
assert!(item.agent.is_some(), "running agent should appear on work item");
let item = state
.current
.iter()
.find(|s| s.story_id == "9860_story_test")
.unwrap();
assert!(
item.agent.is_some(),
"running agent should appear on work item"
);
let agent = item.agent.as_ref().unwrap();
assert_eq!(agent.agent_name, "coder-1");
assert_eq!(agent.status, "running");
@@ -582,11 +603,19 @@ mod tests {
);
let ctx = crate::http::context::AppContext::new_test(root);
ctx.agents.inject_test_agent("9861_story_done", "coder-1", crate::agents::AgentStatus::Completed);
ctx.agents.inject_test_agent(
"9861_story_done",
"coder-1",
crate::agents::AgentStatus::Completed,
);
let state = load_pipeline_state(&ctx).unwrap();
let item = state.current.iter().find(|s| s.story_id == "9861_story_done").unwrap();
let item = state
.current
.iter()
.find(|s| s.story_id == "9861_story_done")
.unwrap();
assert!(
item.agent.is_none(),
"completed agent should not appear on work item"
@@ -606,12 +635,23 @@ mod tests {
);
let ctx = crate::http::context::AppContext::new_test(root);
ctx.agents.inject_test_agent("9862_story_pending", "coder-1", crate::agents::AgentStatus::Pending);
ctx.agents.inject_test_agent(
"9862_story_pending",
"coder-1",
crate::agents::AgentStatus::Pending,
);
let state = load_pipeline_state(&ctx).unwrap();
let item = state.current.iter().find(|s| s.story_id == "9862_story_pending").unwrap();
assert!(item.agent.is_some(), "pending agent should appear on work item");
let item = state
.current
.iter()
.find(|s| s.story_id == "9862_story_pending")
.unwrap();
assert!(
item.agent.is_some(),
"pending agent should appear on work item"
);
assert_eq!(item.agent.as_ref().unwrap().status, "pending");
}
@@ -633,10 +673,18 @@ mod tests {
let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
let state = load_pipeline_state(&ctx).unwrap();
let dependent = state.backlog.iter().find(|s| s.story_id == "9863_story_dependent").unwrap();
let dependent = state
.backlog
.iter()
.find(|s| s.story_id == "9863_story_dependent")
.unwrap();
assert_eq!(dependent.depends_on, Some(vec![10, 11]));
let independent = state.backlog.iter().find(|s| s.story_id == "9864_story_independent").unwrap();
let independent = state
.backlog
.iter()
.find(|s| s.story_id == "9864_story_independent")
.unwrap();
assert_eq!(independent.depends_on, None);
}
@@ -657,9 +705,15 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
let stories = load_upcoming_stories(&ctx).unwrap();
let s1 = stories.iter().find(|s| s.story_id == "9870_story_view_upcoming").unwrap();
let s1 = stories
.iter()
.find(|s| s.story_id == "9870_story_view_upcoming")
.unwrap();
assert_eq!(s1.name.as_deref(), Some("View Upcoming"));
let s2 = stories.iter().find(|s| s.story_id == "9871_story_worktree").unwrap();
let s2 = stories
.iter()
.find(|s| s.story_id == "9871_story_worktree")
.unwrap();
assert_eq!(s2.name.as_deref(), Some("Worktree Orchestration"));
}
@@ -696,24 +750,29 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let results = validate_story_dirs(tmp.path()).unwrap();
let r1 = results.iter().find(|r| r.story_id == "9873_story_todos").unwrap();
let r1 = results
.iter()
.find(|r| r.story_id == "9873_story_todos")
.unwrap();
assert!(r1.valid);
let r2 = results.iter().find(|r| r.story_id == "9874_story_front_matter").unwrap();
let r2 = results
.iter()
.find(|r| r.story_id == "9874_story_front_matter")
.unwrap();
assert!(r2.valid);
}
#[test]
fn validate_story_dirs_missing_front_matter() {
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9875_story_no_fm",
"2_current",
"# No front matter\n",
);
crate::db::write_item_with_content("9875_story_no_fm", "2_current", "# No front matter\n");
let tmp = tempfile::tempdir().unwrap();
let results = validate_story_dirs(tmp.path()).unwrap();
let r = results.iter().find(|r| r.story_id == "9875_story_no_fm").unwrap();
let r = results
.iter()
.find(|r| r.story_id == "9875_story_no_fm")
.unwrap();
assert!(!r.valid);
assert_eq!(r.error.as_deref(), Some("Missing front matter"));
}
@@ -729,7 +788,10 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let results = validate_story_dirs(tmp.path()).unwrap();
let r = results.iter().find(|r| r.story_id == "9876_story_no_name").unwrap();
let r = results
.iter()
.find(|r| r.story_id == "9876_story_no_name")
.unwrap();
assert!(!r.valid);
let err = r.error.as_deref().unwrap();
assert!(err.contains("Missing 'name' field"));
@@ -789,11 +851,7 @@ mod tests {
#[test]
fn next_item_number_increments_beyond_existing() {
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9877_story_foo",
"1_backlog",
"---\nname: Foo\n---\n",
);
crate::db::write_item_with_content("9877_story_foo", "1_backlog", "---\nname: Foo\n---\n");
let tmp = tempfile::tempdir().unwrap();
assert!(next_item_number(tmp.path()).unwrap() >= 9878);
}
@@ -824,7 +882,8 @@ mod tests {
#[test]
fn replace_or_append_section_appends_when_absent() {
let contents = "---\nname: T\n---\n# Story\n";
let new = replace_or_append_section(contents, "## Test Results", "## Test Results\n\nfoo\n");
let new =
replace_or_append_section(contents, "## Test Results", "## Test Results\n\nfoo\n");
assert!(new.contains("## Test Results"));
assert!(new.contains("foo"));
assert!(new.contains("# Story"));
@@ -833,7 +892,11 @@ mod tests {
#[test]
fn replace_or_append_section_replaces_existing() {
let contents = "# Story\n\n## Test Results\n\nold content\n\n## Other\n\nother content\n";
let new = replace_or_append_section(contents, "## Test Results", "## Test Results\n\nnew content\n");
let new = replace_or_append_section(
contents,
"## Test Results",
"## Test Results\n\nnew content\n",
);
assert!(new.contains("new content"));
assert!(!new.contains("old content"));
assert!(new.contains("## Other"));
+271 -66
View File
@@ -4,7 +4,10 @@ use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use super::{create_section_content, next_item_number, read_story_content, replace_section_content, slugify_name, story_stage, write_story_content};
use super::{
create_section_content, next_item_number, read_story_content, replace_section_content,
slugify_name, story_stage, write_story_content,
};
/// Shared create-story logic used by both the OpenApi and MCP handlers.
///
@@ -158,9 +161,7 @@ pub fn add_criterion_to_file(
let insert_after = last_criterion_line
.or(ac_section_start)
.ok_or_else(|| {
format!("Story '{story_id}' has no '## Acceptance Criteria' section.")
})?;
.ok_or_else(|| format!("Story '{story_id}' has no '## Acceptance Criteria' section."))?;
let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
new_lines.insert(insert_after + 1, format!("- [ ] {criterion}"));
@@ -195,7 +196,14 @@ fn json_value_to_yaml_scalar(value: &Value) -> String {
}
Value::String(s) => yaml_encode_str(s),
// Null and Object are not meaningful as YAML scalars; store as quoted strings.
other => format!("\"{}\"", other.to_string().replace('"', "\\\"").replace('\n', " ").replace('\r', "")),
other => format!(
"\"{}\"",
other
.to_string()
.replace('"', "\\\"")
.replace('\n', " ")
.replace('\r', "")
),
}
}
@@ -211,7 +219,10 @@ fn yaml_encode_str(s: &str) -> 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', "")),
s => format!(
"\"{}\"",
s.replace('"', "\\\"").replace('\n', " ").replace('\r', "")
),
}
}
@@ -246,13 +257,17 @@ pub fn update_story_in_file(
if let Some(us) = user_story {
contents = match replace_section_content(&contents, "User Story", us) {
Ok(updated) => updated,
Err(_) => create_section_content(&contents, "User Story", us, Some("Acceptance Criteria")),
Err(_) => {
create_section_content(&contents, "User Story", us, Some("Acceptance Criteria"))
}
};
}
if let Some(desc) = description {
contents = match replace_section_content(&contents, "Description", desc) {
Ok(updated) => updated,
Err(_) => create_section_content(&contents, "Description", desc, Some("Acceptance Criteria")),
Err(_) => {
create_section_content(&contents, "Description", desc, Some("Acceptance Criteria"))
}
};
}
@@ -322,7 +337,11 @@ mod tests {
fs::create_dir_all(&backlog).unwrap();
fs::write(backlog.join("36_story_existing.md"), "").unwrap();
// Also write to content store so next_item_number sees it.
crate::db::write_item_with_content("36_story_existing", "1_backlog", "---\nname: Existing\n---\n");
crate::db::write_item_with_content(
"36_story_existing",
"1_backlog",
"---\nname: Existing\n---\n",
);
let number = super::super::next_item_number(tmp.path()).unwrap();
// The number must be >= 37 (at least higher than the existing "36_story_existing.md"),
@@ -390,9 +409,18 @@ mod tests {
// Read the updated content.
let contents = read_story_content(tmp.path(), "1_test").unwrap();
assert!(contents.contains("- [x] Criterion 0"), "first should be checked");
assert!(contents.contains("- [ ] Criterion 1"), "second should stay unchecked");
assert!(contents.contains("- [ ] Criterion 2"), "third should stay unchecked");
assert!(
contents.contains("- [x] Criterion 0"),
"first should be checked"
);
assert!(
contents.contains("- [ ] Criterion 1"),
"second should stay unchecked"
);
assert!(
contents.contains("- [ ] Criterion 2"),
"third should stay unchecked"
);
}
#[test]
@@ -404,9 +432,18 @@ mod tests {
check_criterion_in_file(tmp.path(), "2_test", 1).unwrap();
let contents = read_story_content(tmp.path(), "2_test").unwrap();
assert!(contents.contains("- [ ] Criterion 0"), "first should stay unchecked");
assert!(contents.contains("- [x] Criterion 1"), "second should be checked");
assert!(contents.contains("- [ ] Criterion 2"), "third should stay unchecked");
assert!(
contents.contains("- [ ] Criterion 0"),
"first should stay unchecked"
);
assert!(
contents.contains("- [x] Criterion 1"),
"second should be checked"
);
assert!(
contents.contains("- [ ] Criterion 2"),
"third should stay unchecked"
);
}
#[test]
@@ -423,7 +460,9 @@ mod tests {
// ── add_criterion_to_file tests ───────────────────────────────────────────
fn story_with_ac_section(criteria: &[&str]) -> String {
let mut s = "---\nname: Test\n---\n\n## User Story\n\nAs a user...\n\n## Acceptance Criteria\n\n".to_string();
let mut s =
"---\nname: Test\n---\n\n## User Story\n\nAs a user...\n\n## Acceptance Criteria\n\n"
.to_string();
for c in criteria {
s.push_str(&format!("- [ ] {c}\n"));
}
@@ -434,7 +473,11 @@ mod tests {
#[test]
fn add_criterion_appends_after_last_criterion() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(tmp.path(), "10_test", &story_with_ac_section(&["First", "Second"]));
setup_story_in_fs(
tmp.path(),
"10_test",
&story_with_ac_section(&["First", "Second"]),
);
add_criterion_to_file(tmp.path(), "10_test", "Third").unwrap();
@@ -450,19 +493,27 @@ mod tests {
#[test]
fn add_criterion_to_empty_section() {
let tmp = tempfile::tempdir().unwrap();
let content = "---\nname: Test\n---\n\n## Acceptance Criteria\n\n## Out of Scope\n\n- N/A\n";
let content =
"---\nname: Test\n---\n\n## Acceptance Criteria\n\n## Out of Scope\n\n- N/A\n";
setup_story_in_fs(tmp.path(), "11_test", content);
add_criterion_to_file(tmp.path(), "11_test", "New AC").unwrap();
let contents = read_story_content(tmp.path(), "11_test").unwrap();
assert!(contents.contains("- [ ] New AC\n"), "criterion should be present");
assert!(
contents.contains("- [ ] New AC\n"),
"criterion should be present"
);
}
#[test]
fn add_criterion_missing_section_returns_error() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(tmp.path(), "12_test", "---\nname: Test\n---\n\nNo AC section here.\n");
setup_story_in_fs(
tmp.path(),
"12_test",
"---\nname: Test\n---\n\nNo AC section here.\n",
);
let result = add_criterion_to_file(tmp.path(), "12_test", "X");
assert!(result.is_err());
@@ -477,12 +528,25 @@ mod tests {
let content = "---\nname: T\n---\n\n## User Story\n\nOld text\n\n## Acceptance Criteria\n\n- [ ] AC\n";
setup_story_in_fs(tmp.path(), "20_test", content);
update_story_in_file(tmp.path(), "20_test", Some("New user story text"), None, None).unwrap();
update_story_in_file(
tmp.path(),
"20_test",
Some("New user story text"),
None,
None,
)
.unwrap();
let result = read_story_content(tmp.path(), "20_test").unwrap();
assert!(result.contains("New user story text"), "new text should be present");
assert!(
result.contains("New user story text"),
"new text should be present"
);
assert!(!result.contains("Old text"), "old text should be replaced");
assert!(result.contains("## Acceptance Criteria"), "other sections preserved");
assert!(
result.contains("## Acceptance Criteria"),
"other sections preserved"
);
}
#[test]
@@ -494,8 +558,14 @@ mod tests {
update_story_in_file(tmp.path(), "21_test", None, Some("New description"), None).unwrap();
let result = read_story_content(tmp.path(), "21_test").unwrap();
assert!(result.contains("New description"), "new description present");
assert!(!result.contains("Old description"), "old description replaced");
assert!(
result.contains("New description"),
"new description present"
);
assert!(
!result.contains("Old description"),
"old description replaced"
);
}
#[test]
@@ -515,16 +585,26 @@ mod tests {
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 user story"), None, None);
assert!(result.is_ok(), "should succeed when section is missing: {result:?}");
let result =
update_story_in_file(tmp.path(), "23_test", Some("New user story"), None, None);
assert!(
result.is_ok(),
"should succeed when section is missing: {result:?}"
);
let updated = read_story_content(tmp.path(), "23_test").unwrap();
assert!(updated.contains("## User Story"), "section should be created");
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");
assert!(
pos_us < pos_ac,
"User Story should be before Acceptance Criteria"
);
}
#[test]
@@ -534,16 +614,34 @@ mod tests {
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 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");
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");
assert!(
pos_desc < pos_ac,
"Description should be before Acceptance Criteria"
);
}
#[test]
@@ -553,32 +651,58 @@ mod tests {
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 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");
assert!(
updated.contains("## Description"),
"section should be created"
);
assert!(
updated.contains("Appended description"),
"text should be present"
);
}
#[test]
fn update_story_sets_agent_front_matter_field() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(tmp.path(), "24_test", "---\nname: T\n---\n\n## User Story\n\nSome story\n");
setup_story_in_fs(
tmp.path(),
"24_test",
"---\nname: T\n---\n\n## User Story\n\nSome story\n",
);
let mut fields = HashMap::new();
fields.insert("agent".to_string(), Value::String("dev".to_string()));
update_story_in_file(tmp.path(), "24_test", None, None, Some(&fields)).unwrap();
let result = read_story_content(tmp.path(), "24_test").unwrap();
assert!(result.contains("agent: \"dev\""), "agent field should be set");
assert!(
result.contains("agent: \"dev\""),
"agent field should be set"
);
assert!(result.contains("name: T"), "name field preserved");
}
#[test]
fn update_story_sets_arbitrary_front_matter_fields() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(tmp.path(), "25_test", "---\nname: T\n---\n\n## User Story\n\nSome story\n");
setup_story_in_fs(
tmp.path(),
"25_test",
"---\nname: T\n---\n\n## User Story\n\nSome story\n",
);
let mut fields = HashMap::new();
fields.insert("qa".to_string(), Value::String("human".to_string()));
@@ -587,19 +711,29 @@ mod tests {
let result = read_story_content(tmp.path(), "25_test").unwrap();
assert!(result.contains("qa: \"human\""), "qa field should be set");
assert!(result.contains("priority: \"high\""), "priority field should be set");
assert!(
result.contains("priority: \"high\""),
"priority field should be set"
);
assert!(result.contains("name: T"), "name field preserved");
}
#[test]
fn update_story_front_matter_only_no_section_required() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(tmp.path(), "26_test", "---\nname: T\n---\n\nNo sections here.\n");
setup_story_in_fs(
tmp.path(),
"26_test",
"---\nname: T\n---\n\nNo sections here.\n",
);
let mut fields = HashMap::new();
fields.insert("agent".to_string(), Value::String("dev".to_string()));
let result = update_story_in_file(tmp.path(), "26_test", None, None, Some(&fields));
assert!(result.is_ok(), "front-matter-only update should not require body sections");
assert!(
result.is_ok(),
"front-matter-only update should not require body sections"
);
let contents = read_story_content(tmp.path(), "26_test").unwrap();
assert!(contents.contains("agent: \"dev\""));
@@ -616,8 +750,14 @@ mod tests {
update_story_in_file(tmp.path(), "27_test", None, None, Some(&fields)).unwrap();
let result = read_story_content(tmp.path(), "27_test").unwrap();
assert!(result.contains("blocked: false"), "bool should be unquoted: {result}");
assert!(!result.contains("blocked: \"false\""), "bool must not be quoted: {result}");
assert!(
result.contains("blocked: false"),
"bool should be unquoted: {result}"
);
assert!(
!result.contains("blocked: \"false\""),
"bool must not be quoted: {result}"
);
}
#[test]
@@ -631,14 +771,24 @@ mod tests {
update_story_in_file(tmp.path(), "28_test", None, None, Some(&fields)).unwrap();
let result = read_story_content(tmp.path(), "28_test").unwrap();
assert!(result.contains("retry_count: 0"), "integer should be unquoted: {result}");
assert!(!result.contains("retry_count: \"0\""), "integer must not be quoted: {result}");
assert!(
result.contains("retry_count: 0"),
"integer should be unquoted: {result}"
);
assert!(
!result.contains("retry_count: \"0\""),
"integer must not be quoted: {result}"
);
}
#[test]
fn update_story_bool_front_matter_parseable_after_write() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(tmp.path(), "29_test", "---\nname: My Story\n---\n\nNo sections.\n");
setup_story_in_fs(
tmp.path(),
"29_test",
"---\nname: My Story\n---\n\nNo sections.\n",
);
let mut fields = HashMap::new();
fields.insert("blocked".to_string(), Value::String("false".to_string()));
@@ -646,7 +796,11 @@ mod tests {
let contents = read_story_content(tmp.path(), "29_test").unwrap();
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");
assert_eq!(
meta.name.as_deref(),
Some("My Story"),
"name preserved after writing bool field"
);
}
// ── Bug 493 regression tests ──────────────────────────────────────────────
@@ -662,8 +816,14 @@ mod tests {
update_story_in_file(tmp.path(), "30_test", None, None, Some(&fields)).unwrap();
let result = read_story_content(tmp.path(), "30_test").unwrap();
assert!(result.contains("depends_on: [490]"), "should be unquoted array: {result}");
assert!(!result.contains("depends_on: \"[490]\""), "must not be quoted: {result}");
assert!(
result.contains("depends_on: [490]"),
"should be unquoted array: {result}"
);
assert!(
!result.contains("depends_on: \"[490]\""),
"must not be quoted: {result}"
);
let meta = parse_front_matter(&result).expect("front matter should parse");
assert_eq!(meta.depends_on, Some(vec![490]));
@@ -690,8 +850,14 @@ mod tests {
})
.expect("story content should exist");
assert!(contents.contains("depends_on: [489]"), "missing front matter: {contents}");
assert!(!contents.contains("- [ ] depends_on"), "must not appear as checkbox: {contents}");
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]));
@@ -709,8 +875,14 @@ mod tests {
update_story_in_file(tmp.path(), "31_test", None, None, Some(&fields)).unwrap();
let result = read_story_content(tmp.path(), "31_test").unwrap();
assert!(result.contains("blocked: false"), "native bool false should be unquoted: {result}");
assert!(!result.contains("blocked: \"false\""), "must not be quoted: {result}");
assert!(
result.contains("blocked: false"),
"native bool false should be unquoted: {result}"
);
assert!(
!result.contains("blocked: \"false\""),
"must not be quoted: {result}"
);
}
#[test]
@@ -723,22 +895,38 @@ mod tests {
update_story_in_file(tmp.path(), "32_test", None, None, Some(&fields)).unwrap();
let result = read_story_content(tmp.path(), "32_test").unwrap();
assert!(result.contains("blocked: true"), "native bool true should be unquoted: {result}");
assert!(!result.contains("blocked: \"true\""), "must not be quoted: {result}");
assert!(
result.contains("blocked: true"),
"native bool true should be unquoted: {result}"
);
assert!(
!result.contains("blocked: \"true\""),
"must not be quoted: {result}"
);
}
#[test]
fn update_story_native_integer_written_unquoted() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(tmp.path(), "33b_test", "---\nname: T\n---\n\nNo sections.\n");
setup_story_in_fs(
tmp.path(),
"33b_test",
"---\nname: T\n---\n\nNo sections.\n",
);
let mut fields = HashMap::new();
fields.insert("retry_count".to_string(), serde_json::json!(3));
update_story_in_file(tmp.path(), "33b_test", None, None, Some(&fields)).unwrap();
let result = read_story_content(tmp.path(), "33b_test").unwrap();
assert!(result.contains("retry_count: 3"), "native integer should be unquoted: {result}");
assert!(!result.contains("retry_count: \"3\""), "must not be quoted: {result}");
assert!(
result.contains("retry_count: 3"),
"native integer should be unquoted: {result}"
);
assert!(
!result.contains("retry_count: \"3\""),
"must not be quoted: {result}"
);
}
#[test]
@@ -751,8 +939,14 @@ mod tests {
update_story_in_file(tmp.path(), "34_test", None, None, Some(&fields)).unwrap();
let result = read_story_content(tmp.path(), "34_test").unwrap();
assert!(result.contains("depends_on: [490, 491]"), "native array should be YAML sequence: {result}");
assert!(!result.contains("depends_on: \"["), "must not be quoted: {result}");
assert!(
result.contains("depends_on: [490, 491]"),
"native array should be YAML sequence: {result}"
);
assert!(
!result.contains("depends_on: \"["),
"must not be quoted: {result}"
);
let meta = parse_front_matter(&result).expect("front matter should parse");
assert_eq!(meta.depends_on, Some(vec![490, 491]));
@@ -761,7 +955,11 @@ mod tests {
#[test]
fn update_story_native_bool_parseable_after_write() {
let tmp = tempfile::tempdir().unwrap();
setup_story_in_fs(tmp.path(), "35_test", "---\nname: My Story\n---\n\nNo sections.\n");
setup_story_in_fs(
tmp.path(),
"35_test",
"---\nname: My Story\n---\n\nNo sections.\n",
);
let mut fields = HashMap::new();
fields.insert("blocked".to_string(), Value::Bool(false));
@@ -769,7 +967,11 @@ mod tests {
let contents = read_story_content(tmp.path(), "35_test").unwrap();
let meta = parse_front_matter(&contents).expect("front matter should parse");
assert_eq!(meta.name.as_deref(), Some("My Story"), "name preserved after writing native bool");
assert_eq!(
meta.name.as_deref(),
Some("My Story"),
"name preserved after writing native bool"
);
}
#[test]
@@ -779,7 +981,10 @@ mod tests {
// String "[490, 491]" still works (backwards compatibility).
let mut fields = HashMap::new();
fields.insert("depends_on".to_string(), Value::String("[490, 491]".to_string()));
fields.insert(
"depends_on".to_string(),
Value::String("[490, 491]".to_string()),
);
update_story_in_file(tmp.path(), "31_test", None, None, Some(&fields)).unwrap();
let result = read_story_content(tmp.path(), "31_test").unwrap();
+5 -1
View File
@@ -56,7 +56,11 @@ pub fn write_coverage_baseline_to_story_file(
Err(_) => return Ok(()), // No story — skip silently
};
let updated = set_front_matter_field(&contents, "coverage_baseline", &format!("{coverage_pct:.1}%"));
let updated = set_front_matter_field(
&contents,
"coverage_baseline",
&format!("{coverage_pct:.1}%"),
);
let stage = story_stage(story_id).unwrap_or_else(|| "2_current".to_string());
write_story_content(project_root, story_id, &stage, &updated);