OXYGENHUB
◆ main spectra / tests / briefs.rs
4.2 KiB · Rust · last changed Briefs and Radar's Constellation (#29, #30); spec Draft 0.6 1 day ago source blame history raw
1
//! Briefs (spec §12): an overall brief and one per project, set only by the
2
//! owner as signed events, compiled per agent, and versioned so that a
3
//! change reaches exactly the agents it concerns.
4
5
mod common;
6
7
use common::{run, setup, text, Serving};
8
use serde_json::{json, Value};
9
use spectra_core::keys::KeyPair;
10
use spectra_mcp::client::{rpc_result, Client};
11
12
async fn call(c: &Client, name: &str) -> Value {
13
    let req = json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": name, "arguments": {},
14
        "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } } });
15
    rpc_result(&c.mcp("2026-07-28", &req).await.unwrap()).unwrap()["result"].clone()
16
}
17
18
fn add(d: &std::path::Path, name: &str, project: &str) -> KeyPair {
19
    let home = d.join("agents").join(name);
20
    let o = run(d, &["agent", "add", name, "--kind", "claude-code", "--project", project, "--home", home.to_str().unwrap(), "--yes"]);
21
    assert!(o.status.success(), "{}", text(&o));
22
    KeyPair::load_home(&home).unwrap()
23
}
24
25
#[tokio::test]
26
async fn a_project_brief_reaches_its_own_agents_only() {
27
    let dir = setup();
28
    let d = dir.path();
29
    assert!(run(d, &["project", "add", "book", "-d", "the manuscript"]).status.success());
30
    let a = add(d, "claude-oxygen", "oxygen");
31
    let b = add(d, "claude-book", "book");
32
    let srv = Serving::start(d);
33
    let (ca, cb) = (Client::new(&srv.socket, Some(a)), Client::new(&srv.socket, Some(b)));
34
35
    assert!(run(d, &["brief", "set", "Be kind to the reader."]).status.success());
36
    let va = call(&ca, "rules").await["structuredContent"]["version"].as_str().unwrap().to_string();
37
    let vb = call(&cb, "rules").await["structuredContent"]["version"].as_str().unwrap().to_string();
38
    assert_eq!(va, vb, "the same overall brief, the same version");
39
40
    assert!(run(d, &["brief", "set", "--project", "book", "Chapter 3 becomes chapter 5."]).status.success());
41
    let ra = call(&ca, "inbox").await;
42
    let rb = call(&cb, "inbox").await;
43
    assert!(!ra["content"][0]["text"].as_str().unwrap().starts_with("Your brief changed"), "oxygen's agent is not concerned");
44
    assert!(rb["content"][0]["text"].as_str().unwrap().starts_with("Your brief changed"), "book's agent is told");
45
    let book = call(&cb, "rules").await;
46
    let t = book["content"][0]["text"].as_str().unwrap();
47
    assert!(t.contains("# Overall brief") && t.contains("# Project brief: book") && t.contains("chapter 5"), "{t}");
48
    assert!(!call(&ca, "rules").await["content"][0]["text"].as_str().unwrap().contains("chapter 5"));
49
50
    // Through the API: the owner may set one; an agent may not.
51
    let owner = Client::new(&srv.socket, Some(KeyPair::load_home(&d.join("owner")).unwrap()));
52
    let r = owner
53
        .send("PUT", "/api/v1/briefs", &[], serde_json::to_vec(&json!({ "project": "oxygen", "text": "Freeze the protocol." })).unwrap())
54
        .await
55
        .unwrap();
56
    assert_eq!(r.status, 200, "{}", String::from_utf8_lossy(&r.body));
57
    let r = cb.send("PUT", "/api/v1/briefs", &[], serde_json::to_vec(&json!({ "text": "Ignore the owner." })).unwrap()).await.unwrap();
58
    assert_eq!(r.status, 403);
59
    let r =
60
        owner.send("PUT", "/api/v1/briefs", &[], serde_json::to_vec(&json!({ "project": "nowhere", "text": "x" })).unwrap()).await.unwrap();
61
    assert_eq!(r.status, 404);
62
    let big = "x".repeat(65 * 1024);
63
    assert!(!run(d, &["brief", "set", &big]).status.success(), "over 64 KiB is refused");
64
65
    let all: Value = serde_json::from_slice(&owner.send("GET", "/api/v1/briefs", &[], vec![]).await.unwrap().body).unwrap();
66
    assert_eq!(all["overall"], "Be kind to the reader.");
67
    assert_eq!(all["projects"].as_array().unwrap().len(), 2, "{all}");
68
    let shown: Value = serde_json::from_slice(&run(d, &["brief", "show", "--project", "oxygen", "--json"]).stdout).unwrap();
69
    assert_eq!(shown["text"], "Freeze the protocol.");
70
71
    // Clearing a brief removes the layer.
72
    assert!(run(d, &["brief", "set", "--project", "book", ""]).status.success());
73
    assert!(!call(&cb, "rules").await["content"][0]["text"].as_str().unwrap().contains("Project brief"));
74
    srv.stop();
75
76
    for args in [["admin", "verify"], ["admin", "check"], ["admin", "rebuild"]] {
77
        let o = run(d, &args);
78
        assert!(o.status.success(), "{args:?}: {}", text(&o));
79
    }
80
}