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> {
app_in(name, home, String::new())
}
fn app_in(name: &str, home: PathBuf, cwd: String) -> 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" }], cwd, stop })
}
async fn post(app: Arc<App>, uri: &str, content_type: &str, body: &str) -> StatusCode {
let req = Request::post(uri).header("host", "127.0.0.1:7979").header("content-type", content_type).body(Body::from(body.to_string())).unwrap();
router(app).oneshot(req).await.unwrap().status()
}
fn seen(home: &std::path::Path) -> Vec<(String, Vec<String>)> {
msg::lines(home)
.into_iter()
.filter_map(|l| match l {
msg::Line::Seen(s) => Some((s.by, s.ids)),
msg::Line::Message(_) => None,
})
.collect()
}
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");
let refused = msg::send(&corp, again, now).expect_err("the tell is unread");
assert!(matches!(refused, msg::SendError::Window { outstanding: 1, window: 1, .. }), "{refused:?}");
assert!(refused.to_string().starts_with("1 unread by bbbb2222-web:"), "{refused}");
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());
assert!(msg::lines(&corp).iter().any(|l| matches!(l, msg::Line::Seen(s) if s.ids == [ask.id.clone()])), "acknowledged in the log");
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", "window")), "{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);
}
#[tokio::test]
async fn the_messages_view_acknowledges_what_it_showed_the_engineer() {
let base = std::env::temp_dir().join(format!("borg-web-messages-seen-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let (corp, work) = (base.join("corp"), base.join("work"));
std::fs::create_dir_all(&corp).unwrap();
std::fs::create_dir_all(&work).unwrap();
let git = |a: &[&str]| std::process::Command::new("git").arg("-C").arg(&work).args(a).output().is_ok_and(|o| o.status.success());
assert!(git(&["init", "-q"]) && git(&["config", "user.email", "ann@x"]));
let now = borg::learn::now_ms();
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 to_me = Message::new(msg::TELL, &from("aaaa1111-web"), msg::ENGINEER, None, now - 3_000, "the build is green");
let to_drone = Message::new(msg::TELL, &from("aaaa1111-web"), "bbbb2222-web", None, now - 2_000, "the parser test is flaky");
msg::send(&corp, to_me.clone(), now).unwrap();
msg::send(&corp, to_drone.clone(), now).unwrap();
let again = Message::new(msg::TELL, &from("aaaa1111-web"), msg::ENGINEER, None, now - 1_000, "and the docs");
assert!(matches!(msg::send(&corp, again.clone(), now), Err(msg::SendError::Window { .. })), "the engineer has not seen the first");
let app = app_in("corp", corp.clone(), work.display().to_string());
let body = json!({"ids": [to_me.id, to_drone.id]}).to_string();
assert_eq!(post(app.clone(), "/api/messages/seen?c=corp", "text/plain", &body).await, StatusCode::UNSUPPORTED_MEDIA_TYPE);
assert!(seen(&corp).is_empty(), "no JSON, nothing acknowledged");
assert_eq!(post(app.clone(), "/api/messages/seen?c=nope", "application/json", &body).await, StatusCode::NOT_FOUND);
assert_eq!(post(app.clone(), "/api/messages/seen?c=corp", "application/json", &body).await, StatusCode::OK);
assert_eq!(seen(&corp), [("engineer:ann@x".to_string(), vec![to_me.id.clone()])], "the engineer's message only, as theirs");
assert_eq!(post(app, "/api/messages/seen?c=corp", "application/json", &body).await, StatusCode::OK);
assert_eq!(seen(&corp).len(), 1, "acknowledged once");
msg::send(&corp, again, now).expect("seen: the sender's window is open again");
let _ = std::fs::remove_dir_all(&base);
}