Skip to main content

fastmcp_console/
config.rs

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