Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:33:28.167Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

visual_test_runner.rs

5212 lines · 183.3 KB · rust
1// Allow blocking process commands in this binary - it's a synchronous test runner
2#![allow(clippy::disallowed_methods)]
3
4//! Visual Test Runner
5//!
6//! This binary runs visual regression tests for Zed's UI. It captures screenshots
7//! of real Zed windows and compares them against baseline images.
8//!
9//! **Note: This tool is macOS-only** because it uses `VisualTestAppContext` which
10//! depends on the macOS Metal renderer for accurate screenshot capture.
11//!
12//! ## How It Works
13//!
14//! This tool uses `VisualTestAppContext` which combines:
15//! - Real Metal/compositor rendering for accurate screenshots
16//! - Deterministic task scheduling via TestDispatcher
17//! - Controllable time via `advance_clock` for testing time-based behaviors
18//!
19//! This approach:
20//! - Does NOT require Screen Recording permission
21//! - Does NOT require the window to be visible on screen
22//! - Captures raw GPUI output without system window chrome
23//! - Is fully deterministic - tooltips, animations, etc. work reliably
24//!
25//! ## Usage
26//!
27//! Run the visual tests:
28//!   cargo run -p zed --bin zed_visual_test_runner --features visual-tests
29//!
30//! Update baseline images (when UI intentionally changes):
31//!   UPDATE_BASELINE=1 cargo run -p zed --bin zed_visual_test_runner --features visual-tests
32//!
33//! ## Environment Variables
34//!
35//!   UPDATE_BASELINE - Set to update baseline images instead of comparing
36//!   VISUAL_TEST_OUTPUT_DIR - Directory to save test output (default: target/visual_tests)
37
38// Stub main for non-macOS platforms
39#[cfg(not(target_os = "macos"))]
40fn main() {
41    eprintln!("Visual test runner is only supported on macOS");
42    std::process::exit(1);
43}
44
45#[cfg(target_os = "macos")]
46fn main() {
47    // Set ZED_STATELESS early to prevent file system access to real config directories
48    // This must be done before any code accesses zed_env_vars::ZED_STATELESS
49    // SAFETY: We're at the start of main(), before any threads are spawned
50    unsafe {
51        std::env::set_var("ZED_STATELESS", "1");
52    }
53
54    // Redirect every derived data path into a temporary directory. A visual
55    // test that exercises real on-disk state — omega#81's harness pins and
56    // receipts do — would otherwise write into the developer's own Omega
57    // installation. Set before anything can read `data_dir`.
58    //
59    // `OMEGA_VISUAL_DATA_DIR` overrides the temporary directory so that two
60    // *processes* can share one data directory. That is what makes omega#77's
61    // restart captures a restart: the second process is a genuinely cold one —
62    // empty statics, nothing carried in memory — and the only thing it has of
63    // the first is what the first left on disk. It is still never the
64    // developer's own data directory; `script/omega-visual-proof` creates a
65    // temporary one and passes it in.
66    let data_dir = match std::env::var("OMEGA_VISUAL_DATA_DIR") {
67        Ok(path) if !path.is_empty() => std::path::PathBuf::from(path),
68        _ => tempfile::tempdir()
69            .expect("Failed to create data directory")
70            .keep(),
71    };
72    std::fs::create_dir_all(&data_dir).expect("Failed to create data directory");
73    paths::set_custom_data_dir(data_dir.to_str().expect("Data directory path is not UTF-8"));
74
75    env_logger::builder()
76        .filter_level(log::LevelFilter::Info)
77        .init();
78
79    let update_baseline = std::env::var("UPDATE_BASELINE").is_ok();
80
81    // Create a temporary directory for test files
82    // Canonicalize the path to resolve symlinks (on macOS, /var -> /private/var)
83    // which prevents "path does not exist" errors during worktree scanning
84    // Use keep() to prevent auto-cleanup - background worktree tasks may still be running
85    // when tests complete, so we let the OS clean up temp directories on process exit
86    let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
87    let temp_path = temp_dir.keep();
88    let canonical_temp = temp_path
89        .canonicalize()
90        .expect("Failed to canonicalize temp directory");
91    let project_path = canonical_temp.join("project");
92    std::fs::create_dir_all(&project_path).expect("Failed to create project directory");
93
94    // Create test files in the real filesystem
95    create_test_files(&project_path);
96
97    let test_result = std::panic::catch_unwind(|| run_visual_tests(project_path, update_baseline));
98
99    // Note: We don't delete temp_path here because background worktree tasks may still
100    // be running. The directory will be cleaned up when the process exits or by the OS.
101
102    match test_result {
103        Ok(Ok(())) => {}
104        Ok(Err(e)) => {
105            // `{:#}` and not `{}`: every failure here is wrapped in the name of
106            // the suite that produced it, and `{}` prints only that wrapper.
107            // A run that failed because a restored record named the wrong run
108            // would report "omega_agent_surfaces" and nothing else, which is
109            // the same class of unreadable failure this file already carries a
110            // comment about.
111            eprintln!("Visual tests failed: {:#}", e);
112            std::process::exit(1);
113        }
114        Err(_) => {
115            eprintln!("Visual tests panicked");
116            std::process::exit(1);
117        }
118    }
119}
120
121// All macOS-specific imports grouped together
122#[cfg(target_os = "macos")]
123use {
124    acp_thread::{AgentConnection, StubAgentConnection},
125    agent_client_protocol::schema::v1 as acp,
126    agent_servers::{AgentServer, AgentServerDelegate},
127    agent_ui::Agent,
128    anyhow::{Context as _, Result},
129    assets::Assets,
130    editor::display_map::DisplayRow,
131    feature_flags::FeatureFlagAppExt as _,
132    git_ui::project_diff::ProjectDiff,
133    gpui::{
134        App, AppContext as _, Bounds, Entity, KeyBinding, Modifiers, VisualTestAppContext,
135        WindowBounds, WindowHandle, WindowOptions, point, px, size,
136    },
137    image::RgbaImage,
138    project::{AgentId, Project},
139    project_panel::ProjectPanel,
140    settings::{NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, Settings as _},
141    settings_ui::SettingsWindow,
142    std::{
143        any::Any,
144        path::{Path, PathBuf},
145        rc::Rc,
146        sync::Arc,
147        time::Duration,
148    },
149    util::ResultExt as _,
150    workspace::{AppState, MultiWorkspace, Workspace},
151    zed_actions::OpenSettingsAt,
152};
153
154// All macOS-specific constants grouped together
155#[cfg(target_os = "macos")]
156mod constants {
157    use std::time::Duration;
158
159    /// Baseline images are stored relative to this file
160    pub const BASELINE_DIR: &str = "crates/zed/test_fixtures/visual_tests";
161
162    /// Embedded test image (Zed app icon) for visual tests.
163    pub const EMBEDDED_TEST_IMAGE: &[u8] = include_bytes!("../resources/app-icon.png");
164
165    /// Threshold for image comparison (0.0 to 1.0)
166    /// Images must match at least this percentage to pass
167    pub const MATCH_THRESHOLD: f64 = 0.99;
168
169    /// Tooltip show delay - must match TOOLTIP_SHOW_DELAY in gpui/src/elements/div.rs
170    pub const TOOLTIP_SHOW_DELAY: Duration = Duration::from_millis(500);
171}
172
173#[cfg(target_os = "macos")]
174use constants::*;
175
176#[cfg(target_os = "macos")]
177fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()> {
178    // Create the visual test context with deterministic task scheduling
179    // Use real Assets so that SVG icons render properly
180    let mut cx = VisualTestAppContext::with_asset_source(
181        gpui_platform::current_platform(false),
182        Arc::new(Assets),
183    );
184
185    // Load embedded fonts (IBM Plex Sans, Lilex, etc.) so UI renders with correct fonts
186    cx.update(|cx| {
187        Assets.load_fonts(cx).unwrap();
188    });
189
190    // Initialize settings store with real default settings (not test settings)
191    // Test settings use Courier font, but we want the real Zed fonts for visual tests
192    cx.update(|cx| {
193        settings::init(cx);
194    });
195
196    // Create AppState using the test initialization
197    let app_state = cx.update(|cx| init_app_state(cx));
198
199    // Set the global app state so settings_ui and other subsystems can find it
200    cx.update(|cx| {
201        AppState::set_global(app_state.clone(), cx);
202    });
203
204    // Initialize all Zed subsystems
205    cx.update(|cx| {
206        gpui_tokio::init(cx);
207        theme_settings::init(theme::LoadThemes::JustBase, cx);
208        client::init(&app_state.client, cx);
209        audio::init(cx);
210        workspace::init(app_state.clone(), cx);
211        release_channel::init(semver::Version::new(0, 0, 0), cx);
212        command_palette::init(cx);
213        editor::init(cx);
214        call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
215        title_bar::init(cx);
216        project_panel::init(cx);
217        outline_panel::init(cx);
218        terminal_view::init(cx);
219        image_viewer::init(cx);
220        search::init(cx);
221        lsp_locations::init(cx);
222        cx.set_global(workspace::PaneSearchBarCallbacks {
223            setup_search_bar: |languages, toolbar, window, cx| {
224                let search_bar = cx.new(|cx| search::BufferSearchBar::new(languages, window, cx));
225                toolbar.update(cx, |toolbar, cx| {
226                    toolbar.add_item(search_bar, window, cx);
227                });
228            },
229            wrap_div_with_search_actions: search::buffer_search::register_pane_search_actions,
230        });
231        prompt_store::init(cx);
232        let prompt_builder = prompt_store::PromptBuilder::load(app_state.fs.clone(), false, cx);
233        language_model::init(cx);
234        client::RefreshLlmTokenListener::register(
235            app_state.client.clone(),
236            app_state.user_store.clone(),
237            cx,
238        );
239        language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
240        git_ui::init(cx);
241        project::AgentRegistryStore::init_global(
242            cx,
243            app_state.fs.clone(),
244            app_state.client.http_client(),
245        );
246        agent_ui::init(
247            app_state.fs.clone(),
248            prompt_builder,
249            app_state.languages.clone(),
250            true,
251            false,
252            cx,
253        );
254        settings_ui::init(cx);
255
256        // Load default keymaps so tooltips can show keybindings like "f9" for ToggleBreakpoint
257        // We load a minimal set of editor keybindings needed for visual tests
258        cx.bind_keys([KeyBinding::new(
259            "f9",
260            editor::actions::ToggleBreakpoint,
261            Some("Editor"),
262        )]);
263
264        // Disable agent notifications during visual tests to avoid popup windows
265        agent_settings::AgentSettings::override_global(
266            agent_settings::AgentSettings {
267                notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
268                play_sound_when_agent_done: PlaySoundWhenAgentDone::Never,
269                ..agent_settings::AgentSettings::get_global(cx).clone()
270            },
271            cx,
272        );
273    });
274
275    // Run until all initialization tasks complete
276    cx.run_until_parked();
277
278    // Open workspace window
279    let window_size = size(px(1280.0), px(800.0));
280    let bounds = Bounds {
281        origin: point(px(0.0), px(0.0)),
282        size: window_size,
283    };
284
285    // Create a project for the workspace
286    let project = cx.update(|cx| {
287        project::Project::local(
288            app_state.client.clone(),
289            app_state.node_runtime.clone(),
290            app_state.user_store.clone(),
291            app_state.languages.clone(),
292            app_state.fs.clone(),
293            None,
294            project::LocalProjectFlags {
295                init_worktree_trust: false,
296                ..Default::default()
297            },
298            cx,
299        )
300    });
301
302    let workspace_window: WindowHandle<Workspace> = cx
303        .update(|cx| {
304            cx.open_window(
305                WindowOptions {
306                    window_bounds: Some(WindowBounds::Windowed(bounds)),
307                    focus: false,
308                    show: false,
309                    ..Default::default()
310                },
311                |window, cx| {
312                    cx.new(|cx| {
313                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
314                    })
315                },
316            )
317        })
318        .context("Failed to open workspace window")?;
319
320    cx.run_until_parked();
321
322    // Add the test project as a worktree
323    let add_worktree_task = workspace_window
324        .update(&mut cx, |workspace, _window, cx| {
325            let project = workspace.project().clone();
326            project.update(cx, |project, cx| {
327                project.find_or_create_worktree(&project_path, true, cx)
328            })
329        })
330        .context("Failed to start adding worktree")?;
331
332    // Use block_test to wait for the worktree task
333    // block_test runs both foreground and background tasks, which is needed because
334    // worktree creation spawns foreground tasks via cx.spawn
335    // Allow parking since filesystem operations happen outside the test dispatcher
336    cx.background_executor.allow_parking();
337    let worktree_result = cx.foreground_executor.block_test(add_worktree_task);
338    cx.background_executor.forbid_parking();
339    worktree_result.context("Failed to add worktree")?;
340
341    cx.run_until_parked();
342
343    // Create and add the project panel
344    let (weak_workspace, async_window_cx) = workspace_window
345        .update(&mut cx, |workspace, window, cx| {
346            (workspace.weak_handle(), window.to_async(cx))
347        })
348        .context("Failed to get workspace handle")?;
349
350    cx.background_executor.allow_parking();
351    let panel = cx
352        .foreground_executor
353        .block_test(ProjectPanel::load(weak_workspace, async_window_cx))
354        .context("Failed to load project panel")?;
355    cx.background_executor.forbid_parking();
356
357    workspace_window
358        .update(&mut cx, |workspace, window, cx| {
359            workspace.add_panel(panel, window, cx);
360        })
361        .log_err();
362
363    cx.run_until_parked();
364
365    // Open the project panel
366    workspace_window
367        .update(&mut cx, |workspace, window, cx| {
368            workspace.open_panel::<ProjectPanel>(window, cx);
369        })
370        .log_err();
371
372    cx.run_until_parked();
373
374    // Open main.rs in the editor
375    let open_file_task = workspace_window
376        .update(&mut cx, |workspace, window, cx| {
377            let worktree = workspace.project().read(cx).worktrees(cx).next();
378            if let Some(worktree) = worktree {
379                let worktree_id = worktree.read(cx).id();
380                let rel_path: std::sync::Arc<util::rel_path::RelPath> =
381                    util::rel_path::rel_path("src/main.rs").into();
382                let project_path: project::ProjectPath = (worktree_id, rel_path).into();
383                Some(workspace.open_path(project_path, None, true, window, cx))
384            } else {
385                None
386            }
387        })
388        .log_err()
389        .flatten();
390
391    if let Some(task) = open_file_task {
392        cx.background_executor.allow_parking();
393        let block_result = cx.foreground_executor.block_test(task);
394        cx.background_executor.forbid_parking();
395        if let Ok(item) = block_result {
396            workspace_window
397                .update(&mut cx, |workspace, window, cx| {
398                    let pane = workspace.active_pane().clone();
399                    pane.update(cx, |pane, cx| {
400                        if let Some(index) = pane.index_for_item(item.as_ref()) {
401                            pane.activate_item(index, true, true, window, cx);
402                        }
403                    });
404                })
405                .log_err();
406        }
407    }
408
409    cx.run_until_parked();
410
411    // Request a window refresh
412    cx.update_window(workspace_window.into(), |_, window, _cx| {
413        window.refresh();
414    })
415    .log_err();
416
417    cx.run_until_parked();
418
419    // Track test results
420    let mut passed = 0;
421    let mut failed = 0;
422    let mut updated = 0;
423
424    // `OMEGA_EXO_VISUAL_ONLY=1` runs the real Exo conversation workspace
425    // proof and nothing else. The lane file must name an isolated Exo install.
426    // This path connects over the shipped ACP stdio transport and sends a real
427    // turn before it captures the wide and narrow layouts.
428    #[cfg(feature = "visual-tests")]
429    if std::env::var("OMEGA_EXO_VISUAL_ONLY").is_ok() {
430        println!("\n--- Omega: real Exo conversation workspace ---");
431        let outcome = run_omega_exo_visual_tests(app_state.clone(), &mut cx, update_baseline);
432        teardown_shared_window(workspace_window, &mut cx);
433        return match outcome {
434            Ok(TestResult::Passed) => {
435                println!("\u{2713} omega_exo_workspace: PASSED");
436                Ok(())
437            }
438            Ok(TestResult::BaselineUpdated(_)) => {
439                println!("\u{2713} omega_exo_workspace: Baselines updated");
440                Ok(())
441            }
442            Err(error) => Err(error.context("omega_exo_workspace")),
443        };
444    }
445
446    // `OMEGA_VISUAL_ONLY=1` runs Omega's own suite and nothing else.
447    //
448    // This runner has no committed baselines for any of the inherited Zed
449    // tests, so a plain run reports "Baseline not found" for every one of them
450    // and Omega's result is buried in the noise. Generating baselines for all
451    // of them would be committing a large set of images nobody has looked at,
452    // which is the opposite of the point. So the Omega suite gets its own
453    // invocation, `script/omega-visual-proof`, and the inherited tests keep the
454    // status they already had: present, and not standing up.
455    #[cfg(feature = "visual-tests")]
456    if std::env::var("OMEGA_VISUAL_ONLY").is_ok() {
457        // `OMEGA_VISUAL_PHASE=restart` is the second process of omega#77's
458        // restart proof. It captures nothing the first process captured: it
459        // reopens two threads the first process left on disk and photographs
460        // the executor lines a cold process derives for them.
461        let restart_phase = std::env::var("OMEGA_VISUAL_PHASE").as_deref() == Ok("restart");
462        let outcome = if restart_phase {
463            println!("\n--- Omega: executor disclosure after a restart ---");
464            run_omega_restart_visual_tests(app_state.clone(), &mut cx, update_baseline)
465        } else {
466            println!("\n--- Omega: front door, executor disclosure, route pin ---");
467            run_omega_agent_visual_tests(app_state.clone(), &mut cx, update_baseline)
468        };
469        // The shared window this function opened above is torn down here as
470        // well as at the end of the full run. Returning early without it left
471        // the sample project's buffers alive, GPUI's leaked-handle check
472        // panicked at drop, and a green suite exited 101 — a script reporting
473        // failure on a run where every capture matched.
474        teardown_shared_window(workspace_window, &mut cx);
475        return match outcome {
476            Ok(TestResult::Passed) => {
477                println!("\u{2713} omega_agent_surfaces: PASSED");
478                Ok(())
479            }
480            Ok(TestResult::BaselineUpdated(_)) => {
481                println!("\u{2713} omega_agent_surfaces: Baselines updated");
482                Ok(())
483            }
484            Err(error) => Err(error.context("omega_agent_surfaces")),
485        };
486    }
487
488    // Run Test 1: Project Panel (with project panel visible)
489    println!("\n--- Test 1: project_panel ---");
490    match run_visual_test(
491        "project_panel",
492        workspace_window.into(),
493        &mut cx,
494        update_baseline,
495    ) {
496        Ok(TestResult::Passed) => {
497            println!("✓ project_panel: PASSED");
498            passed += 1;
499        }
500        Ok(TestResult::BaselineUpdated(_)) => {
501            println!("✓ project_panel: Baseline updated");
502            updated += 1;
503        }
504        Err(e) => {
505            eprintln!("✗ project_panel: FAILED - {}", e);
506            failed += 1;
507        }
508    }
509
510    // Run Test 2: Workspace with Editor
511    println!("\n--- Test 2: workspace_with_editor ---");
512
513    // Close project panel for this test
514    workspace_window
515        .update(&mut cx, |workspace, window, cx| {
516            workspace.close_panel::<ProjectPanel>(window, cx);
517        })
518        .log_err();
519
520    cx.run_until_parked();
521
522    match run_visual_test(
523        "workspace_with_editor",
524        workspace_window.into(),
525        &mut cx,
526        update_baseline,
527    ) {
528        Ok(TestResult::Passed) => {
529            println!("✓ workspace_with_editor: PASSED");
530            passed += 1;
531        }
532        Ok(TestResult::BaselineUpdated(_)) => {
533            println!("✓ workspace_with_editor: Baseline updated");
534            updated += 1;
535        }
536        Err(e) => {
537            eprintln!("✗ workspace_with_editor: FAILED - {}", e);
538            failed += 1;
539        }
540    }
541
542    // Run Test: ThreadItem branch names visual test
543    println!("\n--- Test: thread_item_branch_names ---");
544    match run_thread_item_branch_name_visual_tests(app_state.clone(), &mut cx, update_baseline) {
545        Ok(TestResult::Passed) => {
546            println!("✓ thread_item_branch_names: PASSED");
547            passed += 1;
548        }
549        Ok(TestResult::BaselineUpdated(_)) => {
550            println!("✓ thread_item_branch_names: Baseline updated");
551            updated += 1;
552        }
553        Err(e) => {
554            eprintln!("✗ thread_item_branch_names: FAILED - {}", e);
555            failed += 1;
556        }
557    }
558
559    // Run Test 3: Multi-workspace sidebar visual tests
560    println!("\n--- Test 3: multi_workspace_sidebar ---");
561    match run_multi_workspace_sidebar_visual_tests(app_state.clone(), &mut cx, update_baseline) {
562        Ok(TestResult::Passed) => {
563            println!("✓ multi_workspace_sidebar: PASSED");
564            passed += 1;
565        }
566        Ok(TestResult::BaselineUpdated(_)) => {
567            println!("✓ multi_workspace_sidebar: Baselines updated");
568            updated += 1;
569        }
570        Err(e) => {
571            eprintln!("✗ multi_workspace_sidebar: FAILED - {}", e);
572            failed += 1;
573        }
574    }
575
576    // Run Test 4: Error wrapping visual tests
577    println!("\n--- Test 4: error_message_wrapping ---");
578    match run_error_wrapping_visual_tests(app_state.clone(), &mut cx, update_baseline) {
579        Ok(TestResult::Passed) => {
580            println!("✓ error_message_wrapping: PASSED");
581            passed += 1;
582        }
583        Ok(TestResult::BaselineUpdated(_)) => {
584            println!("✓ error_message_wrapping: Baselines updated");
585            updated += 1;
586        }
587        Err(e) => {
588            eprintln!("✗ error_message_wrapping: FAILED - {}", e);
589            failed += 1;
590        }
591    }
592
593    // Omega's own rendered proofs: the front door with no project, typing
594    // starting a thread, the executor line on three thread kinds, and a pin
595    // that could not be honoured. omega#76, #77, #78.
596    #[cfg(feature = "visual-tests")]
597    {
598        println!("\n--- Omega: front door, executor disclosure, route pin ---");
599        match run_omega_agent_visual_tests(app_state.clone(), &mut cx, update_baseline) {
600            Ok(TestResult::Passed) => {
601                println!("\u{2713} omega_agent_surfaces: PASSED");
602                passed += 1;
603            }
604            Ok(TestResult::BaselineUpdated(_)) => {
605                println!("\u{2713} omega_agent_surfaces: Baselines updated");
606                updated += 1;
607            }
608            Err(e) => {
609                eprintln!("\u{2717} omega_agent_surfaces: FAILED - {}", e);
610                failed += 1;
611            }
612        }
613    }
614
615    // Run Test 5: Agent Thread View tests
616    #[cfg(feature = "visual-tests")]
617    {
618        println!("\n--- Test 5: agent_thread_with_image (collapsed + expanded) ---");
619        match run_agent_thread_view_test(app_state.clone(), &mut cx, update_baseline) {
620            Ok(TestResult::Passed) => {
621                println!("✓ agent_thread_with_image (collapsed + expanded): PASSED");
622                passed += 1;
623            }
624            Ok(TestResult::BaselineUpdated(_)) => {
625                println!("✓ agent_thread_with_image: Baselines updated (collapsed + expanded)");
626                updated += 1;
627            }
628            Err(e) => {
629                eprintln!("✗ agent_thread_with_image: FAILED - {}", e);
630                failed += 1;
631            }
632        }
633    }
634
635    // Run Test 6: Breakpoint Hover visual tests
636    println!("\n--- Test 6: breakpoint_hover (3 variants) ---");
637    match run_breakpoint_hover_visual_tests(app_state.clone(), &mut cx, update_baseline) {
638        Ok(TestResult::Passed) => {
639            println!("✓ breakpoint_hover: PASSED");
640            passed += 1;
641        }
642        Ok(TestResult::BaselineUpdated(_)) => {
643            println!("✓ breakpoint_hover: Baselines updated");
644            updated += 1;
645        }
646        Err(e) => {
647            eprintln!("✗ breakpoint_hover: FAILED - {}", e);
648            failed += 1;
649        }
650    }
651
652    // Run Test 7: Diff Review Button visual tests
653    println!("\n--- Test 7: diff_review_button (3 variants) ---");
654    match run_diff_review_visual_tests(app_state.clone(), &mut cx, update_baseline) {
655        Ok(TestResult::Passed) => {
656            println!("✓ diff_review_button: PASSED");
657            passed += 1;
658        }
659        Ok(TestResult::BaselineUpdated(_)) => {
660            println!("✓ diff_review_button: Baselines updated");
661            updated += 1;
662        }
663        Err(e) => {
664            eprintln!("✗ diff_review_button: FAILED - {}", e);
665            failed += 1;
666        }
667    }
668
669    // Run Test 8: ThreadItem icon decorations visual tests
670    println!("\n--- Test 8: thread_item_icon_decorations ---");
671    match run_thread_item_icon_decorations_visual_tests(app_state.clone(), &mut cx, update_baseline)
672    {
673        Ok(TestResult::Passed) => {
674            println!("✓ thread_item_icon_decorations: PASSED");
675            passed += 1;
676        }
677        Ok(TestResult::BaselineUpdated(_)) => {
678            println!("✓ thread_item_icon_decorations: Baseline updated");
679            updated += 1;
680        }
681        Err(e) => {
682            eprintln!("✗ thread_item_icon_decorations: FAILED - {}", e);
683            failed += 1;
684        }
685    }
686
687    // Run Test: Sidebar with duplicate project names
688    println!("\n--- Test: sidebar_duplicate_names ---");
689    match run_sidebar_duplicate_project_names_visual_tests(
690        app_state.clone(),
691        &mut cx,
692        update_baseline,
693    ) {
694        Ok(TestResult::Passed) => {
695            println!("✓ sidebar_duplicate_names: PASSED");
696            passed += 1;
697        }
698        Ok(TestResult::BaselineUpdated(_)) => {
699            println!("✓ sidebar_duplicate_names: Baselines updated");
700            updated += 1;
701        }
702        Err(e) => {
703            eprintln!("✗ sidebar_duplicate_names: FAILED - {}", e);
704            failed += 1;
705        }
706    }
707
708    // Run Test 9: Tool Permissions Settings UI visual test
709    println!("\n--- Test 9: tool_permissions_settings ---");
710    match run_tool_permissions_visual_tests(app_state.clone(), &mut cx, update_baseline) {
711        Ok(TestResult::Passed) => {
712            println!("✓ tool_permissions_settings: PASSED");
713            passed += 1;
714        }
715        Ok(TestResult::BaselineUpdated(_)) => {
716            println!("✓ tool_permissions_settings: Baselines updated");
717            updated += 1;
718        }
719        Err(e) => {
720            eprintln!("✗ tool_permissions_settings: FAILED - {}", e);
721            failed += 1;
722        }
723    }
724
725    // Run Test 10: Settings UI sub-page auto-open visual tests
726    println!("\n--- Test 10: settings_ui_subpage_auto_open (2 variants) ---");
727    match run_settings_ui_subpage_visual_tests(app_state.clone(), &mut cx, update_baseline) {
728        Ok(TestResult::Passed) => {
729            println!("✓ settings_ui_subpage_auto_open: PASSED");
730            passed += 1;
731        }
732        Ok(TestResult::BaselineUpdated(_)) => {
733            println!("✓ settings_ui_subpage_auto_open: Baselines updated");
734            updated += 1;
735        }
736        Err(e) => {
737            eprintln!("✗ settings_ui_subpage_auto_open: FAILED - {}", e);
738            failed += 1;
739        }
740    }
741
742    // Run Test 11: External agent harness maintenance (omega#81)
743    println!("\n--- Test 11: external_agent_harness_maintenance ---");
744    match run_external_agent_maintenance_visual_tests(app_state.clone(), &mut cx, update_baseline) {
745        Ok(TestResult::Passed) => {
746            println!("\u{2713} external_agent_harness_maintenance: PASSED");
747            passed += 1;
748        }
749        Ok(TestResult::BaselineUpdated(_)) => {
750            println!("\u{2713} external_agent_harness_maintenance: Baselines updated");
751            updated += 1;
752        }
753        Err(e) => {
754            eprintln!(
755                "\u{2717} external_agent_harness_maintenance: FAILED - {}",
756                e
757            );
758            failed += 1;
759        }
760    }
761
762    // Clean up the main workspace's worktree to stop background scanning tasks
763    // This prevents "root path could not be canonicalized" errors when main() drops temp_dir
764    teardown_shared_window(workspace_window, &mut cx);
765
766    // Print summary
767    println!("\n=== Test Summary ===");
768    println!("Passed: {}", passed);
769    println!("Failed: {}", failed);
770    if updated > 0 {
771        println!("Baselines Updated: {}", updated);
772    }
773
774    if failed > 0 {
775        eprintln!("\n=== Visual Tests FAILED ===");
776        Err(anyhow::anyhow!("{} tests failed", failed))
777    } else {
778        println!("\n=== All Visual Tests PASSED ===");
779        Ok(())
780    }
781}
782
783#[cfg(target_os = "macos")]
784enum TestResult {
785    Passed,
786    BaselineUpdated(PathBuf),
787}
788
789#[cfg(target_os = "macos")]
790fn run_visual_test(
791    test_name: &str,
792    window: gpui::AnyWindowHandle,
793    cx: &mut VisualTestAppContext,
794    update_baseline: bool,
795) -> Result<TestResult> {
796    // Ensure all pending work is done
797    cx.run_until_parked();
798
799    // Refresh the window to ensure it's fully rendered
800    cx.update_window(window, |_, window, _cx| {
801        window.refresh();
802    })?;
803
804    cx.run_until_parked();
805
806    // Capture the screenshot using direct texture capture
807    let screenshot = cx.capture_screenshot(window)?;
808
809    // Get paths
810    let baseline_path = get_baseline_path(test_name);
811    let output_dir = std::env::var("VISUAL_TEST_OUTPUT_DIR")
812        .unwrap_or_else(|_| "target/visual_tests".to_string());
813    let output_path = PathBuf::from(&output_dir).join(format!("{}.png", test_name));
814
815    // Ensure output directory exists
816    std::fs::create_dir_all(&output_dir)?;
817
818    // Always save the current screenshot
819    screenshot.save(&output_path)?;
820    println!("  Screenshot saved to: {}", output_path.display());
821
822    if update_baseline {
823        // Update the baseline
824        if let Some(parent) = baseline_path.parent() {
825            std::fs::create_dir_all(parent)?;
826        }
827        screenshot.save(&baseline_path)?;
828        println!("  Baseline updated: {}", baseline_path.display());
829        return Ok(TestResult::BaselineUpdated(baseline_path));
830    }
831
832    // Compare with baseline
833    if !baseline_path.exists() {
834        return Err(anyhow::anyhow!(
835            "Baseline not found: {}. Run with UPDATE_BASELINE=1 to create it.",
836            baseline_path.display()
837        ));
838    }
839
840    let baseline = image::open(&baseline_path)?.to_rgba8();
841    let comparison = compare_images(&screenshot, &baseline);
842
843    println!(
844        "  Match: {:.2}% ({} different pixels)",
845        comparison.match_percentage * 100.0,
846        comparison.diff_pixel_count
847    );
848
849    if comparison.match_percentage >= MATCH_THRESHOLD {
850        Ok(TestResult::Passed)
851    } else {
852        // Save diff image
853        let diff_path = PathBuf::from(&output_dir).join(format!("{}_diff.png", test_name));
854        comparison.diff_image.save(&diff_path)?;
855        println!("  Diff image saved to: {}", diff_path.display());
856
857        Err(anyhow::anyhow!(
858            "Image mismatch: {:.2}% match (threshold: {:.2}%)",
859            comparison.match_percentage * 100.0,
860            MATCH_THRESHOLD * 100.0
861        ))
862    }
863}
864
865/// Tear down the shared workspace window this runner opens for the inherited
866/// tests.
867///
868/// Extracted so the `OMEGA_VISUAL_ONLY` early return runs it too. GPUI checks
869/// for leaked entity handles when the app drops, and skipping this left the
870/// sample project's buffers alive — a green suite that exited 101.
871#[cfg(target_os = "macos")]
872fn teardown_shared_window(
873    workspace_window: WindowHandle<Workspace>,
874    cx: &mut VisualTestAppContext,
875) {
876    workspace_window
877        .update(cx, |workspace, _window, cx| {
878            let project = workspace.project().clone();
879            project.update(cx, |project, cx| {
880                let worktree_ids: Vec<_> =
881                    project.worktrees(cx).map(|wt| wt.read(cx).id()).collect();
882                for id in worktree_ids {
883                    project.remove_worktree(id, cx);
884                }
885            });
886        })
887        .log_err();
888
889    cx.run_until_parked();
890
891    cx.update_window(workspace_window.into(), |_, window, _cx| {
892        window.remove_window();
893    })
894    .log_err();
895
896    cx.run_until_parked();
897
898    // Background tasks, including scrollbar hide timers (1 second).
899    for _ in 0..15 {
900        cx.advance_clock(Duration::from_millis(100));
901        cx.run_until_parked();
902    }
903}
904
905#[cfg(target_os = "macos")]
906fn get_baseline_path(test_name: &str) -> PathBuf {
907    // Get the workspace root (where Cargo.toml is)
908    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
909    let workspace_root = PathBuf::from(manifest_dir)
910        .parent()
911        .and_then(|p| p.parent())
912        .map(|p| p.to_path_buf())
913        .unwrap_or_else(|| PathBuf::from("."));
914
915    workspace_root
916        .join(BASELINE_DIR)
917        .join(format!("{}.png", test_name))
918}
919
920#[cfg(target_os = "macos")]
921struct ImageComparison {
922    match_percentage: f64,
923    diff_image: RgbaImage,
924    diff_pixel_count: u32,
925    #[allow(dead_code)]
926    total_pixels: u32,
927}
928
929#[cfg(target_os = "macos")]
930fn compare_images(actual: &RgbaImage, expected: &RgbaImage) -> ImageComparison {
931    let width = actual.width().max(expected.width());
932    let height = actual.height().max(expected.height());
933    let total_pixels = width * height;
934
935    let mut diff_image = RgbaImage::new(width, height);
936    let mut matching_pixels = 0u32;
937
938    for y in 0..height {
939        for x in 0..width {
940            let actual_pixel = if x < actual.width() && y < actual.height() {
941                *actual.get_pixel(x, y)
942            } else {
943                image::Rgba([0, 0, 0, 0])
944            };
945
946            let expected_pixel = if x < expected.width() && y < expected.height() {
947                *expected.get_pixel(x, y)
948            } else {
949                image::Rgba([0, 0, 0, 0])
950            };
951
952            if pixels_are_similar(&actual_pixel, &expected_pixel) {
953                matching_pixels += 1;
954                // Semi-transparent green for matching pixels
955                diff_image.put_pixel(x, y, image::Rgba([0, 255, 0, 64]));
956            } else {
957                // Bright red for differing pixels
958                diff_image.put_pixel(x, y, image::Rgba([255, 0, 0, 255]));
959            }
960        }
961    }
962
963    let match_percentage = matching_pixels as f64 / total_pixels as f64;
964    let diff_pixel_count = total_pixels - matching_pixels;
965
966    ImageComparison {
967        match_percentage,
968        diff_image,
969        diff_pixel_count,
970        total_pixels,
971    }
972}
973
974#[cfg(target_os = "macos")]
975fn pixels_are_similar(a: &image::Rgba<u8>, b: &image::Rgba<u8>) -> bool {
976    const TOLERANCE: i16 = 2;
977    (a.0[0] as i16 - b.0[0] as i16).abs() <= TOLERANCE
978        && (a.0[1] as i16 - b.0[1] as i16).abs() <= TOLERANCE
979        && (a.0[2] as i16 - b.0[2] as i16).abs() <= TOLERANCE
980        && (a.0[3] as i16 - b.0[3] as i16).abs() <= TOLERANCE
981}
982
983#[cfg(target_os = "macos")]
984fn create_test_files(project_path: &Path) {
985    // Create src directory
986    let src_dir = project_path.join("src");
987    std::fs::create_dir_all(&src_dir).expect("Failed to create src directory");
988
989    // Create main.rs
990    let main_rs = r#"fn main() {
991    println!("Hello, world!");
992
993    let x = 42;
994    let y = x * 2;
995
996    if y > 50 {
997        println!("y is greater than 50");
998    } else {
999        println!("y is not greater than 50");
1000    }
1001
1002    for i in 0..10 {
1003        println!("i = {}", i);
1004    }
1005}
1006
1007fn helper_function(a: i32, b: i32) -> i32 {
1008    a + b
1009}
1010
1011struct MyStruct {
1012    field1: String,
1013    field2: i32,
1014}
1015
1016impl MyStruct {
1017    fn new(name: &str, value: i32) -> Self {
1018        Self {
1019            field1: name.to_string(),
1020            field2: value,
1021        }
1022    }
1023
1024    fn get_value(&self) -> i32 {
1025        self.field2
1026    }
1027}
1028"#;
1029    std::fs::write(src_dir.join("main.rs"), main_rs).expect("Failed to write main.rs");
1030
1031    // Create lib.rs
1032    let lib_rs = r#"//! A sample library for visual testing
1033
1034pub mod utils;
1035
1036/// A public function in the library
1037pub fn library_function() -> String {
1038    "Hello from lib".to_string()
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043    use super::*;
1044
1045    #[test]
1046    fn it_works() {
1047        assert_eq!(library_function(), "Hello from lib");
1048    }
1049}
1050"#;
1051    std::fs::write(src_dir.join("lib.rs"), lib_rs).expect("Failed to write lib.rs");
1052
1053    // Create utils.rs
1054    let utils_rs = r#"//! Utility functions
1055
1056/// Format a number with commas
1057pub fn format_number(n: u64) -> String {
1058    let s = n.to_string();
1059    let mut result = String::new();
1060    for (i, c) in s.chars().rev().enumerate() {
1061        if i > 0 && i % 3 == 0 {
1062            result.push(',');
1063        }
1064        result.push(c);
1065    }
1066    result.chars().rev().collect()
1067}
1068
1069/// Calculate fibonacci number
1070pub fn fibonacci(n: u32) -> u64 {
1071    match n {
1072        0 => 0,
1073        1 => 1,
1074        _ => fibonacci(n - 1) + fibonacci(n - 2),
1075    }
1076}
1077"#;
1078    std::fs::write(src_dir.join("utils.rs"), utils_rs).expect("Failed to write utils.rs");
1079
1080    // Create Cargo.toml
1081    let cargo_toml = r#"[package]
1082name = "test_project"
1083version = "0.1.0"
1084edition = "2021"
1085
1086[dependencies]
1087"#;
1088    std::fs::write(project_path.join("Cargo.toml"), cargo_toml)
1089        .expect("Failed to write Cargo.toml");
1090
1091    // Create README.md
1092    let readme = r#"# Test Project
1093
1094This is a test project for visual testing of Omega.
1095
1096## Features
1097
1098- Feature 1
1099- Feature 2
1100- Feature 3
1101
1102## Usage
1103
1104```bash
1105cargo run
1106```
1107"#;
1108    std::fs::write(project_path.join("README.md"), readme).expect("Failed to write README.md");
1109}
1110
1111#[cfg(target_os = "macos")]
1112fn init_app_state(cx: &mut App) -> Arc<AppState> {
1113    use fs::Fs;
1114    use node_runtime::NodeRuntime;
1115    use session::Session;
1116    use settings::SettingsStore;
1117
1118    if !cx.has_global::<SettingsStore>() {
1119        let settings_store = SettingsStore::test(cx);
1120        cx.set_global(settings_store);
1121    }
1122
1123    // Use the real filesystem instead of FakeFs so we can access actual files on disk
1124    let fs: Arc<dyn Fs> = Arc::new(fs::RealFs::new(None, cx.background_executor().clone()));
1125    <dyn Fs>::set_global(fs.clone(), cx);
1126
1127    let languages = Arc::new(language::LanguageRegistry::test(
1128        cx.background_executor().clone(),
1129    ));
1130    let clock = Arc::new(clock::FakeSystemClock::new());
1131    let http_client = http_client::FakeHttpClient::with_404_response();
1132    let client = client::Client::new(clock, http_client, cx);
1133    let session = cx.new(|cx| session::AppSession::new(Session::test(), cx));
1134    let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
1135    let workspace_store = cx.new(|cx| workspace::WorkspaceStore::new(client.clone(), cx));
1136
1137    theme_settings::init(theme::LoadThemes::JustBase, cx);
1138    client::init(&client, cx);
1139
1140    let app_state = Arc::new(AppState {
1141        client,
1142        fs,
1143        languages,
1144        user_store,
1145        workspace_store,
1146        node_runtime: NodeRuntime::unavailable(),
1147        build_window_options: |_, _| Default::default(),
1148        session,
1149    });
1150    AppState::set_global(app_state.clone(), cx);
1151    app_state
1152}
1153
1154/// Runs visual tests for breakpoint hover states in the editor gutter.
1155///
1156/// This test captures three states:
1157/// 1. Gutter with line numbers, no breakpoint hover (baseline)
1158/// 2. Gutter with breakpoint hover indicator (gray circle)
1159/// 3. Gutter with breakpoint hover AND tooltip
1160#[cfg(target_os = "macos")]
1161fn run_breakpoint_hover_visual_tests(
1162    app_state: Arc<AppState>,
1163    cx: &mut VisualTestAppContext,
1164    update_baseline: bool,
1165) -> Result<TestResult> {
1166    // Create a temporary directory with a simple test file
1167    let temp_dir = tempfile::tempdir()?;
1168    let temp_path = temp_dir.keep();
1169    let canonical_temp = temp_path.canonicalize()?;
1170    let project_path = canonical_temp.join("project");
1171    std::fs::create_dir_all(&project_path)?;
1172
1173    // Create a simple file with a few lines
1174    let src_dir = project_path.join("src");
1175    std::fs::create_dir_all(&src_dir)?;
1176
1177    let test_content = r#"fn main() {
1178    println!("Hello");
1179    let x = 42;
1180}
1181"#;
1182    std::fs::write(src_dir.join("test.rs"), test_content)?;
1183
1184    // Create a small window - just big enough to show gutter and a few lines
1185    let window_size = size(px(300.0), px(200.0));
1186    let bounds = Bounds {
1187        origin: point(px(0.0), px(0.0)),
1188        size: window_size,
1189    };
1190
1191    // Create project
1192    let project = cx.update(|cx| {
1193        project::Project::local(
1194            app_state.client.clone(),
1195            app_state.node_runtime.clone(),
1196            app_state.user_store.clone(),
1197            app_state.languages.clone(),
1198            app_state.fs.clone(),
1199            None,
1200            project::LocalProjectFlags {
1201                init_worktree_trust: false,
1202                ..Default::default()
1203            },
1204            cx,
1205        )
1206    });
1207
1208    // Open workspace window
1209    let workspace_window: WindowHandle<Workspace> = cx
1210        .update(|cx| {
1211            cx.open_window(
1212                WindowOptions {
1213                    window_bounds: Some(WindowBounds::Windowed(bounds)),
1214                    focus: false,
1215                    show: false,
1216                    ..Default::default()
1217                },
1218                |window, cx| {
1219                    cx.new(|cx| {
1220                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
1221                    })
1222                },
1223            )
1224        })
1225        .context("Failed to open breakpoint test window")?;
1226
1227    cx.run_until_parked();
1228
1229    // Add the project as a worktree
1230    let add_worktree_task = workspace_window
1231        .update(cx, |workspace, _window, cx| {
1232            let project = workspace.project().clone();
1233            project.update(cx, |project, cx| {
1234                project.find_or_create_worktree(&project_path, true, cx)
1235            })
1236        })
1237        .context("Failed to start adding worktree")?;
1238
1239    cx.background_executor.allow_parking();
1240    let worktree_result = cx.foreground_executor.block_test(add_worktree_task);
1241    cx.background_executor.forbid_parking();
1242    worktree_result.context("Failed to add worktree")?;
1243
1244    cx.run_until_parked();
1245
1246    // Open the test file
1247    let open_file_task = workspace_window
1248        .update(cx, |workspace, window, cx| {
1249            let worktree = workspace.project().read(cx).worktrees(cx).next();
1250            if let Some(worktree) = worktree {
1251                let worktree_id = worktree.read(cx).id();
1252                let rel_path: std::sync::Arc<util::rel_path::RelPath> =
1253                    util::rel_path::rel_path("src/test.rs").into();
1254                let project_path: project::ProjectPath = (worktree_id, rel_path).into();
1255                Some(workspace.open_path(project_path, None, true, window, cx))
1256            } else {
1257                None
1258            }
1259        })
1260        .log_err()
1261        .flatten();
1262
1263    if let Some(task) = open_file_task {
1264        cx.background_executor.allow_parking();
1265        cx.foreground_executor.block_test(task).log_err();
1266        cx.background_executor.forbid_parking();
1267    }
1268
1269    cx.run_until_parked();
1270
1271    // Wait for the editor to fully load
1272    for _ in 0..10 {
1273        cx.advance_clock(Duration::from_millis(100));
1274        cx.run_until_parked();
1275    }
1276
1277    // Refresh window
1278    cx.update_window(workspace_window.into(), |_, window, _cx| {
1279        window.refresh();
1280    })?;
1281
1282    cx.run_until_parked();
1283
1284    // Test 1: Gutter visible with line numbers, no breakpoint hover
1285    let test1_result = run_visual_test(
1286        "breakpoint_hover_none",
1287        workspace_window.into(),
1288        cx,
1289        update_baseline,
1290    )?;
1291
1292    // Test 2: Breakpoint hover indicator (circle) visible
1293    // The gutter is on the left side. We need to position the mouse over the gutter area
1294    // for line 1. The breakpoint indicator appears in the leftmost part of the gutter.
1295    //
1296    // The breakpoint hover requires multiple steps:
1297    // 1. Draw to register mouse listeners
1298    // 2. Mouse move to trigger gutter_hovered and create GutterHoverButton
1299    // 3. Wait 200ms for is_active to become true
1300    // 4. Draw again to render the indicator
1301    //
1302    // The gutter_position should be in the gutter area to trigger the gutter hover button.
1303    // The button_position should be directly over the breakpoint icon button for tooltip hover.
1304    // Based on debug output: button is at origin=(3.12, 66.5) with size=(14, 16)
1305    let gutter_position = point(px(30.0), px(85.0));
1306    let button_position = point(px(10.0), px(75.0)); // Center of the breakpoint button
1307
1308    // Step 1: Initial draw to register mouse listeners
1309    cx.update_window(workspace_window.into(), |_, window, cx| {
1310        window.draw(cx).clear(cx);
1311    })?;
1312    cx.run_until_parked();
1313
1314    // Step 2: Simulate mouse move into gutter area
1315    cx.simulate_mouse_move(
1316        workspace_window.into(),
1317        gutter_position,
1318        None,
1319        Modifiers::default(),
1320    );
1321
1322    // Step 3: Advance clock past 200ms debounce
1323    cx.advance_clock(Duration::from_millis(300));
1324    cx.run_until_parked();
1325
1326    // Step 4: Draw again to pick up the indicator state change
1327    cx.update_window(workspace_window.into(), |_, window, cx| {
1328        window.draw(cx).clear(cx);
1329    })?;
1330    cx.run_until_parked();
1331
1332    // Step 5: Another mouse move to keep hover state active
1333    cx.simulate_mouse_move(
1334        workspace_window.into(),
1335        gutter_position,
1336        None,
1337        Modifiers::default(),
1338    );
1339
1340    // Step 6: Final draw
1341    cx.update_window(workspace_window.into(), |_, window, cx| {
1342        window.draw(cx).clear(cx);
1343    })?;
1344    cx.run_until_parked();
1345
1346    let test2_result = run_visual_test(
1347        "breakpoint_hover_circle",
1348        workspace_window.into(),
1349        cx,
1350        update_baseline,
1351    )?;
1352
1353    // Test 3: Breakpoint hover with tooltip visible
1354    // The tooltip delay is 500ms (TOOLTIP_SHOW_DELAY constant)
1355    // We need to position the mouse directly over the breakpoint button for the tooltip to show.
1356    // The button hitbox is approximately at (3.12, 66.5) with size (14, 16).
1357
1358    // Move mouse directly over the button to trigger tooltip hover
1359    cx.simulate_mouse_move(
1360        workspace_window.into(),
1361        button_position,
1362        None,
1363        Modifiers::default(),
1364    );
1365
1366    // Draw to register the button's tooltip hover listener
1367    cx.update_window(workspace_window.into(), |_, window, cx| {
1368        window.draw(cx).clear(cx);
1369    })?;
1370    cx.run_until_parked();
1371
1372    // Move mouse over button again to trigger tooltip scheduling
1373    cx.simulate_mouse_move(
1374        workspace_window.into(),
1375        button_position,
1376        None,
1377        Modifiers::default(),
1378    );
1379
1380    // Advance clock past TOOLTIP_SHOW_DELAY (500ms)
1381    cx.advance_clock(TOOLTIP_SHOW_DELAY + Duration::from_millis(100));
1382    cx.run_until_parked();
1383
1384    // Draw to render the tooltip
1385    cx.update_window(workspace_window.into(), |_, window, cx| {
1386        window.draw(cx).clear(cx);
1387    })?;
1388    cx.run_until_parked();
1389
1390    // Refresh window
1391    cx.update_window(workspace_window.into(), |_, window, _cx| {
1392        window.refresh();
1393    })?;
1394
1395    cx.run_until_parked();
1396
1397    let test3_result = run_visual_test(
1398        "breakpoint_hover_tooltip",
1399        workspace_window.into(),
1400        cx,
1401        update_baseline,
1402    )?;
1403
1404    // Clean up: remove worktrees to stop background scanning
1405    workspace_window
1406        .update(cx, |workspace, _window, cx| {
1407            let project = workspace.project().clone();
1408            project.update(cx, |project, cx| {
1409                let worktree_ids: Vec<_> =
1410                    project.worktrees(cx).map(|wt| wt.read(cx).id()).collect();
1411                for id in worktree_ids {
1412                    project.remove_worktree(id, cx);
1413                }
1414            });
1415        })
1416        .log_err();
1417
1418    cx.run_until_parked();
1419
1420    // Close the window
1421    cx.update_window(workspace_window.into(), |_, window, _cx| {
1422        window.remove_window();
1423    })
1424    .log_err();
1425
1426    cx.run_until_parked();
1427
1428    // Give background tasks time to finish
1429    for _ in 0..15 {
1430        cx.advance_clock(Duration::from_millis(100));
1431        cx.run_until_parked();
1432    }
1433
1434    // Return combined result
1435    match (&test1_result, &test2_result, &test3_result) {
1436        (TestResult::Passed, TestResult::Passed, TestResult::Passed) => Ok(TestResult::Passed),
1437        (TestResult::BaselineUpdated(p), _, _)
1438        | (_, TestResult::BaselineUpdated(p), _)
1439        | (_, _, TestResult::BaselineUpdated(p)) => Ok(TestResult::BaselineUpdated(p.clone())),
1440    }
1441}
1442
1443/// Runs visual tests for the settings UI sub-page auto-open feature.
1444///
1445/// This test verifies that when opening settings via OpenSettingsAt with a path
1446/// that maps to a single SubPageLink, the sub-page is automatically opened.
1447///
1448/// This test captures two states:
1449/// 1. Settings opened with a path that maps to multiple items (no auto-open)
1450/// 2. Settings opened with a path that maps to a single SubPageLink (auto-opens sub-page)
1451#[cfg(target_os = "macos")]
1452fn run_settings_ui_subpage_visual_tests(
1453    app_state: Arc<AppState>,
1454    cx: &mut VisualTestAppContext,
1455    update_baseline: bool,
1456) -> Result<TestResult> {
1457    // Create a workspace window for dispatching actions
1458    let window_size = size(px(1280.0), px(800.0));
1459    let bounds = Bounds {
1460        origin: point(px(0.0), px(0.0)),
1461        size: window_size,
1462    };
1463
1464    let project = cx.update(|cx| {
1465        project::Project::local(
1466            app_state.client.clone(),
1467            app_state.node_runtime.clone(),
1468            app_state.user_store.clone(),
1469            app_state.languages.clone(),
1470            app_state.fs.clone(),
1471            None,
1472            project::LocalProjectFlags {
1473                init_worktree_trust: false,
1474                ..Default::default()
1475            },
1476            cx,
1477        )
1478    });
1479
1480    let workspace_window: WindowHandle<MultiWorkspace> = cx
1481        .update(|cx| {
1482            cx.open_window(
1483                WindowOptions {
1484                    window_bounds: Some(WindowBounds::Windowed(bounds)),
1485                    focus: false,
1486                    show: false,
1487                    ..Default::default()
1488                },
1489                |window, cx| {
1490                    let workspace = cx.new(|cx| {
1491                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
1492                    });
1493                    cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
1494                },
1495            )
1496        })
1497        .context("Failed to open workspace window")?;
1498
1499    cx.run_until_parked();
1500
1501    // Test 1: Open settings with a path that maps to multiple items (e.g., "agent")
1502    // This should NOT auto-open a sub-page since multiple items match
1503    workspace_window
1504        .update(cx, |_workspace, window, cx| {
1505            window.dispatch_action(
1506                Box::new(OpenSettingsAt {
1507                    path: "agent".to_string(),
1508                    target: None,
1509                }),
1510                cx,
1511            );
1512        })
1513        .context("Failed to dispatch OpenSettingsAt for multiple items")?;
1514
1515    cx.run_until_parked();
1516
1517    // Find the settings window
1518    let settings_window_1 = cx
1519        .update(|cx| {
1520            cx.windows()
1521                .into_iter()
1522                .find_map(|window| window.downcast::<SettingsWindow>())
1523        })
1524        .context("Settings window not found")?;
1525
1526    // Refresh and capture screenshot
1527    cx.update_window(settings_window_1.into(), |_, window, _cx| {
1528        window.refresh();
1529    })?;
1530    cx.run_until_parked();
1531
1532    let test1_result = run_visual_test(
1533        "settings_ui_no_auto_open",
1534        settings_window_1.into(),
1535        cx,
1536        update_baseline,
1537    )?;
1538
1539    // Close the settings window
1540    cx.update_window(settings_window_1.into(), |_, window, _cx| {
1541        window.remove_window();
1542    })
1543    .log_err();
1544    cx.run_until_parked();
1545
1546    // Test 2: Open settings with a path that maps to a single SubPageLink
1547    // "edit_predictions.providers" maps to the "Configure Providers" SubPageLink
1548    // This should auto-open the sub-page
1549    workspace_window
1550        .update(cx, |_workspace, window, cx| {
1551            window.dispatch_action(
1552                Box::new(OpenSettingsAt {
1553                    path: "edit_predictions.providers".to_string(),
1554                    target: None,
1555                }),
1556                cx,
1557            );
1558        })
1559        .context("Failed to dispatch OpenSettingsAt for single SubPageLink")?;
1560
1561    cx.run_until_parked();
1562
1563    // Find the new settings window
1564    let settings_window_2 = cx
1565        .update(|cx| {
1566            cx.windows()
1567                .into_iter()
1568                .find_map(|window| window.downcast::<SettingsWindow>())
1569        })
1570        .context("Settings window not found for sub-page test")?;
1571
1572    // Refresh and capture screenshot
1573    cx.update_window(settings_window_2.into(), |_, window, _cx| {
1574        window.refresh();
1575    })?;
1576    cx.run_until_parked();
1577
1578    let test2_result = run_visual_test(
1579        "settings_ui_subpage_auto_open",
1580        settings_window_2.into(),
1581        cx,
1582        update_baseline,
1583    )?;
1584
1585    // Clean up: close the settings window
1586    cx.update_window(settings_window_2.into(), |_, window, _cx| {
1587        window.remove_window();
1588    })
1589    .log_err();
1590    cx.run_until_parked();
1591
1592    // Clean up: close the workspace window
1593    cx.update_window(workspace_window.into(), |_, window, _cx| {
1594        window.remove_window();
1595    })
1596    .log_err();
1597    cx.run_until_parked();
1598
1599    // Give background tasks time to finish
1600    for _ in 0..5 {
1601        cx.advance_clock(Duration::from_millis(100));
1602        cx.run_until_parked();
1603    }
1604
1605    // Return combined result
1606    match (&test1_result, &test2_result) {
1607        (TestResult::Passed, TestResult::Passed) => Ok(TestResult::Passed),
1608        (TestResult::BaselineUpdated(p), _) | (_, TestResult::BaselineUpdated(p)) => {
1609            Ok(TestResult::BaselineUpdated(p.clone()))
1610        }
1611    }
1612}
1613
1614/// Runs visual tests for the diff review button in git diff views.
1615///
1616/// This test captures three states:
1617/// 1. Diff view with feature flag enabled (button visible)
1618/// 2. Diff view with feature flag disabled (no button)
1619/// 3. Regular editor with feature flag enabled (no button - only shows in diff views)
1620#[cfg(target_os = "macos")]
1621fn run_diff_review_visual_tests(
1622    app_state: Arc<AppState>,
1623    cx: &mut VisualTestAppContext,
1624    update_baseline: bool,
1625) -> Result<TestResult> {
1626    // Create a temporary directory with test files and a real git repo
1627    let temp_dir = tempfile::tempdir()?;
1628    let temp_path = temp_dir.keep();
1629    let canonical_temp = temp_path.canonicalize()?;
1630    let project_path = canonical_temp.join("project");
1631    std::fs::create_dir_all(&project_path)?;
1632
1633    // Initialize a real git repository
1634    std::process::Command::new("git")
1635        .args(["init"])
1636        .current_dir(&project_path)
1637        .output()?;
1638
1639    // Configure git user for commits
1640    std::process::Command::new("git")
1641        .args(["config", "user.email", "test@test.com"])
1642        .current_dir(&project_path)
1643        .output()?;
1644    std::process::Command::new("git")
1645        .args(["config", "user.name", "Test User"])
1646        .current_dir(&project_path)
1647        .output()?;
1648
1649    // Create a test file with original content
1650    let original_content = "// Original content\n";
1651    std::fs::write(project_path.join("thread-view.tsx"), original_content)?;
1652
1653    // Commit the original file
1654    std::process::Command::new("git")
1655        .args(["add", "thread-view.tsx"])
1656        .current_dir(&project_path)
1657        .output()?;
1658    std::process::Command::new("git")
1659        .args(["commit", "-m", "Initial commit"])
1660        .current_dir(&project_path)
1661        .output()?;
1662
1663    // Modify the file to create a diff
1664    let modified_content = r#"import { ScrollArea } from 'components';
1665import { ButtonAlt, Tooltip } from 'ui';
1666import { Message, FileEdit } from 'types';
1667import { AiPaneTabContext } from 'context';
1668"#;
1669    std::fs::write(project_path.join("thread-view.tsx"), modified_content)?;
1670
1671    // Create window for the diff view - sized to show just the editor
1672    let window_size = size(px(600.0), px(400.0));
1673    let bounds = Bounds {
1674        origin: point(px(0.0), px(0.0)),
1675        size: window_size,
1676    };
1677
1678    // Create project
1679    let project = cx.update(|cx| {
1680        project::Project::local(
1681            app_state.client.clone(),
1682            app_state.node_runtime.clone(),
1683            app_state.user_store.clone(),
1684            app_state.languages.clone(),
1685            app_state.fs.clone(),
1686            None,
1687            project::LocalProjectFlags {
1688                init_worktree_trust: false,
1689                ..Default::default()
1690            },
1691            cx,
1692        )
1693    });
1694
1695    // Add the test directory as a worktree
1696    let add_worktree_task = project.update(cx, |project, cx| {
1697        project.find_or_create_worktree(&project_path, true, cx)
1698    });
1699
1700    cx.background_executor.allow_parking();
1701    cx.foreground_executor
1702        .block_test(add_worktree_task)
1703        .log_err();
1704    cx.background_executor.forbid_parking();
1705
1706    cx.run_until_parked();
1707
1708    // Wait for worktree to be fully scanned and git status to be detected
1709    for _ in 0..5 {
1710        cx.advance_clock(Duration::from_millis(100));
1711        cx.run_until_parked();
1712    }
1713
1714    // Test 1: Diff view with feature flag enabled
1715    // Enable the feature flag
1716    cx.update(|cx| {
1717        cx.update_flags(true, vec!["diff-review".to_string()]);
1718    });
1719
1720    let workspace_window: WindowHandle<Workspace> = cx
1721        .update(|cx| {
1722            cx.open_window(
1723                WindowOptions {
1724                    window_bounds: Some(WindowBounds::Windowed(bounds)),
1725                    focus: false,
1726                    show: false,
1727                    ..Default::default()
1728                },
1729                |window, cx| {
1730                    cx.new(|cx| {
1731                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
1732                    })
1733                },
1734            )
1735        })
1736        .context("Failed to open diff review test window")?;
1737
1738    cx.run_until_parked();
1739
1740    // Create and add the ProjectDiff using the public deploy_at method
1741    workspace_window
1742        .update(cx, |workspace, window, cx| {
1743            ProjectDiff::deploy_at(workspace, None, window, cx);
1744        })
1745        .log_err();
1746
1747    // Wait for diff to render
1748    for _ in 0..5 {
1749        cx.advance_clock(Duration::from_millis(100));
1750        cx.run_until_parked();
1751    }
1752
1753    // Refresh window
1754    cx.update_window(workspace_window.into(), |_, window, _cx| {
1755        window.refresh();
1756    })?;
1757
1758    cx.run_until_parked();
1759
1760    // Capture Test 1: Diff with flag enabled
1761    let test1_result = run_visual_test(
1762        "diff_review_button_enabled",
1763        workspace_window.into(),
1764        cx,
1765        update_baseline,
1766    )?;
1767
1768    // Test 2: Diff view with feature flag disabled
1769    // Disable the feature flag
1770    cx.update(|cx| {
1771        cx.update_flags(false, vec![]);
1772    });
1773
1774    // Refresh window
1775    cx.update_window(workspace_window.into(), |_, window, _cx| {
1776        window.refresh();
1777    })?;
1778
1779    for _ in 0..3 {
1780        cx.advance_clock(Duration::from_millis(100));
1781        cx.run_until_parked();
1782    }
1783
1784    // Capture Test 2: Diff with flag disabled
1785    let test2_result = run_visual_test(
1786        "diff_review_button_disabled",
1787        workspace_window.into(),
1788        cx,
1789        update_baseline,
1790    )?;
1791
1792    // Test 3: Regular editor with flag enabled (should NOT show button)
1793    // Re-enable the feature flag
1794    cx.update(|cx| {
1795        cx.update_flags(true, vec!["diff-review".to_string()]);
1796    });
1797
1798    // Create a new window with just a regular editor
1799    let regular_window: WindowHandle<Workspace> = cx
1800        .update(|cx| {
1801            cx.open_window(
1802                WindowOptions {
1803                    window_bounds: Some(WindowBounds::Windowed(bounds)),
1804                    focus: false,
1805                    show: false,
1806                    ..Default::default()
1807                },
1808                |window, cx| {
1809                    cx.new(|cx| {
1810                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
1811                    })
1812                },
1813            )
1814        })
1815        .context("Failed to open regular editor window")?;
1816
1817    cx.run_until_parked();
1818
1819    // Open a regular file (not a diff view)
1820    let open_file_task = regular_window
1821        .update(cx, |workspace, window, cx| {
1822            let worktree = workspace.project().read(cx).worktrees(cx).next();
1823            if let Some(worktree) = worktree {
1824                let worktree_id = worktree.read(cx).id();
1825                let rel_path: std::sync::Arc<util::rel_path::RelPath> =
1826                    util::rel_path::rel_path("thread-view.tsx").into();
1827                let project_path: project::ProjectPath = (worktree_id, rel_path).into();
1828                Some(workspace.open_path(project_path, None, true, window, cx))
1829            } else {
1830                None
1831            }
1832        })
1833        .log_err()
1834        .flatten();
1835
1836    if let Some(task) = open_file_task {
1837        cx.background_executor.allow_parking();
1838        cx.foreground_executor.block_test(task).log_err();
1839        cx.background_executor.forbid_parking();
1840    }
1841
1842    // Wait for file to open
1843    for _ in 0..3 {
1844        cx.advance_clock(Duration::from_millis(100));
1845        cx.run_until_parked();
1846    }
1847
1848    // Refresh window
1849    cx.update_window(regular_window.into(), |_, window, _cx| {
1850        window.refresh();
1851    })?;
1852
1853    cx.run_until_parked();
1854
1855    // Capture Test 3: Regular editor with flag enabled (no button)
1856    let test3_result = run_visual_test(
1857        "diff_review_button_regular_editor",
1858        regular_window.into(),
1859        cx,
1860        update_baseline,
1861    )?;
1862
1863    // Test 4: Show the diff review overlay on the regular editor
1864    regular_window
1865        .update(cx, |workspace, window, cx| {
1866            // Get the first editor from the workspace
1867            let editors: Vec<_> = workspace.items_of_type::<editor::Editor>(cx).collect();
1868            if let Some(editor) = editors.into_iter().next() {
1869                editor.update(cx, |editor, cx| {
1870                    editor.show_diff_review_overlay(DisplayRow(1)..DisplayRow(1), window, cx);
1871                });
1872            }
1873        })
1874        .log_err();
1875
1876    // Wait for overlay to render
1877    for _ in 0..3 {
1878        cx.advance_clock(Duration::from_millis(100));
1879        cx.run_until_parked();
1880    }
1881
1882    // Refresh window
1883    cx.update_window(regular_window.into(), |_, window, _cx| {
1884        window.refresh();
1885    })?;
1886
1887    cx.run_until_parked();
1888
1889    // Capture Test 4: Regular editor with overlay shown
1890    let test4_result = run_visual_test(
1891        "diff_review_overlay_shown",
1892        regular_window.into(),
1893        cx,
1894        update_baseline,
1895    )?;
1896
1897    // Test 5: Type text into the diff review prompt and submit it
1898    // First, get the prompt editor from the overlay and type some text
1899    regular_window
1900        .update(cx, |workspace, window, cx| {
1901            let editors: Vec<_> = workspace.items_of_type::<editor::Editor>(cx).collect();
1902            if let Some(editor) = editors.into_iter().next() {
1903                editor.update(cx, |editor, cx| {
1904                    // Get the prompt editor from the overlay and insert text
1905                    if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
1906                        prompt_editor.update(cx, |prompt_editor: &mut editor::Editor, cx| {
1907                            prompt_editor.insert(
1908                                "This change needs better error handling",
1909                                window,
1910                                cx,
1911                            );
1912                        });
1913                    }
1914                });
1915            }
1916        })
1917        .log_err();
1918
1919    // Wait for text to be inserted
1920    for _ in 0..3 {
1921        cx.advance_clock(Duration::from_millis(100));
1922        cx.run_until_parked();
1923    }
1924
1925    // Refresh window
1926    cx.update_window(regular_window.into(), |_, window, _cx| {
1927        window.refresh();
1928    })?;
1929
1930    cx.run_until_parked();
1931
1932    // Capture Test 5: Diff review overlay with typed text
1933    let test5_result = run_visual_test(
1934        "diff_review_overlay_with_text",
1935        regular_window.into(),
1936        cx,
1937        update_baseline,
1938    )?;
1939
1940    // Test 6: Submit a comment to store it locally
1941    regular_window
1942        .update(cx, |workspace, window, cx| {
1943            let editors: Vec<_> = workspace.items_of_type::<editor::Editor>(cx).collect();
1944            if let Some(editor) = editors.into_iter().next() {
1945                editor.update(cx, |editor, cx| {
1946                    // Submit the comment that was typed in test 5
1947                    editor.submit_diff_review_comment(window, cx);
1948                });
1949            }
1950        })
1951        .log_err();
1952
1953    // Wait for comment to be stored
1954    for _ in 0..3 {
1955        cx.advance_clock(Duration::from_millis(100));
1956        cx.run_until_parked();
1957    }
1958
1959    // Refresh window
1960    cx.update_window(regular_window.into(), |_, window, _cx| {
1961        window.refresh();
1962    })?;
1963
1964    cx.run_until_parked();
1965
1966    // Capture Test 6: Overlay with one stored comment
1967    let test6_result = run_visual_test(
1968        "diff_review_one_comment",
1969        regular_window.into(),
1970        cx,
1971        update_baseline,
1972    )?;
1973
1974    // Test 7: Add more comments to show multiple comments expanded
1975    regular_window
1976        .update(cx, |workspace, window, cx| {
1977            let editors: Vec<_> = workspace.items_of_type::<editor::Editor>(cx).collect();
1978            if let Some(editor) = editors.into_iter().next() {
1979                editor.update(cx, |editor, cx| {
1980                    // Add second comment
1981                    if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
1982                        prompt_editor.update(cx, |pe, cx| {
1983                            pe.insert("Second comment about imports", window, cx);
1984                        });
1985                    }
1986                    editor.submit_diff_review_comment(window, cx);
1987
1988                    // Add third comment
1989                    if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
1990                        prompt_editor.update(cx, |pe, cx| {
1991                            pe.insert("Third comment about naming conventions", window, cx);
1992                        });
1993                    }
1994                    editor.submit_diff_review_comment(window, cx);
1995                });
1996            }
1997        })
1998        .log_err();
1999
2000    // Wait for comments to be stored
2001    for _ in 0..3 {
2002        cx.advance_clock(Duration::from_millis(100));
2003        cx.run_until_parked();
2004    }
2005
2006    // Refresh window
2007    cx.update_window(regular_window.into(), |_, window, _cx| {
2008        window.refresh();
2009    })?;
2010
2011    cx.run_until_parked();
2012
2013    // Capture Test 7: Overlay with multiple comments expanded
2014    let test7_result = run_visual_test(
2015        "diff_review_multiple_comments_expanded",
2016        regular_window.into(),
2017        cx,
2018        update_baseline,
2019    )?;
2020
2021    // Test 8: Collapse the comments section
2022    regular_window
2023        .update(cx, |workspace, _window, cx| {
2024            let editors: Vec<_> = workspace.items_of_type::<editor::Editor>(cx).collect();
2025            if let Some(editor) = editors.into_iter().next() {
2026                editor.update(cx, |editor, cx| {
2027                    // Toggle collapse using the public method
2028                    editor.set_diff_review_comments_expanded(false, cx);
2029                });
2030            }
2031        })
2032        .log_err();
2033
2034    // Wait for UI to update
2035    for _ in 0..3 {
2036        cx.advance_clock(Duration::from_millis(100));
2037        cx.run_until_parked();
2038    }
2039
2040    // Refresh window
2041    cx.update_window(regular_window.into(), |_, window, _cx| {
2042        window.refresh();
2043    })?;
2044
2045    cx.run_until_parked();
2046
2047    // Capture Test 8: Comments collapsed
2048    let test8_result = run_visual_test(
2049        "diff_review_comments_collapsed",
2050        regular_window.into(),
2051        cx,
2052        update_baseline,
2053    )?;
2054
2055    // Clean up: remove worktrees to stop background scanning
2056    workspace_window
2057        .update(cx, |workspace, _window, cx| {
2058            let project = workspace.project().clone();
2059            project.update(cx, |project, cx| {
2060                let worktree_ids: Vec<_> =
2061                    project.worktrees(cx).map(|wt| wt.read(cx).id()).collect();
2062                for id in worktree_ids {
2063                    project.remove_worktree(id, cx);
2064                }
2065            });
2066        })
2067        .log_err();
2068
2069    cx.run_until_parked();
2070
2071    // Close windows
2072    cx.update_window(workspace_window.into(), |_, window, _cx| {
2073        window.remove_window();
2074    })
2075    .log_err();
2076    cx.update_window(regular_window.into(), |_, window, _cx| {
2077        window.remove_window();
2078    })
2079    .log_err();
2080
2081    cx.run_until_parked();
2082
2083    // Give background tasks time to finish
2084    for _ in 0..15 {
2085        cx.advance_clock(Duration::from_millis(100));
2086        cx.run_until_parked();
2087    }
2088
2089    // Return combined result
2090    let all_results = [
2091        &test1_result,
2092        &test2_result,
2093        &test3_result,
2094        &test4_result,
2095        &test5_result,
2096        &test6_result,
2097        &test7_result,
2098        &test8_result,
2099    ];
2100
2101    // Combine results: if any test updated a baseline, return BaselineUpdated;
2102    // otherwise return Passed. The exhaustive match ensures the compiler
2103    // verifies we handle all TestResult variants.
2104    let result = all_results
2105        .iter()
2106        .fold(TestResult::Passed, |acc, r| match r {
2107            TestResult::Passed => acc,
2108            TestResult::BaselineUpdated(p) => TestResult::BaselineUpdated(p.clone()),
2109        });
2110    Ok(result)
2111}
2112
2113/// Omega's own rendered proofs. `OMEGA-DELTA-0034`, `OMEGA-DELTA-0035`,
2114/// omega#76, omega#77 and omega#78.
2115///
2116/// # Why this exists at all
2117///
2118/// omega#76, #77 and #78 each landed with a source-level suite and no rendered
2119/// evidence, and each said so plainly. Three lanes in a row reported "no check
2120/// here looks at a rendered pixel". That gap is what this function closes: it
2121/// draws the real widget tree through Metal and writes the frame to a PNG, so
2122/// a claim about what the user sees is answered by a picture rather than by a
2123/// `contains` over a source file.
2124///
2125/// # Why it is in-process
2126///
2127/// The obvious alternative — drive the packaged app and screenshot the window
2128/// — has a hazard that has already bitten this workspace: macOS routes
2129/// synthesized keystrokes to the **frontmost application**, not to the process
2130/// named on the command line, so a harness can type into somebody else's
2131/// window and record the reply as the app's. `VisualTestAppContext` dispatches
2132/// keystrokes into this process's own GPUI window and captures that window's
2133/// texture directly, so there is no frontmost app to get wrong and no other
2134/// window that can answer.
2135///
2136/// # The threshold, stated
2137///
2138/// Comparison is `MATCH_THRESHOLD` (0.99): at least 99% of pixels must match
2139/// the committed baseline. Exact equality is not usable even here — font
2140/// rasterisation and theme colour rounding differ by a pixel or two between
2141/// machines — and a threshold nobody states is worse than a loose one.
2142/// Baselines were generated on Apple Silicon with the Metal renderer; this is
2143/// a local gate, and a materially different GPU may need `UPDATE_BASELINE=1`
2144/// and a human looking at the result before trusting it.
2145#[cfg(all(target_os = "macos", feature = "visual-tests"))]
2146fn run_omega_agent_visual_tests(
2147    app_state: Arc<AppState>,
2148    cx: &mut VisualTestAppContext,
2149    update_baseline: bool,
2150) -> Result<TestResult> {
2151    // The window is torn down whatever happens. Without this, an early `Err`
2152    // leaves the panel's editor buffer alive, GPUI's leaked-handle check panics
2153    // at drop, and the panic *replaces* the error that actually explains the
2154    // failure — which is exactly how the first run of this test reported
2155    // "Visual tests panicked" and nothing else.
2156    let outcome = run_omega_agent_visual_tests_inner(app_state, cx, update_baseline);
2157    cx.run_until_parked();
2158    outcome
2159}
2160
2161#[cfg(all(target_os = "macos", feature = "visual-tests"))]
2162fn run_omega_agent_visual_tests_inner(
2163    app_state: Arc<AppState>,
2164    cx: &mut VisualTestAppContext,
2165    update_baseline: bool,
2166) -> Result<TestResult> {
2167    use agent_ui::AgentPanel;
2168
2169    // A project with **no worktree**. This is the whole point of the first two
2170    // captures: a window with nothing to restore is by definition a window with
2171    // no project, so this is the state a fresh install actually reaches.
2172    let project = cx.update(|cx| {
2173        project::Project::local(
2174            app_state.client.clone(),
2175            app_state.node_runtime.clone(),
2176            app_state.user_store.clone(),
2177            app_state.languages.clone(),
2178            app_state.fs.clone(),
2179            None,
2180            project::LocalProjectFlags {
2181                init_worktree_trust: false,
2182                ..Default::default()
2183            },
2184            cx,
2185        )
2186    });
2187
2188    let window_size = size(px(900.0), px(720.0));
2189    let bounds = Bounds {
2190        origin: point(px(0.0), px(0.0)),
2191        size: window_size,
2192    };
2193    let workspace_window: WindowHandle<Workspace> = cx
2194        .update(|cx| {
2195            cx.open_window(
2196                WindowOptions {
2197                    window_bounds: Some(WindowBounds::Windowed(bounds)),
2198                    focus: false,
2199                    show: false,
2200                    ..Default::default()
2201                },
2202                |window, cx| {
2203                    cx.new(|cx| {
2204                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
2205                    })
2206                },
2207            )
2208        })
2209        .context("Failed to open the Omega front door window")?;
2210
2211    cx.run_until_parked();
2212
2213    // The window really has no project. Asserted rather than assumed, because
2214    // every claim these captures make rests on it — a capture taken with a
2215    // worktree quietly present would prove the opposite of what it says.
2216    let visible_worktrees = workspace_window
2217        .update(cx, |workspace, _window, cx| {
2218            workspace.project().read(cx).visible_worktrees(cx).count()
2219        })
2220        .context("Failed to read the project's worktrees")?;
2221    anyhow::ensure!(
2222        visible_worktrees == 0,
2223        "the front door capture needs a projectless window; found {visible_worktrees} worktree(s)"
2224    );
2225
2226    let (weak_workspace, async_window_cx) = workspace_window
2227        .update(cx, |workspace, window, cx| {
2228            (workspace.weak_handle(), window.to_async(cx))
2229        })
2230        .context("Failed to get workspace handle")?;
2231
2232    cx.background_executor.allow_parking();
2233    let panel = cx
2234        .foreground_executor
2235        .block_test(AgentPanel::load(weak_workspace, async_window_cx))
2236        .context("Failed to load AgentPanel")?;
2237    cx.background_executor.forbid_parking();
2238
2239    workspace_window
2240        .update(cx, |workspace, window, cx| {
2241            workspace.add_panel(panel.clone(), window, cx);
2242            workspace.open_panel::<AgentPanel>(window, cx);
2243        })
2244        .context("Failed to add the agent panel")?;
2245
2246    cx.run_until_parked();
2247
2248    // The shipped front door, called the way `crates/zed/src/main.rs` calls it
2249    // on a window with nothing to restore. Not a hand-rolled approximation of
2250    // it: `open_front_door` is the entry `OMEGA-DELTA-0019` added, and driving
2251    // anything else here would photograph a path no user takes.
2252    workspace_window
2253        .update(cx, |_workspace, window, cx| {
2254            AgentPanel::open_front_door(window, cx);
2255        })
2256        .context("Failed to open the front door")?;
2257
2258    cx.run_until_parked();
2259
2260    // The panel must actually be the focused, visible dock surface. A capture
2261    // taken with the panel absent shows the launchpad and would read as a
2262    // perfectly plausible screenshot of something else entirely — which is what
2263    // the first run of this test produced, and how it was caught.
2264    let panel_is_open = workspace_window
2265        .update(cx, |workspace, _window, cx| {
2266            workspace
2267                .panel::<AgentPanel>(cx)
2268                .is_some_and(|panel| panel.read(cx).active_thread_id(cx).is_some())
2269        })
2270        .unwrap_or(false);
2271    anyhow::ensure!(
2272        panel_is_open,
2273        "the agent panel is not the open surface; the capture would show the \
2274         launchpad rather than the front door"
2275    );
2276
2277    // omega#76's exit, first half. Before this delta the panel answered a
2278    // projectless window with "Open Project / Clone Repository" and there was
2279    // nothing to type into.
2280    let thread_id = cx
2281        .read(|cx| panel.read(cx).active_thread_id(cx))
2282        .ok_or_else(|| {
2283            anyhow::anyhow!(
2284                "the front door produced no thread on a projectless window — \
2285                 this is the omega#76 defect, and the capture below would show \
2286                 the empty-project state"
2287            )
2288        })?;
2289
2290    let front_door = run_visual_test(
2291        "omega_front_door_no_project",
2292        workspace_window.into(),
2293        cx,
2294        update_baseline,
2295    )?;
2296
2297    // omega#76's exit, second half: **typing starts a real thread**. The
2298    // keystrokes go into this window through GPUI's own dispatch, so nothing
2299    // depends on which application macOS thinks is frontmost.
2300    cx.simulate_input(workspace_window.into(), "route this thread on purpose");
2301    cx.run_until_parked();
2302
2303    let typed = cx
2304        .read(|cx| {
2305            panel
2306                .read(cx)
2307                .active_thread_view_for_tests()
2308                .and_then(|conversation| conversation.read(cx).root_thread_view())
2309                .map(|view| view.read(cx).message_editor.read(cx).text(cx))
2310        })
2311        .unwrap_or_default();
2312    anyhow::ensure!(
2313        typed.contains("route this thread on purpose"),
2314        "typing on the front door did not reach the thread's composer; the \
2315         editor holds {typed:?}"
2316    );
2317
2318    let typing = run_visual_test(
2319        "omega_front_door_typing",
2320        workspace_window.into(),
2321        cx,
2322        update_baseline,
2323    )?;
2324
2325    // Zoom the panel for the disclosure captures. The executor line is long by
2326    // design — class, agent, model, run, and route reason — and a dock-width
2327    // capture truncates it with an ellipsis, which would make a picture of a
2328    // *truncated* line the evidence that the line renders.
2329    cx.update_window(workspace_window.into(), |_, window, cx| {
2330        panel.update(cx, |panel, cx| {
2331            use workspace::dock::Panel as _;
2332            panel.set_zoomed(true, window, cx);
2333        });
2334    })
2335    .log_err();
2336    cx.run_until_parked();
2337
2338    // omega#77: the executor line, on the native thread the front door just
2339    // built. This thread was routed by `OmegaAgentConnection`, so its line also
2340    // carries omega#78's route reason.
2341    let native_line = cx
2342        .read(|cx| omega_executor_line(&panel, cx))
2343        .ok_or_else(|| anyhow::anyhow!("the native thread has no executor disclosure"))?;
2344    anyhow::ensure!(
2345        native_line.starts_with("native_loop"),
2346        "the front door's own thread must disclose the native loop, not {native_line:?}"
2347    );
2348    anyhow::ensure!(
2349        native_line.contains("routed:"),
2350        "omega#78's wiring is not reaching the rendered line: {native_line:?}"
2351    );
2352    println!("  native executor line: {native_line}");
2353
2354    let native_disclosure = run_visual_test(
2355        "omega_executor_disclosure_native",
2356        workspace_window.into(),
2357        cx,
2358        update_baseline,
2359    )?;
2360
2361    // omega#78, first exit clause: **a pinned executor is always honoured.**
2362    // The native loop is the one executor this build has registered on the
2363    // router, so it is the one honourable pin — and pinning it is not a no-op:
2364    // the line changes from `routed: unpinned` to `routed: pinned`, which is
2365    // the difference between "nobody chose this" and "a person did".
2366    let session_for_pin = cx
2367        .read(|cx| {
2368            panel
2369                .read(cx)
2370                .active_thread_view_for_tests()
2371                .and_then(|conversation| conversation.read(cx).root_thread_view())
2372                .map(|view| view.read(cx).thread.read(cx).session_id().clone())
2373        })
2374        .ok_or_else(|| anyhow::anyhow!("no session to pin"))?;
2375    let router_for_pin = agent_ui::omega_router::active_router().ok_or_else(|| {
2376        anyhow::anyhow!(
2377            "no router was built for the native agent entry — omega#78's \
2378             wiring is the thing under test and it is absent"
2379        )
2380    })?;
2381    let honoured = router_for_pin.pin_session(
2382        &session_for_pin,
2383        omega_front_door::ExecutorPin::new(omega_front_door::ExecutorClass::NativeLoop),
2384        omega_front_door::PinGesture::ExecutorPinMenuItem,
2385    );
2386    anyhow::ensure!(
2387        honoured.reason == omega_front_door::RouteReason::PinHonored,
2388        "a pin to the fail-closed target must be honoured, not {:?}",
2389        honoured.reason
2390    );
2391    cx.update_window(workspace_window.into(), |_, window, _cx| {
2392        window.refresh();
2393    })?;
2394    cx.run_until_parked();
2395
2396    let honoured_line = cx
2397        .read(|cx| omega_executor_line(&panel, cx))
2398        .ok_or_else(|| anyhow::anyhow!("the pinned thread has no executor disclosure"))?;
2399    anyhow::ensure!(
2400        honoured_line.contains("routed: pinned"),
2401        "an honoured pin must say so on the thread's own line: {honoured_line:?}"
2402    );
2403    println!("  honoured-pin executor line: {honoured_line}");
2404
2405    let pin_honoured = run_visual_test(
2406        "omega_route_pin_honoured",
2407        workspace_window.into(),
2408        cx,
2409        update_baseline,
2410    )?;
2411
2412    // omega#78: a pin that cannot be honoured, rendered. No engine is running
2413    // under this harness, so an engine-lane pin falls closed to the native loop
2414    // and the line has to say so — a fallback the user cannot see is the defect
2415    // this packet exists to avoid.
2416    let session_id = cx
2417        .read(|cx| {
2418            panel
2419                .read(cx)
2420                .active_thread_view_for_tests()
2421                .and_then(|conversation| conversation.read(cx).root_thread_view())
2422                .map(|view| view.read(cx).thread.read(cx).session_id().clone())
2423        })
2424        .ok_or_else(|| anyhow::anyhow!("no session to pin"))?;
2425    let router = agent_ui::omega_router::active_router().ok_or_else(|| {
2426        anyhow::anyhow!(
2427            "no router was built for the native agent entry — omega#78's \
2428             wiring is the thing under test and it is absent"
2429        )
2430    })?;
2431    let decision = router.pin_session(
2432        &session_id,
2433        omega_front_door::ExecutorPin::new(omega_front_door::ExecutorClass::EngineLane),
2434        omega_front_door::PinGesture::ExecutorPinMenuItem,
2435    );
2436    anyhow::ensure!(
2437        decision.reason.is_fallback(),
2438        "an engine-lane pin with no engine must fall back, not report {:?}",
2439        decision.reason
2440    );
2441
2442    cx.update_window(workspace_window.into(), |_, window, _cx| {
2443        window.refresh();
2444    })?;
2445    cx.run_until_parked();
2446
2447    let pinned_line = cx
2448        .read(|cx| omega_executor_line(&panel, cx))
2449        .ok_or_else(|| anyhow::anyhow!("the pinned thread has no executor disclosure"))?;
2450    anyhow::ensure!(
2451        pinned_line.contains("fell back to the native loop"),
2452        "the unhonoured pin is not visible on the thread's line: {pinned_line:?}"
2453    );
2454    println!("  unhonoured-pin executor line: {pinned_line}");
2455
2456    let pin_fallback = run_visual_test(
2457        "omega_route_pin_not_honoured",
2458        workspace_window.into(),
2459        cx,
2460        update_baseline,
2461    )?;
2462
2463    // omega#77: the external-ACP kind, on a second thread in the same panel.
2464    let stub: Rc<dyn AgentServer> = Rc::new(StubAgentServer::new(StubAgentConnection::new()));
2465    cx.update_window(workspace_window.into(), |_, window, cx| {
2466        panel.update(cx, |panel, cx| {
2467            panel.open_external_thread_with_server(stub.clone(), window, cx);
2468        });
2469    })?;
2470    cx.run_until_parked();
2471
2472    let external_line = cx
2473        .read(|cx| omega_executor_line(&panel, cx))
2474        .ok_or_else(|| anyhow::anyhow!("the external thread has no executor disclosure"))?;
2475    anyhow::ensure!(
2476        external_line.starts_with("external_acp"),
2477        "a thread on a connection Omega did not build must not be disclosed as \
2478         first-party output: {external_line:?}"
2479    );
2480    println!("  external-acp executor line: {external_line}");
2481
2482    let external_disclosure = run_visual_test(
2483        "omega_executor_disclosure_external_acp",
2484        workspace_window.into(),
2485        cx,
2486        update_baseline,
2487    )?;
2488
2489    // omega#77: the engine-lane kind, on that same external thread.
2490    //
2491    // Deliberately *this* thread and not the front door's own. A lane run is a
2492    // `codex-acp` process the host bridge drives, so the honest record is "this
2493    // run delegated to this agent" — and it is a thread the router did not
2494    // route, so its `route` part is absent. Publishing a lane run onto a
2495    // *routed* thread instead would produce a record `is_coherent` rejects: a
2496    // route reason that says the router fell back to the native loop, on a line
2497    // that claims an engine lane. The coherence assertions below are what
2498    // caught that while this test was being written.
2499    let external_thread_id = cx
2500        .read(|cx| panel.read(cx).active_thread_id(cx))
2501        .ok_or_else(|| anyhow::anyhow!("the external thread has no id"))?;
2502    anyhow::ensure!(
2503        external_thread_id != thread_id,
2504        "the external thread must be a second thread, not the front door's own"
2505    );
2506    agent_ui::omega_host_bridge::publish_engine_lane_run_for_tests(
2507        external_thread_id,
2508        "operation.full-auto.visual".to_string(),
2509    );
2510    cx.update_window(workspace_window.into(), |_, window, _cx| {
2511        window.refresh();
2512    })?;
2513    cx.run_until_parked();
2514
2515    let lane_line = cx
2516        .read(|cx| omega_executor_line(&panel, cx))
2517        .ok_or_else(|| anyhow::anyhow!("the lane thread has no executor disclosure"))?;
2518    anyhow::ensure!(
2519        lane_line.starts_with("engine_lane") && lane_line.contains("operation.full-auto.visual"),
2520        "a thread bound to a lane run must disclose the run: {lane_line:?}"
2521    );
2522    println!("  engine-lane executor line: {lane_line}");
2523
2524    let lane_disclosure = run_visual_test(
2525        "omega_executor_disclosure_engine_lane",
2526        workspace_window.into(),
2527        cx,
2528        update_baseline,
2529    )?;
2530
2531    // omega#77's restart proof, first half: leave behind what a relaunch would
2532    // find, and then end. Everything below the captures writes; nothing below
2533    // them photographs, so no committed baseline can move because of it.
2534    //
2535    // A *second* external thread, because the two restart cases have to be
2536    // distinguishable. The correlation journal names the first thread and not
2537    // this one, so a cold process that confused them would disclose a lane run
2538    // on a thread that never had one — a failure a single-thread shape could
2539    // not see.
2540    let plain_stub: Rc<dyn AgentServer> = Rc::new(StubAgentServer::new(StubAgentConnection::new()));
2541    cx.update_window(workspace_window.into(), |_, window, cx| {
2542        panel.update(cx, |panel, cx| {
2543            panel.open_external_thread_with_server(plain_stub, window, cx);
2544        });
2545    })?;
2546    cx.run_until_parked();
2547
2548    let plain_thread_id = cx
2549        .read(|cx| panel.read(cx).active_thread_id(cx))
2550        .ok_or_else(|| anyhow::anyhow!("the second external thread has no id"))?;
2551    anyhow::ensure!(
2552        plain_thread_id != external_thread_id,
2553        "the restart phase needs two distinct threads; both ids are {plain_thread_id:?}"
2554    );
2555    let plain_record = cx
2556        .read(|cx| omega_executor_record(&panel, cx))
2557        .ok_or_else(|| anyhow::anyhow!("the second external thread has no disclosure"))?;
2558    anyhow::ensure!(
2559        plain_record.class == omega_front_door::ExecutorClass::ExternalAcp
2560            && plain_record.run_ref.is_none(),
2561        "the second external thread must carry no lane run before the restart: {plain_record:?}"
2562    );
2563
2564    // The durable half. `publish_engine_lane_run_for_tests` above wrote a
2565    // process-local index that a restart empties; this writes the correlation
2566    // journal itself, in the production schema, at the path the shipped startup
2567    // reads. The next process gets this and nothing else.
2568    agent_ui::omega_host_bridge::persist_engine_lane_run_for_tests(
2569        external_thread_id,
2570        "operation.full-auto.visual".to_string(),
2571    )?;
2572
2573    write_restart_handoff(&RestartHandoff {
2574        lane_thread: external_thread_id,
2575        lane_line,
2576        external_thread: plain_thread_id,
2577        external_line: plain_record.label(),
2578        agent_id: plain_record.agent_id,
2579        operation_ref: "operation.full-auto.visual".to_string(),
2580    })?;
2581
2582    cx.update_window(workspace_window.into(), |_, window, _cx| {
2583        window.remove_window();
2584    })
2585    .log_err();
2586    cx.run_until_parked();
2587
2588    // Six captures, and `run_visual_test` returns `Err` on a mismatch, so
2589    // reaching here means every one of them matched its baseline or wrote one.
2590    // The aggregate reports "baseline updated" if any capture wrote one, so an
2591    // `UPDATE_BASELINE=1` run is never reported as a pass.
2592    let results = [
2593        front_door,
2594        typing,
2595        native_disclosure,
2596        pin_honoured,
2597        pin_fallback,
2598        lane_disclosure,
2599        external_disclosure,
2600    ];
2601    for result in &results {
2602        if let TestResult::BaselineUpdated(path) = result {
2603            return Ok(TestResult::BaselineUpdated(path.clone()));
2604        }
2605    }
2606    Ok(TestResult::Passed)
2607}
2608
2609/// The executor line the agent panel's active thread would render.
2610///
2611/// Read through the same `ThreadView::executor_disclosure` the render calls, so
2612/// the assertion and the pixels cannot disagree about what the line says.
2613#[cfg(all(target_os = "macos", feature = "visual-tests"))]
2614fn omega_executor_line(panel: &Entity<agent_ui::AgentPanel>, cx: &App) -> Option<String> {
2615    Some(omega_executor_record(panel, cx)?.label())
2616}
2617
2618/// The executor *record* the agent panel's active thread would render from.
2619///
2620/// The record rather than the line, because omega#77's condition is that
2621/// disclosure is a typed record a label renders. A restart proof that compared
2622/// only rendered strings could not tell a restored record from a restored
2623/// string, which is the distinction the condition exists to protect.
2624#[cfg(all(target_os = "macos", feature = "visual-tests"))]
2625fn omega_executor_record(
2626    panel: &Entity<agent_ui::AgentPanel>,
2627    cx: &App,
2628) -> Option<omega_front_door::ExecutorDisclosure> {
2629    let disclosure = panel
2630        .read(cx)
2631        .active_thread_view_for_tests()?
2632        .read(cx)
2633        .root_thread_view()?
2634        .read(cx)
2635        .executor_disclosure(cx);
2636    // Every captured line is asserted coherent, not merely non-empty. A record
2637    // can render a perfectly readable line while claiming two contradictory
2638    // things — an engine lane and a route reason that says the router fell back
2639    // to the native loop is exactly that, and it is what an earlier draft of
2640    // this test was about to photograph and call proof.
2641    assert!(
2642        disclosure.is_coherent(),
2643        "the rendered executor record is incoherent: {disclosure:?}"
2644    );
2645    Some(disclosure)
2646}
2647
2648/// What the recording process leaves for the restarted one. omega#77.
2649///
2650/// The two thread ids and the agent id are the identifiers a real relaunch
2651/// reads back out of `sidebar_threads`; the runner sets `ZED_STATELESS=1`, which
2652/// deliberately keeps that table in memory, so the harness carries them in this
2653/// file instead. The lane run is **not** here: it goes through the production
2654/// correlation journal, at the production path, and the restarted process reads
2655/// it with the production loader. The lines are here so the restarted process
2656/// can assert it rendered *the same disclosure*, not merely a plausible one.
2657#[cfg(all(target_os = "macos", feature = "visual-tests"))]
2658#[derive(serde::Serialize, serde::Deserialize)]
2659#[serde(rename_all = "camelCase")]
2660struct RestartHandoff {
2661    lane_thread: agent_ui::ThreadId,
2662    lane_line: String,
2663    external_thread: agent_ui::ThreadId,
2664    external_line: String,
2665    agent_id: String,
2666    operation_ref: String,
2667}
2668
2669#[cfg(all(target_os = "macos", feature = "visual-tests"))]
2670fn restart_handoff_path() -> std::path::PathBuf {
2671    paths::data_dir().join("omega-visual-restart-handoff.json")
2672}
2673
2674#[cfg(all(target_os = "macos", feature = "visual-tests"))]
2675fn write_restart_handoff(handoff: &RestartHandoff) -> Result<()> {
2676    let path = restart_handoff_path();
2677    if let Some(parent) = path.parent() {
2678        std::fs::create_dir_all(parent)?;
2679    }
2680    std::fs::write(&path, serde_json::to_vec_pretty(handoff)?)
2681        .with_context(|| format!("writing the restart handoff to {}", path.display()))?;
2682    Ok(())
2683}
2684
2685#[cfg(all(target_os = "macos", feature = "visual-tests"))]
2686fn read_restart_handoff() -> Result<RestartHandoff> {
2687    let path = restart_handoff_path();
2688    let bytes = std::fs::read(&path).with_context(|| {
2689        format!(
2690            "no restart handoff at {} — the restart phase must run in the same \
2691             data directory as the phase that recorded it, and after it",
2692            path.display()
2693        )
2694    })?;
2695    Ok(serde_json::from_slice(&bytes)?)
2696}
2697
2698/// omega#77: the executor line on a `codex-acp`-class thread and on an
2699/// engine-lane thread, **after a restart**.
2700///
2701/// This runs in a second process. Every process-local thing the first process
2702/// built is gone — the lane index, the router's recorded routes, the panel, the
2703/// threads themselves — so anything this renders came from disk or was never
2704/// durable in the first place. That is the whole point: the previous lane
2705/// demonstrated a real relaunch for the native kind and could not reach these
2706/// two, and "the mechanism is shared, so I expect them to hold" is not the
2707/// standard the issue set.
2708#[cfg(all(target_os = "macos", feature = "visual-tests"))]
2709fn run_omega_restart_visual_tests(
2710    app_state: Arc<AppState>,
2711    cx: &mut VisualTestAppContext,
2712    update_baseline: bool,
2713) -> Result<TestResult> {
2714    let outcome = run_omega_restart_visual_tests_inner(app_state, cx, update_baseline);
2715    cx.run_until_parked();
2716    outcome
2717}
2718
2719#[cfg(all(target_os = "macos", feature = "visual-tests"))]
2720fn run_omega_restart_visual_tests_inner(
2721    app_state: Arc<AppState>,
2722    cx: &mut VisualTestAppContext,
2723    update_baseline: bool,
2724) -> Result<TestResult> {
2725    use agent_ui::AgentPanel;
2726
2727    let handoff = read_restart_handoff()?;
2728
2729    // A cold process knows nothing until it reads the journal. Asserted before
2730    // the read, because the read is what is under test: if a static had somehow
2731    // survived, or the harness had published the run itself, every assertion
2732    // below would pass for the wrong reason and the pictures would be of a
2733    // process that never restarted anything.
2734    anyhow::ensure!(
2735        agent_ui::omega_host_bridge::engine_lane_run(handoff.lane_thread).is_none(),
2736        "this process already knows a lane run for {:?} before reading the \
2737         journal, so it is not a cold start",
2738        handoff.lane_thread
2739    );
2740
2741    // The production restart edge — the same function `omega_effectd_host_handler`
2742    // calls at startup, not a copy of it.
2743    let restored = agent_ui::omega_host_bridge::reload_engine_lane_runs_from_disk()?;
2744    anyhow::ensure!(
2745        restored == 1,
2746        "the correlation journal named {restored} lane-bound thread(s); the \
2747         recording phase wrote exactly one"
2748    );
2749    let restored_run = agent_ui::omega_host_bridge::engine_lane_run(handoff.lane_thread)
2750        .ok_or_else(|| {
2751            anyhow::anyhow!(
2752                "the reloaded journal does not name {:?}, so nothing survived \
2753                 the restart",
2754                handoff.lane_thread
2755            )
2756        })?;
2757    anyhow::ensure!(
2758        restored_run == handoff.operation_ref,
2759        "the reloaded run is {restored_run:?}, not {:?}",
2760        handoff.operation_ref
2761    );
2762    anyhow::ensure!(
2763        agent_ui::omega_host_bridge::engine_lane_run(handoff.external_thread).is_none(),
2764        "the journal named the plain external thread as a lane run; a restart \
2765         must not invent an engine lane for a thread the user started"
2766    );
2767
2768    let project = cx.update(|cx| {
2769        project::Project::local(
2770            app_state.client.clone(),
2771            app_state.node_runtime.clone(),
2772            app_state.user_store.clone(),
2773            app_state.languages.clone(),
2774            app_state.fs.clone(),
2775            None,
2776            project::LocalProjectFlags {
2777                init_worktree_trust: false,
2778                ..Default::default()
2779            },
2780            cx,
2781        )
2782    });
2783
2784    let window_size = size(px(900.0), px(720.0));
2785    let bounds = Bounds {
2786        origin: point(px(0.0), px(0.0)),
2787        size: window_size,
2788    };
2789    let workspace_window: WindowHandle<Workspace> = cx
2790        .update(|cx| {
2791            cx.open_window(
2792                WindowOptions {
2793                    window_bounds: Some(WindowBounds::Windowed(bounds)),
2794                    focus: false,
2795                    show: false,
2796                    ..Default::default()
2797                },
2798                |window, cx| {
2799                    cx.new(|cx| {
2800                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
2801                    })
2802                },
2803            )
2804        })
2805        .context("Failed to open the restarted window")?;
2806
2807    cx.run_until_parked();
2808
2809    let (weak_workspace, async_window_cx) = workspace_window
2810        .update(cx, |workspace, window, cx| {
2811            (workspace.weak_handle(), window.to_async(cx))
2812        })
2813        .context("Failed to get workspace handle")?;
2814
2815    cx.background_executor.allow_parking();
2816    let panel = cx
2817        .foreground_executor
2818        .block_test(AgentPanel::load(weak_workspace, async_window_cx))
2819        .context("Failed to load AgentPanel")?;
2820    cx.background_executor.forbid_parking();
2821
2822    workspace_window
2823        .update(cx, |workspace, window, cx| {
2824            workspace.add_panel(panel.clone(), window, cx);
2825            workspace.open_panel::<AgentPanel>(window, cx);
2826        })
2827        .context("Failed to add the agent panel")?;
2828
2829    cx.run_until_parked();
2830
2831    // Zoomed for the same reason the recording phase zooms: the line is long by
2832    // design, and a picture of a truncated line is not a picture of the line.
2833    cx.update_window(workspace_window.into(), |_, window, cx| {
2834        panel.update(cx, |panel, cx| {
2835            use workspace::dock::Panel as _;
2836            panel.set_zoomed(true, window, cx);
2837        });
2838    })
2839    .log_err();
2840    cx.run_until_parked();
2841
2842    // The `codex-acp`-class thread, reopened under the id and agent id the
2843    // previous process left behind — the two things `restore_new_draft` reads
2844    // out of the metadata store on a real relaunch.
2845    let agent_id = AgentId::new(handoff.agent_id.clone());
2846    let external_stub: Rc<dyn AgentServer> = Rc::new(StubAgentServer::new(
2847        StubAgentConnection::new().with_agent_id(agent_id.clone()),
2848    ));
2849    cx.update_window(workspace_window.into(), |_, window, cx| {
2850        panel.update(cx, |panel, cx| {
2851            panel.open_external_thread_with_server_under_id(
2852                external_stub,
2853                handoff.external_thread,
2854                window,
2855                cx,
2856            );
2857        });
2858    })?;
2859    cx.run_until_parked();
2860
2861    let reopened_external = cx
2862        .read(|cx| panel.read(cx).active_thread_id(cx))
2863        .ok_or_else(|| anyhow::anyhow!("the reopened external thread has no id"))?;
2864    anyhow::ensure!(
2865        reopened_external == handoff.external_thread,
2866        "the reopened thread is {reopened_external:?}, not the persisted \
2867         {:?} — a new thread wearing the same content proves nothing about a \
2868         restart",
2869        handoff.external_thread
2870    );
2871
2872    let external_line = cx
2873        .read(|cx| omega_executor_line(&panel, cx))
2874        .ok_or_else(|| anyhow::anyhow!("the reopened external thread has no disclosure"))?;
2875    anyhow::ensure!(
2876        external_line == handoff.external_line,
2877        "the restarted process discloses {external_line:?}, where the process \
2878         that owned this thread disclosed {:?}",
2879        handoff.external_line
2880    );
2881    anyhow::ensure!(
2882        external_line.starts_with("external_acp"),
2883        "a thread on a connection Omega did not build must not be disclosed as \
2884         first-party output after a restart either: {external_line:?}"
2885    );
2886    println!("  external-acp executor line after restart: {external_line}");
2887
2888    let external_after_restart = run_visual_test(
2889        "omega_executor_disclosure_external_acp_after_restart",
2890        workspace_window.into(),
2891        cx,
2892        update_baseline,
2893    )?;
2894
2895    // The engine-lane thread. Same reopen, different id — and this one the
2896    // journal on disk names, so the line has to carry the run.
2897    let lane_stub: Rc<dyn AgentServer> = Rc::new(StubAgentServer::new(
2898        StubAgentConnection::new().with_agent_id(agent_id),
2899    ));
2900    cx.update_window(workspace_window.into(), |_, window, cx| {
2901        panel.update(cx, |panel, cx| {
2902            panel.open_external_thread_with_server_under_id(
2903                lane_stub,
2904                handoff.lane_thread,
2905                window,
2906                cx,
2907            );
2908        });
2909    })?;
2910    cx.run_until_parked();
2911
2912    let reopened_lane = cx
2913        .read(|cx| panel.read(cx).active_thread_id(cx))
2914        .ok_or_else(|| anyhow::anyhow!("the reopened lane thread has no id"))?;
2915    anyhow::ensure!(
2916        reopened_lane == handoff.lane_thread,
2917        "the reopened lane thread is {reopened_lane:?}, not the persisted {:?}",
2918        handoff.lane_thread
2919    );
2920
2921    let lane_record = cx
2922        .read(|cx| omega_executor_record(&panel, cx))
2923        .ok_or_else(|| anyhow::anyhow!("the reopened lane thread has no disclosure"))?;
2924    anyhow::ensure!(
2925        lane_record.run_ref.as_deref() == Some(handoff.operation_ref.as_str()),
2926        "the restored record names run {:?}, not {:?}",
2927        lane_record.run_ref,
2928        handoff.operation_ref
2929    );
2930    let lane_line = lane_record.label();
2931    anyhow::ensure!(
2932        lane_line == handoff.lane_line,
2933        "the restarted process discloses {lane_line:?}, where the process that \
2934         owned this thread disclosed {:?}",
2935        handoff.lane_line
2936    );
2937    println!("  engine-lane executor line after restart: {lane_line}");
2938
2939    let lane_after_restart = run_visual_test(
2940        "omega_executor_disclosure_engine_lane_after_restart",
2941        workspace_window.into(),
2942        cx,
2943        update_baseline,
2944    )?;
2945
2946    cx.update_window(workspace_window.into(), |_, window, _cx| {
2947        window.remove_window();
2948    })
2949    .log_err();
2950    cx.run_until_parked();
2951
2952    for result in [&external_after_restart, &lane_after_restart] {
2953        if let TestResult::BaselineUpdated(path) = result {
2954            return Ok(TestResult::BaselineUpdated(path.clone()));
2955        }
2956    }
2957    Ok(TestResult::Passed)
2958}
2959
2960/// A stub AgentServer for visual testing that returns a pre-programmed connection.
2961#[derive(Clone)]
2962#[cfg(target_os = "macos")]
2963struct StubAgentServer {
2964    connection: StubAgentConnection,
2965}
2966
2967#[cfg(target_os = "macos")]
2968impl StubAgentServer {
2969    fn new(connection: StubAgentConnection) -> Self {
2970        Self { connection }
2971    }
2972}
2973
2974#[cfg(target_os = "macos")]
2975impl AgentServer for StubAgentServer {
2976    fn logo(&self) -> ui::IconName {
2977        ui::IconName::OmegaAssistant
2978    }
2979
2980    fn agent_id(&self) -> AgentId {
2981        "Visual Test Agent".into()
2982    }
2983
2984    fn connect(
2985        &self,
2986        _delegate: AgentServerDelegate,
2987        _project: Entity<Project>,
2988        _cx: &mut App,
2989    ) -> gpui::Task<gpui::Result<Rc<dyn AgentConnection>>> {
2990        gpui::Task::ready(Ok(Rc::new(self.connection.clone())))
2991    }
2992
2993    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
2994        self
2995    }
2996}
2997
2998/// A visual-test server that connects to one real, explicitly named Exo lane.
2999///
3000/// The server stores only the lane path so it remains `Send`, as
3001/// [`AgentServer`] requires. The connection itself stays on the GPUI thread.
3002#[derive(Clone)]
3003#[cfg(target_os = "macos")]
3004struct ExoVisualAgentServer {
3005    lane_path: PathBuf,
3006}
3007
3008#[cfg(target_os = "macos")]
3009impl AgentServer for ExoVisualAgentServer {
3010    fn logo(&self) -> ui::IconName {
3011        ui::IconName::OmegaAssistant
3012    }
3013
3014    fn agent_id(&self) -> AgentId {
3015        "Exo".into()
3016    }
3017
3018    fn connect(
3019        &self,
3020        _delegate: AgentServerDelegate,
3021        project: Entity<Project>,
3022        cx: &mut App,
3023    ) -> gpui::Task<gpui::Result<Rc<dyn AgentConnection>>> {
3024        let lane_path = self.lane_path.clone();
3025        let agent_server_store = project.read(cx).agent_server_store().downgrade();
3026        cx.spawn(async move |cx| {
3027            agent_ui::omega_exo_connection::connect_configured_lane(
3028                &lane_path,
3029                project,
3030                agent_server_store,
3031                cx,
3032            )
3033            .await?
3034            .ok_or_else(|| anyhow::anyhow!("the Exo visual lane file is not configured"))
3035        })
3036    }
3037
3038    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3039        self
3040    }
3041}
3042
3043#[cfg(all(target_os = "macos", feature = "visual-tests"))]
3044fn run_omega_exo_visual_tests(
3045    app_state: Arc<AppState>,
3046    cx: &mut VisualTestAppContext,
3047    update_baseline: bool,
3048) -> Result<TestResult> {
3049    let lane_path = PathBuf::from(
3050        std::env::var("OMEGA_EXO_VISUAL_LANE_FILE")
3051            .context("set OMEGA_EXO_VISUAL_LANE_FILE to an isolated Exo lane file")?,
3052    );
3053    anyhow::ensure!(
3054        lane_path.is_file(),
3055        "the Exo visual lane does not exist: {}",
3056        lane_path.display()
3057    );
3058
3059    // Use independent native windows instead of resizing one hidden Metal
3060    // window. The macOS platform does not dispatch a bounds callback for that
3061    // test-only resize, so a second screenshot can silently retain the first
3062    // viewport. Two fresh windows prove both responsive branches directly.
3063    let wide = run_omega_exo_visual_capture(
3064        app_state.clone(),
3065        cx,
3066        lane_path.clone(),
3067        "omega_exo_workspace_wide",
3068        size(px(1320.), px(860.)),
3069        update_baseline,
3070    )?;
3071    let narrow = run_omega_exo_visual_capture(
3072        app_state,
3073        cx,
3074        lane_path,
3075        "omega_exo_workspace_narrow",
3076        size(px(720.), px(900.)),
3077        update_baseline,
3078    )?;
3079
3080    for result in [&wide, &narrow] {
3081        if let TestResult::BaselineUpdated(path) = result {
3082            return Ok(TestResult::BaselineUpdated(path.clone()));
3083        }
3084    }
3085    Ok(TestResult::Passed)
3086}
3087
3088#[cfg(all(target_os = "macos", feature = "visual-tests"))]
3089fn run_omega_exo_visual_capture(
3090    app_state: Arc<AppState>,
3091    cx: &mut VisualTestAppContext,
3092    lane_path: PathBuf,
3093    test_name: &'static str,
3094    window_size: gpui::Size<gpui::Pixels>,
3095    update_baseline: bool,
3096) -> Result<TestResult> {
3097    use agent_ui::AgentPanel;
3098
3099    let project_dir = tempfile::tempdir()?.keep().canonicalize()?;
3100    let project = cx.update(|cx| {
3101        Project::local(
3102            app_state.client.clone(),
3103            app_state.node_runtime.clone(),
3104            app_state.user_store.clone(),
3105            app_state.languages.clone(),
3106            app_state.fs.clone(),
3107            None,
3108            project::LocalProjectFlags {
3109                init_worktree_trust: false,
3110                ..Default::default()
3111            },
3112            cx,
3113        )
3114    });
3115    let add_worktree = project.update(cx, |project, cx| {
3116        project.find_or_create_worktree(&project_dir, true, cx)
3117    });
3118    cx.background_executor.allow_parking();
3119    cx.foreground_executor
3120        .block_test(add_worktree)
3121        .context("failed to add the Exo visual worktree")?;
3122    cx.background_executor.forbid_parking();
3123    cx.run_until_parked();
3124
3125    let bounds = Bounds {
3126        origin: point(px(-10_000.), px(-10_000.)),
3127        size: window_size,
3128    };
3129    let workspace_window: WindowHandle<Workspace> = cx
3130        .update(|cx| {
3131            cx.open_window(
3132                WindowOptions {
3133                    window_bounds: Some(WindowBounds::Windowed(bounds)),
3134                    focus: false,
3135                    show: true,
3136                    ..Default::default()
3137                },
3138                |window, cx| {
3139                    cx.new(|cx| {
3140                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
3141                    })
3142                },
3143            )
3144        })
3145        .context("failed to open the Exo workspace window")?;
3146    cx.run_until_parked();
3147
3148    let (weak_workspace, async_window_cx) = workspace_window
3149        .update(cx, |workspace, window, cx| {
3150            (workspace.weak_handle(), window.to_async(cx))
3151        })
3152        .context("failed to get the Exo workspace handle")?;
3153    cx.background_executor.allow_parking();
3154    let panel = cx
3155        .foreground_executor
3156        .block_test(AgentPanel::load(weak_workspace, async_window_cx))
3157        .context("failed to load the Exo Agent panel")?;
3158    cx.background_executor.forbid_parking();
3159
3160    workspace_window
3161        .update(cx, |workspace, window, cx| {
3162            workspace.add_panel(panel.clone(), window, cx);
3163            workspace.open_panel::<AgentPanel>(window, cx);
3164            panel.update(cx, |panel, cx| {
3165                use workspace::dock::Panel as _;
3166                panel.set_zoomed(true, window, cx);
3167            });
3168        })
3169        .context("failed to open the Exo Agent panel")?;
3170    cx.run_until_parked();
3171
3172    // Keep capture failures inside this closure so the real ACP connection is
3173    // always removed below. Otherwise an early assertion or screenshot error
3174    // masks its own cause with GPUI's leaked ElicitationStore check.
3175    let capture = (|| -> Result<TestResult> {
3176        let server: Rc<dyn AgentServer> = Rc::new(ExoVisualAgentServer { lane_path });
3177        cx.update_window(workspace_window.into(), |_, window, cx| {
3178            panel.update(cx, |panel, cx| {
3179                panel.open_external_thread_with_server(server, window, cx);
3180            });
3181        })?;
3182
3183        // Connecting the child process and completing ACP initialization
3184        // require real I/O. Poll the visible state for at most five seconds;
3185        // one `run_until_parked` can return before stdio initialization wakes
3186        // the foreground executor.
3187        cx.background_executor.allow_parking();
3188        for _ in 0..100 {
3189            cx.run_until_parked();
3190            if cx.read(|cx| {
3191                panel
3192                    .read(cx)
3193                    .active_thread_view_for_tests()
3194                    .is_some_and(|view| view.read(cx).active_thread().is_some())
3195            }) {
3196                break;
3197            }
3198            std::thread::sleep(Duration::from_millis(50));
3199        }
3200        cx.background_executor.forbid_parking();
3201
3202        let thread_view = cx
3203            .read(|cx| panel.read(cx).active_thread_view_for_tests().cloned())
3204            .ok_or_else(|| anyhow::anyhow!("the Exo workspace has no active thread"))?;
3205        let thread = cx
3206            .read(|cx| {
3207                thread_view
3208                    .read(cx)
3209                    .active_thread()
3210                    .map(|active| active.read(cx).thread.clone())
3211            })
3212            .ok_or_else(|| anyhow::anyhow!("the Exo workspace thread is not available"))?;
3213
3214        let marker = "OMEGA-EXO-PANE-READY";
3215        let prompt =
3216            format!("Run one tool that prints OMEGA-EXO-TOOL, then reply with exactly {marker}.");
3217        let send = thread.update(cx, |thread, cx| thread.send(vec![prompt.into()], cx));
3218        cx.background_executor.allow_parking();
3219        let send_result = cx.foreground_executor.block_test(send);
3220        cx.background_executor.forbid_parking();
3221        send_result.context("the real Exo visual turn failed")?;
3222        cx.run_until_parked();
3223
3224        let (transcript, turn) = cx.read(|cx| {
3225            let thread = thread.read(cx);
3226            let connection = thread
3227                .connection()
3228                .clone()
3229                .downcast::<agent_ui::omega_exo_connection::ExoHarnessConnection>()
3230                .expect("the visual thread must retain its Exo connection");
3231            (thread.to_markdown(cx), connection.turn())
3232        });
3233        anyhow::ensure!(
3234            transcript.contains(marker),
3235            "the real Exo response did not reach the transcript:\n{transcript}"
3236        );
3237        anyhow::ensure!(
3238            transcript.contains("Tool Call: shell"),
3239            "the real Exo tool call did not reach the transcript:\n{transcript}"
3240        );
3241        anyhow::ensure!(
3242            transcript.contains("OMEGA-EXO-TOOL"),
3243            "the real Exo tool result did not reach the transcript:\n{transcript}"
3244        );
3245        anyhow::ensure!(
3246            turn.phase == agent_ui::omega_exo_connection::ExoTurnPhase::Completed,
3247            "the real Exo turn did not complete: {turn:?}"
3248        );
3249        anyhow::ensure!(
3250            turn.exo_session_id.is_some()
3251                && turn.exo_turn_id.is_some()
3252                && turn.latest_event_id.is_some(),
3253            "the real Exo turn did not return durable references: {turn:?}"
3254        );
3255
3256        let actual_size = cx.update_window(workspace_window.into(), |_, window, _cx| {
3257            window.viewport_size()
3258        })?;
3259        anyhow::ensure!(
3260            actual_size == window_size,
3261            "{test_name} opened at {actual_size:?}, expected {window_size:?}"
3262        );
3263
3264        run_visual_test(test_name, workspace_window.into(), cx, update_baseline)
3265    })();
3266
3267    // Drop every owner of the real ACP connection before the runner's leaked
3268    // entity check. The panel's connection store retains a successful custom
3269    // connection after its thread and window close. Replacing that cached
3270    // entry first lets the Exo child and its ElicitationStore terminate.
3271    panel.update(cx, |panel, cx| {
3272        panel.connection_store().update(cx, |store, cx| {
3273            store.restart_connection(
3274                Agent::Custom { id: "Exo".into() },
3275                Rc::new(StubAgentServer::new(StubAgentConnection::new())),
3276                cx,
3277            );
3278        });
3279    });
3280    cx.run_until_parked();
3281
3282    workspace_window
3283        .update(cx, |workspace, window, cx| {
3284            workspace.remove_panel(&panel, window, cx);
3285            let project = workspace.project().clone();
3286            project.update(cx, |project, cx| {
3287                let worktree_ids: Vec<_> = project
3288                    .worktrees(cx)
3289                    .map(|worktree| worktree.read(cx).id())
3290                    .collect();
3291                for id in worktree_ids {
3292                    project.remove_worktree(id, cx);
3293                }
3294            });
3295        })
3296        .log_err();
3297    cx.run_until_parked();
3298    drop(panel);
3299    drop(project);
3300    cx.update_window(workspace_window.into(), |_, window, _cx| {
3301        window.remove_window();
3302    })
3303    .log_err();
3304    cx.run_until_parked();
3305    for _ in 0..15 {
3306        cx.advance_clock(Duration::from_millis(100));
3307        cx.run_until_parked();
3308    }
3309
3310    capture
3311}
3312
3313#[cfg(all(target_os = "macos", feature = "visual-tests"))]
3314fn run_agent_thread_view_test(
3315    app_state: Arc<AppState>,
3316    cx: &mut VisualTestAppContext,
3317    update_baseline: bool,
3318) -> Result<TestResult> {
3319    use agent::{AgentTool, ToolInput};
3320    use agent_ui::AgentPanel;
3321
3322    // Create a temporary directory with the test image
3323    // Canonicalize to resolve symlinks (on macOS, /var -> /private/var)
3324    // Use keep() to prevent auto-cleanup - we'll clean up manually after stopping background tasks
3325    let temp_dir = tempfile::tempdir()?;
3326    let temp_path = temp_dir.keep();
3327    let canonical_temp = temp_path.canonicalize()?;
3328    let project_path = canonical_temp.join("project");
3329    std::fs::create_dir_all(&project_path)?;
3330    let image_path = project_path.join("test-image.png");
3331    std::fs::write(&image_path, EMBEDDED_TEST_IMAGE)?;
3332
3333    // Create a project with the test image
3334    let project = cx.update(|cx| {
3335        project::Project::local(
3336            app_state.client.clone(),
3337            app_state.node_runtime.clone(),
3338            app_state.user_store.clone(),
3339            app_state.languages.clone(),
3340            app_state.fs.clone(),
3341            None,
3342            project::LocalProjectFlags {
3343                init_worktree_trust: false,
3344                ..Default::default()
3345            },
3346            cx,
3347        )
3348    });
3349
3350    // Add the test directory as a worktree
3351    let add_worktree_task = project.update(cx, |project, cx| {
3352        project.find_or_create_worktree(&project_path, true, cx)
3353    });
3354
3355    cx.background_executor.allow_parking();
3356    let (worktree, _) = cx
3357        .foreground_executor
3358        .block_test(add_worktree_task)
3359        .context("Failed to add worktree")?;
3360    cx.background_executor.forbid_parking();
3361
3362    cx.run_until_parked();
3363
3364    let worktree_name = cx.read(|cx| worktree.read(cx).root_name_str().to_string());
3365
3366    // Create the necessary entities for the ReadFileTool
3367    let action_log = cx.update(|cx| cx.new(|_| action_log::ActionLog::new(project.clone())));
3368
3369    // Create the ReadFileTool
3370    let tool = Arc::new(agent::ReadFileTool::new(project.clone(), action_log, true));
3371
3372    // Create a test event stream to capture tool output
3373    let (event_stream, mut event_receiver) = agent::ToolCallEventStream::test();
3374
3375    // Run the real ReadFileTool to get the actual image content
3376    let input = agent::ReadFileToolInput {
3377        path: format!("{}/test-image.png", worktree_name),
3378        start_line: None,
3379        end_line: None,
3380    };
3381    let run_task = cx.update(|cx| {
3382        tool.clone()
3383            .run(ToolInput::resolved(input), event_stream, cx)
3384    });
3385
3386    cx.background_executor.allow_parking();
3387    let run_result = cx.foreground_executor.block_test(run_task);
3388    cx.background_executor.forbid_parking();
3389    run_result.map_err(|e| match e {
3390        language_model::LanguageModelToolResultContent::Text(text) => {
3391            anyhow::anyhow!("ReadFileTool failed: {text}")
3392        }
3393        other => anyhow::anyhow!("ReadFileTool failed: {other:?}"),
3394    })?;
3395
3396    cx.run_until_parked();
3397
3398    // Collect the events from the tool execution
3399    let mut tool_content: Vec<acp::ToolCallContent> = Vec::new();
3400    let mut tool_locations: Vec<acp::ToolCallLocation> = Vec::new();
3401
3402    while let Ok(event) = event_receiver.try_recv() {
3403        if let Ok(agent::ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
3404            update,
3405        ))) = event
3406        {
3407            if let Some(content) = update.fields.content {
3408                tool_content.extend(content);
3409            }
3410            if let Some(locations) = update.fields.locations {
3411                tool_locations.extend(locations);
3412            }
3413        }
3414    }
3415
3416    if tool_content.is_empty() {
3417        return Err(anyhow::anyhow!("ReadFileTool did not produce any content"));
3418    }
3419
3420    // Create stub connection with the real tool output
3421    let connection = StubAgentConnection::new();
3422    connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
3423        acp::ToolCall::new(
3424            "read_file",
3425            format!("Read file `{}/test-image.png`", worktree_name),
3426        )
3427        .kind(acp::ToolKind::Read)
3428        .status(acp::ToolCallStatus::Completed)
3429        .locations(tool_locations)
3430        .content(tool_content),
3431    )]);
3432
3433    let stub_agent: Rc<dyn AgentServer> = Rc::new(StubAgentServer::new(connection));
3434
3435    // Create a window sized for the agent panel
3436    let window_size = size(px(500.0), px(900.0));
3437    let bounds = Bounds {
3438        origin: point(px(0.0), px(0.0)),
3439        size: window_size,
3440    };
3441
3442    let workspace_window: WindowHandle<Workspace> = cx
3443        .update(|cx| {
3444            cx.open_window(
3445                WindowOptions {
3446                    window_bounds: Some(WindowBounds::Windowed(bounds)),
3447                    focus: false,
3448                    show: false,
3449                    ..Default::default()
3450                },
3451                |window, cx| {
3452                    cx.new(|cx| {
3453                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
3454                    })
3455                },
3456            )
3457        })
3458        .context("Failed to open agent window")?;
3459
3460    cx.run_until_parked();
3461
3462    // Load the AgentPanel
3463    let (weak_workspace, async_window_cx) = workspace_window
3464        .update(cx, |workspace, window, cx| {
3465            (workspace.weak_handle(), window.to_async(cx))
3466        })
3467        .context("Failed to get workspace handle")?;
3468
3469    cx.background_executor.allow_parking();
3470    let panel = cx
3471        .foreground_executor
3472        .block_test(AgentPanel::load(weak_workspace, async_window_cx))
3473        .context("Failed to load AgentPanel")?;
3474    cx.background_executor.forbid_parking();
3475
3476    cx.update_window(workspace_window.into(), |_, _window, cx| {
3477        workspace_window
3478            .update(cx, |workspace, window, cx| {
3479                workspace.add_panel(panel.clone(), window, cx);
3480                workspace.open_panel::<AgentPanel>(window, cx);
3481            })
3482            .log_err();
3483    })?;
3484
3485    cx.run_until_parked();
3486
3487    // Inject the stub server and open the stub thread
3488    cx.update_window(workspace_window.into(), |_, window, cx| {
3489        panel.update(cx, |panel, cx| {
3490            panel.open_external_thread_with_server(stub_agent.clone(), window, cx);
3491        });
3492    })?;
3493
3494    cx.run_until_parked();
3495
3496    // Get the thread view and send a message
3497    let thread_view = cx
3498        .read(|cx| panel.read(cx).active_thread_view_for_tests().cloned())
3499        .ok_or_else(|| anyhow::anyhow!("No active thread view"))?;
3500
3501    let thread = cx
3502        .read(|cx| {
3503            thread_view
3504                .read(cx)
3505                .active_thread()
3506                .map(|active| active.read(cx).thread.clone())
3507        })
3508        .ok_or_else(|| anyhow::anyhow!("Thread not available"))?;
3509
3510    // Send the message to trigger the image response
3511    let send_future = thread.update(cx, |thread, cx| {
3512        thread.send(vec!["Show me the Omega logo".into()], cx)
3513    });
3514
3515    cx.background_executor.allow_parking();
3516    let send_result = cx.foreground_executor.block_test(send_future);
3517    cx.background_executor.forbid_parking();
3518    send_result.context("Failed to send message")?;
3519
3520    cx.run_until_parked();
3521
3522    // Get the tool call ID for expanding later
3523    let tool_call_id = cx
3524        .read(|cx| {
3525            thread.read(cx).entries().iter().find_map(|entry| {
3526                if let acp_thread::AgentThreadEntry::ToolCall(tool_call) = entry {
3527                    Some(tool_call.id.clone())
3528                } else {
3529                    None
3530                }
3531            })
3532        })
3533        .ok_or_else(|| anyhow::anyhow!("Expected a ToolCall entry in thread"))?;
3534
3535    cx.update_window(workspace_window.into(), |_, window, _cx| {
3536        window.refresh();
3537    })?;
3538
3539    cx.run_until_parked();
3540
3541    // Capture the COLLAPSED state
3542    let collapsed_result = run_visual_test(
3543        "agent_thread_with_image_collapsed",
3544        workspace_window.into(),
3545        cx,
3546        update_baseline,
3547    )?;
3548
3549    // Now expand the tool call so the image is visible
3550    thread_view.update(cx, |view, cx| {
3551        view.expand_tool_call(tool_call_id, cx);
3552    });
3553
3554    cx.run_until_parked();
3555
3556    cx.update_window(workspace_window.into(), |_, window, _cx| {
3557        window.refresh();
3558    })?;
3559
3560    cx.run_until_parked();
3561
3562    // Capture the EXPANDED state
3563    let expanded_result = run_visual_test(
3564        "agent_thread_with_image_expanded",
3565        workspace_window.into(),
3566        cx,
3567        update_baseline,
3568    )?;
3569
3570    // Remove the worktree from the project to stop background scanning tasks
3571    // This prevents "root path could not be canonicalized" errors when we clean up
3572    workspace_window
3573        .update(cx, |workspace, _window, cx| {
3574            let project = workspace.project().clone();
3575            project.update(cx, |project, cx| {
3576                let worktree_ids: Vec<_> =
3577                    project.worktrees(cx).map(|wt| wt.read(cx).id()).collect();
3578                for id in worktree_ids {
3579                    project.remove_worktree(id, cx);
3580                }
3581            });
3582        })
3583        .log_err();
3584
3585    cx.run_until_parked();
3586
3587    // Close the window
3588    // Note: This may cause benign "editor::scroll window not found" errors from scrollbar
3589    // auto-hide timers that were scheduled before the window was closed. These errors
3590    // don't affect test results.
3591    cx.update_window(workspace_window.into(), |_, window, _cx| {
3592        window.remove_window();
3593    })
3594    .log_err();
3595
3596    // Run until all cleanup tasks complete
3597    cx.run_until_parked();
3598
3599    // Give background tasks time to finish, including scrollbar hide timers (1 second)
3600    for _ in 0..15 {
3601        cx.advance_clock(Duration::from_millis(100));
3602        cx.run_until_parked();
3603    }
3604
3605    // Note: We don't delete temp_path here because background worktree tasks may still
3606    // be running. The directory will be cleaned up when the process exits.
3607
3608    match (&collapsed_result, &expanded_result) {
3609        (TestResult::Passed, TestResult::Passed) => Ok(TestResult::Passed),
3610        (TestResult::BaselineUpdated(p), _) | (_, TestResult::BaselineUpdated(p)) => {
3611            Ok(TestResult::BaselineUpdated(p.clone()))
3612        }
3613    }
3614}
3615
3616/// Visual test for the Tool Permissions Settings UI page
3617///
3618/// Takes a screenshot showing the tool config page with matched patterns and verdict.
3619#[cfg(target_os = "macos")]
3620fn run_tool_permissions_visual_tests(
3621    app_state: Arc<AppState>,
3622    cx: &mut VisualTestAppContext,
3623    _update_baseline: bool,
3624) -> Result<TestResult> {
3625    use agent_settings::{AgentSettings, CompiledRegex, ToolPermissions, ToolRules};
3626    use collections::HashMap;
3627    use settings::ToolPermissionMode;
3628    use zed_actions::OpenSettingsAt;
3629
3630    // Set up tool permissions with "hi" as both always_deny and always_allow for terminal
3631    cx.update(|cx| {
3632        let mut tools = HashMap::default();
3633        tools.insert(
3634            Arc::from("terminal"),
3635            ToolRules {
3636                default: None,
3637                always_allow: vec![CompiledRegex::new("hi", false).unwrap()],
3638                always_deny: vec![CompiledRegex::new("hi", false).unwrap()],
3639                always_confirm: vec![],
3640                invalid_patterns: vec![],
3641            },
3642        );
3643        let mut settings = AgentSettings::get_global(cx).clone();
3644        settings.tool_permissions = ToolPermissions {
3645            default: ToolPermissionMode::Confirm,
3646            tools,
3647        };
3648        AgentSettings::override_global(settings, cx);
3649    });
3650
3651    // Create a minimal workspace to dispatch the settings action from
3652    let window_size = size(px(900.0), px(700.0));
3653    let bounds = Bounds {
3654        origin: point(px(0.0), px(0.0)),
3655        size: window_size,
3656    };
3657
3658    let project = cx.update(|cx| {
3659        project::Project::local(
3660            app_state.client.clone(),
3661            app_state.node_runtime.clone(),
3662            app_state.user_store.clone(),
3663            app_state.languages.clone(),
3664            app_state.fs.clone(),
3665            None,
3666            project::LocalProjectFlags {
3667                init_worktree_trust: false,
3668                ..Default::default()
3669            },
3670            cx,
3671        )
3672    });
3673
3674    let workspace_window: WindowHandle<MultiWorkspace> = cx
3675        .update(|cx| {
3676            cx.open_window(
3677                WindowOptions {
3678                    window_bounds: Some(WindowBounds::Windowed(bounds)),
3679                    focus: false,
3680                    show: false,
3681                    ..Default::default()
3682                },
3683                |window, cx| {
3684                    let workspace = cx.new(|cx| {
3685                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
3686                    });
3687                    cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
3688                },
3689            )
3690        })
3691        .context("Failed to open workspace window for settings test")?;
3692
3693    cx.run_until_parked();
3694
3695    // Dispatch the OpenSettingsAt action to open settings at the tool_permissions path
3696    workspace_window
3697        .update(cx, |_workspace, window, cx| {
3698            window.dispatch_action(
3699                Box::new(OpenSettingsAt {
3700                    path: "agent.tool_permissions".to_string(),
3701                    target: None,
3702                }),
3703                cx,
3704            );
3705        })
3706        .context("Failed to dispatch OpenSettingsAt action")?;
3707
3708    cx.run_until_parked();
3709
3710    // Give the settings window time to open and render
3711    for _ in 0..10 {
3712        cx.advance_clock(Duration::from_millis(50));
3713        cx.run_until_parked();
3714    }
3715
3716    // Find the settings window - it should be the newest window (last in the list)
3717    let all_windows = cx.update(|cx| cx.windows());
3718    let settings_window = all_windows.last().copied().context("No windows found")?;
3719
3720    let output_dir = std::env::var("VISUAL_TEST_OUTPUT_DIR")
3721        .unwrap_or_else(|_| "target/visual_tests".to_string());
3722    std::fs::create_dir_all(&output_dir).log_err();
3723
3724    // Navigate to the tool permissions sub-page using the public API
3725    let settings_window_handle = settings_window
3726        .downcast::<settings_ui::SettingsWindow>()
3727        .context("Failed to downcast to SettingsWindow")?;
3728
3729    settings_window_handle
3730        .update(cx, |settings_window, window, cx| {
3731            settings_window.navigate_to_sub_page("agent.tool_permissions", window, cx);
3732        })
3733        .context("Failed to navigate to tool permissions sub-page")?;
3734
3735    cx.run_until_parked();
3736
3737    // Give the sub-page time to render
3738    for _ in 0..10 {
3739        cx.advance_clock(Duration::from_millis(50));
3740        cx.run_until_parked();
3741    }
3742
3743    // Now navigate into a specific tool (Terminal) to show the tool config page
3744    settings_window_handle
3745        .update(cx, |settings_window, window, cx| {
3746            settings_window.push_dynamic_sub_page(
3747                "Terminal",
3748                "Configure Tool Rules",
3749                None,
3750                true,
3751                settings_ui::pages::render_terminal_tool_config,
3752                window,
3753                cx,
3754            );
3755        })
3756        .context("Failed to navigate to Terminal tool config")?;
3757
3758    cx.run_until_parked();
3759
3760    // Give the tool config page time to render
3761    for _ in 0..10 {
3762        cx.advance_clock(Duration::from_millis(50));
3763        cx.run_until_parked();
3764    }
3765
3766    // Refresh and redraw so the "Test Your Rules" input is present
3767    cx.update_window(settings_window, |_, window, cx| {
3768        window.draw(cx).clear(cx);
3769    })
3770    .log_err();
3771    cx.run_until_parked();
3772
3773    cx.update_window(settings_window, |_, window, _cx| {
3774        window.refresh();
3775    })
3776    .log_err();
3777    cx.run_until_parked();
3778
3779    // Focus the first tab stop in the window (the "Test Your Rules" editor
3780    // has tab_index(0) and tab_stop(true)) and type "hi" into it.
3781    cx.update_window(settings_window, |_, window, cx| {
3782        window.focus_next(cx);
3783    })
3784    .log_err();
3785    cx.run_until_parked();
3786
3787    cx.simulate_input(settings_window, "hi");
3788
3789    // Let the UI update with the matched patterns
3790    for _ in 0..5 {
3791        cx.advance_clock(Duration::from_millis(50));
3792        cx.run_until_parked();
3793    }
3794
3795    // Refresh and redraw
3796    cx.update_window(settings_window, |_, window, cx| {
3797        window.draw(cx).clear(cx);
3798    })
3799    .log_err();
3800    cx.run_until_parked();
3801
3802    cx.update_window(settings_window, |_, window, _cx| {
3803        window.refresh();
3804    })
3805    .log_err();
3806    cx.run_until_parked();
3807
3808    // Save screenshot: Tool config page with "hi" typed and matched patterns visible
3809    let tool_config_output_path =
3810        PathBuf::from(&output_dir).join("tool_permissions_test_rules.png");
3811
3812    if let Ok(screenshot) = cx.capture_screenshot(settings_window) {
3813        screenshot.save(&tool_config_output_path).log_err();
3814        println!(
3815            "Screenshot (test rules) saved to: {}",
3816            tool_config_output_path.display()
3817        );
3818    }
3819
3820    // Clean up - close the settings window
3821    cx.update_window(settings_window, |_, window, _cx| {
3822        window.remove_window();
3823    })
3824    .log_err();
3825
3826    // Close the workspace window
3827    cx.update_window(workspace_window.into(), |_, window, _cx| {
3828        window.remove_window();
3829    })
3830    .log_err();
3831
3832    cx.run_until_parked();
3833
3834    // Give background tasks time to finish
3835    for _ in 0..5 {
3836        cx.advance_clock(Duration::from_millis(100));
3837        cx.run_until_parked();
3838    }
3839
3840    // Return success - we're just capturing screenshots, not comparing baselines
3841    Ok(TestResult::Passed)
3842}
3843
3844#[cfg(target_os = "macos")]
3845fn run_multi_workspace_sidebar_visual_tests(
3846    app_state: Arc<AppState>,
3847    cx: &mut VisualTestAppContext,
3848    update_baseline: bool,
3849) -> Result<TestResult> {
3850    // Create temporary directories to act as worktrees for active workspaces
3851    let temp_dir = tempfile::tempdir()?;
3852    let temp_path = temp_dir.keep();
3853    let canonical_temp = temp_path.canonicalize()?;
3854
3855    let workspace1_dir = canonical_temp.join("private-test-remote");
3856    let workspace2_dir = canonical_temp.join("zed");
3857    std::fs::create_dir_all(&workspace1_dir)?;
3858    std::fs::create_dir_all(&workspace2_dir)?;
3859
3860    // Create both projects upfront so we can build both workspaces during
3861    // window creation, before the MultiWorkspace entity exists.
3862    // This avoids a re-entrant read panic that occurs when Workspace::new
3863    // tries to access the window root (MultiWorkspace) while it's being updated.
3864    let project1 = cx.update(|cx| {
3865        project::Project::local(
3866            app_state.client.clone(),
3867            app_state.node_runtime.clone(),
3868            app_state.user_store.clone(),
3869            app_state.languages.clone(),
3870            app_state.fs.clone(),
3871            None,
3872            project::LocalProjectFlags {
3873                init_worktree_trust: false,
3874                ..Default::default()
3875            },
3876            cx,
3877        )
3878    });
3879
3880    let project2 = cx.update(|cx| {
3881        project::Project::local(
3882            app_state.client.clone(),
3883            app_state.node_runtime.clone(),
3884            app_state.user_store.clone(),
3885            app_state.languages.clone(),
3886            app_state.fs.clone(),
3887            None,
3888            project::LocalProjectFlags {
3889                init_worktree_trust: false,
3890                ..Default::default()
3891            },
3892            cx,
3893        )
3894    });
3895
3896    let window_size = size(px(1280.0), px(800.0));
3897    let bounds = Bounds {
3898        origin: point(px(0.0), px(0.0)),
3899        size: window_size,
3900    };
3901
3902    // Open a MultiWorkspace window with both workspaces created at construction time
3903    let multi_workspace_window: WindowHandle<MultiWorkspace> = cx
3904        .update(|cx| {
3905            cx.open_window(
3906                WindowOptions {
3907                    window_bounds: Some(WindowBounds::Windowed(bounds)),
3908                    focus: false,
3909                    show: false,
3910                    ..Default::default()
3911                },
3912                |window, cx| {
3913                    let workspace1 = cx.new(|cx| {
3914                        Workspace::new(None, project1.clone(), app_state.clone(), window, cx)
3915                    });
3916                    let workspace2 = cx.new(|cx| {
3917                        Workspace::new(None, project2.clone(), app_state.clone(), window, cx)
3918                    });
3919                    cx.new(|cx| {
3920                        let mut multi_workspace = MultiWorkspace::new(workspace1, window, cx);
3921                        multi_workspace.activate(workspace2, None, window, cx);
3922                        multi_workspace
3923                    })
3924                },
3925            )
3926        })
3927        .context("Failed to open MultiWorkspace window")?;
3928
3929    cx.run_until_parked();
3930
3931    // Add worktree to workspace 1 (index 0) so it shows as "private-test-remote"
3932    let add_worktree1_task = multi_workspace_window
3933        .update(cx, |multi_workspace, _window, cx| {
3934            let workspace1 = multi_workspace.workspaces().next().unwrap();
3935            let project = workspace1.read(cx).project().clone();
3936            project.update(cx, |project, cx| {
3937                project.find_or_create_worktree(&workspace1_dir, true, cx)
3938            })
3939        })
3940        .context("Failed to start adding worktree 1")?;
3941
3942    cx.background_executor.allow_parking();
3943    cx.foreground_executor
3944        .block_test(add_worktree1_task)
3945        .context("Failed to add worktree 1")?;
3946    cx.background_executor.forbid_parking();
3947
3948    cx.run_until_parked();
3949
3950    // Add worktree to workspace 2 (index 1) so it shows as "zed"
3951    let add_worktree2_task = multi_workspace_window
3952        .update(cx, |multi_workspace, _window, cx| {
3953            let workspace2 = multi_workspace.workspaces().nth(1).unwrap();
3954            let project = workspace2.read(cx).project().clone();
3955            project.update(cx, |project, cx| {
3956                project.find_or_create_worktree(&workspace2_dir, true, cx)
3957            })
3958        })
3959        .context("Failed to start adding worktree 2")?;
3960
3961    cx.background_executor.allow_parking();
3962    cx.foreground_executor
3963        .block_test(add_worktree2_task)
3964        .context("Failed to add worktree 2")?;
3965    cx.background_executor.forbid_parking();
3966
3967    cx.run_until_parked();
3968
3969    // Switch to workspace 1 so it's highlighted as active (index 0)
3970    multi_workspace_window
3971        .update(cx, |multi_workspace, window, cx| {
3972            let workspace = multi_workspace.workspaces().next().unwrap().clone();
3973            multi_workspace.activate(workspace, None, window, cx);
3974        })
3975        .context("Failed to activate workspace 1")?;
3976
3977    cx.run_until_parked();
3978
3979    // Create the sidebar outside the MultiWorkspace update to avoid a
3980    // re-entrant read panic (Sidebar::new reads the MultiWorkspace).
3981    let sidebar = cx
3982        .update_window(multi_workspace_window.into(), |root_view, window, cx| {
3983            let multi_workspace_handle: Entity<MultiWorkspace> = root_view.downcast().unwrap();
3984            cx.new(|cx| sidebar::Sidebar::new(multi_workspace_handle, window, cx))
3985        })
3986        .context("Failed to create sidebar")?;
3987
3988    multi_workspace_window
3989        .update(cx, |multi_workspace, _window, cx| {
3990            multi_workspace.register_sidebar(sidebar.clone(), cx);
3991        })
3992        .context("Failed to register sidebar")?;
3993
3994    cx.run_until_parked();
3995
3996    // Save test threads to the ThreadStore for each workspace
3997    let save_tasks = multi_workspace_window
3998        .update(cx, |multi_workspace, _window, cx| {
3999            let thread_store = agent::ThreadStore::global(cx);
4000            let workspaces: Vec<_> = multi_workspace.workspaces().cloned().collect();
4001            let mut tasks = Vec::new();
4002
4003            for (index, workspace) in workspaces.iter().enumerate() {
4004                let workspace_ref = workspace.read(cx);
4005                let mut paths = Vec::new();
4006                for worktree in workspace_ref.worktrees(cx) {
4007                    let worktree_ref = worktree.read(cx);
4008                    if worktree_ref.is_visible() {
4009                        paths.push(worktree_ref.abs_path().to_path_buf());
4010                    }
4011                }
4012                let path_list = util::path_list::PathList::new(&paths);
4013
4014                let (session_id, title, updated_at) = match index {
4015                    0 => (
4016                        "visual-test-thread-0",
4017                        "Refine thread view scrolling behavior",
4018                        chrono::TimeZone::with_ymd_and_hms(&chrono::Utc, 2024, 6, 15, 10, 30, 0)
4019                            .unwrap(),
4020                    ),
4021                    1 => (
4022                        "visual-test-thread-1",
4023                        "Add line numbers option to FileEditBlock",
4024                        chrono::TimeZone::with_ymd_and_hms(&chrono::Utc, 2024, 6, 15, 11, 0, 0)
4025                            .unwrap(),
4026                    ),
4027                    _ => continue,
4028                };
4029
4030                let task = thread_store.update(cx, |store, cx| {
4031                    store.save_thread(
4032                        acp::SessionId::new(Arc::from(session_id)),
4033                        agent::DbThread {
4034                            title: title.to_string().into(),
4035                            messages: Vec::new(),
4036                            updated_at,
4037                            detailed_summary: None,
4038                            initial_project_snapshot: None,
4039                            cumulative_token_usage: Default::default(),
4040                            request_token_usage: Default::default(),
4041                            model: None,
4042                            profile: None,
4043                            subagent_context: None,
4044                            speed: None,
4045                            thinking_enabled: false,
4046                            thinking_effort: None,
4047                            ui_scroll_position: None,
4048                            draft_prompt: None,
4049                            sandboxed_terminal_temp_dir: None,
4050                            sandbox_grants: Default::default(),
4051                        },
4052                        path_list,
4053                        cx,
4054                    )
4055                });
4056                tasks.push(task);
4057            }
4058            tasks
4059        })
4060        .context("Failed to create test threads")?;
4061
4062    cx.background_executor.allow_parking();
4063    for task in save_tasks {
4064        cx.foreground_executor
4065            .block_test(task)
4066            .context("Failed to save test thread")?;
4067    }
4068    cx.background_executor.forbid_parking();
4069
4070    cx.run_until_parked();
4071
4072    // Open the sidebar
4073    multi_workspace_window
4074        .update(cx, |multi_workspace, window, cx| {
4075            multi_workspace.toggle_sidebar(window, cx);
4076        })
4077        .context("Failed to toggle sidebar")?;
4078
4079    // Let rendering settle
4080    for _ in 0..10 {
4081        cx.advance_clock(Duration::from_millis(100));
4082        cx.run_until_parked();
4083    }
4084
4085    // Refresh the window
4086    cx.update_window(multi_workspace_window.into(), |_, window, _cx| {
4087        window.refresh();
4088    })?;
4089
4090    cx.run_until_parked();
4091
4092    // Capture: sidebar open with active workspaces and recent projects
4093    let test_result = run_visual_test(
4094        "multi_workspace_sidebar_open",
4095        multi_workspace_window.into(),
4096        cx,
4097        update_baseline,
4098    )?;
4099
4100    // Clean up worktrees
4101    multi_workspace_window
4102        .update(cx, |multi_workspace, _window, cx| {
4103            for workspace in multi_workspace.workspaces() {
4104                let project = workspace.read(cx).project().clone();
4105                project.update(cx, |project, cx| {
4106                    let worktree_ids: Vec<_> =
4107                        project.worktrees(cx).map(|wt| wt.read(cx).id()).collect();
4108                    for id in worktree_ids {
4109                        project.remove_worktree(id, cx);
4110                    }
4111                });
4112            }
4113        })
4114        .log_err();
4115
4116    cx.run_until_parked();
4117
4118    // Close the window
4119    cx.update_window(multi_workspace_window.into(), |_, window, _cx| {
4120        window.remove_window();
4121    })
4122    .log_err();
4123
4124    cx.run_until_parked();
4125
4126    for _ in 0..15 {
4127        cx.advance_clock(Duration::from_millis(100));
4128        cx.run_until_parked();
4129    }
4130
4131    Ok(test_result)
4132}
4133
4134#[cfg(target_os = "macos")]
4135struct ErrorWrappingTestView;
4136
4137#[cfg(target_os = "macos")]
4138impl gpui::Render for ErrorWrappingTestView {
4139    fn render(
4140        &mut self,
4141        _window: &mut gpui::Window,
4142        cx: &mut gpui::Context<Self>,
4143    ) -> impl gpui::IntoElement {
4144        use ui::{Button, Callout, IconName, LabelSize, Severity, prelude::*, v_flex};
4145
4146        let long_error_message = "Rate limit reached for gpt-5.2-codex in organization \
4147            org-QmYpir6k6dkULKU1XUSN6pal on tokens per min (TPM): Limit 500000, Used 442480, \
4148            Requested 59724. Please try again in 264ms. Visit \
4149            https://platform.openai.com/account/rate-limits to learn more.";
4150
4151        let retry_description = "Retrying. Next attempt in 4 seconds (Attempt 1 of 2).";
4152
4153        v_flex()
4154            .size_full()
4155            .bg(cx.theme().colors().background)
4156            .p_4()
4157            .gap_4()
4158            .child(
4159                Callout::new()
4160                    .icon(IconName::Warning)
4161                    .severity(Severity::Warning)
4162                    .title(long_error_message)
4163                    .description(retry_description),
4164            )
4165            .child(
4166                Callout::new()
4167                    .severity(Severity::Error)
4168                    .icon(IconName::XCircle)
4169                    .title("An Error Happened")
4170                    .description(long_error_message)
4171                    .actions_slot(Button::new("dismiss", "Dismiss").label_size(LabelSize::Small)),
4172            )
4173            .child(
4174                Callout::new()
4175                    .severity(Severity::Error)
4176                    .icon(IconName::XCircle)
4177                    .title(long_error_message)
4178                    .actions_slot(Button::new("retry", "Retry").label_size(LabelSize::Small)),
4179            )
4180    }
4181}
4182
4183#[cfg(target_os = "macos")]
4184struct ThreadItemBranchNameTestView;
4185
4186#[cfg(target_os = "macos")]
4187impl gpui::Render for ThreadItemBranchNameTestView {
4188    fn render(
4189        &mut self,
4190        _window: &mut gpui::Window,
4191        cx: &mut gpui::Context<Self>,
4192    ) -> impl gpui::IntoElement {
4193        use ui::{
4194            IconName, Label, LabelSize, ThreadItem, ThreadItemWorktreeInfo, WorktreeKind,
4195            prelude::*,
4196        };
4197
4198        let section_label = |text: &str| {
4199            Label::new(text.to_string())
4200                .size(LabelSize::Small)
4201                .color(Color::Muted)
4202        };
4203
4204        let container = || {
4205            v_flex()
4206                .w_80()
4207                .border_1()
4208                .border_color(cx.theme().colors().border_variant)
4209                .bg(cx.theme().colors().panel_background)
4210        };
4211
4212        v_flex()
4213            .size_full()
4214            .bg(cx.theme().colors().background)
4215            .p_4()
4216            .gap_3()
4217            .child(
4218                Label::new("ThreadItem Branch Names")
4219                    .size(LabelSize::Large)
4220                    .color(Color::Default),
4221            )
4222            .child(section_label(
4223                "Linked worktree with branch (worktree / branch)",
4224            ))
4225            .child(
4226                container().child(
4227                    ThreadItem::new("ti-linked-branch", "Fix scrolling behavior")
4228                        .icon(IconName::AiClaude)
4229                        .timestamp("5m")
4230                        .worktrees(vec![ThreadItemWorktreeInfo {
4231                            worktree_name: Some("jade-glen".into()),
4232                            full_path: "/worktrees/jade-glen/zed".into(),
4233                            highlight_positions: Vec::new(),
4234                            kind: WorktreeKind::Linked,
4235                            branch_name: Some("fix-scrolling".into()),
4236                        }]),
4237                ),
4238            )
4239            .child(section_label(
4240                "Linked worktree without branch (detached HEAD)",
4241            ))
4242            .child(
4243                container().child(
4244                    ThreadItem::new("ti-linked-no-branch", "Review worktree cleanup")
4245                        .icon(IconName::AiClaude)
4246                        .timestamp("1h")
4247                        .worktrees(vec![ThreadItemWorktreeInfo {
4248                            worktree_name: Some("focal-arrow".into()),
4249                            full_path: "/worktrees/focal-arrow/zed".into(),
4250                            highlight_positions: Vec::new(),
4251                            kind: WorktreeKind::Linked,
4252                            branch_name: None,
4253                        }]),
4254                ),
4255            )
4256            .child(section_label("Main worktree with branch (nothing shown)"))
4257            .child(
4258                container().child(
4259                    ThreadItem::new("ti-main-branch", "Request for Long Classic Poem")
4260                        .icon(IconName::OmegaAgent)
4261                        .timestamp("2d")
4262                        .worktrees(vec![ThreadItemWorktreeInfo {
4263                            worktree_name: Some("zed".into()),
4264                            full_path: "/projects/zed".into(),
4265                            highlight_positions: Vec::new(),
4266                            kind: WorktreeKind::Main,
4267                            branch_name: Some("main".into()),
4268                        }]),
4269                ),
4270            )
4271            .child(section_label(
4272                "Main worktree without branch (nothing shown)",
4273            ))
4274            .child(
4275                container().child(
4276                    ThreadItem::new("ti-main-no-branch", "Simple greeting thread")
4277                        .icon(IconName::OmegaAgent)
4278                        .timestamp("3d")
4279                        .worktrees(vec![ThreadItemWorktreeInfo {
4280                            worktree_name: Some("zed".into()),
4281                            full_path: "/projects/zed".into(),
4282                            highlight_positions: Vec::new(),
4283                            kind: WorktreeKind::Main,
4284                            branch_name: None,
4285                        }]),
4286                ),
4287            )
4288            .child(section_label("Linked worktree where name matches branch"))
4289            .child(
4290                container().child(
4291                    ThreadItem::new("ti-same-name", "Implement feature")
4292                        .icon(IconName::AiClaude)
4293                        .timestamp("6d")
4294                        .worktrees(vec![ThreadItemWorktreeInfo {
4295                            worktree_name: Some("stoic-reed".into()),
4296                            full_path: "/worktrees/stoic-reed/zed".into(),
4297                            highlight_positions: Vec::new(),
4298                            kind: WorktreeKind::Linked,
4299                            branch_name: Some("stoic-reed".into()),
4300                        }]),
4301                ),
4302            )
4303            .child(section_label(
4304                "Manually opened linked worktree (main_path resolves to original repo)",
4305            ))
4306            .child(
4307                container().child(
4308                    ThreadItem::new("ti-manual-linked", "Robust Git Worktree Rollback")
4309                        .icon(IconName::OmegaAgent)
4310                        .timestamp("40m")
4311                        .worktrees(vec![ThreadItemWorktreeInfo {
4312                            worktree_name: Some("focal-arrow".into()),
4313                            full_path: "/worktrees/focal-arrow/zed".into(),
4314                            highlight_positions: Vec::new(),
4315                            kind: WorktreeKind::Linked,
4316                            branch_name: Some("persist-worktree-3-wiring".into()),
4317                        }]),
4318                ),
4319            )
4320            .child(section_label(
4321                "Linked worktree + branch + diff stats + timestamp",
4322            ))
4323            .child(
4324                container().child(
4325                    ThreadItem::new("ti-linked-full", "Full metadata with diff stats")
4326                        .icon(IconName::AiClaude)
4327                        .timestamp("3w")
4328                        .added(42)
4329                        .removed(17)
4330                        .worktrees(vec![ThreadItemWorktreeInfo {
4331                            worktree_name: Some("jade-glen".into()),
4332                            full_path: "/worktrees/jade-glen/zed".into(),
4333                            highlight_positions: Vec::new(),
4334                            kind: WorktreeKind::Linked,
4335                            branch_name: Some("feature-branch".into()),
4336                        }]),
4337                ),
4338            )
4339            .child(section_label("Long branch name truncation with diff stats"))
4340            .child(
4341                container().child(
4342                    ThreadItem::new("ti-long-branch", "Overflow test with very long branch")
4343                        .icon(IconName::AiClaude)
4344                        .timestamp("2d")
4345                        .added(108)
4346                        .removed(53)
4347                        .worktrees(vec![ThreadItemWorktreeInfo {
4348                            worktree_name: Some("my-project".into()),
4349                            full_path: "/worktrees/my-project/zed".into(),
4350                            highlight_positions: Vec::new(),
4351                            kind: WorktreeKind::Linked,
4352                            branch_name: Some(
4353                                "fix-very-long-branch-name-that-should-truncate".into(),
4354                            ),
4355                        }]),
4356                ),
4357            )
4358            .child(section_label(
4359                "Main worktree with branch + diff stats + timestamp (branch hidden)",
4360            ))
4361            .child(
4362                container().child(
4363                    ThreadItem::new("ti-main-full", "Main worktree with everything")
4364                        .icon(IconName::OmegaAgent)
4365                        .timestamp("5m")
4366                        .added(23)
4367                        .removed(8)
4368                        .worktrees(vec![ThreadItemWorktreeInfo {
4369                            worktree_name: Some("zed".into()),
4370                            full_path: "/projects/zed".into(),
4371                            highlight_positions: Vec::new(),
4372                            kind: WorktreeKind::Main,
4373                            branch_name: Some("sidebar-show-branch-name".into()),
4374                        }]),
4375                ),
4376            )
4377    }
4378}
4379
4380#[cfg(target_os = "macos")]
4381fn run_thread_item_branch_name_visual_tests(
4382    _app_state: Arc<AppState>,
4383    cx: &mut VisualTestAppContext,
4384    update_baseline: bool,
4385) -> Result<TestResult> {
4386    let window_size = size(px(400.0), px(1150.0));
4387    let bounds = Bounds {
4388        origin: point(px(0.0), px(0.0)),
4389        size: window_size,
4390    };
4391
4392    let window = cx
4393        .update(|cx| {
4394            cx.open_window(
4395                WindowOptions {
4396                    window_bounds: Some(WindowBounds::Windowed(bounds)),
4397                    focus: false,
4398                    show: false,
4399                    ..Default::default()
4400                },
4401                |_window, cx| cx.new(|_| ThreadItemBranchNameTestView),
4402            )
4403        })
4404        .context("Failed to open thread item branch name test window")?;
4405
4406    cx.run_until_parked();
4407
4408    cx.update_window(window.into(), |_, window, _cx| {
4409        window.refresh();
4410    })?;
4411
4412    cx.run_until_parked();
4413
4414    let test_result = run_visual_test(
4415        "thread_item_branch_names",
4416        window.into(),
4417        cx,
4418        update_baseline,
4419    )?;
4420
4421    cx.update_window(window.into(), |_, window, _cx| {
4422        window.remove_window();
4423    })
4424    .log_err();
4425
4426    cx.run_until_parked();
4427
4428    for _ in 0..15 {
4429        cx.advance_clock(Duration::from_millis(100));
4430        cx.run_until_parked();
4431    }
4432
4433    Ok(test_result)
4434}
4435
4436#[cfg(target_os = "macos")]
4437struct ThreadItemIconDecorationsTestView;
4438
4439#[cfg(target_os = "macos")]
4440impl gpui::Render for ThreadItemIconDecorationsTestView {
4441    fn render(
4442        &mut self,
4443        _window: &mut gpui::Window,
4444        cx: &mut gpui::Context<Self>,
4445    ) -> impl gpui::IntoElement {
4446        use ui::{IconName, Label, LabelSize, ThreadItem, prelude::*};
4447
4448        let section_label = |text: &str| {
4449            Label::new(text.to_string())
4450                .size(LabelSize::Small)
4451                .color(Color::Muted)
4452        };
4453
4454        let container = || {
4455            v_flex()
4456                .w_80()
4457                .border_1()
4458                .border_color(cx.theme().colors().border_variant)
4459                .bg(cx.theme().colors().panel_background)
4460        };
4461
4462        v_flex()
4463            .size_full()
4464            .bg(cx.theme().colors().background)
4465            .p_4()
4466            .gap_3()
4467            .child(
4468                Label::new("ThreadItem Icon Decorations")
4469                    .size(LabelSize::Large)
4470                    .color(Color::Default),
4471            )
4472            .child(section_label("No decoration (default idle)"))
4473            .child(
4474                container()
4475                    .child(ThreadItem::new("ti-none", "Default idle thread").timestamp("1:00 AM")),
4476            )
4477            .child(section_label("Blue dot (notified)"))
4478            .child(
4479                container().child(
4480                    ThreadItem::new("ti-done", "Generation completed successfully")
4481                        .timestamp("1:05 AM")
4482                        .notified(true),
4483                ),
4484            )
4485            .child(section_label("Yellow triangle (waiting for confirmation)"))
4486            .child(
4487                container().child(
4488                    ThreadItem::new("ti-waiting", "Waiting for user confirmation")
4489                        .timestamp("1:10 AM")
4490                        .status(ui::AgentThreadStatus::WaitingForConfirmation),
4491                ),
4492            )
4493            .child(section_label("Red X (error)"))
4494            .child(
4495                container().child(
4496                    ThreadItem::new("ti-error", "Failed to connect to server")
4497                        .timestamp("1:15 AM")
4498                        .status(ui::AgentThreadStatus::Error),
4499                ),
4500            )
4501            .child(section_label("Spinner (running)"))
4502            .child(
4503                container().child(
4504                    ThreadItem::new("ti-running", "Generating response...")
4505                        .icon(IconName::AiClaude)
4506                        .timestamp("1:20 AM")
4507                        .status(ui::AgentThreadStatus::Running),
4508                ),
4509            )
4510            .child(section_label(
4511                "Spinner + yellow triangle (waiting for confirmation)",
4512            ))
4513            .child(
4514                container().child(
4515                    ThreadItem::new("ti-running-waiting", "Running but needs confirmation")
4516                        .icon(IconName::AiClaude)
4517                        .timestamp("1:25 AM")
4518                        .status(ui::AgentThreadStatus::WaitingForConfirmation),
4519                ),
4520            )
4521    }
4522}
4523
4524#[cfg(target_os = "macos")]
4525fn run_thread_item_icon_decorations_visual_tests(
4526    _app_state: Arc<AppState>,
4527    cx: &mut VisualTestAppContext,
4528    update_baseline: bool,
4529) -> Result<TestResult> {
4530    let window_size = size(px(400.0), px(600.0));
4531    let bounds = Bounds {
4532        origin: point(px(0.0), px(0.0)),
4533        size: window_size,
4534    };
4535
4536    let window = cx
4537        .update(|cx| {
4538            cx.open_window(
4539                WindowOptions {
4540                    window_bounds: Some(WindowBounds::Windowed(bounds)),
4541                    focus: false,
4542                    show: false,
4543                    ..Default::default()
4544                },
4545                |_window, cx| cx.new(|_| ThreadItemIconDecorationsTestView),
4546            )
4547        })
4548        .context("Failed to open thread item icon decorations test window")?;
4549
4550    cx.run_until_parked();
4551
4552    cx.update_window(window.into(), |_, window, _cx| {
4553        window.refresh();
4554    })?;
4555
4556    cx.run_until_parked();
4557
4558    let test_result = run_visual_test(
4559        "thread_item_icon_decorations",
4560        window.into(),
4561        cx,
4562        update_baseline,
4563    )?;
4564
4565    cx.update_window(window.into(), |_, window, _cx| {
4566        window.remove_window();
4567    })
4568    .log_err();
4569
4570    cx.run_until_parked();
4571
4572    for _ in 0..15 {
4573        cx.advance_clock(Duration::from_millis(100));
4574        cx.run_until_parked();
4575    }
4576
4577    Ok(test_result)
4578}
4579
4580#[cfg(target_os = "macos")]
4581fn run_error_wrapping_visual_tests(
4582    _app_state: Arc<AppState>,
4583    cx: &mut VisualTestAppContext,
4584    update_baseline: bool,
4585) -> Result<TestResult> {
4586    let window_size = size(px(500.0), px(400.0));
4587    let bounds = Bounds {
4588        origin: point(px(0.0), px(0.0)),
4589        size: window_size,
4590    };
4591
4592    let window = cx
4593        .update(|cx| {
4594            cx.open_window(
4595                WindowOptions {
4596                    window_bounds: Some(WindowBounds::Windowed(bounds)),
4597                    focus: false,
4598                    show: false,
4599                    ..Default::default()
4600                },
4601                |_window, cx| cx.new(|_| ErrorWrappingTestView),
4602            )
4603        })
4604        .context("Failed to open error wrapping test window")?;
4605
4606    cx.run_until_parked();
4607
4608    cx.update_window(window.into(), |_, window, _cx| {
4609        window.refresh();
4610    })?;
4611
4612    cx.run_until_parked();
4613
4614    let test_result =
4615        run_visual_test("error_message_wrapping", window.into(), cx, update_baseline)?;
4616
4617    cx.update_window(window.into(), |_, window, _cx| {
4618        window.remove_window();
4619    })
4620    .log_err();
4621
4622    cx.run_until_parked();
4623
4624    for _ in 0..15 {
4625        cx.advance_clock(Duration::from_millis(100));
4626        cx.run_until_parked();
4627    }
4628
4629    Ok(test_result)
4630}
4631
4632#[cfg(target_os = "macos")]
4633/// Helper to create a project, add a worktree at the given path, and return the project.
4634fn create_project_with_worktree(
4635    worktree_dir: &Path,
4636    app_state: &Arc<AppState>,
4637    cx: &mut VisualTestAppContext,
4638) -> Result<Entity<Project>> {
4639    let project = cx.update(|cx| {
4640        project::Project::local(
4641            app_state.client.clone(),
4642            app_state.node_runtime.clone(),
4643            app_state.user_store.clone(),
4644            app_state.languages.clone(),
4645            app_state.fs.clone(),
4646            None,
4647            project::LocalProjectFlags {
4648                init_worktree_trust: false,
4649                ..Default::default()
4650            },
4651            cx,
4652        )
4653    });
4654
4655    let add_task = cx.update(|cx| {
4656        project.update(cx, |project, cx| {
4657            project.find_or_create_worktree(worktree_dir, true, cx)
4658        })
4659    });
4660
4661    cx.background_executor.allow_parking();
4662    cx.foreground_executor
4663        .block_test(add_task)
4664        .context("Failed to add worktree")?;
4665    cx.background_executor.forbid_parking();
4666
4667    cx.run_until_parked();
4668    Ok(project)
4669}
4670
4671#[cfg(target_os = "macos")]
4672fn open_sidebar_test_window(
4673    projects: Vec<Entity<Project>>,
4674    app_state: &Arc<AppState>,
4675    cx: &mut VisualTestAppContext,
4676) -> Result<WindowHandle<MultiWorkspace>> {
4677    anyhow::ensure!(!projects.is_empty(), "need at least one project");
4678
4679    let window_size = size(px(400.0), px(600.0));
4680    let bounds = Bounds {
4681        origin: point(px(0.0), px(0.0)),
4682        size: window_size,
4683    };
4684
4685    let mut projects_iter = projects.into_iter();
4686    let first_project = projects_iter
4687        .next()
4688        .ok_or_else(|| anyhow::anyhow!("need at least one project"))?;
4689    let remaining: Vec<_> = projects_iter.collect();
4690
4691    let multi_workspace_window: WindowHandle<MultiWorkspace> = cx
4692        .update(|cx| {
4693            cx.open_window(
4694                WindowOptions {
4695                    window_bounds: Some(WindowBounds::Windowed(bounds)),
4696                    focus: false,
4697                    show: false,
4698                    ..Default::default()
4699                },
4700                |window, cx| {
4701                    let first_ws = cx.new(|cx| {
4702                        Workspace::new(None, first_project.clone(), app_state.clone(), window, cx)
4703                    });
4704                    cx.new(|cx| {
4705                        let mut mw = MultiWorkspace::new(first_ws, window, cx);
4706                        for project in remaining {
4707                            let ws = cx.new(|cx| {
4708                                Workspace::new(None, project, app_state.clone(), window, cx)
4709                            });
4710                            mw.activate(ws, None, window, cx);
4711                        }
4712                        mw
4713                    })
4714                },
4715            )
4716        })
4717        .context("Failed to open MultiWorkspace window")?;
4718
4719    cx.run_until_parked();
4720
4721    // Create the sidebar outside the MultiWorkspace update to avoid a
4722    // re-entrant read panic (Sidebar::new reads the MultiWorkspace).
4723    let sidebar = cx
4724        .update_window(multi_workspace_window.into(), |root_view, window, cx| {
4725            let mw_handle: Entity<MultiWorkspace> = root_view
4726                .downcast()
4727                .map_err(|_| anyhow::anyhow!("Failed to downcast root view to MultiWorkspace"))?;
4728            Ok::<_, anyhow::Error>(cx.new(|cx| sidebar::Sidebar::new(mw_handle, window, cx)))
4729        })
4730        .context("Failed to create sidebar")??;
4731
4732    multi_workspace_window
4733        .update(cx, |mw, _window, cx| {
4734            mw.register_sidebar(sidebar.clone(), cx);
4735        })
4736        .context("Failed to register sidebar")?;
4737
4738    cx.run_until_parked();
4739
4740    // Open the sidebar
4741    multi_workspace_window
4742        .update(cx, |mw, window, cx| {
4743            mw.toggle_sidebar(window, cx);
4744        })
4745        .context("Failed to toggle sidebar")?;
4746
4747    // Let rendering settle
4748    for _ in 0..10 {
4749        cx.advance_clock(Duration::from_millis(100));
4750        cx.run_until_parked();
4751    }
4752
4753    // Refresh the window
4754    cx.update_window(multi_workspace_window.into(), |_, window, _cx| {
4755        window.refresh();
4756    })?;
4757
4758    cx.run_until_parked();
4759
4760    Ok(multi_workspace_window)
4761}
4762
4763#[cfg(target_os = "macos")]
4764fn cleanup_sidebar_test_window(
4765    window: WindowHandle<MultiWorkspace>,
4766    cx: &mut VisualTestAppContext,
4767) -> Result<()> {
4768    window.update(cx, |mw, _window, cx| {
4769        for workspace in mw.workspaces() {
4770            let project = workspace.read(cx).project().clone();
4771            project.update(cx, |project, cx| {
4772                let ids: Vec<_> = project.worktrees(cx).map(|wt| wt.read(cx).id()).collect();
4773                for id in ids {
4774                    project.remove_worktree(id, cx);
4775                }
4776            });
4777        }
4778    })?;
4779
4780    cx.run_until_parked();
4781
4782    cx.update_window(window.into(), |_, window, _cx| {
4783        window.remove_window();
4784    })?;
4785
4786    cx.run_until_parked();
4787
4788    for _ in 0..15 {
4789        cx.advance_clock(Duration::from_millis(100));
4790        cx.run_until_parked();
4791    }
4792
4793    Ok(())
4794}
4795
4796#[cfg(target_os = "macos")]
4797fn run_sidebar_duplicate_project_names_visual_tests(
4798    app_state: Arc<AppState>,
4799    cx: &mut VisualTestAppContext,
4800    update_baseline: bool,
4801) -> Result<TestResult> {
4802    let temp_dir = tempfile::tempdir()?;
4803    let temp_path = temp_dir.keep();
4804    let canonical_temp = temp_path.canonicalize()?;
4805
4806    // Create directory structure where every leaf directory is named "zed" but
4807    // lives at a distinct path. This lets us test that the sidebar correctly
4808    // disambiguates projects whose names would otherwise collide.
4809    //
4810    //   code/zed/       — project1 (single worktree)
4811    //   code/foo/zed/   — project2 (single worktree)
4812    //   code/bar/zed/   — project3, first worktree
4813    //   code/baz/zed/   — project3, second worktree
4814    //
4815    // No two projects share a worktree path, so ProjectGroupBuilder will
4816    // place each in its own group.
4817    let code_zed = canonical_temp.join("code").join("zed");
4818    let foo_zed = canonical_temp.join("code").join("foo").join("zed");
4819    let bar_zed = canonical_temp.join("code").join("bar").join("zed");
4820    let baz_zed = canonical_temp.join("code").join("baz").join("zed");
4821    std::fs::create_dir_all(&code_zed)?;
4822    std::fs::create_dir_all(&foo_zed)?;
4823    std::fs::create_dir_all(&bar_zed)?;
4824    std::fs::create_dir_all(&baz_zed)?;
4825
4826    cx.update(|cx| {
4827        cx.update_flags(true, vec!["agent-v2".to_string()]);
4828    });
4829
4830    let mut has_baseline_update = None;
4831
4832    // Two single-worktree projects whose leaf name is "zed"
4833    {
4834        let project1 = create_project_with_worktree(&code_zed, &app_state, cx)?;
4835        let project2 = create_project_with_worktree(&foo_zed, &app_state, cx)?;
4836
4837        let window = open_sidebar_test_window(vec![project1, project2], &app_state, cx)?;
4838
4839        let result = run_visual_test(
4840            "sidebar_two_projects_same_leaf_name",
4841            window.into(),
4842            cx,
4843            update_baseline,
4844        );
4845
4846        cleanup_sidebar_test_window(window, cx)?;
4847        match result? {
4848            TestResult::Passed => {}
4849            TestResult::BaselineUpdated(path) => {
4850                has_baseline_update = Some(path);
4851            }
4852        }
4853    }
4854
4855    // Three projects, third has two worktrees (all leaf names "zed")
4856    //
4857    // project1: code/zed
4858    // project2: code/foo/zed
4859    // project3: code/bar/zed + code/baz/zed
4860    //
4861    // Each project has a unique set of worktree paths, so they form
4862    // separate groups. The sidebar must disambiguate all three.
4863    {
4864        let project1 = create_project_with_worktree(&code_zed, &app_state, cx)?;
4865        let project2 = create_project_with_worktree(&foo_zed, &app_state, cx)?;
4866
4867        let project3 = create_project_with_worktree(&bar_zed, &app_state, cx)?;
4868        let add_second_worktree = cx.update(|cx| {
4869            project3.update(cx, |project, cx| {
4870                project.find_or_create_worktree(&baz_zed, true, cx)
4871            })
4872        });
4873        cx.background_executor.allow_parking();
4874        cx.foreground_executor
4875            .block_test(add_second_worktree)
4876            .context("Failed to add second worktree to project 3")?;
4877        cx.background_executor.forbid_parking();
4878        cx.run_until_parked();
4879
4880        let window = open_sidebar_test_window(vec![project1, project2, project3], &app_state, cx)?;
4881
4882        let result = run_visual_test(
4883            "sidebar_three_projects_with_multi_worktree",
4884            window.into(),
4885            cx,
4886            update_baseline,
4887        );
4888
4889        cleanup_sidebar_test_window(window, cx)?;
4890        match result? {
4891            TestResult::Passed => {}
4892            TestResult::BaselineUpdated(path) => {
4893                has_baseline_update = Some(path);
4894            }
4895        }
4896    }
4897
4898    if let Some(path) = has_baseline_update {
4899        Ok(TestResult::BaselineUpdated(path))
4900    } else {
4901        Ok(TestResult::Passed)
4902    }
4903}
4904
4905/// Visual test for harness maintenance on the External Agents settings page.
4906/// omega#81, `OMEGA-DELTA-0033`.
4907///
4908/// The gap omega#81 stayed open for was that every maintenance decision existed
4909/// and nothing rendered one: a refusal reached the owner only as agent-launch
4910/// error text, and there was no control to take or remove a pin. So the proof
4911/// that gap is closed has to be a picture of the row.
4912///
4913/// Nothing here is synthetic state handed to a widget. Two registry agents are
4914/// registered, their installed trees are written to the directories the launch
4915/// path measures, one is pinned to a different version through the same
4916/// `pin_installed_harness` the button calls, and the page is then opened and
4917/// photographed. What the screenshot shows is what the enforcement path
4918/// decided.
4919#[cfg(target_os = "macos")]
4920fn run_external_agent_maintenance_visual_tests(
4921    app_state: Arc<AppState>,
4922    cx: &mut VisualTestAppContext,
4923    update_baseline: bool,
4924) -> Result<TestResult> {
4925    use ::collections::HashMap;
4926    use gpui::UpdateGlobal as _;
4927    use project::agent_registry_store::{
4928        AgentRegistryStore, RegistryAgent, RegistryAgentMetadata, RegistryBinaryAgent,
4929        RegistryTargetConfig,
4930    };
4931    use settings::SettingsStore;
4932
4933    const HEALTHY: &str = "omega-visual-harness";
4934    const PINNED: &str = "omega-visual-pinned-harness";
4935
4936    let platform_key = format!(
4937        "{}-{}",
4938        if cfg!(target_os = "macos") {
4939            "darwin"
4940        } else {
4941            "linux"
4942        },
4943        if cfg!(target_arch = "aarch64") {
4944            "aarch64"
4945        } else {
4946            "x86_64"
4947        }
4948    );
4949
4950    let registry_agent = |id: &str, name: &str, version: &str| {
4951        let mut targets = HashMap::default();
4952        targets.insert(
4953            platform_key.clone(),
4954            RegistryTargetConfig {
4955                archive: format!("https://example.invalid/{id}-{version}.tar.gz"),
4956                cmd: "./harness".to_string(),
4957                args: vec![],
4958                sha256: None,
4959                env: HashMap::default(),
4960            },
4961        );
4962        RegistryAgent::Binary(RegistryBinaryAgent {
4963            metadata: RegistryAgentMetadata {
4964                id: AgentId(id.to_string().into()),
4965                name: name.into(),
4966                description: "A wrapped ACP harness.".into(),
4967                version: version.into(),
4968                repository: None,
4969                website: None,
4970                icon_path: None,
4971            },
4972            targets,
4973            supports_current_platform: true,
4974        })
4975    };
4976
4977    cx.update(|cx| {
4978        AgentRegistryStore::init_test_global(
4979            cx,
4980            vec![
4981                registry_agent(HEALTHY, "Visual Harness", "1.0.0"),
4982                // The registry now offers 1.1.0; the pin below freezes 1.0.0,
4983                // so this row is the refusal an owner has to be able to read.
4984                registry_agent(PINNED, "Pinned Harness", "1.1.0"),
4985            ],
4986        );
4987    });
4988
4989    cx.update(|cx| {
4990        SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
4991            store.update_user_settings(cx, |content| {
4992                let agent_servers = content.agent_servers.get_or_insert_default();
4993                for id in [HEALTHY, PINNED] {
4994                    agent_servers.insert(
4995                        id.to_string(),
4996                        settings::CustomAgentServerSettings::Registry {
4997                            default_mode: None,
4998                            env: Default::default(),
4999                            default_config_options: Default::default(),
5000                            favorite_config_option_values: Default::default(),
5001                        },
5002                    );
5003                }
5004            });
5005        });
5006    });
5007
5008    // `OpenSettingsAt` reuses an open settings window, and an earlier test's
5009    // window is bound to an earlier test's project — which has no external
5010    // agents. Close them so the window this test opens is the one it set up.
5011    let existing: Vec<gpui::AnyWindowHandle> = cx
5012        .update(|cx| cx.windows())
5013        .into_iter()
5014        .filter(|window| window.downcast::<SettingsWindow>().is_some())
5015        .collect();
5016    for window in existing {
5017        cx.update_window(window, |_, window, _cx| {
5018            window.remove_window();
5019        })
5020        .log_err();
5021    }
5022    cx.run_until_parked();
5023
5024    let window_size = size(px(900.0), px(700.0));
5025    let bounds = Bounds {
5026        origin: point(px(0.0), px(0.0)),
5027        size: window_size,
5028    };
5029
5030    let project = cx.update(|cx| {
5031        project::Project::local(
5032            app_state.client.clone(),
5033            app_state.node_runtime.clone(),
5034            app_state.user_store.clone(),
5035            app_state.languages.clone(),
5036            app_state.fs.clone(),
5037            None,
5038            project::LocalProjectFlags {
5039                init_worktree_trust: false,
5040                ..Default::default()
5041            },
5042            cx,
5043        )
5044    });
5045
5046    cx.run_until_parked();
5047
5048    // The store owns the derivation of the measured tree, so the test asks it
5049    // rather than re-deriving one. A test that guessed the directory would
5050    // prove a row about a tree the launch path never looks at.
5051    let targets: Vec<(String, PathBuf, String)> = cx.update(|cx| {
5052        let store = project.read(cx).agent_server_store().clone();
5053        let store = store.read(cx);
5054        [HEALTHY, PINNED]
5055            .into_iter()
5056            .filter_map(|id| {
5057                let target = store.maintenance_target(&AgentId(id.to_string().into()))?;
5058                Some((
5059                    id.to_string(),
5060                    target.installed_dir?,
5061                    target.version.to_string(),
5062                ))
5063            })
5064            .collect()
5065    });
5066    anyhow::ensure!(
5067        targets.len() == 2,
5068        "the store did not offer a maintenance target for both registry agents"
5069    );
5070
5071    for (_, dir, version) in &targets {
5072        std::fs::create_dir_all(dir)?;
5073        std::fs::write(dir.join("harness"), format!("harness {version}"))?;
5074        std::fs::write(dir.join("LICENSE"), b"Apache-2.0")?;
5075    }
5076
5077    // Attest the healthy one and freeze the other, through the production
5078    // functions the settings buttons call.
5079    let fs = app_state.fs.clone();
5080    let healthy = targets[0].clone();
5081    let pinned = targets[1].clone();
5082    // Driven on the test executor rather than by blocking this thread: the
5083    // filesystem work these functions do is scheduled by that executor, so
5084    // blocking the thread it runs on deadlocks instead of waiting.
5085    // `RealFs` does its work on real IO threads, so the deterministic scheduler
5086    // has to be allowed to park while it waits for them. This test runs last.
5087    cx.executor().allow_parking();
5088
5089    let outcome: Arc<std::sync::Mutex<Option<Result<()>>>> = Arc::new(std::sync::Mutex::new(None));
5090    let setup = cx.update(|cx| {
5091        let outcome = outcome.clone();
5092        cx.background_spawn(async move {
5093            let result = async {
5094                project::harness_maintenance::reprobe_installed_harness(
5095                    fs.clone(),
5096                    &healthy.0,
5097                    &healthy.2,
5098                    &healthy.1,
5099                    1_784_894_400_000,
5100                )
5101                .await?;
5102                project::harness_maintenance::pin_installed_harness(
5103                    fs.clone(),
5104                    &pinned.0,
5105                    // Frozen at 1.0.0 while the registry advertises 1.1.0.
5106                    "1.0.0",
5107                    &pinned.1,
5108                    1_784_894_400_001,
5109                )
5110                .await?;
5111                anyhow::Ok(())
5112            }
5113            .await;
5114            *outcome.lock().unwrap() = Some(result);
5115        })
5116    });
5117    for _ in 0..200 {
5118        cx.run_until_parked();
5119        if outcome.lock().unwrap().is_some() {
5120            break;
5121        }
5122        std::thread::sleep(Duration::from_millis(20));
5123    }
5124    setup.detach();
5125    match outcome.lock().unwrap().take() {
5126        Some(Ok(())) => {}
5127        Some(Err(error)) => return Err(error).context("preparing harness maintenance state"),
5128        None => anyhow::bail!("harness maintenance setup did not finish"),
5129    }
5130
5131    let workspace_window: WindowHandle<MultiWorkspace> = cx
5132        .update(|cx| {
5133            cx.open_window(
5134                WindowOptions {
5135                    window_bounds: Some(WindowBounds::Windowed(bounds)),
5136                    focus: false,
5137                    show: false,
5138                    ..Default::default()
5139                },
5140                |window, cx| {
5141                    let workspace = cx.new(|cx| {
5142                        Workspace::new(None, project.clone(), app_state.clone(), window, cx)
5143                    });
5144                    cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
5145                },
5146            )
5147        })
5148        .context("Failed to open workspace window for external agents test")?;
5149
5150    cx.run_until_parked();
5151
5152    workspace_window
5153        .update(cx, |_workspace, window, cx| {
5154            window.dispatch_action(
5155                Box::new(OpenSettingsAt {
5156                    path: "agent_servers".to_string(),
5157                    target: None,
5158                }),
5159                cx,
5160            );
5161        })
5162        .context("Failed to dispatch OpenSettingsAt for external agents")?;
5163
5164    cx.run_until_parked();
5165    for _ in 0..10 {
5166        cx.advance_clock(Duration::from_millis(50));
5167        cx.run_until_parked();
5168    }
5169
5170    // Earlier tests leave their own settings windows open, so "the newest
5171    // window" is not reliably this test's. Take the newest one that is a
5172    // settings window.
5173    let all_windows = cx.update(|cx| cx.windows());
5174    let (settings_window, settings_window_handle) = all_windows
5175        .iter()
5176        .rev()
5177        .find_map(|window| Some((*window, window.downcast::<SettingsWindow>()?)))
5178        .context("No settings window found")?;
5179
5180    settings_window_handle
5181        .update(cx, |settings_window, window, cx| {
5182            settings_window.navigate_to_sub_page("agent_servers", window, cx);
5183            settings_window.refresh_harness_maintenance_for_test(cx);
5184        })
5185        .context("Failed to navigate to the External Agents sub-page")?;
5186
5187    cx.run_until_parked();
5188    for _ in 0..20 {
5189        cx.advance_clock(Duration::from_millis(50));
5190        cx.run_until_parked();
5191    }
5192
5193    let result = run_visual_test(
5194        "external_agent_harness_maintenance",
5195        settings_window,
5196        cx,
5197        update_baseline,
5198    )?;
5199
5200    cx.update_window(settings_window, |_, window, _cx| {
5201        window.remove_window();
5202    })
5203    .log_err();
5204    cx.update_window(workspace_window.into(), |_, window, _cx| {
5205        window.remove_window();
5206    })
5207    .log_err();
5208    cx.run_until_parked();
5209
5210    Ok(result)
5211}
5212
Served at tenant.openagents/omega Member data and write actions are omitted.