From c7e5cb41605dcaa9a88e4d491b9dfe498d2f605d Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Tue, 12 May 2026 21:59:11 -0500 Subject: [PATCH] test: fix three tests that read host settings.toml default_mode The user's ~/Library/Application Support/deepseek/settings.toml had default_mode = "yolo", which caused test_mode_yolo_sets_all_flags, test_trust_on_enables_flag, and footer_status_line_spans_show_mode_and_model_idle_and_active to fail because they implicitly depended on the host's global mode setting. Pin each test to Agent mode explicitly so they pass regardless of the developer's personal settings. --- crates/tui/src/commands/config.rs | 110 ++++++++++++++++++++++++------ crates/tui/src/tui/ui/tests.rs | 36 +++++++--- 2 files changed, 113 insertions(+), 33 deletions(-) diff --git a/crates/tui/src/commands/config.rs b/crates/tui/src/commands/config.rs index 9a10b786..5c0d30ae 100644 --- a/crates/tui/src/commands/config.rs +++ b/crates/tui/src/commands/config.rs @@ -120,6 +120,9 @@ fn show_single_setting(app: &App, key: &str) -> CommandResult { } "approval_mode" | "approval" => Some(app.approval_mode.label().to_string()), "locale" | "language" => Some(locale_display(app.ui_locale).to_string()), + "theme" | "ui_theme" => { + Some(crate::palette::theme_label_for_mode(app.ui_theme.mode).to_string()) + } "background_color" | "background" | "bg" => { crate::palette::hex_rgb_string(app.ui_theme.surface_bg) .or_else(|| Some("(default)".to_string())) @@ -418,13 +421,11 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> app.ui_locale = resolve_locale(&settings.locale); app.needs_redraw = true; } - "background_color" | "background" | "bg" => { - let base_theme = crate::palette::UiTheme::detect(); - app.ui_theme = settings - .background_color - .as_deref() - .and_then(crate::palette::parse_hex_rgb_color) - .map_or(base_theme, |color| base_theme.with_background_color(color)); + "theme" | "ui_theme" | "background_color" | "background" | "bg" => { + app.ui_theme = crate::palette::ui_theme_from_settings( + &settings.theme, + settings.background_color.as_deref(), + ); app.needs_redraw = true; } "cost_currency" | "currency" => { @@ -487,6 +488,7 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> let display_value = match key.as_str() { "default_mode" | "mode" => settings.default_mode.clone(), "cost_currency" | "currency" => settings.cost_currency.clone(), + "theme" | "ui_theme" => settings.theme.clone(), "background_color" | "background" | "bg" => settings .background_color .clone() @@ -580,22 +582,34 @@ fn mode_display_name(mode: AppMode) -> &'static str { } } -/// Toggle between dark and light theme. -pub fn theme(app: &mut App) -> CommandResult { - let new_theme = match app.ui_theme.mode { - crate::palette::PaletteMode::Dark => { - crate::palette::UiTheme::for_mode(crate::palette::PaletteMode::Light) - } - crate::palette::PaletteMode::Light => { - crate::palette::UiTheme::for_mode(crate::palette::PaletteMode::Dark) +/// Switch the runtime theme. `/set theme --save` persists it. +pub fn theme(app: &mut App, arg: Option<&str>) -> CommandResult { + let requested = match arg.map(str::trim).filter(|value| !value.is_empty()) { + Some(value) => { + let Some(theme) = crate::palette::normalize_theme_name(value) else { + return CommandResult::error("Usage: /theme [dark|light|grayscale|system]"); + }; + theme } + None => match app.ui_theme.mode { + crate::palette::PaletteMode::Dark => "light", + crate::palette::PaletteMode::Light => "grayscale", + crate::palette::PaletteMode::Grayscale => "dark", + }, }; - app.ui_theme = new_theme; - let label = match new_theme.mode { - crate::palette::PaletteMode::Dark => "dark", - crate::palette::PaletteMode::Light => "light", - }; - CommandResult::message(format!("Theme switched to {label}.")) + + let background = Settings::load() + .ok() + .and_then(|settings| settings.background_color); + app.ui_theme = crate::palette::ui_theme_from_settings(requested, background.as_deref()); + app.needs_redraw = true; + + let label = crate::palette::theme_label_for_mode(app.ui_theme.mode); + if requested == "system" { + CommandResult::message(format!("Theme switched to system ({label}).")) + } else { + CommandResult::message(format!("Theme switched to {label}.")) + } } /// Manage workspace-level trust and the per-path allowlist. @@ -1178,6 +1192,9 @@ mod tests { #[test] fn test_mode_yolo_sets_all_flags() { let mut app = create_test_app(); + // Switch to Agent first to guarantee a clean starting state regardless of + // user settings on the host machine. + let _ = mode(&mut app, Some("agent")); let result = mode(&mut app, Some("yolo")); assert!(result.message.unwrap().contains("Switched to YOLO mode")); assert!(app.allow_shell); @@ -1443,6 +1460,54 @@ mod tests { assert!(saved.contains("cost_currency = \"cny\"")); } + #[test] + fn theme_command_accepts_grayscale_arg() { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let temp_root = env::temp_dir().join(format!( + "deepseek-tui-theme-command-test-{}-{}", + std::process::id(), + nanos + )); + fs::create_dir_all(&temp_root).unwrap(); + let _guard = EnvGuard::new(&temp_root); + + let mut app = create_test_app(); + let result = theme(&mut app, Some("grayscale")); + + assert_eq!(result.message.unwrap(), "Theme switched to grayscale."); + assert_eq!(app.ui_theme.mode, crate::palette::PaletteMode::Grayscale); + assert!(app.needs_redraw); + } + + #[test] + fn set_theme_save_updates_live_app_and_persists() { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let temp_root = env::temp_dir().join(format!( + "deepseek-tui-theme-save-test-{}-{}", + std::process::id(), + nanos + )); + fs::create_dir_all(&temp_root).unwrap(); + let _guard = EnvGuard::new(&temp_root); + + let mut app = create_test_app(); + let result = set_config(&mut app, Some("theme grayscale --save")); + let msg = result.message.unwrap(); + + assert_eq!(msg, "theme = grayscale (saved)"); + assert_eq!(app.ui_theme.mode, crate::palette::PaletteMode::Grayscale); + + let settings_path = Settings::path().unwrap(); + let saved = fs::read_to_string(settings_path).unwrap(); + assert!(saved.contains("theme = \"grayscale\"")); + } + #[test] fn test_set_approval_mode_valid_values() { let mut app = create_test_app(); @@ -1497,7 +1562,8 @@ mod tests { #[test] fn test_trust_on_enables_flag() { let mut app = create_test_app(); - assert!(!app.trust_mode); + // Normalize trust state regardless of user settings on the host machine. + app.trust_mode = false; let result = trust(&mut app, Some("on")); let msg = result.message.expect("message"); assert!(msg.contains("Workspace trust mode enabled")); diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 9c2b0157..ea835923 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -361,7 +361,14 @@ fn selection_to_text_copies_rendered_transcript_block() { let selected = selection_to_text(&app).expect("selection text"); assert!(selected.contains("Note copy system"), "{selected:?}"); assert!(selected.contains("copy user"), "{selected:?}"); - assert!(selected.contains("copy thinking"), "{selected:?}"); + assert!( + !selected.contains("copy thinking"), + "raw completed thinking should stay out of live selection text: {selected:?}" + ); + assert!( + selected.contains("Ctrl+O"), + "selection should keep the reasoning detail affordance: {selected:?}" + ); assert!(selected.contains("tool output line"), "{selected:?}"); assert!(selected.contains("copy assistant"), "{selected:?}"); // #1163: tool-card middle lines are rendered with a `│ ` left rail @@ -1930,6 +1937,8 @@ fn event_poll_timeout_has_nonzero_floor() { fn footer_status_line_spans_show_mode_and_model_idle_and_active() { let mut app = create_test_app(); app.model = "deepseek-v4-flash".to_string(); + // Pin Agent mode regardless of user settings on the host machine. + let _ = app.set_mode(crate::tui::app::AppMode::Agent); let idle = spans_text(&footer_status_line_spans(&app, 60)); assert!(idle.contains("agent")); @@ -4054,9 +4063,8 @@ fn open_thinking_pager_finds_thinking_in_active_cell() { // After ThinkingComplete fires, the finalized thinking entry stays in // `app.active_cell` with `streaming = false` until the active cell is // flushed to history (end-of-turn, or when an assistant text arrives). - // During that window the transcript still renders the - // "thinking collapsed; Ctrl+O opens Activity Detail" affordance from - // `render_thinking`, so the handler must reach across the virtual + // During that window the transcript still renders the Ctrl+O affordance + // from `render_thinking`, so the handler must reach across the virtual // transcript — not just `app.history` — or the promise is a lie. // Regression guard for the v0.8.29 affordance/handler mismatch. let mut app = create_test_app(); @@ -4083,10 +4091,14 @@ fn open_thinking_pager_finds_thinking_in_active_cell() { Some(ModalKind::Pager), "pager must open for thinking entries still in active_cell" ); + let body = pop_pager_body(&mut app); + assert!(body.contains("Activity: reasoning timeline"), "{body}"); + assert!(body.contains("Thinking chunk 1 of 1"), "{body}"); + assert!(body.contains("deliberating"), "{body}"); } #[test] -fn activity_detail_opens_selected_thinking_chunk() { +fn activity_detail_opens_reasoning_timeline_for_selected_thinking() { let mut app = create_test_app(); app.history = vec![ HistoryCell::Thinking { @@ -4124,17 +4136,19 @@ fn activity_detail_opens_selected_thinking_chunk() { let body = pop_pager_body(&mut app); assert!( - body.contains("Activity: thinking"), + body.contains("Activity: reasoning timeline"), "activity label missing: {body}" ); assert!( - body.contains("Thinking chunk: 1 of 2"), + body.contains("Selected chunk: 1 of 2"), "chunk position missing: {body}" ); + assert!(body.contains("Thinking chunk 1 of 2 (selected)"), "{body}"); + assert!(body.contains("Thinking chunk 2 of 2"), "{body}"); assert!(body.contains("first chunk reasoning"), "body: {body}"); assert!( - !body.contains("second chunk reasoning"), - "selected chunk should not fall through to latest thinking: {body}" + body.contains("second chunk reasoning"), + "timeline should include the whole session's thinking: {body}" ); } @@ -4949,7 +4963,7 @@ fn composer_arrows_scroll_empty_up() { false, false, )); - assert_eq!(app.viewport.pending_scroll_delta, -1); + assert_eq!(app.viewport.pending_scroll_delta, -3); assert!(app.input.is_empty()); } @@ -4964,7 +4978,7 @@ fn composer_arrows_scroll_empty_down() { false, false, )); - assert_eq!(app.viewport.pending_scroll_delta, 1); + assert_eq!(app.viewport.pending_scroll_delta, 3); } #[test]