//! Every `--json` output is one JSON document on stdout and nothing else //! (as `oxy`'s are), with and without the daemon; and `spectra say` with //! `spectra inbox --follow` work as a terminal chat with the agents. mod common; use common::{add_agent, hermetic, run, setup, text, Serving}; use serde_json::{json, Value}; use spectra_mcp::client::Client; use std::io::{BufRead, BufReader}; use std::process::Stdio; use std::time::Duration; fn json_of(d: &std::path::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:?} printed more than one JSON document ({e}): {}", String::from_utf8_lossy(&o.stdout))) } #[test] fn every_json_output_parses_without_the_daemon() { let dir = setup(); let d = dir.path(); add_agent(d, "claude-oxygen", &[]); assert_eq!(json_of(d, &["version", "--json"])["ledger_format"], 1); assert_eq!(json_of(d, &["identity", "show", "--json"])["owner"]["name"], "Andy McCall"); assert_eq!(json_of(d, &["agent", "list", "--json"])[0]["name"], "claude-oxygen"); assert_eq!(json_of(d, &["project", "list", "--json"])[0]["name"], "oxygen"); let posted = json_of(d, &["say", "--json", "-c", "general", "the release is tomorrow @claude-oxygen"]); assert_eq!(posted["channel"], "#general"); assert!(json_of(d, &["inbox", "--json"]).as_array().unwrap().is_empty()); assert_eq!(json_of(d, &["search", "--json", "release"]).as_array().unwrap().len(), 1); let v = json_of(d, &["admin", "verify", "--json"]); assert!(v["findings"].as_array().unwrap().is_empty(), "a CLI post verifies: {v}"); } #[tokio::test] async fn a_terminal_chat_with_the_agents() { let dir = setup(); let d = dir.path(); let claude = add_agent(d, "claude-oxygen", &[]); let srv = Serving::start(d); let a = Client::new(&srv.socket, Some(claude)); // Through the running daemon: say, and the api proxy. assert_eq!(json_of(d, &["say", "--json", "hello from the terminal"])["channel"], "#general"); let api: Value = serde_json::from_slice(&run(d, &["api", "GET", "/api/v1/brief"]).stdout).unwrap(); assert_eq!(api["actor"], "human"); let mut follow = hermetic(d).args(["inbox", "--follow", "--json"]).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap(); let mut lines = BufReader::new(follow.stdout.take().unwrap()).lines(); tokio::time::sleep(Duration::from_millis(500)).await; let say = json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "say", "arguments": { "body": "@andy the build is green", "channel": "general" } } }); assert_eq!(a.mcp("2025-11-25", &say).await.unwrap().status, 200); let got = tokio::task::spawn_blocking(move || lines.next()).await.unwrap().expect("a line").unwrap(); let item: Value = serde_json::from_str(&got).unwrap(); assert_eq!(item["from"], "agent:claude-oxygen"); assert!(item["summary"].as_str().unwrap().contains("the build is green")); follow.kill().ok(); follow.wait().ok(); srv.stop(); }