fix(cli): fail continuous runs on peer closure

Keep PeerClosed as a distinct successful library outcome while treating it as an error at the CLI and TUI boundary for uninterrupted continuous runs.

Track aggregate peer-close outcomes independently of bounded recent history and cover finite, continuous, grouped, mixed-failure, interruption, and TUI status behavior.
This commit is contained in:
Nils Andreas Svee committed 2026-07-28 10:23:47 +07:00
1 parent 8ea4c2f8a4
commit 58d11f1dec
7 files changed
+358 -19

No files matched your search

+21 -1
View File
@@ -14,7 +14,7 @@ use irtt_client::{
use crate::{
cmd::tui::args::{ResolvedTuiTarget, TuiArgs},
shared::client::{is_shutdown_requested, ClientSession},
shared::client::{is_shutdown_requested, session::peer_close_run_error, ClientSession},
};
use super::ui::{should_render, TuiConfig, TuiState, TuiStatus, TuiTerminal};
@@ -129,6 +129,16 @@ pub fn run_tui(
let events = session.close()?;
state.process_events(&events);
interrupted |= is_shutdown_requested(shutdown_requested);
if let Some(error) = peer_close_run_error(
continuous,
interrupted,
if session.is_peer_closed() { 1 } else { 0 },
) {
state.set_run_error(error.clone());
render_if_due(&mut terminal, &state, &mut next_render, true)?;
return Err(error.into());
}
state.set_status(TuiStatus::Complete);
render_if_due(&mut terminal, &state, &mut next_render, true)?;
Ok(())
@@ -249,6 +259,7 @@ fn run_group_tui(
}
}
interrupted |= is_shutdown_requested(shutdown_requested);
if outcome.end_reason == ManagedGroupEndReason::Cancelled && !interrupted {
return match exit {
GroupLoopExit::IdleGraceElapsed => {
@@ -262,6 +273,15 @@ fn run_group_tui(
}
};
}
if let Some(error) = peer_close_run_error(
args.is_continuous(),
interrupted,
outcome.peer_closed_target_outcomes,
) {
state.set_run_error(error.clone());
render_if_due(terminal, state, next_render, true)?;
return Err(error.into());
}
let successful_targets = outcome.successful_target_outcomes;
let failed_targets = outcome.failed_target_outcomes;
if !interrupted && successful_targets == 0 && failed_targets > 0 {
+23
View File
@@ -409,6 +409,12 @@ impl TuiState {
self.push_event(format!("error {message}"));
}
pub(super) fn set_run_error(&mut self, message: String) {
self.status = TuiStatus::Error;
self.last_warning = Some(message.clone());
self.push_event(format!("error {message}"));
}
pub(super) fn clear_visible_history(&mut self) {
for target in &mut self.targets {
target.graph_history.clear();
@@ -2412,6 +2418,23 @@ mod tests {
.is_some_and(|warning| warning.contains("pending probe limit")));
}
#[test]
fn run_error_preserves_peer_closed_target_status() {
let mut state = TuiState::with_target_labels(TuiConfig::default(), ["a".to_owned()]);
state.process_target_outcome(&target_outcome("a", ManagedTargetEndReason::PeerClosed));
state.set_run_error(
"continuous run ended because of peer closure (1 target session)".to_owned(),
);
assert_eq!(state.status, TuiStatus::Error);
assert_eq!(state.targets[0].status, TargetStatus::Closed);
assert!(state
.last_warning
.as_deref()
.is_some_and(|warning| warning.contains("peer closure")));
}
#[test]
fn all_failed_terminal_outcomes_set_error_status() {
let mut state =