//! Projects from the command line (spec ยง23.1): relations come from //! `.oxymodules`, matched by remote, whichever is registered first; an agent //! needs a registered project. use spectra_core::keys::KeyPair; use std::path::Path; use std::process::{Command, Output}; fn hermetic(dir: &Path) -> Command { let mut c = Command::new(env!("CARGO_BIN_EXE_spectra")); c.env("SPECTRA_DATA", dir.join("data")) .env("OXY_HOME", dir.join("owner")) .env("HOME", dir) .env("XDG_DATA_HOME", dir.join("xdg")) .env("NO_COLOR", "1"); c } fn run(dir: &Path, args: &[&str]) -> Output { hermetic(dir).args(args).output().unwrap() } fn text(o: &Output) -> String { format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr)) } /// A checkout as `oxy` leaves one: an id, a remote, and the subrepos it pins. fn checkout(at: &Path, remote: &str, pins: &[&str]) { std::fs::create_dir_all(at.join(".oxy")).unwrap(); std::fs::write(at.join(".oxy/id"), "0123456789abcdef0123456789abcdef").unwrap(); std::fs::write(at.join(".oxy/config"), format!("remote.origin.url = \"{remote}\"\n")).unwrap(); let modules: String = pins.iter().enumerate().map(|(i, u)| format!("[deps/{i}]\nurl = {u}\nbookmark = main\n")).collect(); std::fs::write(at.join(".oxymodules"), modules).unwrap(); } fn setup() -> tempfile::TempDir { let dir = tempfile::tempdir().unwrap(); KeyPair::create_home(&dir.path().join("owner"), "Andy McCall", "").unwrap(); assert!(run(dir.path(), &["init"]).status.success()); dir } fn projects(d: &Path) -> serde_json::Value { serde_json::from_slice(&run(d, &["project", "list", "--json"]).stdout).unwrap() } #[test] fn relations_are_found_whichever_project_comes_first() { for hub_first in [false, true] { let dir = setup(); let d = dir.path(); checkout(&d.join("oxygen"), "https://hub.example/andy/oxygen", &[]); checkout(&d.join("oxygenhub"), "https://hub.example/andy/oxygenhub", &["https://hub.example/andy/oxygen"]); let order = if hub_first { ["oxygenhub", "oxygen"] } else { ["oxygen", "oxygenhub"] }; for p in order { let o = run(d, &["project", "add", d.join(p).to_str().unwrap()]); assert!(o.status.success(), "{}", text(&o)); } let list = projects(d); let hub = list.as_array().unwrap().iter().find(|p| p["name"] == "oxygenhub").unwrap(); let oxy = list.as_array().unwrap().iter().find(|p| p["name"] == "oxygen").unwrap(); assert_eq!(hub["depends_on"], serde_json::json!(["oxygen"]), "hub first: {hub_first}"); assert_eq!(oxy["depended_on_by"], serde_json::json!(["oxygenhub"])); assert!(run(d, &["admin", "check"]).status.success()); assert!(run(d, &["admin", "verify"]).status.success()); } } #[test] fn a_project_is_registered_once_and_an_agent_needs_one() { let dir = setup(); let d = dir.path(); checkout(&d.join("oxygen"), "https://hub.example/andy/oxygen", &[]); assert!(run(d, &["project", "add", d.join("oxygen").to_str().unwrap()]).status.success()); let again = run(d, &["project", "add", d.join("oxygen").to_str().unwrap(), "--name", "other"]); assert!(!again.status.success() && text(&again).contains("already a project"), "{}", text(&again)); let o = run( d, &["agent", "add", "kimi", "--kind", "kimi-code", "--project", "nowhere", "--home", d.join("kimi").to_str().unwrap(), "--wait", "0"], ); assert!(!o.status.success() && text(&o).contains("no project named nowhere"), "{}", text(&o)); assert!(!d.join("kimi").exists(), "no key is made for an agent that cannot be added"); }