Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/vp_global_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ url = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

[dev-dependencies]
httpmock = { workspace = true }
serial_test = { workspace = true }
temp-env = { workspace = true }
vp_shared = { workspace = true, features = ["test-utils"] }
Expand Down
10 changes: 0 additions & 10 deletions crates/vp_global_cli/src/command_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,16 +652,6 @@ mod tests {
assert_eq!(summary, None);
}

#[test]
fn help_entry_uses_static_inline_description() {
let help = COMMANDS
.iter()
.find(|entry| entry.command == "help")
.expect("help command should exist");
assert_eq!(help.label, "help");
assert_eq!(help.summary, "View all commands and details");
}

#[test]
fn picker_instruction_mentions_search() {
assert_eq!(
Expand Down
10 changes: 0 additions & 10 deletions crates/vp_global_cli/src/commands/env/bin_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,16 +376,6 @@ mod tests {
.await;
}

#[tokio::test]
async fn test_load_nonexistent() {
let temp_dir = TempDir::new().unwrap();
vp_shared::EnvConfig::with_vars_async([(env_vars::VP_HOME, temp_dir.path())], |_| async {
let loaded = BinConfig::load("nonexistent").await.unwrap();
assert!(loaded.is_none());
})
.await;
}

#[test]
fn test_source_defaults_to_vp() {
let config = BinConfig::new(
Expand Down
127 changes: 50 additions & 77 deletions crates/vp_global_cli/src/commands/env/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,10 +597,42 @@ pub async fn resolve_version_alias(
})
}

/// Run `test` with `vars` and a local `VP_NODE_DIST_MIRROR`, so version resolution stays offline.
#[cfg(test)]
pub(super) async fn with_mock_node_index<Fut: Future<Output = ()>>(
vars: impl IntoIterator<Item = (&'static str, &std::ffi::OsStr)>,
test: impl FnOnce() -> Fut,
) {
use httpmock::prelude::*;

// Fixed versions keep resolved aliases stable across new Node.js releases.
const NODE_INDEX: &str = r#"[
{"version": "v25.1.0", "lts": false},
{"version": "v24.11.0", "lts": "Krypton"},
{"version": "v24.10.0", "lts": false},
{"version": "v22.21.0", "lts": "Jod"},
{"version": "v22.20.0", "lts": "Jod"},
{"version": "v20.19.0", "lts": "Iron"},
{"version": "v20.18.0", "lts": "Iron"}
]"#;

let server = MockServer::start_async().await;
server
.mock_async(|when, then| {
when.method(GET).path("/index.json");
then.status(200).body(NODE_INDEX);
})
.await;
let vars = vars
.into_iter()
.map(|(name, value)| (name, value.to_os_string()))
.chain([(vp_shared::env_vars::VP_NODE_DIST_MIRROR, server.base_url().into())]);
vp_shared::EnvConfig::with_vars_async(vars, |_| test()).await;
}

#[cfg(test)]
mod tests {
use tempfile::TempDir;
use vp_js_runtime::VersionSource;
use vp_shared::env_vars;
use vt_path::AbsolutePathBuf;

Expand Down Expand Up @@ -737,47 +769,6 @@ mod tests {
.await;
}

#[tokio::test]
async fn test_resolve_version_from_engines_node() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();

// Create package.json with engines.node
// Also create an empty .node-version to stop walk-up from finding parent project's version
let package_json = r#"{"engines":{"node":"20.18.0"}}"#;
tokio::fs::write(temp_path.join("package.json"), package_json).await.unwrap();

// Use resolve_node_version directly with walk_up=false to test engines.node specifically
let resolution = resolve_node_version(&temp_path, false)
.await
.map_err(|e| Error::ConfigError(e.to_string().into()))
.unwrap()
.unwrap();

assert_eq!(&*resolution.version, "20.18.0");
assert_eq!(resolution.source, VersionSource::EnginesNode);
}

#[tokio::test]
async fn test_resolve_version_from_dev_engines() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();

// Create package.json with devEngines.runtime
let package_json = r#"{"devEngines":{"runtime":{"name":"node","version":"20.18.0"}}}"#;
tokio::fs::write(temp_path.join("package.json"), package_json).await.unwrap();

// Use resolve_node_version directly with walk_up=false to test devEngines specifically
let resolution = resolve_node_version(&temp_path, false)
.await
.map_err(|e| Error::ConfigError(e.to_string().into()))
.unwrap()
.unwrap();

assert_eq!(&*resolution.version, "20.18.0");
assert_eq!(resolution.source, VersionSource::DevEnginesRuntime);
}

#[tokio::test]
async fn test_resolve_version_from_nvmrc() {
let temp_dir = TempDir::new().unwrap();
Expand Down Expand Up @@ -858,11 +849,10 @@ mod tests {
}

#[tokio::test]
#[ignore] // Requires running outside of any Node.js project (walk-up finds .node-version)
async fn test_resolve_version_alias_default_no_source_path() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
vp_shared::EnvConfig::with_vars_async([(env_vars::VP_HOME, temp_dir.path())], |_| async {
with_mock_node_index([(env_vars::VP_HOME, temp_dir.path().as_os_str())], || async {
Comment thread
liangmiQwQ marked this conversation as resolved.
Resolved
let config =
Config { default_node_version: Some("lts".to_string()), ..Default::default() };
save_config(&config).await.unwrap();
Expand All @@ -872,14 +862,15 @@ mod tests {
tokio::fs::create_dir_all(&test_dir).await.unwrap();

let resolution = resolve_version(&test_dir).await.unwrap();
assert_eq!(resolution.version, "24.11.0");
assert_eq!(resolution.source, "default");
assert!(resolution.source_path.is_none(), "Alias defaults should not have source_path");
assert!(resolution.is_range);
})
.await;
}

#[tokio::test]
#[ignore] // Requires running outside of any Node.js project (walk-up finds .node-version)
async fn test_resolve_version_exact_default_has_source_path() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
Expand All @@ -906,7 +897,7 @@ mod tests {
async fn test_resolve_version_invalid_node_version_falls_through_to_lts() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
vp_shared::EnvConfig::with_vars_async([(env_vars::VP_HOME, temp_dir.path())], |_| async {
with_mock_node_index([(env_vars::VP_HOME, temp_dir.path().as_os_str())], || async {
// Create .node-version file with invalid version
tokio::fs::write(temp_path.join(".node-version"), "invalid-version\n").await.unwrap();

Expand All @@ -915,6 +906,7 @@ mod tests {

// Should fall through to LTS since the .node-version is invalid
// and no user default is configured
assert_eq!(resolution.version, "24.11.0");
assert_eq!(resolution.source, "lts");
assert!(resolution.source_path.is_none());
assert!(resolution.is_range, "LTS fallback should be marked as range");
Expand Down Expand Up @@ -949,7 +941,7 @@ mod tests {
async fn test_resolve_version_invalid_node_version_falls_through_to_engines_node() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
vp_shared::EnvConfig::with_vars_async([(env_vars::VP_HOME, temp_dir.path())], |_| async {
with_mock_node_index([(env_vars::VP_HOME, temp_dir.path().as_os_str())], || async {
// Create .node-version file with invalid version (typo or unsupported alias)
tokio::fs::write(temp_path.join(".node-version"), "laetst\n").await.unwrap();

Expand All @@ -962,12 +954,7 @@ mod tests {

// Should fall through to engines.node since .node-version is invalid
assert_eq!(resolution.source, "engines.node");
// Version should be resolved from ^20.18.0 (a 20.x version)
assert!(
resolution.version.starts_with("20."),
"Expected version to start with '20.', got: {}",
resolution.version
);
assert_eq!(resolution.version, "20.19.0");
})
.await;
}
Expand All @@ -976,7 +963,7 @@ mod tests {
async fn test_resolve_version_invalid_node_version_falls_through_to_dev_engines() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
vp_shared::EnvConfig::with_vars_async([(env_vars::VP_HOME, temp_dir.path())], |_| async {
with_mock_node_index([(env_vars::VP_HOME, temp_dir.path().as_os_str())], || async {
// Create .node-version file with invalid version
tokio::fs::write(temp_path.join(".node-version"), "invalid\n").await.unwrap();

Expand All @@ -989,12 +976,7 @@ mod tests {

// Should fall through to devEngines.runtime since .node-version is invalid
assert_eq!(resolution.source, "devEngines.runtime");
// Version should be resolved from ^20.18.0 (a 20.x version)
assert!(
resolution.version.starts_with("20."),
"Expected version to start with '20.', got: {}",
resolution.version
);
assert_eq!(resolution.version, "20.19.0");
})
.await;
}
Expand All @@ -1003,7 +985,7 @@ mod tests {
async fn test_resolve_version_invalid_engines_node_falls_through_to_dev_engines() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
vp_shared::EnvConfig::with_vars_async([(env_vars::VP_HOME, temp_dir.path())], |_| async {
with_mock_node_index([(env_vars::VP_HOME, temp_dir.path().as_os_str())], || async {
// Create package.json with invalid engines.node but valid devEngines.runtime
// No .node-version file — resolve_node_version returns EnginesNode source
let package_json = r#"{"engines":{"node":"invalid"},"devEngines":{"runtime":{"name":"node","version":"^20.18.0"}}}"#;
Expand All @@ -1013,11 +995,7 @@ mod tests {
let resolution = resolve_version(&temp_path).await.unwrap();

assert_eq!(resolution.source, "devEngines.runtime");
assert!(
resolution.version.starts_with("20."),
"Expected version to start with '20.', got: {}",
resolution.version
);
assert_eq!(resolution.version, "20.19.0");
})
.await;
}
Expand Down Expand Up @@ -1140,7 +1118,7 @@ mod tests {
async fn test_resolve_version_latest_alias_in_node_version() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
vp_shared::EnvConfig::with_vars_async([(env_vars::VP_HOME, temp_dir.path())], |_| async {
with_mock_node_index([(env_vars::VP_HOME, temp_dir.path().as_os_str())], || async {
// Create .node-version file with "latest" alias
tokio::fs::write(temp_path.join(".node-version"), "latest\n").await.unwrap();

Expand All @@ -1150,12 +1128,8 @@ mod tests {
assert_eq!(resolution.source, ".node-version");
// "latest" is a range (should be re-resolved periodically)
assert!(resolution.is_range, "'latest' should be marked as a range");
// Version should be at least v20.x
assert!(
resolution.version.starts_with("2") || resolution.version.starts_with("3"),
"Expected version to be at least v20.x, got: {}",
resolution.version
);
// "latest" includes non-LTS releases
assert_eq!(resolution.version, "25.1.0");
})
.await;
}
Expand Down Expand Up @@ -1189,16 +1163,15 @@ mod tests {
async fn test_resolve_version_env_var_partial_version() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
vp_shared::EnvConfig::with_vars_async(
with_mock_node_index(
[
(env_vars::VP_HOME, temp_dir.path().as_os_str()),
(env_vars::VP_NODE_VERSION, std::ffi::OsStr::new("22")),
],
|_| async {
|| async {
let resolution = resolve_version(&temp_path).await.unwrap();

assert!(resolution.version.starts_with("22."));
assert!(NodeProvider::is_exact_version(&resolution.version));
assert_eq!(resolution.version, "22.21.0");
assert_eq!(resolution.source, VERSION_ENV_VAR);
},
)
Expand Down
Loading
Loading