//! Controlled-clock coverage of both upgrade windows and discovery exclusion. use super::*; use crate::{ process::{Process, ProcessInfo}, runtime::Runtime, world::World, }; use tokio::{ sync::mpsc, task::JoinHandle, time::{Instant, advance}, }; /// A process with active work that ignores both superseded and EOF. fn process( host: &Host, pid: &str, method: &str, ) -> (mpsc::UnboundedReceiver, JoinHandle<()>) { let (input, incoming) = mpsc::unbounded_channel(); let task = tokio::spawn(std::future::pending()); host.0.state.lock().unwrap().processes.insert( pid.into(), Process { info: ProcessInfo { plugin: "notes".into(), version: "old".into(), method: method.into(), params: json!({}), parent: None, detached: true, start_time: 0, state: "running".into(), }, memory: sausage_vfs::Memory::default(), files: sausage_vfs::Process::default(), store_ended: watch::channel(true).0, input: Some(input), abort: Some(task.abort_handle()), caller: "console".into(), id: None, answer: None, ended: watch::channel(false).0, stopped: false, superseded: false, }, ); (incoming, task) } /// PROTOCOL 3.7/3.6: five seconds running, then five seconds stopping; discovery is untouched. #[tokio::test] async fn two_five_second_windows_and_discovery_exclusion() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().join("world"); World::create(&root).unwrap(); let host = Host::test_world(&root, Runtime::new(&dir.path().join("cache")).unwrap()) .await .unwrap(); let (mut input, work) = process(&host, "old-work", "work"); let (mut discovery_input, discovery) = process(&host, "old-discovery", "rpc.discover"); let upgrade = host.snapshot_upgrade("notes", "old".into(), "new"); assert_eq!(upgrade.processes.len(), 1); tokio::time::pause(); let start = Instant::now(); host.begin_upgrade(upgrade); assert_eq!( input.recv().await.unwrap(), json!({"jsonrpc":"2.0","method":"rpc.superseded","params":{"current":"new"}}) ); // Run the upgrade dispatcher, then its per-process task, so the deadline is armed. tokio::task::yield_now().await; tokio::task::yield_now().await; let window = Duration::from_secs(5); assert_eq!(UPGRADE_GRACE, window); // tokio::time::sleep documents millisecond granularity; the paused driver // may round a deadline up by one tick. Check each window independently. let tick = Duration::from_millis(1); let before_deadline = window - tick; advance(before_deadline).await; tokio::task::yield_now().await; assert_eq!(host.processes()["old-work"].state, "running"); assert!(!work.is_finished()); advance(tick).await; assert!( input.recv().await.is_none(), "EOF at grace, even with active work" ); let stopped = Instant::now(); assert!((window..=window + tick).contains(&(stopped - start))); assert_eq!(host.processes()["old-work"].state, "stopping"); // begin_retire schedules the independent PROTOCOL 3.6 deadline. tokio::task::yield_now().await; advance(before_deadline).await; tokio::task::yield_now().await; assert!( !work.is_finished(), "on_stop still has its full five seconds" ); advance(tick).await; assert!(work.await.unwrap_err().is_cancelled()); assert!((window..=window + tick).contains(&(Instant::now() - stopped))); assert_eq!(host.processes()["old-discovery"].state, "running"); assert!(matches!( discovery_input.try_recv(), Err(mpsc::error::TryRecvError::Empty) )); assert!(!discovery.is_finished()); discovery.abort(); let _ = discovery.await; // Synthetic processes have no execution cleanup task; discard only this test table. host.0.shutdown.store(true, Ordering::SeqCst); host.0.state.lock().unwrap().processes.clear(); host.stop().await; }