OXYGENHUB
# b576d05cf862 spectra / tests / common / mod.rs
4.7 KiB · Rust · last changed Serve other machines over TLS, trusted through the instance key (#20) 1 day ago source blame history raw
1
//! Helpers for end-to-end tests: a hermetic environment, the owner's
2
//! delegation as `oxy identity delegate add` makes it, agents added through
3
//! the CLI, and the daemon started and stopped as a real process.
4
#![allow(dead_code)]
5
6
use oxy_core::durable::{write_atomic, write_atomic_private};
7
use oxy_core::identity::{Delegate, SecretKey};
8
use spectra_core::keys::KeyPair;
9
use spectra_core::principals::DELEGATION_SCOPE;
10
use std::path::{Path, PathBuf};
11
use std::process::{Child, Command, Output};
12
13
pub fn hermetic(dir: &Path) -> Command {
14
    let mut c = Command::new(env!("CARGO_BIN_EXE_spectra"));
15
    c.env("SPECTRA_DATA", dir.join("data"))
16
        .env("OXY_HOME", dir.join("owner"))
17
        .env("HOME", dir)
18
        .env("XDG_DATA_HOME", dir.join("xdg"))
19
        .env("NO_COLOR", "1");
20
    c
21
}
22
23
pub fn run(dir: &Path, args: &[&str]) -> Output {
24
    hermetic(dir).args(args).output().unwrap()
25
}
26
27
pub fn text(o: &Output) -> String {
28
    format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))
29
}
30
31
/// `oxy identity delegate add` in the owner's home.
32
pub fn delegate(owner_home: &Path, agent: &KeyPair) {
33
    let cur = KeyPair::load_home(owner_home).unwrap();
34
    let keys = owner_home.join("keys");
35
    let (_, secret) = SecretKey::decode(&std::fs::read(keys.join(format!("{}.secret", cur.key.to_hex()))).unwrap()).unwrap();
36
    let mut d = cur.identity.delegates.clone();
37
    d.push(Delegate { key: agent.key, scope: DELEGATION_SCOPE, expires: None });
38
    let next = cur.identity.successor(d, None, spectra_core::ids::now_ms());
39
    let k = next.hash();
40
    write_atomic_private(&keys.join(format!("{}.secret", k.to_hex())), &secret.encode(&k)).unwrap();
41
    write_atomic(&keys.join(format!("{}.identity", k.to_hex())), &next.encode()).unwrap();
42
    oxy_core::repo::Config::set_user(&owner_home.join("config.toml"), "author.key", &k.to_hex()).unwrap();
43
}
44
45
/// Make an agent's home, delegate to it, and add it.
46
pub fn add_agent(d: &Path, name: &str, extra: &[&str]) -> KeyPair {
47
    let home = d.join("agents").join(name);
48
    let pair = KeyPair::create_home(&home, name, "").unwrap();
49
    delegate(&d.join("owner"), &pair);
50
    let mut args =
51
        vec!["agent", "add", name, "--kind", "claude-code", "--project", "oxygen", "--home", home.to_str().unwrap(), "--wait", "0"];
52
    args.extend_from_slice(extra);
53
    let o = run(d, &args);
54
    assert!(o.status.success(), "{}", text(&o));
55
    pair
56
}
57
58
pub struct Serving {
59
    pub child: Child,
60
    pub socket: PathBuf,
61
}
62
63
impl Serving {
64
    pub fn start(d: &Path) -> Serving {
65
        let mut child = hermetic(d).args(["serve", "--bind", "127.0.0.1:0"]).stdout(std::process::Stdio::piped()).spawn().unwrap();
66
        let socket = d.join("data/spectra.sock");
67
        for _ in 0..200 {
68
            if std::os::unix::net::UnixStream::connect(&socket).is_ok() {
69
                return Serving { child, socket };
70
            }
71
            std::thread::sleep(std::time::Duration::from_millis(25));
72
        }
73
        child.kill().ok();
74
        child.wait().ok();
75
        panic!("spectra serve did not start");
76
    }
77
    /// Serve with TLS on a loopback port of its own; returns the port.
78
    pub fn start_tls(d: &Path) -> (Serving, u16) {
79
        use std::io::BufRead;
80
        let mut child =
81
            hermetic(d).args(["serve", "--bind", "127.0.0.1:0", "--tls", "on"]).stdout(std::process::Stdio::piped()).spawn().unwrap();
82
        let out = child.stdout.take().unwrap();
83
        let (tx, rx) = std::sync::mpsc::channel();
84
        std::thread::spawn(move || {
85
            for line in std::io::BufReader::new(out).lines().map_while(Result::ok) {
86
                if let Some(url) = line.split_whitespace().find(|w| w.starts_with("https://")) {
87
                    let port: u16 = url.rsplit(':').next().unwrap().parse().unwrap();
88
                    let _ = tx.send(port);
89
                }
90
            }
91
        });
92
        let port = rx.recv_timeout(std::time::Duration::from_secs(10)).expect("spectra serve printed its https address");
93
        (Serving { child, socket: d.join("data/spectra.sock") }, port)
94
    }
95
96
    pub fn stop(mut self) {
97
        unsafe { libc::kill(self.child.id() as i32, libc::SIGTERM) };
98
        let status = self.child.wait().unwrap();
99
        assert!(status.success(), "a clean stop on SIGTERM");
100
        assert!(!self.socket.exists(), "the socket is removed");
101
    }
102
}
103
104
pub fn setup() -> tempfile::TempDir {
105
    let dir = tempfile::tempdir().unwrap();
106
    let d = dir.path();
107
    KeyPair::create_home(&d.join("owner"), "Andy McCall", "").unwrap();
108
    assert!(run(d, &["init"]).status.success());
109
    std::fs::create_dir_all(d.join("oxygen")).unwrap();
110
    assert!(run(d, &["project", "add", d.join("oxygen").to_str().unwrap()]).status.success());
111
    dir
112
}