//! A world for scenario tests: an owner, the instance, the default channels, //! two projects with their channels, and agents the owner delegated to, all //! recorded as `spectra init`, `project add` and `agent add` record them. #![allow(dead_code)] use spectra_core::conversation::{default_channel_events, project_channel_event, Author, Signer}; use spectra_core::ids::now_ms; use spectra_core::keys::KeyPair; use spectra_core::ledger::Ledger; use spectra_core::principals::{identity_payload, signed_event, Principal, Principals}; use std::path::PathBuf; pub struct World { pub dir: tempfile::TempDir, pub owner_home: PathBuf, pub ledger: Ledger, pub owner: KeyPair, } impl World { pub async fn new(projects: &[&str]) -> World { let dir = tempfile::tempdir().unwrap(); let owner_home = dir.path().join("owner"); let owner = KeyPair::create_home(&owner_home, "Andy McCall", "andy@example.org").unwrap(); let data = dir.path().join("data"); std::fs::create_dir_all(&data).unwrap(); let instance = KeyPair::instance(&data, "laptop").unwrap(); let ledger = Ledger::open(&data.join("spectra.db"), Some(instance.clone())).await.unwrap(); let mut first = vec![ signed_event(&owner, "human", "identity.owner", identity_payload(&owner.identity)), signed_event(&instance, "system", "identity.instance", identity_payload(&instance.identity)), ]; first.extend(default_channel_events()); ledger.append(first).await.unwrap(); for p in projects { let payload = serde_json::json!({ "name": p, "path": format!("/src/{p}"), "modules": [] }); ledger.append(vec![signed_event(&owner, "human", "project.added", payload), project_channel_event(p)]).await.unwrap(); } World { dir, owner_home, ledger, owner } } /// Delegate to a new agent and add it, as `spectra agent add` does. pub async fn agent(&mut self, name: &str, project: &str) -> KeyPair { let agent = KeyPair::generate(name, ""); spectra_core::keys::delegate(&self.owner_home, agent.key, None).unwrap(); self.owner = KeyPair::load_home(&self.owner_home).unwrap(); let mut rotated = identity_payload(&self.owner.identity); rotated["chain"] = serde_json::json!([]); let mut p = identity_payload(&agent.identity); p["name"] = name.into(); p["kind"] = "claude-code".into(); p["project"] = project.into(); p["owner_name"] = "Andy McCall".into(); p["scopes"] = spectra_core::principals::DEFAULT_SCOPES.into(); self.ledger .append(vec![ signed_event(&self.owner, "human", "identity.rotated", rotated), signed_event(&self.owner, "human", "agent.added", p), ]) .await .unwrap(); agent } pub async fn principals(&self) -> Principals { Principals::load(&self.ledger).await.unwrap() } /// An author signing from the command line, as the CLI does. pub async fn author<'a>(&self, pair: &'a KeyPair) -> Author<'a> { let principal: Principal = self.principals().await.principal(&pair.key, now_ms()).unwrap(); Author { principal, signer: Signer::Cli(pair) } } pub async fn verify_clean(&self) { let v = self.ledger.verify(&mut Principals::default()).await.unwrap(); assert!(v.clean(), "{:?}", v.findings); assert!(self.ledger.check().await.unwrap().is_empty(), "projections drifted"); } }