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:
@@ -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
@@ -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"));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user