use super::ensure_installed;
use aube_manifest::PackageJson;
use clap::Args;
use miette::{Context, IntoDiagnostic, miette};
use std::io::IsTerminal;
use std::path::Path;
#[derive(Debug, Args)]
pub struct RunArgs {
pub script: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub args: Vec<String>,
#[arg(long)]
pub if_present: bool,
#[arg(long, value_name = "[[HOST:]PORT]", num_args = 0..=1, require_equals = true, default_missing_value = "")]
pub inspect: Option<String>,
#[arg(long, value_name = "[[HOST:]PORT]", num_args = 0..=1, require_equals = true, default_missing_value = "")]
pub inspect_brk: Option<String>,
#[arg(long)]
pub no_bail: bool,
#[arg(long)]
pub no_install: bool,
#[arg(long, overrides_with = "sort")]
pub no_sort: bool,
#[arg(long)]
pub parallel: bool,
#[arg(long)]
pub report_summary: bool,
#[arg(long)]
pub reporter_hide_prefix: bool,
#[arg(long, value_name = "PACKAGE")]
pub resume_from: Option<String>,
#[arg(long)]
pub reverse: bool,
#[arg(long, overrides_with = "no_sort")]
pub sort: bool,
#[arg(short = 's')]
pub silent: bool,
#[arg(long, value_name = "N")]
pub workspace_concurrency: Option<usize>,
#[command(flatten)]
pub lockfile: crate::cli_args::LockfileArgs,
#[command(flatten)]
pub network: crate::cli_args::NetworkArgs,
#[command(flatten)]
pub virtual_store: crate::cli_args::VirtualStoreArgs,
}
#[derive(Debug, Args)]
pub struct ScriptArgs {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub args: Vec<String>,
#[arg(long)]
pub no_install: bool,
#[command(flatten)]
pub lockfile: crate::cli_args::LockfileArgs,
#[command(flatten)]
pub network: crate::cli_args::NetworkArgs,
#[command(flatten)]
pub virtual_store: crate::cli_args::VirtualStoreArgs,
}
pub async fn run(
run_args: RunArgs,
filter: aube_workspace::selector::EffectiveFilter,
) -> miette::Result<()> {
run_args.network.install_overrides();
run_args.lockfile.install_overrides();
run_args.virtual_store.install_overrides();
let RunArgs {
script,
args,
no_install,
no_sort,
if_present,
inspect,
inspect_brk,
parallel,
no_bail: _,
report_summary: _,
reporter_hide_prefix,
resume_from,
reverse,
silent,
sort: _,
workspace_concurrency,
lockfile: _,
network: _,
virtual_store: _,
} = run_args;
let silent = silent || super::global_output_flags().silent;
let script = match script {
Some(s) => s,
None => prompt_for_script()?,
};
let node_args = node_args_from_run_flags(inspect, inspect_brk);
let recursive = RecursiveOpts {
sort: !no_sort,
reverse,
resume_from,
workspace_concurrency,
reporter_hide_prefix,
};
run_script_with(
&script, &args, &node_args, no_install, if_present, parallel, silent, &filter, recursive,
)
.await
}
#[derive(Debug, Clone)]
pub(crate) struct RecursiveOpts {
pub sort: bool,
pub reverse: bool,
pub resume_from: Option<String>,
pub workspace_concurrency: Option<usize>,
pub reporter_hide_prefix: bool,
}
impl Default for RecursiveOpts {
fn default() -> Self {
Self {
sort: true,
reverse: false,
resume_from: None,
workspace_concurrency: None,
reporter_hide_prefix: false,
}
}
}
fn node_args_from_run_flags(inspect: Option<String>, inspect_brk: Option<String>) -> Vec<String> {
let mut args = Vec::with_capacity(2);
if let Some(value) = inspect {
args.push(node_arg("--inspect", &value));
}
if let Some(value) = inspect_brk {
args.push(node_arg("--inspect-brk", &value));
}
args
}
fn node_arg(flag: &str, value: &str) -> String {
if value.is_empty() {
flag.to_string()
} else {
format!("{flag}={value}")
}
}
fn prompt_for_script() -> miette::Result<String> {
let initial_cwd = crate::dirs::cwd()?;
let cwd = crate::dirs::find_project_root(&initial_cwd).ok_or_else(|| {
miette!(
"no package.json found in {} or any parent directory",
initial_cwd.display()
)
})?;
let scripts = read_scripts_in_order(&cwd)?;
if scripts.is_empty() {
return Err(miette!(
"no scripts defined in {}",
cwd.join("package.json").display()
));
}
if !std::io::stdin().is_terminal() {
let names: Vec<&str> = scripts.iter().map(|(n, _)| n.as_str()).collect();
return Err(miette!(
"aube run: script name required when stdin is not a TTY. Available scripts: {}",
names.join(", ")
));
}
let mut picker = demand::Select::new("Select a script to run")
.description("package.json scripts")
.filterable(true);
for (name, cmd) in &scripts {
let label = format!("{name}: {cmd}");
picker = picker.option(demand::DemandOption::new(name.clone()).label(&label));
}
match picker.run() {
Ok(name) => Ok(name),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => std::process::exit(130),
Err(e) => Err(e)
.into_diagnostic()
.wrap_err("failed to read script selection"),
}
}
fn read_scripts_in_order(cwd: &Path) -> miette::Result<Vec<(String, String)>> {
let path = cwd.join("package.json");
let bytes = std::fs::read(&path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {}", path.display()))?;
let value: serde_json::Value = serde_json::from_slice(&bytes)
.into_diagnostic()
.wrap_err_with(|| format!("failed to parse {}", path.display()))?;
let Some(serde_json::Value::Object(obj)) = value.get("scripts") else {
return Ok(Vec::new());
};
Ok(obj
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect())
}
pub(crate) async fn run_script(
script: &str,
args: &[String],
no_install: bool,
if_present: bool,
filter: &aube_workspace::selector::EffectiveFilter,
) -> miette::Result<()> {
let silent = super::global_output_flags().silent;
run_script_with(
script,
args,
&[],
no_install,
if_present,
false,
silent,
filter,
RecursiveOpts::default(),
)
.await
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_script_with(
script: &str,
args: &[String],
node_args: &[String],
no_install: bool,
if_present: bool,
parallel: bool,
silent: bool,
filter: &aube_workspace::selector::EffectiveFilter,
recursive: RecursiveOpts,
) -> miette::Result<()> {
let initial_cwd = crate::dirs::cwd()?;
let cwd = match crate::dirs::find_project_root(&initial_cwd) {
Some(p) => p,
None if !filter.is_empty() => {
crate::dirs::find_workspace_root(&initial_cwd).ok_or_else(|| {
miette!(
"no project (package.json) or workspace root \
(aube-workspace.yaml / pnpm-workspace.yaml) found in {} \
or any parent directory",
initial_cwd.display()
)
})?
}
None => {
return Err(miette!(
"no package.json found in {} or any parent directory",
initial_cwd.display()
));
}
};
let enable_pre_post_scripts = configure_script_settings_for_project(&cwd)?;
if !filter.is_empty() {
return run_script_filtered(
&cwd,
script,
args,
node_args,
no_install,
if_present,
parallel,
silent,
filter,
enable_pre_post_scripts,
recursive,
)
.await;
}
let manifest = load_manifest(&cwd)?;
if !manifest.scripts.contains_key(script) {
ensure_installed(no_install).await?;
let bin_path = super::project_modules_dir(&cwd).join(".bin").join(script);
if bin_path.exists() {
return super::exec::exec_bin_with_node_args(
&cwd, &bin_path, script, args, node_args, false,
)
.await;
}
if if_present {
return Ok(());
}
let mut names: Vec<&str> = manifest.scripts.keys().map(String::as_str).collect();
names.sort_unstable();
let hint = if names.is_empty() {
"no scripts defined in package.json".to_string()
} else {
format!("available scripts: {}", names.join(", "))
};
return Err(miette!("script not found: {script}\n {hint}"));
}
ensure_installed(no_install).await?;
exec_script_chain(
&cwd,
&manifest,
script,
args,
node_args,
enable_pre_post_scripts,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn run_script_filtered(
cwd: &Path,
script: &str,
args: &[String],
node_args: &[String],
no_install: bool,
if_present: bool,
parallel: bool,
silent: bool,
filter: &aube_workspace::selector::EffectiveFilter,
enable_pre_post_scripts: bool,
recursive: RecursiveOpts,
) -> miette::Result<()> {
let (_root, matched) = super::select_workspace_packages(cwd, filter, "run")?;
let matched = order_matched_packages(matched, &recursive)?;
ensure_installed(no_install).await?;
if let Some(concurrency) = effective_concurrency(parallel, recursive.workspace_concurrency) {
return run_filtered_parallel(
script,
args,
node_args,
if_present,
silent,
enable_pre_post_scripts,
matched,
concurrency,
recursive.reporter_hide_prefix,
recursive.reverse,
)
.await;
}
for pkg in &matched {
let name = pkg
.name
.as_deref()
.unwrap_or_else(|| pkg.dir.to_str().unwrap_or("(unnamed)"));
if !pkg.manifest.scripts.contains_key(script) {
let bin_path = super::project_modules_dir(&pkg.dir)
.join(".bin")
.join(script);
if bin_path.exists() {
if !silent {
tracing::info!("aube run: {name} -> {script}");
}
super::exec::exec_bin_with_node_args(
&pkg.dir, &bin_path, script, args, node_args, false,
)
.await?;
continue;
}
if if_present {
continue;
}
return Err(miette!("aube run: package {name} has no `{script}` script"));
}
if !silent {
tracing::info!("aube run: {name} -> {script}");
}
exec_script_chain(
&pkg.dir,
&pkg.manifest,
script,
args,
node_args,
enable_pre_post_scripts,
)
.await?;
}
Ok(())
}
pub(crate) fn order_matched_packages(
mut matched: Vec<aube_workspace::selector::SelectedPackage>,
recursive: &RecursiveOpts,
) -> miette::Result<Vec<aube_workspace::selector::SelectedPackage>> {
if recursive.sort {
matched = aube_workspace::topo::topological_sort(matched);
}
if recursive.reverse {
matched.reverse();
}
if let Some(name) = recursive.resume_from.as_deref() {
let idx = matched
.iter()
.position(|p| p.name.as_deref() == Some(name))
.ok_or_else(|| {
miette!(
"--resume-from package `{name}` is not in the matched \
workspace set"
)
})?;
matched.drain(..idx);
}
Ok(matched)
}
pub(crate) fn effective_concurrency(
parallel: bool,
workspace_concurrency: Option<usize>,
) -> Option<usize> {
match (parallel, workspace_concurrency) {
(_, Some(0)) => Some(
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
.max(1),
),
(_, Some(n)) => Some(n.min(tokio::sync::Semaphore::MAX_PERMITS)),
(true, None) => Some(tokio::sync::Semaphore::MAX_PERMITS),
(false, None) => None,
}
}
#[allow(clippy::too_many_arguments)]
async fn run_filtered_parallel(
script: &str,
args: &[String],
node_args: &[String],
if_present: bool,
silent: bool,
enable_pre_post_scripts: bool,
matched: Vec<aube_workspace::selector::SelectedPackage>,
concurrency: usize,
reporter_hide_prefix: bool,
reverse: bool,
) -> miette::Result<()> {
use std::sync::Arc;
use tokio::sync::Semaphore;
let runnable: Vec<_> = matched
.into_iter()
.filter_map(|pkg| {
if pkg.manifest.scripts.contains_key(script) {
Some(Ok((pkg, None)))
} else {
let bin_path = super::project_modules_dir(&pkg.dir)
.join(".bin")
.join(script);
if bin_path.exists() {
return Some(Ok((pkg, Some(bin_path))));
}
if if_present {
return None;
}
let name = pkg
.name
.clone()
.unwrap_or_else(|| pkg.dir.display().to_string());
Some(Err(miette!(
"aube run: package {name} has no `{script}` script"
)))
}
})
.collect::<miette::Result<Vec<_>>>()?;
let runnable_pkgs: Vec<aube_workspace::selector::SelectedPackage> =
runnable.iter().map(|(p, _)| p.clone()).collect();
let prereqs = aube_workspace::topo::compute_prereq_indices(&runnable_pkgs);
let prereqs = if reverse {
aube_workspace::topo::transpose_prereqs(&prereqs)
} else {
prereqs
};
let senders: Vec<tokio::sync::watch::Sender<bool>> = (0..runnable.len())
.map(|_| tokio::sync::watch::channel(false).0)
.collect();
let prereq_rxs_per_task: Vec<Vec<tokio::sync::watch::Receiver<bool>>> = (0..runnable.len())
.map(|i| prereqs[i].iter().map(|&j| senders[j].subscribe()).collect())
.collect();
let sem = Arc::new(Semaphore::new(concurrency));
let mut tasks: Vec<tokio::task::JoinHandle<miette::Result<std::process::ExitStatus>>> =
Vec::with_capacity(runnable.len());
let mut task_names: Vec<String> = Vec::with_capacity(runnable.len());
let mut senders_iter = senders.into_iter();
let mut prereq_rxs_iter = prereq_rxs_per_task.into_iter();
for (index, (pkg, bin_path)) in runnable.into_iter().enumerate() {
let name = pkg
.name
.clone()
.unwrap_or_else(|| pkg.dir.display().to_string());
if !silent {
tracing::info!("aube run: {name} -> {script} (parallel)");
}
let output_mode = if reporter_hide_prefix {
super::run_output::OutputMode::NoPrefix
} else {
super::run_output::OutputMode::prefix(pkg.name.as_deref(), index)
};
let prereq_rxs = prereq_rxs_iter.next().expect("one rx vec per package");
let done_tx = senders_iter.next().expect("one sender per package");
let script = script.to_string();
let args = args.to_vec();
let node_args = node_args.to_vec();
let dir = pkg.dir.clone();
let manifest = pkg.manifest.clone();
let sem = Arc::clone(&sem);
task_names.push(name);
tasks.push(tokio::spawn(async move {
for mut rx in prereq_rxs {
while !*rx.borrow_and_update() {
if rx.changed().await.is_err() {
break;
}
}
}
let _permit = sem
.acquire_owned()
.await
.map_err(|e| miette!("workspace concurrency semaphore closed: {e}"))?;
let result = if let Some(bin_path) = bin_path {
super::exec::exec_bin_status_with_node_args(
&dir,
&bin_path,
&script,
&args,
&node_args,
false,
&output_mode,
)
.await
} else {
exec_script_status_chain(
&dir,
&manifest,
&script,
&args,
&node_args,
enable_pre_post_scripts,
&output_mode,
)
.await
};
let _ = done_tx.send(true);
result
}));
}
let mut first_err: Option<miette::Report> = None;
let mut first_exit: Option<i32> = None;
for (t, name) in tasks.into_iter().zip(task_names) {
match t.await {
Ok(Ok(status)) => {
if !status.success() && first_exit.is_none() {
let code = aube_scripts::exit_code_from_status(status);
first_exit = Some(code);
first_err = Some(miette!(
"aube run: `{script}` failed in {name} (exit {code})"
));
}
}
Ok(Err(e)) if first_err.is_none() => first_err = Some(e),
Ok(Err(_)) => {}
Err(e) if first_err.is_none() => first_err = Some(miette!("task panicked: {e}")),
Err(_) => {}
}
}
if let Some(code) = first_exit {
std::process::exit(code);
}
if let Some(e) = first_err {
return Err(e);
}
Ok(())
}
pub(crate) fn load_manifest(cwd: &Path) -> miette::Result<PackageJson> {
PackageJson::from_path(&cwd.join("package.json"))
.map_err(miette::Report::new)
.wrap_err("failed to read package.json")
}
fn configure_script_settings_for_project(cwd: &Path) -> miette::Result<bool> {
let files = crate::commands::FileSources::load(cwd);
let (_, raw_workspace) = aube_manifest::workspace::load_both(cwd)
.into_diagnostic()
.wrap_err("failed to load workspace config")?;
let env_snapshot = aube_settings::values::capture_env();
let ctx = files.ctx(&raw_workspace, &env_snapshot, &[]);
let enable_pre_post_scripts = aube_settings::resolved::enable_pre_post_scripts(&ctx);
super::configure_script_settings(&ctx);
Ok(enable_pre_post_scripts)
}
pub(crate) async fn exec_optional(
cwd: &Path,
manifest: &PackageJson,
script: &str,
args: &[String],
) -> miette::Result<bool> {
if !manifest.scripts.contains_key(script) {
return Ok(false);
}
exec_script(cwd, manifest, script, args).await?;
Ok(true)
}
pub(crate) async fn exec_script(
cwd: &Path,
manifest: &PackageJson,
script: &str,
args: &[String],
) -> miette::Result<()> {
exec_script_with_node_args(cwd, manifest, script, args, &[]).await
}
async fn build_script_command(
cwd: &Path,
manifest: &PackageJson,
script: &str,
cmd: &str,
args: &[String],
node_args: &[String],
) -> miette::Result<tokio::process::Command> {
let cmd = inject_node_args(cmd, node_args);
let shell_cmd = if args.is_empty() {
cmd
} else {
let mut buf =
String::with_capacity(cmd.len() + args.iter().map(|a| a.len() + 3).sum::<usize>());
buf.push_str(&cmd);
for a in args {
buf.push(' ');
buf.push_str(&aube_scripts::shell_quote_arg(a));
}
buf
};
let bin_dir = super::project_modules_dir(cwd).join(".bin");
let node_gyp_bin_dir = super::install::node_gyp_bootstrap::lazy_shim_bin_dir(&bin_dir)?;
let path = std::env::var_os("PATH").unwrap_or_default();
let mut entries = Vec::with_capacity(2 + usize::from(node_gyp_bin_dir.is_some()));
entries.push(bin_dir);
if let Some(dir) = node_gyp_bin_dir {
entries.push(dir);
}
entries.extend(std::env::split_paths(&path));
let new_path = std::env::join_paths(entries).unwrap_or(path);
let script_dir = if cwd.is_absolute() {
cwd.to_path_buf()
} else {
crate::dirs::cwd()?.join(cwd)
};
let node_gyp_project_dir =
crate::dirs::find_workspace_root(&script_dir).unwrap_or_else(|| script_dir.clone());
let mut command = aube_scripts::spawn_shell(&shell_cmd);
command
.env("PATH", &new_path)
.current_dir(cwd)
.env(
"AUBE_NODE_GYP_EXE",
std::env::current_exe().into_diagnostic()?,
)
.env("AUBE_NODE_GYP_PROJECT_DIR", node_gyp_project_dir)
.env("npm_lifecycle_event", script)
.stderr(aube_scripts::child_stderr());
if let Some(ref name) = manifest.name {
command.env("npm_package_name", name);
}
if let Some(ref version) = manifest.version {
command.env("npm_package_version", version);
}
if std::env::var_os("INIT_CWD").is_none() {
let init_cwd = crate::dirs::cwd().ok().unwrap_or_else(|| cwd.to_path_buf());
command.env("INIT_CWD", init_cwd);
}
Ok(command)
}
fn inject_node_args(cmd: &str, node_args: &[String]) -> String {
if node_args.is_empty() {
return cmd.to_string();
}
let trimmed = cmd.trim_start();
let leading_len = cmd.len() - trimmed.len();
let Some(rest) = trimmed.strip_prefix("node") else {
return cmd.to_string();
};
if !rest.is_empty() && !rest.starts_with(char::is_whitespace) {
return cmd.to_string();
}
let mut out =
String::with_capacity(cmd.len() + node_args.iter().map(|arg| arg.len() + 1).sum::<usize>());
out.push_str(&cmd[..leading_len + 4]);
for arg in node_args {
out.push(' ');
out.push_str(&aube_scripts::shell_quote_arg(arg));
}
out.push_str(rest);
out
}
pub(crate) async fn exec_script_chain(
cwd: &Path,
manifest: &PackageJson,
script: &str,
args: &[String],
node_args: &[String],
enable_pre_post_scripts: bool,
) -> miette::Result<()> {
if enable_pre_post_scripts {
let pre = format!("pre{script}");
exec_optional(cwd, manifest, &pre, &[]).await?;
}
exec_script_with_node_args(cwd, manifest, script, args, node_args).await?;
if enable_pre_post_scripts {
let post = format!("post{script}");
exec_optional(cwd, manifest, &post, &[]).await?;
}
Ok(())
}
async fn exec_script_with_node_args(
cwd: &Path,
manifest: &PackageJson,
script: &str,
args: &[String],
node_args: &[String],
) -> miette::Result<()> {
let cmd = manifest
.scripts
.get(script)
.ok_or_else(|| miette!("script not found: {script}"))?;
let mut command = build_script_command(cwd, manifest, script, cmd, args, node_args).await?;
let status = command
.status()
.await
.into_diagnostic()
.wrap_err("failed to execute script")?;
if !status.success() {
std::process::exit(aube_scripts::exit_code_from_status(status));
}
Ok(())
}
pub(crate) async fn exec_script_status(
cwd: &Path,
manifest: &PackageJson,
script: &str,
args: &[String],
output_mode: &super::run_output::OutputMode,
) -> miette::Result<std::process::ExitStatus> {
exec_script_status_with_node_args(cwd, manifest, script, args, &[], output_mode).await
}
pub(crate) async fn exec_script_status_chain(
cwd: &Path,
manifest: &PackageJson,
script: &str,
args: &[String],
node_args: &[String],
enable_pre_post_scripts: bool,
output_mode: &super::run_output::OutputMode,
) -> miette::Result<std::process::ExitStatus> {
if enable_pre_post_scripts {
let pre = format!("pre{script}");
if manifest.scripts.contains_key(&pre) {
let status = exec_script_status(cwd, manifest, &pre, &[], output_mode).await?;
if !status.success() {
return Ok(status);
}
}
}
let status =
exec_script_status_with_node_args(cwd, manifest, script, args, node_args, output_mode)
.await?;
if !status.success() {
return Ok(status);
}
if enable_pre_post_scripts {
let post = format!("post{script}");
if manifest.scripts.contains_key(&post) {
return exec_script_status(cwd, manifest, &post, &[], output_mode).await;
}
}
Ok(status)
}
async fn exec_script_status_with_node_args(
cwd: &Path,
manifest: &PackageJson,
script: &str,
args: &[String],
node_args: &[String],
output_mode: &super::run_output::OutputMode,
) -> miette::Result<std::process::ExitStatus> {
let cmd = manifest
.scripts
.get(script)
.ok_or_else(|| miette!("script not found: {script}"))?;
let command = build_script_command(cwd, manifest, script, cmd, args, node_args).await?;
super::run_output::run_command(command, output_mode).await
}
#[cfg(test)]
mod tests {
use super::{
RecursiveOpts, effective_concurrency, inject_node_args, node_args_from_run_flags,
order_matched_packages,
};
use aube_manifest::PackageJson;
use aube_workspace::selector::SelectedPackage;
use std::collections::BTreeMap;
use std::path::PathBuf;
fn pkg(name: &str, deps: &[&str]) -> SelectedPackage {
let manifest = PackageJson {
name: Some(name.to_string()),
dependencies: deps
.iter()
.map(|d| ((*d).to_string(), "*".to_string()))
.collect::<BTreeMap<_, _>>(),
..PackageJson::default()
};
SelectedPackage {
name: Some(name.to_string()),
version: None,
private: false,
dir: PathBuf::from(name),
manifest,
}
}
fn order_names(out: &[SelectedPackage]) -> Vec<&str> {
out.iter().map(|p| p.name.as_deref().unwrap()).collect()
}
#[test]
fn no_parallel_no_concurrency_is_sequential() {
assert_eq!(effective_concurrency(false, None), None);
}
#[test]
fn parallel_alone_uses_semaphore_max_permits() {
assert_eq!(
effective_concurrency(true, None),
Some(tokio::sync::Semaphore::MAX_PERMITS)
);
}
#[test]
fn explicit_concurrency_above_max_permits_is_clamped() {
assert_eq!(
effective_concurrency(true, Some(usize::MAX)),
Some(tokio::sync::Semaphore::MAX_PERMITS)
);
}
#[test]
fn concurrency_overrides_unbounded_parallel() {
assert_eq!(effective_concurrency(true, Some(4)), Some(4));
}
#[test]
fn concurrency_alone_implies_parallel() {
assert_eq!(effective_concurrency(false, Some(3)), Some(3));
}
#[test]
fn concurrency_one_explicit_takes_parallel_path() {
assert_eq!(effective_concurrency(false, Some(1)), Some(1));
assert_eq!(effective_concurrency(true, Some(1)), Some(1));
}
#[test]
fn concurrency_zero_picks_cpu_count() {
let n = effective_concurrency(false, Some(0)).expect("Some on explicit cap");
assert!(n >= 1, "available_parallelism floor is 1");
}
#[test]
fn order_matched_applies_topo_then_reverse_then_resume() {
let opts = RecursiveOpts {
sort: true,
reverse: true,
resume_from: Some("lib".to_string()),
..RecursiveOpts::default()
};
let out = order_matched_packages(
vec![
pkg("app", &["lib"]),
pkg("lib", &["core"]),
pkg("core", &[]),
],
&opts,
)
.unwrap();
assert_eq!(order_names(&out), vec!["lib", "core"]);
}
#[test]
fn order_matched_resume_from_unknown_package_errors() {
let opts = RecursiveOpts {
sort: false,
resume_from: Some("nonexistent".to_string()),
..RecursiveOpts::default()
};
let err = order_matched_packages(vec![pkg("a", &[])], &opts).unwrap_err();
assert!(err.to_string().contains("nonexistent"));
}
#[test]
fn order_matched_no_sort_preserves_input_order() {
let opts = RecursiveOpts {
sort: false,
..RecursiveOpts::default()
};
let out =
order_matched_packages(vec![pkg("app", &["lib"]), pkg("lib", &[])], &opts).unwrap();
assert_eq!(order_names(&out), vec!["app", "lib"]);
}
#[test]
fn node_args_from_flags_supports_optional_values() {
assert_eq!(
node_args_from_run_flags(Some(String::new()), Some("0.0.0.0:9230".to_string())),
vec![
"--inspect".to_string(),
"--inspect-brk=0.0.0.0:9230".to_string()
]
);
}
#[test]
fn inject_node_args_only_touches_direct_node_commands() {
let args = vec!["--inspect".to_string()];
assert_eq!(
inject_node_args("node test.js", &args),
format!(
"node {} test.js",
aube_scripts::shell_quote_arg("--inspect")
)
);
assert_eq!(inject_node_args("tsx test.ts", &args), "tsx test.ts");
assert_eq!(
inject_node_args("node-gyp rebuild", &args),
"node-gyp rebuild"
);
}
}