diff --git a/crates/mind-body/src/heartbeat.rs b/crates/mind-body/src/heartbeat.rs index c12ef5b..2d46291 100644 --- a/crates/mind-body/src/heartbeat.rs +++ b/crates/mind-body/src/heartbeat.rs @@ -426,6 +426,14 @@ fn addresses(who: &PersonaId, delivery: &ChatDelivery) -> Option<&'static str> { } } +/// Whether a delivery wakes `who` off the beat: the operator or another Mind of +/// the roster addresses her. A Mind naming herself does not; a member does not. +fn wakes(config: &Config, who: &PersonaId, delivery: &ChatDelivery) -> bool { + let from_operator = config.operators.contains(&delivery.sender); + let from_other_mind = delivery.sender != who.matrix_id && config.personas.iter().any(|mind| mind.matrix_id == delivery.sender); + (from_operator || from_other_mind) && addresses(who, delivery).is_some() +} + /// The mouth pages a pending answer, so fewer deliveries may come than wait. /// The heartbeat acks nothing: it asks again after the highest seq it has until /// the answer is empty. A mouth that never empties is cut off at `PENDING_PAGES`. @@ -593,7 +601,7 @@ pub fn poll(organs: &Organs, config: &Config) -> Result { let mut launches: Vec = Vec::new(); let mut launched: Vec = Vec::new(); - // Off the beat: the operator naming her, once per message, and her Self run + // Off the beat: the operator or another Mind of the roster naming her, once per message, and her Self run // having ended since her last turn. Neither waits for the beat; both need her // idle and released. for row in &mut rows { @@ -606,7 +614,7 @@ pub fn poll(organs: &Organs, config: &Config) -> Result { pending.sort_by_key(|delivery| delivery.seq); pending .into_iter() - .filter(|delivery| config.operators.contains(&delivery.sender) && addresses(&row.seat.who, delivery).is_some()) + .filter(|delivery| wakes(config, &row.seat.who, delivery)) .map(|delivery| format!("{}:{}", slug.0, delivery.event_id)) .find(|key| !state.handled_mentions.contains(key)) }; diff --git a/crates/mind-body/src/heartbeat/tests.rs b/crates/mind-body/src/heartbeat/tests.rs index 4b29597..5e10aab 100644 --- a/crates/mind-body/src/heartbeat/tests.rs +++ b/crates/mind-body/src/heartbeat/tests.rs @@ -462,6 +462,30 @@ fn the_operator_naming_a_persona_launches_her_at_once_off_the_beat_and_once_per_ assert_eq!(rig.poll(), Outcome::Polled { launched: vec![Slug("ann".into())] }); } +#[test] +fn another_mind_naming_a_persona_wakes_her_once_off_the_beat() { + let rig = Rig::new(); + rig.turn(ANN, 10); + rig.turn(BOB, 10); + rig.store.save(&HeartbeatState { last_beat_at: rig.now(), handled_mentions: vec![] }).unwrap(); + rig.say(BOB, delivery(1, "@ann:chat", "Bob, a word", &[])); + assert_eq!(rig.poll(), Outcome::Polled { launched: vec![Slug("bob".into())] }, "a Mind of the roster wakes her at once"); + rig.end_run(BOB); + assert_eq!(rig.poll(), Outcome::Polled { launched: vec![] }, "the same message polled again wakes nobody"); + assert_eq!(rig.runs(BOB), 2); +} + +#[test] +fn a_mind_naming_no_one_or_herself_wakes_nobody() { + let rig = Rig::new(); + rig.turn(ANN, 10); + rig.turn(BOB, 10); + rig.store.save(&HeartbeatState { last_beat_at: rig.now(), handled_mentions: vec![] }).unwrap(); + rig.say(BOB, delivery(1, "@ann:chat", "nothing to anyone", &[])); + rig.say(BOB, delivery(2, "@bob:chat", "Bob reminds Bob", &["@bob:chat"])); + assert_eq!(rig.poll(), Outcome::Polled { launched: vec![] }); +} + #[test] fn a_member_naming_a_persona_launches_nobody_off_the_beat_but_counts_as_full_urgency_on_it() { let rig = Rig::new();