use std::process::Command;
use crate::core::metadata::Metadata;
use crate::paths;
use crate::uv::UvClient;
use crate::uv::version as uv_version;
#[derive(Debug, Clone, PartialEq)]
pub enum CheckStatus {
Ok,
Warning(String),
Error(String),
}
#[derive(Debug)]
pub struct CheckResult {
pub id: &'static str,
pub name: &'static str,
pub status: CheckStatus,
pub suggestion: Option<String>,
pub details: Option<String>,
}
impl CheckResult {
pub fn ok(id: &'static str, name: &'static str) -> Self {
Self {
id,
name,
status: CheckStatus::Ok,
suggestion: None,
details: None,
}
}
pub fn warn(id: &'static str, name: &'static str, message: impl Into<String>) -> Self {
Self {
id,
name,
status: CheckStatus::Warning(message.into()),
suggestion: None,
details: None,
}
}
pub fn error(id: &'static str, name: &'static str, message: impl Into<String>) -> Self {
Self {
id,
name,
status: CheckStatus::Error(message.into()),
suggestion: None,
details: None,
}
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn with_details(mut self, details: impl Into<String>) -> Self {
self.details = Some(details.into());
self
}
pub fn is_ok(&self) -> bool {
matches!(self.status, CheckStatus::Ok)
}
pub fn is_warning(&self) -> bool {
matches!(self.status, CheckStatus::Warning(_))
}
pub fn is_error(&self) -> bool {
matches!(self.status, CheckStatus::Error(_))
}
}
pub trait Check: Send + Sync {
fn id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn run(&self) -> Vec<CheckResult>;
}
pub struct Doctor {
checks: Vec<Box<dyn Check>>,
}
impl Doctor {
pub fn new() -> Self {
Self {
checks: vec![
Box::new(UvCheck),
Box::new(HomeCheck),
Box::new(VirtualenvCheck),
Box::new(SymlinkCheck),
Box::new(ShellCheck),
Box::new(VersionCheck),
],
}
}
pub fn run_all(&self) -> Vec<CheckResult> {
self.checks.iter().flat_map(|c| c.run()).collect()
}
pub fn run_and_fix(&self, output: &crate::output::Output) -> Vec<CheckResult> {
let mut all_results = Vec::new();
for check in &self.checks {
let results = check.run();
for result in results {
if result.is_error() {
if let Some(fixed_result) = self.try_fix(&result, output) {
output.doctor_check(&fixed_result);
all_results.push(fixed_result);
continue;
}
}
output.doctor_check(&result);
all_results.push(result);
}
}
all_results
}
fn try_fix(&self, result: &CheckResult, output: &crate::output::Output) -> Option<CheckResult> {
match result.id {
"home" => self.fix_home(result, output),
"symlink" => self.fix_symlink(result, output),
_ => None,
}
}
fn fix_home(
&self,
result: &CheckResult,
output: &crate::output::Output,
) -> Option<CheckResult> {
if let CheckStatus::Error(msg) = &result.status {
if msg.contains("not found") {
if let Ok(home) = paths::scoop_home() {
output.info(&format!("Creating {}...", home.display()));
match std::fs::create_dir_all(&home) {
Ok(_) => {
let _ = std::fs::create_dir_all(home.join("virtualenvs"));
return Some(
CheckResult::ok("home", "SCOOP_HOME directory")
.with_details(format!("created {}", home.display())),
);
}
Err(e) => {
return Some(
CheckResult::error(
"home",
"SCOOP_HOME directory",
format!("failed to create: {}", e),
)
.with_suggestion("Check permissions"),
);
}
}
}
}
}
None
}
fn fix_symlink(
&self,
result: &CheckResult,
output: &crate::output::Output,
) -> Option<CheckResult> {
let venv_name = if let CheckStatus::Error(msg) = &result.status {
msg.split('\'').nth(1).map(|s| s.to_string())
} else {
None
}?;
output.info(&format!("Attempting to fix symlink for '{}'...", venv_name));
let venvs_dir = paths::virtualenvs_dir().ok()?;
let venv_path = venvs_dir.join(&venv_name);
if !venv_path.exists() {
return Some(
CheckResult::error(
"symlink",
"broken symlink",
format!("environment '{}' not found", venv_name),
)
.with_suggestion(format!("scoop create {} <python-version>", venv_name)),
);
}
let metadata_path = venv_path.join(Metadata::FILE_NAME);
let python_version = if metadata_path.exists() {
match std::fs::read_to_string(&metadata_path) {
Ok(content) => match serde_json::from_str::<Metadata>(&content) {
Ok(meta) => Some(meta.python_version),
Err(_) => None,
},
Err(_) => None,
}
} else {
let pyvenv_cfg = venv_path.join("pyvenv.cfg");
if pyvenv_cfg.exists() {
std::fs::read_to_string(&pyvenv_cfg)
.ok()
.and_then(|content| {
for line in content.lines() {
if line.starts_with("version") {
return line.split('=').nth(1).map(|v| v.trim().to_string());
}
}
None
})
} else {
None
}
};
let python_version = match python_version {
Some(v) => v,
None => {
return Some(
CheckResult::error(
"symlink",
"broken symlink",
format!("could not determine Python version for '{}'", venv_name),
)
.with_suggestion(format!(
"scoop remove {} && scoop create {} <python-version>",
venv_name, venv_name
)),
);
}
};
output.info(&format!("Found Python version: {}", python_version));
let uv = match UvClient::new() {
Ok(uv) => uv,
Err(_) => {
return Some(
CheckResult::error("symlink", "broken symlink", "uv not available")
.with_suggestion("Install uv first"),
);
}
};
let python_path = match uv.find_python(&python_version) {
Ok(Some(info)) => match info.path {
Some(path) => path,
None => {
return Some(
CheckResult::error(
"symlink",
"broken symlink",
format!("Python {} path not found", python_version),
)
.with_suggestion(format!("scoop install {}", python_version)),
);
}
},
Ok(None) => {
return Some(
CheckResult::error(
"symlink",
"broken symlink",
format!("Python {} not installed", python_version),
)
.with_suggestion(format!("scoop install {}", python_version)),
);
}
Err(_) => {
return Some(
CheckResult::error(
"symlink",
"broken symlink",
"failed to find Python installation",
)
.with_suggestion(format!("scoop install {}", python_version)),
);
}
};
let symlink_path = crate::paths::virtualenv_python_exe(&venv_path);
if symlink_path.exists() || symlink_path.is_symlink() {
if let Err(e) = std::fs::remove_file(&symlink_path) {
return Some(
CheckResult::error(
"symlink",
"broken symlink",
format!("failed to remove old symlink: {}", e),
)
.with_suggestion("Check file permissions"),
);
}
}
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
if let Err(e) = symlink(&python_path, &symlink_path) {
return Some(
CheckResult::error(
"symlink",
"broken symlink",
format!("failed to create symlink: {}", e),
)
.with_suggestion("Check file permissions"),
);
}
}
#[cfg(not(unix))]
{
return Some(
CheckResult::warn(
"symlink",
"broken symlink",
"symlink fix not supported on this platform",
)
.with_suggestion("Manually recreate the symlink"),
);
}
output.success(&format!("Fixed symlink for '{}'", venv_name));
Some(
CheckResult::ok("symlink", "broken symlink")
.with_details(format!("fixed symlink for '{}'", venv_name)),
)
}
}
impl Default for Doctor {
fn default() -> Self {
Self::new()
}
}
struct UvCheck;
impl UvCheck {
fn install_hint() -> &'static str {
if cfg!(target_os = "macos") {
"brew install uv OR curl -LsSf https://astral.sh/uv/install.sh | sh"
} else if cfg!(target_os = "windows") {
"powershell -ExecutionPolicy ByPass -c \"irm https://astral.sh/uv/install.ps1 | iex\""
} else {
"curl -LsSf https://astral.sh/uv/install.sh | sh"
}
}
}
impl Check for UvCheck {
fn id(&self) -> &'static str {
"uv"
}
fn name(&self) -> &'static str {
"uv installation"
}
fn run(&self) -> Vec<CheckResult> {
match Command::new("uv").arg("--version").output() {
Ok(output) if output.status.success() => {
let raw = String::from_utf8_lossy(&output.stdout);
let raw = raw.trim();
if let Some(version) = uv_version::parse(raw) {
if !uv_version::meets_minimum(version) {
return vec![
CheckResult::error(
self.id(),
self.name(),
format!(
"uv {} is older than the supported minimum ({})",
uv_version::format_version(version),
uv_version::format_version(uv_version::MIN_VERSION),
),
)
.with_details(raw.to_string())
.with_suggestion(format!("Upgrade uv: {}", Self::install_hint())),
];
}
}
vec![CheckResult::ok(self.id(), self.name()).with_details(raw.to_string())]
}
_ => {
vec![
CheckResult::error(self.id(), self.name(), "uv not found in PATH")
.with_details("scoop requires uv to manage Python environments")
.with_suggestion(format!("Install uv: {}", Self::install_hint())),
]
}
}
}
}
struct HomeCheck;
impl Check for HomeCheck {
fn id(&self) -> &'static str {
"home"
}
fn name(&self) -> &'static str {
"SCOOP_HOME directory"
}
fn run(&self) -> Vec<CheckResult> {
match paths::scoop_home() {
Ok(path) if path.exists() => {
match path.metadata() {
Ok(meta) if !meta.permissions().readonly() => {
vec![
CheckResult::ok(self.id(), self.name())
.with_details(format!("{}", path.display())),
]
}
_ => {
vec![
CheckResult::error(self.id(), self.name(), "directory not writable")
.with_suggestion(format!("chmod 755 {}", path.display())),
]
}
}
}
Ok(path) => {
vec![
CheckResult::error(self.id(), self.name(), "directory not found")
.with_suggestion(format!("mkdir -p {}", path.display())),
]
}
Err(_) => {
vec![
CheckResult::error(
self.id(),
self.name(),
"could not determine home directory",
)
.with_suggestion("Set SCOOP_HOME environment variable"),
]
}
}
}
}
struct VirtualenvCheck;
impl Check for VirtualenvCheck {
fn id(&self) -> &'static str {
"venv"
}
fn name(&self) -> &'static str {
"virtual environments"
}
fn run(&self) -> Vec<CheckResult> {
let venvs_dir = match paths::virtualenvs_dir() {
Ok(dir) => dir,
Err(_) => {
return vec![CheckResult::error(
self.id(),
self.name(),
"virtualenvs directory not found",
)];
}
};
if !venvs_dir.exists() {
return vec![
CheckResult::ok(self.id(), self.name()).with_details("no environments yet"),
];
}
let mut results = Vec::new();
let mut healthy = 0;
let mut broken_names = Vec::new();
if let Ok(entries) = std::fs::read_dir(&venvs_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let python_path = crate::paths::virtualenv_python_exe(&path);
let pyvenv_cfg = path.join("pyvenv.cfg");
if python_path.exists() && pyvenv_cfg.exists() {
healthy += 1;
} else {
broken_names.push(name);
}
}
}
}
for name in &broken_names {
results.push(
CheckResult::error(
"venv",
"broken virtualenv",
format!("'{}' is corrupted", name),
)
.with_suggestion(format!(
"scoop remove {} && scoop create {} <python-version>",
name, name
)),
);
}
if broken_names.is_empty() {
if healthy > 0 {
results.push(
CheckResult::ok(self.id(), self.name())
.with_details(format!("{} environments, all healthy", healthy)),
);
} else {
results.push(
CheckResult::ok(self.id(), self.name()).with_details("no environments yet"),
);
}
}
results
}
}
struct SymlinkCheck;
impl Check for SymlinkCheck {
fn id(&self) -> &'static str {
"symlink"
}
fn name(&self) -> &'static str {
"symbolic links"
}
fn run(&self) -> Vec<CheckResult> {
let venvs_dir = match paths::virtualenvs_dir() {
Ok(dir) if dir.exists() => dir,
_ => return vec![],
};
let mut results = Vec::new();
let mut valid = 0;
let mut broken_names = Vec::new();
if let Ok(entries) = std::fs::read_dir(&venvs_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let python_path = crate::paths::virtualenv_python_exe(&path);
if python_path.is_symlink() {
match std::fs::read_link(&python_path) {
Ok(target) if target.exists() => {
valid += 1;
}
Ok(_) => {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
broken_names.push(name);
}
Err(_) => {
}
}
}
}
}
}
for name in &broken_names {
results.push(
CheckResult::error(
"symlink",
"broken symlink",
format!("Python symlink in '{}' is broken", name),
)
.with_suggestion(format!(
"scoop remove {} && scoop create {} <python-version>",
name, name
)),
);
}
if broken_names.is_empty() && valid > 0 {
results.push(
CheckResult::ok(self.id(), self.name())
.with_details(format!("{} symlinks valid", valid)),
);
}
results
}
}
struct ShellCheck;
impl Check for ShellCheck {
fn id(&self) -> &'static str {
"shell"
}
fn name(&self) -> &'static str {
"shell configuration"
}
fn run(&self) -> Vec<CheckResult> {
let home = match dirs::home_dir() {
Some(h) => h,
None => {
return vec![CheckResult::error(
self.id(),
self.name(),
"could not determine home directory",
)];
}
};
let shell = std::env::var("SHELL").unwrap_or_default();
let shell_name = std::path::Path::new(&shell)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_lowercase();
let config_files: Vec<(&str, std::path::PathBuf)> = match shell_name.as_str() {
"zsh" => vec![("zsh", home.join(".zshrc"))],
"bash" => {
if cfg!(target_os = "macos") {
vec![
("bash", home.join(".bash_profile")),
("bash", home.join(".bashrc")),
]
} else {
vec![("bash", home.join(".bashrc"))]
}
}
_ => {
return vec![
CheckResult::warn(
self.id(),
self.name(),
format!("unsupported shell: {}", shell_name),
)
.with_details("Supported shells: bash, zsh")
.with_suggestion("Manual setup may be required"),
];
}
};
for (_shell_type, config_path) in &config_files {
if config_path.exists() {
match std::fs::read_to_string(config_path) {
Ok(content) => {
if content.contains("scoop init") {
return vec![
CheckResult::ok(self.id(), self.name())
.with_details(format!("found in {}", config_path.display())),
];
}
}
Err(_) => {
return vec![CheckResult::warn(
self.id(),
self.name(),
format!("could not read {}", config_path.display()),
)];
}
}
}
}
let shell_type = if shell_name == "zsh" { "zsh" } else { "bash" };
let config_file = if shell_name == "zsh" {
"~/.zshrc"
} else if cfg!(target_os = "macos") {
"~/.bash_profile"
} else {
"~/.bashrc"
};
vec![
CheckResult::error(
self.id(),
self.name(),
"scoop init not found in shell config",
)
.with_suggestion(format!(
"Add to {}: eval \"$(scoop init {})\"",
config_file, shell_type
)),
]
}
}
fn classify_version_entry(
id: &'static str,
name: &'static str,
entry: &str,
venvs_dir: Option<&std::path::Path>,
) -> CheckResult {
if entry.eq_ignore_ascii_case("system") {
return CheckResult::ok(id, name).with_details("system Python (no virtualenv)");
}
let env_exists = venvs_dir
.map(|dir| dir.join(entry).exists())
.unwrap_or(false);
if env_exists {
CheckResult::ok(id, name).with_details(format!("set to '{}'", entry))
} else {
CheckResult::error(id, name, format!("references non-existent env '{}'", entry))
.with_suggestion(format!("Run: scoop create {} <python-version>", entry))
}
}
struct VersionCheck;
impl Check for VersionCheck {
fn id(&self) -> &'static str {
"version"
}
fn name(&self) -> &'static str {
"version files"
}
fn run(&self) -> Vec<CheckResult> {
let mut results = Vec::new();
let venvs_dir = paths::virtualenvs_dir().ok();
if let Ok(global_file) = paths::global_version_file() {
if global_file.exists() {
match std::fs::read_to_string(&global_file) {
Ok(content) => {
let env_name = content.trim();
if !env_name.is_empty() {
results.push(classify_version_entry(
"version:global",
"global version",
env_name,
venvs_dir.as_deref(),
));
}
}
Err(_) => {
results.push(
CheckResult::warn(
"version:global",
"global version",
"could not read global version file",
)
.with_suggestion(format!("Check file: {}", global_file.display())),
);
}
}
}
}
let current_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => return results,
};
let local_file = paths::local_version_file(¤t_dir);
if local_file.exists() {
match std::fs::read_to_string(&local_file) {
Ok(content) => {
let env_name = content.trim();
if !env_name.is_empty() {
results.push(classify_version_entry(
"version:local",
"local version",
env_name,
venvs_dir.as_deref(),
));
}
}
Err(_) => {
results.push(
CheckResult::warn(
"version:local",
"local version",
"could not read local version file",
)
.with_suggestion("Check .scoop-version file permissions"),
);
}
}
}
if results.is_empty() {
results.push(
CheckResult::ok(self.id(), self.name()).with_details("no version files configured"),
);
}
results
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_result_ok() {
let result = CheckResult::ok("test", "Test Check");
assert_eq!(result.id, "test");
assert_eq!(result.name, "Test Check");
assert!(result.is_ok());
assert!(!result.is_warning());
assert!(!result.is_error());
}
#[test]
fn test_check_result_warn() {
let result = CheckResult::warn("test", "Test Check", "warning message");
assert!(result.is_warning());
assert!(!result.is_ok());
assert!(!result.is_error());
}
#[test]
fn test_check_result_error() {
let result = CheckResult::error("test", "Test Check", "error message");
assert!(result.is_error());
assert!(!result.is_ok());
assert!(!result.is_warning());
}
#[test]
fn test_check_result_with_suggestion() {
let result =
CheckResult::error("test", "Test Check", "error").with_suggestion("fix it like this");
assert!(result.suggestion.is_some());
assert_eq!(result.suggestion.unwrap(), "fix it like this");
}
#[test]
fn test_check_result_with_details() {
let result = CheckResult::ok("test", "Test Check").with_details("version 1.0.0");
assert!(result.details.is_some());
assert_eq!(result.details.unwrap(), "version 1.0.0");
}
#[test]
fn test_check_result_builder_chain() {
let result = CheckResult::warn("test", "Test Check", "warning")
.with_suggestion("do this")
.with_details("more info");
assert!(result.is_warning());
assert!(result.suggestion.is_some());
assert!(result.details.is_some());
}
#[test]
fn test_doctor_has_default_checks() {
let doctor = Doctor::new();
assert!(!doctor.checks.is_empty());
}
#[test]
fn test_check_status_equality() {
assert_eq!(CheckStatus::Ok, CheckStatus::Ok);
assert_eq!(
CheckStatus::Warning("a".to_string()),
CheckStatus::Warning("a".to_string())
);
assert_ne!(
CheckStatus::Warning("a".to_string()),
CheckStatus::Warning("b".to_string())
);
}
use crate::test_utils::with_temp_scoop_home;
use serial_test::serial;
#[test]
#[serial]
fn virtualenv_check_emits_broken_for_corrupted_env() {
with_temp_scoop_home(|temp| {
let broken = temp.path().join("virtualenvs").join("brokenenv");
std::fs::create_dir_all(&broken).unwrap();
let results = VirtualenvCheck.run();
assert!(!results.is_empty(), "expected at least one CheckResult");
assert!(
results.iter().any(|r| r.is_error()
&& r.name.contains("broken")
&& matches!(&r.status, CheckStatus::Error(msg) if msg.contains("brokenenv"))),
"expected an error CheckResult naming the broken env, got {results:#?}"
);
});
}
#[test]
#[serial]
fn virtualenv_check_returns_ok_for_healthy_env() {
with_temp_scoop_home(|temp| {
let env = temp.path().join("virtualenvs").join("healthy");
std::fs::create_dir_all(env.join("bin")).unwrap();
std::fs::write(env.join("bin").join("python"), "").unwrap();
std::fs::write(env.join("pyvenv.cfg"), "").unwrap();
let results = VirtualenvCheck.run();
assert!(!results.is_empty(), "expected a non-empty result set");
assert!(
results.iter().all(|r| r.is_ok()),
"all results should be Ok, got {results:#?}"
);
});
}
#[cfg(unix)]
#[test]
#[serial]
fn symlink_check_emits_error_for_broken_python_symlink() {
with_temp_scoop_home(|temp| {
use std::os::unix::fs::symlink;
let env = temp.path().join("virtualenvs").join("brokenlink");
std::fs::create_dir_all(env.join("bin")).unwrap();
symlink("/nonexistent/python", env.join("bin").join("python")).unwrap();
let results = SymlinkCheck.run();
assert!(
!results.is_empty(),
"broken symlink env must produce at least one result"
);
assert!(
results.iter().any(|r| r.is_error()),
"expected at least one error result, got {results:#?}"
);
});
}
#[test]
fn classify_treats_system_as_valid_sentinel() {
let result = classify_version_entry("version:test", "test version", "system", None);
assert!(
result.is_ok(),
"system must classify as Ok, got {result:#?}"
);
assert!(
result
.details
.as_deref()
.is_some_and(|d| d.contains("system Python")),
"details should explain the sentinel, got {:?}",
result.details
);
}
#[test]
fn classify_is_case_insensitive_for_system_sentinel() {
for variant in ["SYSTEM", "System", "sYsTeM"] {
let result = classify_version_entry("version:test", "test version", variant, None);
assert!(
result.is_ok(),
"case-variant '{variant}' must classify as Ok"
);
}
}
#[test]
fn classify_existing_env_is_ok() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("myenv")).unwrap();
let result =
classify_version_entry("version:test", "test version", "myenv", Some(tmp.path()));
assert!(result.is_ok());
assert!(
result
.details
.as_deref()
.is_some_and(|d| d.contains("myenv"))
);
}
#[test]
fn classify_missing_env_is_error() {
let tmp = tempfile::tempdir().unwrap();
let result = classify_version_entry(
"version:test",
"test version",
"missingenv",
Some(tmp.path()),
);
assert!(result.is_error());
assert!(
result
.suggestion
.as_deref()
.is_some_and(|s| s.contains("scoop create"))
);
}
#[test]
#[serial]
fn version_check_treats_global_system_sentinel_as_ok() {
with_temp_scoop_home(|temp| {
std::fs::write(temp.path().join("version"), "system").unwrap();
std::fs::create_dir_all(temp.path().join("virtualenvs")).unwrap();
let results = VersionCheck.run();
let global = results
.iter()
.find(|r| r.id == "version:global")
.expect("version:global must run when ~/.scoop/version exists");
assert!(
global.is_ok(),
"version:global must be Ok for system sentinel, got {global:#?}"
);
});
}
#[test]
#[serial]
fn version_check_treats_local_system_sentinel_as_ok() {
with_temp_scoop_home(|temp| {
let cwd_guard = TempDirCwdGuard::new();
std::fs::write(cwd_guard.path().join(".scoop-version"), "system").unwrap();
std::fs::create_dir_all(temp.path().join("virtualenvs")).unwrap();
let results = VersionCheck.run();
let local = results
.iter()
.find(|r| r.id == "version:local")
.expect("version:local check should run when .scoop-version exists");
assert!(
local.is_ok(),
"version:local must be Ok for system sentinel, got {local:#?}"
);
});
}
struct TempDirCwdGuard {
_tmp: tempfile::TempDir,
new_cwd: std::path::PathBuf,
original: std::path::PathBuf,
}
impl TempDirCwdGuard {
fn new() -> Self {
let original = std::env::current_dir().expect("cwd readable");
let tmp = tempfile::tempdir().unwrap();
let new_cwd = tmp.path().to_path_buf();
std::env::set_current_dir(&new_cwd).expect("chdir into tempdir");
Self {
_tmp: tmp,
new_cwd,
original,
}
}
fn path(&self) -> &std::path::Path {
&self.new_cwd
}
}
impl Drop for TempDirCwdGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original);
}
}
#[test]
#[serial]
fn fix_symlink_returns_some_for_parseable_error_name() {
with_temp_scoop_home(|temp| {
let broken = temp.path().join("virtualenvs");
std::fs::create_dir_all(&broken).unwrap();
let probe = CheckResult::error(
"symlink",
"broken symlink",
"Python symlink in 'fix-target' is broken".to_string(),
);
let doctor = Doctor::new();
let output = crate::output::Output::new(0, true, true, false);
let fixed = doctor.fix_symlink(&probe, &output);
assert!(fixed.is_some(), "fix_symlink must return Some");
let r = fixed.unwrap();
assert!(r.is_error() || r.is_warning());
assert!(
r.suggestion
.as_deref()
.is_some_and(|s| s.contains("scoop create"))
|| matches!(&r.status, CheckStatus::Error(msg) if msg.contains("fix-target"))
);
});
}
}