fabro(01KN4JD6GJTC3PHC8G80EA950B): implement (success)

Fabro-Run: 01KN4JD6GJTC3PHC8G80EA950B
Fabro-Completed: 5
Fabro-Checkpoint: 76bcffe4c0

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-04-01 13:16:56 +00:00
parent 91ec9e001c
commit 964ceff724
2 changed files with 170 additions and 1 deletions

View file

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
#[cfg(feature = "server")]
use std::process::Command;
#[cfg(feature = "server")]
@ -6,6 +6,7 @@ use std::sync::LazyLock;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use fabro_config::FabroSettingsExt;
#[cfg(feature = "server")]
use fabro_config::server::{ApiAuthStrategy, AuthProvider};
use fabro_config::user::{default_user_config_path, legacy_user_config_path};
@ -241,6 +242,53 @@ pub(crate) fn check_config(
}
}
pub(crate) fn check_storage_dir(path: &Path, readable: bool, writable: bool) -> CheckResult {
let summary = path.display().to_string();
let exists = path.is_dir();
let mut details = vec![
CheckDetail::new(format!("Exists: {}", if exists { "yes" } else { "no" })),
CheckDetail::new(format!("Readable: {}", if readable { "yes" } else { "no" })),
CheckDetail::new(format!("Writable: {}", if writable { "yes" } else { "no" })),
];
if !exists {
return CheckResult {
name: "Storage directory".to_string(),
status: CheckStatus::Error,
summary,
details,
remediation: Some(format!("Create the directory: mkdir -p {}", path.display())),
};
}
if !readable || !writable {
let mut issues = Vec::new();
if !readable {
issues.push("not readable");
}
if !writable {
issues.push("not writable");
}
details.push(CheckDetail::new(format!("Issues: {}", issues.join(", "))));
return CheckResult {
name: "Storage directory".to_string(),
status: CheckStatus::Error,
summary,
details,
remediation: Some(format!("Fix permissions on {}", path.display())),
};
}
CheckResult {
name: "Storage directory".to_string(),
status: CheckStatus::Pass,
summary,
details,
remediation: None,
}
}
pub(crate) fn check_llm_providers(
statuses: &[(Provider, bool)],
live_results: Option<&[(Provider, Result<(), String>)]>,
@ -979,6 +1027,13 @@ pub(crate) async fn run_doctor(
let legacy_config_path = legacy_user_config_path();
let legacy_config_exists = legacy_config_path.as_ref().is_some_and(|p| p.exists());
let storage_dir = globals
.storage_dir
.clone()
.unwrap_or_else(|| cli_settings.storage_dir());
let storage_readable = std::fs::read_dir(&storage_dir).is_ok();
let storage_writable = tempfile::tempfile_in(&storage_dir).is_ok();
let llm_statuses: Vec<(Provider, bool)> = Provider::ALL
.iter()
.map(|p| (*p, p.has_api_key()))
@ -1190,6 +1245,7 @@ pub(crate) async fn run_doctor(
None
},
),
check_storage_dir(&storage_dir, storage_readable, storage_writable),
check_llm_providers(&llm_statuses, llm_live_results.as_deref()),
check_github_app(&github_status),
],
@ -1273,6 +1329,40 @@ mod tests {
);
}
// -- check_storage_dir --
#[test]
fn check_storage_dir_pass() {
let dir = tempfile::tempdir().unwrap();
let result = check_storage_dir(dir.path(), true, true);
assert_eq!(result.status, CheckStatus::Pass);
assert!(result.summary.contains(dir.path().to_str().unwrap()));
assert!(result.remediation.is_none());
}
#[test]
fn check_storage_dir_not_exists() {
let path = PathBuf::from("/tmp/nonexistent-fabro-doctor-test-xyz");
let result = check_storage_dir(&path, false, false);
assert_eq!(result.status, CheckStatus::Error);
assert!(result.summary.contains("nonexistent-fabro-doctor-test-xyz"));
assert!(result.remediation.as_deref().unwrap().contains("mkdir -p"));
}
#[test]
fn check_storage_dir_not_writable() {
let dir = tempfile::tempdir().unwrap();
let result = check_storage_dir(dir.path(), true, false);
assert_eq!(result.status, CheckStatus::Error);
assert!(
result
.remediation
.as_deref()
.unwrap()
.contains("Fix permissions")
);
}
// -- check_llm_providers --
#[test]

View file

@ -45,6 +45,7 @@ fn dry_run_flag() {
Required
[!] Configuration (no user config file found)
[✓] Storage directory ([STORAGE_DIR])
[✓] LLM providers (1 configured)
[!] GitHub App (not configured)
@ -63,6 +64,84 @@ fn dry_run_flag() {
");
}
#[test]
fn storage_dir_shown_in_output() {
let context = test_context!();
let mut cmd = context.doctor();
cmd.arg("--dry-run");
cmd.env("ANTHROPIC_API_KEY", "sk-test-dummy");
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Fabro Doctor
Required
[!] Configuration (no user config file found)
[✓] Storage directory ([STORAGE_DIR])
[✓] LLM providers (1 configured)
[!] GitHub App (not configured)
Optional
[!] Cloud sandbox (no sandbox configured)
[!] Brave Search (not configured)
Found issues in 4 categories.
Warnings:
• Configuration — Create ~/.fabro/user.toml
• GitHub App — Configure GitHub App in server.toml and set env vars to enable GitHub integration
• Cloud sandbox — Set DAYTONA_API_KEY to enable cloud sandbox execution
• Brave Search — Set BRAVE_SEARCH_API_KEY to enable web search
----- stderr -----
");
}
#[test]
fn storage_dir_missing_shows_error() {
let context = test_context!();
let mut cmd = context.doctor();
cmd.arg("--dry-run");
cmd.env("FABRO_STORAGE_DIR", "/tmp/nonexistent-fabro-doctor-xyz");
cmd.env("ANTHROPIC_API_KEY", "sk-test-dummy");
let filters = {
let mut f = context.filters();
f.push((
regex::escape("/tmp/nonexistent-fabro-doctor-xyz"),
"[MISSING_DIR]".to_string(),
));
f
};
fabro_snapshot!(filters, cmd, @"
success: false
exit_code: 1
----- stdout -----
Fabro Doctor
Required
[!] Configuration (no user config file found)
[✗] Storage directory ([MISSING_DIR])
[✓] LLM providers (1 configured)
[!] GitHub App (not configured)
Optional
[!] Cloud sandbox (no sandbox configured)
[!] Brave Search (not configured)
Found issues in 5 categories.
Errors:
• Storage directory — Create the directory: mkdir -p [MISSING_DIR]
Warnings:
• Configuration — Create ~/.fabro/user.toml
• GitHub App — Configure GitHub App in server.toml and set env vars to enable GitHub integration
• Cloud sandbox — Set DAYTONA_API_KEY to enable cloud sandbox execution
• Brave Search — Set BRAVE_SEARCH_API_KEY to enable web search
----- stderr -----
");
}
#[fabro_macros::e2e_test(live("ANTHROPIC_API_KEY"))]
fn live_doctor() {
let context = test_context!();