From e2efc61beb529008ebc7cd6b865dbbb3a52ac23d Mon Sep 17 00:00:00 2001 From: teernisse Date: Fri, 13 Feb 2026 14:19:56 -0500 Subject: [PATCH] refactor(cli): extract stage_spinner to shared progress module Moves stage_spinner() from a private function in sync.rs to a pub function in cli/progress.rs so it can be reused by the timeline and search commands. The function creates a numbered spinner (e.g. [1/3]) for pipeline stages, returning a hidden no-op bar in robot mode to keep caller code path-uniform. sync.rs now imports from crate::cli::progress::stage_spinner instead of defining its own copy. Adds unit tests for robot mode (hidden bar), human mode (prefix/message properties), and prefix formatting. Co-Authored-By: Claude Opus 4.6 --- src/cli/commands/sync.rs | 17 +------------ src/cli/progress.rs | 54 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/cli/commands/sync.rs b/src/cli/commands/sync.rs index af43c53..2372944 100644 --- a/src/cli/commands/sync.rs +++ b/src/cli/commands/sync.rs @@ -7,6 +7,7 @@ use tracing::Instrument; use tracing::{info, warn}; use crate::Config; +use crate::cli::progress::stage_spinner; use crate::core::error::Result; use crate::core::metrics::{MetricsLayer, StageTiming}; use crate::core::shutdown::ShutdownSignal; @@ -42,22 +43,6 @@ pub struct SyncResult { pub status_enrichment_errors: usize, } -fn stage_spinner(stage: u8, total: u8, msg: &str, robot_mode: bool) -> ProgressBar { - if robot_mode { - return ProgressBar::hidden(); - } - let pb = crate::cli::progress::multi().add(ProgressBar::new_spinner()); - pb.set_style( - ProgressStyle::default_spinner() - .template("{spinner:.blue} {prefix} {msg}") - .expect("valid template"), - ); - pb.enable_steady_tick(std::time::Duration::from_millis(80)); - pb.set_prefix(format!("[{stage}/{total}]")); - pb.set_message(msg.to_string()); - pb -} - pub async fn run_sync( config: &Config, options: SyncOptions, diff --git a/src/cli/progress.rs b/src/cli/progress.rs index 7249ac9..ed002b5 100644 --- a/src/cli/progress.rs +++ b/src/cli/progress.rs @@ -1,4 +1,4 @@ -use indicatif::MultiProgress; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use std::io::Write; use std::sync::LazyLock; use tracing_subscriber::fmt::MakeWriter; @@ -9,6 +9,26 @@ pub fn multi() -> &'static MultiProgress { &MULTI } +/// Create a spinner for a numbered pipeline stage. +/// +/// Returns a hidden (no-op) bar in robot mode so callers can use +/// the same code path regardless of output mode. +pub fn stage_spinner(stage: u8, total: u8, msg: &str, robot_mode: bool) -> ProgressBar { + if robot_mode { + return ProgressBar::hidden(); + } + let pb = multi().add(ProgressBar::new_spinner()); + pb.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.blue} {prefix} {msg}") + .expect("valid template"), + ); + pb.enable_steady_tick(std::time::Duration::from_millis(80)); + pb.set_prefix(format!("[{stage}/{total}]")); + pb.set_message(msg.to_string()); + pb +} + #[derive(Clone)] pub struct SuspendingWriter; @@ -50,7 +70,6 @@ impl<'a> MakeWriter<'a> for SuspendingWriter { #[cfg(test)] mod tests { use super::*; - use indicatif::ProgressBar; #[test] fn multi_returns_same_instance() { @@ -88,4 +107,35 @@ mod tests { let w = MakeWriter::make_writer(&writer); drop(w); } + + #[test] + fn stage_spinner_robot_mode_returns_hidden() { + let pb = stage_spinner(1, 3, "Testing...", true); + assert!(pb.is_hidden()); + } + + #[test] + fn stage_spinner_human_mode_sets_properties() { + // In non-TTY test environments, MultiProgress may report bars as + // hidden. Verify the human-mode code path by checking that prefix + // and message are configured (robot-mode returns a bare hidden bar). + let pb = stage_spinner(1, 3, "Testing...", false); + assert_eq!(pb.prefix(), "[1/3]"); + assert_eq!(pb.message(), "Testing..."); + pb.finish_and_clear(); + } + + #[test] + fn stage_spinner_sets_prefix_format() { + let pb = stage_spinner(2, 5, "Working...", false); + assert_eq!(pb.prefix(), "[2/5]"); + pb.finish_and_clear(); + } + + #[test] + fn stage_spinner_sets_message() { + let pb = stage_spinner(1, 3, "Seeding timeline...", false); + assert_eq!(pb.message(), "Seeding timeline..."); + pb.finish_and_clear(); + } }