OXYGENHUB
# 1876f2ce71e4 spectra / tests / cli_identity.rs
5.3 KiB · Rust · last changed Spectra stands alone: its own identities, signatures and CBOR; projects of any kind of work (#26, #27) 1 day ago source blame history raw
1
//! The owner and agents from the command line (spec §8.0, §8.1, §8.3), as
2
//! the built binary in a hermetic environment: `identity create` makes the
3
//! owner, `init` proves it, `agent add` delegates only when the owner
4
//! confirms, ownership is shown, and the ledger verifies throughout.
5
6
use spectra_core::keys::KeyPair;
7
use std::path::Path;
8
use std::process::{Command, Output};
9
10
fn hermetic(dir: &Path) -> Command {
11
    let mut c = Command::new(env!("CARGO_BIN_EXE_spectra"));
12
    c.env("SPECTRA_DATA", dir.join("data"))
13
        .env("SPECTRA_HOME", dir.join("owner"))
14
        .env("HOME", dir)
15
        .env("XDG_DATA_HOME", dir.join("xdg"))
16
        .env("NO_COLOR", "1");
17
    c
18
}
19
20
fn run(dir: &Path, args: &[&str]) -> Output {
21
    hermetic(dir).args(args).output().unwrap()
22
}
23
24
fn text(o: &Output) -> String {
25
    format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))
26
}
27
28
fn verify_clean(dir: &Path) {
29
    let o = run(dir, &["admin", "verify", "--json"]);
30
    let v: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|_| panic!("{}", text(&o)));
31
    assert!(o.status.success() && v["findings"].as_array().unwrap().is_empty(), "{v}");
32
}
33
34
#[test]
35
fn init_then_an_owned_agent_end_to_end() {
36
    let dir = tempfile::tempdir().unwrap();
37
    let d = dir.path();
38
    let o = run(d, &["identity", "create", "--name", "Andy McCall", "--contact", "[email protected]"]);
39
    assert!(o.status.success(), "{}", text(&o));
40
    assert!(!run(d, &["identity", "create", "--name", "again"]).status.success(), "one identity per home");
41
42
    let o = run(d, &["init", "--name", "laptop"]);
43
    assert!(o.status.success(), "{}", text(&o));
44
    assert!(!run(d, &["init"]).status.success(), "a second init is refused");
45
    let owner = KeyPair::load_home(&d.join("owner")).unwrap();
46
    let instance = KeyPair::instance(&d.join("data"), "laptop").unwrap();
47
    assert!(owner.identity.delegates_to(&instance.key, 1), "init delegates to its own instance key");
48
    verify_clean(d);
49
50
    // A project is any work; with no folder at all, too.
51
    let o = run(d, &["project", "add", "oxygen", "-d", "the version control system"]);
52
    assert!(o.status.success(), "{}", text(&o));
53
    let home = d.join("agents/claude-oxygen");
54
    let home_s = home.to_str().unwrap();
55
    let args = ["agent", "add", "claude-oxygen", "--kind", "claude-code", "--project", "oxygen", "--home", home_s, "--expires", "8h"];
56
    let o = run(d, &args);
57
    assert!(!o.status.success(), "not a terminal and no --yes: nothing delegated");
58
    assert!(text(&o).contains("--yes"), "{}", text(&o));
59
    let agent = KeyPair::load_home(&home).unwrap();
60
    assert!(!KeyPair::load_home(&d.join("owner")).unwrap().identity.delegates_to(&agent.key, 1));
61
62
    let mut yes = args.to_vec();
63
    yes.push("--yes");
64
    let o = run(d, &yes);
65
    assert!(o.status.success(), "{}", text(&o));
66
    assert!(text(&o).contains("Added claude-oxygen") && text(&o).contains("Andy McCall's agent"), "{}", text(&o));
67
    assert_eq!(KeyPair::load_home(&home).unwrap().key, agent.key, "the key made first is the key recorded");
68
    let d8 = KeyPair::load_home(&d.join("owner")).unwrap().identity.delegate(&agent.key).cloned().unwrap();
69
    assert!(d8.expires.is_some(), "the expiry was delegated");
70
71
    let list: serde_json::Value = serde_json::from_slice(&run(d, &["agent", "list", "--json"]).stdout).unwrap();
72
    assert_eq!(list[0]["display"], "claude-oxygen · Andy McCall's agent");
73
    assert_eq!(list[0]["state"], "ok");
74
    verify_clean(d);
75
76
    let o = run(d, &["agent", "renew", "claude-oxygen", "--expires", "never"]);
77
    assert!(o.status.success(), "{}", text(&o));
78
    assert_eq!(KeyPair::load_home(&d.join("owner")).unwrap().identity.delegate(&agent.key).unwrap().expires, None);
79
    verify_clean(d);
80
81
    assert!(run(d, &["agent", "disable", "claude-oxygen"]).status.success());
82
    assert!(KeyPair::load_home(&d.join("owner")).unwrap().identity.delegate(&agent.key).is_none(), "the delegation is removed");
83
    let list: serde_json::Value = serde_json::from_slice(&run(d, &["agent", "list", "--json"]).stdout).unwrap();
84
    assert!(list[0]["state"].as_str().unwrap().contains("disabled"), "{list}");
85
    verify_clean(d);
86
    assert!(run(d, &["admin", "check"]).status.success());
87
88
    // Piped, as here, output is plain: no colour codes, no glyphs.
89
    let o = run(d, &["agent", "list"]);
90
    let t = text(&o);
91
    assert!(!t.contains('\x1b') && !t.contains('✔'), "{t}");
92
}
93
94
#[test]
95
fn an_owner_without_a_secret_key_cannot_init() {
96
    let dir = tempfile::tempdir().unwrap();
97
    let o = run(dir.path(), &["init"]);
98
    assert!(!o.status.success());
99
    assert!(text(&o).contains("the owner signs with their own identity"), "{}", text(&o));
100
}
101
102
#[tokio::test]
103
async fn a_second_writer_is_refused() {
104
    let dir = tempfile::tempdir().unwrap();
105
    let d = dir.path();
106
    KeyPair::create_home(&d.join("owner"), "Andy McCall", "").unwrap();
107
    assert!(run(d, &["init"]).status.success());
108
    let instance = KeyPair::instance(&d.join("data"), "x").unwrap();
109
    let held = spectra_core::ledger::Ledger::open(&d.join("data/spectra.db"), Some(instance)).await.unwrap();
110
    let o = run(d, &["agent", "disable", "nobody"]);
111
    assert!(text(&o).contains("another process is writing"), "{}", text(&o));
112
    assert!(run(d, &["admin", "verify"]).status.success(), "reading needs no lock");
113
    drop(held);
114
}