| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
mod common; |
| 6 |
|
| 7 |
use common::{add_agent, 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 |
|
| 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 |
let o = run(c, &["connect", &url, "--yes"]); |
| 30 |
assert!(o.status.success(), "{}", text(&o)); |
| 31 |
(srv, port) |
| 32 |
} |
| 33 |
|
| 34 |
#[tokio::test] |
| 35 |
async fn every_command_works_from_another_machine() { |
| 36 |
let (sd, cd) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()); |
| 37 |
let (s, c) = (sd.path(), cd.path()); |
| 38 |
let (srv, _port) = connected(s, c); |
| 39 |
|
| 40 |
std::fs::create_dir_all(c.join("oxygen")).unwrap(); |
| 41 |
let o = run(c, &["project", "add", c.join("oxygen").to_str().unwrap()]); |
| 42 |
assert!(o.status.success(), "{}", text(&o)); |
| 43 |
add_agent(c, "claude-oxygen", &[]); |
| 44 |
add_agent(c, "spare", &[]); |
| 45 |
|
| 46 |
let id = json_of(c, &["identity", "show", "--json"]); |
| 47 |
assert_eq!(id["instance"]["name"], "earth"); |
| 48 |
assert_eq!(id["instance"]["delegated"], true, "{id}"); |
| 49 |
let agents = json_of(c, &["agent", "list", "--json"]); |
| 50 |
let claude = agents.as_array().unwrap().iter().find(|a| a["name"] == "claude-oxygen").unwrap(); |
| 51 |
assert_eq!(claude["state"], "ok", "{claude}"); |
| 52 |
assert_eq!(claude["display"], "claude-oxygen · Andy McCall's agent"); |
| 53 |
let projects = json_of(c, &["project", "list", "--json"]); |
| 54 |
assert_eq!(projects[0]["name"], "oxygen"); |
| 55 |
assert!(projects[0]["agents"].as_array().unwrap().contains(&json!("claude-oxygen"))); |
| 56 |
let o = run(c, &["agent", "disable", "spare"]); |
| 57 |
assert!(o.status.success(), "{}", text(&o)); |
| 58 |
|
| 59 |
assert_eq!(json_of(c, &["say", "--json", "-c", "general", "the release is tomorrow"])["channel"], "#general"); |
| 60 |
assert!(json_of(c, &["inbox", "--json"]).is_array()); |
| 61 |
assert_eq!(json_of(c, &["search", "--json", "release"]).as_array().unwrap().len(), 1); |
| 62 |
let brief: Value = serde_json::from_slice(&run(c, &["api", "GET", "/api/v1/brief"]).stdout).unwrap(); |
| 63 |
assert_eq!(brief["actor"], "human"); |
| 64 |
|
| 65 |
|
| 66 |
let mut cmd = tokio::process::Command::from(hermetic(c)); |
| 67 |
let home = c.join("agents/claude-oxygen"); |
| 68 |
cmd.args(["mcp", "--agent", "claude-oxygen", "--home", home.to_str().unwrap()]) |
| 69 |
.stdin(Stdio::piped()) |
| 70 |
.stdout(Stdio::piped()) |
| 71 |
.stderr(Stdio::piped()) |
| 72 |
.kill_on_drop(true); |
| 73 |
let mut shim = cmd.spawn().unwrap(); |
| 74 |
let mut stdin = shim.stdin.take().unwrap(); |
| 75 |
let mut lines = BufReader::new(shim.stdout.take().unwrap()).lines(); |
| 76 |
for v in [ |
| 77 |
json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "t", "version": "1" } } }), |
| 78 |
json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "brief", "arguments": {} } }), |
| 79 |
] { |
| 80 |
stdin.write_all(format!("{v}\n").as_bytes()).await.unwrap(); |
| 81 |
} |
| 82 |
stdin.flush().await.unwrap(); |
| 83 |
let mut brief = Value::Null; |
| 84 |
for _ in 0..2 { |
| 85 |
let line = tokio::time::timeout(Duration::from_secs(20), lines.next_line()).await.unwrap().unwrap().unwrap(); |
| 86 |
let v: Value = serde_json::from_str(&line).unwrap(); |
| 87 |
if v["id"] == 2 { |
| 88 |
brief = v; |
| 89 |
} |
| 90 |
} |
| 91 |
assert!(brief["result"]["content"][0]["text"].as_str().unwrap().contains("You are claude-oxygen · Andy McCall's agent"), "{brief}"); |
| 92 |
drop(shim); |
| 93 |
|
| 94 |
|
| 95 |
let v = json_of(c, &["admin", "verify", "--json"]); |
| 96 |
assert!(v["findings"].as_array().unwrap().is_empty(), "{v}"); |
| 97 |
let o = run(c, &["admin", "check"]); |
| 98 |
assert!(!o.status.success() && text(&o).contains("runs on the server"), "{}", text(&o)); |
| 99 |
assert!(!c.join("data").exists(), "the workstation has no data directory"); |
| 100 |
srv.stop(); |
| 101 |
|
| 102 |
let o = run(s, &["admin", "verify"]); |
| 103 |
assert!(o.status.success(), "{}", text(&o)); |
| 104 |
} |