Skip to content

Commit c48b9ea

Browse files
authored
fix(runtime/doctor): ensure workspace doctor checks support multi agents (#7544)
- d0e31e1 fix(runtime/doctor): ensure workspace doctor checks support multi agents - 927b877 ci: make clippy happy - 23cd5ed Merge branch 'master' into tmigone/doctor-multi-workspace Signed-off-by: Tomás Migone <tomasmigone@gmail.com>
1 parent 2d261b2 commit c48b9ea

1 file changed

Lines changed: 144 additions & 12 deletions

File tree

  • crates/zeroclaw-runtime/src/doctor

crates/zeroclaw-runtime/src/doctor/mod.rs

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -832,25 +832,43 @@ fn check_workspace(config: &Config, items: &mut Vec<DiagItem>) {
832832
}
833833
}
834834

835-
// Key workspace files
836-
check_file_exists(ws, "SOUL.md", false, cat, items);
837-
check_file_exists(ws, "AGENTS.md", false, cat, items);
835+
// Per-agent personality files. These are resolved per agent from
836+
// `<install>/agents/<alias>/workspace/` (or an explicit
837+
// `[agents.<alias>.workspace.path]` override) — never from `data_dir`.
838+
// Iterate every enabled agent so multi-agent installs each get checked,
839+
// and name the alias in the result so the report is unambiguous. Sorted
840+
// for deterministic output (HashMap iteration order is unspecified).
841+
let mut agent_aliases: Vec<&String> = config.agents.keys().collect();
842+
agent_aliases.sort();
843+
for alias in agent_aliases {
844+
let agent = config.agents.get(alias).expect("alias from keys()");
845+
if !agent.enabled {
846+
continue;
847+
}
848+
let agent_ws = config.agent_workspace_dir(alias);
849+
check_agent_file(&agent_ws, "SOUL.md", alias, cat, items);
850+
check_agent_file(&agent_ws, "AGENTS.md", alias, cat, items);
851+
}
838852
}
839853

