Skip to main content

fastmcp_console/
config.rs

1//! Centralized configuration for FastMCP console output.
2//!
3//! `ConsoleConfig` provides a single point of configuration for all aspects
4//! of rich console output, supporting both programmatic and environment
5//! variable-based configuration.
6
7use crate::detection::DisplayContext;
8use std::env;
9
10/// Comprehensive configuration for FastMCP console output
11#[derive(Debug, Clone)]
12pub struct ConsoleConfig {
13    // Display mode
14    /// Override display context (None = auto-detect)
15    pub context: Option<DisplayContext>,
16    /// Force color output even in non-TTY
17    pub force_color: Option<bool>,
18    /// Force plain text mode (no styling)
19    pub force_plain: bool,
20
21    // Theme
22    /// Custom color overrides (theme accessed via crate::theme::theme())
23    pub custom_colors: Option<CustomColors>,
24
25    // Startup
26    /// Show startup banner
27    pub show_banner: bool,
28    /// Show capabilities list in banner
29    pub show_capabilities: bool,
30    /// Banner display style
31    pub banner_style: BannerStyle,
32
33    // Logging
34    /// Log level filter
35    pub log_level: Option<log::Level>,
36    /// Show timestamps in logs
37    pub log_timestamps: bool,
38    /// Show target module in logs
39    pub log_targets: bool,
40    /// Show file and line in logs
41    pub log_file_line: bool,
42
43    // Runtime
44    /// Show periodic stats
45    pub show_stats_periodic: bool,
46    /// Stats display interval in seconds
47    pub stats_interval_secs: u64,
48    /// Show request/response traffic
49    pub show_request_traffic: bool,
50    /// Traffic logging verbosity
51    pub traffic_verbosity: TrafficVerbosity,
52
53    // Errors
54    /// Show fix suggestions for errors
55    pub show_suggestions: bool,
56    /// Show error codes
57    pub show_error_codes: bool,
58    /// Show backtraces for errors
59    pub show_backtrace: bool,
60
61    // Output limits
62    /// Maximum rows in tables
63    pub max_table_rows: usize,
64    /// Maximum depth for JSON display
65    pub max_json_depth: usize,
66    /// Truncate long strings at this length
67    pub truncate_at: usize,
68}
69
70/// Style variants for the startup banner
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub enum BannerStyle {
73    /// Full banner with logo, info panel, and capabilities
74    #[default]
75    Full,
76    /// Compact banner without logo
77    Compact,
78    /// Minimal one-line banner
79    Minimal,
80    /// No banner at all
81    None,
82}
83
84/// Verbosity levels for traffic logging
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
86pub enum TrafficVerbosity {
87    /// No traffic logging
88    #[default]
89    None,
90    /// Summary only (method name, timing)
91    Summary,
92    /// Include headers/metadata
93    Headers,
94    /// Full request/response bodies
95    Full,
96}
97
98/// Custom color overrides
99#[derive(Debug, Clone, Default)]
100pub struct CustomColors {
101    /// Primary brand color override
102    pub primary: Option<String>,
103    /// Secondary accent color override
104    pub secondary: Option<String>,
105    /// Success color override
106    pub success: Option<String>,
107    /// Warning color override
108    pub warning: Option<String>,
109    /// Error color override
110    pub error: Option<String>,
111}
112
113impl Default for ConsoleConfig {
114    fn default() -> Self {
115        Self {
116            context: None,
117            force_color: None,
118            force_plain: false,
119            custom_colors: None,
120            show_banner: true,
121            show_capabilities: true,
122            banner_style: BannerStyle::Full,
123            log_level: None,
124            log_timestamps: true,
125            log_targets: true,
126            log_file_line: false,
127            show_stats_periodic: false,
128            stats_interval_secs: 60,
129            show_request_traffic: false,
130            traffic_verbosity: TrafficVerbosity::None,
131            show_suggestions: true,
132            show_error_codes: true,
133            show_backtrace: false,
134            max_table_rows: 100,
135            max_json_depth: 5,
136            truncate_at: 200,
137        }
138    }
139}
140
141impl ConsoleConfig {
142    /// Create config with defaults
143    #[must_use]
144    pub fn new() -> Self {
145        Self::default()
146    }
147
148    /// Create config from environment variables
149    ///
150    /// # Environment Variables
151    ///
152    /// | Variable | Values | Description |
153    /// |----------|--------|-------------|
154    /// | `FASTMCP_FORCE_COLOR` | (set) | Force rich output |
155    /// | `FASTMCP_PLAIN` | (set) | Force plain output |
156    /// | `NO_COLOR` | (set) | Disable colors (standard) |
157    /// | `FASTMCP_BANNER` | full/compact/minimal/none | Banner style |
158    /// | `FASTMCP_LOG` | trace/debug/info/warn/error | Log level |
159    /// | `FASTMCP_LOG_TIMESTAMPS` | 0/1 | Show timestamps |
160    /// | `FASTMCP_TRAFFIC` | none/summary/headers/full | Traffic logging |
161    /// | `RUST_BACKTRACE` | 1/full | Show backtraces |
162    #[must_use]
163    pub fn from_env() -> Self {
164        Self::from_lookup(|key| env::var(key).ok())
165    }
166
167    fn from_lookup<F>(lookup: F) -> Self
168    where
169        F: Fn(&str) -> Option<String>,
170    {
171        let mut config = Self::default();
172
173        // Display mode
174        if lookup("FASTMCP_FORCE_COLOR").is_some() {
175            config.force_color = Some(true);
176        }
177        if lookup("FASTMCP_PLAIN").is_some() || lookup("NO_COLOR").is_some() {
178            config.force_plain = true;
179        }
180
181        // Banner
182        if let Some(val) = lookup("FASTMCP_BANNER") {
183            config.banner_style = match val.to_lowercase().as_str() {
184                "compact" => BannerStyle::Compact,
185                "minimal" => BannerStyle::Minimal,
186                "none" | "0" | "false" => BannerStyle::None,
187                // "full" and any other value default to Full
188                _ => BannerStyle::Full,
189            };
190            config.show_banner = !matches!(config.banner_style, BannerStyle::None);
191        }
192
193        // Logging
194        if let Some(level) = lookup("FASTMCP_LOG") {
195            config.log_level = match level.to_lowercase().as_str() {
196                "trace" => Some(log::Level::Trace),
197                "debug" => Some(log::Level::Debug),
198                "info" => Some(log::Level::Info),
199                "warn" | "warning" => Some(log::Level::Warn),
200                "error" => Some(log::Level::Error),
201                _ => None,
202            };
203        }
204        if lookup("FASTMCP_LOG_TIMESTAMPS")
205            .map(|v| v == "0" || v.to_lowercase() == "false")
206            .unwrap_or(false)
207        {
208            config.log_timestamps = false;
209        }
210
211        // Traffic
212        if let Some(val) = lookup("FASTMCP_TRAFFIC") {
213            config.traffic_verbosity = match val.to_lowercase().as_str() {
214                "summary" | "1" => TrafficVerbosity::Summary,
215                "headers" | "2" => TrafficVerbosity::Headers,
216                "full" | "3" => TrafficVerbosity::Full,
217                // "none", "0", and any other value default to None
218                _ => TrafficVerbosity::None,
219            };
220            config.show_request_traffic =
221                !matches!(config.traffic_verbosity, TrafficVerbosity::None);
222        }
223
224        // Errors
225        if lookup("RUST_BACKTRACE").is_some() {
226            config.show_backtrace = true;
227        }
228
229        config
230    }
231
232    // ─────────────────────────────────────────────────
233    // Builder Methods
234    // ─────────────────────────────────────────────────
235
236    /// Force color output
237    #[must_use]
238    pub fn force_color(mut self, force: bool) -> Self {
239        self.force_color = Some(force);
240        self
241    }
242
243    /// Enable plain text mode (no styling)
244    #[must_use]
245    pub fn plain_mode(mut self) -> Self {
246        self.force_plain = true;
247        self
248    }
249
250    /// Set the banner style
251    #[must_use]
252    pub fn with_banner(mut self, style: BannerStyle) -> Self {
253        self.banner_style = style;
254        self.show_banner = !matches!(style, BannerStyle::None);
255        self
256    }
257
258    /// Disable the banner entirely
259    #[must_use]
260    pub fn without_banner(mut self) -> Self {
261        self.show_banner = false;
262        self.banner_style = BannerStyle::None;
263        self
264    }
265
266    /// Set the log level
267    #[must_use]
268    pub fn with_log_level(mut self, level: log::Level) -> Self {
269        self.log_level = Some(level);
270        self
271    }
272
273    /// Set traffic logging verbosity
274    #[must_use]
275    pub fn with_traffic(mut self, verbosity: TrafficVerbosity) -> Self {
276        self.traffic_verbosity = verbosity;
277        self.show_request_traffic = !matches!(verbosity, TrafficVerbosity::None);
278        self
279    }
280
281    /// Enable periodic stats display
282    #[must_use]
283    pub fn with_periodic_stats(mut self, interval_secs: u64) -> Self {
284        self.show_stats_periodic = true;
285        self.stats_interval_secs = interval_secs;
286        self
287    }
288
289    /// Disable fix suggestions for errors
290    #[must_use]
291    pub fn without_suggestions(mut self) -> Self {
292        self.show_suggestions = false;
293        self
294    }
295
296    /// Set custom colors
297    #[must_use]
298    pub fn with_custom_colors(mut self, colors: CustomColors) -> Self {
299        self.custom_colors = Some(colors);
300        self
301    }
302
303    /// Set display context explicitly
304    #[must_use]
305    pub fn with_context(mut self, context: DisplayContext) -> Self {
306        self.context = Some(context);
307        self
308    }
309
310    /// Set maximum table rows
311    #[must_use]
312    pub fn with_max_table_rows(mut self, max: usize) -> Self {
313        self.max_table_rows = max;
314        self
315    }
316
317    /// Set maximum JSON depth
318    #[must_use]
319    pub fn with_max_json_depth(mut self, max: usize) -> Self {
320        self.max_json_depth = max;
321        self
322    }
323
324    /// Set truncation length
325    #[must_use]
326    pub fn with_truncate_at(mut self, len: usize) -> Self {
327        self.truncate_at = len;
328        self
329    }
330
331    // ─────────────────────────────────────────────────
332    // Accessor Methods
333    // ─────────────────────────────────────────────────
334
335    /// Get the theme (uses global theme singleton)
336    #[must_use]
337    pub fn theme(&self) -> &'static crate::theme::FastMcpTheme {
338        crate::theme::theme()
339    }
340
341    // ─────────────────────────────────────────────────
342    // Resolution Methods
343    // ─────────────────────────────────────────────────
344
345    /// Resolve the display context based on config and environment
346    #[must_use]
347    pub fn resolve_context(&self) -> DisplayContext {
348        if self.force_plain {
349            return DisplayContext::new_agent();
350        }
351        if let Some(true) = self.force_color {
352            return DisplayContext::new_human();
353        }
354        self.context.unwrap_or_else(DisplayContext::detect)
355    }
356
357    /// Check if rich output should be used based on resolved context
358    #[must_use]
359    pub fn should_use_rich(&self) -> bool {
360        self.resolve_context().is_human()
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use std::collections::HashMap;
368
369    fn config_from_pairs(pairs: &[(&str, &str)]) -> ConsoleConfig {
370        let map: HashMap<&str, &str> = pairs.iter().copied().collect();
371        ConsoleConfig::from_lookup(|key| map.get(key).map(|v| (*v).to_string()))
372    }
373
374    #[test]
375    fn test_default_config() {
376        let config = ConsoleConfig::new();
377        assert!(config.show_banner);
378        assert!(config.show_capabilities);
379        assert_eq!(config.banner_style, BannerStyle::Full);
380        assert!(config.log_timestamps);
381        assert!(!config.force_plain);
382        assert_eq!(config.max_table_rows, 100);
383    }
384
385    #[test]
386    fn test_builder_pattern() {
387        let config = ConsoleConfig::new()
388            .with_banner(BannerStyle::Compact)
389            .with_log_level(log::Level::Debug)
390            .with_traffic(TrafficVerbosity::Summary)
391            .with_periodic_stats(30);
392
393        assert_eq!(config.banner_style, BannerStyle::Compact);
394        assert_eq!(config.log_level, Some(log::Level::Debug));
395        assert_eq!(config.traffic_verbosity, TrafficVerbosity::Summary);
396        assert!(config.show_stats_periodic);
397        assert_eq!(config.stats_interval_secs, 30);
398    }
399
400    #[test]
401    fn test_plain_mode() {
402        let config = ConsoleConfig::new().plain_mode();
403        assert!(config.force_plain);
404        assert_eq!(config.resolve_context(), DisplayContext::Agent);
405    }
406
407    #[test]
408    fn test_force_color() {
409        let config = ConsoleConfig::new().force_color(true);
410        assert_eq!(config.force_color, Some(true));
411        assert_eq!(config.resolve_context(), DisplayContext::Human);
412    }
413
414    #[test]
415    fn test_without_banner() {
416        let config = ConsoleConfig::new().without_banner();
417        assert!(!config.show_banner);
418        assert_eq!(config.banner_style, BannerStyle::None);
419    }
420
421    #[test]
422    fn test_from_lookup_defaults_when_empty() {
423        let config = config_from_pairs(&[]);
424        assert_eq!(config.banner_style, BannerStyle::Full);
425        assert_eq!(config.log_level, None);
426        assert!(config.log_timestamps);
427        assert_eq!(config.traffic_verbosity, TrafficVerbosity::None);
428        assert!(!config.show_request_traffic);
429        assert!(!config.show_backtrace);
430    }
431
432    #[test]
433    fn test_from_lookup_display_mode_flags() {
434        let config = config_from_pairs(&[("FASTMCP_FORCE_COLOR", "1"), ("FASTMCP_PLAIN", "1")]);
435        assert_eq!(config.force_color, Some(true));
436        assert!(config.force_plain);
437
438        let no_color = config_from_pairs(&[("NO_COLOR", "1")]);
439        assert!(no_color.force_plain);
440    }
441
442    #[test]
443    fn test_from_lookup_banner_variants() {
444        let compact = config_from_pairs(&[("FASTMCP_BANNER", "compact")]);
445        assert_eq!(compact.banner_style, BannerStyle::Compact);
446        assert!(compact.show_banner);
447
448        let minimal = config_from_pairs(&[("FASTMCP_BANNER", "minimal")]);
449        assert_eq!(minimal.banner_style, BannerStyle::Minimal);
450        assert!(minimal.show_banner);
451
452        let none_false = config_from_pairs(&[("FASTMCP_BANNER", "false")]);
453        assert_eq!(none_false.banner_style, BannerStyle::None);
454        assert!(!none_false.show_banner);
455
456        let none_zero = config_from_pairs(&[("FASTMCP_BANNER", "0")]);
457        assert_eq!(none_zero.banner_style, BannerStyle::None);
458        assert!(!none_zero.show_banner);
459
460        let fallback = config_from_pairs(&[("FASTMCP_BANNER", "unknown")]);
461        assert_eq!(fallback.banner_style, BannerStyle::Full);
462        assert!(fallback.show_banner);
463    }
464
465    #[test]
466    fn test_from_lookup_log_levels_and_timestamp_toggle() {
467        let trace = config_from_pairs(&[("FASTMCP_LOG", "trace")]);
468        assert_eq!(trace.log_level, Some(log::Level::Trace));
469
470        let debug = config_from_pairs(&[("FASTMCP_LOG", "debug")]);
471        assert_eq!(debug.log_level, Some(log::Level::Debug));
472
473        let warn_alias = config_from_pairs(&[("FASTMCP_LOG", "warning")]);
474        assert_eq!(warn_alias.log_level, Some(log::Level::Warn));
475
476        let invalid = config_from_pairs(&[("FASTMCP_LOG", "verbose")]);
477        assert_eq!(invalid.log_level, None);
478
479        let timestamps_disabled_zero = config_from_pairs(&[("FASTMCP_LOG_TIMESTAMPS", "0")]);
480        assert!(!timestamps_disabled_zero.log_timestamps);
481
482        let timestamps_disabled_false = config_from_pairs(&[("FASTMCP_LOG_TIMESTAMPS", "false")]);
483        assert!(!timestamps_disabled_false.log_timestamps);
484
485        let timestamps_enabled = config_from_pairs(&[("FASTMCP_LOG_TIMESTAMPS", "1")]);
486        assert!(timestamps_enabled.log_timestamps);
487    }
488
489    #[test]
490    fn test_from_lookup_traffic_variants_and_backtrace() {
491        let summary = config_from_pairs(&[("FASTMCP_TRAFFIC", "summary")]);
492        assert_eq!(summary.traffic_verbosity, TrafficVerbosity::Summary);
493        assert!(summary.show_request_traffic);
494
495        let headers = config_from_pairs(&[("FASTMCP_TRAFFIC", "2")]);
496        assert_eq!(headers.traffic_verbosity, TrafficVerbosity::Headers);
497        assert!(headers.show_request_traffic);
498
499        let full = config_from_pairs(&[("FASTMCP_TRAFFIC", "3")]);
500        assert_eq!(full.traffic_verbosity, TrafficVerbosity::Full);
501        assert!(full.show_request_traffic);
502
503        let none = config_from_pairs(&[("FASTMCP_TRAFFIC", "none")]);
504        assert_eq!(none.traffic_verbosity, TrafficVerbosity::None);
505        assert!(!none.show_request_traffic);
506
507        let unknown = config_from_pairs(&[("FASTMCP_TRAFFIC", "loud")]);
508        assert_eq!(unknown.traffic_verbosity, TrafficVerbosity::None);
509        assert!(!unknown.show_request_traffic);
510
511        let backtrace = config_from_pairs(&[("RUST_BACKTRACE", "full")]);
512        assert!(backtrace.show_backtrace);
513    }
514
515    #[test]
516    fn test_additional_builder_methods_and_accessors() {
517        let custom = CustomColors {
518            primary: Some("#123456".to_string()),
519            secondary: None,
520            success: Some("#22aa22".to_string()),
521            warning: None,
522            error: Some("#ff0000".to_string()),
523        };
524
525        let config = ConsoleConfig::new()
526            .without_suggestions()
527            .with_custom_colors(custom.clone())
528            .with_context(DisplayContext::new_agent())
529            .with_max_table_rows(50)
530            .with_max_json_depth(3)
531            .with_truncate_at(80);
532
533        assert!(!config.show_suggestions);
534        assert!(config.custom_colors.is_some());
535        assert_eq!(
536            config
537                .custom_colors
538                .as_ref()
539                .and_then(|c| c.primary.as_deref()),
540            Some("#123456")
541        );
542        assert_eq!(config.context, Some(DisplayContext::Agent));
543        assert_eq!(config.max_table_rows, 50);
544        assert_eq!(config.max_json_depth, 3);
545        assert_eq!(config.truncate_at, 80);
546        assert!(std::ptr::eq(config.theme(), crate::theme::theme()));
547    }
548
549    #[test]
550    fn test_context_resolution_and_should_use_rich() {
551        let plain = ConsoleConfig::new().plain_mode();
552        assert_eq!(plain.resolve_context(), DisplayContext::Agent);
553        assert!(!plain.should_use_rich());
554
555        let forced_rich = ConsoleConfig::new().force_color(true);
556        assert_eq!(forced_rich.resolve_context(), DisplayContext::Human);
557        assert!(forced_rich.should_use_rich());
558
559        let explicit_agent = ConsoleConfig::new().with_context(DisplayContext::new_agent());
560        assert_eq!(explicit_agent.resolve_context(), DisplayContext::Agent);
561        assert!(!explicit_agent.should_use_rich());
562
563        let explicit_human = ConsoleConfig::new().with_context(DisplayContext::new_human());
564        assert_eq!(explicit_human.resolve_context(), DisplayContext::Human);
565        assert!(explicit_human.should_use_rich());
566    }
567
568    #[test]
569    fn test_builder_methods_via_fn_pointers() {
570        let set_force_color: fn(ConsoleConfig, bool) -> ConsoleConfig = ConsoleConfig::force_color;
571        let set_banner: fn(ConsoleConfig, BannerStyle) -> ConsoleConfig =
572            ConsoleConfig::with_banner;
573        let set_log: fn(ConsoleConfig, log::Level) -> ConsoleConfig = ConsoleConfig::with_log_level;
574        let set_traffic: fn(ConsoleConfig, TrafficVerbosity) -> ConsoleConfig =
575            ConsoleConfig::with_traffic;
576        let set_stats: fn(ConsoleConfig, u64) -> ConsoleConfig = ConsoleConfig::with_periodic_stats;
577        let disable_suggestions: fn(ConsoleConfig) -> ConsoleConfig =
578            ConsoleConfig::without_suggestions;
579        let set_custom: fn(ConsoleConfig, CustomColors) -> ConsoleConfig =
580            ConsoleConfig::with_custom_colors;
581        let set_context: fn(ConsoleConfig, DisplayContext) -> ConsoleConfig =
582            ConsoleConfig::with_context;
583        let set_rows: fn(ConsoleConfig, usize) -> ConsoleConfig =
584            ConsoleConfig::with_max_table_rows;
585        let set_depth: fn(ConsoleConfig, usize) -> ConsoleConfig =
586            ConsoleConfig::with_max_json_depth;
587        let set_truncate: fn(ConsoleConfig, usize) -> ConsoleConfig =
588            ConsoleConfig::with_truncate_at;
589
590        let custom = CustomColors {
591            primary: Some("#111111".to_string()),
592            secondary: Some("#222222".to_string()),
593            success: None,
594            warning: Some("#ffaa00".to_string()),
595            error: None,
596        };
597
598        let config = set_truncate(
599            set_depth(
600                set_rows(
601                    set_context(
602                        set_custom(
603                            disable_suggestions(set_stats(
604                                set_traffic(
605                                    set_log(
606                                        set_banner(
607                                            set_force_color(ConsoleConfig::new(), false),
608                                            BannerStyle::None,
609                                        ),
610                                        log::Level::Error,
611                                    ),
612                                    TrafficVerbosity::Headers,
613                                ),
614                                15,
615                            )),
616                            custom.clone(),
617                        ),
618                        DisplayContext::new_human(),
619                    ),
620                    12,
621                ),
622                7,
623            ),
624            42,
625        );
626
627        assert_eq!(config.force_color, Some(false));
628        assert_eq!(config.banner_style, BannerStyle::None);
629        assert!(!config.show_banner);
630        assert_eq!(config.log_level, Some(log::Level::Error));
631        assert_eq!(config.traffic_verbosity, TrafficVerbosity::Headers);
632        assert!(config.show_request_traffic);
633        assert!(config.show_stats_periodic);
634        assert_eq!(config.stats_interval_secs, 15);
635        assert!(!config.show_suggestions);
636        assert_eq!(
637            config
638                .custom_colors
639                .as_ref()
640                .and_then(|c| c.secondary.as_deref()),
641            Some("#222222")
642        );
643        assert_eq!(config.context, Some(DisplayContext::Human));
644        assert_eq!(config.max_table_rows, 12);
645        assert_eq!(config.max_json_depth, 7);
646        assert_eq!(config.truncate_at, 42);
647    }
648
649    #[test]
650    fn test_from_env_and_fallback_context_resolution_paths() {
651        let _ = ConsoleConfig::from_env();
652
653        let forced_false = ConsoleConfig::new()
654            .force_color(false)
655            .with_context(DisplayContext::new_agent());
656        assert_eq!(forced_false.resolve_context(), DisplayContext::Agent);
657        assert!(!forced_false.should_use_rich());
658
659        let explicit_human = ConsoleConfig::new()
660            .force_color(false)
661            .with_context(DisplayContext::new_human());
662        assert_eq!(explicit_human.resolve_context(), DisplayContext::Human);
663        assert!(explicit_human.should_use_rich());
664    }
665
666    // =========================================================================
667    // Additional coverage tests (bd-2ebx)
668    // =========================================================================
669
670    #[test]
671    fn banner_style_and_traffic_verbosity_defaults() {
672        assert_eq!(BannerStyle::default(), BannerStyle::Full);
673        assert_eq!(TrafficVerbosity::default(), TrafficVerbosity::None);
674    }
675
676    #[test]
677    fn console_config_debug_and_clone() {
678        let config = ConsoleConfig::new()
679            .with_log_level(log::Level::Info)
680            .with_max_table_rows(42);
681        let debug = format!("{config:?}");
682        assert!(debug.contains("ConsoleConfig"));
683        assert!(debug.contains("42"));
684
685        let cloned = config.clone();
686        assert_eq!(cloned.max_table_rows, 42);
687        assert_eq!(cloned.log_level, Some(log::Level::Info));
688    }
689
690    #[test]
691    fn custom_colors_default_all_none() {
692        let colors = CustomColors::default();
693        assert!(colors.primary.is_none());
694        assert!(colors.secondary.is_none());
695        assert!(colors.success.is_none());
696        assert!(colors.warning.is_none());
697        assert!(colors.error.is_none());
698
699        let debug = format!("{colors:?}");
700        assert!(debug.contains("CustomColors"));
701    }
702
703    #[test]
704    fn from_lookup_banner_none_literal_and_full_explicit() {
705        let none = config_from_pairs(&[("FASTMCP_BANNER", "none")]);
706        assert_eq!(none.banner_style, BannerStyle::None);
707        assert!(!none.show_banner);
708
709        let full = config_from_pairs(&[("FASTMCP_BANNER", "full")]);
710        assert_eq!(full.banner_style, BannerStyle::Full);
711        assert!(full.show_banner);
712    }
713
714    #[test]
715    fn from_lookup_remaining_log_levels() {
716        let info = config_from_pairs(&[("FASTMCP_LOG", "info")]);
717        assert_eq!(info.log_level, Some(log::Level::Info));
718
719        let warn = config_from_pairs(&[("FASTMCP_LOG", "warn")]);
720        assert_eq!(warn.log_level, Some(log::Level::Warn));
721
722        let error = config_from_pairs(&[("FASTMCP_LOG", "error")]);
723        assert_eq!(error.log_level, Some(log::Level::Error));
724    }
725
726    #[test]
727    fn from_lookup_traffic_numeric_one() {
728        let summary = config_from_pairs(&[("FASTMCP_TRAFFIC", "1")]);
729        assert_eq!(summary.traffic_verbosity, TrafficVerbosity::Summary);
730        assert!(summary.show_request_traffic);
731    }
732
733    #[test]
734    fn with_banner_none_clears_show_banner() {
735        let config = ConsoleConfig::new().with_banner(BannerStyle::None);
736        assert!(!config.show_banner);
737        assert_eq!(config.banner_style, BannerStyle::None);
738    }
739
740    #[test]
741    fn with_traffic_none_clears_show_request_traffic() {
742        let config = ConsoleConfig::new()
743            .with_traffic(TrafficVerbosity::Full)
744            .with_traffic(TrafficVerbosity::None);
745        assert!(!config.show_request_traffic);
746        assert_eq!(config.traffic_verbosity, TrafficVerbosity::None);
747    }
748
749    #[test]
750    fn default_fields_full_coverage() {
751        let config = ConsoleConfig::default();
752        assert!(config.log_targets);
753        assert!(!config.log_file_line);
754        assert!(config.show_error_codes);
755        assert!(!config.show_stats_periodic);
756        assert_eq!(config.stats_interval_secs, 60);
757        assert_eq!(config.max_json_depth, 5);
758        assert_eq!(config.truncate_at, 200);
759        assert!(config.show_suggestions);
760        assert!(config.custom_colors.is_none());
761        assert!(config.context.is_none());
762        assert!(config.force_color.is_none());
763    }
764}