OXYGENHUB
◆ v0.1.0 spectra / tests / common / mod.rs
4.1 KiB · Rust · last changed Spectra stands alone: its own identities, signatures and CBOR; projects of any kind of work (#26, #27) 1 day ago source blame history raw
037c45d8 Andy McCall ↶ 1
//! Helpers for end-to-end tests: a hermetic environment, the owner's
98a0fb9f Andy McCall ↶ 2
//! delegation as Spectra makes it, agents added through the CLI, and the
3
//! daemon started and stopped as a real process.
037c45d8 Andy McCall ↶ 4
#![allow(dead_code)]
5
6
use spectra_core::keys::KeyPair;
7
use std::path::{Path, PathBuf};
8
use std::process::{Child, Command, Output};
9
10
pub fn hermetic(dir: &Path) -> Command {
11
    let mut c = Command::new(env!("CARGO_BIN_EXE_spectra"));
12
    c.env("SPECTRA_DATA", dir.join("data"))
98a0fb9f Andy McCall ↶ 13
        .env("SPECTRA_HOME", dir.join("owner"))
037c45d8 Andy McCall ↶ 14
        .env("HOME", dir)
15
        .env("XDG_DATA_HOME", dir.join("xdg"))
16
        .env("NO_COLOR", "1");
17
    c
18
}
19
20
pub fn run(dir: &Path, args: &[&str]) -> Output {
21
    hermetic(dir).args(args).output().unwrap()
22
}
23
24
pub fn text(o: &Output) -> String {
25
    format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))
26
}
27
98a0fb9f Andy McCall ↶ 28
/// The owner delegates to `key`, as `spectra agent add` and `connect` do.
037c45d8 Andy McCall ↶ 29
pub fn delegate(owner_home: &Path, agent: &KeyPair) {
98a0fb9f Andy McCall ↶ 30
    spectra_core::keys::delegate(owner_home, agent.key, None).unwrap();
037c45d8 Andy McCall ↶ 31
}
32
98a0fb9f Andy McCall ↶ 33
/// Add an agent through the CLI, confirming the delegation.
037c45d8 Andy McCall ↶ 34
pub fn add_agent(d: &Path, name: &str, extra: &[&str]) -> KeyPair {
35
    let home = d.join("agents").join(name);
98a0fb9f Andy McCall ↶ 36
    let mut args = vec!["agent", "add", name, "--kind", "claude-code", "--project", "oxygen", "--home", home.to_str().unwrap(), "--yes"];
037c45d8 Andy McCall ↶ 37
    args.extend_from_slice(extra);
38
    let o = run(d, &args);
39
    assert!(o.status.success(), "{}", text(&o));
98a0fb9f Andy McCall ↶ 40
    KeyPair::load_home(&home).unwrap()
037c45d8 Andy McCall ↶ 41
}
42
43
pub struct Serving {
44
    pub child: Child,
45
    pub socket: PathBuf,
46
}
47
48
impl Serving {
49
    pub fn start(d: &Path) -> Serving {
50
        let mut child = hermetic(d).args(["serve", "--bind", "127.0.0.1:0"]).stdout(std::process::Stdio::piped()).spawn().unwrap();
51
        let socket = d.join("data/spectra.sock");
52
        for _ in 0..200 {
53
            if std::os::unix::net::UnixStream::connect(&socket).is_ok() {
54
                return Serving { child, socket };
55
            }
56
            std::thread::sleep(std::time::Duration::from_millis(25));
57
        }
58
        child.kill().ok();
59
        child.wait().ok();
60
        panic!("spectra serve did not start");
61
    }
b576d05c Andy McCall ↶ 62
    /// Serve with TLS on a loopback port of its own; returns the port.
63
    pub fn start_tls(d: &Path) -> (Serving, u16) {
64
        use std::io::BufRead;
65
        let mut child =
66
            hermetic(d).args(["serve", "--bind", "127.0.0.1:0", "--tls", "on"]).stdout(std::process::Stdio::piped()).spawn().unwrap();
67
        let out = child.stdout.take().unwrap();
68
        let (tx, rx) = std::sync::mpsc::channel();
69
        std::thread::spawn(move || {
70
            for line in std::io::BufReader::new(out).lines().map_while(Result::ok) {
71
                if let Some(url) = line.split_whitespace().find(|w| w.starts_with("https://")) {
72
                    let port: u16 = url.rsplit(':').next().unwrap().parse().unwrap();
73
                    let _ = tx.send(port);
74
                }
75
            }
76
        });
77
        let port = rx.recv_timeout(std::time::Duration::from_secs(10)).expect("spectra serve printed its https address");
78
        (Serving { child, socket: d.join("data/spectra.sock") }, port)
79
    }
80
037c45d8 Andy McCall ↶ 81
    pub fn stop(mut self) {
82
        unsafe { libc::kill(self.child.id() as i32, libc::SIGTERM) };
83
        let status = self.child.wait().unwrap();
84
        assert!(status.success(), "a clean stop on SIGTERM");
85
        assert!(!self.socket.exists(), "the socket is removed");
86
    }
87
}
88
77df591d Andy McCall ↶ 89
/// A test that fails mid-way must not leave its daemon running: an orphan
90
/// holds the test's output open and looks like a hang.
91
impl Drop for Serving {
92
    fn drop(&mut self) {
93
        if let Ok(None) = self.child.try_wait() {
94
            let _ = self.child.kill();
95
            let _ = self.child.wait();
96
        }
97
    }
98
}
99
037c45d8 Andy McCall ↶ 100
pub fn setup() -> tempfile::TempDir {
101
    let dir = tempfile::tempdir().unwrap();
102
    let d = dir.path();
103
    KeyPair::create_home(&d.join("owner"), "Andy McCall", "").unwrap();
104
    assert!(run(d, &["init"]).status.success());
105
    std::fs::create_dir_all(d.join("oxygen")).unwrap();
106
    assert!(run(d, &["project", "add", d.join("oxygen").to_str().unwrap()]).status.success());
107
    dir
108
}