#![cfg(unix)]
use std::io::Write;
use std::path::Path;
use std::sync::mpsc::{Receiver, channel};
use std::time::{Duration, Instant};
use cellix::hold::{self, Held, Job};
fn job(name: &str, script: &str, folder: &Path) -> Job {
Job {
name: name.to_string(),
argv: vec!["sh".to_string(), "-c".to_string(), script.to_string()],
env: vec!["CELLIX_MARK=seen".to_string()],
folder: folder.to_path_buf(),
size: (80, 24),
pixels: (0, 0),
}
}
fn start(folder: &Path, job: &Job) -> Held {
hold::start(Path::new(env!("CARGO_BIN_EXE_cellix")), folder, job).unwrap()
}
fn attach(held: &Held) -> (Receiver<Vec<u8>>, Receiver<()>) {
let (output, seen) = channel();
let (done, over) = channel();
held.listen(output, done).unwrap();
(seen, over)
}
fn read_until(seen: &Receiver<Vec<u8>>, wanted: &str) -> String {
let deadline = Instant::now() + Duration::from_secs(10);
let mut text = String::new();
while Instant::now() < deadline {
if let Ok(bytes) = seen.recv_timeout(Duration::from_millis(100)) {
text.push_str(&String::from_utf8_lossy(&bytes));
if text.contains(wanted) {
return text;
}
}
}
panic!("no {wanted} in: {text}");
}
#[test]
fn a_program_runs_on_with_nobody_attached_and_shows_what_it_did() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("held");
let script = "echo first $CELLIX_MARK; read line; echo got $line; sleep 30";
let held = start(&folder, &job("Shell", script, dir.path()));
let (seen, _) = attach(&held);
read_until(&seen, "first seen");
held.keys().unwrap().write_all(b"hello\n").unwrap();
read_until(&seen, "got hello");
let socket = held.socket.clone();
drop(seen);
drop(held);
let running = hold::left_running(&folder);
assert_eq!(running.len(), 1);
assert_eq!(running[0].0, "Shell");
let again = Held::connect(&socket).unwrap();
let (seen, _) = attach(&again);
let text = read_until(&seen, "got hello");
assert!(text.contains("first seen"), "{text}");
again.kill();
}
#[test]
fn a_program_that_ends_while_nobody_is_attached_is_told_about_later() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("held");
let held = start(&folder, &job("Job", "echo finished; exit 0", dir.path()));
let socket = held.socket.clone();
drop(held);
std::thread::sleep(Duration::from_millis(500));
let again = Held::connect(&socket).unwrap();
let (seen, over) = attach(&again);
read_until(&seen, "finished");
over.recv_timeout(Duration::from_secs(5)).unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
while !hold::left_running(&folder).is_empty() {
assert!(Instant::now() < deadline, "the host should have gone");
std::thread::sleep(Duration::from_millis(20));
}
}
#[test]
fn stopping_a_held_program_ends_it() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("held");
let held = start(&folder, &job("Long", "echo up; sleep 60", dir.path()));
let (seen, over) = attach(&held);
read_until(&seen, "up");
held.kill();
over.recv_timeout(Duration::from_secs(5)).unwrap();
}
#[test]
fn a_new_size_reaches_the_program() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("held");
let script = "trap 'stty size' WINCH; echo ready; sleep 5 & wait; sleep 5";
let held = start(&folder, &job("Size", script, dir.path()));
let (seen, _) = attach(&held);
read_until(&seen, "ready");
held.resize((100, 30), (1000, 600));
read_until(&seen, "30 100");
held.kill();
}
#[test]
fn a_dead_host_leaves_nothing_behind() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("held");
std::fs::create_dir_all(&folder).unwrap();
std::fs::write(folder.join("9-9.sock"), "").unwrap();
std::fs::write(folder.join("9-9.json"), "{}").unwrap();
assert!(hold::left_running(&folder).is_empty());
assert_eq!(std::fs::read_dir(&folder).unwrap().count(), 0);
}
#[test]
fn a_host_that_dies_with_the_machine_leaves_what_resumes_it() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("held");
let script = r#"echo working on it; printf '{"agent":"Claude Code","session":"abc-123"}' > "${CELLIX_HELD%.sock}.agent"; echo noted; sleep 60"#;
let held = start(&folder, &job("Claude Code", script, dir.path()));
let (seen, _) = attach(&held);
read_until(&seen, "noted");
let socket = held.socket.clone();
drop(seen);
drop(held);
let killed = std::process::Command::new("pkill")
.args(["-KILL", "-f", &socket.display().to_string()])
.status()
.unwrap();
assert!(killed.success(), "the host should have been found");
let deadline = Instant::now() + Duration::from_secs(5);
while std::os::unix::net::UnixStream::connect(&socket).is_ok() {
assert!(Instant::now() < deadline, "the host should have died");
std::thread::sleep(Duration::from_millis(20));
}
assert!(hold::left_running(&folder).is_empty());
let stopped = hold::stopped(&folder);
assert_eq!(stopped.len(), 1, "{stopped:?}");
let stopped = &stopped[0];
assert_eq!(stopped.job.name, "Claude Code");
assert_eq!(stopped.job.folder, dir.path());
let output = String::from_utf8_lossy(&stopped.output);
assert!(output.contains("working on it"), "{output}");
let agent = stopped.agent.as_ref().expect("the agent's word");
assert_eq!(agent.session, "abc-123");
assert!(stopped.job.env.is_empty(), "{:?}", stopped.job.env);
assert_eq!(hold::stopped(&folder).len(), 1);
stopped.forget();
assert!(hold::stopped(&folder).is_empty());
}
#[test]
fn a_program_that_finishes_leaves_nothing_to_resume() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("held");
let held = start(&folder, &job("Job", "echo finished; exit 0", dir.path()));
let (seen, over) = attach(&held);
read_until(&seen, "finished");
over.recv_timeout(Duration::from_secs(5)).unwrap();
drop(held);
let deadline = Instant::now() + Duration::from_secs(5);
while std::fs::read_dir(&folder)
.unwrap()
.flatten()
.any(|e| e.path().is_file())
{
assert!(Instant::now() < deadline, "the host should have cleared up");
std::thread::sleep(Duration::from_millis(20));
}
assert!(hold::stopped(&folder).is_empty());
}
#[test]
fn two_cellix_windows_share_a_program_and_whoever_types_sets_its_size() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("held");
let script = r#"echo ready; while read line; do echo "got $line at $(stty size)"; done"#;
let held = start(&folder, &job("Shared", script, dir.path()));
let (first, _) = attach(&held);
read_until(&first, "ready");
held.resize((100, 30), (1000, 600));
let other = Held::connect(&held.socket).unwrap();
let (second, _) = attach(&other);
read_until(&second, "ready");
other.resize((80, 24), (800, 480));
held.keys().unwrap().write_all(b"one\n").unwrap();
read_until(&first, "got one at 30 100");
read_until(&second, "got one at 30 100");
other.keys().unwrap().write_all(b"two\n").unwrap();
read_until(&second, "got two at 24 80");
read_until(&first, "got two at 24 80");
drop(second);
drop(other);
held.keys().unwrap().write_all(b"three\n").unwrap();
read_until(&first, "got three at 30 100");
held.kill();
}