| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
mod common; |
| 6 |
|
| 7 |
use common::World; |
| 8 |
use spectra_core::conversation::{self as conv, Post, Posted}; |
| 9 |
use spectra_core::ids::now_ms; |
| 10 |
|
| 11 |
fn post(body: &str) -> Post { |
| 12 |
Post { body: body.into(), ..Default::default() } |
| 13 |
} |
| 14 |
|
| 15 |
fn id(p: Posted) -> String { |
| 16 |
match p { |
| 17 |
Posted::Message { id, .. } => id, |
| 18 |
Posted::Duplicate { id } => panic!("dropped as a duplicate of {id}"), |
| 19 |
} |
| 20 |
} |
| 21 |
|
| 22 |
#[tokio::test] |
| 23 |
async fn agents_post_where_they_belong_and_nowhere_else() { |
| 24 |
let mut w = World::new(&["oxygen", "oxygenhub"]).await; |
| 25 |
let claude = w.agent("claude-oxygen", "oxygen").await; |
| 26 |
let a = w.author(&claude).await; |
| 27 |
let at = conv::post(&w.ledger, &a, post("default goes to my project"), now_ms()).await.unwrap(); |
| 28 |
assert!(matches!(&at, Posted::Message { channel, .. } if channel == "#oxygen"), "{at:?}"); |
| 29 |
for c in ["general", "humans", "#oxygen"] { |
| 30 |
conv::post(&w.ledger, &a, Post { channel: Some(c.into()), ..post(&format!("hello {c}")) }, now_ms()).await.unwrap(); |
| 31 |
} |
| 32 |
for (c, why) in [("oxygenhub", "another project"), ("system", "Spectra's")] { |
| 33 |
let err = conv::post(&w.ledger, &a, Post { channel: Some(c.into()), ..post("nope") }, now_ms()).await.unwrap_err(); |
| 34 |
assert!(err.to_string().contains(why), "{c}: {err}"); |
| 35 |
} |
| 36 |
let owner_pair = w.owner.clone(); |
| 37 |
let owner = w.author(&owner_pair).await; |
| 38 |
conv::post(&w.ledger, &owner, Post { channel: Some("oxygenhub".into()), ..post("the owner posts anywhere but #system") }, now_ms()) |
| 39 |
.await |
| 40 |
.unwrap(); |
| 41 |
w.verify_clean().await; |
| 42 |
} |
| 43 |
|
| 44 |
#[tokio::test] |
| 45 |
async fn threads_mentions_and_direct_messages_reach_the_inbox() { |
| 46 |
let mut w = World::new(&["oxygen", "oxygenhub"]).await; |
| 47 |
let claude = w.agent("claude-oxygen", "oxygen").await; |
| 48 |
let kimi = w.agent("kimi-hub", "oxygenhub").await; |
| 49 |
let (c, k) = (w.author(&claude).await, w.author(&kimi).await); |
| 50 |
let root = id(conv::post( |
| 51 |
&w.ledger, |
| 52 |
&c, |
| 53 |
Post { channel: Some("general".into()), ..post("@kimi-hub the wire protocol grew a field; see oxy:change:abcdef12") }, |
| 54 |
now_ms(), |
| 55 |
) |
| 56 |
.await |
| 57 |
.unwrap()); |
| 58 |
let items = conv::inbox(&w.ledger, "agent:kimi-hub", Some(&k)).await.unwrap(); |
| 59 |
assert_eq!(items.len(), 1); |
| 60 |
assert_eq!((items[0].kind.as_str(), items[0].from.as_str(), items[0].channel.as_str()), ("mention", "agent:claude-oxygen", "#general")); |
| 61 |
let reply = id(conv::post(&w.ledger, &k, Post { reply_to: Some(root.clone()), ..post("@andy should I bump the pin?") }, now_ms()) |
| 62 |
.await |
| 63 |
.unwrap()); |
| 64 |
let nested = |
| 65 |
id(conv::post(&w.ledger, &c, Post { reply_to: Some(reply.clone()), ..post("yes, after the release") }, now_ms()).await.unwrap()); |
| 66 |
let (_, thread) = conv::read(&w.ledger, &c.principal, None, Some(&nested), None, 50).await.unwrap(); |
| 67 |
assert_eq!( |
| 68 |
thread.iter().map(|m| m.id.clone()).collect::<Vec<_>>(), |
| 69 |
[root.clone(), reply.clone(), nested], |
| 70 |
"a reply to a reply joins the root's thread" |
| 71 |
); |
| 72 |
let (_, roots) = conv::read(&w.ledger, &c.principal, Some("general"), None, None, 50).await.unwrap(); |
| 73 |
assert_eq!(roots.len(), 1); |
| 74 |
assert_eq!(roots[0].replies, 2); |
| 75 |
assert_eq!(conv::unread(&w.ledger, "human").await.unwrap(), 1, "@andy is the owner"); |
| 76 |
|
| 77 |
assert_eq!(conv::ack(&w.ledger, &k, std::slice::from_ref(&root), Some("ack")).await.unwrap(), 1); |
| 78 |
assert!(conv::inbox(&w.ledger, "agent:kimi-hub", None).await.unwrap().is_empty()); |
| 79 |
|
| 80 |
let dm = conv::ensure_direct(&w.ledger, &c.principal, "kimi-hub").await.unwrap(); |
| 81 |
assert_eq!(dm.display(), "@claude-oxygen+kimi-hub"); |
| 82 |
conv::post(&w.ledger, &c, Post { channel: Some(dm.name.clone()), ..post("just between us") }, now_ms()).await.unwrap(); |
| 83 |
assert_eq!(conv::inbox(&w.ledger, "agent:kimi-hub", None).await.unwrap()[0].kind, "dm"); |
| 84 |
let third = w.agent("third", "oxygen").await; |
| 85 |
let t = w.author(&third).await; |
| 86 |
assert!(conv::read(&w.ledger, &t.principal, Some(&dm.name), None, None, 10).await.is_err(), "not a member"); |
| 87 |
assert!(conv::post(&w.ledger, &t, Post { channel: Some(dm.name.clone()), ..post("let me in") }, now_ms()).await.is_err()); |
| 88 |
w.verify_clean().await; |
| 89 |
} |
| 90 |
|
| 91 |
#[tokio::test] |
| 92 |
async fn duplicates_limits_and_redaction() { |
| 93 |
let mut w = World::new(&["oxygen"]).await; |
| 94 |
let claude = w.agent("claude-oxygen", "oxygen").await; |
| 95 |
let c = w.author(&claude).await; |
| 96 |
let first = id(conv::post(&w.ledger, &c, post("build failed on main"), now_ms()).await.unwrap()); |
| 97 |
assert!( |
| 98 |
matches!(conv::post(&w.ledger, &c, post("build failed on main"), now_ms()).await.unwrap(), Posted::Duplicate { id } if id == first) |
| 99 |
); |
| 100 |
assert!(conv::post(&w.ledger, &c, post(&"x".repeat(conv::MAX_BODY + 1)), now_ms()).await.is_err()); |
| 101 |
assert!(conv::post(&w.ledger, &c, post(" "), now_ms()).await.is_err()); |
| 102 |
assert!(conv::post(&w.ledger, &c, Post { kind: Some("shout".into()), ..post("x") }, now_ms()).await.is_err()); |
| 103 |
|
| 104 |
let secret = id(conv::post(&w.ledger, &c, post("the token is hunter2"), now_ms()).await.unwrap()); |
| 105 |
assert_eq!(conv::search(&w.ledger, &c.principal, "hunter2", None, None, None, 10).await.unwrap().len(), 1); |
| 106 |
let kimi = w.agent("kimi", "oxygen").await; |
| 107 |
let k = w.author(&kimi).await; |
| 108 |
assert!(conv::redact(&w.ledger, &k, &secret, "not mine").await.is_err(), "only its author or the owner"); |
| 109 |
conv::redact(&w.ledger, &c, &secret, "a secret pasted by mistake").await.unwrap(); |
| 110 |
let (_, msgs) = conv::read(&w.ledger, &c.principal, None, None, None, 10).await.unwrap(); |
| 111 |
let shown = msgs.iter().find(|m| m.id == secret).unwrap(); |
| 112 |
assert_eq!(shown.body, "[redacted: a secret pasted by mistake]"); |
| 113 |
assert!(conv::search(&w.ledger, &c.principal, "hunter2", None, None, None, 10).await.unwrap().is_empty(), "gone from search"); |
| 114 |
w.verify_clean().await; |
| 115 |
} |
| 116 |
|
| 117 |
#[tokio::test] |
| 118 |
async fn search_finds_stemmed_words_and_respects_who_may_read() { |
| 119 |
let mut w = World::new(&["oxygen"]).await; |
| 120 |
let claude = w.agent("claude-oxygen", "oxygen").await; |
| 121 |
let kimi = w.agent("kimi", "oxygen").await; |
| 122 |
let (c, k) = (w.author(&claude).await, w.author(&kimi).await); |
| 123 |
conv::post( |
| 124 |
&w.ledger, |
| 125 |
&c, |
| 126 |
Post { channel: Some("general".into()), ..post("We decided to bump the oxygen pin after releasing 0.11") }, |
| 127 |
now_ms(), |
| 128 |
) |
| 129 |
.await |
| 130 |
.unwrap(); |
| 131 |
conv::post(&w.ledger, &c, post("Unrelated chatter about lunch"), now_ms()).await.unwrap(); |
| 132 |
let dm = conv::ensure_direct(&w.ledger, &c.principal, "human").await.unwrap(); |
| 133 |
conv::post(&w.ledger, &c, Post { channel: Some(dm.name.clone()), ..post("privately: the pin bump worries me") }, now_ms()) |
| 134 |
.await |
| 135 |
.unwrap(); |
| 136 |
|
| 137 |
let hits = conv::search(&w.ledger, &k.principal, "bumping pins", None, None, None, 20).await.unwrap(); |
| 138 |
assert_eq!(hits.len(), 1, "stemmed, and the direct message is not kimi's to see: {hits:?}"); |
| 139 |
assert!(hits[0].snippet.contains("[bump]"), "{}", hits[0].snippet); |
| 140 |
assert_eq!(hits[0].channel, "#general"); |
| 141 |
let owner_pair = w.owner.clone(); |
| 142 |
let owner = w.author(&owner_pair).await; |
| 143 |
assert_eq!( |
| 144 |
conv::search(&w.ledger, &owner.principal, "pin", None, None, None, 20).await.unwrap().len(), |
| 145 |
2, |
| 146 |
"the owner reads everything" |
| 147 |
); |
| 148 |
assert_eq!( |
| 149 |
conv::search(&w.ledger, &k.principal, "\"oxygen pin\"", Some("general"), Some("claude-oxygen"), None, 20).await.unwrap().len(), |
| 150 |
1 |
| 151 |
); |
| 152 |
assert!(conv::search(&w.ledger, &k.principal, "lunch", Some("general"), None, None, 20).await.unwrap().is_empty()); |
| 153 |
let err = conv::search(&w.ledger, &k.principal, "AND OR (", None, None, None, 20).await.unwrap_err(); |
| 154 |
assert!(err.to_string().contains("does not parse"), "{err}"); |
| 155 |
w.verify_clean().await; |
| 156 |
} |