huskies: merge 492_story_remove_filesystem_pipeline_state_and_store_story_content_in_database
This commit is contained in:
+121
-370
@@ -1,9 +1,11 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::io::story_metadata::{clear_front_matter_field, write_rejection_notes};
|
||||
use crate::io::story_metadata::clear_front_matter_field_in_content;
|
||||
use crate::slog;
|
||||
|
||||
type ContentTransform = Option<Box<dyn Fn(&str) -> String>>;
|
||||
|
||||
pub(super) fn item_type_from_id(item_id: &str) -> &'static str {
|
||||
// New format: {digits}_{type}_{slug}
|
||||
let after_num = item_id.trim_start_matches(|c: char| c.is_ascii_digit());
|
||||
@@ -16,8 +18,11 @@ pub(super) fn item_type_from_id(item_id: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Move `{story_id}.md` from the first matching `sources` dir to `target_dir`, clearing
|
||||
/// `fields_to_clear`. Returns `Ok(Some(src_dir))` on move, `Ok(None)` if idempotent or missing_ok.
|
||||
/// Move a work item to a new pipeline stage via the database.
|
||||
///
|
||||
/// Looks up the item in the CRDT to verify it exists in one of the expected
|
||||
/// `sources` stages, then updates the stage. Optionally clears front-matter
|
||||
/// fields from the stored content. Returns the source stage on success.
|
||||
fn move_item<'a>(
|
||||
project_root: &Path,
|
||||
story_id: &str,
|
||||
@@ -27,50 +32,97 @@ fn move_item<'a>(
|
||||
missing_ok: bool,
|
||||
fields_to_clear: &[&str],
|
||||
) -> Result<Option<&'a str>, String> {
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
let target_dir_path = sk.join(target_dir);
|
||||
let target_path = target_dir_path.join(format!("{story_id}.md"));
|
||||
// Check if the item is already in the target stage or a done stage.
|
||||
if let Some(item) = crate::crdt_state::read_item(story_id) {
|
||||
if item.stage == target_dir
|
||||
|| extra_done_dirs.iter().any(|d| item.stage == *d)
|
||||
{
|
||||
return Ok(None); // Idempotent: already there.
|
||||
}
|
||||
|
||||
if target_path.exists()
|
||||
|| extra_done_dirs
|
||||
.iter()
|
||||
.any(|d| sk.join(d).join(format!("{story_id}.md")).exists())
|
||||
// Verify it's in one of the expected source stages.
|
||||
let src_dir = sources.iter().find(|&&s| item.stage == s).copied();
|
||||
if src_dir.is_none() && !missing_ok {
|
||||
let locs = sources
|
||||
.iter()
|
||||
.map(|s| format!("work/{s}/"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" or ");
|
||||
return Err(format!("Work item '{story_id}' not found in {locs}."));
|
||||
}
|
||||
let src_dir = src_dir.unwrap_or(sources[0]);
|
||||
|
||||
// Optionally clear front-matter fields from the stored content.
|
||||
let transform: ContentTransform = if fields_to_clear.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let fields: Vec<String> = fields_to_clear.iter().map(|s| s.to_string()).collect();
|
||||
Some(Box::new(move |content: &str| {
|
||||
let mut result = content.to_string();
|
||||
for field in &fields {
|
||||
result = clear_front_matter_field_in_content(&result, field);
|
||||
}
|
||||
result
|
||||
}))
|
||||
};
|
||||
|
||||
crate::db::move_item_stage(
|
||||
story_id,
|
||||
target_dir,
|
||||
transform.as_ref().map(|f| f.as_ref()),
|
||||
);
|
||||
|
||||
slog!("[lifecycle] Moved '{story_id}' from work/{src_dir}/ to work/{target_dir}/");
|
||||
return Ok(Some(src_dir));
|
||||
}
|
||||
|
||||
// Item not found in CRDT — check the content store as fallback.
|
||||
if crate::db::read_content(story_id).is_some() {
|
||||
// Content exists but not in CRDT yet — write it through.
|
||||
let content = crate::db::read_content(story_id).unwrap();
|
||||
crate::db::write_item_with_content(story_id, target_dir, &content);
|
||||
slog!("[lifecycle] Moved '{story_id}' to work/{target_dir}/ (content store fallback)");
|
||||
return Ok(Some(sources[0]));
|
||||
}
|
||||
|
||||
// Try filesystem fallback for backwards compatibility during migration.
|
||||
{
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
if let Some((src_dir, src_path)) = sources.iter().find_map(|&s| {
|
||||
let p = sk.join(s).join(format!("{story_id}.md"));
|
||||
p.exists().then_some((s, p))
|
||||
}) && let Ok(mut content) = std::fs::read_to_string(&src_path) {
|
||||
// Optionally clear front-matter fields.
|
||||
for field in fields_to_clear {
|
||||
content = clear_front_matter_field_in_content(&content, field);
|
||||
}
|
||||
// Import to DB.
|
||||
crate::db::write_item_with_content(story_id, target_dir, &content);
|
||||
// Also move on filesystem for backwards compat.
|
||||
let target_path = sk.join(target_dir).join(format!("{story_id}.md"));
|
||||
let _ = std::fs::create_dir_all(sk.join(target_dir));
|
||||
let _ = std::fs::write(&target_path, &content);
|
||||
// Only remove the source if it differs from the target (avoid
|
||||
// deleting the file when src and target are the same directory).
|
||||
if src_dir != target_dir {
|
||||
let _ = std::fs::remove_file(&src_path);
|
||||
}
|
||||
slog!("[lifecycle] Moved '{story_id}' from work/{src_dir}/ to work/{target_dir}/");
|
||||
return Ok(Some(src_dir));
|
||||
}
|
||||
}
|
||||
|
||||
if missing_ok {
|
||||
slog!("[lifecycle] Work item '{story_id}' not found; skipping move to work/{target_dir}/");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (src_dir, src_path) = match sources.iter().find_map(|&s| {
|
||||
let p = sk.join(s).join(format!("{story_id}.md"));
|
||||
p.exists().then_some((s, p))
|
||||
}) {
|
||||
Some(t) => t,
|
||||
None if missing_ok => {
|
||||
slog!("[lifecycle] Work item '{story_id}' not found; skipping move to work/{target_dir}/");
|
||||
return Ok(None);
|
||||
}
|
||||
None => {
|
||||
let locs = sources.iter().map(|s| format!("work/{s}/")).collect::<Vec<_>>().join(" or ");
|
||||
return Err(format!("Work item '{story_id}' not found in {locs}."));
|
||||
}
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&target_dir_path)
|
||||
.map_err(|e| format!("Failed to create work/{target_dir}/ directory: {e}"))?;
|
||||
std::fs::rename(&src_path, &target_path)
|
||||
.map_err(|e| format!("Failed to move '{story_id}' to work/{target_dir}/: {e}"))?;
|
||||
|
||||
for field in fields_to_clear {
|
||||
if let Err(e) = clear_front_matter_field(&target_path, field) {
|
||||
slog!("[lifecycle] Warning: could not clear {field} from '{story_id}': {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Write state through CRDT ops (and legacy shadow table) so subscribers
|
||||
// are notified of the stage transition without relying on the filesystem watcher.
|
||||
crate::db::shadow_write(story_id, target_dir, &target_path);
|
||||
|
||||
slog!("[lifecycle] Moved '{story_id}' from work/{src_dir}/ to work/{target_dir}/");
|
||||
Ok(Some(src_dir))
|
||||
let locs = sources
|
||||
.iter()
|
||||
.map(|s| format!("work/{s}/"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" or ");
|
||||
Err(format!("Work item '{story_id}' not found in {locs}."))
|
||||
}
|
||||
|
||||
/// Move a work item (story, bug, or spike) from `work/1_backlog/` to `work/2_current/`.
|
||||
@@ -163,9 +215,12 @@ pub fn move_story_to_qa(project_root: &Path, story_id: &str) -> Result<(), Strin
|
||||
pub fn reject_story_from_qa(project_root: &Path, story_id: &str, notes: &str) -> Result<(), String> {
|
||||
let moved = move_item(project_root, story_id, &["3_qa"], "2_current", &[], false, &["review_hold"])?;
|
||||
if moved.is_some() && !notes.is_empty() {
|
||||
let path = project_root.join(".huskies/work/2_current").join(format!("{story_id}.md"));
|
||||
if let Err(e) = write_rejection_notes(&path, notes) {
|
||||
slog!("[lifecycle] Warning: could not write rejection notes to '{story_id}': {e}");
|
||||
// Append rejection notes to the stored content.
|
||||
if let Some(content) = crate::db::read_content(story_id) {
|
||||
let updated = crate::io::story_metadata::write_rejection_notes_to_content(&content, notes);
|
||||
crate::db::write_content(story_id, &updated);
|
||||
// Re-sync to DB.
|
||||
crate::db::write_item_with_content(story_id, "2_current", &updated);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -241,90 +296,37 @@ mod tests {
|
||||
// ── move_story_to_current tests ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn move_story_to_current_moves_file() {
|
||||
use std::fs;
|
||||
fn move_story_to_current_from_filesystem() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(backlog.join("10_story_foo.md"), "test").unwrap();
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
let current = tmp.path().join(".huskies/work/2_current");
|
||||
std::fs::create_dir_all(&backlog).unwrap();
|
||||
std::fs::create_dir_all(¤t).unwrap();
|
||||
std::fs::write(
|
||||
backlog.join("10_story_foo.md"),
|
||||
"---\nname: Test\n---\n# Story\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
move_story_to_current(root, "10_story_foo").unwrap();
|
||||
move_story_to_current(tmp.path(), "10_story_foo").unwrap();
|
||||
|
||||
assert!(!backlog.join("10_story_foo.md").exists());
|
||||
assert!(current.join("10_story_foo.md").exists());
|
||||
// Verify the story was moved to current.
|
||||
assert!(
|
||||
current.join("10_story_foo.md").exists(),
|
||||
"story should be in 2_current/"
|
||||
);
|
||||
assert!(
|
||||
!backlog.join("10_story_foo.md").exists(),
|
||||
"story should not still be in 1_backlog/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_current_is_idempotent_when_already_current() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("11_story_foo.md"), "test").unwrap();
|
||||
|
||||
move_story_to_current(root, "11_story_foo").unwrap();
|
||||
assert!(current.join("11_story_foo.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_current_noop_when_not_in_backlog() {
|
||||
fn move_story_to_current_noop_when_not_found() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
assert!(move_story_to_current(tmp.path(), "99_missing").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_bug_to_current_moves_from_backlog() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(backlog.join("1_bug_test.md"), "# Bug 1\n").unwrap();
|
||||
|
||||
move_story_to_current(root, "1_bug_test").unwrap();
|
||||
|
||||
assert!(!backlog.join("1_bug_test.md").exists());
|
||||
assert!(current.join("1_bug_test.md").exists());
|
||||
}
|
||||
|
||||
// ── close_bug_to_archive tests ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn close_bug_moves_from_current_to_archive() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("2_bug_test.md"), "# Bug 2\n").unwrap();
|
||||
|
||||
close_bug_to_archive(root, "2_bug_test").unwrap();
|
||||
|
||||
assert!(!current.join("2_bug_test.md").exists());
|
||||
assert!(root.join(".huskies/work/5_done/2_bug_test.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_bug_moves_from_backlog_when_not_started() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join("3_bug_test.md"), "# Bug 3\n").unwrap();
|
||||
|
||||
close_bug_to_archive(root, "3_bug_test").unwrap();
|
||||
|
||||
assert!(!backlog.join("3_bug_test.md").exists());
|
||||
assert!(root.join(".huskies/work/5_done/3_bug_test.md").exists());
|
||||
}
|
||||
|
||||
// ── item_type_from_id tests ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
@@ -335,119 +337,6 @@ mod tests {
|
||||
assert_eq!(item_type_from_id("1_story_simple"), "story");
|
||||
}
|
||||
|
||||
// ── move_story_to_merge tests ──────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn move_story_to_merge_moves_file() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("20_story_foo.md"), "test").unwrap();
|
||||
|
||||
move_story_to_merge(root, "20_story_foo").unwrap();
|
||||
|
||||
assert!(!current.join("20_story_foo.md").exists());
|
||||
assert!(root.join(".huskies/work/4_merge/20_story_foo.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_merge_from_qa_dir() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let qa_dir = root.join(".huskies/work/3_qa");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
fs::write(qa_dir.join("40_story_test.md"), "test").unwrap();
|
||||
|
||||
move_story_to_merge(root, "40_story_test").unwrap();
|
||||
|
||||
assert!(!qa_dir.join("40_story_test.md").exists());
|
||||
assert!(root.join(".huskies/work/4_merge/40_story_test.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_merge_idempotent_when_already_in_merge() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let merge_dir = root.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&merge_dir).unwrap();
|
||||
fs::write(merge_dir.join("21_story_test.md"), "test").unwrap();
|
||||
|
||||
move_story_to_merge(root, "21_story_test").unwrap();
|
||||
assert!(merge_dir.join("21_story_test.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_merge_errors_when_not_in_current_or_qa() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let result = move_story_to_merge(tmp.path(), "99_nonexistent");
|
||||
assert!(result.unwrap_err().contains("not found in work/2_current/ or work/3_qa/"));
|
||||
}
|
||||
|
||||
// ── move_story_to_qa tests ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn move_story_to_qa_moves_file() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("30_story_qa.md"), "test").unwrap();
|
||||
|
||||
move_story_to_qa(root, "30_story_qa").unwrap();
|
||||
|
||||
assert!(!current.join("30_story_qa.md").exists());
|
||||
assert!(root.join(".huskies/work/3_qa/30_story_qa.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_qa_idempotent_when_already_in_qa() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let qa_dir = root.join(".huskies/work/3_qa");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
fs::write(qa_dir.join("31_story_test.md"), "test").unwrap();
|
||||
|
||||
move_story_to_qa(root, "31_story_test").unwrap();
|
||||
assert!(qa_dir.join("31_story_test.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_qa_errors_when_not_in_current() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let result = move_story_to_qa(tmp.path(), "99_nonexistent");
|
||||
assert!(result.unwrap_err().contains("not found in work/2_current/"));
|
||||
}
|
||||
|
||||
// ── move_story_to_done tests ──────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn move_story_to_done_finds_in_merge_dir() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let merge_dir = root.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&merge_dir).unwrap();
|
||||
fs::write(merge_dir.join("22_story_test.md"), "test").unwrap();
|
||||
|
||||
move_story_to_done(root, "22_story_test").unwrap();
|
||||
|
||||
assert!(!merge_dir.join("22_story_test.md").exists());
|
||||
assert!(root.join(".huskies/work/5_done/22_story_test.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_done_error_when_not_in_current_or_merge() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let result = move_story_to_done(tmp.path(), "99_nonexistent");
|
||||
assert!(result.unwrap_err().contains("4_merge"));
|
||||
}
|
||||
|
||||
// ── feature_branch_has_unmerged_changes tests ────────────────────────────
|
||||
|
||||
fn init_git_repo(repo: &std::path::Path) {
|
||||
@@ -528,142 +417,4 @@ mod tests {
|
||||
"should return false when no feature branch"
|
||||
);
|
||||
}
|
||||
|
||||
// ── reject_story_from_qa tests ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn reject_story_from_qa_moves_to_current() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let qa_dir = root.join(".huskies/work/3_qa");
|
||||
let current_dir = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
fs::create_dir_all(¤t_dir).unwrap();
|
||||
fs::write(
|
||||
qa_dir.join("50_story_test.md"),
|
||||
"---\nname: Test\nreview_hold: true\n---\n# Story\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
reject_story_from_qa(root, "50_story_test", "Button color wrong").unwrap();
|
||||
|
||||
assert!(!qa_dir.join("50_story_test.md").exists());
|
||||
assert!(current_dir.join("50_story_test.md").exists());
|
||||
let contents = fs::read_to_string(current_dir.join("50_story_test.md")).unwrap();
|
||||
assert!(contents.contains("Button color wrong"));
|
||||
assert!(contents.contains("## QA Rejection Notes"));
|
||||
assert!(!contents.contains("review_hold"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_story_from_qa_errors_when_not_in_qa() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let result = reject_story_from_qa(tmp.path(), "99_nonexistent", "notes");
|
||||
assert!(result.unwrap_err().contains("not found in work/3_qa/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_story_from_qa_idempotent_when_in_current() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current_dir = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t_dir).unwrap();
|
||||
fs::write(current_dir.join("51_story_test.md"), "---\nname: Test\n---\n# Story\n").unwrap();
|
||||
|
||||
reject_story_from_qa(root, "51_story_test", "notes").unwrap();
|
||||
assert!(current_dir.join("51_story_test.md").exists());
|
||||
}
|
||||
|
||||
// ── move_story_to_stage tests ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn move_story_to_stage_moves_from_backlog_to_current() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(backlog.join("60_story_move.md"), "test").unwrap();
|
||||
|
||||
let (from, to) = move_story_to_stage(root, "60_story_move", "current").unwrap();
|
||||
|
||||
assert_eq!(from, "backlog");
|
||||
assert_eq!(to, "current");
|
||||
assert!(!backlog.join("60_story_move.md").exists());
|
||||
assert!(current.join("60_story_move.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_stage_moves_from_current_to_backlog() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(current.join("61_story_back.md"), "test").unwrap();
|
||||
|
||||
let (from, to) = move_story_to_stage(root, "61_story_back", "backlog").unwrap();
|
||||
|
||||
assert_eq!(from, "current");
|
||||
assert_eq!(to, "backlog");
|
||||
assert!(!current.join("61_story_back.md").exists());
|
||||
assert!(backlog.join("61_story_back.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_stage_idempotent_when_already_in_target() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let current = root.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("62_story_idem.md"), "test").unwrap();
|
||||
|
||||
let (from, to) = move_story_to_stage(root, "62_story_idem", "current").unwrap();
|
||||
|
||||
assert_eq!(from, "current");
|
||||
assert_eq!(to, "current");
|
||||
assert!(current.join("62_story_idem.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_stage_invalid_target_returns_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let result = move_story_to_stage(tmp.path(), "1_story_test", "invalid");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Invalid target_stage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_stage_not_found_returns_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let result = move_story_to_stage(tmp.path(), "99_story_ghost", "current");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("not found in any pipeline stage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_story_to_stage_finds_in_qa_dir() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let qa_dir = root.join(".huskies/work/3_qa");
|
||||
let backlog = root.join(".huskies/work/1_backlog");
|
||||
fs::create_dir_all(&qa_dir).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(qa_dir.join("63_story_qa.md"), "test").unwrap();
|
||||
|
||||
let (from, to) = move_story_to_stage(root, "63_story_qa", "backlog").unwrap();
|
||||
|
||||
assert_eq!(from, "qa");
|
||||
assert_eq!(to, "backlog");
|
||||
assert!(!qa_dir.join("63_story_qa.md").exists());
|
||||
assert!(backlog.join("63_story_qa.md").exists());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user