test(tui-core): add unit tests for UiState state machine (#2450)
Add 25 unit tests covering the previously untested UiState::reduce state machine and snapshot: - Default state verification - Key navigation: pane switching (1-5), unknown key no-op - Prompt lifecycle: pending_tasks increment, response delta tracking - Tool lifecycle: active_tool set/clear, pending_tasks decrement, saturation - Job lifecycle: active_jobs increment/decrement, progress clamping, saturation - Approval lifecycle: pending_approvals increment/decrement, saturation - Pause/Resume: flag toggling - Tick: ScheduleBackgroundRefresh effect - Snapshot: field presence, state change reflection Co-authored-by: Hu Qiantao <huqiantao@HudeMacBook-Air.local>
This commit is contained in:
@@ -227,3 +227,244 @@ impl UiState {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── Default state ──────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_state_is_chat_pane_and_ready() {
|
||||
let state = UiState::default();
|
||||
assert_eq!(state.active_pane, Pane::Chat);
|
||||
assert!(!state.paused);
|
||||
assert_eq!(state.last_response_delta, None);
|
||||
assert_eq!(state.active_tool, None);
|
||||
assert_eq!(state.pending_tasks, 0);
|
||||
assert_eq!(state.active_jobs, 0);
|
||||
assert_eq!(state.pending_approvals, 0);
|
||||
assert_eq!(state.status_line, "ready");
|
||||
}
|
||||
|
||||
// ── Key navigation ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn key_1_switches_to_chat_pane() {
|
||||
let mut state = UiState::default();
|
||||
let effects = state.reduce(UiEvent::KeyPressed('1'));
|
||||
assert_eq!(state.active_pane, Pane::Chat);
|
||||
assert_eq!(effects, vec![UiEffect::Render]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_2_switches_to_diff_pane() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::KeyPressed('2'));
|
||||
assert_eq!(state.active_pane, Pane::Diff);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_3_switches_to_tasks_pane() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::KeyPressed('3'));
|
||||
assert_eq!(state.active_pane, Pane::Tasks);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_4_switches_to_agents_pane() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::KeyPressed('4'));
|
||||
assert_eq!(state.active_pane, Pane::Agents);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_5_switches_to_jobs_pane() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::KeyPressed('5'));
|
||||
assert_eq!(state.active_pane, Pane::Jobs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_key_produces_no_effects() {
|
||||
let mut state = UiState::default();
|
||||
let effects = state.reduce(UiEvent::KeyPressed('x'));
|
||||
assert!(effects.is_empty());
|
||||
assert_eq!(state.active_pane, Pane::Chat);
|
||||
}
|
||||
|
||||
// ── Prompt lifecycle ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn prompt_submitted_increments_pending_tasks() {
|
||||
let mut state = UiState::default();
|
||||
let effects = state.reduce(UiEvent::PromptSubmitted("hello".to_string()));
|
||||
assert_eq!(state.pending_tasks, 1);
|
||||
assert_eq!(state.status_line, "prompt submitted");
|
||||
assert!(effects.contains(&UiEffect::Render));
|
||||
assert!(effects.contains(&UiEffect::PersistCheckpoint));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_delta_updates_last_delta() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::ResponseDelta("partial".to_string()));
|
||||
assert_eq!(state.last_response_delta, Some("partial".to_string()));
|
||||
assert_eq!(state.status_line, "streaming response");
|
||||
}
|
||||
|
||||
// ── Tool lifecycle ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn tool_started_sets_active_tool() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::ToolStarted("shell".to_string()));
|
||||
assert_eq!(state.active_tool, Some("shell".to_string()));
|
||||
assert_eq!(state.status_line, "tool running: shell");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_finished_clears_active_tool_and_decrements_tasks() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::PromptSubmitted("test".to_string()));
|
||||
assert_eq!(state.pending_tasks, 1);
|
||||
state.reduce(UiEvent::ToolFinished("shell".to_string()));
|
||||
assert_eq!(state.active_tool, None);
|
||||
assert_eq!(state.pending_tasks, 0);
|
||||
assert_eq!(state.status_line, "tool finished: shell");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_finished_saturates_at_zero() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::ToolFinished("shell".to_string()));
|
||||
assert_eq!(state.pending_tasks, 0);
|
||||
}
|
||||
|
||||
// ── Job lifecycle ──────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn job_queued_increments_active_jobs() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::JobQueued("build".to_string()));
|
||||
assert_eq!(state.active_jobs, 1);
|
||||
assert_eq!(state.status_line, "job queued");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_progress_updates_status_line() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::JobProgress {
|
||||
job_id: "j1".to_string(),
|
||||
progress: 75,
|
||||
});
|
||||
assert_eq!(state.status_line, "job progress: 75%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_progress_clamps_over_100() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::JobProgress {
|
||||
job_id: "j1".to_string(),
|
||||
progress: 150,
|
||||
});
|
||||
assert_eq!(state.status_line, "job progress: 100%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_completed_decrements_active_jobs() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::JobQueued("build".to_string()));
|
||||
assert_eq!(state.active_jobs, 1);
|
||||
state.reduce(UiEvent::JobCompleted("build".to_string()));
|
||||
assert_eq!(state.active_jobs, 0);
|
||||
assert_eq!(state.status_line, "job completed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_completed_saturates_at_zero() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::JobCompleted("build".to_string()));
|
||||
assert_eq!(state.active_jobs, 0);
|
||||
}
|
||||
|
||||
// ── Approval lifecycle ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn approval_requested_increments_pending() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::ApprovalRequested("exec".to_string()));
|
||||
assert_eq!(state.pending_approvals, 1);
|
||||
assert_eq!(state.status_line, "approval requested");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_resolved_decrements_pending() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::ApprovalRequested("exec".to_string()));
|
||||
state.reduce(UiEvent::ApprovalResolved("exec".to_string()));
|
||||
assert_eq!(state.pending_approvals, 0);
|
||||
assert_eq!(state.status_line, "approval resolved");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_resolved_saturates_at_zero() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::ApprovalResolved("exec".to_string()));
|
||||
assert_eq!(state.pending_approvals, 0);
|
||||
}
|
||||
|
||||
// ── Pause/Resume ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn pause_sets_paused_flag() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::PauseRequested);
|
||||
assert!(state.paused);
|
||||
assert_eq!(state.status_line, "paused");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_clears_paused_flag() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::PauseRequested);
|
||||
state.reduce(UiEvent::ResumeRequested);
|
||||
assert!(!state.paused);
|
||||
assert_eq!(state.status_line, "resumed");
|
||||
}
|
||||
|
||||
// ── Tick ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn tick_schedules_background_refresh() {
|
||||
let mut state = UiState::default();
|
||||
let effects = state.reduce(UiEvent::Tick);
|
||||
assert_eq!(effects, vec![UiEffect::ScheduleBackgroundRefresh]);
|
||||
}
|
||||
|
||||
// ── Snapshot ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn snapshot_contains_all_fields() {
|
||||
let state = UiState::default();
|
||||
let snap = state.snapshot();
|
||||
assert!(snap.contains("pane=Chat"));
|
||||
assert!(snap.contains("paused=false"));
|
||||
assert!(snap.contains("pending_tasks=0"));
|
||||
assert!(snap.contains("active_jobs=0"));
|
||||
assert!(snap.contains("pending_approvals=0"));
|
||||
assert!(snap.contains("status=ready"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_reflects_state_changes() {
|
||||
let mut state = UiState::default();
|
||||
state.reduce(UiEvent::KeyPressed('2'));
|
||||
state.reduce(UiEvent::PauseRequested);
|
||||
state.reduce(UiEvent::PromptSubmitted("test".to_string()));
|
||||
let snap = state.snapshot();
|
||||
assert!(snap.contains("pane=Diff"));
|
||||
assert!(snap.contains("paused=true"));
|
||||
assert!(snap.contains("pending_tasks=1"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user