use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding {
pub key: String,
pub kind: FindingKind,
pub note: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FindingKind {
Deprecated,
Renamed { to: String },
ValueShouldBe { expected: String },
Recommended { suggested_value: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Report {
pub config_path: PathBuf,
pub findings: Vec<Finding>,
}
impl Report {
pub fn is_clean(&self) -> bool {
self.findings.is_empty()
}
pub fn render(&self) -> String {
let mut out = format!(
"# bee-tui :config-doctor report\n# config: {}\n\n",
self.config_path.display(),
);
if self.findings.is_empty() {
out.push_str("(no findings — config looks clean against the deprecation list)\n");
return out;
}
out.push_str(&format!("findings: {}\n\n", self.findings.len()));
for f in &self.findings {
let label = match &f.kind {
FindingKind::Deprecated => "DEPRECATED ".to_string(),
FindingKind::Renamed { to } => format!("RENAMED → {to}"),
FindingKind::ValueShouldBe { expected } => {
format!("VALUE → {expected}")
}
FindingKind::Recommended { suggested_value } => {
format!("MISSING → {suggested_value}")
}
};
out.push_str(&format!(" {label} {}: {}\n", f.key, f.note));
}
out.push_str(
"\n# bee-tui does NOT edit your bee.yaml. Apply changes by hand and restart Bee.\n",
);
out
}
pub fn summary(&self) -> String {
if self.findings.is_empty() {
"config-doctor: no findings — config looks clean".into()
} else {
format!(
"config-doctor: {} finding{} — see report",
self.findings.len(),
if self.findings.len() == 1 { "" } else { "s" },
)
}
}
}
pub fn audit(config_path: &Path) -> Result<Report, String> {
let body = std::fs::read_to_string(config_path)
.map_err(|e| format!("read {}: {e}", config_path.display()))?;
let keys = parse_top_level_keys(&body);
let findings = check_against_rules(&keys);
Ok(Report {
config_path: config_path.to_path_buf(),
findings,
})
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct ConfigKeys {
pub entries: Vec<(String, String)>,
}
impl ConfigKeys {
pub fn has(&self, key: &str) -> bool {
self.entries.iter().any(|(k, _)| k == key)
}
pub fn value(&self, key: &str) -> Option<&str> {
self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
}
}
pub fn parse_top_level_keys(body: &str) -> ConfigKeys {
let mut entries = Vec::new();
for raw in body.lines() {
if raw.starts_with(' ') || raw.starts_with('\t') {
continue;
}
let line = raw
.find(" #")
.map(|i| &raw[..i])
.unwrap_or(raw)
.trim_end();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some(colon) = line.find(':') else {
continue;
};
let (k, rest) = line.split_at(colon);
let key = k.trim().to_string();
if key.is_empty() {
continue;
}
let value = rest[1..].trim().trim_matches(|c: char| c == '"' || c == '\'');
entries.push((key, value.to_string()));
}
ConfigKeys { entries }
}
fn check_against_rules(keys: &ConfigKeys) -> Vec<Finding> {
let mut findings = Vec::new();
if keys.has("chain-enable") {
findings.push(Finding {
key: "chain-enable".into(),
kind: FindingKind::Deprecated,
note: "removed in recent Bee — chain interaction is always on".into(),
});
}
if keys.has("block-hash") {
findings.push(Finding {
key: "block-hash".into(),
kind: FindingKind::Deprecated,
note: "no longer used — Bee derives the genesis hash itself".into(),
});
}
if keys.has("transaction") {
findings.push(Finding {
key: "transaction".into(),
kind: FindingKind::Deprecated,
note: "no longer used".into(),
});
}
if keys.has("swap-endpoint") {
findings.push(Finding {
key: "swap-endpoint".into(),
kind: FindingKind::Renamed {
to: "blockchain-rpc-endpoint".into(),
},
note: "rename and carry the URL value over".into(),
});
}
if keys.has("admin-password") {
findings.push(Finding {
key: "admin-password".into(),
kind: FindingKind::Deprecated,
note: "Bee's admin password mechanism was removed".into(),
});
}
if keys.has("debug-api-addr") {
findings.push(Finding {
key: "debug-api-addr".into(),
kind: FindingKind::Deprecated,
note: "debug API folded into the main listener — use --debug-api-enable=true".into(),
});
}
match keys.value("debug-api-enable") {
Some("true" | "True" | "TRUE") => {
}
Some(other) => {
findings.push(Finding {
key: "debug-api-enable".into(),
kind: FindingKind::ValueShouldBe {
expected: "true".into(),
},
note: format!(
"currently `{other}` — set to true so :diagnose --pprof can fetch CPU profile + trace"
),
});
}
None => {
findings.push(Finding {
key: "debug-api-enable".into(),
kind: FindingKind::Recommended {
suggested_value: "true".into(),
},
note: "missing — :diagnose --pprof requires Bee's debug API to be enabled".into(),
});
}
}
match keys.value("skip-postage-snapshot") {
Some("true" | "True" | "TRUE") => {}
Some(_) | None => {
findings.push(Finding {
key: "skip-postage-snapshot".into(),
kind: FindingKind::ValueShouldBe {
expected: "true".into(),
},
note: "recent Bee versions ignore the postage snapshot — skipping it cuts startup time".into(),
});
}
}
match keys.value("use-postage-snapshot") {
Some("false" | "False" | "FALSE") | None => {}
Some(_) => {
findings.push(Finding {
key: "use-postage-snapshot".into(),
kind: FindingKind::ValueShouldBe {
expected: "false".into(),
},
note: "leave the postage snapshot off (skip-postage-snapshot=true is the canonical setting)".into(),
});
}
}
if !keys.has("storage-incentives-enable") {
findings.push(Finding {
key: "storage-incentives-enable".into(),
kind: FindingKind::Recommended {
suggested_value: "true (or false for non-staking light nodes)".into(),
},
note: "set explicitly so future Bee defaults don't change your mode silently".into(),
});
}
findings
}
#[cfg(test)]
mod tests {
use super::*;
fn ck(entries: &[(&str, &str)]) -> ConfigKeys {
ConfigKeys {
entries: entries.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
}
}
#[test]
fn parser_skips_comments_and_indented_lines() {
let yaml = r#"
# top-level comment
api-addr: 0.0.0.0:1633
swap-enable: true # inline comment
nested:
child: ignored
other: also-ignored
debug-api-enable: false
"#;
let parsed = parse_top_level_keys(yaml);
let keys: Vec<&str> = parsed.entries.iter().map(|(k, _)| k.as_str()).collect();
assert!(keys.contains(&"api-addr"));
assert!(keys.contains(&"swap-enable"));
assert!(keys.contains(&"debug-api-enable"));
assert!(!keys.contains(&"child"));
assert!(!keys.contains(&"other"));
assert!(keys.contains(&"nested"));
}
#[test]
fn parser_unquotes_values() {
let yaml = r#"
url: "https://example.com"
single: 'foo'
plain: bar
"#;
let parsed = parse_top_level_keys(yaml);
assert_eq!(parsed.value("url"), Some("https://example.com"));
assert_eq!(parsed.value("single"), Some("foo"));
assert_eq!(parsed.value("plain"), Some("bar"));
}
#[test]
fn deprecated_keys_each_trigger_a_finding() {
let keys = ck(&[
("chain-enable", "true"),
("block-hash", "0xabc"),
("transaction", "0xdef"),
("admin-password", "hunter2"),
("debug-api-addr", "127.0.0.1:1635"),
("debug-api-enable", "true"),
("skip-postage-snapshot", "true"),
("storage-incentives-enable", "true"),
]);
let findings = check_against_rules(&keys);
let deprecated_keys: Vec<&str> = findings
.iter()
.filter(|f| matches!(f.kind, FindingKind::Deprecated))
.map(|f| f.key.as_str())
.collect();
assert!(deprecated_keys.contains(&"chain-enable"));
assert!(deprecated_keys.contains(&"block-hash"));
assert!(deprecated_keys.contains(&"transaction"));
assert!(deprecated_keys.contains(&"admin-password"));
assert!(deprecated_keys.contains(&"debug-api-addr"));
assert!(!deprecated_keys.contains(&"debug-api-enable"));
assert!(!deprecated_keys.contains(&"skip-postage-snapshot"));
}
#[test]
fn swap_endpoint_renames_to_blockchain_rpc_endpoint() {
let keys = ck(&[
("swap-endpoint", "https://rpc.gnosischain.com"),
("debug-api-enable", "true"),
("skip-postage-snapshot", "true"),
("storage-incentives-enable", "true"),
]);
let findings = check_against_rules(&keys);
let f = findings
.iter()
.find(|f| f.key == "swap-endpoint")
.expect("swap-endpoint not flagged");
match &f.kind {
FindingKind::Renamed { to } => assert_eq!(to, "blockchain-rpc-endpoint"),
_ => panic!("expected Renamed kind"),
}
}
#[test]
fn missing_debug_api_enable_is_recommended() {
let keys = ck(&[
("skip-postage-snapshot", "true"),
("storage-incentives-enable", "true"),
]);
let findings = check_against_rules(&keys);
let f = findings
.iter()
.find(|f| f.key == "debug-api-enable")
.expect("debug-api-enable not flagged");
assert!(matches!(f.kind, FindingKind::Recommended { .. }));
}
#[test]
fn debug_api_enable_false_is_value_should_be_finding() {
let keys = ck(&[
("debug-api-enable", "false"),
("skip-postage-snapshot", "true"),
("storage-incentives-enable", "true"),
]);
let findings = check_against_rules(&keys);
let f = findings
.iter()
.find(|f| f.key == "debug-api-enable")
.expect("debug-api-enable not flagged");
match &f.kind {
FindingKind::ValueShouldBe { expected } => assert_eq!(expected, "true"),
_ => panic!("expected ValueShouldBe"),
}
}
#[test]
fn clean_config_produces_zero_deprecated_findings() {
let keys = ck(&[
("api-addr", "0.0.0.0:1633"),
("data-dir", "/var/lib/bee"),
("password", "hunter2"),
("blockchain-rpc-endpoint", "https://rpc.gnosischain.com"),
("debug-api-enable", "true"),
("skip-postage-snapshot", "true"),
("storage-incentives-enable", "true"),
]);
let findings = check_against_rules(&keys);
assert!(findings.is_empty(), "got: {findings:#?}");
}
#[test]
fn report_render_says_clean_when_no_findings() {
let r = Report {
config_path: PathBuf::from("/etc/bee.yaml"),
findings: vec![],
};
let s = r.render();
assert!(s.contains("clean"));
assert!(r.is_clean());
}
#[test]
fn report_render_lists_each_finding_on_its_own_line() {
let r = Report {
config_path: PathBuf::from("/etc/bee.yaml"),
findings: vec![
Finding {
key: "chain-enable".into(),
kind: FindingKind::Deprecated,
note: "n/a".into(),
},
Finding {
key: "swap-endpoint".into(),
kind: FindingKind::Renamed {
to: "blockchain-rpc-endpoint".into(),
},
note: "rename".into(),
},
],
};
let s = r.render();
assert!(s.contains("DEPRECATED"));
assert!(s.contains("RENAMED → blockchain-rpc-endpoint"));
assert_eq!(s.matches("chain-enable").count(), 1);
}
}