use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
struct Sandbox {
base: PathBuf,
source: PathBuf,
mind_home: PathBuf,
claude_home: PathBuf,
}
struct Run {
stdout: String,
stderr: String,
success: bool,
}
impl Sandbox {
fn new() -> Sandbox {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-lobes-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let source = base.join("agents");
let sb = Sandbox {
base: base.clone(),
source: source.clone(),
mind_home: base.join("mind"),
claude_home: base.join("claude"),
};
write(
&source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\n",
);
write(
&source.join("rules/style.md"),
"---\ndescription: ASCII only\n---\n# style rule\n",
);
git(&source, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&source, &["config", "user.email", "t@t"]);
git(&source, &["config", "user.name", "t"]);
git(&source, &["add", "-A"]);
git(&source, &["commit", "-qm", "initial"]);
std::fs::create_dir_all(&sb.mind_home).unwrap();
sb
}
fn mind(&self, args: &[&str]) -> Run {
self.run(args, &[])
}
fn mind_env(&self, args: &[&str], envs: &[(&str, &str)]) -> Run {
self.run(args, envs)
}
fn run(&self, args: &[&str], envs: &[(&str, &str)]) -> Run {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mind"));
cmd.args(args)
.env("MIND_HOME", &self.mind_home)
.env("CLAUDE_HOME", &self.claude_home)
.env_remove("MIND_AGENT_HOMES")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (k, v) in envs {
cmd.env(k, v);
}
let out = cmd.output().expect("run mind");
Run {
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
success: out.status.success(),
}
}
fn source_spec(&self) -> String {
self.source.to_string_lossy().into_owned()
}
fn write_config(&self, body: &str) {
write(&self.mind_home.join("config.toml"), body);
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.base);
}
}
fn write(path: &Path, contents: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, contents).unwrap();
}
fn git(repo: &Path, args: &[&str]) {
std::fs::create_dir_all(repo).unwrap();
let status = Command::new("git")
.args(args)
.current_dir(repo)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("run git");
assert!(status.success(), "git {args:?} failed");
}
fn parse_json(stdout: &str) -> serde_json::Value {
serde_json::from_str(stdout.trim()).unwrap_or_else(|e| panic!("not JSON: {e}\n{stdout}"))
}
#[test]
fn preset_add_records_path_and_kinds() {
let sb = Sandbox::new();
let added = sb.mind(&["config", "lobes", "add", "--preset", "gemini"]);
assert!(added.success, "preset add failed: {}", added.stderr);
let listed = sb.mind(&["config", "lobes", "list", "--json"]);
let v = parse_json(&listed.stdout);
let lobes = v["lobes"].as_array().expect("lobes array");
let gemini = lobes
.iter()
.find(|l| l["path"].as_str().is_some_and(|p| p.ends_with(".gemini")))
.expect("a .gemini lobe entry");
let kinds: Vec<&str> = gemini["kinds"]
.as_array()
.expect("kinds array")
.iter()
.map(|k| k.as_str().unwrap())
.collect();
assert_eq!(kinds, vec!["skill", "agent"], "gemini admits skill+agent");
let bad = sb.mind(&["config", "lobes", "add", "--preset", "emacs"]);
assert!(!bad.success, "unknown preset must fail");
assert!(bad.stderr.contains("preset"), "{}", bad.stderr);
let both = sb.mind(&["config", "lobes", "add", "/tmp/x", "--preset", "gemini"]);
assert!(!both.success, "path + --preset must conflict");
}
#[test]
fn kinds_filter_excludes_rule_from_skill_only_lobe() {
let sb = Sandbox::new();
let skill_lobe = sb.base.join("gemini-lobe");
sb.write_config(&format!(
"lobes = [\"{claude}\", {{ path = \"{skill}\", kinds = [\"skill\"] }}]\n",
claude = sb.claude_home.display(),
skill = skill_lobe.display(),
));
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(sb.mind(&["learn", "review"]).success, "learn skill");
assert!(sb.mind(&["learn", "style"]).success, "learn rule");
assert!(
std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_ok(),
"skill must link into the Claude lobe"
);
assert!(
std::fs::symlink_metadata(skill_lobe.join("skills/review")).is_ok(),
"skill must link into the skill-only lobe"
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("rules/style")).is_ok()
|| std::fs::symlink_metadata(sb.claude_home.join("rules/style.md")).is_ok(),
"rule must link into the Claude lobe"
);
assert!(
std::fs::symlink_metadata(skill_lobe.join("rules/style.md")).is_err(),
"rule must NOT link into a skill-only lobe (HARN-3)"
);
let manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(sb.mind_home.join("manifest.json")).unwrap())
.unwrap();
let rule_links: Vec<&str> = manifest["items"]["rule:style"]["links"]
.as_array()
.expect("rule links array")
.iter()
.map(|l| l.as_str().unwrap())
.collect();
let skill_lobe_str = skill_lobe.display().to_string();
assert!(
!rule_links.iter().any(|l| l.starts_with(&skill_lobe_str)),
"rule manifest links must omit the skill-only lobe: {rule_links:?}"
);
assert!(
rule_links
.iter()
.any(|l| l.starts_with(&sb.claude_home.display().to_string())),
"rule manifest links must include the Claude lobe: {rule_links:?}"
);
let linked = std::fs::read_to_string(skill_lobe.join("skills/review/SKILL.md")).unwrap();
let original = std::fs::read_to_string(sb.source.join("skills/review/SKILL.md")).unwrap();
assert_eq!(
linked, original,
"mind links skill/agent files verbatim; no frontmatter rewrite (HARN-6)"
);
}
#[test]
fn list_and_show_display_kinds() {
let sb = Sandbox::new();
assert!(
sb.mind(&["config", "lobes", "add", "--preset", "gemini"])
.success
);
let list = sb.mind(&["config", "lobes", "list"]).stdout;
assert!(
list.contains(".gemini") && list.contains("skill") && list.contains("agent"),
"list must show the kinds filter: {list}"
);
let show = sb.mind(&["config", "show"]).stdout;
assert!(
show.contains(".gemini") && show.contains("skill"),
"show must surface the kinds filter: {show}"
);
}
#[test]
fn detect_reports_then_yes_adds() {
let sb = Sandbox::new();
let detect_home = sb.base.join("detect");
std::fs::create_dir_all(detect_home.join(".gemini")).unwrap();
let detect_str = detect_home.to_string_lossy().into_owned();
let report = sb.mind_env(
&["config", "lobes", "detect"],
&[("MIND_DETECT_HOME", &detect_str)],
);
assert!(report.success, "detect failed: {}", report.stderr);
assert!(
report.stdout.contains("gemini"),
"detect must report gemini: {}",
report.stdout
);
let after_report = sb.mind(&["config", "lobes", "list", "--json"]);
let v = parse_json(&after_report.stdout);
assert!(
!v["lobes"]
.as_array()
.unwrap()
.iter()
.any(|l| l["path"].as_str().is_some_and(|p| p.ends_with(".gemini"))),
"report-only detect must NOT add the lobe: {}",
after_report.stdout
);
let json = sb.mind_env(
&["config", "lobes", "detect", "--json"],
&[("MIND_DETECT_HOME", &detect_str)],
);
let jv = parse_json(&json.stdout);
assert_eq!(jv["action"], "lobe-detect", "{}", json.stdout);
assert_eq!(
jv["added"], false,
"json report must not mutate: {}",
json.stdout
);
assert!(
jv["detected"]
.as_array()
.unwrap()
.iter()
.any(|d| d["preset"] == "gemini"),
"detected must list gemini: {}",
json.stdout
);
let added = sb.mind_env(
&["config", "lobes", "detect", "--yes"],
&[("MIND_DETECT_HOME", &detect_str)],
);
assert!(added.success, "detect --yes failed: {}", added.stderr);
let after_add = sb.mind(&["config", "lobes", "list", "--json"]);
let av = parse_json(&after_add.stdout);
let gemini = av["lobes"]
.as_array()
.unwrap()
.iter()
.find(|l| l["path"].as_str().is_some_and(|p| p.ends_with(".gemini")))
.expect("detect --yes must add the gemini lobe");
assert_eq!(
gemini["path"].as_str().unwrap(),
detect_home.join(".gemini").display().to_string(),
"added lobe path must be under the detection base"
);
}
#[test]
fn detect_yes_text_output_reports_and_persists() {
let sb = Sandbox::new();
let detect_home = sb.base.join("detect");
std::fs::create_dir_all(detect_home.join(".gemini")).unwrap();
let detect_str = detect_home.to_string_lossy().into_owned();
let added = sb.mind_env(
&["config", "lobes", "detect", "--yes"],
&[("MIND_DETECT_HOME", &detect_str)],
);
assert!(added.success, "detect --yes failed: {}", added.stderr);
assert!(
added.stdout.contains("gemini") && added.stdout.contains("added"),
"text --yes must report the added gemini lobe: {}",
added.stdout
);
let list = sb.mind(&["config", "lobes", "list"]).stdout;
assert!(
list.contains(".gemini"),
"detect --yes must persist the lobe: {list}"
);
}
#[test]
fn detect_no_homes_reports_nothing_and_mutates_nothing() {
let sb = Sandbox::new();
let detect_home = sb.base.join("empty-detect");
std::fs::create_dir_all(&detect_home).unwrap();
let detect_str = detect_home.to_string_lossy().into_owned();
let text = sb.mind_env(
&["config", "lobes", "detect", "--yes"],
&[("MIND_DETECT_HOME", &detect_str)],
);
assert!(text.success, "detect failed: {}", text.stderr);
assert!(
text.stdout.contains("no new harness homes"),
"empty detection must report none found: {}",
text.stdout
);
let json = sb.mind_env(
&["config", "lobes", "detect", "--json", "--yes"],
&[("MIND_DETECT_HOME", &detect_str)],
);
let jv = parse_json(&json.stdout);
assert_eq!(jv["action"], "lobe-detect");
assert_eq!(
jv["added"], false,
"no candidates => added=false: {}",
json.stdout
);
assert!(
jv["detected"].as_array().unwrap().is_empty(),
"detected must be empty: {}",
json.stdout
);
let list = sb.mind(&["config", "lobes", "list", "--json"]);
let lv = parse_json(&list.stdout);
assert!(
!lv["lobes"].as_array().unwrap().iter().any(|l| l["path"]
.as_str()
.is_some_and(|p| p.ends_with(".gemini") || p.ends_with(".agents"))),
"empty detection must not have added any harness lobe: {}",
list.stdout
);
}
#[test]
fn detect_dedups_codex_and_universal_same_path() {
let sb = Sandbox::new();
let detect_home = sb.base.join("detect-dup");
std::fs::create_dir_all(detect_home.join(".codex")).unwrap();
std::fs::create_dir_all(detect_home.join(".agents")).unwrap();
let detect_str = detect_home.to_string_lossy().into_owned();
let json = sb.mind_env(
&["config", "lobes", "detect", "--json"],
&[("MIND_DETECT_HOME", &detect_str)],
);
let jv = parse_json(&json.stdout);
let agents_path = detect_home.join(".agents").display().to_string();
let agents_entries: Vec<&serde_json::Value> = jv["detected"]
.as_array()
.unwrap()
.iter()
.filter(|d| d["path"].as_str() == Some(agents_path.as_str()))
.collect();
assert_eq!(
agents_entries.len(),
1,
"codex+universal must collapse to ONE ~/.agents candidate: {}",
json.stdout
);
let added = sb.mind_env(
&["config", "lobes", "detect", "--yes"],
&[("MIND_DETECT_HOME", &detect_str)],
);
assert!(added.success, "{}", added.stderr);
let list = sb.mind(&["config", "lobes", "list", "--json"]);
let lv = parse_json(&list.stdout);
let agents_lobes: Vec<&serde_json::Value> = lv["lobes"]
.as_array()
.unwrap()
.iter()
.filter(|l| l["path"].as_str() == Some(agents_path.as_str()))
.collect();
assert_eq!(
agents_lobes.len(),
1,
"only one ~/.agents lobe must be persisted: {}",
list.stdout
);
}
#[test]
fn preset_add_antigravity_antigravity_cli_and_codex() {
let cases = [
("antigravity", ".gemini/config", vec!["skill"]),
("antigravity-cli", ".gemini/antigravity-cli", vec!["skill"]),
("codex", ".agents", vec!["skill"]),
];
for (preset, suffix, want_kinds) in cases {
let sb = Sandbox::new();
let added = sb.mind(&["config", "lobes", "add", "--preset", preset]);
assert!(added.success, "{preset} add failed: {}", added.stderr);
let listed = sb.mind(&["config", "lobes", "list", "--json"]);
let v = parse_json(&listed.stdout);
let entry = v["lobes"]
.as_array()
.expect("lobes array")
.iter()
.find(|l| l["path"].as_str().is_some_and(|p| p.ends_with(suffix)))
.unwrap_or_else(|| panic!("a {suffix} lobe entry for {preset}: {}", listed.stdout));
let kinds: Vec<&str> = entry["kinds"]
.as_array()
.expect("kinds array")
.iter()
.map(|k| k.as_str().unwrap())
.collect();
assert_eq!(kinds, want_kinds, "{preset} kinds");
}
}
#[test]
fn kinds_filtered_lobe_lifecycle_forget_and_introspect() {
let sb = Sandbox::new();
let skill_lobe = sb.base.join("skill-only-lobe");
sb.write_config(&format!(
"lobes = [\"{claude}\", {{ path = \"{skill}\", kinds = [\"skill\"] }}]\n",
claude = sb.claude_home.display(),
skill = skill_lobe.display(),
));
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(sb.mind(&["learn", "review"]).success, "learn skill");
assert!(sb.mind(&["learn", "style"]).success, "learn rule");
let intro = sb.mind(&["introspect", "--json"]);
assert!(intro.success, "introspect failed: {}", intro.stderr);
let iv = parse_json(&intro.stdout);
let issues = iv["issues"].as_array().expect("issues array");
assert!(
issues.is_empty(),
"a kinds-filtered lobe must produce no drift/missing-link issues: {}",
intro.stdout
);
let forget_rule = sb.mind(&["forget", "style", "--yes"]);
assert!(
forget_rule.success,
"forget of a rule in a kinds-filtered setup must succeed: {}",
forget_rule.stderr
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("rules/style.md")).is_err(),
"the recorded Claude rule link must be removed by forget"
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_ok()
&& std::fs::symlink_metadata(skill_lobe.join("skills/review")).is_ok(),
"the skill must remain linked in both lobes after forgetting the rule"
);
let forget_skill = sb.mind(&["forget", "review", "--yes"]);
assert!(forget_skill.success, "{}", forget_skill.stderr);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err()
&& std::fs::symlink_metadata(skill_lobe.join("skills/review")).is_err(),
"forget must remove the skill from every recorded lobe"
);
let intro2 = sb.mind(&["introspect", "--json"]);
let iv2 = parse_json(&intro2.stdout);
assert!(
iv2["issues"].as_array().unwrap().is_empty(),
"introspect must stay clean after forget: {}",
intro2.stdout
);
}
#[test]
fn upgrade_respects_kinds_filter() {
let sb = Sandbox::new();
let skill_lobe = sb.base.join("skill-only-lobe");
sb.write_config(&format!(
"lobes = [\"{claude}\", {{ path = \"{skill}\", kinds = [\"skill\"] }}]\n",
claude = sb.claude_home.display(),
skill = skill_lobe.display(),
));
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(sb.mind(&["learn", "style"]).success, "learn rule");
write(
&sb.source.join("rules/style.md"),
"---\ndescription: ASCII only\n---\n# style rule v2\n",
);
git(&sb.source, &["commit", "-aqm", "bump rule"]);
assert!(sb.mind(&["sync"]).success, "sync failed");
let up = sb.mind(&["upgrade", "--yes"]);
assert!(
up.success,
"upgrade of a kinds-filtered rule must succeed: {}",
up.stderr
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("rules/style.md")).is_ok(),
"rule must remain linked in the Claude lobe after upgrade"
);
assert!(
std::fs::symlink_metadata(skill_lobe.join("rules/style.md")).is_err(),
"rule must NOT be linked into the skill-only lobe after upgrade (HARN-2)"
);
let manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(sb.mind_home.join("manifest.json")).unwrap())
.unwrap();
let rule_links: Vec<&str> = manifest["items"]["rule:style"]["links"]
.as_array()
.expect("rule links")
.iter()
.map(|l| l.as_str().unwrap())
.collect();
let skill_lobe_str = skill_lobe.display().to_string();
assert!(
!rule_links.iter().any(|l| l.starts_with(&skill_lobe_str)),
"rule links must still omit the skill-only lobe after upgrade: {rule_links:?}"
);
}
#[test]
fn lobes_add_without_path_or_preset_errors() {
let sb = Sandbox::new();
let run = sb.mind(&["config", "lobes", "add"]);
assert!(!run.success, "add with no target must fail");
assert!(
run.stderr.contains("path") && run.stderr.contains("--preset"),
"error must mention both a path and --preset: {}",
run.stderr
);
}
#[test]
fn preset_add_preserves_bare_entry_shape() {
let sb = Sandbox::new();
sb.write_config(&format!("lobes = [\"{}\"]\n", sb.claude_home.display()));
let added = sb.mind(&["config", "lobes", "add", "--preset", "gemini"]);
assert!(added.success, "preset add failed: {}", added.stderr);
let raw = std::fs::read_to_string(sb.mind_home.join("config.toml")).unwrap();
let bare = format!("\"{}\"", sb.claude_home.display());
assert!(
raw.contains(&bare),
"the original bare lobe must remain a bare string after rewrite:\n{raw}"
);
assert!(
raw.contains("kinds") && raw.contains(".gemini"),
"the preset lobe must be a table with kinds:\n{raw}"
);
let listed = sb.mind(&["config", "lobes", "list", "--json"]);
let v = parse_json(&listed.stdout);
let lobes = v["lobes"].as_array().unwrap();
let claude_str = sb.claude_home.display().to_string();
assert!(
lobes
.iter()
.any(|l| l.as_str() == Some(claude_str.as_str())),
"a bare lobe must serialize as a plain JSON string (all-kinds): {}",
listed.stdout
);
assert!(
lobes.iter().any(|l| l.is_object()
&& l["path"].as_str().is_some_and(|p| p.ends_with(".gemini"))
&& l["kinds"].is_array()),
"the preset lobe must serialize as an object with a kinds array: {}",
listed.stdout
);
}
#[test]
fn remove_preset_added_detailed_lobe_by_path() {
let sb = Sandbox::new();
assert!(
sb.mind(&["config", "lobes", "add", "--preset", "gemini"])
.success
);
let listed = sb.mind(&["config", "lobes", "list", "--json"]);
let v = parse_json(&listed.stdout);
let gemini_path = v["lobes"]
.as_array()
.unwrap()
.iter()
.find_map(|l| {
l["path"]
.as_str()
.filter(|p| p.ends_with(".gemini"))
.map(str::to_string)
})
.expect("gemini lobe path");
let removed = sb.mind(&["config", "lobes", "remove", &gemini_path]);
assert!(
removed.success,
"removing a detailed preset lobe by path must succeed: {}",
removed.stderr
);
let after = sb.mind(&["config", "lobes", "list", "--json"]);
let av = parse_json(&after.stdout);
assert!(
!av["lobes"]
.as_array()
.unwrap()
.iter()
.any(|l| l["path"].as_str().is_some_and(|p| p.ends_with(".gemini"))),
"the gemini lobe must be gone after remove: {}",
after.stdout
);
}
#[test]
fn tool_with_explicit_link_respects_kinds_filter() {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-tool-lobe-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let source = base.join("agents");
write(&source.join("toolkit/run.sh"), "#!/bin/sh\necho hi\n");
write(
&source.join("mind.toml"),
"[source]\ndescription = \"tool source\"\n\n[[items]]\nkind = \"tool\"\nname = \"toolkit\"\npath = \"toolkit\"\nlink = \"tools/toolkit\"\n",
);
git(&source, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&source, &["config", "user.email", "t@t"]);
git(&source, &["config", "user.name", "t"]);
git(&source, &["add", "-A"]);
git(&source, &["commit", "-qm", "initial"]);
let mind_home = base.join("mind");
let claude_home = base.join("claude");
std::fs::create_dir_all(&mind_home).unwrap();
let skill_lobe = base.join("skill-only-lobe");
let run = |args: &[&str]| -> Run {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mind"));
cmd.args(args)
.env("MIND_HOME", &mind_home)
.env("CLAUDE_HOME", &claude_home)
.env_remove("MIND_AGENT_HOMES")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let out = cmd.output().expect("run mind");
Run {
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
success: out.status.success(),
}
};
write(
&mind_home.join("config.toml"),
&format!(
"lobes = [\"{claude}\", {{ path = \"{skill}\", kinds = [\"skill\"] }}]\n",
claude = claude_home.display(),
skill = skill_lobe.display(),
),
);
assert!(run(&["meld", &source.to_string_lossy()]).success);
let learned = run(&["learn", "toolkit"]);
assert!(learned.success, "learn tool failed: {}", learned.stderr);
assert!(
std::fs::symlink_metadata(claude_home.join("tools/toolkit")).is_ok(),
"a tool with an explicit link must link into a no-kinds lobe (TOOL-4)"
);
assert!(
std::fs::symlink_metadata(skill_lobe.join("tools/toolkit")).is_err(),
"a skill-only lobe must not receive a tool link (HARN-1)"
);
let manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(mind_home.join("manifest.json")).unwrap())
.unwrap();
let links: Vec<&str> = manifest["items"]["tool:toolkit"]["links"]
.as_array()
.expect("tool links")
.iter()
.map(|l| l.as_str().unwrap())
.collect();
let skill_lobe_str = skill_lobe.display().to_string();
assert!(
!links.iter().any(|l| l.starts_with(&skill_lobe_str)),
"tool links must omit the skill-only lobe: {links:?}"
);
assert!(
links
.iter()
.any(|l| l.starts_with(&claude_home.display().to_string())),
"tool links must include the no-kinds Claude lobe: {links:?}"
);
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn detect_short_yes_flag_is_accepted() {
let sb = Sandbox::new();
let detect_home = sb.base.join("detect-empty");
std::fs::create_dir_all(&detect_home).unwrap();
let detect_str = detect_home.to_string_lossy().into_owned();
let run = sb.mind_env(
&["config", "lobes", "detect", "-y"],
&[("MIND_DETECT_HOME", &detect_str)],
);
assert!(
run.success,
"detect -y must succeed (not error on the flag): stderr={}",
run.stderr
);
assert!(
!run.stderr.contains("unexpected argument"),
"detect -y must not produce 'unexpected argument': {}",
run.stderr
);
}
#[test]
fn detect_global_yes_pre_verb_is_accepted() {
let sb = Sandbox::new();
let detect_home = sb.base.join("detect-empty2");
std::fs::create_dir_all(&detect_home).unwrap();
let detect_str = detect_home.to_string_lossy().into_owned();
let run = sb.mind_env(
&["-y", "config", "lobes", "detect"],
&[("MIND_DETECT_HOME", &detect_str)],
);
assert!(
run.success,
"mind -y config lobes detect must succeed: stderr={}",
run.stderr
);
assert!(
!run.stderr.contains("unexpected argument"),
"mind -y detect must not error on the flag: {}",
run.stderr
);
let detect_home2 = sb.base.join("detect-empty3");
std::fs::create_dir_all(&detect_home2).unwrap();
let detect_str2 = detect_home2.to_string_lossy().into_owned();
let run2 = sb.mind_env(
&["--yes", "config", "lobes", "detect"],
&[("MIND_DETECT_HOME", &detect_str2)],
);
assert!(
run2.success,
"mind --yes config lobes detect must succeed: stderr={}",
run2.stderr
);
}