@@ -0,0 +1,455 @@
1+#![cfg(all(feature = "mock-vm", target_os = "macos"))]
2+
3+use std::{
4+ fs,
5+ io::{BufRead, BufReader, Read},
6+ os::unix::net::UnixStream,
7+ path::{Path, PathBuf},
8+ process::{Child, Command, Stdio},
9+ sync::mpsc,
10+ thread,
11+ time::{Duration, Instant},
12+};
13+
14+use tempfile::TempDir;
15+
16+#[test]
17+fn mock_vm_allows_refcount_concurrency() {
18+ let temp = TempDir::new().unwrap();
19+ let mut supervisor = spawn_supervisor(&temp, 0, 1200, "e2e_vm_mock".to_string());
20+
21+ supervisor.clients = connect_clients(
22+ &supervisor.socket_path,
23+ 12,
24+ Duration::from_secs(2),
25+ true,
26+ "e2e_vm_mock",
27+ );
28+ log_line("e2e_vm_mock", "connected 12 clients");
29+
30+ assert_manager_alive_for(
31+ &mut supervisor.child,
32+ Duration::from_millis(900),
33+ "vm manager exited while clients active",
34+ );
35+
36+ let remaining = supervisor.clients.split_off(6);
37+ supervisor.clients = remaining;
38+ log_line("e2e_vm_mock", "dropped 6 clients");
39+ assert_manager_alive_for(
40+ &mut supervisor.child,
41+ Duration::from_millis(900),
42+ "vm manager exited while clients active",
43+ );
44+
45+ supervisor.clients.clear();
46+ log_line("e2e_vm_mock", "dropped remaining clients");
47+ wait_for_exit(&mut supervisor.child, Duration::from_secs(10));
48+ let status = supervisor.child.wait().unwrap();
49+ assert!(status.success(), "vm manager exited with {status}");
50+}
51+
52+#[test]
53+fn mock_vm_many_managers_many_clients() {
54+ let temp = TempDir::new().unwrap();
55+ let mut supervisors = Vec::new();
56+
57+ for idx in 0..3 {
58+ supervisors.push(spawn_supervisor(
59+ &temp,
60+ idx + 1,
61+ 1400,
62+ format!("e2e_vm_mock_{idx}"),
63+ ));
64+ }
65+
66+ for supervisor in &mut supervisors {
67+ supervisor.clients = connect_clients(
68+ &supervisor.socket_path,
69+ 8,
70+ Duration::from_secs(2),
71+ true,
72+ "e2e_vm_mock",
73+ );
74+ }
75+ log_line("e2e_vm_mock", "connected 8 clients per manager");
76+
77+ for supervisor in &mut supervisors {
78+ assert_manager_alive_for(
79+ &mut supervisor.child,
80+ Duration::from_millis(900),
81+ "manager exited while clients active",
82+ );
83+ }
84+
85+ supervisors[0].clients.clear();
86+ log_line("e2e_vm_mock", "dropped all clients for manager 0");
87+ wait_for_exit(&mut supervisors[0].child, Duration::from_secs(10));
88+ let status = supervisors[0].child.wait().unwrap();
89+ assert!(status.success(), "manager 0 exited with {status}");
90+
91+ for supervisor in supervisors.iter_mut().skip(1) {
92+ assert_manager_alive_for(
93+ &mut supervisor.child,
94+ Duration::from_millis(900),
95+ "another manager exited early",
96+ );
97+ }
98+
99+ for supervisor in supervisors.iter_mut().skip(1) {
100+ supervisor.clients.clear();
101+ }
102+ log_line("e2e_vm_mock", "dropped remaining clients");
103+ for supervisor in supervisors.iter_mut().skip(1) {
104+ wait_for_exit(&mut supervisor.child, Duration::from_secs(10));
105+ let status = supervisor.child.wait().unwrap();
106+ assert!(status.success(), "manager exited with {status}");
107+ }
108+}
109+
110+#[test]
111+fn mock_vm_monkey_processes() {
112+ let temp = TempDir::new().unwrap();
113+ let mut rng = Lcg::new(0x5eed_f00d_dead_beef);
114+ let mut supervisors = Vec::new();
115+ let mut next_id = 0usize;
116+ let max_supervisors = 4usize;
117+ let steps = 25usize;
118+
119+ supervisors.push(spawn_supervisor(
120+ &temp,
121+ next_id,
122+ 1200,
123+ format!("e2e_vm_monkey_{next_id}"),
124+ ));
125+ next_id += 1;
126+
127+ for step in 0..steps {
128+ prune_exited_supervisors(&mut supervisors, "e2e_vm_monkey");
129+ let roll = rng.gen_range(100);
130+ log_line(
131+ "e2e_vm_monkey",
132+ &format!("step {step} roll={roll} supervisors={}", supervisors.len()),
133+ );
134+ if roll < 20 && supervisors.len() < max_supervisors {
135+ supervisors.push(spawn_supervisor(
136+ &temp,
137+ next_id,
138+ 1200,
139+ format!("e2e_vm_monkey_{next_id}"),
140+ ));
141+ log_line("e2e_vm_monkey", &format!("spawned supervisor {next_id}"));
142+ next_id += 1;
143+ } else if roll < 45 && !supervisors.is_empty() {
144+ let idx = rng.gen_range(supervisors.len());
145+ let mut supervisor = supervisors.swap_remove(idx);
146+ log_line(
147+ "e2e_vm_monkey",
148+ &format!("killing supervisor {}", supervisor.label),
149+ );
150+ kill_supervisor(&mut supervisor, Duration::from_secs(5));
151+ } else if roll < 80 && !supervisors.is_empty() {
152+ let idx = rng.gen_range(supervisors.len());
153+ let burst = 1 + rng.gen_range(3);
154+ let new_clients = connect_clients(
155+ &supervisors[idx].socket_path,
156+ burst,
157+ Duration::from_secs(1),
158+ false,
159+ "e2e_vm_monkey",
160+ );
161+ supervisors[idx].clients.extend(new_clients);
162+ log_line(
163+ "e2e_vm_monkey",
164+ &format!("connected {burst} clients to {}", supervisors[idx].label),
165+ );
166+ } else if !supervisors.is_empty() {
167+ let idx = rng.gen_range(supervisors.len());
168+ if !supervisors[idx].clients.is_empty() {
169+ let len = supervisors[idx].clients.len();
170+ let drop_count = 1 + rng.gen_range(len);
171+ supervisors[idx].clients.drain(0..drop_count.min(len));
172+ log_line(
173+ "e2e_vm_monkey",
174+ &format!(
175+ "dropped {drop_count} clients from {}",
176+ supervisors[idx].label
177+ ),
178+ );
179+ }
180+ }
181+ thread::sleep(Duration::from_millis(200));
182+ }
183+
184+ log_line("e2e_vm_monkey", "final cleanup");
185+ for supervisor in supervisors.iter_mut() {
186+ shutdown_supervisor(supervisor, Duration::from_secs(10));
187+ }
188+}
189+
190+#[test]
191+fn mock_vm_exits_without_clients() {
192+ let temp = TempDir::new().unwrap();
193+ let mut supervisor = spawn_supervisor(&temp, 99, 300, "e2e_vm_no_clients".to_string());
194+ wait_for_exit(&mut supervisor.child, Duration::from_secs(5));
195+ let status = supervisor.child.wait().unwrap();
196+ assert!(status.success(), "vm manager exited with {status}");
197+}
198+
199+#[test]
200+fn mock_vm_reconnect_resets_shutdown() {
201+ let temp = TempDir::new().unwrap();
202+ let mut supervisor = spawn_supervisor(&temp, 100, 800, "e2e_vm_reconnect".to_string());
203+
204+ supervisor.clients = connect_clients(
205+ &supervisor.socket_path,
206+ 1,
207+ Duration::from_secs(2),
208+ true,
209+ "e2e_vm_reconnect",
210+ );
211+ supervisor.clients.clear();
212+ thread::sleep(Duration::from_millis(400));
213+
214+ supervisor.clients = connect_clients(
215+ &supervisor.socket_path,
216+ 1,
217+ Duration::from_secs(2),
218+ true,
219+ "e2e_vm_reconnect",
220+ );
221+ assert_manager_alive_for(
222+ &mut supervisor.child,
223+ Duration::from_millis(600),
224+ "vm manager exited despite reconnect",
225+ );
226+
227+ supervisor.clients.clear();
228+ wait_for_exit(&mut supervisor.child, Duration::from_secs(10));
229+ let status = supervisor.child.wait().unwrap();
230+ assert!(status.success(), "vm manager exited with {status}");
231+}
232+
233+struct Supervisor {
234+ child: Child,
235+ socket_path: PathBuf,
236+ clients: Vec<UnixStream>,
237+ label: String,
238+}
239+
240+fn write_config(project: &Path, auto_shutdown_ms: u64) {
241+ let config = format!(
242+ r#"[box]
243+cpu_count = 2
244+ram_mb = 2048
245+disk_gb = 5
246+mounts = []
247+
248+[supervisor]
249+auto_shutdown_ms = {auto_shutdown_ms}
250+"#
251+ );
252+ fs::write(project.join("vibebox.toml"), config).unwrap();
253+}
254+
255+fn spawn_supervisor(
256+ temp: &TempDir,
257+ idx: usize,
258+ auto_shutdown_ms: u64,
259+ label: String,
260+) -> Supervisor {
261+ let home = temp.path().join(format!("home-{idx}"));
262+ let cache_home = temp.path().join(format!("cache-{idx}"));
263+ let project = temp.path().join(format!("project-{idx}"));
264+ fs::create_dir_all(&home).unwrap();
265+ fs::create_dir_all(&cache_home).unwrap();
266+ fs::create_dir_all(&project).unwrap();
267+ write_config(&project, auto_shutdown_ms);
268+
269+ let mut child = Command::new(assert_cmd::cargo_bin!("vibebox-supervisor"))
270+ .current_dir(&project)
271+ .env("HOME", &home)
272+ .env("XDG_CACHE_HOME", &cache_home)
273+ .env("VIBEBOX_INTERNAL", "1")
274+ .stdout(Stdio::piped())
275+ .stderr(Stdio::piped())
276+ .spawn()
277+ .unwrap();
278+ if let Some(stdout) = child.stdout.take() {
279+ spawn_prefix_reader(label.clone(), "stdout", stdout);
280+ }
281+ if let Some(stderr) = child.stderr.take() {
282+ spawn_prefix_reader(label.clone(), "stderr", stderr);
283+ }
284+
285+ let socket_path = project.join(".vibebox").join("vm.sock");
286+ wait_for_socket(&socket_path, Duration::from_secs(10));
287+
288+ Supervisor {
289+ child,
290+ socket_path,
291+ clients: Vec::new(),
292+ label,
293+ }
294+}
295+
296+fn connect_clients(
297+ socket_path: &Path,
298+ count: usize,
299+ timeout: Duration,
300+ require_all: bool,
301+ label: &str,
302+) -> Vec<UnixStream> {
303+ let (tx, rx) = mpsc::channel();
304+ let mut handles = Vec::with_capacity(count);
305+ for _ in 0..count {
306+ let path = socket_path.to_path_buf();
307+ let tx = tx.clone();
308+ handles.push(thread::spawn(move || {
309+ let stream = connect_client_with_retry(&path, timeout);
310+ let _ = tx.send(stream);
311+ }));
312+ }
313+ drop(tx);
314+ let mut clients = Vec::with_capacity(count);
315+ for stream in rx.into_iter().flatten() {
316+ clients.push(stream);
317+ }
318+ for handle in handles {
319+ handle.join().unwrap();
320+ }
321+ if require_all && clients.len() != count {
322+ panic!(
323+ "client count mismatch: expected {count} got {}",
324+ clients.len()
325+ );
326+ }
327+ if !require_all && clients.len() != count {
328+ log_line(
329+ label,
330+ &format!("connected {} of {count} clients", clients.len()),
331+ );
332+ }
333+ clients
334+}
335+
336+fn connect_client_with_retry(path: &Path, timeout: Duration) -> Option<UnixStream> {
337+ let start = Instant::now();
338+ loop {
339+ match UnixStream::connect(path) {
340+ Ok(stream) => return Some(stream),
341+ Err(_) => {
342+ if start.elapsed() > timeout {
343+ return None;
344+ }
345+ thread::sleep(Duration::from_millis(50));
346+ }
347+ }
348+ }
349+}
350+
351+fn wait_for_socket(path: &Path, timeout: Duration) {
352+ let start = Instant::now();
353+ loop {
354+ if UnixStream::connect(path).is_ok() {
355+ return;
356+ }
357+ if start.elapsed() > timeout {
358+ panic!("timed out waiting for socket {}", path.display());
359+ }
360+ thread::sleep(Duration::from_millis(100));
361+ }
362+}
363+
364+fn assert_manager_alive(child: &mut Child, message: &str) {
365+ assert!(child.try_wait().unwrap().is_none(), "{message}");
366+}
367+
368+fn assert_manager_alive_for(child: &mut Child, duration: Duration, message: &str) {
369+ let start = Instant::now();
370+ while start.elapsed() < duration {
371+ assert_manager_alive(child, message);
372+ thread::sleep(Duration::from_millis(100));
373+ }
374+}
375+
376+fn kill_supervisor(supervisor: &mut Supervisor, timeout: Duration) {
377+ supervisor.clients.clear();
378+ let _ = supervisor.child.kill();
379+ wait_for_exit(&mut supervisor.child, timeout);
380+ let _ = supervisor.child.wait();
381+}
382+
383+fn prune_exited_supervisors(supervisors: &mut Vec<Supervisor>, label: &str) {
384+ supervisors.retain_mut(|supervisor| {
385+ if supervisor.child.try_wait().unwrap().is_some() {
386+ log_line(label, &format!("removed exited {}", supervisor.label));
387+ false
388+ } else {
389+ true
390+ }
391+ });
392+}
393+
394+fn shutdown_supervisor(supervisor: &mut Supervisor, timeout: Duration) {
395+ supervisor.clients.clear();
396+ wait_for_exit(&mut supervisor.child, timeout);
397+ if supervisor.child.try_wait().unwrap().is_none() {
398+ let _ = supervisor.child.kill();
399+ let _ = supervisor.child.wait();
400+ }
401+}
402+
403+fn wait_for_exit(child: &mut Child, timeout: Duration) {
404+ let start = Instant::now();
405+ loop {
406+ if child.try_wait().unwrap().is_some() {
407+ return;
408+ }
409+ if start.elapsed() > timeout {
410+ panic!("timed out waiting for mock vm supervisor exit");
411+ }
412+ thread::sleep(Duration::from_millis(200));
413+ }
414+}
415+
416+fn spawn_prefix_reader(label: String, stream: &'static str, reader: impl Read + Send + 'static) {
417+ thread::spawn(move || {
418+ let buf = BufReader::new(reader);
419+ for line in buf.lines() {
420+ match line {
421+ Ok(line) => println!("[{}][{}] {}", label, stream, line),
422+ Err(err) => {
423+ eprintln!("[{}][{}] read error: {}", label, stream, err);
424+ break;
425+ }
426+ }
427+ }
428+ });
429+}
430+
431+fn log_line(prefix: &str, message: &str) {
432+ println!("[{}] {}", prefix, message);
433+}
434+
435+struct Lcg {
436+ state: u64,
437+}
438+
439+impl Lcg {
440+ fn new(seed: u64) -> Self {
441+ Self { state: seed }
442+ }
443+
444+ fn next_u32(&mut self) -> u32 {
445+ self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
446+ (self.state >> 32) as u32
447+ }
448+
449+ fn gen_range(&mut self, upper: usize) -> usize {
450+ if upper == 0 {
451+ return 0;
452+ }
453+ (self.next_u32() as usize) % upper
454+ }
455+}