From 1ff880f0b7eac14af300773e358a7c1573c910c6 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:48:50 -0500 Subject: [PATCH 1/2] fix(cli): fix duplicate versions and validation in 'mise tool' command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed duplicate version display when a tool version is both current and installed The issue was in list_installed_versions() pushing versions twice - Added proper error handling for invalid tool names - Added HashSet deduplication as extra safeguard against duplicates - Created comprehensive e2e tests for the tool command Fixes reported issues: - 'mise tool opentofu' showing same version twice in installed versions - 'mise tool INVALID' not erroring out properly 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- e2e/cli/test_tool | 33 +++++++++++++++++++++++++++++++++ src/cli/tool.rs | 17 ++++++++++++++++- src/toolset/mod.rs | 9 +++++---- 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100755 e2e/cli/test_tool diff --git a/e2e/cli/test_tool b/e2e/cli/test_tool new file mode 100755 index 0000000000..20991c93be --- /dev/null +++ b/e2e/cli/test_tool @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Test valid tool information +mise tool node | grep -q "Backend:" || fail "Backend field not found" + +# Test JSON output +mise tool node --json | grep -q '"backend"' || fail "JSON backend field not found" + +# Test specific field filters +backend_output=$(mise tool node --backend) +[[ -n $backend_output ]] || fail "Backend output is empty" + +# Test that invalid tool names should error +assert_fail "mise tool INVALID_TOOL_NAME" + +# Test duplicate version bug - install multiple versions of a tool +mise use tiny@1.0.0 +mise use tiny@1.1.0 +output=$(mise tool tiny --installed) +# Count occurrences of each version - should be 1 each +count_1_0=$(echo "$output" | grep -o "1.0.0" | wc -l) +count_1_1=$(echo "$output" | grep -o "1.1.0" | wc -l) +[[ $count_1_0 -eq 1 ]] || fail "Version 1.0.0 appears $count_1_0 times, expected 1" +[[ $count_1_1 -eq 1 ]] || fail "Version 1.1.0 appears $count_1_1 times, expected 1" + +# Test that installed versions don't show duplicates +installed_versions=$(mise tool tiny --installed) +# Convert to array and check for uniqueness - sort both to compare +IFS=' ' read -ra versions_array <<<"$installed_versions" +sorted_installed=$(printf '%s\n' "${versions_array[@]}" | sort | tr '\n' ' ' | sed 's/ $//') +unique_versions=$(printf '%s\n' "${versions_array[@]}" | sort -u | tr '\n' ' ' | sed 's/ $//') +[[ $sorted_installed == "$unique_versions" ]] || fail "Installed versions contain duplicates: $installed_versions" diff --git a/src/cli/tool.rs b/src/cli/tool.rs index 23e747ac0a..a786560b4c 100644 --- a/src/cli/tool.rs +++ b/src/cli/tool.rs @@ -1,6 +1,7 @@ use eyre::Result; use itertools::Itertools; use serde_derive::Serialize; +use std::collections::HashSet; use crate::cli::args::BackendArg; use crate::config::Config; @@ -61,7 +62,19 @@ impl Tool { let tvl = ts.versions.get(&self.tool); let tv = tvl.map(|tvl| tvl.versions.first().unwrap()); let ba = tv.map(|tv| tv.ba()).unwrap_or_else(|| &self.tool); - let backend = ba.backend().ok(); + + // Check if the backend exists and fail if it doesn't + let backend = match ba.backend() { + Ok(b) => Some(b), + Err(e) => { + // If no versions are configured for this tool, it's likely invalid + if tvl.is_none() { + return Err(e); + } + None + } + }; + let description = if let Some(backend) = backend { backend.description().await } else { @@ -76,6 +89,8 @@ impl Tool { .into_iter() .filter(|(b, _)| b.ba().as_ref() == ba) .map(|(_, tv)| tv.version) + .collect::>() + .into_iter() .collect::>(), active_versions: tvl.map(|tvl| { tvl.versions diff --git a/src/toolset/mod.rs b/src/toolset/mod.rs index 82ee6f66e6..51473b40a2 100644 --- a/src/toolset/mod.rs +++ b/src/toolset/mod.rs @@ -502,11 +502,12 @@ impl Toolset { for v in b.list_installed_versions() { if let Some((p, tv)) = current_versions.get(&(b.id().into(), v.clone())) { versions.push((p.clone(), tv.clone())); + } else { + let tv = ToolRequest::new(b.ba().clone(), &v, ToolSource::Unknown)? + .resolve(config, &Default::default()) + .await?; + versions.push((b.clone(), tv)); } - let tv = ToolRequest::new(b.ba().clone(), &v, ToolSource::Unknown)? - .resolve(config, &Default::default()) - .await?; - versions.push((b.clone(), tv)); } } Ok(versions) From 7e69f6466f1fa84da2868f25bd647edd3104d726 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:28:06 -0500 Subject: [PATCH 2/2] wip --- e2e/cli/test_tool | 15 +-------------- src/cli/tool.rs | 47 +++++++++++++++++++++++++++++++++++++++++------ src/ui/style.rs | 4 ++++ 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/e2e/cli/test_tool b/e2e/cli/test_tool index 20991c93be..b43029fe21 100755 --- a/e2e/cli/test_tool +++ b/e2e/cli/test_tool @@ -17,17 +17,4 @@ assert_fail "mise tool INVALID_TOOL_NAME" # Test duplicate version bug - install multiple versions of a tool mise use tiny@1.0.0 mise use tiny@1.1.0 -output=$(mise tool tiny --installed) -# Count occurrences of each version - should be 1 each -count_1_0=$(echo "$output" | grep -o "1.0.0" | wc -l) -count_1_1=$(echo "$output" | grep -o "1.1.0" | wc -l) -[[ $count_1_0 -eq 1 ]] || fail "Version 1.0.0 appears $count_1_0 times, expected 1" -[[ $count_1_1 -eq 1 ]] || fail "Version 1.1.0 appears $count_1_1 times, expected 1" - -# Test that installed versions don't show duplicates -installed_versions=$(mise tool tiny --installed) -# Convert to array and check for uniqueness - sort both to compare -IFS=' ' read -ra versions_array <<<"$installed_versions" -sorted_installed=$(printf '%s\n' "${versions_array[@]}" | sort | tr '\n' ' ' | sed 's/ $//') -unique_versions=$(printf '%s\n' "${versions_array[@]}" | sort -u | tr '\n' ' ' | sed 's/ $//') -[[ $sorted_installed == "$unique_versions" ]] || fail "Installed versions contain duplicates: $installed_versions" +assert "mise tool tiny --installed" "1.0.0 1.1.0" diff --git a/src/cli/tool.rs b/src/cli/tool.rs index a786560b4c..85219d8152 100644 --- a/src/cli/tool.rs +++ b/src/cli/tool.rs @@ -1,7 +1,7 @@ +use crate::ui::style; use eyre::Result; use itertools::Itertools; use serde_derive::Serialize; -use std::collections::HashSet; use crate::cli::args::BackendArg; use crate::config::Config; @@ -89,8 +89,7 @@ impl Tool { .into_iter() .filter(|(b, _)| b.ba().as_ref() == ba) .map(|(_, tv)| tv.version) - .collect::>() - .into_iter() + .unique() .collect::>(), active_versions: tvl.map(|tvl| { tvl.versions @@ -152,7 +151,23 @@ impl Tool { miseprintln!("[none]"); } } else if self.filter.installed { - miseprintln!("{}", info.installed_versions.join(" ")); + let active_set = info + .active_versions + .as_ref() + .map(|v| v.iter().collect::>()) + .unwrap_or_default(); + let installed_with_bold = info + .installed_versions + .iter() + .map(|v| { + if active_set.contains(v) { + style::nbold(v).to_string() + } else { + v.to_string() + } + }) + .join(" "); + miseprintln!("{}", installed_with_bold); } else if self.filter.active { if let Some(active_versions) = info.active_versions { miseprintln!("{}", active_versions.join(" ")); @@ -185,9 +200,29 @@ impl Tool { if let Some(description) = info.description { table.push(("Description:", description)); } - table.push(("Installed Versions:", info.installed_versions.join(" "))); + // Bold currently active versions within the installed list for clarity + let active_set = info + .active_versions + .as_ref() + .map(|v| v.iter().collect::>()) + .unwrap_or_default(); + let installed_with_bold = info + .installed_versions + .iter() + .map(|v| { + if active_set.contains(v) { + style::nbold(v).to_string() + } else { + v.to_string() + } + }) + .join(" "); + table.push(("Installed Versions:", installed_with_bold)); if let Some(active_versions) = info.active_versions { - table.push(("Active Version:", active_versions.join(" "))); + table.push(( + "Active Version:", + style::nbold(active_versions.join(" ")).to_string(), + )); } if let Some(requested_versions) = info.requested_versions { table.push(("Requested Version:", requested_versions.join(" "))); diff --git a/src/ui/style.rs b/src/ui/style.rs index 4fb14c52f1..4380183d31 100644 --- a/src/ui/style.rs +++ b/src/ui/style.rs @@ -55,6 +55,10 @@ pub fn ebold(val: D) -> StyledObject { estyle(val).bold() } +pub fn nbold(val: D) -> StyledObject { + nstyle(val).bold() +} + pub fn epath(path: &Path) -> StyledObject { estyle(display_path(path)) }