840-
fn check_file_exists(
841-
base: &Path,
854+
/// Existence check for an optional per-agent workspace file. Prefixes the
855+
/// owning agent alias as `[alias]` so a multi-agent report stays legible and
856+
/// `(optional)` keeps its single, consistent meaning as the severity hint
857+
/// (e.g. `[default] SOUL.md present`, `[default] AGENTS.md not found (optional)`).
858+
fn check_agent_file(
859+
workspace_dir: &Path,
842860
name: &str,
843-
required: bool,
861+
alias: &str,
844862
cat: &'static str,
845863
items: &mut Vec<DiagItem>,
846864
) {
847-
let path = base.join(name);
848-
if path.is_file() {
849-
items.push(DiagItem::ok(cat, format!("{name} present")));
850-
} else if required {
851-
items.push(DiagItem::error(cat, format!("{name} missing")));
865+
if workspace_dir.join(name).is_file() {
866+
items.push(DiagItem::ok(cat, format!("[{alias}] {name} present")));
852867
} else {
853-
items.push(DiagItem::warn(cat, format!("{name} not found (optional)")));
868+
items.push(DiagItem::warn(
869+
cat,
870+
format!("[{alias}] {name} not found (optional)"),
871+
));
854872
}
855873
}
856874

@@ -1391,6 +1409,120 @@ mod tests {
13911409
);
13921410
}
13931411

1412+
/// Build a Config whose install root is `root`, with an existing
1413+
/// `data_dir` (so `check_workspace` doesn't early-return) and no agents.
1414+
/// `config_path` anchors `install_root_dir()` → `agent_workspace_dir()`.
1415+
fn workspace_test_config(root: &Path) -> Config {
1416+
let mut config = Config {
1417+
config_path: root.join("config.toml"),
1418+
data_dir: root.join("data"),
1419+
..Config::default()
1420+
};
1421+
std::fs::create_dir_all(&config.data_dir).unwrap();
1422+
config.agents.clear();
1423+
config
1424+
}
1425+
1426+
fn add_enabled_agent(config: &mut Config, alias: &str) {
1427+
config.agents.insert(
1428+
alias.to_string(),
1429+
zeroclaw_config::schema::AliasedAgentConfig {
1430+
enabled: true,
1431+
..Default::default()
1432+
},
1433+
);
1434+
}
1435+
1436+
#[test]
1437+
fn check_workspace_finds_soul_in_agent_workspace_not_data_dir() {
1438+
let tmp = TempDir::new().unwrap();
1439+
let mut config = workspace_test_config(tmp.path());
1440+
add_enabled_agent(&mut config, "default");
1441+
1442+
// SOUL.md lives in the agent workspace — the real load location.
1443+
let ws = config.agent_workspace_dir("default");
1444+
std::fs::create_dir_all(&ws).unwrap();
1445+
std::fs::write(ws.join("SOUL.md"), b"# soul").unwrap();
1446+
// A decoy in data_dir must NOT satisfy the check (proves we don't
1447+
// probe data_dir for personality files).
1448+
std::fs::write(config.data_dir.join("SOUL.md"), b"# decoy").unwrap();
1449+
1450+
let mut items = Vec::new();
1451+
check_workspace(&config, &mut items);
1452+
1453+
let soul = items
1454+
.iter()
1455+
.find(|i| i.message.contains("SOUL.md"))
1456+
.expect("SOUL.md diagnostic present");
1457+
assert_eq!(soul.severity, Severity::Ok);
1458+
assert_eq!(soul.message, "[default] SOUL.md present");
1459+
// No bare data_dir-style message ever surfaces.
1460+
assert!(
1461+
!items.iter().any(|i| i.message == "SOUL.md present"),
1462+
"doctor must not report SOUL.md from data_dir"
1463+
);
1464+
}
1465+
1466+
#[test]
1467+
fn check_workspace_warns_when_agent_soul_missing() {
1468+
let tmp = TempDir::new().unwrap();
1469+
let mut config = workspace_test_config(tmp.path());
1470+
add_enabled_agent(&mut config, "default");
1471+
// Workspace dir need not exist; the file simply isn't there.
1472+
1473+
let mut items = Vec::new();
1474+
check_workspace(&config, &mut items);
1475+
1476+
let soul = items
1477+
.iter()
1478+
.find(|i| i.message.contains("SOUL.md"))
1479+
.expect("SOUL.md diagnostic present");
1480+
assert_eq!(soul.severity, Severity::Warn);
1481+
assert_eq!(soul.message, "[default] SOUL.md not found (optional)");
1482+
}
1483+
1484+
#[test]
1485+
fn check_workspace_skips_disabled_agents() {
1486+
let tmp = TempDir::new().unwrap();
1487+
let mut config = workspace_test_config(tmp.path());
1488+
config.agents.insert(
1489+
"dormant".to_string(),
1490+
zeroclaw_config::schema::AliasedAgentConfig {
1491+
enabled: false,
1492+
..Default::default()
1493+
},
1494+
);
1495+
1496+
let mut items = Vec::new();
1497+
check_workspace(&config, &mut items);
1498+
1499+
assert!(
1500+
!items.iter().any(|i| i.message.contains("dormant")),
1501+
"disabled agents must not produce workspace-file diagnostics"
1502+
);
1503+
}
1504+
1505+
#[test]
1506+
fn check_workspace_checks_each_enabled_agent() {
1507+
let tmp = TempDir::new().unwrap();
1508+
let mut config = workspace_test_config(tmp.path());
1509+
add_enabled_agent(&mut config, "alpha");
1510+
add_enabled_agent(&mut config, "zeta");
1511+
1512+
let mut items = Vec::new();
1513+
check_workspace(&config, &mut items);
1514+
1515+
// Each enabled agent gets its own SOUL.md + AGENTS.md probe, named.
1516+
let messages: Vec<&str> = items.iter().map(|i| i.message.as_str()).collect();
1517+
for alias in ["alpha", "zeta"] {
1518+
let expected = format!("[{alias}] SOUL.md not found (optional)");
1519+
assert!(
1520+
messages.contains(&expected.as_str()),
1521+
"expected per-agent SOUL.md diagnostic for {alias}; got {messages:?}"
1522+
);
1523+
}
1524+
}
1525+
13941526
#[test]
13951527
fn diagnose_flags_web_dist_dir_with_tilde() {
13961528
// Asserts the localized Fluent message resolves and inlines the path +

0 commit comments

Comments
 (0)