OXYGENHUB
# 67bfa06a894f spectra / tests / client.rs
5.0 KiB · Rust · last changed The command line from another machine: every command through the API (#23, completes #21) 1 day ago source blame history raw
1
//! The whole command line from a workstation (spec §8.6, spectra#21 and
2
//! #23): the daemon on one machine, the owner, their agents and every
3
//! command on another, with no ledger file on the workstation at all.
4
5
mod common;
6
7
use common::{add_agent, delegate, hermetic, run, text, Serving};
8
use serde_json::{json, Value};
9
use spectra_core::keys::KeyPair;
10
use std::path::Path;
11
use std::process::Stdio;
12
use std::time::Duration;
13
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
14
15
fn json_of(d: &Path, args: &[&str]) -> Value {
16
    let o = run(d, args);
17
    assert!(o.status.success(), "{args:?}: {}", text(&o));
18
    serde_json::from_slice(&o.stdout)
19
        .unwrap_or_else(|e| panic!("{args:?}: one JSON document ({e}): {}", String::from_utf8_lossy(&o.stdout)))
20
}
21
22
/// A server in `s`, claimed by the owner whose home is `c/owner`.
23
fn connected(s: &Path, c: &Path) -> (Serving, u16) {
24
    KeyPair::create_home(&c.join("owner"), "Andy McCall", "").unwrap();
25
    let owner = KeyPair::load_home(&c.join("owner")).unwrap();
26
    assert!(run(s, &["init", "--owner", &owner.key.to_hex(), "--name", "earth"]).status.success());
27
    let (srv, port) = Serving::start_tls(s);
28
    let url = format!("https://127.0.0.1:{port}");
29
    assert_eq!(run(c, &["connect", &url]).status.code(), Some(1));
30
    delegate(&c.join("owner"), &KeyPair::instance(&s.join("data"), "earth").unwrap());
31
    let o = run(c, &["connect", &url]);
32
    assert!(o.status.success(), "{}", text(&o));
33
    (srv, port)
34
}
35
36
#[tokio::test]
37
async fn every_command_works_from_another_machine() {
38
    let (sd, cd) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
39
    let (s, c) = (sd.path(), cd.path());
40
    let (srv, _port) = connected(s, c);
41
42
    std::fs::create_dir_all(c.join("oxygen")).unwrap();
43
    let o = run(c, &["project", "add", c.join("oxygen").to_str().unwrap()]);
44
    assert!(o.status.success(), "{}", text(&o));
45
    add_agent(c, "claude-oxygen", &[]);
46
    add_agent(c, "spare", &[]);
47
48
    let id = json_of(c, &["identity", "show", "--json"]);
49
    assert_eq!(id["instance"]["name"], "earth");
50
    assert_eq!(id["instance"]["delegated"], true, "{id}");
51
    let agents = json_of(c, &["agent", "list", "--json"]);
52
    let claude = agents.as_array().unwrap().iter().find(|a| a["name"] == "claude-oxygen").unwrap();
53
    assert_eq!(claude["state"], "ok", "{claude}");
54
    assert_eq!(claude["display"], "claude-oxygen · Andy McCall's agent");
55
    let projects = json_of(c, &["project", "list", "--json"]);
56
    assert_eq!(projects[0]["name"], "oxygen");
57
    assert!(projects[0]["agents"].as_array().unwrap().contains(&json!("claude-oxygen")));
58
    let o = run(c, &["agent", "disable", "spare"]);
59
    assert!(o.status.success(), "{}", text(&o));
60
61
    assert_eq!(json_of(c, &["say", "--json", "-c", "general", "the release is tomorrow"])["channel"], "#general");
62
    assert!(json_of(c, &["inbox", "--json"]).is_array());
63
    assert_eq!(json_of(c, &["search", "--json", "release"]).as_array().unwrap().len(), 1);
64
    let brief: Value = serde_json::from_slice(&run(c, &["api", "GET", "/api/v1/brief"]).stdout).unwrap();
65
    assert_eq!(brief["actor"], "human");
66
67
    // An agent's shim on the workstation talks to the server.
68
    let mut cmd = tokio::process::Command::from(hermetic(c));
69
    let home = c.join("agents/claude-oxygen");
70
    cmd.args(["mcp", "--agent", "claude-oxygen", "--home", home.to_str().unwrap()])
71
        .stdin(Stdio::piped())
72
        .stdout(Stdio::piped())
73
        .stderr(Stdio::piped())
74
        .kill_on_drop(true);
75
    let mut shim = cmd.spawn().unwrap();
76
    let mut stdin = shim.stdin.take().unwrap();
77
    let mut lines = BufReader::new(shim.stdout.take().unwrap()).lines();
78
    for v in [
79
        json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "t", "version": "1" } } }),
80
        json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "brief", "arguments": {} } }),
81
    ] {
82
        stdin.write_all(format!("{v}\n").as_bytes()).await.unwrap();
83
    }
84
    stdin.flush().await.unwrap();
85
    let mut brief = Value::Null;
86
    for _ in 0..2 {
87
        let line = tokio::time::timeout(Duration::from_secs(20), lines.next_line()).await.unwrap().unwrap().unwrap();
88
        let v: Value = serde_json::from_str(&line).unwrap();
89
        if v["id"] == 2 {
90
            brief = v;
91
        }
92
    }
93
    assert!(brief["result"]["content"][0]["text"].as_str().unwrap().contains("You are claude-oxygen · Andy McCall's agent"), "{brief}");
94
    drop(shim);
95
96
    // The ledger stays on the server: verify asks it; check and rebuild refuse here.
97
    let v = json_of(c, &["admin", "verify", "--json"]);
98
    assert!(v["findings"].as_array().unwrap().is_empty(), "{v}");
99
    let o = run(c, &["admin", "check"]);
100
    assert!(!o.status.success() && text(&o).contains("runs on the server"), "{}", text(&o));
101
    assert!(!c.join("data").exists(), "the workstation has no data directory");
102
    srv.stop();
103
104
    let o = run(s, &["admin", "verify"]);
105
    assert!(o.status.success(), "{}", text(&o));
106
}