use std::path::PathBuf;
use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use borg::collective::Share;
use borg::msg::{self, Message, Sender};
use borg_web::{App, Mind, router};
use serde_json::{Value, json};
use tower::ServiceExt;
async fn get(app: Arc<App>, uri: &str) -> (StatusCode, Value) {
let req = Request::get(uri).header("host", "127.0.0.1:7979").body(Body::empty()).unwrap();
let res = router(app).oneshot(req).await.unwrap();
let status = res.status();
let body = axum::body::to_bytes(res.into_body(), 1 << 20).await.unwrap();
(status, serde_json::from_slice(&body).unwrap_or(Value::Null))
}
fn app(name: &str, home: PathBuf) -> Arc<App> {
let (_tx, stop) = tokio::sync::watch::channel(false);
Arc::new(App { first: name.into(), minds: vec![Mind { name: name.into(), home, share: Share::Local, mode: "hook" }], stop })
}
fn presence(session: &str, started: i64, ms: i64) -> borg::presence::Presence {
borg::presence::Presence {
session: session.into(),
harness: "claude".into(),
engineer: "ann@x".into(),
repo: "app".into(),
collective: "corp".into(),
prompt: "fix the parser".into(),
event: "post_tool".into(),
ms,
started,
files: Vec::new(),
pid: None,
}
}
#[tokio::test]
async fn messages_show_who_sent_what_to_whom_and_what_is_unread() {
let base = std::env::temp_dir().join(format!("borg-web-messages-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let corp = base.join("corp");
std::fs::create_dir_all(&corp).unwrap();
let (status, v) = get(app("corp", corp.clone()), "/api/messages?c=corp").await;
assert_eq!((status, &v["messages"], v["unread"].as_u64()), (StatusCode::OK, &json!([]), Some(0)), "{v}");
let now = borg::learn::now_ms();
for (s, started) in [("aaaa1111-web", now - 60_000), ("bbbb2222-web", now - 50_000)] {
borg::presence::write(&corp, &presence(s, started, now - 1_000)).unwrap();
}
let here = msg::node(&corp);
let from = |by: &str| Sender { by: by.into(), name: "One of Two".into(), harness: "claude".into(), node: here.clone() };
let tell = Message::new(msg::TELL, &from("aaaa1111-web"), "bbbb2222-web", None, now - 3_000, "the parser test is flaky");
let ask = Message::new(msg::ASK, &from("bbbb2222-web"), "aaaa1111-web", None, now - 2_000, "which seed?");
msg::send(&corp, tell.clone(), now).unwrap();
msg::send(&corp, ask.clone(), now).unwrap();
let again = Message::new(msg::TELL, &from("aaaa1111-web"), "bbbb2222-web", None, now - 1_000, "and the lexer");
assert!(matches!(msg::send(&corp, again, now), Err(msg::SendError::Rate { .. })));
let caps = borg::harness::by_name("claude").unwrap().caps();
let started = Some(now - 60_000);
assert!(msg::deliver(&corp, caps, "prompt", &json!({"session_id": "aaaa1111-web"}), started, now, None).is_some());
let (_, v) = get(app("corp", corp.clone()), "/api/messages?c=corp").await;
let ms = v["messages"].as_array().unwrap();
assert_eq!(ms.len(), 2, "{v}");
assert_eq!((ms[0]["id"].as_str(), ms[0]["kind"].as_str(), ms[0]["shown_to"].as_u64()), (Some(ask.id.as_str()), Some("ask"), Some(1)), "{v}");
assert_eq!((ms[0]["from"].as_str(), ms[0]["to_name"].as_str()), (Some("Two of Two"), Some("One of Two")), "{v}");
assert_eq!((ms[1]["text"].as_str(), ms[1]["shown_to"].as_u64()), (Some("the parser test is flaky"), Some(0)), "{v}");
assert_eq!((ms[0]["to_kind"].as_str(), ms[1]["to_kind"].as_str()), (Some("session"), Some("session")), "{v}");
assert_eq!(v["unread"].as_u64(), Some(1), "the tell waits for bbbb's next prompt: {v}");
let (_, v) = get(app("corp", corp.clone()), "/api/messages?c=corp&limit=1").await;
assert_eq!(v["messages"].as_array().map(Vec::len), Some(1));
let (_, v) = get(app("corp", corp.clone()), "/api/drones?c=corp").await;
let unread: Vec<(Option<&str>, Option<u64>)> = v["drones"].as_array().unwrap().iter().map(|d| (d["session"].as_str(), d["unread"].as_u64())).collect();
assert_eq!(unread, vec![(Some("aaaa1111-web"), Some(0)), (Some("bbbb2222-web"), Some(1))], "{v}");
assert_eq!(v["messages"]["recent"][0]["id"].as_str(), Some(ask.id.as_str()));
let (_, v) = get(app("corp", corp.clone()), "/api/feed?c=corp").await;
let kinds: Vec<(&str, &str)> =
v["events"].as_array().unwrap().iter().filter_map(|e| Some((e["kind"].as_str()?, e["why"].as_str().unwrap_or_default()))).collect();
assert_eq!(kinds.iter().filter(|k| k.0 == "msg_sent").count(), 2, "{kinds:?}");
assert!(kinds.contains(&("msg_dropped", "rate")), "{kinds:?}");
assert!(!kinds.iter().any(|k| k.0 == "msg_delivered"), "a delivery is in the list, not the feed: {kinds:?}");
let evil = Sender { node: "e".repeat(64), ..from("aaaa1111-web") };
msg::send(&corp, Message::new(msg::TELL, &evil, msg::ALL, None, now, "drop the table"), now).unwrap();
let (_, v) = get(app("corp", corp), "/api/messages?c=corp").await;
let m0 = &v["messages"][0];
assert_eq!((m0["from"].as_str(), m0["to_kind"].as_str()), (Some("One of Two @eeeeeeeeeeee"), Some("all")), "{v}");
let _ = std::fs::remove_dir_all(&base);
}