use super::{acked, agent, nothing};
use crate::attention::*;
use crate::git_tree::{Agent, AgentState};
use crate::ui_state::{SeenKind, UiState};
#[test]
fn no_signals_is_no_attention() {
let a = attention(&agent("a"), "ws", ¬hing);
assert!(!a.any());
assert!(a.kinds().is_empty());
assert_eq!(
a,
Attention {
notify: false,
stopped: false,
refused: false,
budget: false,
conflicted: false,
mail: false,
held: false,
flagged: false,
}
);
}
#[test]
fn each_marked_signal_fires_when_unseen() {
type Case = (fn(&mut Agent), AttentionKind);
let cases: &[Case] = &[
(|a| a.notify_oid = Some("n".into()), AttentionKind::Notify),
(|a| a.budget_oid = Some("b".into()), AttentionKind::Budget),
(
|a| a.conflicted_oid = Some("c".into()),
AttentionKind::Conflicted,
),
];
for (mutate, kind) in cases {
let mut ag = agent("a");
mutate(&mut ag);
let att = attention(&ag, "ws", ¬hing);
assert!(att.any(), "{kind:?} should fire");
assert_eq!(att.kinds(), vec![*kind]);
assert!(!att.mail, "a mark never implies mail");
}
}
#[test]
fn rest_fires_however_the_conversation_came_to_rest() {
for state in [AgentState::Quiescent, AgentState::Stopped] {
let mut ag = agent("a");
ag.state = state;
let att = attention(&ag, "ws", ¬hing);
assert!(att.stopped, "{state:?} is a turn waiting on you");
assert_eq!(att.kinds(), vec![AttentionKind::Stopped]);
assert!(!attention(&ag, "ws", &acked(SeenKind::Stopped, "tip-a")).stopped);
}
for state in [AgentState::Live, AgentState::InFlight] {
let mut ag = agent("a");
ag.state = state;
assert!(
!attention(&ag, "ws", ¬hing).stopped,
"{state:?} is still running โ nothing is waiting on you"
);
}
}
#[test]
fn abandoned_suppresses_the_rest_signal_from_either_rest() {
for state in [AgentState::Quiescent, AgentState::Stopped] {
let mut ag = agent("a");
ag.state = state;
ag.abandoned_oid = Some("dead".into());
let att = attention(&ag, "ws", ¬hing);
assert!(
!att.stopped,
"abandoned = will-not-retry suppresses rest (ยง6), {state:?}"
);
assert!(!att.any());
}
}
#[test]
fn a_park_is_attention_no_acknowledgement_can_quiet() {
let mut ag = agent("a");
ag.held = Some(crate::control::hold::Held {
tool_use_id: "toolu_1".into(),
tool: "bash".into(),
reason: "bash {\"command\":\"curl x\"} classified open-world".into(),
});
let att = attention(&ag, "ws", ¬hing);
assert!(att.held && att.any());
assert_eq!(att.kinds(), vec![AttentionKind::Held]);
for kind in [
SeenKind::Notify,
SeenKind::Stopped,
SeenKind::Budget,
SeenKind::Conflicted,
] {
assert!(attention(&ag, "ws", &acked(kind, "toolu_1")).held);
}
assert!(
evidence(&ag).is_empty(),
"a park writes no watermark โ there is nothing to acknowledge"
);
}
#[test]
fn rest_evidence_is_the_tip_of_an_unabandoned_agent_at_rest() {
let mut ag = agent("a");
assert_eq!(rest_evidence(&ag), None, "Live: no rest evidence");
ag.state = AgentState::Quiescent;
assert_eq!(rest_evidence(&ag), Some("tip-a".to_string()));
ag.abandoned_oid = Some("dead".into());
assert_eq!(rest_evidence(&ag), None, "abandoned: no rest evidence");
}
#[test]
fn acked_oid_clears_but_moved_oid_re_arms() {
let mut ag = agent("a");
ag.notify_oid = Some("v2".into());
assert!(
attention(&ag, "ws", &acked(SeenKind::Notify, "v1")).notify,
"a moved ref re-notifies (ยง4.1)"
);
assert!(!attention(&ag, "ws", &acked(SeenKind::Notify, "v2")).notify);
}
#[test]
fn stopped_tip_watermark_gates_on_the_branch_tip() {
let mut ag = agent("a");
ag.state = AgentState::Stopped;
assert!(!attention(&ag, "ws", &acked(SeenKind::Stopped, "tip-a")).stopped);
assert!(attention(&ag, "ws", &acked(SeenKind::Stopped, "tip-old")).stopped);
}
#[test]
fn mail_fires_only_on_pending_and_definite_free() {
let cases = [
(AgentState::Quiescent, false, 2, true), (AgentState::Stopped, false, 1, true), (AgentState::Quiescent, true, 2, false), (AgentState::Stopped, true, 3, false), (AgentState::Live, false, 5, false), (AgentState::InFlight, false, 5, false), (AgentState::Quiescent, false, 0, false), ];
for (state, uncertain, pending, expect) in cases {
let mut ag = agent("a");
ag.state = state;
ag.state_uncertain = uncertain;
ag.pending = vec![crate::inboxview::InboxEntry::default(); pending];
ag.abandoned_oid = Some("x".into());
assert_eq!(
attention(&ag, "ws", ¬hing).mail,
expect,
"{state:?} uncertain={uncertain} pending={pending}"
);
}
}
#[test]
fn all_signals_together_list_in_badge_order() {
let mut ag = agent("a");
ag.notify_oid = Some("n".into());
ag.state = AgentState::Stopped;
ag.budget_oid = Some("b".into());
ag.conflicted_oid = Some("c".into());
ag.pending = vec![crate::inboxview::InboxEntry::default()]; assert_eq!(
attention(&ag, "ws", ¬hing).kinds(),
vec![
AttentionKind::Notify,
AttentionKind::Stopped,
AttentionKind::Budget,
AttentionKind::Conflicted,
AttentionKind::Mail,
]
);
}
#[test]
fn wires_through_a_real_ui_state() {
let mut ui = UiState::open(std::path::PathBuf::from("/nonexistent/dir/ui.json"));
ui.record_seen("ws", "a", &[(SeenKind::Notify, "oid1".to_string())]);
let mut ag = agent("a");
ag.notify_oid = Some("oid1".into());
let seen = |k: SeenKind, w: &str, a: &str, o: &str| ui.is_seen(k, w, a, o);
assert!(!attention(&ag, "ws", &seen).notify);
ag.notify_oid = Some("oid2".into());
assert!(attention(&ag, "ws", &seen).notify);
}