1use std::collections::{BTreeMap, HashSet};
27use std::path::Path;
28
29use serde::Serialize;
30use serde_json::{Value, json};
31
32use crate::clock::{DAY_MS, ago};
33use crate::collective::{self, Route, Share};
34use crate::msg::{self, Address, Message};
35use crate::presence::{self, Presence, State};
36use crate::{install, learn, protocol, select, steer, update};
37
38#[derive(Debug, Clone, Serialize)]
39pub struct Drone {
40 pub designation: String,
42 pub number: usize,
44 pub node: String,
46 pub state: State,
47 pub paused: bool,
49 #[serde(flatten)]
50 pub p: Presence,
51}
52
53#[derive(Debug, Clone, Default, Serialize)]
55pub struct Inbox {
56 pub unread: BTreeMap<String, usize>,
58 pub recent: Vec<Message>,
60 pub day: usize,
62 #[serde(skip)]
64 pub here: String,
65 pub listen: BTreeMap<String, crate::listen::Status>,
67}
68
69pub const RECENT: usize = 5;
71
72const NUMBERS: [&str; 20] = [
73 "One",
74 "Two",
75 "Three",
76 "Four",
77 "Five",
78 "Six",
79 "Seven",
80 "Eight",
81 "Nine",
82 "Ten",
83 "Eleven",
84 "Twelve",
85 "Thirteen",
86 "Fourteen",
87 "Fifteen",
88 "Sixteen",
89 "Seventeen",
90 "Eighteen",
91 "Nineteen",
92 "Twenty",
93];
94
95pub fn word(n: usize) -> String {
96 NUMBERS.get(n.wrapping_sub(1)).map(|s| s.to_string()).unwrap_or_else(|| n.to_string())
97}
98
99pub fn assemble(local: Vec<Presence>, remote: Vec<presence::Remote>, now: i64, alive: impl Fn(u32) -> bool, paused: impl Fn(&Presence) -> bool) -> Vec<Drone> {
101 let mut all: Vec<(String, Presence, State, bool)> = local
102 .into_iter()
103 .map(|p| {
104 let s = presence::state(&p, now, &alive);
105 let z = s != State::Ended && paused(&p);
106 (String::new(), p, s, z)
107 })
108 .collect();
109 all.extend(remote.into_iter().map(|r| {
111 let s = presence::state(&r.p, now, |_| true);
112 (r.key, r.p, s, false)
113 }));
114 all.sort_by(|a, b| (a.1.started, &a.0, &a.1.session).cmp(&(b.1.started, &b.0, &b.1.session)));
115 let n = all.len();
116 all.into_iter()
117 .enumerate()
118 .map(|(i, (node, p, state, paused))| Drone { designation: format!("{} of {}", word(i + 1), word(n)), number: i + 1, node, state, paused, p })
119 .collect()
120}
121
122pub fn drones(home: &Path, may_sync: bool, now: i64) -> Vec<Drone> {
124 let remote = if may_sync { presence::remote(home, now) } else { Vec::new() };
125 assemble(presence::local(home, now), remote, now, presence::alive, |p| steer::paused(home, &p.session, Some(p.started), now))
126}
127
128pub fn inbox(home: &Path, drones: &[Drone], now: i64) -> Inbox {
131 let listen = listen_of(home, drones);
132 if std::fs::metadata(msg::path(home)).is_err() {
133 return Inbox { listen, ..Inbox::default() };
134 }
135 let all: Vec<Message> = msg::read(home).into_iter().filter(|m| now - m.ms <= msg::TTL_MS).collect();
136 let here = msg::node(home);
137 let unread = unread_of(home, drones, &here, &all, now);
138 let day = all.iter().filter(|m| now - m.ms <= DAY_MS).count();
139 let mut recent = all;
140 recent.sort_by(|a, b| (b.ms, &b.id).cmp(&(a.ms, &a.id)));
141 recent.truncate(RECENT);
142 Inbox { unread, recent, day, here, listen }
143}
144
145pub fn acknowledge(home: &Path, cwd: &str, shown: &[&Message], done: &mut HashSet<String>, now: i64) {
154 let to_engineer: Vec<&Message> =
155 shown.iter().copied().filter(|m| matches!(Address::parse(&m.to), Some(Address::Engineer { .. })) && !done.contains(&m.id)).collect();
156 if to_engineer.is_empty() {
157 return;
158 }
159 let _ = msg::ack_engineer(home, cwd, &to_engineer, now);
160 done.extend(to_engineer.iter().map(|m| m.id.clone()));
161}
162
163pub fn engineer_looks<'a>(caller: impl Fn(&[Drone]) -> Result<Option<(String, &'static str)>, String>, mut drones: impl Iterator<Item = &'a [Drone]>) -> bool {
173 drones.all(|ds| matches!(caller(ds), Ok(None)))
174}
175
176pub(crate) fn engineer_dir(dir: Option<&str>) -> String {
179 let d = dir.map_or_else(|| std::env::current_dir().unwrap_or_default(), std::path::PathBuf::from);
180 std::path::absolute(&d).unwrap_or(d).display().to_string()
181}
182
183fn listen_of(home: &Path, drones: &[Drone]) -> BTreeMap<String, crate::listen::Status> {
185 drones
186 .iter()
187 .filter(|d| d.node.is_empty() && d.state != State::Ended)
188 .filter_map(|d| crate::listen::status(home, &d.p.session).map(|s| (d.p.session.clone(), s)))
189 .collect()
190}
191
192pub fn wake_cell(inbox: &Inbox, d: &Drone) -> &'static str {
195 inbox.listen.get(&d.p.session).map_or("-", |s| s.name())
196}
197
198pub fn unread_of(home: &Path, drones: &[Drone], here: &str, all: &[Message], now: i64) -> BTreeMap<String, usize> {
201 drones
202 .iter()
203 .filter(|d| d.node.is_empty() && d.state != State::Ended)
204 .map(|d| (d.p.session.clone(), msg::unread(home, all, here, &d.p.session, d.p.started, now)))
205 .collect()
206}
207
208fn drone_at<'a>(drones: &'a [Drone], here: &str, node: &str, session: &str) -> Option<&'a Drone> {
210 drones.iter().find(|d| d.p.session == session && if d.node.is_empty() { node == here } else { d.node == node })
211}
212
213pub fn sender(drones: &[Drone], here: &str, m: &Message) -> String {
218 let name = match Address::parse(&m.by) {
219 Some(Address::Engineer { email }) if email.is_empty() => msg::ENGINEER.into(),
220 Some(Address::Engineer { email }) => collective::aliases().display(&email),
221 _ => match drone_at(drones, here, &m.node, &m.by) {
222 Some(d) => d.designation.clone(),
223 None => msg::sender_label(m),
224 },
225 };
226 if m.node == here { name } else { format!("{name} @{}", msg::short_node(&m.node)) }
227}
228
229pub fn recipient(drones: &[Drone], here: &str, to: &str) -> String {
234 match Address::parse(to) {
235 Some(Address::Engineer { email }) if !email.is_empty() => collective::aliases().display(&email),
236 Some(Address::Local { node }) if node == here => msg::LOCAL.into(),
237 Some(Address::Local { node }) => format!("{} @{}", msg::LOCAL, msg::short_node(&node)),
238 Some(Address::All) => msg::COLLECTIVE.into(),
239 Some(Address::Session { id, node }) => {
240 let d = match node {
241 Some(n) => drone_at(drones, here, &n, &id),
242 None => drones.iter().find(|d| d.p.session == id),
243 };
244 d.map_or_else(|| msg::short_session(&id), |d| d.designation.clone())
245 }
246 Some(a) => a.to_string(),
247 None => msg::short_session(to),
248 }
249}
250
251pub fn message_line(drones: &[Drone], here: &str, m: &Message) -> String {
254 let ask = if m.asks() { " asks" } else { "" };
255 format!("{}{ask} -> {}: {}", sender(drones, here, m), recipient(drones, here, &m.to), m.text)
256}
257
258pub fn unread_cell(inbox: &Inbox, d: &Drone) -> String {
260 inbox.unread.get(&d.p.session).map_or_else(|| "-".into(), usize::to_string)
261}
262
263pub fn resolve<'a>(drones: &'a [Drone], target: &str) -> Result<Option<&'a Drone>, String> {
269 let t = target.trim();
270 if [msg::ALL, msg::COLLECTIVE, msg::LOCAL].iter().any(|w| t.eq_ignore_ascii_case(w)) {
271 return Ok(None);
272 }
273 let first = t.split_whitespace().next().unwrap_or_default();
274 let number = first.parse::<usize>().ok().or_else(|| NUMBERS.iter().position(|w| w.eq_ignore_ascii_case(first)).map(|i| i + 1));
275 if let Some(n) = number {
276 return drones.iter().find(|d| d.number == n).map(Some).ok_or_else(|| format!("no drone {target}: there are {} (`borg top`)", drones.len()));
277 }
278 let mut hits: Vec<&Drone> = drones.iter().filter(|d| d.p.session == t || (t.len() >= 4 && d.p.session.starts_with(t))).collect();
279 if hits.iter().any(|d| d.state != State::Ended) {
280 hits.retain(|d| d.state != State::Ended);
281 }
282 if hits.iter().any(|d| d.node.is_empty()) {
283 hits.retain(|d| d.node.is_empty());
284 }
285 match hits.as_slice() {
286 [d] => Ok(Some(d)),
287 [] => Err(format!("no drone or session {target:?} (`borg top`)")),
288 _ => Err(format!("{target:?} names {} sessions: give more of the id", hits.len())),
289 }
290}
291
292pub const COLUMN_NODE_CHARS: usize = 6;
296
297pub fn column_node(node: &str) -> String {
299 node.chars().take(COLUMN_NODE_CHARS).collect()
300}
301
302const MESSAGE_CHARS: usize = 120;
305
306fn cut(s: &str, n: usize) -> String {
307 if s.chars().count() <= n { s.to_string() } else { format!("{}…", s.chars().take(n - 1).collect::<String>()) }
308}
309
310pub fn shows_last(total: usize, last: usize, width: usize) -> bool {
315 total <= width || total.saturating_sub(last) < width.saturating_sub(1)
316}
317
318fn prints_text(drones: &[Drone], here: &str, m: &Message) -> bool {
322 shows_last(message_line(drones, here, m).chars().count(), m.text.chars().count(), MESSAGE_CHARS)
323}
324
325pub fn render(name: &str, share: Share, drones: &[Drone], inbox: &Inbox, now: i64) -> String {
327 let working = drones.iter().filter(|d| matches!(d.state, State::Working | State::Waiting)).count();
328 let remote = drones.iter().filter(|d| !d.node.is_empty()).count();
329 let share = match share {
330 Share::Team => "team",
331 Share::Local => "local, never leaves this machine",
332 };
333 let mut s = format!("{name} ({share}): {} drone(s), {working} active, {remote} remote\n", drones.len());
334 if !drones.is_empty() {
335 s.push_str(&drone_table(drones, inbox, now));
336 }
337 if !inbox.recent.is_empty() {
338 s.push_str(&format!(" messages: {} in the last day, {} unread\n", inbox.day, inbox.unread.values().sum::<usize>()));
339 for m in &inbox.recent {
340 s.push_str(&format!(" {:>6} {}\n", ago(now - m.ms), cut(&message_line(drones, &inbox.here, m), MESSAGE_CHARS)));
341 }
342 }
343 s
344}
345
346fn drone_table(drones: &[Drone], inbox: &Inbox, now: i64) -> String {
347 let mut s = format!(
348 " {:<18} {:<8} {:<24} {:<14} {:<8} {:<6} {:>6} {:>6} {:<40} {}\n",
349 "DRONE", "SESSION", "ENGINEER", "REPO", "STATE", "WAKE", "IDLE", "UNREAD", "DOING", "FILES"
350 );
351 for d in drones {
352 let state = if d.paused { "paused" } else { d.state.name() };
353 let engineer = if d.node.is_empty() { d.p.engineer.clone() } else { format!("{} @{}", d.p.engineer, column_node(&d.node)) };
354 let line = format!(
355 " {:<18} {:<8} {:<24} {:<14} {:<8} {:<6} {:>6} {:>6} {:<40} {}",
356 d.designation,
357 cut(&d.p.session, 8),
358 cut(if engineer.is_empty() { "?" } else { &engineer }, 24),
359 cut(&d.p.repo, 14),
360 state,
361 wake_cell(inbox, d),
362 ago(now - d.p.ms),
363 unread_cell(inbox, d),
364 cut(&d.p.prompt, 40),
365 d.p.files.join(", ")
366 );
367 s.push_str(line.trim_end());
368 s.push('\n');
369 }
370 s
371}
372
373pub fn to_json(name: &str, share: Share, drones: &[Drone]) -> Value {
374 json!({"collective": name, "share": if share == Share::Team { "team" } else { "local" }, "drones": drones})
375}
376
377pub fn to_json_with(name: &str, share: Share, drones: &[Drone], inbox: &Inbox) -> Value {
379 let mut v = to_json(name, share, drones);
380 if let Some(ds) = v["drones"].as_array_mut() {
381 for (o, d) in ds.iter_mut().zip(drones) {
382 if let Some(n) = inbox.unread.get(&d.p.session) {
383 o["unread"] = json!(n);
384 }
385 if let Some(l) = inbox.listen.get(&d.p.session) {
386 o["listen"] = json!(l);
387 }
388 }
389 }
390 v["messages"] = json!({"day": inbox.day, "recent": inbox.recent});
391 v
392}
393
394#[derive(Debug, Clone, Default, PartialEq, Serialize)]
404pub struct Selection {
405 pub policy: &'static str,
408 pub default: &'static str,
411 pub source: &'static str,
414 pub gate: f64,
419 pub allowance: f64,
424 pub ledger: f64,
425 pub fit: String,
427 pub at: u64,
429 pub checks: u64,
431 pub sessions: u64,
432 pub deviations: Vec<Count>,
437 pub fallbacks: Vec<Count>,
441 pub spend: Vec<Spend>,
446 pub budget: Option<Budget>,
448 pub effects: Vec<Estimate>,
451 pub half_life: Option<u32>,
453 pub labels: Vec<Count>,
457 pub declaration: Option<Estimate>,
459 pub delivery: Option<select::policy::Rates>,
462 pub evaluation: Option<Evaluation>,
464 pub measurement: Option<Measurement>,
467 pub pool: Vec<Yield>,
469 pub latency: Vec<Latency>,
471 pub test: Option<Test>,
474 pub judge: Vec<crate::judge_length::Length>,
478}
479
480#[derive(Debug, Clone, Default, PartialEq, Serialize)]
485pub struct Test {
486 pub risk: f64,
489 pub set: bool,
491 pub ran: u64,
494 pub control: u64,
495 pub refused: u64,
498 pub sessions: u64,
500 pub kept: u64,
503 pub by_k: Vec<Draws>,
505 pub due: bool,
512 pub due_checks: u64,
513 pub due_sessions: u64,
514 pub due_min_sessions: u64,
515 pub dilution: Vec<Estimate>,
516 pub ask_free: Option<Estimate>,
517}
518
519#[derive(Debug, Clone, Default, PartialEq, Serialize)]
521pub struct Draws {
522 pub k: u32,
523 pub draws: Vec<u64>,
524 pub refused: u64,
525}
526
527impl Test {
528 #[must_use]
531 pub fn of(summary: &select::summary::Summary, risk: Option<f64>) -> Test {
532 use select::replay::{TEST_CHECKS, TEST_MIN_SESSIONS, TEST_SESSIONS, test_readout};
533 let e = &summary.experiment;
534 let (ran, refused) = e.totals();
535 let r = test_readout(summary);
536 let estimate = |name: String, unit: &str, x: &crate::ope::Estimate| {
538 x.value.map(|v| Estimate { name, unit: unit.into(), mean: 100.0 * v, lo: 100.0 * x.lo, hi: 100.0 * x.hi, n: x.n as u64 })
539 };
540 let mut by_k: Vec<Draws> = Vec::new();
541 let at = |by_k: &mut Vec<Draws>, k: u32| match by_k.iter().position(|d| d.k == k) {
542 Some(i) => i,
543 None => {
544 by_k.push(Draws { k, draws: vec![0; k as usize + 1], refused: 0 });
545 by_k.len() - 1
546 }
547 };
548 for &(k, s, n) in &e.ran {
549 let i = at(&mut by_k, k);
550 if let Some(c) = by_k[i].draws.get_mut(s as usize) {
551 *c += n;
552 }
553 }
554 for &(k, n) in &e.refused {
555 let i = at(&mut by_k, k);
556 by_k[i].refused += n;
557 }
558 by_k.sort_by_key(|d| d.k);
559 Test {
560 risk: risk.unwrap_or(select::policy::ALPHA),
561 set: risk.is_some(),
562 ran,
563 control: e.control,
564 refused,
565 sessions: u64::from(e.sessions),
566 kept: r.kept,
567 by_k,
568 due: r.due,
569 due_checks: TEST_CHECKS,
570 due_sessions: u64::from(TEST_SESSIONS),
571 due_min_sessions: u64::from(TEST_MIN_SESSIONS),
572 dilution: r.by_asked.iter().filter_map(|(m, x)| estimate(format!("{m} asked − 1 asked"), "points changed", x)).collect(),
573 ask_free: r.ask_free.as_ref().and_then(|x| estimate("grade 2 asked − handed over".into(), "points changed", x)),
574 }
575 }
576}
577
578#[derive(Debug, Clone, Default, PartialEq, Serialize)]
580pub struct Count {
581 pub name: String,
582 pub part: u64,
583 pub whole: u64,
584}
585
586#[derive(Debug, Clone, Default, PartialEq, Serialize)]
588pub struct Spend {
589 pub source: String,
591 pub calls: u64,
592 pub rounds: u64,
594 pub usd: f64,
595}
596
597#[derive(Debug, Clone, Default, PartialEq, Serialize)]
601pub struct Budget {
602 pub tokens: f64,
603 pub spent: f64,
604 pub price: f64,
605}
606
607#[derive(Debug, Clone, Default, PartialEq, Serialize)]
609pub struct Estimate {
610 pub name: String,
611 pub unit: String,
616 pub mean: f64,
617 pub lo: f64,
618 pub hi: f64,
619 pub n: u64,
621}
622
623#[derive(Debug, Clone, Default, PartialEq, Serialize)]
626pub struct Evaluation {
627 pub sessions: u64,
629 pub mean: f64,
632 pub ucb: Option<f64>,
633 pub units: u64,
635 pub ess: f64,
636 pub max_weight: f64,
638 pub truncated: u64,
640}
641
642#[derive(Debug, Clone, Default, PartialEq, Serialize)]
647pub struct Measurement {
648 pub placebos: u64,
650 pub judged: u64,
652 pub tail_shown: u64,
655 pub tail_applied: u64,
656}
657
658#[derive(Debug, Clone, Default, PartialEq, Serialize)]
661pub struct Yield {
662 pub from: u32,
663 pub to: u32,
664 pub shown: u64,
665 pub applied: u64,
666 pub rate: f64,
667}
668
669#[derive(Debug, Clone, Default, PartialEq, Serialize)]
672pub struct Latency {
673 pub harness: String,
674 pub checks: u64,
675 pub p50_ms: u64,
677 pub p95_ms: u64,
678 pub budget_ms: Option<u64>,
680 pub over: u64,
683}
684
685const QUANTILES: [f64; 2] = [0.50, 0.95];
688
689fn quantile(sorted: &[u64], q: f64) -> u64 {
691 let rank = ((q * sorted.len() as f64).ceil() as usize).clamp(1, sorted.len().max(1));
692 sorted.get(rank - 1).copied().unwrap_or(0)
693}
694
695impl Selection {
696 #[must_use]
700 pub fn of(summary: &select::summary::Summary, policy: select::Policy, risk: Option<f64>) -> Selection {
701 use select::summary::{LIVE, Label, STOP};
702 let s = summary;
703 let learned = policy == select::Policy::Learned;
704 let reverted = learned && s.monitor.reverted;
705 let default = select::precedence(None, None, false).name();
706 let count = |name: &str, part: usize, whole: usize| Count { name: name.into(), part: part as u64, whole: whole as u64 };
707 let (sessions, checks) = s.checked();
709 let deviations = match s.gate.checks {
710 0 => Vec::new(),
711 n => vec![count("route", s.gate.deviated as usize, n as usize)],
712 };
713 let fallbacks = select::summary::FELL_BACK
715 .iter()
716 .zip(s.fell_back)
717 .filter(|_| checks > 0)
718 .map(|(reason, n)| Count { name: (*reason).into(), part: n, whole: checks })
719 .collect();
720 let spend = [("stop", STOP), ("live", LIVE)]
721 .into_iter()
722 .filter(|(_, i)| s.spend[*i].calls > 0)
723 .map(|(source, i)| Spend { source: source.into(), calls: s.spend[i].calls, rounds: s.spend[i].rounds, usd: s.spend[i].usd })
724 .collect();
725 let budget = s.fit.budget.map(|tokens| Budget { tokens, spent: s.pool.tokens as f64 / sessions.max(1) as f64, price: s.fit.lambda });
726 let judged = |l: Label| l.verdict();
728 let of = |route: select::Route| {
734 let of: Vec<_> = s.applies.iter().filter(|a| a.route == route && (route != select::Route::Free || !a.had)).collect();
735 (of.iter().filter(|a| judged(a.label)).count(), of.len())
736 };
737 let mut labels = Vec::new();
738 for (name, route) in [("judged, asked", select::Route::Ask), ("judged, free", select::Route::Free), ("judged, placebo", select::Route::Placebo)] {
739 let (part, whole) = of(route);
740 labels.push(count(name, part, whole));
741 }
742 let answers: Vec<_> = s.applies.iter().filter(|a| a.jv >= crate::predict::PURE_UNCLEAR_FROM && (judged(a.label) || a.label.pending())).collect();
747 let share = |name: &str, label: Label, route: Option<select::Route>| {
748 let of: Vec<_> = answers.iter().filter(|a| route.is_none_or(|r| a.route == r)).collect();
749 count(name, of.iter().filter(|a| a.label == label).count(), of.len())
750 };
751 for (name, label) in [("unclear", Label::Unclear), ("not arisen", Label::NotArisen), ("consequence remains", Label::Remains)] {
752 labels.push(share(name, label, None));
753 }
754 for (name, route) in [("unclear, asked", select::Route::Ask), ("unclear, free", select::Route::Free), ("unclear, placebo", select::Route::Placebo)] {
755 labels.push(share(name, Label::Unclear, Some(route)));
756 }
757 let later = s.applies.iter().filter(|a| matches!(a.route, select::Route::Not | select::Route::Placebo)).count();
758 let (placebos_judged, placebos) = of(select::Route::Placebo);
759 labels.push(count("placebo", placebos, later));
760 labels.retain(|c| c.whole > 0);
761 let measurement = (checks > 0).then_some(Measurement {
762 placebos: placebos as u64,
763 judged: placebos_judged as u64,
764 tail_shown: s.pool.tail.0,
765 tail_applied: s.pool.tail.1,
766 });
767 let share = |stripped: bool| {
769 let of: Vec<_> = s.applies.iter().filter(|a| a.route == select::Route::Ask && judged(a.label) && a.stripped == stripped).collect();
770 (of.iter().filter(|a| a.label == Label::Changed).count() as f64, of.len() as f64)
771 };
772 let ((c1, n1), (c0, n0)) = (share(false), share(true));
773 let declaration = (n1 > 0.0 && n0 > 0.0).then(|| {
774 let (r1, r0) = (c1 / n1, c0 / n0);
775 let se = (r1 * (1.0 - r1) / n1 + r0 * (1.0 - r0) / n0).sqrt();
776 let mean = 100.0 * (r1 - r0);
777 Estimate { name: "declaration".into(), unit: "points".into(), mean, lo: mean - 100.0 * Z95 * se, hi: mean + 100.0 * Z95 * se, n: (n1 + n0) as u64 }
778 });
779 let pool = s
780 .pool
781 .bins
782 .iter()
783 .enumerate()
784 .filter(|(_, (shown, _))| *shown > 0)
785 .map(|(b, &(shown, applied))| Yield {
786 from: (1u32 << b) - 1,
787 to: (1u32 << (b + 1)) - 1,
788 shown,
789 applied,
790 rate: s.fit.pool.yields.get(b).map_or(applied as f64 / shown as f64, |(a, b)| a / (a + b)),
791 })
792 .collect();
793 let (allowance, ledger) = if learned && !reverted { select::policy::gate_parts(&s.gate, risk.unwrap_or(select::policy::ALPHA)) } else { (0.0, 0.0) };
794 let mon = &s.monitor;
795 let evaluation = (mon.sessions > 0).then(|| Evaluation {
796 sessions: u64::from(mon.sessions),
797 mean: mon.mean,
798 ucb: mon.ucb,
799 units: u64::from(mon.labelled),
800 ess: mon.ess,
801 max_weight: mon.max_weight,
802 truncated: u64::from(mon.truncated),
803 });
804 let latency = s
805 .pool
806 .stops
807 .iter()
808 .filter(|(_, calls)| !calls.is_empty())
809 .map(|(harness, calls)| {
810 let mut ms: Vec<u64> = calls.iter().map(|&(ms, _)| u64::from(ms)).collect();
811 ms.sort_unstable();
812 let budget = crate::harness::ALL.iter().find(|h| h.name() == harness).and_then(|h| h.caps().stop_budget);
813 Latency {
814 harness: harness.clone(),
815 checks: ms.len() as u64,
816 p50_ms: quantile(&ms, QUANTILES[0]),
817 p95_ms: quantile(&ms, QUANTILES[1]),
818 budget_ms: budget.map(|b| u64::try_from(b.as_millis()).unwrap_or(u64::MAX)),
819 over: calls.iter().filter(|(_, failed)| *failed).count() as u64,
820 }
821 })
822 .collect();
823 Selection {
824 policy: policy.name(),
825 default,
826 source: if reverted { select::REVERTED_SOURCE } else { policy.name() },
827 gate: if learned && !reverted { select::policy::remaining_at(&s.gate, risk.unwrap_or(select::policy::ALPHA)) } else { 0.0 },
828 allowance,
829 ledger,
830 fit: s.fit.key.clone(),
831 at: s.at,
832 checks,
833 sessions,
834 deviations,
835 fallbacks,
836 spend,
837 budget,
838 effects: s.fit.model.as_ref().map(|m| effects(m, s.fit.labels)).unwrap_or_default(),
839 half_life: s.fit.model.as_ref().map(|_| s.fit.half_life.unwrap_or(0)),
840 labels,
841 declaration,
842 delivery: (!s.fit.key.is_empty()).then(|| s.fit.rates.clone()),
843 evaluation,
844 measurement,
845 pool,
846 latency,
847 test: (learned || s.experiment.totals() != (0, 0)).then(|| Test::of(s, risk)),
848 judge: Vec::new(),
849 }
850 }
851
852 #[must_use]
855 pub fn shown(&self) -> bool {
856 self.checks > 0 || self.source != self.default
857 }
858}
859
860const Z95: f64 = 1.96;
862
863fn effects(m: &select::policy::Model, n: u32) -> Vec<Estimate> {
867 let p = select::policy::P;
868 if m.beta.len() != p || m.chol.len() != p * p {
869 return Vec::new();
870 }
871 let sd = |k: usize| m.chol[k * p..=k * p + k].iter().map(|x| x * x).sum::<f64>().sqrt();
873 let mut out: Vec<Estimate> = (0..p)
874 .map(|k| Estimate {
875 name: select::policy::NAMES[k].into(),
876 unit: "coefficient".into(),
877 mean: m.beta[k],
878 lo: m.beta[k] - Z95 * sd(k),
879 hi: m.beta[k] + Z95 * sd(k),
880 n: u64::from(n),
881 })
882 .collect();
883 for (name, v) in ["session spread", "lesson spread", "judge spread"].into_iter().zip(m.spread) {
884 out.push(Estimate { name: name.into(), unit: "logit".into(), mean: v, lo: v, hi: v, n: u64::from(n) });
885 }
886 out
887}
888
889#[must_use]
891pub fn selection(lessons: &Path, policy: select::Policy) -> Selection {
892 let mut s = Selection::of(&select::summary::load(lessons), policy, select::risk_of(lessons));
893 let current = crate::hook::judge_model(&std::env::var("BORG_PREDICT_MODEL").unwrap_or_default());
895 s.judge = crate::judge_length::lengths(&lessons.with_file_name("state"), ¤t);
896 s
897}
898
899fn grouped(n: usize) -> String {
901 let digits = n.to_string();
902 let mut out = String::new();
903 for (i, c) in digits.chars().enumerate() {
904 if i > 0 && (digits.len() - i).is_multiple_of(3) {
905 out.push(',');
906 }
907 out.push(c);
908 }
909 out
910}
911
912#[must_use]
915pub fn judge_length_line(l: &crate::judge_length::Length) -> String {
916 use crate::judge_length::Decision;
917 let start = crate::hook::END_START_BUDGET;
918 let how = if l.start {
919 "the start: no decided step moved it".to_string()
920 } else if l.chars > start {
921 format!("measured, up from the start {}", grouped(start))
922 } else {
923 format!("measured, down from the start {}", grouped(start))
924 };
925 let mut parts = vec![format!("{}: session-end budget {} chars ({how})", l.model, grouped(l.chars))];
926 let measured: Vec<String> = l
927 .steps
928 .iter()
929 .filter(|s| s.sent > 0)
930 .map(|s| {
931 let what = match s.decision {
932 Decision::Reference => format!("reference, line {:.0}%", 100.0 * l.line.unwrap_or(0.0)),
933 Decision::Below => "below the reference".into(),
934 Decision::Holds => "holds".into(),
935 Decision::Fails => "fails".into(),
936 Decision::Open => "open".into(),
937 };
938 format!("≤{} {:.0}% of {} [{:.0}–{:.0}%] {what}", grouped(s.upto), 100.0 * s.answered as f64 / s.sent as f64, s.sent, 100.0 * s.lo, 100.0 * s.hi)
939 })
940 .collect();
941 parts.push(if measured.is_empty() { "no judge call measured yet".into() } else { format!("answered by prompt size: {}", measured.join(", ")) });
942 if l.line.is_none() && !measured.is_empty() {
943 parts.push(format!(
944 "no reference yet (the smallest steps with {} lessons in {} calls)",
945 crate::judge_length::REFERENCE_LESSONS,
946 crate::judge_length::MIN_CALLS
947 ));
948 }
949 parts.push(match l.explore {
950 Some(up) if l.steps.iter().any(|s| s.upto == up && s.decision == Decision::Fails) => {
951 format!("exploring the failed ≤{} on {:.0}% of session-end passes", grouped(up), 100.0 * l.psi)
952 }
953 Some(up) => format!("exploring ≤{} on {:.0}% of session-end passes", grouped(up), 100.0 * l.psi),
954 None => "nothing explored".into(),
955 });
956 parts.join(" · ")
957}
958
959#[must_use]
968pub fn selection_line(s: &Selection) -> String {
969 let default = s.source == s.default;
970 let head = match s.source {
971 select::REVERTED_SOURCE => "REVERTED to the fixed rule: the monitor saw harm beyond the gate's share (sticky)".to_string(),
972 "learned" => {
973 let opt = if default { "" } else { " (opt-in, BORG_SELECT=learned)" };
974 if s.gate > 0.0 {
975 format!("learned{opt}, gate {:.3} (quality it may still risk below the fixed rule)", s.gate)
976 } else if s.fit.is_empty() || s.allowance <= 0.0 {
977 format!("learned{opt}, cold start: gate 0, every decision the fixed rule's")
978 } else {
979 format!("learned{opt}, gate spent (ledger q05 {:+.3} against allowance {:.3}), deviations need positive evidence", s.ledger, s.allowance)
982 }
983 }
984 "fixed" if default => "the fixed rule (the default; BORG_SELECT=learned opts in to the learned policy)".into(),
985 "fixed" => "the fixed rule (BORG_SELECT=fixed)".into(),
986 "cap" => "the fixed rule with an ask cap (BORG_STOP_ASK_CAP, evaluation only)".into(),
987 "attention" => "attention's cut (BORG_STOP_ATTENTION=on, evaluation only)".into(),
988 other => other.to_string(),
989 };
990 if s.checks == 0 {
991 return format!("{head} · no graded check folded yet");
992 }
993 let mut parts = vec![head, format!("{} checks", s.checks)];
994 if !s.deviations.is_empty() {
995 parts.push(format!("deviated: {}", s.deviations.iter().map(|c| format!("{} {}/{}", c.name, c.part, c.whole)).collect::<Vec<_>>().join(", ")));
996 }
997 let fell: Vec<String> = s.fallbacks.iter().filter(|c| c.part > 0).map(|c| format!("{} {}", c.name, c.part)).collect();
998 if !fell.is_empty() {
999 parts.push(format!("learned decisions the fixed rule took over: {}", fell.join(", ")));
1000 }
1001 if let Some(t) = &s.test {
1002 parts.push(test_line(t));
1003 }
1004 if let Some(m) = &s.measurement {
1005 parts.push(format!("placebos {} drawn, {} judged", m.placebos, m.judged));
1006 if m.tail_shown > 0 {
1007 parts.push(format!("tail {} shown past the pool, {} applied", m.tail_shown, m.tail_applied));
1008 }
1009 }
1010 if let Some(e) = &s.evaluation {
1011 let reading = match e.ucb {
1012 Some(u) => format!("monitor {:+.3}, bound {u:+.3} over {} sessions (below 0 reverts)", e.mean, e.sessions),
1013 None => format!("monitor {:+.3} over {} sessions, no bound yet", e.mean, e.sessions),
1014 };
1015 parts.push(format!("{reading}, ESS {:.0} of {} weighed, largest weight {:.1}× the mean", e.ess, e.units, e.max_weight));
1016 }
1017 if !s.latency.is_empty() {
1018 let secs = |ms: u64| format!("{:.1} s", ms as f64 / 1000.0);
1019 let each: Vec<String> = s
1020 .latency
1021 .iter()
1022 .map(|l| {
1023 let budget = l.budget_ms.map_or_else(String::new, |b| format!(" of {}", secs(b)));
1024 let failed = if l.over > 0 { format!(", {} failed", l.over) } else { String::new() };
1025 format!("{} {}{budget}{failed}", l.harness, secs(l.p95_ms))
1026 })
1027 .collect();
1028 parts.push(format!("stop p95 {}", each.join("; ")));
1029 }
1030 if s.sessions > 0 && !s.spend.is_empty() {
1031 let n = s.sessions as f64;
1032 let (usd, rounds) = s.spend.iter().fold((0.0, 0), |(u, r), x| (u + x.usd, r + x.rounds));
1033 parts.push(format!("${:.2} and {:.1} extra rounds per session ({} sessions)", usd / n, rounds as f64 / n, s.sessions));
1034 }
1035 if let Some(b) = &s.budget {
1036 parts.push(format!("budget {:.0} pool tokens per session: {:.0} read, price {:.2e} per token", b.tokens, b.spent, b.price));
1037 }
1038 parts.join(" · ")
1039}
1040
1041fn test_line(t: &Test) -> String {
1046 let risk = if t.set { format!("risk {:.2} (the collective's)", t.risk) } else { format!("risk {:.2}", t.risk) };
1047 match (t.ran, t.risk <= 0.0) {
1048 (0, true) => return "question-count test off (risk 0)".into(),
1049 (0, false) => return format!("question-count test: no check yet, {risk}"),
1050 _ => {}
1051 }
1052 let kept = if t.kept < t.ran { format!(", {} still in the summary", t.kept) } else { String::new() };
1053 let head =
1054 format!("question-count test {} drawn ({} control, {} refused by the gate{kept}) in {} sessions, {risk}", t.ran, t.control, t.refused, t.sessions);
1055 match (&t.ask_free, t.due) {
1056 (Some(e), true) => format!("{head}; readout: grade 2 asked − handed over {:+.1} [{:+.1}, {:+.1}] points changed", e.mean, e.lo, e.hi),
1057 (None, true) => format!("{head}; readout due, no judged grade 2 on both routes yet"),
1058 (_, false) => format!("{head}; readout after {} checks in {} sessions, or {} sessions", t.due_checks, t.due_min_sessions, t.due_sessions),
1059 }
1060}
1061
1062#[must_use]
1064pub fn render_with(name: &str, share: Share, drones: &[Drone], inbox: &Inbox, select: Option<&Selection>, now: i64) -> String {
1065 let mut s = render(name, share, drones, inbox, now);
1066 if let Some(sel) = select.filter(|s| s.shown()) {
1067 s.push_str(&format!(" selection: {}\n", selection_line(sel)));
1068 }
1069 s
1070}
1071
1072pub fn version_line<'a>(stores: impl IntoIterator<Item = &'a Path>, checked: Option<&update::Checked>, check: bool) -> Option<String> {
1078 let mut parts: Vec<String> = Vec::new();
1079 if let Some(l) = checked.and_then(|c| c.latest.as_deref()).filter(|l| check && update::newer(l, update::VERSION)) {
1080 parts.push(format!("borg {l} is released, this is {}: borg update", update::VERSION));
1081 }
1082 let aliases = collective::aliases();
1083 let ours = protocol::current();
1084 for home in stores.into_iter().filter(|h| h.join("mesh").is_dir()) {
1085 let versions = protocol::versions(home);
1086 let mismatches = protocol::mismatches(home);
1087 let name = |key: &str| aliases.display(&protocol::member(key, versions.get(key)));
1088 for (key, m) in &mismatches {
1089 parts.push(format!("nothing syncs with {} ({}, this node {})", name(key), m.theirs, m.ours));
1090 }
1091 for (key, v) in versions.iter().filter(|(k, v)| v.protocol < ours && !mismatches.contains_key(*k)) {
1092 parts.push(format!("{} on /{} misses {}", name(key), v.protocol, protocol::misses(v.protocol, ours)));
1093 }
1094 }
1095 parts.dedup();
1096 (!parts.is_empty()).then(|| parts.join(" · "))
1097}
1098
1099fn routes(dir: Option<&str>) -> Result<Vec<Route>, String> {
1101 let cfg = collective::load()?;
1102 let base = learn::home();
1103 Ok(match dir {
1104 Some(d) => vec![collective::route(cfg.as_ref(), d, &base)].into_iter().filter(|r| *r != Route::Off).collect(),
1105 None => install::collectives(cfg.as_ref(), &base),
1106 })
1107}
1108
1109pub fn gather(dir: Option<&str>, now: i64) -> Result<(Vec<crate::top_live::Coll>, Option<String>), String> {
1112 let mut out = Vec::new();
1113 let routes = routes(dir)?;
1114 let stores: Vec<&Path> = routes
1115 .iter()
1116 .filter_map(|r| match r {
1117 Route::Collective { home, .. } => Some(home.as_path()),
1118 Route::Off => None,
1119 })
1120 .collect();
1121 let version = version_line(stores, update::checked().as_ref(), update::on());
1122 for r in &routes {
1123 let Route::Collective { name, home, share } = r else { continue };
1124 presence::prune(home, now);
1125 let mut ds = drones(home, collective::may_sync(r), now);
1126 for d in &mut ds {
1127 d.p.engineer = collective::aliases().display(&d.p.engineer);
1128 }
1129 let inbox = inbox(home, &ds, now);
1130 if ds.is_empty() && inbox.recent.is_empty() && dir.is_none() {
1131 continue;
1132 }
1133 let lessons = home.join("lessons.jsonl");
1136 let summary = select::summary::peek(&lessons);
1137 let select = Some(Selection::of(&summary, select::policy(), select::risk_of(&lessons))).filter(Selection::shown).map(|s| selection_line(&s));
1138 out.push(crate::top_live::Coll { name: name.clone(), home: home.clone(), share: *share, drones: ds, inbox, select });
1139 }
1140 Ok((out, version))
1141}
1142
1143fn screen(dir: Option<&str>, json_out: bool, now: i64) -> Result<String, String> {
1145 let routes = routes(dir)?;
1146 let text = screen_of(&routes, dir.is_some(), &engineer_dir(dir), msg::caller, json_out, now);
1147 let stores = routes.iter().filter_map(|r| match r {
1148 Route::Collective { home, .. } => Some(home.as_path()),
1149 Route::Off => None,
1150 });
1151 Ok(match version_line(stores, update::checked().as_ref(), update::on()).filter(|_| !json_out) {
1152 Some(v) => format!("{v}\n\n{text}"),
1153 None => text,
1154 })
1155}
1156
1157fn screen_of(
1162 routes: &[Route],
1163 one: bool,
1164 cwd: &str,
1165 caller: impl Fn(&[Drone]) -> Result<Option<(String, &'static str)>, String>,
1166 json_out: bool,
1167 now: i64,
1168) -> String {
1169 let mut text = String::new();
1170 let mut all = Vec::new();
1171 let (mut gathered, mut printed) = (Vec::new(), Vec::new());
1174 for r in routes {
1175 let Route::Collective { name, home, share } = r else { continue };
1176 presence::prune(home, now);
1177 let mut ds = drones(home, collective::may_sync(r), now);
1178 for d in &mut ds {
1180 d.p.engineer = collective::aliases().display(&d.p.engineer);
1181 }
1182 let inbox = inbox(home, &ds, now);
1183 let sel = selection(&home.join("lessons.jsonl"), select::policy());
1184 if json_out {
1185 let mut v = to_json_with(name, *share, &ds, &inbox);
1186 v["select"] = json!(sel);
1187 all.push(v);
1188 } else if !ds.is_empty() || !inbox.recent.is_empty() || one {
1189 text.push_str(&render_with(name, *share, &ds, &inbox, Some(&sel), now));
1190 text.push('\n');
1191 printed.push((home, inbox.recent.into_iter().filter(|m| prints_text(&ds, &inbox.here, m)).collect::<Vec<_>>()));
1192 }
1193 gathered.push(ds);
1194 }
1195 if !printed.is_empty() && engineer_looks(caller, gathered.iter().map(Vec::as_slice)) {
1196 let mut done = HashSet::new();
1197 for (home, recent) in &printed {
1198 acknowledge(home, cwd, &recent.iter().collect::<Vec<_>>(), &mut done, now);
1199 }
1200 }
1201 if json_out {
1202 return serde_json::to_string_pretty(&all).unwrap_or_default() + "\n";
1203 }
1204 if text.is_empty() {
1205 text = "no drones (borg top lists sessions whose hooks ran in the last day)\n".into();
1206 }
1207 text
1208}
1209
1210pub fn main(args: &[&str]) -> i32 {
1211 let (mut dir, mut once, mut json_out) = (None, false, false);
1212 let mut it = args.iter();
1213 while let Some(a) = it.next() {
1214 match *a {
1215 "--dir" if dir.is_none() => match it.next() {
1216 Some(d) => dir = Some(*d),
1217 None => return usage("--dir needs a value"),
1218 },
1219 "--once" => once = true,
1220 "--json" => json_out = true,
1221 a => return usage(&format!("unknown flag {a}")),
1222 }
1223 }
1224 if !json_out && !once && unsafe { libc::isatty(1) } == 1 {
1226 return crate::top_live::run(dir);
1227 }
1228 match screen(dir, json_out, learn::now_ms()) {
1229 Ok(s) => {
1230 print!("{s}");
1231 0
1232 }
1233 Err(e) => {
1234 eprintln!("borg top: {e}");
1235 1
1236 }
1237 }
1238}
1239
1240fn usage(why: &str) -> i32 {
1241 eprintln!("borg top: {why}\nusage: borg top [--once] [--json] [--dir DIR]");
1242 2
1243}
1244
1245#[cfg(test)]
1246pub(crate) fn tests_fixture() -> Vec<Drone> {
1247 tests::fixture()
1248}
1249
1250#[cfg(test)]
1251pub(crate) fn tests_inbox() -> Inbox {
1252 tests::inbox_fixture()
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use super::*;
1258
1259 fn p(session: &str, engineer: &str, started: i64, ms: i64, event: &str, prompt: &str, files: &[&str]) -> Presence {
1260 Presence {
1261 session: session.into(),
1262 harness: "claude".into(),
1263 engineer: engineer.into(),
1264 repo: "borg-ai".into(),
1265 collective: "corp".into(),
1266 prompt: prompt.into(),
1267 files: files.iter().map(|f| f.to_string()).collect(),
1268 event: event.into(),
1269 ms,
1270 started,
1271 pid: None,
1272 }
1273 }
1274
1275 const NOW: i64 = 10_000_000;
1276
1277 pub(crate) fn fixture() -> Vec<Drone> {
1278 let local = vec![
1279 p("aaaa1111-x", "alice@x", 100, NOW - 5_000, "post_tool", "fix the flaky sync test", &["crates/borg-mesh/src/sync.rs", "src/recon.rs"]),
1280 p("cccc3333-x", "alice@x", 300, NOW - presence::IDLE_MS - 60_000, "stop", "write the release notes for the mesh daemon and more words", &[]),
1281 ];
1282 let remote = vec![presence::Remote {
1283 p: p("bbbb2222-y", "bob@y", 200, NOW - 120_000, "stop", "review PR 42", &["README.md"]),
1284 key: "beefcafe".repeat(8),
1285 sig: String::new(),
1286 }];
1287 assemble(local, remote, NOW, |_| true, |p| p.session.starts_with("aaaa"))
1288 }
1289
1290 fn message(by: &str, name: &str, to: &str, kind: msg::Kind, ms: i64, text: &str) -> Message {
1291 message_on("n1", by, name, to, kind, ms, text)
1292 }
1293
1294 fn message_on(node: &str, by: &str, name: &str, to: &str, kind: msg::Kind, ms: i64, text: &str) -> Message {
1295 let from = msg::Sender { by: by.into(), name: name.into(), harness: "claude".into(), node: node.into() };
1296 Message::new(kind, &from, to, None, ms, text)
1297 }
1298
1299 pub(crate) fn inbox_fixture() -> Inbox {
1300 Inbox {
1301 unread: [("aaaa1111-x".to_string(), 2), ("cccc3333-x".to_string(), 0)].into_iter().collect(),
1302 recent: vec![
1303 message("cccc3333-x", "Three of Three", "aaaa1111-x", msg::ASK, NOW - 60_000, "which test\nflakes?"),
1304 message("engineer:alice@x", "alice@x", msg::ALL, msg::TELL, NOW - 120_000, "release at five"),
1305 message_on(&"feed".repeat(16), "gone-session", "Six of Six", "zzzz9999-q", msg::TELL, NOW - 180_000, "done"),
1306 ],
1307 day: 3,
1308 here: "n1".into(),
1309 listen: [("aaaa1111-x".to_string(), crate::listen::Status::Waits), ("cccc3333-x".to_string(), crate::listen::Status::Listens)]
1310 .into_iter()
1311 .collect(),
1312 }
1313 }
1314
1315 #[test]
1316 fn renders_one_screen_per_collective() {
1317 let out = render("corp", Share::Team, &fixture(), &inbox_fixture(), NOW);
1318 let expected = "\
1319corp (team): 3 drone(s), 2 active, 1 remote
1320 DRONE SESSION ENGINEER REPO STATE WAKE IDLE UNREAD DOING FILES
1321 One of Three aaaa111… alice@x borg-ai paused waiter 5s 2 fix the flaky sync test crates/borg-mesh/src/sync.rs, src/recon.rs
1322 Two of Three bbbb222… bob@y @beefca borg-ai waiting - 2m - review PR 42 README.md
1323 Three of Three cccc333… alice@x borg-ai idle on 31m 0 write the release notes for the mesh da…
1324 messages: 3 in the last day, 2 unread
1325 1m Three of Three asks -> One of Three: which test flakes?
1326 2m alice@x -> collective: release at five
1327 3m Six of Six @feedfeedfeed -> zzzz9999: done
1328";
1329 assert_eq!(out, expected, "\n{out}");
1330 let none = Inbox::default();
1331 assert_eq!(render("private", Share::Local, &[], &none, NOW), "private (local, never leaves this machine): 0 drone(s), 0 active, 0 remote\n");
1332 }
1333
1334 fn learned() -> Selection {
1338 let spend = |source: &str, rounds, usd| Spend { source: source.into(), calls: 1, rounds, usd };
1339 let evaluation = Evaluation { sessions: 12, mean: 0.004, ucb: Some(0.031), units: 300, ess: 180.0, max_weight: 6.5, truncated: 3 };
1340 Selection {
1341 policy: "learned",
1342 default: "fixed",
1343 source: "learned",
1344 gate: 0.12,
1345 checks: 480,
1346 sessions: 12,
1347 deviations: vec![Count { name: "route".into(), part: 5, whole: 400 }],
1348 spend: vec![spend("stop", 4, 3.0), spend("live", 0, 2.04)],
1349 evaluation: Some(evaluation),
1350 measurement: Some(Measurement { placebos: 40, judged: 31, tail_shown: 12, tail_applied: 3 }),
1351 test: Some(Test {
1352 risk: 0.1,
1353 ran: 30,
1354 control: 9,
1355 refused: 4,
1356 sessions: 6,
1357 kept: 30,
1358 due_checks: 300,
1359 due_sessions: 30,
1360 due_min_sessions: 15,
1361 ..Test::default()
1362 }),
1363 ..Selection::default()
1364 }
1365 }
1366
1367 #[test]
1371 fn the_judges_effective_length_is_read_from_the_stores_cost_lines_and_shown() {
1372 let home = std::env::temp_dir().join(format!("borg-top-judge-length-{}", std::process::id()));
1373 let _ = std::fs::remove_dir_all(&home);
1374 std::fs::create_dir_all(home.join("state")).unwrap();
1375 let line = |chars: usize, n: u64, a: u64| {
1376 format!(
1377 "judge call=1 pending={n} resolved={a} chars={chars} returned= unmatched= verdicts=changed:{a},ignored:0,not_arisen:0,consequence_remains:0,unclear:{} reason_chars=1 model=x:big at=end\n",
1378 n - a
1379 )
1380 .repeat(10)
1381 };
1382 std::fs::write(home.join("state/s.cost"), line(30_000, 40, 20) + &line(70_000, 30, 15) + &line(100_000, 30, 15)).unwrap();
1383 let sel = selection(&home.join("lessons.jsonl"), select::policy());
1384 let big = sel.judge.iter().find(|l| l.model == "x:big").expect("the store's judge");
1385 assert_eq!((big.chars, big.start, big.explore), (108_000, false, Some(162_000)));
1386 assert_eq!(
1387 judge_length_line(big),
1388 "x:big: session-end budget 108,000 chars (measured, up from the start 72,000) · answered by prompt size: ≤32,000 50% of 400 [45–55%] reference, line 42%, ≤72,000 50% of 300 [44–56%] holds, ≤108,000 50% of 300 [44–56%] holds · exploring ≤162,000 on 20% of session-end passes"
1389 );
1390 let v = serde_json::to_value(&sel).unwrap();
1391 assert_eq!(v["judge"].as_array().map(|a| a.iter().any(|j| j["model"] == "x:big" && j["chars"] == 108_000)), Some(true), "{v}");
1392 let none = crate::judge_length::Length::of("y:new", &crate::judge_length::Steps::new());
1393 assert_eq!(
1394 judge_length_line(&none),
1395 "y:new: session-end budget 72,000 chars (the start: no decided step moved it) · no judge call measured yet · exploring ≤108,000 on 20% of session-end passes"
1396 );
1397 assert_eq!((grouped(0), grouped(999), grouped(1_000), grouped(1_234_567)), ("0".into(), "999".into(), "1,000".into(), "1,234,567".into()));
1398 let _ = std::fs::remove_dir_all(&home);
1399 }
1400
1401 #[test]
1402 fn the_selection_line_says_what_decides_and_prices_spend_only_under_a_budget() {
1403 use select::Policy;
1404 use select::summary::Summary;
1405 let fresh = |policy, default| Selection { default, ..Selection::of(&Summary::default(), policy, None) };
1406 let fixed = fresh(Policy::Fixed, "fixed");
1408 assert_eq!(selection_line(&fixed), "the fixed rule (the default; BORG_SELECT=learned opts in to the learned policy) · no graded check folded yet");
1409 assert!(!fixed.shown(), "the default with nothing folded: nothing to show on the drones screen");
1410 let opted = fresh(Policy::Learned, "fixed");
1411 assert_eq!(
1412 selection_line(&opted),
1413 "learned (opt-in, BORG_SELECT=learned), cold start: gate 0, every decision the fixed rule's · no graded check folded yet"
1414 );
1415 assert!(opted.shown(), "a switch decides: shown");
1416 assert_eq!(selection_line(&fresh(Policy::Fixed, "learned")), "the fixed rule (BORG_SELECT=fixed) · no graded check folded yet");
1418 assert!(!fresh(Policy::Learned, "learned").shown() && fresh(Policy::Fixed, "learned").shown());
1419 let d = select::precedence(None, None, false);
1420 assert_eq!(Selection::of(&Summary::default(), Policy::Cap(2), None).default, d.name(), "the default is select.rs's, no switch set");
1421 assert!(!Selection::of(&Summary::default(), d, None).shown());
1422 let mut s = learned();
1423 let line = selection_line(&s);
1424 assert_eq!(
1425 line,
1426 "learned (opt-in, BORG_SELECT=learned), gate 0.120 (quality it may still risk below the fixed rule) · 480 checks · deviated: route 5/400 · question-count test 30 drawn (9 control, 4 refused by the gate) in 6 sessions, risk 0.10; readout after 300 checks in 15 sessions, or 30 sessions · placebos 40 drawn, 31 judged · tail 12 shown past the pool, 3 applied · monitor +0.004, bound +0.031 over 12 sessions (below 0 reverts), ESS 180 of 300 weighed, largest weight 6.5× the mean · $0.42 and 0.3 extra rounds per session (12 sessions)"
1427 );
1428 assert!(!line.contains("budget") && !line.contains("price"), "no budget set: spend measured, never priced: {line}");
1429 let mut early = learned();
1430 early.evaluation = Some(Evaluation { ucb: None, sessions: 3, units: 40, ess: 12.4, max_weight: 3.0, ..Evaluation::default() });
1431 let line = selection_line(&early);
1432 assert!(line.contains(" · monitor +0.000 over 3 sessions, no bound yet, ESS 12 of 40 weighed, largest weight 3.0× the mean · "), "{line}");
1433 early.latency = vec![
1434 Latency { harness: "codex".into(), checks: 20, p50_ms: 10_000, p95_ms: 19_000, budget_ms: Some(30_000), over: 1 },
1435 Latency { harness: "claude".into(), checks: 1, p50_ms: 4_000, p95_ms: 4_000, budget_ms: None, over: 0 },
1436 ];
1437 let line = selection_line(&early);
1438 assert!(line.contains(" · stop p95 codex 19.0 s of 30.0 s, 1 failed; claude 4.0 s · $"), "per harness, against its stop budget: {line}");
1439 s.budget = Some(Budget { tokens: 30_000.0, spent: 24_000.0, price: 0.0002 });
1440 assert!(selection_line(&s).ends_with(" · budget 30000 pool tokens per session: 24000 read, price 2.00e-4 per token"), "{}", selection_line(&s));
1441 s.source = select::REVERTED_SOURCE;
1442 assert!(selection_line(&s).starts_with("REVERTED to the fixed rule"), "{}", selection_line(&s));
1443 }
1444
1445 #[test]
1449 fn a_spent_gate_is_not_a_cold_start() {
1450 use select::Policy;
1451 use select::summary::{Gate, Summary};
1452 let line = |s: &Summary| selection_line(&Selection { default: "learned", ..Selection::of(s, Policy::Learned, None) });
1453 let mut s = Summary::default();
1454 s.fit.key = "3556601-bc6d6191".into();
1455 assert!(line(&s).starts_with("learned, cold start: gate 0, every decision the fixed rule's"), "{}", line(&s));
1457 s.gate = Gate { gain: vec![-0.1018; select::policy::DRAWS], allowance: 0.0760, ..Gate::default() };
1458 let spent = Selection::of(&s, Policy::Learned, None);
1459 assert_eq!((spent.gate, spent.allowance, spent.ledger), (0.0, 0.0760, -0.1018));
1460 assert!(line(&s).starts_with("learned, gate spent (ledger q05 -0.102 against allowance 0.076), deviations need positive evidence · "), "{}", line(&s));
1461 s.fit.key.clear();
1463 assert!(line(&s).starts_with("learned, cold start: gate 0"), "{}", line(&s));
1464 s.fit.key = "3556601-bc6d6191".into();
1466 s.gate.gain = vec![-0.05; select::policy::DRAWS];
1467 assert!(line(&s).starts_with("learned, gate 0.026 (quality it may still risk below the fixed rule)"), "{}", line(&s));
1468 let fixed = Selection::of(&s, Policy::Fixed, None);
1470 assert_eq!((fixed.allowance, fixed.ledger), (0.0, 0.0));
1471 }
1472
1473 #[test]
1476 fn a_store_says_how_its_lessons_are_selected() {
1477 use select::Policy;
1478 let home = std::env::temp_dir().join(format!("borg-top-select-{}", std::process::id()));
1479 let _ = std::fs::remove_dir_all(&home);
1480 std::fs::create_dir_all(&home).unwrap();
1481 let lessons = home.join("lessons.jsonl");
1482 std::fs::write(&lessons, "").unwrap();
1483 let line = |lessons: &Path, policy| selection_line(&selection(lessons, policy));
1484 assert!(line(&lessons, Policy::Learned).ends_with("cold start: gate 0, every decision the fixed rule's · no graded check folded yet"));
1485 assert!(line(&lessons, Policy::Fixed).starts_with("the fixed rule ("), "{}", line(&lessons, Policy::Fixed));
1486 assert!(line(&lessons, Policy::Cap(2)).contains("(BORG_STOP_ASK_CAP, evaluation only)"));
1487 let absent = home.join("absent").join("lessons.jsonl");
1488 assert!(line(&absent, Policy::Attention).contains("(BORG_STOP_ATTENTION=on, evaluation only)"), "no store: the prior");
1489 let _ = std::fs::remove_dir_all(&home);
1490 }
1491
1492 #[test]
1494 fn the_selection_line_follows_the_screen_when_shown() {
1495 let (ds, inbox) = (fixture(), inbox_fixture());
1496 let plain = render("corp", Share::Team, &ds, &inbox, NOW);
1497 assert_eq!(render_with("corp", Share::Team, &ds, &inbox, None, NOW), plain);
1498 let cold = Selection::of(&select::summary::Summary::default(), select::precedence(None, None, false), None);
1499 assert_eq!(render_with("corp", Share::Team, &ds, &inbox, Some(&cold), NOW), plain, "the default at cold start: no line");
1500 let s = learned();
1501 assert_eq!(render_with("corp", Share::Team, &ds, &inbox, Some(&s), NOW), format!("{plain} selection: {}\n", selection_line(&s)));
1502 }
1503
1504 #[test]
1506 fn the_selection_serialises_every_section_for_the_page() {
1507 let v = serde_json::to_value(learned()).unwrap();
1508 let mut keys: Vec<&str> = v.as_object().unwrap().keys().map(String::as_str).collect();
1509 keys.sort_unstable();
1510 let want = "allowance at budget checks declaration default delivery deviations effects evaluation fallbacks fit gate half_life judge labels latency ledger measurement policy pool sessions source spend test";
1511 assert_eq!(keys, want.split(' ').collect::<Vec<_>>());
1512 assert_eq!((&v["budget"], &v["spend"][0]["source"], &v["deviations"][0]["whole"]), (&Value::Null, &json!("stop"), &json!(400)));
1513 let mut keys: Vec<&str> = v["evaluation"].as_object().unwrap().keys().map(String::as_str).collect();
1514 keys.sort_unstable();
1515 assert_eq!(keys, ["ess", "max_weight", "mean", "sessions", "truncated", "ucb", "units"]);
1516 assert_eq!((&v["default"], &v["measurement"]), (&json!("fixed"), &json!({"placebos": 40, "judged": 31, "tail_shown": 12, "tail_applied": 3})));
1517 let mut keys: Vec<&str> = v["test"].as_object().unwrap().keys().map(String::as_str).collect();
1518 keys.sort_unstable();
1519 let want = "ask_free by_k control dilution due due_checks due_min_sessions due_sessions kept ran refused risk sessions set";
1520 assert_eq!(keys, want.split(' ').collect::<Vec<_>>());
1521 }
1522
1523 #[test]
1528 fn the_judged_share_leaves_out_what_its_session_already_had() {
1529 use crate::outcome::{self, Outcome};
1530 use select::summary::{Summary, fold};
1531 let rec = |kind: &str, check: &str, channel: &str, ms: i64| Outcome {
1532 kind: kind.into(),
1533 session: "s".into(),
1534 lessons: vec!["A".into()],
1535 check: check.into(),
1536 channel: channel.into(),
1537 ms,
1538 ..Default::default()
1539 };
1540 let graded = |check: &str, ms: i64| Outcome {
1541 grades: vec![2],
1542 ranks: vec![0],
1543 applied: 1,
1544 free: vec!["A".into()],
1545 pf: vec![1.0],
1546 policy: "fixed".into(),
1547 shown: 20,
1548 tokens: 6_000,
1549 ..rec(outcome::GRADED, check, "live", ms)
1550 };
1551 let log = [
1552 graded("c1", 10),
1553 Outcome { route: outcome::ROUTE_FREE.into(), ..rec(outcome::DELIVERY, "c1", "", 11) },
1554 Outcome { judge: 2, ..rec(outcome::CHANGED, "c1", "queued", 12) },
1555 graded("c2", 20),
1556 Outcome { route: outcome::ROUTE_DROPPED_DONE.into(), ..rec(outcome::DELIVERY, "c2", "", 21) },
1557 Outcome { lessons: vec!["B".into()], ..rec(outcome::ASKED, "", "early", 25) },
1559 Outcome { lessons: vec!["B".into()], grades: vec![3], chosen: vec!["B".into()], free: Vec::new(), p: vec![1.0], pf: vec![0.0], ..graded("c3", 30) },
1560 Outcome { lessons: vec!["B".into()], route: outcome::ROUTE_ASKED.into(), ..rec(outcome::DELIVERY, "c3", "", 31) },
1561 Outcome { lessons: vec!["B".into()], judge: 2, ..rec(outcome::CHANGED, "c3", "live", 32) },
1562 ];
1563 let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1564 let s = fold(&text, Summary::default());
1565 assert_eq!(s.applies.iter().map(|a| a.had).collect::<Vec<_>>(), [false, true, true], "c2's A was handed over at c1, c3's B early");
1566 let sel = Selection::of(&s, select::Policy::Fixed, None);
1567 let row = |name: &str| sel.labels.iter().find(|c| c.name == name).map(|c| (c.part, c.whole));
1568 assert_eq!(row("judged, free"), Some((1, 1)), "c1's delivered and judged; c2's had, out: {:?}", sel.labels);
1569 assert_eq!(row("judged, asked"), Some((1, 1)), "c3's question of what B's session had, judged: {:?}", sel.labels);
1570 }
1571
1572 #[test]
1576 fn the_verdict_shares_read_judge_three_answers_per_route() {
1577 use crate::outcome::{self, Outcome};
1578 use select::summary::{Summary, fold};
1579 let rec = |kind: &str, l: &str, check: &str, channel: &str, ms: i64, judge: u8| Outcome {
1580 kind: kind.into(),
1581 session: "s".into(),
1582 lessons: vec![l.into()],
1583 check: check.into(),
1584 channel: channel.into(),
1585 ms,
1586 judge,
1587 ..Default::default()
1588 };
1589 let graded = |check: &str, ms: i64, chosen: &[&str], free: &[&str], placebo: &[&str]| {
1590 let lessons: Vec<String> = chosen.iter().chain(free).chain(placebo).map(|l| l.to_string()).collect();
1591 let n = lessons.len();
1592 Outcome {
1593 grades: vec![2; n],
1594 ranks: (0..n as u16).collect(),
1595 applied: n as u16,
1596 chosen: chosen.iter().map(|l| l.to_string()).collect(),
1597 free: free.iter().map(|l| l.to_string()).collect(),
1598 placebo: placebo.iter().map(|l| l.to_string()).collect(),
1599 p: lessons.iter().map(|l| f64::from(u8::from(chosen.contains(&l.as_str())))).collect(),
1600 pf: lessons.iter().map(|l| f64::from(u8::from(free.contains(&l.as_str())))).collect(),
1601 psi: if placebo.is_empty() { 0.0 } else { 0.5 },
1602 policy: "fixed".into(),
1603 lessons,
1604 ..rec(outcome::GRADED, "", check, "stop", ms, 0)
1605 }
1606 };
1607 let deliver = |l: &str, check: &str, route: &str, ms: i64| Outcome { route: route.into(), ..rec(outcome::DELIVERY, l, check, "", ms, 0) };
1608 let log = [
1609 graded("c0", 5, &[], &["O"], &[]),
1611 deliver("O", "c0", outcome::ROUTE_FREE, 6),
1612 rec(outcome::UNCLEAR, "O", "c0", "queued", 7, 2),
1613 graded("c1", 10, &["A", "B"], &["F", "G", "H"], &["P", "Q"]),
1614 deliver("A", "c1", outcome::ROUTE_ASKED, 11),
1615 deliver("B", "c1", outcome::ROUTE_ASKED, 11),
1616 deliver("F", "c1", outcome::ROUTE_FREE, 11),
1617 deliver("G", "c1", outcome::ROUTE_FREE, 11),
1618 deliver("H", "c1", outcome::ROUTE_FREE, 11),
1619 rec(outcome::CHANGED, "A", "c1", "stop", 20, 3),
1620 rec(outcome::UNCLEAR, "B", "c1", "stop", 20, 3),
1621 rec(outcome::UNCLEAR, "F", "c1", "queued", 20, 3),
1622 Outcome { at: outcome::AT_END.into(), ..rec(outcome::NOT_ARISEN, "F", "c1", "queued", 30, 3) },
1623 rec(outcome::REMAINS, "G", "c1", "queued", 20, 3),
1624 rec(outcome::UNCLEAR, "H", "c1", "queued", 20, 3),
1625 rec(outcome::UNCLEAR, "P", "c1", outcome::PLACEBO, 20, 3),
1626 rec(outcome::IGNORED, "Q", "c1", outcome::PLACEBO, 20, 3),
1627 ];
1628 let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1629 let sel = Selection::of(&fold(&text, Summary::default()), select::Policy::Fixed, None);
1630 let row = |name: &str| sel.labels.iter().find(|c| c.name == name).map(|c| (c.part, c.whole));
1631 assert_eq!(row("unclear"), Some((3, 7)), "B, H and P of seven judge-3 answers, O's older one out: {:?}", sel.labels);
1632 assert_eq!(row("not arisen"), Some((1, 7)));
1633 assert_eq!(row("consequence remains"), Some((1, 7)));
1634 assert_eq!(row("unclear, asked"), Some((1, 2)));
1635 assert_eq!(row("unclear, free"), Some((1, 3)), "F's end verdict replaced its unclear: {:?}", sel.labels);
1636 assert_eq!(row("unclear, placebo"), Some((1, 2)));
1637 }
1638
1639 #[test]
1644 fn the_measurement_counts_the_placebos_the_fixed_rule_drew_and_judged() {
1645 use crate::outcome::{self, Outcome};
1646 use select::summary::{Summary, fold};
1647 let rec = |kind: &str, lessons: &[&str], check: &str, channel: &str, ms: i64| Outcome {
1648 kind: kind.into(),
1649 session: "s".into(),
1650 lessons: lessons.iter().map(|l| l.to_string()).collect(),
1651 check: check.into(),
1652 channel: channel.into(),
1653 ms,
1654 ..Default::default()
1655 };
1656 let graded = Outcome {
1657 grades: vec![1, 1, 2],
1658 ranks: vec![0, 1, 2],
1659 applied: 3,
1660 free: vec!["C".into()],
1661 placebo: vec!["A".into(), "B".into()],
1662 psi: 0.5,
1663 policy: "fixed".into(),
1664 shown: 20,
1665 tokens: 6_000,
1666 ..rec(outcome::GRADED, &["A", "B", "C"], "c1", "stop", 10)
1667 };
1668 let tailed = Outcome {
1670 grades: vec![1],
1671 ranks: vec![0],
1672 applied: 1,
1673 policy: "fixed".into(),
1674 shown: 20,
1675 tokens: 6_000,
1676 tail: vec!["T1".into(), "T2".into()],
1677 pt: 0.5,
1678 fill: vec![21, 25, 21],
1679 ..rec(outcome::GRADED, &["T2"], "c2", "stop", 14)
1680 };
1681 let log = [
1682 graded,
1683 tailed,
1684 Outcome { route: outcome::ROUTE_PLACEBO.into(), ..rec(outcome::DELIVERY, &["A"], "c1", "", 11) },
1685 Outcome { route: outcome::ROUTE_PLACEBO.into(), ..rec(outcome::DELIVERY, &["B"], "c1", "", 11) },
1686 Outcome { judge: 2, ..rec(outcome::IGNORED, &["A"], "c1", outcome::PLACEBO, 12) },
1687 Outcome { judge: 2, ..rec(outcome::CHANGED, &["C"], "c1", "queued", 13) },
1688 ];
1689 let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1690 let sel = Selection::of(&fold(&text, Summary::default()), select::Policy::Fixed, None);
1691 assert_eq!(sel.measurement, Some(Measurement { placebos: 2, judged: 1, tail_shown: 2, tail_applied: 1 }), "{sel:?}");
1692 assert!(selection_line(&sel).contains(" · 2 checks · placebos 2 drawn, 1 judged · tail 2 shown past the pool, 1 applied"), "{}", selection_line(&sel));
1693 assert_eq!(Selection::of(&Summary::default(), select::Policy::Fixed, None).measurement, None, "nothing folded: not measured");
1694 }
1695
1696 #[test]
1700 fn the_selection_counts_the_learned_decisions_the_fixed_rule_took_over_by_reason() {
1701 use crate::outcome::{self, Outcome};
1702 use select::summary::{Summary, fold};
1703 let graded = |check: &str, policy: &str, reason: &str, ms: i64| Outcome {
1704 kind: outcome::GRADED.into(),
1705 session: "s".into(),
1706 lessons: vec!["A".into()],
1707 grades: vec![1],
1708 ranks: vec![0],
1709 applied: 1,
1710 free: vec!["A".into()],
1711 pf: vec![1.0],
1712 channel: "live".into(),
1713 check: check.into(),
1714 policy: policy.into(),
1715 reason: reason.into(),
1716 ms,
1717 ..Default::default()
1718 };
1719 let log = [
1720 graded("c1", "learned", "", 1),
1721 graded("c2", "fixed", outcome::DECIDED_BUSY, 2),
1722 graded("c3", "fixed", outcome::DECIDED_NO_RECORD, 3),
1723 graded("c4", "fixed", outcome::DECIDED_BUSY, 4),
1724 ];
1725 let lines = |log: &[Outcome]| log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect::<String>();
1726 let sel = Selection::of(&fold(&lines(&log), Summary::default()), select::Policy::Learned, None);
1727 let rows: Vec<(&str, u64, u64)> = sel.fallbacks.iter().map(|c| (c.name.as_str(), c.part, c.whole)).collect();
1728 assert_eq!(rows, [("lock_busy", 2, 4), ("lock_failed", 0, 4), ("unrecorded", 1, 4)]);
1729 let line = selection_line(&sel);
1730 assert!(line.contains(" · 4 checks · learned decisions the fixed rule took over: lock_busy 2, unrecorded 1 · "), "{line}");
1731 let none = Selection::of(&fold(&lines(&log[..1]), Summary::default()), select::Policy::Learned, None);
1732 assert!(none.fallbacks.iter().all(|c| c.part == 0) && none.fallbacks.len() == 3, "{none:?}");
1733 assert!(!selection_line(&none).contains("took over"), "{}", selection_line(&none));
1734 assert!(Selection::of(&Summary::default(), select::Policy::Learned, None).fallbacks.is_empty(), "nothing folded: not measured");
1735 }
1736
1737 #[test]
1742 fn the_selection_fills_latency_evaluation_and_delivery_from_the_summary() {
1743 use select::summary::{Summary, fold};
1744 let call = |harness: &str, channel: &str, dur_ms: u64, failed: bool| {
1745 let o = crate::outcome::Outcome {
1746 kind: "graded".into(),
1747 session: "s".into(),
1748 channel: channel.into(),
1749 harness: harness.into(),
1750 dur_ms,
1751 failed,
1752 tokens: 6_000,
1753 ..Default::default()
1754 };
1755 format!("{}\n", serde_json::to_string(&o).unwrap())
1756 };
1757 let mut log: String = (1..=20).map(|k| call("codex", "stop", k * 1_000, k == 20)).collect();
1758 log += &call("claude", "stop", 4_000, false);
1759 log += &call("codex", "live", 99_000, false);
1760 log += &call("nonesuch", "stop", 5_000, false);
1761 let mut s = fold(&log, Summary::default());
1762 s.monitor = select::policy::Monitor {
1763 sessions: 12,
1764 units: 400,
1765 mean: 0.004,
1766 ucb: Some(0.031),
1767 labelled: 300,
1768 ess: 180.0,
1769 max_weight: 6.5,
1770 truncated: 3,
1771 ..Default::default()
1772 };
1773 s.fit.key = "fit".into();
1774 s.fit.rates = select::policy::Rates { free_stop: 0.6, free_live: 0.8, later: 0.4, wait: 0.25, ask_live: 1.0 };
1775 let sel = Selection::of(&s, select::Policy::Learned, None);
1776 let budget = |h: &str| crate::harness::ALL.iter().find(|x| x.name() == h).and_then(|x| x.caps().stop_budget).map(|b| b.as_millis() as u64);
1777 assert!(budget("codex").is_some() && budget("claude").is_none(), "codex names a stop budget, claude none");
1778 let want = vec![
1779 Latency { harness: "claude".into(), checks: 1, p50_ms: 4_000, p95_ms: 4_000, budget_ms: None, over: 0 },
1780 Latency { harness: "codex".into(), checks: 20, p50_ms: 10_000, p95_ms: 19_000, budget_ms: budget("codex"), over: 1 },
1781 ];
1782 assert_eq!(sel.latency, want);
1783 let e = sel.evaluation.as_ref().expect("the monitor weighed labelled applies");
1784 assert_eq!(*e, Evaluation { sessions: 12, mean: 0.004, ucb: Some(0.031), units: 300, ess: 180.0, max_weight: 6.5, truncated: 3 });
1785 assert_eq!(sel.delivery.as_ref().map(|r| r.wait), Some(0.25));
1786 let keep = select::summary::LATENCY_KEEP as u64;
1788 let many: String = (1..=keep + 1).map(|k| call("claude", "stop", k, false)).collect();
1789 let kept = Selection::of(&fold(&many, Summary::default()), select::Policy::Learned, None);
1790 assert_eq!((kept.latency[0].checks, kept.latency[0].p50_ms), (keep, 1 + keep / 2), "the oldest call dropped");
1791 let cold = Selection::of(&Summary::default(), select::Policy::Learned, None);
1792 assert!(cold.latency.is_empty() && cold.evaluation.is_none() && cold.delivery.is_none(), "{cold:?}");
1793 assert_eq!((quantile(&[], 0.95), quantile(&[7], 0.5)), (0, 7));
1794 }
1795
1796 fn tested(check: &str, session: &str, ms: i64, source: &str, xk: Option<u32>, xs: Option<u32>) -> crate::outcome::Outcome {
1799 crate::outcome::Outcome {
1800 kind: crate::outcome::GRADED.into(),
1801 session: session.into(),
1802 lessons: vec!["A".into(), "B".into()],
1803 grades: vec![2, 1],
1804 ranks: vec![0, 1],
1805 applied: 2,
1806 check: check.into(),
1807 channel: "live".into(),
1808 policy: "learned".into(),
1809 source: source.into(),
1810 xk,
1811 xs,
1812 ms,
1813 ..Default::default()
1814 }
1815 }
1816
1817 #[test]
1823 fn the_test_section_is_the_summarys_fold_of_its_checks() {
1824 use select::policy::{ALPHA, EVIDENCE, EXPERIMENT, EXPERIMENT_CONTROL, GATE};
1825 use select::summary::{Summary, fold};
1826 let log = [
1827 tested("c1", "s1", 1, EXPERIMENT, Some(2), Some(1)),
1828 tested("c2", "s1", 2, EXPERIMENT_CONTROL, Some(2), Some(0)),
1829 tested("c3", "s2", 3, EXPERIMENT, Some(2), Some(1)),
1830 tested("c4", "s2", 4, GATE, Some(2), Some(1)),
1831 tested("c5", "s2", 5, GATE, Some(1), Some(0)),
1832 tested("c6", "s3", 6, EVIDENCE, None, None),
1833 tested("c7", "s3", 7, GATE, None, None),
1834 crate::outcome::Outcome { policy: "fixed".into(), ..tested("c8", "s3", 8, EXPERIMENT, Some(2), Some(2)) },
1835 ];
1836 let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1837 let s = fold(&text, Summary::default());
1838 let t = Selection::of(&s, select::Policy::Learned, Some(0.2)).test.expect("the learned policy shows its test");
1839 let draws = |k: u32, draws: Vec<u64>, refused: u64| Draws { k, draws, refused };
1840 assert_eq!((t.ran, t.control, t.refused, t.sessions), (5, 1, 2, 2), "{t:?}");
1841 assert_eq!(t.by_k, vec![draws(1, vec![1, 0], 1), draws(2, vec![1, 3, 0], 1)]);
1842 assert_eq!((t.risk, t.set), (0.2, true), "the collective's risk");
1843 assert!(!t.due && t.dilution.is_empty() && t.ask_free.is_none(), "5 checks, 2 sessions: not due, nothing estimated");
1844 assert_eq!((t.due_checks, t.due_sessions), (select::replay::TEST_CHECKS, u64::from(select::replay::TEST_SESSIONS)));
1845 let line = selection_line(&Selection::of(&s, select::Policy::Learned, Some(0.2)));
1846 assert!(
1847 line.contains(" · question-count test 5 drawn (1 control, 2 refused by the gate) in 2 sessions, risk 0.20 (the collective's); readout after 300 checks in 15 sessions, or 30 sessions · "),
1848 "{line}"
1849 );
1850 assert_eq!(t.kept, t.ran, "every drawn check still held: {t:?}");
1851 let mut compacted = s.clone();
1853 compacted.checks.remove(0);
1854 let c = Selection::of(&compacted, select::Policy::Learned, Some(0.2)).test.unwrap();
1855 assert_eq!((c.ran, c.kept), (5, 4), "{c:?}");
1856 assert!(
1857 test_line(&c).starts_with("question-count test 5 drawn (1 control, 2 refused by the gate, 4 still in the summary) in 2 sessions"),
1858 "{}",
1859 test_line(&c)
1860 );
1861 let default = Selection::of(&s, select::Policy::Learned, None).test.unwrap();
1862 assert_eq!((default.risk, default.set), (ALPHA, false), "unset: the policy's ALPHA");
1863 let cold = Selection::of(&Summary::default(), select::Policy::Learned, Some(0.0));
1865 assert_eq!(cold.test.as_ref().map(|t| (t.ran, t.risk)), Some((0, 0.0)));
1866 assert_eq!(test_line(cold.test.as_ref().unwrap()), "question-count test off (risk 0)");
1867 let none = Selection::of(&Summary::default(), select::Policy::Learned, None).test.unwrap();
1868 assert_eq!(test_line(&none), "question-count test: no check yet, risk 0.10");
1869 assert!(Selection::of(&Summary::default(), select::Policy::Fixed, None).test.is_none());
1871 assert_eq!(Selection::of(&s, select::Policy::Fixed, None).test.map(|t| t.ran), Some(5), "an earlier opt-in's checks");
1872 }
1873
1874 #[test]
1878 fn the_gate_shown_is_the_one_in_force_at_the_collectives_risk() {
1879 use select::summary::{Gate, Summary};
1880 let s = Summary { gate: Gate { gain: vec![0.0; select::policy::DRAWS], allowance: 0.3, ..Gate::default() }, ..Summary::default() };
1881 let gate = |risk: Option<f64>| Selection::of(&s, select::Policy::Learned, risk).gate;
1882 assert!((gate(None) - 0.3).abs() < 1e-12, "{}", gate(None));
1883 assert!((gate(Some(0.2)) - 0.6).abs() < 1e-12, "twice ALPHA: twice the allowance, {}", gate(Some(0.2)));
1884 assert_eq!(gate(Some(0.0)), 0.0, "risk 0");
1885 }
1886
1887 #[test]
1890 fn the_test_readout_shows_the_replays_estimates_once_due() {
1891 use crate::outcome::{self, Outcome};
1892 use select::policy::EXPERIMENT;
1893 use select::summary::{Summary, fold};
1894 let sessions = u64::from(select::replay::TEST_SESSIONS);
1895 let mut text = String::new();
1896 let mut line = |o: &Outcome| text.push_str(&format!("{}\n", serde_json::to_string(o).unwrap()));
1897 for i in 0..sessions {
1898 let (session, check, ms) = (format!("s{i}"), format!("{i:012x}"), 10 * i as i64);
1899 let asked = i % 2 == 0;
1901 let g = Outcome {
1902 lessons: vec!["A".into()],
1903 grades: vec![2],
1904 ranks: vec![0],
1905 applied: 1,
1906 chosen: if asked { vec!["A".into()] } else { Vec::new() },
1907 free: if asked { Vec::new() } else { vec!["A".into()] },
1908 p: vec![0.5],
1909 pf: vec![0.5],
1910 ..tested(&check, &session, ms, EXPERIMENT, Some(1), Some(u32::from(asked)))
1911 };
1912 line(&g);
1913 let rec = |kind: &str, ms: i64| Outcome {
1914 kind: kind.into(),
1915 session: session.clone(),
1916 lessons: vec!["A".into()],
1917 check: check.clone(),
1918 ms,
1919 ..Default::default()
1920 };
1921 let route = if asked { outcome::ROUTE_ASKED } else { outcome::ROUTE_FREE };
1922 line(&Outcome { route: route.into(), ..rec(outcome::DELIVERY, ms + 1) });
1923 let verdict = if asked { outcome::CHANGED } else { outcome::IGNORED };
1925 line(&Outcome { judge: 2, channel: if asked { "live" } else { "queued" }.into(), ..rec(verdict, ms + 2) });
1926 }
1927 let s = fold(&text, Summary::default());
1928 let t = Selection::of(&s, select::Policy::Learned, None).test.unwrap();
1929 assert!(t.due && t.sessions == sessions, "{t:?}");
1930 let r = select::replay::test_readout(&s);
1931 let want = r.ask_free.as_ref().and_then(|x| x.value).expect("judged 2s on both routes");
1932 let e = t.ask_free.as_ref().expect("estimated once due");
1933 assert_eq!((e.mean, e.unit.as_str()), (100.0 * want, "points changed"));
1934 assert!(e.mean > 0.0, "asking changed the work, handing over did not: {e:?}");
1935 assert!(t.dilution.is_empty() && r.by_asked.is_empty(), "{t:?}");
1937 let line = selection_line(&Selection::of(&s, select::Policy::Learned, None));
1938 assert!(line.contains(&format!("; readout: grade 2 asked − handed over {:+.1} [", e.mean)), "{line}");
1939 }
1940
1941 #[test]
1944 fn unread_counts_follow_the_shown_once_claims() {
1945 let home = std::env::temp_dir().join(format!("borg-top-inbox-{}", std::process::id()));
1946 let _ = std::fs::remove_dir_all(&home);
1947 std::fs::create_dir_all(&home).unwrap();
1948 let ds = fixture();
1949 assert!(inbox(&home, &ds, NOW).recent.is_empty());
1950 let here = msg::node(&home);
1951 msg::send(&home, message_on(&here, "cccc3333-x", "Three of Three", "aaaa1111-x", msg::TELL, NOW - 3_000, "look at recon"), NOW).unwrap();
1952 msg::send(&home, message_on(&here, "cccc3333-x", "Three of Three", msg::ALL, msg::TELL, NOW - 2_000, "lunch"), NOW).unwrap();
1953 let i = inbox(&home, &ds, NOW);
1954 assert_eq!(i.unread.get("aaaa1111-x"), Some(&2), "{i:?}");
1955 assert_eq!(i.unread.get("cccc3333-x"), Some(&0), "its own broadcast");
1956 assert!(!i.unread.contains_key(&ds[1].p.session), "remote: claimed on its node");
1957 assert_eq!((i.day, i.recent[0].text.as_str()), (2, "lunch"));
1958 let caps = crate::harness::by_name("claude").expect("a harness").caps();
1959 let ev = json!({"session_id": "aaaa1111-x"});
1960 assert!(msg::deliver(&home, caps, "prompt", &ev, Some(100), NOW, None).is_some());
1961 assert_eq!(inbox(&home, &ds, NOW).unread.get("aaaa1111-x"), Some(&0));
1962 let _ = std::fs::remove_dir_all(&home);
1963 }
1964
1965 fn seen_lines(home: &Path) -> Vec<(String, Vec<String>)> {
1967 msg::lines(home)
1968 .into_iter()
1969 .filter_map(|l| match l {
1970 msg::Line::Seen(s) => Some((s.by, s.ids)),
1971 msg::Line::Message(_) => None,
1972 })
1973 .collect()
1974 }
1975
1976 fn shell(sessions: Vec<(String, &'static str)>, pids: Vec<(u32, &'static str)>) -> impl Fn(&[Drone]) -> Result<Option<(String, &'static str)>, String> {
1979 move |ds| msg::pick(sessions.clone(), &pids, ds)
1980 }
1981
1982 fn acked_fixture(base: &Path) -> (std::path::PathBuf, String, Message) {
1985 let _ = std::fs::remove_dir_all(base);
1986 let (home, work) = (base.join("store"), base.join("work"));
1987 std::fs::create_dir_all(&home).unwrap();
1988 std::fs::create_dir_all(&work).unwrap();
1989 let git = |a: &[&str]| std::process::Command::new("git").arg("-C").arg(&work).args(a).output().is_ok_and(|o| o.status.success());
1990 assert!(git(&["init", "-q"]) && git(&["config", "user.email", "ann@x"]));
1991 let here = msg::node(&home);
1992 let to_me = message_on(&here, "aaaa1111-x", "One of Three", msg::ENGINEER, msg::TELL, NOW - 3_000, "the build is green");
1993 let to_drone = message_on(&here, "cccc3333-x", "Three of Three", "aaaa1111-x", msg::TELL, NOW - 2_000, "look at recon");
1994 msg::send(&home, to_me.clone(), NOW).unwrap();
1995 msg::send(&home, to_drone, NOW).unwrap();
1996 (home, work.display().to_string(), to_me)
1997 }
1998
1999 #[test]
2003 fn the_screen_acknowledges_the_messages_to_the_engineer_it_prints() {
2004 let base = std::env::temp_dir().join(format!("borg-top-ack-{}", std::process::id()));
2005 let (home, cwd, to_me) = acked_fixture(&base);
2006 let routes = [Route::Collective { name: "corp".into(), home: home.clone(), share: Share::Local }];
2007 let terminal = || shell(Vec::new(), Vec::new());
2008 let json = screen_of(&routes, true, &cwd, terminal(), true, NOW);
2009 assert!(json.contains("the build is green") && seen_lines(&home).is_empty(), "--json acknowledges nothing: {json}");
2010 let text = screen_of(&routes, true, &cwd, terminal(), false, NOW);
2011 assert!(text.contains("the build is green") && text.contains("look at recon"), "{text}");
2012 assert_eq!(seen_lines(&home), [("engineer:ann@x".to_string(), vec![to_me.id.clone()])], "the engineer's message only, as theirs");
2013 screen_of(&routes, true, &cwd, terminal(), false, NOW + 1_000);
2014 assert_eq!(seen_lines(&home).len(), 1, "acknowledged once: a later look writes no second line");
2015 let _ = std::fs::remove_dir_all(&base);
2016 }
2017
2018 #[test]
2023 fn a_session_running_the_screen_acknowledges_nothing_as_the_engineer() {
2024 let base = std::env::temp_dir().join(format!("borg-top-ack-session-{}", std::process::id()));
2025 let (home, cwd, to_me) = acked_fixture(&base);
2026 let other = base.join("other");
2027 std::fs::create_dir_all(&other).unwrap();
2028 let pid = std::process::id();
2029 let oc = Presence { harness: "opencode".into(), pid: Some(pid), ..p("dddd4444-x", "ann@x", NOW - 9_000, NOW - 1_000, "prompt", "docs", &[]) };
2030 presence::write(&other, &oc).unwrap();
2031 let routes = [
2032 Route::Collective { name: "corp".into(), home: home.clone(), share: Share::Local },
2033 Route::Collective { name: "side".into(), home: other.clone(), share: Share::Local },
2034 ];
2035 let shells = [
2036 ("Claude Code", shell(vec![("eeee5555-x".into(), "claude")], Vec::new())),
2037 ("Codex in Claude Code, neither with presence", shell(vec![("eeee5555-x".into(), "claude"), ("ffff6666-x".into(), "codex")], Vec::new())),
2038 ("opencode, its drone in the other collective", shell(Vec::new(), vec![(pid, "opencode")])),
2039 ];
2040 for (what, caller) in shells {
2041 let text = screen_of(&routes, false, &cwd, caller, false, NOW);
2042 assert!(text.contains("the build is green"), "{what}: shown {text}");
2043 assert!(seen_lines(&home).is_empty() && seen_lines(&other).is_empty(), "{what}: acknowledged nothing as the engineer");
2044 }
2045 screen_of(&routes, false, &cwd, shell(Vec::new(), vec![(pid + 1, "opencode")]), false, NOW);
2046 assert_eq!(seen_lines(&home), [("engineer:ann@x".to_string(), vec![to_me.id.clone()])], "a terminal (no drone of its pid): the engineer's");
2047 let _ = std::fs::remove_dir_all(&base);
2048 }
2049
2050 #[test]
2055 fn the_printed_screen_acknowledges_only_the_messages_whose_text_it_prints() {
2056 let base = std::env::temp_dir().join(format!("borg-top-ack-cut-{}", std::process::id()));
2057 let (home, cwd, to_me) = acked_fixture(&base);
2058 let email = format!("{}@x", "a".repeat(58));
2060 assert!(std::process::Command::new("git").args(["-C", &cwd, "config", "user.email", &email]).output().is_ok_and(|o| o.status.success()));
2061 let (here, to) = (msg::node(&home), format!("{}:{email}", msg::ENGINEER));
2062 let one = message_on(&here, "bbbb2222-x", &"n".repeat(52), &to, msg::TELL, NOW - 1_500, "one character shows");
2063 let none = message_on(&here, "dddd4444-x", &"n".repeat(53), &to, msg::TELL, NOW - 1_000, "none shows");
2064 msg::send(&home, one.clone(), NOW).unwrap();
2065 msg::send(&home, none.clone(), NOW).unwrap();
2066 let routes = [Route::Collective { name: "corp".into(), home: home.clone(), share: Share::Local }];
2067 let text = screen_of(&routes, true, &cwd, shell(Vec::new(), Vec::new()), false, NOW);
2068 assert!(text.contains(&format!("{} -> {email}: o…", one.name)) && text.contains(&format!("{} -> {email}: …", none.name)), "{text}");
2069 let mut acked: Vec<String> = seen_lines(&home).into_iter().flat_map(|(by, ids)| ids.into_iter().map(move |id| format!("{by} {id}"))).collect();
2070 acked.sort();
2071 let mut want = [format!("{to} {}", to_me.id), format!("{to} {}", one.id)];
2072 want.sort();
2073 assert_eq!(acked, want, "the line that shows one character of its text is read; the one cut before it is not");
2074 let _ = std::fs::remove_dir_all(&base);
2075 }
2076
2077 #[test]
2081 fn a_live_screen_acknowledges_each_message_once() {
2082 let base = std::env::temp_dir().join(format!("borg-top-ack-once-{}", std::process::id()));
2083 let _ = std::fs::remove_dir_all(&base);
2084 std::fs::create_dir_all(&base).unwrap();
2085 let home = base.clone();
2086 let here = msg::node(&home);
2087 let to_me = |by: &str, ms: i64, text: &str| message_on(&here, by, "a drone", msg::ENGINEER, msg::TELL, ms, text);
2088 let first = to_me("aaaa1111-x", NOW - 3_000, "the build is green");
2089 msg::send(&home, first.clone(), NOW).unwrap();
2090 let cwd = base.display().to_string();
2091 let mut done = HashSet::new();
2092 let recent = |now: i64| inbox(&home, &[], now).recent;
2093 acknowledge(&home, &cwd, &recent(NOW).iter().collect::<Vec<_>>(), &mut done, NOW);
2094 assert_eq!(seen_lines(&home).len(), 1, "{:?}", seen_lines(&home));
2095 assert!(done.contains(&first.id));
2096 let kept: String = std::fs::read_to_string(msg::path(&home)).unwrap().lines().filter(|l| !l.contains("\"seen\"")).map(|l| format!("{l}\n")).collect();
2099 std::fs::write(msg::path(&home), kept).unwrap();
2100 acknowledge(&home, &cwd, &recent(NOW + 1_000).iter().collect::<Vec<_>>(), &mut done, NOW + 1_000);
2101 assert!(seen_lines(&home).is_empty(), "skipped: no second ack of a done id");
2102 let second = to_me("cccc3333-x", NOW + 2_000, "and the docs");
2104 msg::send(&home, second.clone(), NOW + 2_000).unwrap();
2105 acknowledge(&home, &cwd, &recent(NOW + 3_000).iter().collect::<Vec<_>>(), &mut done, NOW + 3_000);
2106 assert_eq!(seen_lines(&home).into_iter().map(|(_, ids)| ids).collect::<Vec<_>>(), [vec![second.id.clone()]], "only the new one");
2107 let _ = std::fs::remove_dir_all(&base);
2108 }
2109
2110 #[test]
2113 fn a_sender_is_tied_to_the_node_its_line_names() {
2114 let ds = fixture();
2115 let own = message("aaaa1111-x", "anything", msg::ALL, msg::TELL, NOW, "hi");
2116 assert_eq!(sender(&ds, "n1", &own), "One of Three", "this node's drone");
2117 let forged = message_on("evil0123456789", "aaaa1111-x", "One of Three", msg::ALL, msg::TELL, NOW, "hi");
2118 assert_eq!(sender(&ds, "n1", &forged), "One of Three @evil01234567", "the claim, with the node it came from");
2119 let remote = message_on(&"beefcafe".repeat(8), &ds[1].p.session, "x", msg::ALL, msg::TELL, NOW, "hi");
2120 assert_eq!(sender(&ds, "n1", &remote), "Two of Three @beefcafebeef", "the remote drone on its own node");
2121 assert_eq!(recipient(&ds, "n1", "aaaa1111-x@n1"), "One of Three");
2122 assert_eq!(recipient(&ds, "n1", "aaaa1111-x@evil"), "aaaa1111", "not the local drone of that id");
2123 assert_eq!((recipient(&ds, "n1", "engineer"), recipient(&ds, "n1", msg::ALL)), ("engineer".into(), "collective".into()), "an `all` line");
2124 assert_eq!(recipient(&ds, "n1", "local@n1"), "local");
2125 assert_eq!(recipient(&ds, "n1", "local@beefcafebeefcafe00"), "local @beefcafebeef");
2126 }
2127
2128 #[test]
2132 fn the_line_on_borg_itself_names_a_release_and_the_protocols_that_differ() {
2133 let home = std::env::temp_dir().join(format!("borg-top-version-{}", std::process::id()));
2134 let _ = std::fs::remove_dir_all(&home);
2135 std::fs::create_dir_all(&home).unwrap();
2136 let newer = update::Checked { checked_ms: 1, latest: Some("99.0.0".into()), error: None };
2137 let current = update::Checked { latest: Some(update::VERSION.into()), ..newer.clone() };
2138 assert_eq!(version_line([home.as_path()], None, true), None);
2139 assert_eq!(version_line([home.as_path()], Some(¤t), true), None);
2140 let release = format!("borg 99.0.0 is released, this is {}: borg update", update::VERSION);
2141 assert_eq!(version_line([home.as_path()], Some(&newer), true), Some(release.clone()));
2142 assert_eq!(version_line([home.as_path()], Some(&newer), false), None, "BORG_UPDATE_CHECK=off");
2143
2144 let ours = protocol::current();
2145 let v = |protocol, engineer: Option<&str>| protocol::PeerVersion { borg: None, protocol, ms: 1, engineer: engineer.map(str::to_string) };
2146 protocol::note_version(&home, "k-same", &v(ours, Some("ann@x"))).unwrap();
2147 assert_eq!(version_line([home.as_path()], None, true), None, "a member on this node's protocol is no news");
2148 protocol::note_version(&home, "k-old", &v(ours - 1, Some("bob@x"))).unwrap();
2149 let mismatch = protocol::Mismatch { theirs: format!("borg/mesh/{}", ours + 1), ours: format!("borg/mesh/{ours}"), ms: 2 };
2150 protocol::note_mismatch(&home, "k-new", mismatch).unwrap();
2151 let line = version_line([home.as_path()], Some(&newer), true).unwrap();
2152 let old = format!("bob@x on /{} misses /{ours}: {}", ours - 1, protocol::note(ours).unwrap());
2153 let none = format!("nothing syncs with peer k-new (borg/mesh/{}, this node borg/mesh/{ours})", ours + 1);
2154 assert_eq!(line, format!("{release} · {none} · {old}"));
2155 let _ = std::fs::remove_dir_all(&home);
2156 }
2157
2158 #[test]
2159 fn json_for_scripts() {
2160 let v = to_json_with("corp", Share::Team, &fixture(), &inbox_fixture());
2161 assert_eq!((v["drones"][0]["unread"].as_u64(), v["drones"][1].get("unread")), (Some(2), None));
2162 assert_eq!((v["messages"]["day"].as_u64(), v["messages"]["recent"][0]["kind"].as_str()), (Some(3), Some("ask")));
2163 assert_eq!(v["collective"], "corp");
2164 let d = &v["drones"];
2165 assert_eq!(d.as_array().unwrap().len(), 3);
2166 assert_eq!(d[0]["designation"], "One of Three");
2167 assert_eq!(d[0]["session"], "aaaa1111-x");
2168 assert_eq!(d[0]["state"], "working");
2169 assert_eq!(d[0]["paused"], true);
2170 assert_eq!(d[1]["node"], "beefcafe".repeat(8));
2171 assert_eq!(d[1]["state"], "waiting");
2172 assert_eq!(d[2]["state"], "idle");
2173 assert_eq!(d[2]["files"], json!([]));
2174 }
2175
2176 #[test]
2178 fn a_name_matching_here_and_on_a_member_names_the_drone_here() {
2179 let local = vec![p("aaaa1111-x", "alice@x", 100, NOW - 5_000, "post_tool", "a", &[])];
2180 let remote = ["aaaa1111-x", "aaaa2222-z"].map(|s| presence::Remote {
2181 p: p(s, "bob@y", 50, NOW - 5_000, "post_tool", "b", &[]),
2182 key: "beefcafe".repeat(8),
2183 sig: String::new(),
2184 });
2185 let ds = assemble(local, remote.into(), NOW, |_| true, |_| false);
2186 for t in ["aaaa", "aaaa1111", "aaaa1111-x"] {
2187 let d = resolve(&ds, t).unwrap().unwrap();
2188 assert!(d.node.is_empty() && d.p.session == "aaaa1111-x", "{t}");
2189 }
2190 assert_eq!(resolve(&ds, "aaaa2").unwrap().unwrap().node, "beefcafe".repeat(8), "only a member's matches");
2191 assert!(!resolve(&ds, "One").unwrap().unwrap().node.is_empty(), "a number names one drone of all, wherever it runs");
2192 }
2193
2194 #[test]
2196 fn a_name_matching_an_ended_drone_here_and_a_live_member_names_the_live_one() {
2197 let local = vec![p("aaaa1111-x", "alice@x", 100, NOW - 5_000, "end", "a", &[])];
2198 let remote =
2199 vec![presence::Remote { p: p("aaaa2222-z", "bob@y", 50, NOW - 5_000, "post_tool", "b", &[]), key: "beefcafe".repeat(8), sig: String::new() }];
2200 let ds = assemble(local, remote, NOW, |_| true, |_| false);
2201 let d = resolve(&ds, "aaaa").unwrap().unwrap();
2202 assert_eq!((d.p.session.as_str(), d.node.as_str()), ("aaaa2222-z", "beefcafe".repeat(8).as_str()), "the live one, though remote");
2203 assert!(resolve(&ds, "aaaa1111").unwrap().unwrap().node.is_empty(), "an ended drone is still named by its own id");
2204 }
2205
2206 #[test]
2207 fn resolves_drones_by_designation_or_session() {
2208 let ds = fixture();
2209 assert_eq!(resolve(&ds, "Two").unwrap().unwrap().p.session, "bbbb2222-y");
2210 assert_eq!(resolve(&ds, "two of three").unwrap().unwrap().p.session, "bbbb2222-y");
2211 assert_eq!(resolve(&ds, "3").unwrap().unwrap().p.session, "cccc3333-x");
2212 assert_eq!(resolve(&ds, "aaaa").unwrap().unwrap().p.session, "aaaa1111-x");
2213 assert!(resolve(&ds, "all").unwrap().is_none());
2214 assert!(resolve(&ds, "Four").is_err());
2215 assert!(resolve(&ds, "zzzz").is_err());
2216 assert!(resolve(&ds, "aa").is_err(), "too short a prefix");
2217 assert!(["collective", "local"].iter().all(|w| resolve(&ds, w).unwrap().is_none()));
2218 assert_eq!(resolve(&ds, "bbbb").unwrap().unwrap().p.session, "bbbb2222-y", "nothing here matches: the member's");
2219 assert_eq!(word(21), "21");
2220 }
2221}