mod common;
use common::*;
use lonca_daemon::protocol::Response;
use std::time::{Duration, Instant};
const SYM: &str = "src/lib.rs#charge";
fn two_agents() -> (Env, String, String) {
let mut e = Env::new();
e.init(None);
e.human_change(vec![file("src/lib.rs", LIB)], "seed");
e.svc.process_all();
let a = e.session("s1", &["src/**"]);
e.bind(&a);
let b = e.session("s2", &["src/**"]);
e.bind(&b);
(e, a, b)
}
fn next_seq(e: &mut Env) -> u64 {
e.ok("status", Some("demo"), None, json!({}))["next_seq"]
.as_u64()
.unwrap()
}
fn watch(e: &mut Env, params: Value) -> Value {
e.ok("events.watch", Some("demo"), None, params)
}
fn waiter(svc: &Arc<Service>, token: &str, params: Value) -> std::thread::JoinHandle<Response> {
let svc = svc.clone();
let token = token.to_string();
std::thread::spawn(move || {
svc.handle(Request {
id: 77,
method: "events.watch".into(),
repo: Some("demo".into()),
token: Some(token),
params,
})
})
}
fn kinds(answer: &Value) -> Vec<(String, String)> {
let events = answer["events"].as_array().unwrap();
let pair = |e: &Value| {
let s = e["session"].as_str().unwrap().to_string();
(s, e["kind"].as_str().unwrap().to_string())
};
events.iter().map(pair).collect()
}
fn yield_to(e: &mut Env, from: &str, to: &str, set: &str) {
let message = json!({"type": "yield", "set": set, "to": to});
let p = json!({"thread": "t1", "to_session": to, "message": message});
let r = e.ok("negotiate", None, Some(from), p);
assert_eq!(r["decision"]["outcome"], "allow", "{r}");
}
#[test]
fn a_waiter_wakes_on_a_handover_and_lets_others_work_meanwhile() {
let (mut e, a, b) = two_agents();
let set = e.lease(&a, SYM)["set"].as_str().unwrap().to_string();
let from = next_seq(&mut e);
let token = e.login().unwrap();
let t = Instant::now();
let w = waiter(
&e.svc,
&token,
json!({"from": from, "sessions": ["s2"], "wait_ms": 20_000}),
);
std::thread::sleep(Duration::from_millis(200));
let r = e.lease(&b, "src/lib.rs#refund");
assert_eq!(r["decision"]["outcome"], "allow", "{r}");
std::thread::sleep(Duration::from_millis(100));
assert!(!w.is_finished(), "a lease of s2's own woke it");
yield_to(&mut e, &a, "s2", &set);
let got = w.join().unwrap();
let waited = t.elapsed().as_millis();
assert!(got.ok, "{:?}", got.error);
let got = got.result;
let s2 = |k: &str| ("s2".to_string(), k.to_string());
assert_eq!(
kinds(&got),
vec![s2("lease_received"), s2("message")],
"{got}"
);
assert_eq!(got["events"][0]["from"], "s1");
assert_eq!(got["next"].as_u64().unwrap(), next_seq(&mut e));
assert!(waited < 1_800, "waited {waited} ms");
}
#[test]
fn a_watcher_takes_nothing_from_the_inbox_and_never_reads_the_words() {
let (mut e, a, b) = two_agents();
let from = next_seq(&mut e);
let message = json!({"type": "claim", "symbols": [SYM], "reason": "SECRET-WORDS-1729"});
let p = json!({"thread": "t-hidden", "to_session": "s2", "message": message});
e.ok("negotiate", None, Some(&a), p);
let got = watch(&mut e, json!({"from": from}));
assert_eq!(kinds(&got), vec![("s2".into(), "message".into())], "{got}");
let said = got.to_string();
assert!(!said.contains("SECRET-WORDS-1729"), "{said}");
assert!(!said.contains("t-hidden"), "{said}");
watch(&mut e, json!({"from": from}));
let inbox = e.ok("inbox", None, Some(&b), json!({}));
assert_eq!(inbox["events"][0]["kind"], "message", "{inbox}");
assert!(
inbox["text"]
.as_str()
.unwrap()
.contains("SECRET-WORDS-1729")
);
}
#[test]
fn an_integration_names_the_change_and_its_session() {
let (mut e, a, _b) = two_agents();
let base = e.trunk();
e.wrote(&a, "src/lib.rs");
e.lease(&a, SYM);
let edited = LIB.replacen(" a\n", " a + 1\n", 1);
let r = e.propose(&a, &base, vec![file("src/lib.rs", &edited)], &[SYM]);
assert_eq!(r["decision"]["outcome"], "allow", "{r}");
let from = next_seq(&mut e);
e.svc.process_all();
let got = watch(&mut e, json!({"from": from, "wait_ms": 5_000}));
assert_eq!(
kinds(&got),
vec![("s1".into(), "integrated".into())],
"{got}"
);
assert_eq!(got["events"][0]["change"], r["change"]);
assert!(got["events"][0]["seq"].as_u64().unwrap() >= from);
}
#[test]
fn a_reservation_is_reported_once_while_it_lasts() {
let (mut e, a, b) = two_agents();
let set = e.lease(&b, SYM)["set"].as_str().unwrap().to_string();
let items = json!([{"symbol": SYM, "kind": "edit"}]);
e.ok(
"lease.interest",
None,
Some(&a),
json!({"items": items, "intent": "next"}),
);
let from = next_seq(&mut e);
e.ok("lease.release", None, Some(&b), json!({"set": set}));
let got = watch(&mut e, json!({"from": from}));
let reserved: Vec<&Value> = got["events"]
.as_array()
.unwrap()
.iter()
.filter(|e| e["kind"] == "reserved")
.collect();
assert_eq!(reserved.len(), 1, "{got}");
assert_eq!(reserved[0]["session"], "s1");
assert!(reserved[0]["seq"].is_null(), "{got}");
assert!(reserved[0]["until_ms"].as_i64().unwrap() > e.now());
let id = reserved[0]["reservation"].clone();
assert_eq!(got["reserved"], json!([id]));
let next = got["next"].clone();
let t = Instant::now();
let again = watch(
&mut e,
json!({"from": next, "reserved": [id], "wait_ms": 300}),
);
assert!(t.elapsed().as_millis() >= 250);
assert_eq!(again["events"], json!([]), "{again}");
assert_eq!(again["reserved"], json!([id]));
let fresh = watch(&mut e, json!({"from": next}));
assert_eq!(fresh["events"][0]["reservation"], id);
let inbox = e.ok("inbox", None, Some(&a), json!({}));
assert_eq!(inbox["events"][0]["kind"], "reserved", "{inbox}");
}
#[test]
fn from_now_on_nothing_older_and_an_empty_wait_runs_its_time() {
let (mut e, a, _b) = two_agents();
let p = json!({"thread": "t1", "to_session": "s2",
"message": {"type": "claim", "symbols": [SYM], "reason": "old"}});
e.ok("negotiate", None, Some(&a), p);
let t = Instant::now();
let got = watch(&mut e, json!({"wait_ms": 300}));
let held = t.elapsed().as_millis();
assert_eq!(got["events"], json!([]), "{got}");
assert_eq!(got["next"].as_u64().unwrap(), next_seq(&mut e));
assert!((250..3_000).contains(&held), "held {held} ms");
let t = Instant::now();
let got = watch(&mut e, json!({"from": got["next"]}));
assert_eq!(got["events"], json!([]));
assert!(t.elapsed().as_millis() < 200);
let from = next_seq(&mut e);
let token = e.login().unwrap();
let w = waiter(&e.svc, &token, json!({"from": from, "wait_ms": 600}));
e.human(Op::Freeze { reason: "x".into() });
let got = w.join().unwrap().result;
assert_eq!(got["events"], json!([]), "{got}");
assert!(got["next"].as_u64().unwrap() > from);
}
#[test]
fn an_ended_session_ends_the_wait_with_its_reason() {
let (mut e, _a, _b) = two_agents();
let from = next_seq(&mut e);
let token = e.login().unwrap();
let w = waiter(
&e.svc,
&token,
json!({"from": from, "sessions": ["s1", "s2"], "wait_ms": 20_000}),
);
std::thread::sleep(Duration::from_millis(200));
let r = e.human(Op::SessionEnd {
session: "s1".into(),
reason: "runner stopped it".into(),
});
assert_eq!(r["decision"]["outcome"], "allow", "{r}");
let got = w.join().unwrap().result;
assert_eq!(
got["ended"],
json!([{"session": "s1", "reason": "ended: runner stopped it"}]),
"{got}"
);
let t = Instant::now();
let again = watch(
&mut e,
json!({"from": got["next"], "sessions": ["s1", "s2"], "wait_ms": 5_000}),
);
assert!(t.elapsed().as_millis() < 500);
assert_eq!(again["ended"][0]["session"], "s1");
let got = watch(&mut e, json!({"from": again["next"], "wait_ms": 100}));
assert_eq!(got["ended"], json!([]));
e.advance(3_600_000);
let got = watch(&mut e, json!({"sessions": ["s2"], "wait_ms": 5_000}));
assert_eq!(
got["ended"],
json!([{"session": "s2", "reason": "expired"}]),
"{got}"
);
}
#[test]
fn only_a_human_watches_and_only_its_own_sessions() {
let (mut e, a, _b) = two_agents();
let r = e.call("events.watch", None, Some(&a), json!({}));
assert_eq!(r.unwrap_err().0, "forbidden");
let r = e.anon("events.watch", Some("demo"), None, json!({}));
assert_eq!(r.unwrap_err().0, "unauthenticated");
let bora = KeyPair::generate();
e.human(Op::HumanEnroll {
human: "bora".into(),
key: HumanKey {
alg: "ed25519".into(),
public: bora.public_hex(),
},
});
let sig = sign_login_ed25519(&bora, "bora", None, 3_600_000, e.now());
let tb = e
.anon("human.login", None, None, json!({"sig": sig}))
.unwrap()["token"]
.as_str()
.unwrap()
.to_string();
for named in ["s1", "nope"] {
let r = e.anon(
"events.watch",
Some("demo"),
Some(&tb),
json!({"sessions": [named]}),
);
let (code, msg) = r.unwrap_err();
assert_eq!(code, "forbidden");
assert_eq!(msg, format!("session {named} is not one you started"));
}
let p = json!({"thread": "t1", "to_session": "s2",
"message": {"type": "claim", "symbols": [SYM], "reason": "x"}});
e.ok("negotiate", None, Some(&a), p);
let got = e
.anon("events.watch", Some("demo"), Some(&tb), json!({"from": 0}))
.unwrap();
assert_eq!(got["events"], json!([]), "{got}");
let got = watch(&mut e, json!({"from": 0}));
assert!(
kinds(&got).contains(&("s2".into(), "message".into())),
"{got}"
);
let before = next_seq(&mut e);
watch(&mut e, json!({"from": 0}));
assert_eq!(next_seq(&mut e), before);
}
#[test]
fn an_approval_of_a_request_older_than_from_is_found() {
let (mut e, _, _) = two_agents();
let base = e.trunk();
let c = e.session("s3", &["src/**"]);
e.lease(&c, SYM);
let edited = LIB.replacen(" a\n", " a + 1\n", 1);
let r = e.propose(&c, &base, vec![file("src/lib.rs", &edited)], &[SYM]);
assert_eq!(r["decision"]["outcome"], "escalate", "{r}");
let asked = r["seq"].as_u64().unwrap();
let from = next_seq(&mut e);
let d = e.human(Op::Approve {
target_seq: asked,
note: "reviewed".into(),
checks: vec![],
});
assert_eq!(d["decision"]["outcome"], "allow", "{d}");
let got = watch(&mut e, json!({"from": from, "sessions": ["s3"]}));
assert_eq!(kinds(&got)[0], ("s3".into(), "approved".into()), "{got}");
assert_eq!(got["events"][0]["seq"], d["seq"]);
}