|
| 1 | +//! Cold-reboot round-trip for co-tenant workspaces (multiple windows over one |
| 2 | +//! project root). |
| 3 | +//! |
| 4 | +//! This is the invariant the interactive Orchestrator dock could not be |
| 5 | +//! scripted to confirm: extract a tab into a second window over the *same* |
| 6 | +//! root, quit, relaunch — and each of the two co-tenant windows must restore |
| 7 | +//! its OWN file. The source keeps `alpha`, the extracted window keeps `beta`. |
| 8 | +//! Without a durable per-window identity the two on-disk files would collapse |
| 9 | +//! (both loading the freshest snapshot, so both showing `beta`) — exactly the |
| 10 | +//! "same buffer opened twice" hazard multiple-workspaces-per-root has to avoid. |
| 11 | +//! |
| 12 | +//! Lives in its own integration binary because it sets the process-global |
| 13 | +//! `XDG_DATA_HOME` to isolate persistence: workspace save/load key off |
| 14 | +//! `$XDG_DATA_HOME/fresh`, and the editor's boot discovery reads the same |
| 15 | +//! `DirectoryContext::data_dir`, so both must point at one isolated tree. A |
| 16 | +//! shared-process test binary (e.g. the big `e2e_tests`) can't host that |
| 17 | +//! global mutation without poisoning its siblings. Linux-gated: |
| 18 | +//! `dirs::data_dir()` ignores `XDG_DATA_HOME` off Linux. |
| 19 | +#![cfg(target_os = "linux")] |
| 20 | + |
| 21 | +use fresh::config::Config; |
| 22 | +use fresh::config_io::DirectoryContext; |
| 23 | +use fresh::model::filesystem::StdFileSystem; |
| 24 | +use std::collections::BTreeSet; |
| 25 | +use std::path::{Path, PathBuf}; |
| 26 | +use std::sync::Arc; |
| 27 | + |
| 28 | +/// Isolate ALL editor persistence into `base`: `$XDG_DATA_HOME/fresh` is where |
| 29 | +/// workspace save/load live, and the returned `DirectoryContext`'s `data_dir` |
| 30 | +/// is the SAME path — so session-1 saves and session-2 boot discovery agree, |
| 31 | +/// inside the test's temp tree. |
| 32 | +fn isolated_dir_context(base: &Path) -> DirectoryContext { |
| 33 | + let xdg_data = base.join("xdg-data"); |
| 34 | + std::fs::create_dir_all(&xdg_data).unwrap(); |
| 35 | + std::env::set_var("XDG_DATA_HOME", &xdg_data); |
| 36 | + DirectoryContext { |
| 37 | + data_dir: xdg_data.join("fresh"), |
| 38 | + config_dir: base.join("config"), |
| 39 | + home_dir: Some(base.join("home")), |
| 40 | + documents_dir: None, |
| 41 | + downloads_dir: None, |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +fn editor_in(project: &Path, dir_context: &DirectoryContext) -> fresh::app::Editor { |
| 46 | + let filesystem: Arc<dyn fresh::model::filesystem::FileSystem + Send + Sync> = |
| 47 | + Arc::new(StdFileSystem); |
| 48 | + let config = Config { |
| 49 | + check_for_updates: false, |
| 50 | + ..Config::default() |
| 51 | + }; |
| 52 | + fresh::app::Editor::for_test( |
| 53 | + config, |
| 54 | + 80, |
| 55 | + 24, |
| 56 | + Some(project.to_path_buf()), |
| 57 | + dir_context.clone(), |
| 58 | + fresh::view::color_support::ColorCapability::TrueColor, |
| 59 | + filesystem, |
| 60 | + None, |
| 61 | + None, |
| 62 | + false, |
| 63 | + false, |
| 64 | + ) |
| 65 | + .unwrap() |
| 66 | +} |
| 67 | + |
| 68 | +#[test] |
| 69 | +fn co_tenants_persist_and_restore_each_own_file() { |
| 70 | + let sandbox = tempfile::tempdir().unwrap(); |
| 71 | + let dir_context = isolated_dir_context(sandbox.path()); |
| 72 | + let project = sandbox.path().join("project"); |
| 73 | + std::fs::create_dir(&project).unwrap(); |
| 74 | + let project = project.canonicalize().unwrap(); |
| 75 | + let alpha = project.join("alpha.txt"); |
| 76 | + let beta = project.join("beta.txt"); |
| 77 | + std::fs::write(&alpha, "alpha\n").unwrap(); |
| 78 | + std::fs::write(&beta, "beta\n").unwrap(); |
| 79 | + |
| 80 | + // Session 1: open both files (beta focused last), extract beta into a |
| 81 | + // co-tenant over the same root, then persist every window. |
| 82 | + { |
| 83 | + let mut e1 = editor_in(&project, &dir_context); |
| 84 | + e1.open_file(&alpha).unwrap(); |
| 85 | + e1.open_file(&beta).unwrap(); |
| 86 | + let beta_buffer = e1.active_buffer(); |
| 87 | + e1.extract_tab_to_new_workspace(beta_buffer); |
| 88 | + // Source window keeps alpha; the new co-tenant took beta. |
| 89 | + e1.save_all_windows_workspaces().unwrap(); |
| 90 | + } |
| 91 | + |
| 92 | + // Two distinct on-disk workspace files now describe this one root. |
| 93 | + let workspaces_dir = dir_context.data_dir.join("workspaces"); |
| 94 | + let files: Vec<_> = std::fs::read_dir(&workspaces_dir) |
| 95 | + .unwrap() |
| 96 | + .filter_map(|e| e.ok()) |
| 97 | + .filter(|e| e.path().extension().is_some_and(|x| x == "json")) |
| 98 | + .collect(); |
| 99 | + assert_eq!( |
| 100 | + files.len(), |
| 101 | + 2, |
| 102 | + "two co-tenant windows must persist as two distinct workspace files, got: {:?}", |
| 103 | + files.iter().map(|e| e.file_name()).collect::<Vec<_>>() |
| 104 | + ); |
| 105 | + |
| 106 | + // Session 2: cold reboot at the same root. Boot discovery rebuilds both |
| 107 | + // co-tenant windows; restore the foreground (as a real launch does) and |
| 108 | + // lazily materialize the background co-tenant. |
| 109 | + let mut e2 = editor_in(&project, &dir_context); |
| 110 | + e2.restore_active_window_on_launch(false).unwrap(); |
| 111 | + e2.materialize_all_windows(); |
| 112 | + |
| 113 | + let mut file_sets: Vec<BTreeSet<PathBuf>> = Vec::new(); |
| 114 | + for id in 1..=64u64 { |
| 115 | + if let Some(w) = e2.session(fresh_core::WindowId(id)) { |
| 116 | + if w.root != project { |
| 117 | + continue; |
| 118 | + } |
| 119 | + let paths: BTreeSet<PathBuf> = w.buffers.paths().into_iter().collect(); |
| 120 | + if !paths.is_empty() { |
| 121 | + file_sets.push(paths); |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + file_sets.sort(); |
| 126 | + |
| 127 | + // Each co-tenant restored its OWN file: one window holds alpha, the other |
| 128 | + // beta — not two copies of the freshest snapshot, and not a single |
| 129 | + // survivor with the other silently dropped. |
| 130 | + assert_eq!( |
| 131 | + file_sets, |
| 132 | + vec![ |
| 133 | + BTreeSet::from([alpha.clone()]), |
| 134 | + BTreeSet::from([beta.clone()]), |
| 135 | + ], |
| 136 | + "each restored co-tenant must reopen exactly its own file" |
| 137 | + ); |
| 138 | +} |
0 commit comments