Skip to content

Commit d0e31e1

Browse files
committed
fix(runtime/doctor): ensure workspace doctor checks support multi agents
Signed-off-by: Tomás Migone <tomasmigone@gmail.com>
1 parent 5fc9d3c commit d0e31e1

1 file changed

Lines changed: 142 additions & 12 deletions

File tree

  • crates/zeroclaw-runtime/src/doctor

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

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

0 commit comments

Comments
 (0)