story-kit: merge 287_story_rename_upcoming_pipeline_stage_to_backlog
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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---"));
|
||||
|
||||
@@ -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(¤t).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(¤t).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(¤t).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}");
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user