use std::collections::BTreeMap;
use std::collections::HashMap;
use cargo_metadata::PackageId;
use cargo_metadata::TargetKind;
use cargo_metadata::semver::Version;
use notify::event::DataChange;
use notify::event::EventKind;
use notify::event::ModifyKind;
use super::*;
use crate::lint;
use crate::project::FileStamp;
use crate::project::ManifestFingerprint;
use crate::project::PackageRecord;
use crate::project::PublishPolicy;
use crate::project::TargetRecord;
use crate::project::WorkspaceMetadata;
use crate::scan;
use crate::tui::keymap::TargetsAction;
use crate::tui::panes;
fn metadata_with_example(
root: &AbsolutePath,
package_name: &str,
example_name: &str,
) -> WorkspaceMetadata {
let target = TargetRecord {
name: example_name.to_string(),
kinds: vec![TargetKind::Example],
src_path: AbsolutePath::from(
root.as_path()
.join("examples")
.join(format!("{example_name}.rs")),
),
required_features: Vec::new(),
};
let package = PackageRecord {
name: package_name.to_string(),
version: Version::new(0, 1, 0),
edition: "2021".to_string(),
description: None,
license: None,
homepage: None,
repository: None,
manifest_path: AbsolutePath::from(root.as_path().join("Cargo.toml")),
targets: vec![target],
publish: PublishPolicy::Any,
};
let mut packages = HashMap::new();
packages.insert(
PackageId {
repr: format!("{package_name}-{}", root.display()),
},
package,
);
WorkspaceMetadata {
workspace_root: root.clone(),
target_directory: AbsolutePath::from(root.as_path().join("target")),
packages,
fingerprint: ManifestFingerprint {
manifest: FileStamp {
content_hash: [0_u8; 32],
},
lockfile: None,
rust_toolchain: None,
configs: BTreeMap::new(),
},
out_of_tree_target_bytes: None,
}
}
#[test]
fn detail_cache_separates_root_and_worktree_rows_with_same_path() {
let primary_ws = make_workspace_raw(
None,
"~/ws",
vec![inline_group(vec![make_member(Some("a"), "~/ws/a")])],
None,
);
let linked_ws = make_workspace_raw(
None,
"~/ws_feat",
vec![inline_group(vec![make_member(Some("b"), "~/ws_feat/b")])],
Some("ws_feat"),
);
let root = make_workspace_worktrees_item(primary_ws, vec![linked_ws]);
let mut app = make_app(&[make_workspace_project(None, "~/ws")]);
app.config.current_mut().lint.enabled = true;
apply_items(&mut app, &[root]);
app.project_list.expanded.insert(ExpandKey::Node(0));
app.ensure_visible_rows_cached();
app.project_list
.lint_at_path_mut(&test_path("~/ws"))
.unwrap()
.set_status(LintStatus::Passed(parse_ts("2026-03-30T14:22:18-05:00")));
app.project_list
.lint_at_path_mut(&test_path("~/ws_feat"))
.unwrap()
.set_status(LintStatus::Failed(parse_ts("2026-03-30T15:22:18-05:00")));
app.project_list.set_cursor(0);
app.sync_selected_project();
app.ensure_detail_cached();
let root_worktrees = app.panes.git.content().map(|g| g.worktrees.clone());
assert_eq!(root_worktrees.as_ref().map(Vec::len), Some(2));
assert_eq!(
root_worktrees
.as_ref()
.and_then(|wts| wts.get(1))
.map(|wt| wt.name.as_str()),
Some("ws_feat")
);
app.project_list.set_cursor(1);
app.sync_selected_project();
app.ensure_detail_cached();
assert_eq!(app.panes.git.content().map(|g| g.worktrees.len()), Some(0));
}
#[test]
fn workspace_worktree_group_root_uses_worktree_group_title() {
let primary_ws = make_workspace_raw(Some("bevy_brp"), "~/rust/bevy_brp", vec![], None);
let linked_ws = make_workspace_raw(
Some("bevy_brp_style_fix"),
"~/rust/bevy_brp_style_fix",
vec![],
Some("bevy_brp_style_fix"),
);
let root = make_workspace_worktrees_item(primary_ws, vec![linked_ws]);
let mut app = make_app(&[]);
apply_items(&mut app, &[root]);
app.project_list.set_cursor(0);
app.sync_selected_project();
app.ensure_detail_cached();
let package = app.panes.package.content().unwrap();
assert_eq!(package.package_title, "Worktree Group");
assert_eq!(package.title_name, "bevy_brp");
assert_eq!(
panes::DetailField::Targets.package_value(package),
"workspace"
);
assert_eq!(
package.worktree_group_summary.as_ref().map(|s| s.worktrees),
Some(2)
);
assert_eq!(
package.worktree_group_summary.as_ref().map(|s| s.deleted),
Some(0)
);
let rows = panes::package_rows_from_data(package);
assert_eq!(
&rows[..6],
&[
panes::PackageRow::Description,
panes::PackageRow::Section(panes::PackageSection::WorktreeGroupSummary),
panes::PackageRow::Field(panes::DetailField::Worktrees),
panes::PackageRow::Field(panes::DetailField::Lint),
panes::PackageRow::Field(panes::DetailField::Ci),
panes::PackageRow::Section(panes::PackageSection::PrimaryWorkspace),
]
);
}
#[test]
fn workspace_worktree_group_root_targets_include_each_checkout() {
let primary_ws = make_workspace_raw(Some("hana"), "/tmp/hana", vec![], None);
let linked_ws = make_workspace_raw(
Some("hana"),
"/tmp/hana_style_fix",
vec![],
Some("hana_style_fix"),
);
let root = make_workspace_worktrees_item(primary_ws, vec![linked_ws]);
let primary_path = test_path("/tmp/hana");
let linked_path = test_path("/tmp/hana_style_fix");
let mut app = make_app(&[]);
apply_items(&mut app, &[root]);
{
let handle = app.scan.metadata_store_handle();
let mut store = handle.lock().unwrap_or_else(|_| std::process::abort());
store.upsert(metadata_with_example(&primary_path, "hana", "showcase"));
store.upsert(metadata_with_example(&linked_path, "hana", "showcase"));
}
app.project_list.set_cursor(0);
app.sync_selected_project();
app.ensure_detail_cached();
let targets = app.panes.targets.content().unwrap();
let labels: Vec<String> = targets
.examples
.iter()
.map(|entry| entry.source.label().to_string())
.collect();
assert_eq!(labels, vec!["hana/hana", "hana_style_fix/hana"]);
let project_paths: Vec<String> = targets
.examples
.iter()
.map(|entry| entry.project_path.display().to_string())
.collect();
assert_eq!(
project_paths,
vec![
primary_path.display().to_string(),
linked_path.display().to_string(),
]
);
assert!(
targets
.examples
.iter()
.all(|entry| entry.package_name == "hana")
);
app.panes.targets.viewport.set_pos(1);
panes::dispatch_targets_action(TargetsAction::ReleaseBuild, &mut app);
let pending = app
.inflight
.take_pending_example_run()
.unwrap_or_else(|| std::process::abort());
assert_eq!(pending.abs_path, linked_path.display().to_string());
assert_eq!(pending.package_name.as_deref(), Some("hana"));
assert!(pending.build_mode.is_release());
}
#[test]
fn package_worktree_group_root_reverts_to_package_after_linked_dismissed() {
let tmp = tempfile::tempdir().unwrap_or_else(|_| std::process::abort());
let primary_dir = tmp.path().join("cargo-mend");
let linked_dir = tmp.path().join("cargo-mend_style_fix");
std::fs::create_dir_all(&primary_dir).unwrap_or_else(|_| std::process::abort());
std::fs::create_dir_all(&linked_dir).unwrap_or_else(|_| std::process::abort());
let primary_path = primary_dir.to_string_lossy().to_string();
let linked_path = linked_dir.to_string_lossy().to_string();
let root = make_package_worktrees_item(
make_package_raw(Some("cargo-mend"), &primary_path, None),
vec![make_package_raw(
Some("cargo-mend"),
&linked_path,
Some("cargo-mend_style_fix"),
)],
);
let mut app = make_app(&[root]);
app.project_list.set_cursor(0);
app.sync_selected_project();
app.ensure_detail_cached();
assert_eq!(
app.panes
.package
.content()
.map(|p| p.package_title.as_str()),
Some("Worktree Group")
);
assert_eq!(app.panes.git.content().map(|g| g.worktrees.len()), Some(2));
assert!(app.expand(), "root worktree group should expand");
app.ensure_visible_rows_cached();
std::fs::remove_dir_all(&linked_dir).unwrap_or_else(|_| std::process::abort());
app.handle_disk_usage(Path::new(&linked_path), 0);
app.ensure_visible_rows_cached();
app.project_list.set_cursor(2);
let target = app
.focused_dismiss_target()
.expect("deleted linked worktree should be dismissable");
app.dismiss(target);
app.ensure_detail_cached();
let package = app.panes.package.content().unwrap();
assert_eq!(package.package_title, "Package");
assert_eq!(package.title_name, "cargo-mend");
assert!(package.worktree_group_summary.is_none());
assert_eq!(app.panes.git.content().map(|g| g.worktrees.len()), Some(0));
}
#[test]
fn worktree_group_summary_counts_visible_and_deleted_entries() {
let root = make_package_worktrees_item(
make_package_raw(Some("cargo-mend"), "~/rust/cargo-mend", None),
vec![make_package_raw(
Some("cargo-mend"),
"~/rust/cargo-mend_style_fix",
Some("cargo-mend_style_fix"),
)],
);
let mut app = make_app(&[root]);
app.project_list
.at_path_mut(test_path("~/rust/cargo-mend_style_fix").as_path())
.expect("linked worktree should exist")
.visibility = Deleted;
app.project_list.set_cursor(0);
app.sync_selected_project();
app.ensure_detail_cached();
let package = app.panes.package.content().unwrap();
assert_eq!(package.package_title, "Worktree Group");
assert_eq!(
package.worktree_group_summary.as_ref().map(|s| s.worktrees),
Some(1)
);
assert_eq!(
package.worktree_group_summary.as_ref().map(|s| s.deleted),
Some(1)
);
let rows = panes::package_rows_from_data(package);
assert_eq!(
&rows[..7],
&[
panes::PackageRow::Description,
panes::PackageRow::Section(panes::PackageSection::WorktreeGroupSummary),
panes::PackageRow::Field(panes::DetailField::Worktrees),
panes::PackageRow::Field(panes::DetailField::DeletedWorktrees),
panes::PackageRow::Field(panes::DetailField::Lint),
panes::PackageRow::Field(panes::DetailField::Ci),
panes::PackageRow::Section(panes::PackageSection::PrimaryPackage),
]
);
}
#[test]
fn dismissed_linked_worktree_is_omitted_from_group_git_summary() {
let root = make_package_worktrees_item(
make_package_raw(Some("cargo-mend"), "~/rust/cargo-mend", None),
vec![
make_package_raw(
Some("cargo-mend"),
"~/rust/cargo-mend_style_fix",
Some("cargo-mend_style_fix"),
),
make_package_raw(
Some("cargo-mend"),
"~/rust/cargo-mend_old_fix",
Some("cargo-mend_old_fix"),
),
],
);
let mut app = make_app(&[root]);
let dismissed_path = test_path("~/rust/cargo-mend_old_fix");
app.project_list
.at_path_mut(dismissed_path.as_path())
.expect("dismissed worktree should exist")
.visibility = Dismissed;
app.project_list.set_cursor(0);
app.sync_selected_project();
app.ensure_detail_cached();
let git = app.panes.git.content().unwrap();
let names: Vec<&str> = git.worktrees.iter().map(|wt| wt.name.as_str()).collect();
assert_eq!(names, vec!["cargo-mend", "cargo-mend_style_fix"]);
assert_eq!(
app.panes
.package
.content()
.map(|p| p.package_title.as_str()),
Some("Worktree Group")
);
}
#[test]
fn linked_worktree_entry_builds_detail_for_selected_row() {
let primary_ws = make_workspace_raw(
Some("cargo-port"),
"~/rust/cargo-port",
vec![inline_group(vec![make_member(
Some("cargo-port"),
"~/rust/cargo-port/crates/cargo-port",
)])],
None,
);
let linked_ws = make_workspace_raw_with_primary(
Some("cargo-port_speedup"),
"~/rust/cargo-port_speedup",
vec![inline_group(vec![make_member(
Some("cargo-port"),
"~/rust/cargo-port_speedup/crates/cargo-port",
)])],
Some("cargo-port_speedup"),
Some("~/rust/cargo-port"),
);
let root = make_workspace_worktrees_item(primary_ws, vec![linked_ws.clone()]);
let mut app = make_app(&[]);
apply_items(&mut app, &[root]);
app.project_list.expanded.insert(ExpandKey::Node(0));
app.ensure_visible_rows_cached();
assert_eq!(
app.visible_rows(),
vec![
VisibleRow::Root { node_index: 0 },
VisibleRow::WorktreeEntry {
node_index: 0,
worktree_index: 0,
},
VisibleRow::WorktreeEntry {
node_index: 0,
worktree_index: 1,
},
]
);
app.project_list.set_cursor(2);
app.sync_selected_project();
app.ensure_detail_cached();
assert_eq!(
app.project_list
.selected_project_path()
.map(Path::to_path_buf),
Some(linked_ws.path().to_path_buf())
);
assert_eq!(
app.panes.package.content().map(|p| p.path.as_str()),
Some("~/rust/cargo-port_speedup")
);
assert!(
app.tabbable_panes().contains(&PaneId::Package),
"linked worktree selection should expose the package pane"
);
}
#[test]
fn disk_rollup_deduplicates_primary_worktree_path() {
let root = make_package_worktrees_item(
make_package_raw(None, "~/ws", None),
vec![make_package_raw(None, "~/ws_feat", Some("ws_feat"))],
);
let mut app = make_app(&[make_project(None, "~/ws")]);
apply_items(&mut app, &[root]);
app.handle_disk_usage(test_path("~/ws").as_path(), 15);
app.handle_disk_usage(test_path("~/ws_feat").as_path(), 21);
assert_eq!(app.project_list[0].disk_usage_bytes(), Some(36));
assert_eq!(
panes::formatted_disk_for_item(&app.project_list[0].item),
crate::tui::render::format_bytes(36)
);
}
#[test]
fn handle_project_discovered_deduplicates_by_path() {
let mut app = make_app(&[]);
let pkg1 = RootItem::Rust(RustProject::Package(make_package_raw(
Some("foo"),
"/abs/foo",
None,
)));
let pkg2 = RootItem::Rust(RustProject::Package(make_package_raw(
Some("foo"),
"/abs/foo",
None,
)));
let pkg3 = RootItem::Rust(RustProject::Package(make_package_raw(
Some("bar"),
"/abs/bar",
None,
)));
app.handle_project_discovered(pkg1);
app.handle_project_discovered(pkg2);
app.handle_project_discovered(pkg3);
assert_eq!(app.project_list.len(), 2);
}
#[test]
fn handle_project_discovered_inserts_new_root_in_sorted_position() {
let mut app = make_app(&[
make_project(Some("cargo-mend"), "~/rust/cargo-mend"),
make_project(Some("cargo-port"), "~/rust/cargo-port"),
make_project(Some("rust-template"), "~/rust/rust-template"),
]);
assert!(app.handle_project_discovered(make_project(
Some("cache-apt-pkgs-action"),
"~/rust/cache-apt-pkgs-action",
)));
let actual: Vec<_> = app
.project_list
.iter()
.map(|entry| entry.item.path())
.collect();
assert_eq!(
actual,
vec![
test_path("~/rust/cache-apt-pkgs-action").as_path(),
test_path("~/rust/cargo-mend").as_path(),
test_path("~/rust/cargo-port").as_path(),
test_path("~/rust/rust-template").as_path(),
]
);
}
#[test]
fn handle_project_discovered_registers_new_root_with_lint_runtime() {
let project_dir = tempfile::tempdir().unwrap_or_else(|_| std::process::abort());
std::fs::create_dir_all(project_dir.path().join("src"))
.unwrap_or_else(|_| std::process::abort());
std::fs::write(
project_dir.path().join("Cargo.toml"),
manifest_contents("new_worktree", false),
)
.unwrap_or_else(|_| std::process::abort());
std::fs::write(
project_dir.path().join("src").join("lib.rs"),
"pub fn demo() {}\n",
)
.unwrap_or_else(|_| std::process::abort());
let cache_dir = tempfile::tempdir().unwrap_or_else(|_| std::process::abort());
let mut cfg = CargoPortConfig::default();
cfg.cache.root = cache_dir.path().to_string_lossy().to_string();
cfg.lint.enabled = true;
cfg.lint.include = vec![project_dir.path().to_string_lossy().to_string()];
cfg.lint.commands = vec![crate::config::LintCommandConfig {
name: "echo".to_string(),
command: "echo lint ok".to_string(),
}];
let mut app = make_app_with_config(&[], &cfg);
assert!(app.handle_project_discovered(item_from_project_dir(project_dir.path())));
let trigger = lint::classify_event_path(
project_dir.path(),
EventKind::Modify(ModifyKind::Data(DataChange::Any)),
&project_dir.path().join("src").join("lib.rs"),
)
.unwrap_or_else(|| std::process::abort());
app.lint
.runtime()
.unwrap_or_else(|| std::process::abort())
.lint_trigger(trigger);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let mut passed = false;
while std::time::Instant::now() < deadline {
app.poll_background();
if matches!(
crate::tui::state::Lint::status_for_path(&app.project_list, project_dir.path()),
LintStatus::Passed(_)
) {
passed = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(
passed,
"newly discovered project should have an active lint worker for later edits"
);
}
#[test]
fn handle_project_discovered_creates_worktree_group_from_single_primary() {
for kind in [WorktreeProjectKind::Package, WorktreeProjectKind::Workspace] {
expect_synthetic_discovery_creates_group(kind);
}
}
#[test]
fn handle_project_discovered_slots_new_worktree_into_existing_group() {
for kind in [WorktreeProjectKind::Package, WorktreeProjectKind::Workspace] {
expect_synthetic_discovery_appends_existing_group(kind);
}
}
#[test]
fn background_discovery_from_real_worktree_creates_group() {
for kind in [WorktreeProjectKind::Package, WorktreeProjectKind::Workspace] {
expect_real_discovery_creates_group(kind);
}
}
#[test]
fn discovered_workspace_worktree_with_members_expands_as_worktree_then_workspace() {
let tmp = tempfile::tempdir().unwrap_or_else(|_| std::process::abort());
let primary_dir = tmp.path().join("bevy_brp");
let linked_dir = tmp.path().join("bevy_brp_test");
init_workspace_git_project_with_member(&primary_dir, "bevy_brp", "extras");
let primary_item = item_from_project_dir(&primary_dir);
let mut app = make_app(&[primary_item]);
add_git_worktree(&primary_dir, &linked_dir, "test/brp");
let linked_item =
scan::discover_project_item(&linked_dir).unwrap_or_else(|| std::process::abort());
assert!(
app.handle_bg_msg(BackgroundMsg::ProjectDiscovered { item: linked_item }),
"discovery should request a derived-state rebuild"
);
let RootItem::Worktrees(group) = &app.project_list[0].item else {
panic!("expected discovered workspace worktree to form a worktree group");
};
assert_eq!(group.linked.len(), 1);
let RustProject::Workspace(linked_ws) = &group.linked[0] else {
panic!("linked entry should be a workspace");
};
assert!(
linked_ws.has_members(),
"linked workspace worktree should arrive with member groups populated"
);
app.project_list.set_cursor(0);
assert!(app.expand(), "root should expand into worktree entries");
app.ensure_visible_rows_cached();
assert_eq!(
app.visible_rows(),
&[
VisibleRow::Root { node_index: 0 },
VisibleRow::WorktreeEntry {
node_index: 0,
worktree_index: 0,
},
VisibleRow::WorktreeEntry {
node_index: 0,
worktree_index: 1,
},
]
);
app.project_list.set_cursor(2);
assert!(
app.expand(),
"linked workspace worktree should expand into its workspace members"
);
app.ensure_visible_rows_cached();
assert!(
app.visible_rows().iter().any(|row| matches!(
row,
VisibleRow::WorktreeMember {
node_index: 0,
worktree_index: 1,
..
}
)),
"expanded linked workspace worktree should show member rows"
);
}
#[test]
fn expanded_workspace_root_discovery_immediately_renders_primary_workspace_and_linked_row() {
let tmp = tempfile::tempdir().unwrap_or_else(|_| std::process::abort());
let primary_dir = tmp.path().join("bevy_brp");
let linked_dir = tmp.path().join("bevy_brp_test");
init_workspace_git_project_with_member(&primary_dir, "bevy_brp", "extras");
let mut primary_item = item_from_project_dir(&primary_dir);
let RootItem::Rust(RustProject::Workspace(primary_ws)) = &mut primary_item else {
panic!("expected primary workspace root item");
};
*primary_ws.groups_mut() = vec![inline_group(vec![make_member(
Some("extras"),
&primary_dir.join("extras").to_string_lossy(),
)])];
let mut app = make_app(&[]);
apply_items(&mut app, &[primary_item]);
app.project_list.expanded.insert(ExpandKey::Node(0));
app.ensure_visible_rows_cached();
assert_eq!(
app.visible_rows(),
&[
VisibleRow::Root { node_index: 0 },
VisibleRow::Member {
node_index: 0,
group_index: 0,
member_index: 0,
},
]
);
add_git_worktree(&primary_dir, &linked_dir, "test/brp");
let linked_item =
scan::discover_project_item(&linked_dir).unwrap_or_else(|| std::process::abort());
apply_bg_msg(
&mut app,
BackgroundMsg::ProjectDiscovered { item: linked_item },
);
assert_eq!(
app.visible_rows(),
&[
VisibleRow::Root { node_index: 0 },
VisibleRow::WorktreeEntry {
node_index: 0,
worktree_index: 0,
},
VisibleRow::WorktreeMember {
node_index: 0,
worktree_index: 0,
group_index: 0,
member_index: 0,
},
VisibleRow::WorktreeEntry {
node_index: 0,
worktree_index: 1,
},
],
"discovering a linked workspace worktree while the primary root is expanded should preserve the primary workspace subtree immediately"
);
let rendered = rendered_root_name_cells(&mut app);
assert!(
rendered
.iter()
.any(|row| row.contains("bevy_brp") && row.contains(":2")),
"root row should still render the worktree badge after discovery: {rendered:?}"
);
assert!(
rendered.iter().any(|row| row.contains("bevy_brp_test")),
"linked worktree row should render immediately without a collapse/expand cycle: {rendered:?}"
);
assert!(
rendered.iter().any(|row| row.contains("extras")),
"primary workspace member rows should remain visible after the root becomes a worktree group: {rendered:?}"
);
}
#[test]
fn stale_workspace_regroup_immediately_renders_primary_workspace_and_linked_row() {
let tmp = tempfile::tempdir().unwrap_or_else(|_| std::process::abort());
let primary_dir = tmp.path().join("bevy_brp");
let linked_dir = tmp.path().join("bevy_brp_test");
init_workspace_git_project_with_member(&primary_dir, "bevy_brp", "extras");
let mut primary_item = item_from_project_dir(&primary_dir);
let RootItem::Rust(RustProject::Workspace(primary_ws)) = &mut primary_item else {
panic!("expected primary workspace root item");
};
*primary_ws.groups_mut() = vec![inline_group(vec![make_member(
Some("extras"),
&primary_dir.join("extras").to_string_lossy(),
)])];
let mut app = make_app(&[]);
apply_items(&mut app, &[primary_item]);
app.project_list.expanded.insert(ExpandKey::Node(0));
app.ensure_visible_rows_cached();
assert_eq!(
app.visible_rows(),
&[
VisibleRow::Root { node_index: 0 },
VisibleRow::Member {
node_index: 0,
group_index: 0,
member_index: 0,
},
]
);
add_git_worktree(&primary_dir, &linked_dir, "test/brp");
let stale_discovery = RootItem::Rust(RustProject::Workspace(make_workspace_raw(
Some("bevy_brp"),
&linked_dir.to_string_lossy(),
Vec::new(),
None,
)));
apply_bg_msg(
&mut app,
BackgroundMsg::ProjectDiscovered {
item: stale_discovery,
},
);
let refreshed = item_from_project_dir(&linked_dir);
apply_bg_msg(
&mut app,
BackgroundMsg::ProjectRefreshed { item: refreshed },
);
assert_eq!(
app.visible_rows(),
&[
VisibleRow::Root { node_index: 0 },
VisibleRow::WorktreeEntry {
node_index: 0,
worktree_index: 0,
},
VisibleRow::WorktreeMember {
node_index: 0,
worktree_index: 0,
group_index: 0,
member_index: 0,
},
VisibleRow::WorktreeEntry {
node_index: 0,
worktree_index: 1,
},
],
"refresh regroup should preserve the expanded primary workspace subtree immediately"
);
let rendered = rendered_root_name_cells(&mut app);
assert!(
rendered.iter().any(|row| row.contains("bevy_brp_test")),
"regrouped linked worktree row should render immediately without a collapse/expand cycle: {rendered:?}"
);
assert!(
rendered.iter().any(|row| row.contains("extras")),
"regrouped primary workspace member rows should remain visible: {rendered:?}"
);
}
#[test]
fn background_discovery_from_real_worktree_appends_existing_group() {
for kind in [WorktreeProjectKind::Package, WorktreeProjectKind::Workspace] {
expect_real_discovery_appends_existing_group(kind);
}
}
#[test]
fn refreshed_worktree_metadata_regroups_stale_top_level_discovery() {
for kind in [WorktreeProjectKind::Workspace, WorktreeProjectKind::Package] {
expect_refresh_regroups_stale_top_level_discovery(kind);
}
}
#[test]
fn refreshed_worktree_metadata_appends_into_existing_group() {
for kind in [WorktreeProjectKind::Workspace, WorktreeProjectKind::Package] {
expect_refresh_appends_stale_discovery_into_existing_group(kind);
}
}
#[test]
fn stale_discovery_refresh_then_delete_dismisses_to_root() {
for kind in [WorktreeProjectKind::Workspace, WorktreeProjectKind::Package] {
assert_stale_discovery_refresh_then_delete_dismisses_to_root(kind);
}
}
fn assert_stale_discovery_refresh_then_delete_dismisses_to_root(kind: WorktreeProjectKind) {
let tmp = tempfile::tempdir().unwrap_or_else(|_| std::process::abort());
let primary_dir = tmp.path().join(kind.primary_name());
let linked_dir = tmp.path().join(kind.linked_name());
kind.init_primary_repo(&primary_dir);
let primary_item = item_from_project_dir(&primary_dir);
let mut app = make_app(&[primary_item]);
add_git_worktree(
&primary_dir,
&linked_dir,
&format!("test/{}", kind.branch_prefix()),
);
let stale_discovery = match kind {
WorktreeProjectKind::Package => RootItem::Rust(RustProject::Package(make_package_raw(
Some(kind.primary_name()),
&linked_dir.to_string_lossy(),
None,
))),
WorktreeProjectKind::Workspace => {
RootItem::Rust(RustProject::Workspace(make_workspace_raw(
Some(kind.primary_name()),
&linked_dir.to_string_lossy(),
Vec::new(),
None,
)))
},
};
apply_bg_msg(
&mut app,
BackgroundMsg::ProjectDiscovered {
item: stale_discovery,
},
);
let refreshed = item_from_project_dir(&linked_dir);
apply_bg_msg(
&mut app,
BackgroundMsg::ProjectRefreshed { item: refreshed },
);
assert_deleted_linked_worktree_dismisses_to_root(&mut app, &linked_dir);
}
#[test]
fn background_disk_zero_from_real_package_worktree_can_be_dismissed_to_root() {
let tmp = tempfile::tempdir().unwrap_or_else(|_| std::process::abort());
let primary_dir = tmp.path().join("app");
let linked_dir = tmp.path().join("app_test");
init_git_project(&primary_dir, "app", false);
add_git_worktree(&primary_dir, &linked_dir, "test/app");
let primary_item = item_from_project_dir(&primary_dir);
let linked_item = item_from_project_dir(&linked_dir);
let mut app = make_app(&[primary_item, linked_item]);
assert_deleted_linked_worktree_dismisses_to_root(&mut app, &linked_dir);
}
#[test]
fn background_disk_zero_from_real_workspace_worktree_can_be_dismissed_to_root() {
let tmp = tempfile::tempdir().unwrap_or_else(|_| std::process::abort());
let primary_dir = tmp.path().join("obsidian_knife");
let linked_dir = tmp.path().join("obsidian_knife_test");
init_git_project(&primary_dir, "obsidian_knife", true);
add_git_worktree(&primary_dir, &linked_dir, "test/obsidian");
let primary_item = item_from_project_dir(&primary_dir);
let linked_item = item_from_project_dir(&linked_dir);
let mut app = make_app(&[primary_item, linked_item]);
assert_deleted_linked_worktree_dismisses_to_root(&mut app, &linked_dir);
}
#[test]
fn handle_project_discovered_does_not_allocate_per_comparison() {
let mut app = make_app(&[]);
let start = std::time::Instant::now();
for i in 0..200 {
let path = format!("/abs/project_{i}");
let item = RootItem::Rust(RustProject::Package(make_package_raw(None, &path, None)));
app.handle_project_discovered(item);
}
let elapsed = start.elapsed();
assert_eq!(app.project_list.len(), 200);
assert!(
elapsed.as_millis() < 100,
"discovery of 200 projects took {elapsed:?} - possible display_path allocation regression"
);
}
#[test]
fn is_deleted_does_not_allocate_display_paths() {
let mut app = make_app(&[]);
for i in 0..200 {
let path = format!("/abs/project_{i}");
let item = RootItem::Rust(RustProject::Package(make_package_raw(None, &path, None)));
app.project_list.push(item);
}
let target = app.project_list[100].path().to_path_buf();
app.project_list
.at_path_mut(&target)
.expect("target project should exist")
.visibility = Deleted;
let start = std::time::Instant::now();
for _ in 0..1000 {
let _ = app.project_list.is_deleted(&target);
}
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 100,
"1000 is_deleted calls took {elapsed:?} -- possible display_path allocation regression"
);
}