story-kit: merge 121_story_test_coverage_io_watcher_rs
This commit is contained in:
@@ -307,6 +307,220 @@ pub fn start_watcher(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
/// Initialise a minimal git repo so commit operations work.
|
||||||
|
fn init_git_repo(dir: &std::path::Path) {
|
||||||
|
use std::process::Command;
|
||||||
|
Command::new("git")
|
||||||
|
.args(["init"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.expect("git init");
|
||||||
|
Command::new("git")
|
||||||
|
.args(["config", "user.email", "test@example.com"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.expect("git config email");
|
||||||
|
Command::new("git")
|
||||||
|
.args(["config", "user.name", "Test"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.expect("git config name");
|
||||||
|
Command::new("git")
|
||||||
|
.args(["commit", "--allow-empty", "-m", "init"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.expect("git initial commit");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create the `.story_kit/work/{stage}/` dir tree inside `root`.
|
||||||
|
fn make_stage_dir(root: &std::path::Path, stage: &str) -> PathBuf {
|
||||||
|
let dir = root.join(".story_kit").join("work").join(stage);
|
||||||
|
fs::create_dir_all(&dir).expect("create stage dir");
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── git_add_work_and_commit ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn git_commit_returns_true_when_file_added() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
init_git_repo(tmp.path());
|
||||||
|
let stage_dir = make_stage_dir(tmp.path(), "2_current");
|
||||||
|
fs::write(
|
||||||
|
stage_dir.join("42_story_foo.md"),
|
||||||
|
"---\nname: test\n---\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result = git_add_work_and_commit(tmp.path(), "story-kit: start 42_story_foo");
|
||||||
|
assert_eq!(result, Ok(true), "should return Ok(true) when a commit was made");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn git_commit_returns_false_when_nothing_to_commit() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
init_git_repo(tmp.path());
|
||||||
|
let stage_dir = make_stage_dir(tmp.path(), "2_current");
|
||||||
|
fs::write(
|
||||||
|
stage_dir.join("42_story_foo.md"),
|
||||||
|
"---\nname: test\n---\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// First commit — should succeed.
|
||||||
|
git_add_work_and_commit(tmp.path(), "story-kit: start 42_story_foo").unwrap();
|
||||||
|
|
||||||
|
// Second call with no changes — should return Ok(false).
|
||||||
|
let result = git_add_work_and_commit(tmp.path(), "story-kit: start 42_story_foo");
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(false),
|
||||||
|
"should return Ok(false) when nothing to commit"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── flush_pending ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flush_pending_commits_and_broadcasts_work_item_for_addition() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
init_git_repo(tmp.path());
|
||||||
|
let stage_dir = make_stage_dir(tmp.path(), "2_current");
|
||||||
|
let story_path = stage_dir.join("42_story_foo.md");
|
||||||
|
fs::write(&story_path, "---\nname: test\n---\n").unwrap();
|
||||||
|
|
||||||
|
let (tx, mut rx) = tokio::sync::broadcast::channel(16);
|
||||||
|
let mut pending = HashMap::new();
|
||||||
|
pending.insert(story_path, "2_current".to_string());
|
||||||
|
|
||||||
|
flush_pending(&pending, tmp.path(), &tx);
|
||||||
|
|
||||||
|
let evt = rx.try_recv().expect("expected a broadcast event");
|
||||||
|
match evt {
|
||||||
|
WatcherEvent::WorkItem {
|
||||||
|
stage,
|
||||||
|
item_id,
|
||||||
|
action,
|
||||||
|
commit_msg,
|
||||||
|
} => {
|
||||||
|
assert_eq!(stage, "2_current");
|
||||||
|
assert_eq!(item_id, "42_story_foo");
|
||||||
|
assert_eq!(action, "start");
|
||||||
|
assert_eq!(commit_msg, "story-kit: start 42_story_foo");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected event: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flush_pending_broadcasts_for_all_pipeline_stages() {
|
||||||
|
let stages = [
|
||||||
|
("1_upcoming", "create", "story-kit: create 10_story_x"),
|
||||||
|
("3_qa", "qa", "story-kit: queue 10_story_x for QA"),
|
||||||
|
("4_merge", "merge", "story-kit: queue 10_story_x for merge"),
|
||||||
|
("5_archived", "accept", "story-kit: accept 10_story_x"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (stage, expected_action, expected_msg) in stages {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
init_git_repo(tmp.path());
|
||||||
|
let stage_dir = make_stage_dir(tmp.path(), stage);
|
||||||
|
let story_path = stage_dir.join("10_story_x.md");
|
||||||
|
fs::write(&story_path, "---\nname: test\n---\n").unwrap();
|
||||||
|
|
||||||
|
let (tx, mut rx) = tokio::sync::broadcast::channel(16);
|
||||||
|
let mut pending = HashMap::new();
|
||||||
|
pending.insert(story_path, stage.to_string());
|
||||||
|
|
||||||
|
flush_pending(&pending, tmp.path(), &tx);
|
||||||
|
|
||||||
|
let evt = rx.try_recv().expect("expected broadcast for stage {stage}");
|
||||||
|
match evt {
|
||||||
|
WatcherEvent::WorkItem {
|
||||||
|
action, commit_msg, ..
|
||||||
|
} => {
|
||||||
|
assert_eq!(action, expected_action, "stage {stage}");
|
||||||
|
assert_eq!(commit_msg, expected_msg, "stage {stage}");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected event for stage {stage}: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flush_pending_deletion_only_broadcasts_remove_event() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
init_git_repo(tmp.path());
|
||||||
|
// Create the work dir tree but NOT the file (simulates a deletion).
|
||||||
|
make_stage_dir(tmp.path(), "2_current");
|
||||||
|
let deleted_path = tmp
|
||||||
|
.path()
|
||||||
|
.join(".story_kit")
|
||||||
|
.join("work")
|
||||||
|
.join("2_current")
|
||||||
|
.join("42_story_foo.md");
|
||||||
|
|
||||||
|
let (tx, mut rx) = tokio::sync::broadcast::channel(16);
|
||||||
|
let mut pending = HashMap::new();
|
||||||
|
pending.insert(deleted_path, "2_current".to_string());
|
||||||
|
|
||||||
|
flush_pending(&pending, tmp.path(), &tx);
|
||||||
|
|
||||||
|
// Even when nothing was committed (file never existed), an event is broadcast.
|
||||||
|
let evt = rx.try_recv().expect("expected a broadcast event for deletion");
|
||||||
|
match evt {
|
||||||
|
WatcherEvent::WorkItem {
|
||||||
|
action, item_id, ..
|
||||||
|
} => {
|
||||||
|
assert_eq!(action, "remove");
|
||||||
|
assert_eq!(item_id, "42_story_foo");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected event: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flush_pending_skips_unknown_stage_for_addition() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
init_git_repo(tmp.path());
|
||||||
|
// File sits in an unrecognised directory.
|
||||||
|
let unknown_dir = tmp.path().join(".story_kit").join("work").join("9_unknown");
|
||||||
|
fs::create_dir_all(&unknown_dir).unwrap();
|
||||||
|
let path = unknown_dir.join("42_story_foo.md");
|
||||||
|
fs::write(&path, "---\nname: test\n---\n").unwrap();
|
||||||
|
|
||||||
|
let (tx, mut rx) = tokio::sync::broadcast::channel(16);
|
||||||
|
let mut pending = HashMap::new();
|
||||||
|
pending.insert(path, "9_unknown".to_string());
|
||||||
|
|
||||||
|
flush_pending(&pending, tmp.path(), &tx);
|
||||||
|
|
||||||
|
// No event should be broadcast because stage_metadata returns None for unknown stages.
|
||||||
|
assert!(
|
||||||
|
rx.try_recv().is_err(),
|
||||||
|
"no event should be broadcast for unknown stage"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flush_pending_empty_pending_does_nothing() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
init_git_repo(tmp.path());
|
||||||
|
make_stage_dir(tmp.path(), "2_current");
|
||||||
|
|
||||||
|
let (tx, mut rx) = tokio::sync::broadcast::channel(16);
|
||||||
|
let pending: HashMap<PathBuf, String> = HashMap::new();
|
||||||
|
|
||||||
|
// Should not panic and should not broadcast anything.
|
||||||
|
flush_pending(&pending, tmp.path(), &tx);
|
||||||
|
assert!(rx.try_recv().is_err(), "no event for empty pending map");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── stage_for_path (additional edge cases) ────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stage_for_path_recognises_pipeline_dirs() {
|
fn stage_for_path_recognises_pipeline_dirs() {
|
||||||
|
|||||||
Reference in New Issue
Block a user