story-kit: merge 287_story_rename_upcoming_pipeline_stage_to_backlog

This commit is contained in:
Dave
2026-03-18 14:31:12 +00:00
parent 967ebd7a84
commit df6f792214
26 changed files with 250 additions and 228 deletions

View File

@@ -339,7 +339,7 @@ impl AgentsApi {
.map_err(bad_request)?;
let stages = [
("1_upcoming", "upcoming"),
("1_backlog", "backlog"),
("2_current", "current"),
("3_qa", "qa"),
("4_merge", "merge"),
@@ -809,12 +809,12 @@ allowed_tools = ["Read", "Bash"]
}
#[tokio::test]
async fn get_work_item_content_returns_content_from_upcoming() {
async fn get_work_item_content_returns_content_from_backlog() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_stage_dir(root, "1_upcoming");
make_stage_dir(root, "1_backlog");
std::fs::write(
root.join(".story_kit/work/1_upcoming/42_story_foo.md"),
root.join(".story_kit/work/1_backlog/42_story_foo.md"),
"---\nname: \"Foo Story\"\n---\n\n# Story 42: Foo Story\n\nSome content.",
)
.unwrap();
@@ -828,7 +828,7 @@ allowed_tools = ["Read", "Bash"]
.unwrap()
.0;
assert!(result.content.contains("Some content."));
assert_eq!(result.stage, "upcoming");
assert_eq!(result.stage, "backlog");
assert_eq!(result.name, Some("Foo Story".to_string()));
}
@@ -1113,7 +1113,7 @@ allowed_tools = ["Read", "Bash"]
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
// Create work dirs including 2_current for the story file.
for stage in &["1_upcoming", "2_current", "5_done", "6_archived"] {
for stage in &["1_backlog", "2_current", "5_done", "6_archived"] {
std::fs::create_dir_all(root.join(".story_kit").join("work").join(stage)).unwrap();
}

View File

@@ -672,7 +672,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
},
{
"name": "create_spike",
"description": "Create a spike file in .story_kit/work/1_upcoming/ with a deterministic filename and YAML front matter. Returns the spike_id.",
"description": "Create a spike file in .story_kit/work/1_backlog/ with a deterministic filename and YAML front matter. Returns the spike_id.",
"inputSchema": {
"type": "object",
"properties": {
@@ -690,7 +690,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
},
{
"name": "create_bug",
"description": "Create a bug file in work/1_upcoming/ with a deterministic filename and auto-commit to master. Returns the bug_id.",
"description": "Create a bug file in work/1_backlog/ with a deterministic filename and auto-commit to master. Returns the bug_id.",
"inputSchema": {
"type": "object",
"properties": {
@@ -725,7 +725,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
},
{
"name": "list_bugs",
"description": "List all open bugs in work/1_upcoming/ matching the _bug_ naming convention.",
"description": "List all open bugs in work/1_backlog/ matching the _bug_ naming convention.",
"inputSchema": {
"type": "object",
"properties": {}
@@ -733,7 +733,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
},
{
"name": "create_refactor",
"description": "Create a refactor work item in work/1_upcoming/ with a deterministic filename and YAML front matter. Returns the refactor_id.",
"description": "Create a refactor work item in work/1_backlog/ with a deterministic filename and YAML front matter. Returns the refactor_id.",
"inputSchema": {
"type": "object",
"properties": {
@@ -756,7 +756,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
},
{
"name": "list_refactors",
"description": "List all open refactors in work/1_upcoming/ matching the _refactor_ naming convention.",
"description": "List all open refactors in work/1_backlog/ matching the _refactor_ naming convention.",
"inputSchema": {
"type": "object",
"properties": {}
@@ -764,7 +764,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
},
{
"name": "close_bug",
"description": "Archive a bug from work/2_current/ or work/1_upcoming/ to work/5_done/ and auto-commit to master.",
"description": "Archive a bug from work/2_current/ or work/1_backlog/ to work/5_done/ and auto-commit to master.",
"inputSchema": {
"type": "object",
"properties": {
@@ -1022,7 +1022,7 @@ fn tool_create_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
.get("acceptance_criteria")
.and_then(|v| serde_json::from_value(v.clone()).ok());
// Spike 61: write the file only — the filesystem watcher detects the new
// .md file in work/1_upcoming/ and auto-commits with a deterministic message.
// .md file in work/1_backlog/ and auto-commits with a deterministic message.
let commit = false;
let root = ctx.state.get_project_root()?;
@@ -1091,16 +1091,16 @@ fn tool_get_pipeline_status(ctx: &AppContext) -> Result<String, String> {
active.extend(map_items(&state.merge, "merge"));
active.extend(map_items(&state.done, "done"));
let upcoming: Vec<Value> = state
.upcoming
let backlog: Vec<Value> = state
.backlog
.iter()
.map(|s| json!({ "story_id": s.story_id, "name": s.name }))
.collect();
serde_json::to_string_pretty(&json!({
"active": active,
"upcoming": upcoming,
"upcoming_count": upcoming.len(),
"backlog": backlog,
"backlog_count": backlog.len(),
}))
.map_err(|e| format!("Serialization error: {e}"))
}
@@ -2452,7 +2452,7 @@ mod tests {
let root = tmp.path();
for (stage, id, name) in &[
("1_upcoming", "10_story_upcoming", "Upcoming Story"),
("1_backlog", "10_story_upcoming", "Upcoming Story"),
("2_current", "20_story_current", "Current Story"),
("3_qa", "30_story_qa", "QA Story"),
("4_merge", "40_story_merge", "Merge Story"),
@@ -2481,11 +2481,11 @@ mod tests {
assert!(stages.contains(&"merge"));
assert!(stages.contains(&"done"));
// Upcoming backlog
let upcoming = parsed["upcoming"].as_array().unwrap();
assert_eq!(upcoming.len(), 1);
assert_eq!(upcoming[0]["story_id"], "10_story_upcoming");
assert_eq!(parsed["upcoming_count"], 1);
// Backlog
let backlog = parsed["backlog"].as_array().unwrap();
assert_eq!(backlog.len(), 1);
assert_eq!(backlog[0]["story_id"], "10_story_upcoming");
assert_eq!(parsed["backlog_count"], 1);
}
#[test]
@@ -2801,8 +2801,8 @@ mod tests {
let t = tool.unwrap();
let desc = t["description"].as_str().unwrap();
assert!(
desc.contains("work/1_upcoming/"),
"create_bug description should reference work/1_upcoming/, got: {desc}"
desc.contains("work/1_backlog/"),
"create_bug description should reference work/1_backlog/, got: {desc}"
);
assert!(
!desc.contains(".story_kit/bugs"),
@@ -2826,8 +2826,8 @@ mod tests {
let t = tool.unwrap();
let desc = t["description"].as_str().unwrap();
assert!(
desc.contains("work/1_upcoming/"),
"list_bugs description should reference work/1_upcoming/, got: {desc}"
desc.contains("work/1_backlog/"),
"list_bugs description should reference work/1_backlog/, got: {desc}"
);
assert!(
!desc.contains(".story_kit/bugs"),
@@ -2911,7 +2911,7 @@ mod tests {
assert!(result.contains("1_bug_login_crash"));
let bug_file = tmp
.path()
.join(".story_kit/work/1_upcoming/1_bug_login_crash.md");
.join(".story_kit/work/1_backlog/1_bug_login_crash.md");
assert!(bug_file.exists());
}
@@ -2927,15 +2927,15 @@ mod tests {
#[test]
fn tool_list_bugs_returns_open_bugs() {
let tmp = tempfile::tempdir().unwrap();
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming");
std::fs::create_dir_all(&upcoming_dir).unwrap();
let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
std::fs::create_dir_all(&backlog_dir).unwrap();
std::fs::write(
upcoming_dir.join("1_bug_crash.md"),
backlog_dir.join("1_bug_crash.md"),
"# Bug 1: App Crash\n",
)
.unwrap();
std::fs::write(
upcoming_dir.join("2_bug_typo.md"),
backlog_dir.join("2_bug_typo.md"),
"# Bug 2: Typo in Header\n",
)
.unwrap();
@@ -2963,9 +2963,9 @@ mod tests {
fn tool_close_bug_moves_to_archive() {
let tmp = tempfile::tempdir().unwrap();
setup_git_repo_in(tmp.path());
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming");
std::fs::create_dir_all(&upcoming_dir).unwrap();
let bug_file = upcoming_dir.join("1_bug_crash.md");
let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
std::fs::create_dir_all(&backlog_dir).unwrap();
let bug_file = backlog_dir.join("1_bug_crash.md");
std::fs::write(&bug_file, "# Bug 1: Crash\n").unwrap();
// Stage the file so it's tracked
std::process::Command::new("git")
@@ -3035,7 +3035,7 @@ mod tests {
assert!(result.contains("1_spike_compare_encoders"));
let spike_file = tmp
.path()
.join(".story_kit/work/1_upcoming/1_spike_compare_encoders.md");
.join(".story_kit/work/1_backlog/1_spike_compare_encoders.md");
assert!(spike_file.exists());
let contents = std::fs::read_to_string(&spike_file).unwrap();
assert!(contents.starts_with("---\nname: \"Compare Encoders\"\n---"));
@@ -3050,7 +3050,7 @@ mod tests {
let result = tool_create_spike(&json!({"name": "My Spike"}), &ctx).unwrap();
assert!(result.contains("1_spike_my_spike"));
let spike_file = tmp.path().join(".story_kit/work/1_upcoming/1_spike_my_spike.md");
let spike_file = tmp.path().join(".story_kit/work/1_backlog/1_spike_my_spike.md");
assert!(spike_file.exists());
let contents = std::fs::read_to_string(&spike_file).unwrap();
assert!(contents.starts_with("---\nname: \"My Spike\"\n---"));

View File

@@ -35,7 +35,7 @@ pub struct StoryValidationResult {
/// Full pipeline state across all stages.
#[derive(Clone, Debug, Serialize)]
pub struct PipelineState {
pub upcoming: Vec<UpcomingStory>,
pub backlog: Vec<UpcomingStory>,
pub current: Vec<UpcomingStory>,
pub qa: Vec<UpcomingStory>,
pub merge: Vec<UpcomingStory>,
@@ -46,7 +46,7 @@ pub struct PipelineState {
pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
let agent_map = build_active_agent_map(ctx);
Ok(PipelineState {
upcoming: load_stage_items(ctx, "1_upcoming", &HashMap::new())?,
backlog: load_stage_items(ctx, "1_backlog", &HashMap::new())?,
current: load_stage_items(ctx, "2_current", &agent_map)?,
qa: load_stage_items(ctx, "3_qa", &agent_map)?,
merge: load_stage_items(ctx, "4_merge", &agent_map)?,
@@ -130,7 +130,7 @@ fn load_stage_items(
}
pub fn load_upcoming_stories(ctx: &AppContext) -> Result<Vec<UpcomingStory>, String> {
load_stage_items(ctx, "1_upcoming", &HashMap::new())
load_stage_items(ctx, "1_backlog", &HashMap::new())
}
/// Shared create-story logic used by both the OpenApi and MCP handlers.
@@ -152,11 +152,11 @@ pub fn create_story_file(
}
let filename = format!("{story_number}_story_{slug}.md");
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
fs::create_dir_all(&upcoming_dir)
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?;
let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
fs::create_dir_all(&backlog_dir)
.map_err(|e| format!("Failed to create backlog directory: {e}"))?;
let filepath = upcoming_dir.join(&filename);
let filepath = backlog_dir.join(&filename);
if filepath.exists() {
return Err(format!("Story file already exists: {filename}"));
}
@@ -206,7 +206,7 @@ pub fn create_story_file(
// ── Bug file helpers ──────────────────────────────────────────────
/// Create a bug file in `work/1_upcoming/` with a deterministic filename and auto-commit.
/// Create a bug file in `work/1_backlog/` with a deterministic filename and auto-commit.
///
/// Returns the bug_id (e.g. `"4_bug_login_crash"`).
pub fn create_bug_file(
@@ -226,9 +226,9 @@ pub fn create_bug_file(
}
let filename = format!("{bug_number}_bug_{slug}.md");
let bugs_dir = root.join(".story_kit").join("work").join("1_upcoming");
let bugs_dir = root.join(".story_kit").join("work").join("1_backlog");
fs::create_dir_all(&bugs_dir)
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?;
.map_err(|e| format!("Failed to create backlog directory: {e}"))?;
let filepath = bugs_dir.join(&filename);
if filepath.exists() {
@@ -276,7 +276,7 @@ pub fn create_bug_file(
// ── Spike file helpers ────────────────────────────────────────────
/// Create a spike file in `work/1_upcoming/` with a deterministic filename.
/// Create a spike file in `work/1_backlog/` with a deterministic filename.
///
/// Returns the spike_id (e.g. `"4_spike_filesystem_watcher_architecture"`).
pub fn create_spike_file(
@@ -292,11 +292,11 @@ pub fn create_spike_file(
}
let filename = format!("{spike_number}_spike_{slug}.md");
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
fs::create_dir_all(&upcoming_dir)
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?;
let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
fs::create_dir_all(&backlog_dir)
.map_err(|e| format!("Failed to create backlog directory: {e}"))?;
let filepath = upcoming_dir.join(&filename);
let filepath = backlog_dir.join(&filename);
if filepath.exists() {
return Err(format!("Spike file already exists: {filename}"));
}
@@ -338,7 +338,7 @@ pub fn create_spike_file(
Ok(spike_id)
}
/// Create a refactor work item file in `work/1_upcoming/`.
/// Create a refactor work item file in `work/1_backlog/`.
///
/// Returns the refactor_id (e.g. `"5_refactor_split_agents_rs"`).
pub fn create_refactor_file(
@@ -355,11 +355,11 @@ pub fn create_refactor_file(
}
let filename = format!("{refactor_number}_refactor_{slug}.md");
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
fs::create_dir_all(&upcoming_dir)
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?;
let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
fs::create_dir_all(&backlog_dir)
.map_err(|e| format!("Failed to create backlog directory: {e}"))?;
let filepath = upcoming_dir.join(&filename);
let filepath = backlog_dir.join(&filename);
if filepath.exists() {
return Err(format!("Refactor file already exists: {filename}"));
}
@@ -427,18 +427,18 @@ fn extract_bug_name(path: &Path) -> Option<String> {
None
}
/// List all open bugs — files in `work/1_upcoming/` matching the `_bug_` naming pattern.
/// List all open bugs — files in `work/1_backlog/` matching the `_bug_` naming pattern.
///
/// Returns a sorted list of `(bug_id, name)` pairs.
pub fn list_bug_files(root: &Path) -> Result<Vec<(String, String)>, String> {
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
if !upcoming_dir.exists() {
let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
if !backlog_dir.exists() {
return Ok(Vec::new());
}
let mut bugs = Vec::new();
for entry in
fs::read_dir(&upcoming_dir).map_err(|e| format!("Failed to read upcoming directory: {e}"))?
fs::read_dir(&backlog_dir).map_err(|e| format!("Failed to read backlog directory: {e}"))?
{
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
let path = entry.path();
@@ -477,18 +477,18 @@ fn is_refactor_item(stem: &str) -> bool {
after_num.starts_with("_refactor_")
}
/// List all open refactors — files in `work/1_upcoming/` matching the `_refactor_` naming pattern.
/// List all open refactors — files in `work/1_backlog/` matching the `_refactor_` naming pattern.
///
/// Returns a sorted list of `(refactor_id, name)` pairs.
pub fn list_refactor_files(root: &Path) -> Result<Vec<(String, String)>, String> {
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
if !upcoming_dir.exists() {
let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
if !backlog_dir.exists() {
return Ok(Vec::new());
}
let mut refactors = Vec::new();
for entry in fs::read_dir(&upcoming_dir)
.map_err(|e| format!("Failed to read upcoming directory: {e}"))?
for entry in fs::read_dir(&backlog_dir)
.map_err(|e| format!("Failed to read backlog directory: {e}"))?
{
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
let path = entry.path();
@@ -525,11 +525,11 @@ pub fn list_refactor_files(root: &Path) -> Result<Vec<(String, String)>, String>
/// Locate a work item file by searching all active pipeline stages.
///
/// Searches in priority order: 2_current, 1_upcoming, 3_qa, 4_merge, 5_done, 6_archived.
/// Searches in priority order: 2_current, 1_backlog, 3_qa, 4_merge, 5_done, 6_archived.
fn find_story_file(project_root: &Path, story_id: &str) -> Result<PathBuf, String> {
let filename = format!("{story_id}.md");
let sk = project_root.join(".story_kit").join("work");
for stage in &["2_current", "1_upcoming", "3_qa", "4_merge", "5_done", "6_archived"] {
for stage in &["2_current", "1_backlog", "3_qa", "4_merge", "5_done", "6_archived"] {
let path = sk.join(stage).join(&filename);
if path.exists() {
return Ok(path);
@@ -778,7 +778,7 @@ fn next_item_number(root: &std::path::Path) -> Result<u32, String> {
let work_base = root.join(".story_kit").join("work");
let mut max_num: u32 = 0;
for subdir in &["1_upcoming", "2_current", "3_qa", "4_merge", "5_done", "6_archived"] {
for subdir in &["1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived"] {
let dir = work_base.join(subdir);
if !dir.exists() {
continue;
@@ -973,10 +973,10 @@ pub fn validate_story_dirs(
) -> Result<Vec<StoryValidationResult>, String> {
let mut results = Vec::new();
// Directories to validate: work/2_current/ + work/1_upcoming/
// Directories to validate: work/2_current/ + work/1_backlog/
let dirs_to_validate: Vec<PathBuf> = vec![
root.join(".story_kit").join("work").join("2_current"),
root.join(".story_kit").join("work").join("1_upcoming"),
root.join(".story_kit").join("work").join("1_backlog"),
];
for dir in &dirs_to_validate {
@@ -1042,7 +1042,7 @@ mod tests {
let root = tmp.path().to_path_buf();
for (stage, id) in &[
("1_upcoming", "10_story_upcoming"),
("1_backlog", "10_story_upcoming"),
("2_current", "20_story_current"),
("3_qa", "30_story_qa"),
("4_merge", "40_story_merge"),
@@ -1060,8 +1060,8 @@ mod tests {
let ctx = crate::http::context::AppContext::new_test(root);
let state = load_pipeline_state(&ctx).unwrap();
assert_eq!(state.upcoming.len(), 1);
assert_eq!(state.upcoming[0].story_id, "10_story_upcoming");
assert_eq!(state.backlog.len(), 1);
assert_eq!(state.backlog[0].story_id, "10_story_upcoming");
assert_eq!(state.current.len(), 1);
assert_eq!(state.current[0].story_id, "20_story_current");
@@ -1164,15 +1164,15 @@ mod tests {
#[test]
fn load_upcoming_parses_metadata() {
let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
fs::create_dir_all(&upcoming).unwrap();
let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&backlog).unwrap();
fs::write(
upcoming.join("31_story_view_upcoming.md"),
backlog.join("31_story_view_upcoming.md"),
"---\nname: View Upcoming\n---\n# Story\n",
)
.unwrap();
fs::write(
upcoming.join("32_story_worktree.md"),
backlog.join("32_story_worktree.md"),
"---\nname: Worktree Orchestration\n---\n# Story\n",
)
.unwrap();
@@ -1189,11 +1189,11 @@ mod tests {
#[test]
fn load_upcoming_skips_non_md_files() {
let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
fs::create_dir_all(&upcoming).unwrap();
fs::write(upcoming.join(".gitkeep"), "").unwrap();
let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&backlog).unwrap();
fs::write(backlog.join(".gitkeep"), "").unwrap();
fs::write(
upcoming.join("31_story_example.md"),
backlog.join("31_story_example.md"),
"---\nname: A Story\n---\n",
)
.unwrap();
@@ -1208,16 +1208,16 @@ mod tests {
fn validate_story_dirs_valid_files() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".story_kit/work/2_current");
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&current).unwrap();
fs::create_dir_all(&upcoming).unwrap();
fs::create_dir_all(&backlog).unwrap();
fs::write(
current.join("28_story_todos.md"),
"---\nname: Show TODOs\n---\n# Story\n",
)
.unwrap();
fs::write(
upcoming.join("36_story_front_matter.md"),
backlog.join("36_story_front_matter.md"),
"---\nname: Enforce Front Matter\n---\n# Story\n",
)
.unwrap();
@@ -1302,7 +1302,7 @@ mod tests {
#[test]
fn next_item_number_empty_dirs() {
let tmp = tempfile::tempdir().unwrap();
let base = tmp.path().join(".story_kit/work/1_upcoming");
let base = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&base).unwrap();
assert_eq!(next_item_number(tmp.path()).unwrap(), 1);
}
@@ -1310,13 +1310,13 @@ mod tests {
#[test]
fn next_item_number_scans_all_dirs() {
let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
let backlog = tmp.path().join(".story_kit/work/1_backlog");
let current = tmp.path().join(".story_kit/work/2_current");
let archived = tmp.path().join(".story_kit/work/5_done");
fs::create_dir_all(&upcoming).unwrap();
fs::create_dir_all(&backlog).unwrap();
fs::create_dir_all(&current).unwrap();
fs::create_dir_all(&archived).unwrap();
fs::write(upcoming.join("10_story_foo.md"), "").unwrap();
fs::write(backlog.join("10_story_foo.md"), "").unwrap();
fs::write(current.join("20_story_bar.md"), "").unwrap();
fs::write(archived.join("15_story_baz.md"), "").unwrap();
assert_eq!(next_item_number(tmp.path()).unwrap(), 21);
@@ -1334,9 +1334,9 @@ mod tests {
#[test]
fn create_story_writes_correct_content() {
let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
fs::create_dir_all(&upcoming).unwrap();
fs::write(upcoming.join("36_story_existing.md"), "").unwrap();
let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&backlog).unwrap();
fs::write(backlog.join("36_story_existing.md"), "").unwrap();
let number = next_item_number(tmp.path()).unwrap();
assert_eq!(number, 37);
@@ -1345,7 +1345,7 @@ mod tests {
assert_eq!(slug, "my_new_feature");
let filename = format!("{number}_{slug}.md");
let filepath = upcoming.join(&filename);
let filepath = backlog.join(&filename);
let mut content = String::new();
content.push_str("---\n");
@@ -1377,10 +1377,10 @@ mod tests {
let result = create_story_file(tmp.path(), name, None, None, false);
assert!(result.is_ok(), "create_story_file failed: {result:?}");
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
let backlog = tmp.path().join(".story_kit/work/1_backlog");
let story_id = result.unwrap();
let filename = format!("{story_id}.md");
let contents = fs::read_to_string(upcoming.join(&filename)).unwrap();
let contents = fs::read_to_string(backlog.join(&filename)).unwrap();
let meta = parse_front_matter(&contents).expect("front matter should be valid YAML");
assert_eq!(meta.name.as_deref(), Some(name));
@@ -1389,10 +1389,10 @@ mod tests {
#[test]
fn create_story_rejects_duplicate() {
let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
fs::create_dir_all(&upcoming).unwrap();
let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&backlog).unwrap();
let filepath = upcoming.join("1_story_my_feature.md");
let filepath = backlog.join("1_story_my_feature.md");
fs::write(&filepath, "existing").unwrap();
// Simulate the check
@@ -1511,17 +1511,17 @@ mod tests {
}
#[test]
fn find_story_file_searches_current_then_upcoming() {
fn find_story_file_searches_current_then_backlog() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".story_kit/work/2_current");
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&current).unwrap();
fs::create_dir_all(&upcoming).unwrap();
fs::create_dir_all(&backlog).unwrap();
// Only in upcoming
fs::write(upcoming.join("6_test.md"), "").unwrap();
// Only in backlog
fs::write(backlog.join("6_test.md"), "").unwrap();
let found = find_story_file(tmp.path(), "6_test").unwrap();
assert!(found.ends_with("1_upcoming/6_test.md") || found.ends_with("1_upcoming\\6_test.md"));
assert!(found.ends_with("1_backlog/6_test.md") || found.ends_with("1_backlog\\6_test.md"));
// Also in current — current should win
fs::write(current.join("6_test.md"), "").unwrap();
@@ -1724,19 +1724,19 @@ mod tests {
#[test]
fn next_item_number_increments_from_existing_bugs() {
let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
fs::create_dir_all(&upcoming).unwrap();
fs::write(upcoming.join("1_bug_crash.md"), "").unwrap();
fs::write(upcoming.join("3_bug_another.md"), "").unwrap();
let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&backlog).unwrap();
fs::write(backlog.join("1_bug_crash.md"), "").unwrap();
fs::write(backlog.join("3_bug_another.md"), "").unwrap();
assert_eq!(next_item_number(tmp.path()).unwrap(), 4);
}
#[test]
fn next_item_number_scans_archived_too() {
let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
let backlog = tmp.path().join(".story_kit/work/1_backlog");
let archived = tmp.path().join(".story_kit/work/5_done");
fs::create_dir_all(&upcoming).unwrap();
fs::create_dir_all(&backlog).unwrap();
fs::create_dir_all(&archived).unwrap();
fs::write(archived.join("5_bug_old.md"), "").unwrap();
assert_eq!(next_item_number(tmp.path()).unwrap(), 6);
@@ -1752,11 +1752,11 @@ mod tests {
#[test]
fn list_bug_files_excludes_archive_subdir() {
let tmp = tempfile::tempdir().unwrap();
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming");
let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
let archived_dir = tmp.path().join(".story_kit/work/5_done");
fs::create_dir_all(&upcoming_dir).unwrap();
fs::create_dir_all(&backlog_dir).unwrap();
fs::create_dir_all(&archived_dir).unwrap();
fs::write(upcoming_dir.join("1_bug_open.md"), "# Bug 1: Open Bug\n").unwrap();
fs::write(backlog_dir.join("1_bug_open.md"), "# Bug 1: Open Bug\n").unwrap();
fs::write(archived_dir.join("2_bug_closed.md"), "# Bug 2: Closed Bug\n").unwrap();
let result = list_bug_files(tmp.path()).unwrap();
@@ -1768,11 +1768,11 @@ mod tests {
#[test]
fn list_bug_files_sorted_by_id() {
let tmp = tempfile::tempdir().unwrap();
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming");
fs::create_dir_all(&upcoming_dir).unwrap();
fs::write(upcoming_dir.join("3_bug_third.md"), "# Bug 3: Third\n").unwrap();
fs::write(upcoming_dir.join("1_bug_first.md"), "# Bug 1: First\n").unwrap();
fs::write(upcoming_dir.join("2_bug_second.md"), "# Bug 2: Second\n").unwrap();
let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&backlog_dir).unwrap();
fs::write(backlog_dir.join("3_bug_third.md"), "# Bug 3: Third\n").unwrap();
fs::write(backlog_dir.join("1_bug_first.md"), "# Bug 1: First\n").unwrap();
fs::write(backlog_dir.join("2_bug_second.md"), "# Bug 2: Second\n").unwrap();
let result = list_bug_files(tmp.path()).unwrap();
assert_eq!(result.len(), 3);
@@ -1810,7 +1810,7 @@ mod tests {
let filepath = tmp
.path()
.join(".story_kit/work/1_upcoming/1_bug_login_crash.md");
.join(".story_kit/work/1_backlog/1_bug_login_crash.md");
assert!(filepath.exists());
let contents = fs::read_to_string(&filepath).unwrap();
assert!(
@@ -1854,7 +1854,7 @@ mod tests {
)
.unwrap();
let filepath = tmp.path().join(".story_kit/work/1_upcoming/1_bug_some_bug.md");
let filepath = tmp.path().join(".story_kit/work/1_backlog/1_bug_some_bug.md");
let contents = fs::read_to_string(&filepath).unwrap();
assert!(
contents.starts_with("---\nname: \"Some Bug\"\n---"),
@@ -1876,7 +1876,7 @@ mod tests {
let filepath = tmp
.path()
.join(".story_kit/work/1_upcoming/1_spike_filesystem_watcher_architecture.md");
.join(".story_kit/work/1_backlog/1_spike_filesystem_watcher_architecture.md");
assert!(filepath.exists());
let contents = fs::read_to_string(&filepath).unwrap();
assert!(
@@ -1900,7 +1900,7 @@ mod tests {
create_spike_file(tmp.path(), "FS Watcher Spike", Some(description)).unwrap();
let filepath =
tmp.path().join(".story_kit/work/1_upcoming/1_spike_fs_watcher_spike.md");
tmp.path().join(".story_kit/work/1_backlog/1_spike_fs_watcher_spike.md");
let contents = fs::read_to_string(&filepath).unwrap();
assert!(contents.contains(description));
}
@@ -1910,7 +1910,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
create_spike_file(tmp.path(), "My Spike", None).unwrap();
let filepath = tmp.path().join(".story_kit/work/1_upcoming/1_spike_my_spike.md");
let filepath = tmp.path().join(".story_kit/work/1_backlog/1_spike_my_spike.md");
let contents = fs::read_to_string(&filepath).unwrap();
// Should have placeholder TBD in Question section
assert!(contents.contains("## Question\n\n- TBD\n"));
@@ -1931,10 +1931,10 @@ mod tests {
let result = create_spike_file(tmp.path(), name, None);
assert!(result.is_ok(), "create_spike_file failed: {result:?}");
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
let backlog = tmp.path().join(".story_kit/work/1_backlog");
let spike_id = result.unwrap();
let filename = format!("{spike_id}.md");
let contents = fs::read_to_string(upcoming.join(&filename)).unwrap();
let contents = fs::read_to_string(backlog.join(&filename)).unwrap();
let meta = parse_front_matter(&contents).expect("front matter should be valid YAML");
assert_eq!(meta.name.as_deref(), Some(name));
@@ -1943,9 +1943,9 @@ mod tests {
#[test]
fn create_spike_file_increments_from_existing_items() {
let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
fs::create_dir_all(&upcoming).unwrap();
fs::write(upcoming.join("5_story_existing.md"), "").unwrap();
let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&backlog).unwrap();
fs::write(backlog.join("5_story_existing.md"), "").unwrap();
let spike_id = create_spike_file(tmp.path(), "My Spike", None).unwrap();
assert!(spike_id.starts_with("6_spike_"), "expected spike number 6, got: {spike_id}");

View File

@@ -79,7 +79,7 @@ enum WsResponse {
},
/// Full pipeline state pushed on connect and after every work-item watcher event.
PipelineState {
upcoming: Vec<crate::http::workflow::UpcomingStory>,
backlog: Vec<crate::http::workflow::UpcomingStory>,
current: Vec<crate::http::workflow::UpcomingStory>,
qa: Vec<crate::http::workflow::UpcomingStory>,
merge: Vec<crate::http::workflow::UpcomingStory>,
@@ -160,7 +160,7 @@ impl From<WatcherEvent> for Option<WsResponse> {
impl From<PipelineState> for WsResponse {
fn from(s: PipelineState) -> Self {
WsResponse::PipelineState {
upcoming: s.upcoming,
backlog: s.backlog,
current: s.current,
qa: s.qa,
merge: s.merge,
@@ -695,7 +695,7 @@ mod tests {
agent: None,
};
let resp = WsResponse::PipelineState {
upcoming: vec![story],
backlog: vec![story],
current: vec![],
qa: vec![],
merge: vec![],
@@ -703,8 +703,8 @@ mod tests {
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "pipeline_state");
assert_eq!(json["upcoming"].as_array().unwrap().len(), 1);
assert_eq!(json["upcoming"][0]["story_id"], "10_story_test");
assert_eq!(json["backlog"].as_array().unwrap().len(), 1);
assert_eq!(json["backlog"][0]["story_id"], "10_story_test");
assert!(json["current"].as_array().unwrap().is_empty());
assert!(json["done"].as_array().unwrap().is_empty());
}
@@ -824,7 +824,7 @@ mod tests {
#[test]
fn pipeline_state_converts_to_ws_response() {
let state = PipelineState {
upcoming: vec![UpcomingStory {
backlog: vec![UpcomingStory {
story_id: "1_story_a".to_string(),
name: Some("Story A".to_string()),
error: None,
@@ -851,8 +851,8 @@ mod tests {
let resp: WsResponse = state.into();
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "pipeline_state");
assert_eq!(json["upcoming"].as_array().unwrap().len(), 1);
assert_eq!(json["upcoming"][0]["story_id"], "1_story_a");
assert_eq!(json["backlog"].as_array().unwrap().len(), 1);
assert_eq!(json["backlog"][0]["story_id"], "1_story_a");
assert_eq!(json["current"].as_array().unwrap().len(), 1);
assert_eq!(json["current"][0]["story_id"], "2_story_b");
assert!(json["qa"].as_array().unwrap().is_empty());
@@ -864,7 +864,7 @@ mod tests {
#[test]
fn empty_pipeline_state_converts_to_ws_response() {
let state = PipelineState {
upcoming: vec![],
backlog: vec![],
current: vec![],
qa: vec![],
merge: vec![],
@@ -873,7 +873,7 @@ mod tests {
let resp: WsResponse = state.into();
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "pipeline_state");
assert!(json["upcoming"].as_array().unwrap().is_empty());
assert!(json["backlog"].as_array().unwrap().is_empty());
assert!(json["current"].as_array().unwrap().is_empty());
assert!(json["qa"].as_array().unwrap().is_empty());
assert!(json["merge"].as_array().unwrap().is_empty());
@@ -991,7 +991,7 @@ mod tests {
#[test]
fn pipeline_state_with_agent_converts_correctly() {
let state = PipelineState {
upcoming: vec![],
backlog: vec![],
current: vec![UpcomingStory {
story_id: "10_story_x".to_string(),
name: Some("Story X".to_string()),
@@ -1046,7 +1046,7 @@ mod tests {
let root = tmp.path().to_path_buf();
// Create minimal pipeline dirs so load_pipeline_state succeeds.
for stage in &["1_upcoming", "2_current", "3_qa", "4_merge"] {
for stage in &["1_backlog", "2_current", "3_qa", "4_merge"] {
std::fs::create_dir_all(root.join(".story_kit").join("work").join(stage)).unwrap();
}
@@ -1155,7 +1155,7 @@ mod tests {
assert_eq!(initial["type"], "pipeline_state");
// All stages should be empty arrays since no .md files were created.
assert!(initial["upcoming"].as_array().unwrap().is_empty());
assert!(initial["backlog"].as_array().unwrap().is_empty());
assert!(initial["current"].as_array().unwrap().is_empty());
assert!(initial["qa"].as_array().unwrap().is_empty());
assert!(initial["merge"].as_array().unwrap().is_empty());