mod common;
use fresh::config::Config;
use fresh::workspace::{get_workspace_path, Workspace};
use std::path::Path;
use tempfile::TempDir;
use common::harness::EditorTestHarness;
fn read_workspace(working_dir: &Path) -> Option<Workspace> {
let path = get_workspace_path(working_dir).ok()?;
let bytes = std::fs::read(path).ok()?;
serde_json::from_slice(&bytes).ok()
}
fn close_unnamed_buffers(harness: &mut EditorTestHarness) {
let ids: Vec<_> = harness
.editor()
.active_window()
.buffer_metadata
.iter()
.filter_map(|(id, m)| {
let path_empty = m
.file_path()
.map(|p| p.as_os_str().is_empty())
.unwrap_or(true);
let is_file_kind = m.file_path().is_some();
if is_file_kind && path_empty {
Some(*id)
} else {
None
}
})
.collect();
for id in ids {
let _ = harness.editor_mut().force_close_buffer(id);
}
}
#[test]
fn save_with_only_virtual_buffer_does_not_clobber_real_workspace() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().join("project");
std::fs::create_dir(&project_dir).unwrap();
let project_dir = project_dir.canonicalize().unwrap();
let real_file = project_dir.join("kept.txt");
std::fs::write(&real_file, "important user content").unwrap();
let mut harness = EditorTestHarness::with_config_and_working_dir(
80,
24,
Config::default(),
project_dir.clone(),
)
.unwrap();
harness.open_file(&real_file).unwrap();
harness.editor_mut().save_workspace().unwrap();
let initial = read_workspace(&project_dir).expect("first save should write the workspace");
assert!(
!initial.has_no_real_content(),
"sanity: first save must record the open file"
);
let _virtual_id = harness
.editor_mut()
.active_window_mut()
.create_virtual_buffer("Dashboard".to_string(), "dashboard".to_string(), true);
close_unnamed_buffers(&mut harness);
let real_id = harness
.editor()
.active_window()
.buffer_metadata
.iter()
.find(|(_, m)| {
m.file_path()
.map(|p| p.ends_with("kept.txt"))
.unwrap_or(false)
})
.map(|(id, _)| *id)
.expect("real file buffer must exist after open_file");
harness.editor_mut().force_close_buffer(real_id).unwrap();
harness.editor_mut().save_workspace().unwrap();
let after = read_workspace(&project_dir)
.expect("workspace file must still exist; the guard skips the write, not delete it");
assert!(
!after.has_no_real_content(),
"all-virtual save must NOT clobber the real workspace (issue #2027); got empty workspace"
);
let after_files: Vec<_> = after
.split_states
.values()
.flat_map(|s| s.open_tabs.iter().cloned())
.collect();
assert!(
!after_files.is_empty(),
"previous open_tabs must be preserved after the no-op save"
);
}
fn pty_available() -> bool {
use portable_pty::{native_pty_system, PtySize};
native_pty_system()
.openpty(PtySize {
rows: 1,
cols: 1,
pixel_width: 0,
pixel_height: 0,
})
.is_ok()
}
#[test]
fn closing_restored_terminal_with_only_dashboard_drops_it_from_workspace() {
if !pty_available() {
eprintln!("Skipping terminal workspace test: PTY not available");
return;
}
let temp = TempDir::new().unwrap();
let project_dir = temp.path().join("project");
std::fs::create_dir(&project_dir).unwrap();
let project_dir = project_dir.canonicalize().unwrap();
let mut harness = EditorTestHarness::with_config_and_working_dir(
80,
24,
Config::default(),
project_dir.clone(),
)
.unwrap();
harness.editor_mut().open_terminal();
let terminal_id = harness.editor().active_buffer();
close_unnamed_buffers(&mut harness);
harness.editor_mut().save_workspace().unwrap();
let initial = read_workspace(&project_dir).expect("first save should write the workspace");
assert_eq!(
initial.terminals.len(),
1,
"sanity: first save must record the open terminal"
);
assert!(
initial.has_no_preservable_content(),
"sanity: the only on-disk content is the terminal (nothing to preserve)"
);
harness
.editor_mut()
.active_window_mut()
.create_virtual_buffer("Dashboard".to_string(), "dashboard".to_string(), true);
close_unnamed_buffers(&mut harness);
harness
.editor_mut()
.force_close_buffer(terminal_id)
.unwrap();
harness.editor_mut().save_workspace().unwrap();
let after = read_workspace(&project_dir).expect("workspace file must still exist");
assert!(
after.terminals.is_empty(),
"a closed terminal must not survive in the saved workspace (it would be \
resurrected on the next restart); got {} terminal(s)",
after.terminals.len()
);
}
#[test]
fn closing_real_files_without_virtual_buffer_overwrites_workspace() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().join("project");
std::fs::create_dir(&project_dir).unwrap();
let project_dir = project_dir.canonicalize().unwrap();
let real_file = project_dir.join("once.txt");
std::fs::write(&real_file, "first session").unwrap();
let mut harness = EditorTestHarness::with_config_and_working_dir(
80,
24,
Config::default(),
project_dir.clone(),
)
.unwrap();
harness.open_file(&real_file).unwrap();
harness.editor_mut().save_workspace().unwrap();
let before = read_workspace(&project_dir).unwrap();
let before_has_once = before.split_states.values().any(|s| {
s.open_tabs.iter().any(|t| {
use fresh::workspace::SerializedTabRef;
matches!(t, SerializedTabRef::File(p) if p.ends_with("once.txt"))
})
});
assert!(before_has_once, "sanity: first save must include once.txt");
let real_id = harness
.editor()
.active_window()
.buffer_metadata
.iter()
.find(|(_, m)| {
m.file_path()
.map(|p| p.ends_with("once.txt"))
.unwrap_or(false)
})
.map(|(id, _)| *id)
.expect("real file buffer must exist after open_file");
harness.editor_mut().force_close_buffer(real_id).unwrap();
harness.editor_mut().save_workspace().unwrap();
let after = read_workspace(&project_dir).expect("workspace file must still exist");
let after_has_once = after.split_states.values().any(|s| {
s.open_tabs.iter().any(|t| {
use fresh::workspace::SerializedTabRef;
matches!(t, SerializedTabRef::File(p) if p.ends_with("once.txt"))
})
});
assert!(
!after_has_once,
"closing the real file (no virtual buffers present) must remove it from \
the saved workspace, but once.txt is still listed: {:#?}",
after.split_states
);
}