use super::*;
use std::fs;
use tempfile::TempDir;
fn write(dir: &Path, rel: &str, body: &str) {
let path = dir.join(rel);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, body).unwrap();
}
fn mkdir(dir: &Path, rel: &str) {
fs::create_dir_all(dir.join(rel)).unwrap();
}
fn members(root: &Path) -> Vec<String> {
let budget = ProbeBudget::new();
probe_roots(root, &budget)
.roots
.into_iter()
.skip(1) .map(|p| {
p.strip_prefix(root)
.unwrap()
.to_string_lossy()
.replace('\\', "/")
})
.collect()
}
#[test]
fn single_package_project_probes_only_root() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"package.json",
r#"{"dependencies":{"react":"^18"}}"#,
);
let budget = ProbeBudget::new();
let probe = probe_roots(tmp.path(), &budget);
assert_eq!(probe.roots, vec![tmp.path().to_path_buf()]);
assert!(
!probe.truncated,
"a flag that is always true carries no information (#7781)"
);
}
#[test]
fn npm_workspaces_array_members() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"package.json",
r#"{"workspaces":["packages/*","apps/*"]}"#,
);
mkdir(tmp.path(), "packages/ui");
mkdir(tmp.path(), "packages/core");
mkdir(tmp.path(), "apps/web");
assert_eq!(
members(tmp.path()),
vec!["packages/core", "packages/ui", "apps/web"]
);
}
#[test]
fn npm_workspaces_object_form() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"package.json",
r#"{"workspaces":{"packages":["libs/*"],"nohoist":["**/x"]}}"#,
);
mkdir(tmp.path(), "libs/a");
assert_eq!(members(tmp.path()), vec!["libs/a"]);
}
#[test]
fn declared_globs_beat_conventional_paths() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"package.json",
r#"{"workspaces":["services/*"]}"#,
);
mkdir(tmp.path(), "services/api");
mkdir(tmp.path(), "packages/ignored");
assert_eq!(members(tmp.path()), vec!["services/api"]);
}
#[test]
fn pnpm_workspace_members() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"pnpm-workspace.yaml",
"packages:\n - 'packages/*'\n - \"apps/*\"\n - tools/cli\n",
);
mkdir(tmp.path(), "packages/ui");
mkdir(tmp.path(), "apps/web");
mkdir(tmp.path(), "tools/cli");
let m = members(tmp.path());
assert!(m.contains(&"packages/ui".to_string()));
assert!(m.contains(&"apps/web".to_string()));
assert!(m.contains(&"tools/cli".to_string()), "literal path member");
}
#[test]
fn pnpm_workspace_ignores_other_keys() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"pnpm-workspace.yaml",
"catalog:\n - 'should/not/count'\npackages:\n - 'apps/*'\n",
);
mkdir(tmp.path(), "apps/web");
mkdir(tmp.path(), "should/not/count");
assert_eq!(members(tmp.path()), vec!["apps/web"]);
}
#[test]
fn elixir_umbrella_members() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"mix.exs",
"defmodule Umbrella.MixProject do\n def project do\n [apps_path: \"apps\"]\n end\nend\n",
);
mkdir(tmp.path(), "apps/my_app");
mkdir(tmp.path(), "apps/my_app_web");
assert_eq!(members(tmp.path()), vec!["apps/my_app", "apps/my_app_web"]);
}
#[test]
fn plain_mix_project_declares_no_members() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"mix.exs",
"defp deps, do: [{:jason, \"~> 1.4\"}]\n",
);
mkdir(tmp.path(), "apps/not_an_umbrella");
assert!(members(tmp.path()).is_empty());
}
#[test]
fn malformed_package_json_declares_no_members() {
let tmp = TempDir::new().unwrap();
write(tmp.path(), "package.json", "{ not json");
mkdir(tmp.path(), "packages/a");
assert!(members(tmp.path()).is_empty());
}
#[test]
fn unknown_root_declares_no_members() {
let tmp = TempDir::new().unwrap();
write(tmp.path(), "README.md", "# hi\n");
assert!(members(tmp.path()).is_empty());
}
#[test]
fn glob_expands_one_level() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"package.json",
r#"{"workspaces":["packages/*"]}"#,
);
mkdir(tmp.path(), "packages/a/nested/deep");
assert_eq!(members(tmp.path()), vec!["packages/a"]);
}
#[test]
fn double_star_is_one_level() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"package.json",
r#"{"workspaces":["packages/**"]}"#,
);
mkdir(tmp.path(), "packages/a/deep");
assert_eq!(members(tmp.path()), vec!["packages/a"]);
}
#[test]
fn prefix_glob_matches_named_members() {
let tmp = TempDir::new().unwrap();
write(tmp.path(), "package.json", r#"{"workspaces":["pkg-*"]}"#);
mkdir(tmp.path(), "pkg-ui");
mkdir(tmp.path(), "other");
assert_eq!(members(tmp.path()), vec!["pkg-ui"]);
}
#[test]
fn literal_member_path() {
let tmp = TempDir::new().unwrap();
write(tmp.path(), "package.json", r#"{"workspaces":["apps/web"]}"#);
mkdir(tmp.path(), "apps/web");
assert_eq!(members(tmp.path()), vec!["apps/web"]);
}
#[test]
fn files_are_never_members() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"package.json",
r#"{"workspaces":["packages/*"]}"#,
);
write(tmp.path(), "packages/README.md", "x");
mkdir(tmp.path(), "packages/real");
assert_eq!(members(tmp.path()), vec!["packages/real"]);
}
#[test]
fn pattern_with_parent_escape_is_rejected() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"package.json",
r#"{"workspaces":["../escape/*","packages/*"]}"#,
);
mkdir(tmp.path(), "packages/ok");
assert_eq!(
members(tmp.path()),
vec!["packages/ok"],
"the `..` pattern contributes nothing"
);
}
#[test]
fn cargo_workspace_members() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"Cargo.toml",
"[workspace]\nmembers = [\"crates/*\", \"tools/cli\"]\n",
);
mkdir(tmp.path(), "crates/alpha");
mkdir(tmp.path(), "crates/beta");
mkdir(tmp.path(), "tools/cli");
assert_eq!(
members(tmp.path()),
vec!["crates/alpha", "crates/beta", "tools/cli"]
);
}
#[test]
fn non_workspace_cargo_toml_declares_no_members() {
let tmp = TempDir::new().unwrap();
write(tmp.path(), "Cargo.toml", "[package]\nname = \"solo\"\n");
mkdir(tmp.path(), "crates/alpha");
assert!(members(tmp.path()).is_empty());
let broken = TempDir::new().unwrap();
write(broken.path(), "Cargo.toml", "[workspace\nmembers = [");
mkdir(broken.path(), "crates/alpha");
assert!(
members(broken.path()).is_empty(),
"unparseable TOML is fail-closed, not an error"
);
}
#[test]
fn member_count_is_capped() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"package.json",
r#"{"workspaces":["packages/*"]}"#,
);
for i in 0..(MAX_WORKSPACE_MEMBERS + 20) {
mkdir(tmp.path(), &format!("packages/p{i:04}"));
}
let budget = ProbeBudget::new();
let probe = probe_roots(tmp.path(), &budget);
assert_eq!(
probe.roots.len(),
MAX_WORKSPACE_MEMBERS + 1,
"root plus at most MAX_WORKSPACE_MEMBERS members"
);
assert!(
probe.truncated,
"#7781: dropping declared members is a resource cap the caller must see, \
not a silently short list"
);
}
#[test]
fn pattern_count_is_capped() {
let tmp = TempDir::new().unwrap();
let over = MAX_WORKSPACE_PATTERNS + 5;
let globs: Vec<String> = (0..over).map(|i| format!("\"g{i:04}/*\"")).collect();
write(
tmp.path(),
"package.json",
&format!("{{\"workspaces\":[{}]}}", globs.join(",")),
);
mkdir(tmp.path(), "g0000/first");
mkdir(tmp.path(), &format!("g{:04}/last", over - 1));
let found = members(tmp.path());
assert!(
found.iter().any(|p| p.ends_with("first")),
"a member behind an in-cap pattern is still probed: {found:?}"
);
assert!(
!found.iter().any(|p| p.ends_with("last")),
"a member reachable only past the pattern cap is NOT probed: {found:?}"
);
let budget = ProbeBudget::new();
assert!(
probe_roots(tmp.path(), &budget).truncated,
"#7781: the pattern cap drops declared members too, so it reports itself"
);
}
#[test]
fn oversized_manifest_is_not_read() {
let tmp = TempDir::new().unwrap();
let mut body = String::from(r#"{"workspaces":["packages/*"],"pad":""#);
body.push_str(&"x".repeat(MAX_MANIFEST_BYTES as usize + 1));
body.push_str("\"}");
write(tmp.path(), "package.json", &body);
mkdir(tmp.path(), "packages/a");
assert!(members(tmp.path()).is_empty());
}
#[test]
fn budget_exhaustion_stops_probing() {
let tmp = TempDir::new().unwrap();
write(
tmp.path(),
"package.json",
r#"{"workspaces":["packages/*"]}"#,
);
mkdir(tmp.path(), "packages/a");
let generous = ProbeBudget::new();
assert_eq!(probe_roots(tmp.path(), &generous).roots.len(), 2);
let starved = ProbeBudget::with_bytes(0);
assert_eq!(
probe_roots(tmp.path(), &starved).roots.len(),
1,
"no budget -> root only"
);
}
#[test]
fn budget_is_shared_across_members() {
let tmp = TempDir::new().unwrap();
let decl = r#"{"workspaces":["packages/*"]}"#;
write(tmp.path(), "package.json", decl);
mkdir(tmp.path(), "packages/a");
write(tmp.path(), "packages/a/package.json", r#"{"x":1}"#);
let budget = ProbeBudget::with_bytes(decl.len() as u64);
let roots = probe_roots(tmp.path(), &budget).roots;
assert_eq!(roots.len(), 2, "the declaration itself was affordable");
assert!(
!budget.take(1),
"the budget is spent, so any further read fails closed"
);
}
#[test]
fn a_second_read_of_one_path_costs_no_budget() {
let tmp = TempDir::new().unwrap();
let body = r#"{"dependencies":{"svelte":"^5"}}"#;
write(tmp.path(), "package.json", body);
let path = tmp.path().join("package.json");
let budget = ProbeBudget::with_bytes(body.len() as u64);
assert_eq!(read_bounded(&path, &budget).as_deref(), Some(body));
fs::remove_file(&path).unwrap();
assert_eq!(
read_bounded(&path, &budget).as_deref(),
Some(body),
"the second needle is answered from the memo, not from disk"
);
assert_eq!(read_bounded(&path, &budget).as_deref(), Some(body));
}
#[test]
fn read_bounded_rejects_a_directory() {
let tmp = TempDir::new().unwrap();
mkdir(tmp.path(), "adir");
let budget = ProbeBudget::new();
assert!(read_bounded(&tmp.path().join("adir"), &budget).is_none());
assert!(read_bounded(&tmp.path().join("missing"), &budget).is_none());
}