story-kit: merge 121_story_test_coverage_io_watcher_rs

This commit is contained in:
Dave
2026-02-23 23:55:09 +00:00
parent 9e17aff486
commit e15ae3027f

View File

@@ -307,6 +307,220 @@ pub fn start_watcher(
#[cfg(test)]
mod tests {
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]
fn stage_for_path_recognises_pipeline_dirs() {