use super::*;
#[test]
fn test_network_failure_sets_check_pending_not_retry() {
let state = create_test_state();
let event = PipelineEvent::Agent(AgentEvent::InvocationFailed {
role: AgentRole::Developer,
agent: AgentName::from("agent1"),
exit_code: 1,
error_kind: AgentErrorKind::Network,
retriable: true,
});
let new_state = reduce(state.clone(), event);
assert!(
new_state.connectivity.check_pending,
"Network failure should set check_pending"
);
assert!(
!new_state.connectivity.is_offline,
"Should not be offline after just setting check_pending"
);
assert_eq!(
new_state.continuation.same_agent_retry_count, state.continuation.same_agent_retry_count,
"same_agent_retry_count should be preserved"
);
assert_eq!(
new_state.continuation.xsd_retry_count, state.continuation.xsd_retry_count,
"xsd_retry_count should be preserved"
);
}
#[test]
fn test_offline_detection_freezes_retry_state() {
let state = PipelineState {
continuation: ContinuationState {
xsd_retry_count: 3,
xsd_retry_pending: true,
same_agent_retry_count: 2,
same_agent_retry_pending: true,
..ContinuationState::new()
},
..create_test_state()
};
let event = PipelineEvent::Agent(AgentEvent::InvocationFailed {
role: AgentRole::Developer,
agent: AgentName::from("agent1"),
exit_code: 1,
error_kind: AgentErrorKind::Network,
retriable: true,
});
let new_state = reduce(state.clone(), event);
assert_eq!(
new_state.continuation.xsd_retry_count, 3,
"xsd_retry_count should be preserved"
);
assert!(
new_state.continuation.xsd_retry_pending,
"xsd_retry_pending should be preserved"
);
assert_eq!(
new_state.continuation.same_agent_retry_count, 2,
"same_agent_retry_count should be preserved"
);
assert!(
new_state.continuation.same_agent_retry_pending,
"same_agent_retry_pending should be preserved"
);
}
#[test]
fn test_non_network_failure_still_consumes_budget() {
let state = create_test_state();
let event = PipelineEvent::Agent(AgentEvent::InvocationFailed {
role: AgentRole::Developer,
agent: AgentName::from("agent1"),
exit_code: 1,
error_kind: AgentErrorKind::InternalError,
retriable: false, });
let new_state = reduce(state.clone(), event);
assert!(
new_state.continuation.same_agent_retry_pending,
"Internal error should trigger same-agent retry"
);
assert_eq!(
new_state.continuation.same_agent_retry_count, 1,
"same_agent_retry_count should increment"
);
assert!(
!new_state.connectivity.check_pending,
"Non-network error should not set check_pending"
);
}
#[test]
fn test_connectivity_check_succeeded_while_online() {
let state = PipelineState {
connectivity: ConnectivityState {
check_pending: true,
..ConnectivityState::default()
},
..create_test_state()
};
let event = PipelineEvent::Agent(AgentEvent::ConnectivityCheckSucceeded);
let new_state = reduce(state.clone(), event);
assert!(
!new_state.connectivity.check_pending,
"Successful check should clear check_pending"
);
assert!(
!new_state.connectivity.is_offline,
"Should remain online after successful probe"
);
assert_eq!(
new_state.connectivity.consecutive_failures, 0,
"Failure count should reset"
);
}
#[test]
fn test_connectivity_check_failed_below_threshold() {
let state = PipelineState {
connectivity: ConnectivityState {
check_pending: true,
consecutive_failures: 0,
..ConnectivityState::default()
},
..create_test_state()
};
let event = PipelineEvent::Agent(AgentEvent::ConnectivityCheckFailed);
let new_state = reduce(state.clone(), event);
assert_eq!(
new_state.connectivity.consecutive_failures, 1,
"Failure count should increment"
);
assert!(
new_state.connectivity.check_pending,
"Should still be checking (threshold not reached)"
);
assert!(
!new_state.connectivity.is_offline,
"Should not be offline yet (threshold=2)"
);
}
#[test]
fn test_connectivity_check_failed_at_threshold_enters_offline() {
let state = PipelineState {
connectivity: ConnectivityState {
check_pending: true,
consecutive_failures: 1,
..ConnectivityState::default()
},
..create_test_state()
};
let event = PipelineEvent::Agent(AgentEvent::ConnectivityCheckFailed);
let new_state = reduce(state.clone(), event);
assert!(
new_state.connectivity.is_offline,
"Should be offline after reaching failure threshold"
);
assert!(
new_state.connectivity.poll_pending,
"Should have poll_pending when offline"
);
assert!(
!new_state.connectivity.check_pending,
"check_pending should be cleared when entering offline"
);
}
#[test]
fn test_back_online_resumes_without_budget_consumption() {
let state = PipelineState {
connectivity: ConnectivityState {
is_offline: true,
poll_pending: true,
consecutive_failures: 2,
..ConnectivityState::default()
},
continuation: ContinuationState {
xsd_retry_count: 3,
same_agent_retry_count: 2,
..ContinuationState::new()
},
..create_test_state()
};
let event = PipelineEvent::Agent(AgentEvent::ConnectivityCheckSucceeded);
let new_state = reduce(state.clone(), event);
assert!(!new_state.connectivity.is_offline, "Should be back online");
assert!(
!new_state.connectivity.poll_pending,
"poll_pending should be cleared"
);
assert!(
!new_state.connectivity.check_pending,
"check_pending should be cleared"
);
assert_eq!(
new_state.continuation.xsd_retry_count, 3,
"xsd_retry_count should be preserved through offline window"
);
assert_eq!(
new_state.continuation.same_agent_retry_count, 2,
"same_agent_retry_count should be preserved through offline window"
);
}
#[test]
fn test_offline_state_check_pending_blocks_retry_in_orchestrator() {
let state = PipelineState {
connectivity: ConnectivityState {
check_pending: true,
..ConnectivityState::default()
},
continuation: ContinuationState {
xsd_retry_pending: true,
xsd_retry_count: 3,
..ContinuationState::new()
},
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::CheckNetworkConnectivity),
"check_pending should block xsd_retry_pending: got {:?}",
effect
);
}
#[test]
fn test_offline_state_poll_pending_blocks_continuation_in_orchestrator() {
let state = PipelineState {
connectivity: ConnectivityState {
is_offline: true,
poll_pending: true,
offline_poll_interval_ms: 5000,
..ConnectivityState::default()
},
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::PollForConnectivity { .. }),
"poll_pending should block continuation: got {:?}",
effect
);
}
#[test]
fn test_back_online_allows_xsd_retry_to_proceed() {
let state = PipelineState {
connectivity: ConnectivityState::default(), continuation: ContinuationState {
xsd_retry_pending: true,
xsd_retry_count: 3,
..ContinuationState::new()
},
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
Effect::PreparePlanningPrompt {
prompt_mode: crate::reducer::PromptMode::XsdRetry,
..
}
),
"After back online, xsd_retry should proceed: got {:?}",
effect
);
}
#[test]
fn test_orchestrator_connectivity_priority_before_same_agent_retry() {
let state = PipelineState {
connectivity: ConnectivityState {
check_pending: true,
..ConnectivityState::default()
},
continuation: ContinuationState {
same_agent_retry_pending: true,
same_agent_retry_count: 1,
..ContinuationState::new()
},
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::CheckNetworkConnectivity),
"check_pending should block same_agent_retry_pending: got {:?}",
effect
);
}
#[test]
fn test_debounce_prevents_rapid_offline_online_thrashing() {
let mut state = PipelineState {
connectivity: ConnectivityState {
check_pending: true,
..ConnectivityState::default()
},
..create_test_state()
};
state = reduce(
state,
PipelineEvent::Agent(AgentEvent::ConnectivityCheckFailed),
);
assert!(
!state.connectivity.is_offline,
"Should not be offline after 1 failure"
);
state = reduce(
state,
PipelineEvent::Agent(AgentEvent::ConnectivityCheckSucceeded),
);
assert!(
!state.connectivity.is_offline,
"Should still be online after success"
);
assert_eq!(
state.connectivity.consecutive_failures, 0,
"Failure count should reset on success"
);
state = reduce(
state,
PipelineEvent::Agent(AgentEvent::ConnectivityCheckFailed),
);
assert!(
!state.connectivity.is_offline,
"Should not be offline after 2 non-consecutive failures"
);
state = reduce(
state,
PipelineEvent::Agent(AgentEvent::ConnectivityCheckSucceeded),
);
assert!(!state.connectivity.is_offline, "Should remain online");
}
#[test]
fn test_single_success_exits_offline_mode() {
let state = PipelineState {
connectivity: ConnectivityState {
is_offline: true,
poll_pending: true,
consecutive_failures: 2,
required_successes_to_go_online: 1, ..ConnectivityState::default()
},
..create_test_state()
};
let event = PipelineEvent::Agent(AgentEvent::ConnectivityCheckSucceeded);
let new_state = reduce(state, event);
assert!(
!new_state.connectivity.is_offline,
"Should be back online after 1 success"
);
assert!(
!new_state.connectivity.poll_pending,
"poll_pending should be cleared"
);
}
#[test]
fn test_connectivity_interruption_metric_increments_on_offline_entry() {
let state = PipelineState {
connectivity: ConnectivityState {
check_pending: true,
consecutive_failures: 1,
required_failures_to_go_offline: 2,
..ConnectivityState::default()
},
..create_test_state()
};
let new_state = reduce(
state,
PipelineEvent::Agent(AgentEvent::ConnectivityCheckFailed),
);
assert!(new_state.connectivity.is_offline, "Should be offline now");
assert_eq!(
new_state.metrics.connectivity_interruptions_total, 1,
"Should record exactly one connectivity interruption on offline entry"
);
}
#[test]
fn test_connectivity_interruption_metric_does_not_increment_during_polling() {
let state = PipelineState {
connectivity: ConnectivityState {
is_offline: true,
poll_pending: true,
consecutive_failures: 2,
..ConnectivityState::default()
},
metrics: RunMetrics {
connectivity_interruptions_total: 1,
..RunMetrics::default()
},
..create_test_state()
};
let new_state = reduce(
state,
PipelineEvent::Agent(AgentEvent::ConnectivityCheckFailed),
);
assert_eq!(
new_state.metrics.connectivity_interruptions_total, 1,
"Should NOT increment again while already offline"
);
}