//! The whole command line from a workstation (spec §8.6, spectra#21 and //! #23): the daemon on one machine, the owner, their agents and every //! command on another, with no ledger file on the workstation at all. mod common; use common::{add_agent, hermetic, run, text, Serving}; use serde_json::{json, Value}; use spectra_core::keys::KeyPair; use std::path::Path; use std::process::Stdio; use std::time::Duration; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; fn json_of(d: &Path, args: &[&str]) -> Value { let o = run(d, args); assert!(o.status.success(), "{args:?}: {}", text(&o)); serde_json::from_slice(&o.stdout) .unwrap_or_else(|e| panic!("{args:?}: one JSON document ({e}): {}", String::from_utf8_lossy(&o.stdout))) } /// A server in `s`, claimed by the owner whose home is `c/owner`. fn connected(s: &Path, c: &Path) -> (Serving, u16) { KeyPair::create_home(&c.join("owner"), "Andy McCall", "").unwrap(); let owner = KeyPair::load_home(&c.join("owner")).unwrap(); assert!(run(s, &["init", "--owner", &owner.key.to_hex(), "--name", "earth"]).status.success()); let (srv, port) = Serving::start_tls(s); let url = format!("https://127.0.0.1:{port}"); let o = run(c, &["connect", &url, "--yes"]); assert!(o.status.success(), "{}", text(&o)); (srv, port) } #[tokio::test] async fn every_command_works_from_another_machine() { let (sd, cd) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()); let (s, c) = (sd.path(), cd.path()); let (srv, _port) = connected(s, c); std::fs::create_dir_all(c.join("oxygen")).unwrap(); let o = run(c, &["project", "add", c.join("oxygen").to_str().unwrap()]); assert!(o.status.success(), "{}", text(&o)); add_agent(c, "claude-oxygen", &[]); add_agent(c, "spare", &[]); let id = json_of(c, &["identity", "show", "--json"]); assert_eq!(id["instance"]["name"], "earth"); assert_eq!(id["instance"]["delegated"], true, "{id}"); let agents = json_of(c, &["agent", "list", "--json"]); let claude = agents.as_array().unwrap().iter().find(|a| a["name"] == "claude-oxygen").unwrap(); assert_eq!(claude["state"], "ok", "{claude}"); assert_eq!(claude["display"], "claude-oxygen · Andy McCall's agent"); let projects = json_of(c, &["project", "list", "--json"]); assert_eq!(projects[0]["name"], "oxygen"); assert!(projects[0]["agents"].as_array().unwrap().contains(&json!("claude-oxygen"))); let o = run(c, &["agent", "disable", "spare"]); assert!(o.status.success(), "{}", text(&o)); assert_eq!(json_of(c, &["say", "--json", "-c", "general", "the release is tomorrow"])["channel"], "#general"); assert!(json_of(c, &["inbox", "--json"]).is_array()); assert_eq!(json_of(c, &["search", "--json", "release"]).as_array().unwrap().len(), 1); let brief: Value = serde_json::from_slice(&run(c, &["api", "GET", "/api/v1/brief"]).stdout).unwrap(); assert_eq!(brief["actor"], "human"); // An agent's shim on the workstation talks to the server. let mut cmd = tokio::process::Command::from(hermetic(c)); let home = c.join("agents/claude-oxygen"); cmd.args(["mcp", "--agent", "claude-oxygen", "--home", home.to_str().unwrap()]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true); let mut shim = cmd.spawn().unwrap(); let mut stdin = shim.stdin.take().unwrap(); let mut lines = BufReader::new(shim.stdout.take().unwrap()).lines(); for v in [ json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "t", "version": "1" } } }), json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "brief", "arguments": {} } }), ] { stdin.write_all(format!("{v}\n").as_bytes()).await.unwrap(); } stdin.flush().await.unwrap(); let mut brief = Value::Null; for _ in 0..2 { let line = tokio::time::timeout(Duration::from_secs(20), lines.next_line()).await.unwrap().unwrap().unwrap(); let v: Value = serde_json::from_str(&line).unwrap(); if v["id"] == 2 { brief = v; } } assert!(brief["result"]["content"][0]["text"].as_str().unwrap().contains("You are claude-oxygen · Andy McCall's agent"), "{brief}"); drop(shim); // The ledger stays on the server: verify asks it; check and rebuild refuse here. let v = json_of(c, &["admin", "verify", "--json"]); assert!(v["findings"].as_array().unwrap().is_empty(), "{v}"); let o = run(c, &["admin", "check"]); assert!(!o.status.success() && text(&o).contains("runs on the server"), "{}", text(&o)); assert!(!c.join("data").exists(), "the workstation has no data directory"); srv.stop(); let o = run(s, &["admin", "verify"]); assert!(o.status.success(), "{}", text(&o)); }