diff --git a/server/src/chat/timer.rs b/server/src/chat/timer.rs index a40ef1fd..3339db6a 100644 --- a/server/src/chat/timer.rs +++ b/server/src/chat/timer.rs @@ -935,4 +935,50 @@ mod tests { assert!(result.contains("421_story_foo"), "unexpected: {result}"); assert!(result.contains("Pending timers"), "unexpected: {result}"); } + + // ── AC: firing a timer for a backlog story moves it to current ─────── + + /// When a timer fires for a story in backlog, the tick loop calls + /// `move_story_to_current` before `start_agent`. This test exercises + /// that exact sequence (minus the agent pool) to prove the story ends + /// up in `2_current/` after firing. + #[test] + fn fired_timer_for_backlog_story_moves_to_current() { + use std::fs; + + let dir = TempDir::new().unwrap(); + let root = dir.path(); + let backlog = root.join(".storkit/work/1_backlog"); + let current = root.join(".storkit/work/2_current"); + fs::create_dir_all(&backlog).unwrap(); + fs::create_dir_all(¤t).unwrap(); + fs::write(backlog.join("421_story_foo.md"), "---\nname: Foo\n---\n").unwrap(); + + // Add a past timer so take_due returns it immediately. + let store = TimerStore::load(root.join("timers.json")); + let past = Utc::now() - Duration::seconds(1); + store.add("421_story_foo".to_string(), past).unwrap(); + + // Drain due timers — same as the tick loop does. + let due = store.take_due(Utc::now()); + assert_eq!(due.len(), 1, "expected one fired timer"); + + // Apply the move-to-current step the tick loop performs. + for entry in &due { + crate::agents::lifecycle::move_story_to_current(root, &entry.story_id) + .expect("move_story_to_current should succeed for backlog story"); + } + + // Story must now be in 2_current/, not in 1_backlog/. + assert!( + current.join("421_story_foo.md").exists(), + "story should be in 2_current/ after timer fires" + ); + assert!( + !backlog.join("421_story_foo.md").exists(), + "story should no longer be in 1_backlog/ after timer fires" + ); + // Timer was consumed. + assert!(store.list().is_empty(), "fired timer should be removed from store"); + } }