storkit: merge 462_bug_stage_transition_notifications_can_arrive_out_of_order_and_show_wrong_story_name
This commit is contained in:
@@ -44,6 +44,9 @@ pub enum WatcherEvent {
|
||||
action: String,
|
||||
/// The deterministic git commit message used (or that would have been used).
|
||||
commit_msg: String,
|
||||
/// The pipeline stage the item moved FROM, populated for move operations.
|
||||
/// `None` for creations, deletions, or synthetic events.
|
||||
from_stage: Option<String>,
|
||||
},
|
||||
/// `.storkit/project.toml` was modified at the project root (not inside a worktree).
|
||||
ConfigChanged,
|
||||
@@ -276,12 +279,27 @@ fn flush_pending(
|
||||
slog!("[watcher] flush (broadcast-only): {commit_msg}");
|
||||
}
|
||||
|
||||
// For move operations, find the source stage from deleted entries with matching item_id.
|
||||
let from_stage: Option<String> = if !additions.is_empty() {
|
||||
pending
|
||||
.iter()
|
||||
.filter(|(path, _)| !path.exists())
|
||||
.find(|(path, _)| {
|
||||
path.file_stem()
|
||||
.and_then(|s| s.to_str()) == Some(item_id.as_str())
|
||||
})
|
||||
.map(|(_, stage)| stage.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Always broadcast the event so connected WebSocket clients stay in sync.
|
||||
let evt = WatcherEvent::WorkItem {
|
||||
stage: dest_stage.to_string(),
|
||||
item_id,
|
||||
action: action.to_string(),
|
||||
commit_msg,
|
||||
from_stage,
|
||||
};
|
||||
let _ = event_tx.send(evt);
|
||||
}
|
||||
@@ -623,6 +641,7 @@ mod tests {
|
||||
item_id,
|
||||
action,
|
||||
commit_msg,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(stage, "1_backlog");
|
||||
assert_eq!(item_id, "42_story_foo");
|
||||
@@ -667,6 +686,7 @@ mod tests {
|
||||
item_id,
|
||||
action,
|
||||
commit_msg,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(stage, "2_current");
|
||||
assert_eq!(item_id, "42_story_foo");
|
||||
@@ -922,6 +942,75 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── flush_pending from_stage ─────────────────────────────────────────────
|
||||
|
||||
/// AC3: when a pending map contains both a deletion (source stage) and a
|
||||
/// creation (dest stage) for the same item_id, the broadcast event should
|
||||
/// have `from_stage` set to the source stage key.
|
||||
#[test]
|
||||
fn flush_pending_sets_from_stage_for_move_operations() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
init_git_repo(tmp.path());
|
||||
|
||||
// Destination exists (file moved here).
|
||||
let merge_dir = make_stage_dir(tmp.path(), "4_merge");
|
||||
let merge_path = merge_dir.join("42_story_foo.md");
|
||||
fs::write(&merge_path, "---\nname: test\n---\n").unwrap();
|
||||
|
||||
// Source path does NOT exist (file was moved away).
|
||||
make_stage_dir(tmp.path(), "3_qa");
|
||||
let qa_path = tmp
|
||||
.path()
|
||||
.join(".storkit")
|
||||
.join("work")
|
||||
.join("3_qa")
|
||||
.join("42_story_foo.md");
|
||||
|
||||
let (tx, mut rx) = tokio::sync::broadcast::channel(16);
|
||||
let mut pending = HashMap::new();
|
||||
pending.insert(merge_path, "4_merge".to_string()); // addition
|
||||
pending.insert(qa_path, "3_qa".to_string()); // deletion
|
||||
|
||||
flush_pending(&pending, tmp.path(), &tx);
|
||||
|
||||
let evt = rx.try_recv().expect("expected event");
|
||||
match evt {
|
||||
WatcherEvent::WorkItem {
|
||||
stage, from_stage, ..
|
||||
} => {
|
||||
assert_eq!(stage, "4_merge");
|
||||
assert_eq!(from_stage, Some("3_qa".to_string()));
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// AC3: when a pending map has only an addition (creation, not a move),
|
||||
/// `from_stage` should be `None`.
|
||||
#[test]
|
||||
fn flush_pending_sets_from_stage_to_none_for_creations() {
|
||||
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("55_story_new.md");
|
||||
fs::write(&story_path, "---\nname: New Story\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 event");
|
||||
match evt {
|
||||
WatcherEvent::WorkItem { from_stage, .. } => {
|
||||
assert_eq!(from_stage, None, "creation should have no from_stage");
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ── stage_for_path (additional edge cases) ────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user