Watch
0
0
Fork
You've already forked hyperhive
0

swarm-controller: test how a subagent's status becomes its list entry

The running, stale and finished rule, the status deciding the entry over the
newest row, and the fallback to the row when a subagent has no status.
This commit is contained in:
iris 2026-11-15 15:14:45 +01:00 • committed by mara
commit 7122084850

View file

@ -472,9 +472,102 @@ fn read_failed(agent: &str, e: &dyn std::error::Error) -> problem_details::Probl
#[cfg(test)]
mod tests {
use super::header;
use super::{NewestRow, SubagentEntry, SubagentState, entry_of, header};
use swarm_wire::subagent_status::{STALE_AFTER_S, State, Status};
use swarm_wire::subagent_term::{HEADER_EFFORT, HEADER_MODEL, subagent_names};
const NOW: i64 = 1_000_000;
fn status(state: State, since: i64, ts: i64) -> Status {
Status {
state,
since,
exit: None,
model: Some("opus".to_owned()),
effort: Some("high".to_owned()),
ts,
}
}
fn row() -> NewestRow {
NewestRow {
unix: NOW - 30,
model: Some("haiku".to_owned()),
effort: None,
}
}
/// A running status counts as running until three heartbeats are missed,
/// then stale; a finished one is finished however fresh it is.
#[test]
fn a_state_is_running_until_its_heartbeats_stop() {
let fresh = status(State::Running, NOW - 500, NOW - 10);
assert_eq!(SubagentState::of(&fresh, NOW), SubagentState::Running);
let edge = status(State::Running, NOW - 500, NOW - STALE_AFTER_S);
assert_eq!(SubagentState::of(&edge, NOW), SubagentState::Running);
let silent = status(State::Running, NOW - 500, NOW - STALE_AFTER_S - 1);
assert_eq!(SubagentState::of(&silent, NOW), SubagentState::Stale);
let done = status(State::Finished, NOW - 500, NOW - 1);
assert_eq!(SubagentState::of(&done, NOW), SubagentState::Finished);
}
/// With a status the entry takes the state, the start, the exit status
/// and the model and effort from it, and ignores any newest row.
#[test]
fn a_status_decides_the_entry_over_the_newest_row() {
let mut done = status(State::Finished, NOW - 500, NOW - 1);
done.exit = Some(2);
let entry = entry_of("scout".to_owned(), Some(done), Some(Ok(row())), NOW);
assert_eq!(
entry,
SubagentEntry {
name: "scout".to_owned(),
last_row_unix: None,
model: Some("opus".to_owned()),
effort: Some("high".to_owned()),
state: Some(SubagentState::Finished),
since_unix: Some(NOW - 500),
exit: Some(2),
}
);
}
/// Without a status, as from an older daemon, the entry is what the newest
/// row says and has no state, so the swarm UI judges it from the row's age.
#[test]
fn without_a_status_the_entry_is_the_newest_row() {
let entry = entry_of("scout".to_owned(), None, Some(Ok(row())), NOW);
assert_eq!(
entry,
SubagentEntry {
name: "scout".to_owned(),
last_row_unix: Some(NOW - 30),
model: Some("haiku".to_owned()),
effort: None,
state: None,
since_unix: None,
exit: None,
}
);
}
/// A row that could not be read leaves the time and headers out and
/// still lists the subagent.
#[test]
fn a_failed_row_lookup_still_lists_the_subagent() {
let failed = entry_of(
"scout".to_owned(),
None,
Some(Err("timeout".to_owned())),
NOW,
);
assert_eq!(failed.name, "scout");
assert_eq!(failed.last_row_unix, None);
assert_eq!(failed.state, None);
let skipped = entry_of("scout".to_owned(), None, None, NOW);
assert_eq!(skipped, failed);
}
/// The list is what `STREAM.INFO` reports for the stream's subjects, by
/// name: a subagent that published once and one that published many rows
/// list the same way, and the order is the name's, not the stream's.