use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU32, Ordering};
use serde_json::Value;
static COUNTER: AtomicU32 = AtomicU32::new(0);
const PROFILES: &str = "[profiles]\n\
default = [\"basic\"]\n\
basic = [\"skill:review\", \"rule:style\"]\n\
full = [\"basic\", \"agent:*\", \"skill:deploy\"]\n";
struct Sandbox {
base: PathBuf,
source: PathBuf,
mind_home: PathBuf,
claude_home: PathBuf,
}
struct Run {
stdout: String,
stderr: String,
success: bool,
}
impl Sandbox {
fn with_toml(name: &str, mind_toml: &str) -> Sandbox {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-profiles-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let source = base.join(name);
let sb = Sandbox {
base: base.clone(),
source: source.clone(),
mind_home: base.join("mind"),
claude_home: base.join("claude"),
};
for skill in ["review", "test", "helper"] {
write_file(
&source.join(format!("skills/{skill}/SKILL.md")),
&format!("---\nname: {skill}\ndescription: {skill} skill\n---\n# {skill}\n"),
);
}
write_file(
&source.join("skills/deploy/SKILL.md"),
"---\nname: deploy\ndescription: deploy skill\n---\n# deploy\nUses {{ns:helper}}.\n",
);
write_file(
&source.join("agents/planner.md"),
"---\nname: planner\ndescription: Planner agent\n---\n# planner\n",
);
write_file(
&source.join("rules/style.md"),
"---\ndescription: Style rule\n---\n# style\n",
);
if !mind_toml.is_empty() {
write_file(&source.join("mind.toml"), mind_toml);
}
git_init(&source);
sb
}
fn new(name: &str) -> Sandbox {
Sandbox::with_toml(name, PROFILES)
}
fn spec(&self) -> String {
self.source.to_string_lossy().into_owned()
}
fn identity(&self) -> String {
format!(
"local/{}/{}",
self.base.file_name().unwrap().to_string_lossy(),
self.source.file_name().unwrap().to_string_lossy(),
)
}
fn mind(&self, args: &[&str]) -> Run {
self.mind_env(args, &[], None)
}
fn mind_env(&self, args: &[&str], envs: &[(&str, &str)], stdin: Option<&str>) -> Run {
use std::io::Write;
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_MAX_METADATA_SIZE")
.env_remove("MIND_TTY")
.env_remove("COLUMNS")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(match stdin {
Some(_) => Stdio::piped(),
None => Stdio::null(),
});
for (k, v) in envs {
cmd.env(k, v);
}
let mut child = cmd.spawn().expect("run mind");
if let Some(text) = stdin {
child
.stdin
.as_mut()
.expect("piped stdin")
.write_all(text.as_bytes())
.expect("write stdin");
}
let out = child.wait_with_output().expect("wait for 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_entry(&self, identity: &str) -> Option<Value> {
let raw = std::fs::read_to_string(self.mind_home.join("sources.json")).ok()?;
let v: Value = serde_json::from_str(&raw).expect("sources.json is JSON");
let list = v
.get("sources")
.and_then(Value::as_array)
.cloned()
.or_else(|| v.as_array().cloned())
.unwrap_or_default();
list.into_iter()
.find(|s| s["name"].as_str() == Some(identity))
}
fn recorded_profiles(&self) -> Option<Vec<String>> {
strings(&self.source_entry(&self.identity())?["profiles"])
}
fn recorded_snapshot(&self) -> Option<Vec<String>> {
strings(&self.source_entry(&self.identity())?["profile_snapshot"])
}
fn manifest_item(&self, key: &str) -> Option<Value> {
let raw = std::fs::read_to_string(self.mind_home.join("manifest.json")).ok()?;
let v: Value = serde_json::from_str(&raw).expect("manifest.json is JSON");
v["items"].get(key).cloned()
}
fn installed(&self) -> Vec<String> {
let Ok(raw) = std::fs::read_to_string(self.mind_home.join("manifest.json")) else {
return Vec::new();
};
let v: Value = serde_json::from_str(&raw).expect("manifest.json is JSON");
let mut keys: Vec<String> = v["items"]
.as_object()
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
keys.sort();
keys
}
fn reasons(&self, key: &str) -> Vec<String> {
self.manifest_item(key)
.and_then(|i| strings(&i["profiles"]))
.unwrap_or_default()
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.base);
}
}
fn strings(v: &Value) -> Option<Vec<String>> {
v.as_array().map(|a| {
a.iter()
.map(|s| s.as_str().unwrap_or_default().to_string())
.collect()
})
}
fn svec(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
fn write_file(path: &Path, contents: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, contents).unwrap();
}
fn git_init(dir: &Path) {
for args in [
vec!["-c", "init.defaultBranch=main", "init", "-q"],
vec!["config", "user.email", "t@t"],
vec!["config", "user.name", "t"],
vec!["add", "-A"],
vec!["commit", "-qm", "initial"],
] {
let status = Command::new("git")
.args(&args)
.current_dir(dir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("run git");
assert!(status.success(), "git {args:?} in {dir:?}");
}
}
fn backticked(text: &str) -> String {
let start = text
.find('`')
.unwrap_or_else(|| panic!("expected a backticked command in: {text}"));
let rest = &text[start + 1..];
let end = rest.find('`').expect("a closing backtick");
rest[..end].to_string()
}
fn shell_split(s: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut cur = String::new();
let mut in_token = false;
let mut quoted = false;
for c in s.chars() {
if quoted {
if c == '\'' {
quoted = false;
} else {
cur.push(c);
}
continue;
}
if c.is_whitespace() {
if in_token {
tokens.push(std::mem::take(&mut cur));
in_token = false;
}
continue;
}
in_token = true;
if c == '\'' {
quoted = true;
} else {
cur.push(c);
}
}
if in_token {
tokens.push(cur);
}
tokens
}
fn run_printed(sb: &Sandbox, command: &str, extra: &[&str]) -> Run {
let mut last = None;
for step in command.split("&&") {
let argv = shell_split(step.trim());
assert_eq!(argv.first().map(String::as_str), Some("mind"), "{command}");
let mut args: Vec<&str> = argv[1..].iter().map(String::as_str).collect();
args.extend_from_slice(extra);
let run = sb.mind(&args);
if !run.success {
return run;
}
last = Some(run);
}
last.expect("at least one step")
}
fn json_doc(run: &Run) -> Value {
serde_json::from_str(run.stdout.trim())
.unwrap_or_else(|e| panic!("stdout is one JSON object ({e}): {}", run.stdout))
}
#[test]
fn source_without_profiles_keeps_install_all() {
let sb = Sandbox::with_toml("plain", "");
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(
sb.installed(),
svec(&[
"agent:planner",
"rule:style",
"skill:deploy",
"skill:helper",
"skill:review",
"skill:test"
])
);
assert_eq!(sb.recorded_profiles(), None);
assert_eq!(sb.recorded_snapshot(), None);
assert!(sb.reasons("skill:review").is_empty());
}
#[test]
fn profiles_without_default_keep_install_all_and_stay_convention() {
let sb = Sandbox::with_toml("nodefault", "[profiles]\nbasic = [\"skill:review\"]\n");
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(sb.installed().len(), 6, "{:?}", sb.installed());
assert_eq!(sb.recorded_profiles(), None);
assert!(sb.reasons("skill:review").is_empty());
}
#[test]
fn plain_meld_offers_the_default_expansion_and_records_it() {
let sb = Sandbox::new("src");
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(sb.installed(), svec(&["rule:style", "skill:review"]));
assert!(r.stdout.contains("enabled default"), "{}", r.stdout);
assert!(
r.stdout.contains("not enabled: basic, full"),
"{}",
r.stdout
);
assert!(r.stdout.contains("--profile"), "{}", r.stdout);
assert!(r.stdout.contains("--all-profiles"), "{}", r.stdout);
assert_eq!(sb.recorded_profiles(), Some(svec(&["default"])));
assert_eq!(
sb.recorded_snapshot(),
Some(svec(&["rule:style", "skill:review"]))
);
assert_eq!(sb.reasons("skill:review"), svec(&["default"]));
let item = sb.manifest_item("skill:review").unwrap();
assert!(item.get("explicit").is_none(), "{item}");
}
#[test]
fn profile_flag_adds_to_default_and_tags_closure_items() {
let sb = Sandbox::new("src");
let r = sb.mind(&["meld", &sb.spec(), "--profile", "full,basic", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(
sb.installed(),
svec(&[
"agent:planner",
"rule:style",
"skill:deploy",
"skill:helper",
"skill:review"
])
);
assert_eq!(
sb.recorded_profiles(),
Some(svec(&["basic", "default", "full"]))
);
assert_eq!(sb.reasons("skill:helper"), svec(&["full"]));
assert_eq!(
sb.reasons("skill:review"),
svec(&["basic", "default", "full"])
);
let snap = sb.recorded_snapshot().unwrap();
assert!(
!snap.contains(&"skill:helper".to_string()),
"a closure item is not a member: {snap:?}"
);
assert!(!sb.installed().contains(&"skill:test".to_string()));
}
#[test]
fn no_default_profile_alone_installs_nothing_and_names_the_remedy() {
let sb = Sandbox::new("src");
let r = sb.mind(&["meld", &sb.spec(), "--no-default-profile", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(sb.installed().is_empty(), "{:?}", sb.installed());
assert!(
sb.source_entry(&sb.identity()).is_some(),
"still registered"
);
assert_eq!(sb.recorded_profiles(), None);
assert!(r.stdout.contains("mind learn"), "{}", r.stdout);
assert!(r.stdout.contains("--profile <name>"), "{}", r.stdout);
}
#[test]
fn no_default_profile_with_profile_enables_only_the_named() {
let sb = Sandbox::new("src");
let r = sb.mind(&[
"meld",
&sb.spec(),
"--no-default-profile",
"--profile",
"full",
"--yes",
]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(sb.recorded_profiles(), Some(svec(&["full"])));
assert_eq!(sb.reasons("skill:review"), svec(&["full"]));
}
#[test]
fn all_profiles_enables_every_declared_profile_but_not_unlisted_items() {
let sb = Sandbox::new("src");
let r = sb.mind(&[
"meld",
&sb.spec(),
"--all-profiles",
"--profile",
"basic",
"--yes",
]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(
sb.recorded_profiles(),
Some(svec(&["basic", "default", "full"]))
);
assert!(
!sb.installed().contains(&"skill:test".to_string()),
"an item in no profile is not installed: {:?}",
sb.installed()
);
}
#[test]
fn malformed_profile_name_is_refused_before_the_clone() {
let sb = Sandbox::new("src");
let r = sb.mind(&["meld", &sb.spec(), "--profile", "Bad", "--yes"]);
assert!(!r.success);
assert!(
r.stderr.contains("invalid profile name 'Bad'"),
"{}",
r.stderr
);
assert!(sb.source_entry(&sb.identity()).is_none());
}
#[test]
fn undeclared_profile_is_profile_not_found_and_source_stays_melded() {
let sb = Sandbox::new("src");
let r = sb.mind(&["meld", &sb.spec(), "--profile", "nope", "--yes"]);
assert!(!r.success);
assert!(
r.stderr.contains("declares no profile 'nope'"),
"{}",
r.stderr
);
assert!(r.stderr.contains("basic, default, full"), "{}", r.stderr);
assert!(sb.source_entry(&sb.identity()).is_some());
assert!(sb.installed().is_empty());
assert_eq!(sb.recorded_profiles(), None);
let plain = Sandbox::with_toml("plain", "");
let r = plain.mind(&["meld", &plain.spec(), "--profile", "full", "--yes"]);
assert!(!r.success);
assert!(r.stderr.contains("declares no profiles"), "{}", r.stderr);
}
#[test]
fn exact_member_naming_no_item_is_bad_reference_at_meld() {
let sb = Sandbox::with_toml(
"src",
"[profiles]\ndefault = [\"skill:missing\", \"command:*\"]\n",
);
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(!r.success, "{}", r.stdout);
assert!(r.stderr.contains("skill:missing"), "{}", r.stderr);
assert!(sb.installed().is_empty());
let ok = Sandbox::with_toml(
"src",
"[profiles]\ndefault = [\"skill:review\", \"command:*\"]\n",
);
let r = ok.mind(&["meld", &ok.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(ok.installed(), svec(&["skill:review"]));
}
#[test]
fn profile_composes_with_learn_and_conflicts_with_register_only() {
let sb = Sandbox::new("src");
let r = sb.mind(&["meld", &sb.spec(), "--learn", "test", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(
sb.installed(),
svec(&["rule:style", "skill:review", "skill:test"])
);
assert!(sb.reasons("skill:test").is_empty());
assert_eq!(
sb.recorded_snapshot(),
Some(svec(&["rule:style", "skill:review"])),
"a --learn match is not a member"
);
for flag in [&["--profile", "full"][..], &["--all-profiles"][..]] {
let other = Sandbox::new("src");
let spec = other.spec();
let mut args = vec!["meld", spec.as_str(), "--register-only"];
args.extend_from_slice(flag);
let r = other.mind(&args);
assert!(!r.success, "{flag:?} must conflict with --register-only");
assert!(r.stderr.contains("cannot be used with"), "{}", r.stderr);
assert!(other.source_entry(&other.identity()).is_none());
}
}
#[test]
fn non_tty_meld_without_yes_records_nothing_and_its_remedy_works() {
let sb = Sandbox::new("src");
let r = sb.mind(&["meld", &sb.spec(), "--profile", "full"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(sb.installed().is_empty());
assert_eq!(sb.recorded_profiles(), None);
let line = r
.stdout
.lines()
.find(|l| l.contains("not a TTY"))
.unwrap_or_else(|| panic!("{}", r.stdout));
let cmd = backticked(line);
assert!(cmd.contains("--profile default,full"), "{cmd}");
let run = run_printed(&sb, &cmd, &["--yes"]);
assert!(run.success, "{} {}", run.stdout, run.stderr);
assert_eq!(sb.recorded_profiles(), Some(svec(&["default", "full"])));
assert!(sb.installed().contains(&"skill:deploy".to_string()));
}
#[test]
fn tty_meld_prompts_once_and_records_only_on_yes() {
let sb = Sandbox::new("src");
let r = sb.mind_env(
&["meld", &sb.spec(), "--profile", "basic"],
&[("MIND_TTY", "1")],
Some("n\n"),
);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(r.stdout.contains("would learn 2 item(s)"), "{}", r.stdout);
assert!(
r.stdout.contains("skipped; run `mind learn"),
"{}",
r.stdout
);
assert!(sb.installed().is_empty());
assert_eq!(sb.recorded_profiles(), None);
let sb = Sandbox::new("src");
let r = sb.mind_env(
&["meld", &sb.spec(), "--profile", "basic"],
&[("MIND_TTY", "1")],
Some("y\n"),
);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(sb.installed(), svec(&["rule:style", "skill:review"]));
assert_eq!(sb.recorded_profiles(), Some(svec(&["basic", "default"])));
}
const NO_DEFAULT_PROFILES: &str = "[profiles]\n\
basic = [\"skill:review\", \"rule:style\"]\n\
full = [\"basic\", \"agent:*\", \"skill:deploy\"]\n";
#[test]
fn interactive_meld_asks_which_profiles_only_without_a_default() {
let sb = Sandbox::with_toml("src", NO_DEFAULT_PROFILES);
let r = sb.mind_env(
&["meld", &sb.spec()],
&[("MIND_TTY", "1")],
Some("full\ny\n"),
);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(r.stdout.contains("enable which profiles?"), "{}", r.stdout);
assert_eq!(sb.recorded_profiles(), Some(svec(&["full"])));
assert!(sb.installed().contains(&"agent:planner".to_string()));
assert!(!sb.installed().contains(&"skill:test".to_string()));
let sb = Sandbox::with_toml("src", NO_DEFAULT_PROFILES);
let r = sb.mind_env(&["meld", &sb.spec()], &[("MIND_TTY", "1")], Some("\ny\n"));
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(r.stdout.contains("enable which profiles?"), "{}", r.stdout);
assert_eq!(sb.recorded_profiles(), None);
assert!(sb.installed().contains(&"skill:test".to_string()));
let sb = Sandbox::with_toml("src", NO_DEFAULT_PROFILES);
let r = sb.mind_env(&["meld", &sb.spec()], &[("MIND_TTY", "1")], Some("nope\n"));
assert!(!r.success, "{} {}", r.stdout, r.stderr);
assert!(sb.installed().is_empty());
let sb = Sandbox::with_toml("src", NO_DEFAULT_PROFILES);
let r = sb.mind_env(
&["meld", &sb.spec()],
&[("MIND_TTY", "1")],
Some("Bad Name\n"),
);
assert!(!r.success, "{} {}", r.stdout, r.stderr);
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(sb.installed().contains(&"skill:test".to_string()));
let sb = Sandbox::new("src");
let r = sb.mind_env(&["meld", &sb.spec()], &[("MIND_TTY", "1")], Some("y\n"));
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(!r.stdout.contains("enable which profiles?"), "{}", r.stdout);
assert_eq!(sb.recorded_profiles(), Some(svec(&["default"])));
let sb = Sandbox::with_toml("src", NO_DEFAULT_PROFILES);
let r = sb.mind_env(
&["meld", &sb.spec(), "--profile", "basic"],
&[("MIND_TTY", "1")],
Some("y\n"),
);
assert!(!r.stdout.contains("enable which profiles?"), "{}", r.stdout);
let sb = Sandbox::with_toml("src", NO_DEFAULT_PROFILES);
let r = sb.mind_env(
&["meld", &sb.spec(), "--learn", "skill:test"],
&[("MIND_TTY", "1")],
Some("y\n"),
);
assert!(!r.stdout.contains("enable which profiles?"), "{}", r.stdout);
}
#[test]
fn remeld_adds_to_the_recorded_set_and_never_disables() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
assert_eq!(sb.recorded_profiles(), Some(svec(&["default"])));
let r = sb.mind(&["meld", &sb.spec(), "--profile", "full", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(sb.recorded_profiles(), Some(svec(&["default", "full"])));
assert!(sb.installed().contains(&"skill:deploy".to_string()));
let r = sb.mind(&["meld", &sb.spec(), "--no-default-profile", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(
sb.recorded_profiles(),
Some(svec(&["default", "full"])),
"a re-meld never disables a profile"
);
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(!sb.installed().contains(&"skill:test".to_string()));
}
#[test]
fn learn_source_profile_uses_learns_gate_and_records() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--register-only"]).success);
let r = sb.mind(&["learn", "src", "--profile", "full", "--dry-run"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(r.stdout.contains("would learn 5 item(s)"), "{}", r.stdout);
assert!(sb.installed().is_empty());
assert_eq!(sb.recorded_profiles(), None);
let r = sb.mind(&["learn", "src", "--profile", "full"]);
assert!(!r.success, "{} {}", r.stdout, r.stderr);
assert!(r.stderr.contains("--yes"), "{}", r.stderr);
assert!(sb.installed().is_empty());
assert_eq!(sb.recorded_profiles(), None);
let r = sb.mind(&["learn", "src", "--profile", "full", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(sb.recorded_profiles(), Some(svec(&["full"])));
assert_eq!(sb.installed().len(), 5, "{:?}", sb.installed());
let r = sb.mind(&["learn", "src", "--all-profiles", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(
sb.recorded_profiles(),
Some(svec(&["basic", "default", "full"]))
);
assert!(!sb.installed().contains(&"skill:test".to_string()));
}
#[test]
fn learn_source_profile_without_deps_installs_directly() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--register-only"]).success);
let r = sb.mind(&["learn", &sb.identity(), "--profile", "basic"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(sb.installed(), svec(&["rule:style", "skill:review"]));
assert_eq!(sb.recorded_profiles(), Some(svec(&["basic"])));
let r = sb.mind(&["learn", "src", "--profile", "nope"]);
assert!(!r.success);
assert!(
r.stderr.contains("declares no profile 'nope'"),
"{}",
r.stderr
);
}
#[test]
fn json_meld_reports_profiles_and_union_pending() {
let sb = Sandbox::new("src");
let r = sb.mind(&["--json", "meld", &sb.spec(), "--profile", "full"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let v = json_doc(&r);
assert_eq!(v["pending_items"], 5, "{v}");
assert_eq!(strings(&v["profiles"]), Some(svec(&["default", "full"])));
assert!(sb.installed().is_empty());
assert_eq!(sb.recorded_profiles(), None);
let sb = Sandbox::new("src");
let r = sb.mind(&["--json", "meld", &sb.spec(), "--profile", "full", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let v = json_doc(&r);
assert_eq!(v["installed"].as_array().map(Vec::len), Some(5), "{v}");
assert_eq!(strings(&v["profiles"]), Some(svec(&["default", "full"])));
assert_eq!(sb.recorded_profiles(), Some(svec(&["default", "full"])));
}
#[test]
fn json_learn_reports_profiles() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--register-only"]).success);
let r = sb.mind(&["--json", "learn", "src", "--profile", "basic"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let v = json_doc(&r);
assert_eq!(v["action"], "learn");
assert_eq!(v["outcome"], "installed");
assert_eq!(strings(&v["profiles"]), Some(svec(&["basic"])));
assert_eq!(v["installed"].as_array().map(Vec::len), Some(2), "{v}");
}
#[test]
fn profile_flags_on_an_item_link_are_a_usage_error() {
let sb = Sandbox::new("src");
let link = format!("file://{}/tree/main/skills/review", sb.spec());
let r = sb.mind(&["meld", &link, "--profile", "basic"]);
assert!(!r.success);
assert!(
r.stderr
.contains("--profile cannot be used with an item link"),
"{}",
r.stderr
);
let r = sb.mind(&["learn", &link, "--all-profiles"]);
assert!(!r.success);
assert!(
r.stderr
.contains("--all-profiles cannot be used with an item link"),
"{}",
r.stderr
);
let r = sb.mind(&["meld", &link, "--no-default-profile"]);
assert!(!r.success);
assert!(sb.installed().is_empty());
}
#[test]
fn by_name_learn_marks_a_profile_item_explicit() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
assert!(
sb.manifest_item("skill:review")
.unwrap()
.get("explicit")
.is_none()
);
let r = sb.mind(&["learn", "review"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let item = sb.manifest_item("skill:review").unwrap();
assert_eq!(item["explicit"], true, "{item}");
assert_eq!(sb.reasons("skill:review"), svec(&["default"]));
}
#[test]
fn enabling_a_profile_over_a_hand_installed_item_keeps_it_explicit() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--register-only"]).success);
assert!(sb.mind(&["learn", "review"]).success);
let r = sb.mind(&["learn", "src", "--profile", "basic", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let item = sb.manifest_item("skill:review").unwrap();
assert_eq!(item["explicit"], true, "{item}");
assert_eq!(sb.reasons("skill:review"), svec(&["basic"]));
assert!(item.get("explicit").is_some());
let style = sb.manifest_item("rule:style").unwrap();
assert!(style.get("explicit").is_none(), "{style}");
}
fn curator(nested: &Sandbox, entry: &str) -> Sandbox {
let toml = format!(
"[discover]\nsources = [{{ source = {:?}{entry} }}]\n",
nested.spec()
);
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-profiles-cur-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let source = base.join("registry");
write_file(&source.join("README.md"), "# registry\n");
write_file(&source.join("mind.toml"), &toml);
git_init(&source);
Sandbox {
base: base.clone(),
source,
mind_home: base.join("mind"),
claude_home: base.join("claude"),
}
}
impl Sandbox {
fn nested_profiles(&self, nested: &Sandbox) -> Option<Vec<String>> {
strings(&self.source_entry(&nested.identity())?["profiles"])
}
}
#[test]
fn curator_entry_profiles_apply_like_the_flags() {
let nested = Sandbox::new("nested");
let reg = curator(&nested, ", profiles = [\"full\"]");
let r = reg.mind(&["meld", ®.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(
reg.nested_profiles(&nested),
Some(svec(&["default", "full"]))
);
assert!(reg.installed().contains(&"skill:deploy".to_string()));
}
#[test]
fn curator_default_profile_false_acts_as_no_default_profile() {
let nested = Sandbox::new("nested");
let reg = curator(&nested, ", profiles = [\"full\"], default-profile = false");
let r = reg.mind(&["meld", ®.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(reg.nested_profiles(&nested), Some(svec(&["full"])));
}
#[test]
fn curator_profiles_with_install_items_install_the_union() {
let nested = Sandbox::new("nested");
let reg = curator(
&nested,
", profiles = [\"basic\"], install-items = [\"skill:test\"]",
);
let r = reg.mind(&["meld", ®.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(
reg.installed(),
svec(&["rule:style", "skill:review", "skill:test"])
);
}
#[test]
fn curator_install_true_offers_the_nested_default() {
let nested = Sandbox::new("nested");
let reg = curator(&nested, ", install = true");
let r = reg.mind(&["meld", ®.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(reg.installed(), svec(&["rule:style", "skill:review"]));
assert_eq!(reg.nested_profiles(&nested), Some(svec(&["default"])));
let plain = Sandbox::with_toml("plain", "");
let reg = curator(&plain, ", install = true");
let r = reg.mind(&["meld", ®.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(reg.installed().len(), 6, "{:?}", reg.installed());
}
#[test]
fn curator_undeclared_profile_is_bad_reference_at_meld() {
let nested = Sandbox::new("nested");
let reg = curator(&nested, ", profiles = [\"nope\"]");
let r = reg.mind(&["meld", ®.spec(), "--yes"]);
assert!(!r.success, "{}", r.stdout);
assert!(r.stderr.contains("profile 'nope'"), "{}", r.stderr);
assert!(reg.installed().is_empty());
}
fn add_skill(sb: &Sandbox, name: &str, body: &str) {
write_file(
&sb.source.join(format!("skills/{name}/SKILL.md")),
&format!("---\nname: {name}\ndescription: {name} skill\n---\n{body}\n"),
);
for args in [vec!["add", "-A"], vec!["commit", "-qm", "more"]] {
let status = Command::new("git")
.args(&args)
.current_dir(&sb.source)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("run git");
assert!(status.success(), "git {args:?}");
}
}
#[test]
fn closure_item_shared_by_two_members_carries_the_union_of_reasons() {
let sb = Sandbox::with_toml(
"src",
"[profiles]\np1 = [\"skill:deploy\"]\np2 = [\"skill:deploy2\"]\n",
);
add_skill(&sb, "deploy2", "# deploy2\nUses {{ns:helper}}.");
let r = sb.mind(&["meld", &sb.spec(), "--profile", "p1,p2", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(
sb.installed(),
svec(&["skill:deploy", "skill:deploy2", "skill:helper"])
);
assert_eq!(sb.reasons("skill:helper"), svec(&["p1", "p2"]));
assert_eq!(sb.reasons("skill:deploy"), svec(&["p1"]));
assert_eq!(sb.reasons("skill:deploy2"), svec(&["p2"]));
assert_eq!(
sb.recorded_snapshot(),
Some(svec(&["skill:deploy", "skill:deploy2"]))
);
}
#[test]
fn closure_pending_items_count_each_item_once_in_json() {
let sb = Sandbox::with_toml(
"src",
"[profiles]\np1 = [\"skill:deploy\"]\np2 = [\"skill:deploy2\"]\n",
);
add_skill(&sb, "deploy2", "# deploy2\nUses {{ns:helper}}.");
let r = sb.mind(&["--json", "meld", &sb.spec(), "--profile", "p1,p2"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(json_doc(&r)["pending_items"], 3, "{}", r.stdout);
assert!(sb.installed().is_empty());
}
#[test]
fn policy_skipped_member_stays_out_of_the_snapshot() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--register-only"]).success);
let policy_path = sb.base.join("policy.toml");
write_file(
&policy_path,
"[sources]\nlock = true\nallow = [\"example.invalid/*\"]\n",
);
let policy = policy_path.to_string_lossy().into_owned();
let r = sb.mind_env(
&["learn", "src", "--profile", "basic", "--yes"],
&[("MIND_POLICY_FILE", policy.as_str())],
None,
);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("not permitted by the managed policy"),
"{} {}",
r.stdout,
r.stderr
);
assert!(sb.installed().is_empty(), "{} {}", r.stdout, r.stderr);
let snap = sb.recorded_snapshot().unwrap_or_default();
assert!(snap.is_empty(), "skipped members snapshotted: {snap:?}");
}
#[test]
fn partial_install_failure_still_records_what_installed() {
let sb = Sandbox::with_toml(
"src",
"[profiles]\ndefault = [\"skill:review\", \"skill:zbroken\"]\n",
);
add_skill(&sb, "zbroken", "# zbroken\nUses {{ns:nonexistent}}.");
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(!r.success, "{} {}", r.stdout, r.stderr);
assert!(!sb.installed().contains(&"skill:zbroken".to_string()));
assert_eq!(sb.installed(), svec(&["skill:review"]), "{}", r.stderr);
assert_eq!(sb.reasons("skill:review"), svec(&["default"]));
assert_eq!(sb.recorded_snapshot(), Some(svec(&["skill:review"])));
assert_eq!(sb.recorded_profiles(), Some(svec(&["default"])));
assert!(sb.source_entry(&sb.identity()).is_some());
}
#[test]
fn remeld_json_with_profile_flags_installs_and_reports() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let r = sb.mind(&["--json", "meld", &sb.spec(), "--profile", "full"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let v = json_doc(&r);
assert_eq!(v["outcome"], "already-melded", "{v}");
assert_eq!(strings(&v["profiles"]), Some(svec(&["default", "full"])));
assert_eq!(v["pending_items"], 3, "{v}");
assert_eq!(sb.recorded_profiles(), Some(svec(&["default"])));
let r = sb.mind(&["--json", "meld", &sb.spec(), "--profile", "full", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let v = json_doc(&r);
assert_eq!(strings(&v["profiles"]), Some(svec(&["default", "full"])));
assert_eq!(v["installed"].as_array().map(Vec::len), Some(3), "{v}");
assert_eq!(sb.recorded_profiles(), Some(svec(&["default", "full"])));
assert!(sb.installed().contains(&"skill:helper".to_string()));
}
#[test]
fn curator_json_install_profiles_apply_like_the_flags() {
let nested = Sandbox::new("nested");
let reg = curator(&nested, ", profiles = [\"full\"], default-profile = false");
let r = reg.mind(&["--json", "meld", ®.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(reg.nested_profiles(&nested), Some(svec(&["full"])));
assert!(reg.installed().contains(&"skill:deploy".to_string()));
assert!(reg.installed().contains(&"agent:planner".to_string()));
let nested = Sandbox::new("nested");
let reg = curator(&nested, ", install = true");
let r = reg.mind(&["--json", "meld", ®.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(reg.installed(), svec(&["rule:style", "skill:review"]));
assert_eq!(reg.nested_profiles(&nested), Some(svec(&["default"])));
let nested = Sandbox::new("nested");
let reg = curator(&nested, ", profiles = [\"nope\"]");
let r = reg.mind(&["--json", "meld", ®.spec(), "--yes"]);
assert!(!r.success, "{}", r.stdout);
assert!(reg.installed().is_empty());
}
#[test]
fn enabled_profile_matching_nothing_is_recorded_and_installs_nothing() {
let sb = Sandbox::with_toml("src", "[profiles]\ndefault = [\"command:*\"]\n");
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(sb.installed().is_empty(), "{:?}", sb.installed());
assert_eq!(sb.recorded_profiles(), Some(svec(&["default"])));
assert_eq!(
sb.recorded_snapshot().unwrap_or_default(),
Vec::<String>::new()
);
let sb = Sandbox::with_toml("src", "[profiles]\ndefault = [\"command:*\"]\n");
let r = sb.mind(&["--json", "meld", &sb.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(strings(&json_doc(&r)["profiles"]), Some(svec(&["default"])));
assert!(sb.installed().is_empty());
}
#[test]
fn learn_source_profile_with_unknown_ambiguous_or_hash_selector_fails_cleanly() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--register-only"]).success);
let r = sb.mind(&["learn", "nosuch", "--profile", "basic", "--yes"]);
assert!(!r.success, "{}", r.stdout);
assert!(r.stderr.contains("nosuch"), "{}", r.stderr);
let r = sb.mind(&["learn", "src#review", "--profile", "basic", "--yes"]);
assert!(!r.success, "{}", r.stdout);
assert!(sb.installed().is_empty());
assert_eq!(sb.recorded_profiles(), None);
let other = Sandbox::new("src");
assert!(sb.mind(&["meld", &other.spec(), "--register-only"]).success);
let r = sb.mind(&["learn", "src", "--profile", "basic", "--yes"]);
assert!(!r.success, "{}", r.stdout);
assert!(sb.installed().is_empty());
assert_eq!(sb.recorded_profiles(), None);
let r = sb.mind(&["learn", &sb.identity(), "--profile", "basic", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(sb.recorded_profiles(), Some(svec(&["basic"])));
}
#[test]
fn learn_json_with_closure_needs_confirmation_and_installs_nothing() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--register-only"]).success);
let r = sb.mind(&["--json", "learn", "src", "--profile", "full"]);
assert!(!r.success, "{} {}", r.stdout, r.stderr);
assert!(
r.stderr.contains("confirmation") || r.stdout.contains("confirmation"),
"{} {}",
r.stdout,
r.stderr
);
assert!(sb.installed().is_empty());
assert_eq!(sb.recorded_profiles(), None);
let r = sb.mind(&["--json", "learn", "src", "--profile", "full", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(json_doc(&r)["outcome"], "installed");
assert_eq!(sb.installed().len(), 5);
}
#[test]
fn no_default_profile_composes_with_register_only() {
let sb = Sandbox::new("src");
let r = sb.mind(&[
"meld",
&sb.spec(),
"--register-only",
"--no-default-profile",
]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(sb.installed().is_empty());
assert!(sb.source_entry(&sb.identity()).is_some());
assert_eq!(sb.recorded_profiles(), None);
}
#[test]
fn help_text_mentions_the_profile_flags() {
let sb = Sandbox::new("src");
let r = sb.mind(&["meld", "--help"]);
assert!(r.success);
for flag in ["--profile", "--no-default-profile", "--all-profiles"] {
assert!(r.stdout.contains(flag), "meld --help lacks {flag}");
}
let r = sb.mind(&["learn", "--help"]);
assert!(r.success);
for flag in ["--profile", "--all-profiles"] {
assert!(r.stdout.contains(flag), "learn --help lacks {flag}");
}
}
#[test]
fn learn_all_conflicts_with_profile_flags() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--register-only"]).success);
for flag in ["--profile=basic", "--all-profiles"] {
let r = sb.mind(&["learn", "src", "--all", flag, "--yes"]);
assert!(!r.success, "{flag}");
assert!(r.stderr.contains("cannot be used with"), "{}", r.stderr);
}
assert!(sb.installed().is_empty());
}
fn hooked(name: &str) -> Sandbox {
hooked_with(name, PROFILES)
}
fn hooked_with(name: &str, profiles: &str) -> Sandbox {
let toml = format!(
"{profiles}\n[[hooks]]\nrun = \"sh ./log.sh source install\"\nevent = \"install\"\n\n\
[[hooks]]\nrun = \"sh ./log.sh source uninstall\"\nevent = \"uninstall\"\n"
);
let sb = Sandbox::with_toml(name, &toml);
let log = "printf '%s %s %s|%s\\n' \"$1\" \"$2\" \"$MIND_PROFILES\" \"$MIND_ITEM_PROFILES\" \
>> \"$HOOK_LOG\"\n";
write_file(&sb.source.join("log.sh"), log);
for skill in ["review", "deploy", "helper"] {
let body = if skill == "deploy" {
"Uses {{ns:helper}}."
} else {
""
};
write_file(
&sb.source.join(format!("skills/{skill}/SKILL.md")),
&format!(
"---\nname: {skill}\ndescription: {skill} skill\n\
install: sh log.sh {skill} install\nuninstall: sh log.sh {skill} uninstall\n\
---\n# {skill}\n{body}\n"
),
);
write_file(&sb.source.join(format!("skills/{skill}/log.sh")), log);
}
add_skill(&sb, "test", "# test");
sb
}
impl Sandbox {
fn hook_log(&self) -> std::path::PathBuf {
self.base.join("hook.log")
}
fn mind_hooked(&self, args: &[&str], envs: &[(&str, &str)], stdin: Option<&str>) -> Run {
let log = self.hook_log();
let mut all: Vec<(&str, &str)> = vec![("HOOK_LOG", log.to_str().unwrap())];
all.extend_from_slice(envs);
let mut argv: Vec<&str> = args.to_vec();
argv.push("--accept-hooks=install,update,uninstall");
self.mind_env(&argv, &all, stdin)
}
fn take_hook_log(&self) -> Vec<String> {
let text = std::fs::read_to_string(self.hook_log()).unwrap_or_default();
let _ = std::fs::remove_file(self.hook_log());
text.lines().map(str::to_string).collect()
}
}
#[test]
fn meld_hooks_see_the_enabled_profiles_and_item_reasons() {
let sb = hooked("src");
let r = sb.mind_hooked(
&["meld", &sb.spec(), "--profile", "full", "--yes"],
&[],
None,
);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let log = sb.take_hook_log();
assert_eq!(
log.first().map(String::as_str),
Some("source install default,full|")
);
assert!(
log.contains(&"deploy install default,full|full".to_string()),
"{log:?}"
);
assert!(
log.contains(&"helper install default,full|full".to_string()),
"{log:?}"
);
let review = sb.reasons("skill:review").join(",");
assert!(!review.is_empty());
assert!(
log.contains(&format!("review install default,full|{review}")),
"{log:?}"
);
}
#[test]
fn hook_vars_are_always_set_and_override_inherited_values() {
let sb = hooked("src");
let r = sb.mind_hooked(
&["meld", &sb.spec(), "--register-only"],
&[("MIND_PROFILES", "leak"), ("MIND_ITEM_PROFILES", "leak")],
None,
);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(sb.take_hook_log(), svec(&["source install |"]));
}
#[test]
fn source_hooks_see_the_picker_answer() {
let sb = hooked_with("src", NO_DEFAULT_PROFILES);
let r = sb.mind_hooked(
&["meld", &sb.spec()],
&[("MIND_TTY", "1")],
Some("full\ny\n"),
);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let log = sb.take_hook_log();
assert_eq!(
log.first().map(String::as_str),
Some("source install full|"),
"{log:?}"
);
assert_eq!(sb.recorded_profiles(), Some(svec(&["full"])));
}
#[test]
fn nested_source_hooks_see_the_curator_entry_selection() {
let nested = hooked("nested");
let reg = curator(&nested, ", profiles = [\"full\"], default-profile = false");
let log = reg.hook_log();
let r = reg.mind_env(
&[
"meld",
®.spec(),
"--yes",
"--accept-hooks=install,update,uninstall",
],
&[("HOOK_LOG", log.to_str().unwrap())],
None,
);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let log = reg.take_hook_log();
assert_eq!(
log.first().map(String::as_str),
Some("source install full|"),
"{log:?}"
);
assert!(
log.contains(&"deploy install full|full".to_string()),
"{log:?}"
);
}
#[test]
fn enabling_and_disabling_a_profile_reports_the_resulting_set() {
let sb = hooked("src");
assert!(
sb.mind_hooked(&["meld", &sb.spec(), "--yes"], &[], None)
.success
);
sb.take_hook_log();
let r = sb.mind_hooked(&["learn", "src", "--profile", "full", "--yes"], &[], None);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let log = sb.take_hook_log();
assert!(
log.contains(&"deploy install default,full|full".to_string()),
"{log:?}"
);
assert!(!log.iter().any(|l| l.starts_with("source ")), "{log:?}");
let r = sb.mind_hooked(&["forget", "src", "--profile", "full", "--yes"], &[], None);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let log = sb.take_hook_log();
assert!(
log.contains(&"deploy uninstall default|full".to_string()),
"{log:?}"
);
assert!(
log.contains(&"helper uninstall default|full".to_string()),
"{log:?}"
);
assert!(!log.iter().any(|l| l.starts_with("source ")), "{log:?}");
}
#[test]
fn unmeld_and_hooks_run_see_the_recorded_state() {
let sb = hooked("src");
assert!(
sb.mind_hooked(
&["meld", &sb.spec(), "--profile", "full", "--yes"],
&[],
None
)
.success
);
sb.take_hook_log();
let r = sb.mind_hooked(
&["hooks", "run", "src#skill:deploy", "--event", "uninstall"],
&[],
None,
);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(
sb.take_hook_log(),
svec(&["deploy uninstall default,full|full"])
);
let r = sb.mind_hooked(&["unmeld", "src", "--yes"], &[], None);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let log = sb.take_hook_log();
assert!(
log.contains(&"source uninstall default,full|".to_string()),
"{log:?}"
);
assert!(
log.contains(&"helper uninstall default,full|full".to_string()),
"{log:?}"
);
}
fn profile_block<'a>(stdout: &'a str, profile: &str) -> Vec<&'a str> {
let mut lines = stdout
.lines()
.skip_while(|l| l.starts_with(" ") || l.split_whitespace().nth(1) != Some(profile));
let heading = lines
.next()
.unwrap_or_else(|| panic!("no block for {profile} in: {stdout}"));
std::iter::once(heading)
.chain(lines.take_while(|l| l.starts_with(" ")))
.collect()
}
fn block_field(stdout: &str, profile: &str, label: &str) -> Option<String> {
let marker = format!("{label}:");
let block = profile_block(stdout, profile);
let start = block
.iter()
.position(|l| l.trim_start().starts_with(&marker))?;
let mut words = vec![block[start].trim_start()[marker.len()..].trim()];
words.extend(
block[start + 1..]
.iter()
.take_while(|l| l.starts_with(" "))
.map(|l| l.trim()),
);
Some(words.join(" "))
}
fn enable_command(stdout: &str, profile: &str) -> String {
block_field(stdout, profile, "enable")
.unwrap_or_else(|| panic!("no enable line for {profile} in: {stdout}"))
}
#[test]
fn recall_profiles_lists_members_enabled_state_and_counts() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let r = sb.mind(&["recall", "--profiles"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let out = &r.stdout;
assert!(
out.contains(&format!("profiles of {}:", sb.identity())),
"{out}"
);
let row = |name: &str| profile_block(out, name)[0].to_string();
assert!(row("default").contains("(enabled)"), "{out}");
assert!(row("default").contains("2/2 installed"), "{out}");
assert_eq!(
block_field(out, "basic", "members").as_deref(),
Some("skill:review, rule:style"),
"{out}"
);
assert!(!row("basic").contains("(enabled)"), "{out}");
assert!(row("full").contains("2/4 installed"), "{out}");
assert_eq!(block_field(out, "default", "enable"), None, "{out}");
let cmd = enable_command(out, "full");
let r = run_printed(&sb, &cmd, &["--yes"]);
assert!(r.success, "{cmd}: {} {}", r.stdout, r.stderr);
assert_eq!(sb.recorded_profiles(), Some(svec(&["default", "full"])));
assert!(sb.manifest_item("agent:planner").is_some());
let r = sb.mind(&["recall", "--profiles", "src"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(r.stdout.contains("full (enabled)"), "{}", r.stdout);
assert!(r.stdout.contains("4/4 installed"), "{}", r.stdout);
}
#[test]
fn profile_listing_wraps_members_and_aligns_counts() {
let members = [
"skill:review",
"skill:test",
"skill:helper",
"skill:deploy",
"agent:planner",
"rule:style",
"skill:*",
"agent:*",
];
let quoted: Vec<String> = members.iter().map(|m| format!("\"{m}\"")).collect();
let toml = format!(
"[profiles]\nwide = [{}]\nlong-profile-name = [\"rule:style\"]\n",
quoted.join(", ")
);
let sb = Sandbox::with_toml("src", &toml);
assert!(sb.mind(&["meld", &sb.spec(), "--register-only"]).success);
let r = sb.mind_env(&["recall", "--profiles"], &[("COLUMNS", "40")], None);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let out = &r.stdout;
let wide = profile_block(out, "wide");
assert!(
wide.iter()
.filter(|l| l.contains("members:") || l.starts_with(" "))
.count()
> 1,
"members should wrap at 40 columns: {out}"
);
for line in out.lines() {
if line.starts_with("profiles of") || line.trim_start().starts_with("enable:") {
continue;
}
assert!(
line.chars().count() <= 40,
"line over 40 columns: {line:?}\n{out}"
);
}
assert_eq!(
block_field(out, "wide", "members"),
Some(members.join(", ")),
"{out}"
);
let at = |name: &str| profile_block(out, name)[0].find(" installed");
assert!(at("wide").is_some(), "{out}");
assert_eq!(at("wide"), at("long-profile-name"), "{out}");
let cmd = enable_command(out, "wide");
assert_eq!(
shell_split(&cmd),
svec(&["mind", "learn", &sb.identity(), "--profile", "wide"])
);
}
#[test]
fn recall_profiles_without_profiled_sources_says_so() {
let sb = Sandbox::with_toml("plain", "");
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let r = sb.mind(&["recall", "--profiles"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("no melded source declares profiles"),
"{}",
r.stdout
);
let r = sb.mind(&["recall", "--profiles", "plain"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
r.stdout
.contains(&format!("{} declares no profiles", sb.identity())),
"{}",
r.stdout
);
let r = sb.mind(&["recall", "--profiles", "nope"]);
assert!(!r.success, "{}", r.stdout);
let r = sb.mind(&["recall", "--profiles", "--sources"]);
assert!(
!r.success,
"--profiles conflicts with --sources: {}",
r.stdout
);
}
#[test]
fn recall_profiles_marks_a_profile_whose_expansion_fails() {
let sb = Sandbox::with_toml(
"src",
"[profiles]\nbasic = [\"skill:review\"]\nbroken = [\"skill:missing\"]\n",
);
assert!(sb.mind(&["meld", &sb.spec(), "--register-only"]).success);
let r = sb.mind(&["recall", "--profiles"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(r.stdout.contains("0/1 installed"), "{}", r.stdout);
let error = block_field(&r.stdout, "broken", "error");
assert!(
error
.as_deref()
.is_some_and(|e| e.contains("skill:missing")),
"{}",
r.stdout
);
let doc = json_doc(&sb.mind(&["recall", "--profiles", "--json"]));
let profiles = doc["items"][0]["profiles"].as_array().unwrap();
let broken = profiles.iter().find(|p| p["name"] == "broken").unwrap();
assert!(broken["error"].is_string(), "{broken}");
assert!(broken.get("total").is_none(), "{broken}");
}
#[test]
fn recall_profiles_json_carries_items_and_counts() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let doc = json_doc(&sb.mind(&["recall", "--profiles", "--json"]));
let src = &doc["items"][0];
assert_eq!(src["source"], sb.identity());
let profiles = src["profiles"].as_array().unwrap();
let names: Vec<&str> = profiles
.iter()
.map(|p| p["name"].as_str().unwrap())
.collect();
assert_eq!(names, vec!["basic", "default", "full"]);
let full = &profiles[2];
assert_eq!(full["enabled"], false);
assert_eq!(
full["members"],
serde_json::json!(["basic", "agent:*", "skill:deploy"])
);
assert_eq!(
full["items"],
serde_json::json!([
"agent:planner",
"rule:style",
"skill:deploy",
"skill:review"
])
);
assert_eq!(full["installed"], 2);
assert_eq!(full["total"], 4);
assert_eq!(profiles[1]["enabled"], true);
}
#[test]
fn learn_with_a_bare_profile_flag_lists_the_choices() {
let sb = Sandbox::new("src");
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let before = sb.installed();
let r = sb.mind(&["learn", "src", "--profile"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
r.stdout
.contains(&format!("profiles of {}:", sb.identity())),
"{}",
r.stdout
);
assert!(
enable_command(&r.stdout, "full").starts_with("mind learn"),
"{}",
r.stdout
);
assert_eq!(sb.installed(), before);
assert_eq!(sb.recorded_profiles(), Some(svec(&["default"])));
let doc = json_doc(&sb.mind(&["learn", "src", "--profile", "--json"]));
assert_eq!(doc["items"][0]["source"], sb.identity());
assert_eq!(sb.installed(), before);
let r = sb.mind(&["learn", "src", "--profile", "--profile", "full", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(sb.recorded_profiles(), Some(svec(&["default", "full"])));
}
#[test]
fn profile_header_names_the_recall_and_learn_commands() {
let sb = Sandbox::new("src");
let r = sb.mind(&["meld", &sb.spec(), "--no-default-profile"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let line = r
.stdout
.lines()
.find(|l| l.contains("not enabled:"))
.unwrap_or_else(|| panic!("{}", r.stdout));
assert!(line.contains("not enabled: basic, default, full"), "{line}");
let recall = backticked(line);
assert_eq!(
shell_split(&recall),
svec(&["mind", "recall", "--profiles", &sb.identity()])
);
let r = run_printed(&sb, &recall, &[]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(r.stdout.contains("full"), "{}", r.stdout);
let after_recall = &line[line.find(&recall).unwrap() + recall.len() + 1..];
assert_eq!(
shell_split(&backticked(after_recall)),
svec(&["mind", "learn", &sb.identity(), "--profile", "<name>"])
);
}
#[test]
fn profile_install_prints_progress_before_resolving() {
let sb = Sandbox::new("src");
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains(&format!(
"resolving profile(s) default of {} ...",
sb.identity()
)),
"{}",
r.stdout
);
let r = sb.mind(&["learn", "src", "--profile", "full", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let progress = r
.stdout
.find(&format!(
"resolving profile(s) full of {} ...",
sb.identity()
))
.unwrap_or_else(|| panic!("{}", r.stdout));
let first_learned = r
.stdout
.find("learned ")
.unwrap_or_else(|| panic!("{}", r.stdout));
assert!(progress < first_learned, "{}", r.stdout);
assert!(
!r.stdout.contains("already installed; nothing to do"),
"{}",
r.stdout
);
assert_eq!(
sb.installed(),
svec(&[
"agent:planner",
"rule:style",
"skill:deploy",
"skill:helper",
"skill:review"
])
);
sb.mind(&["forget", "src", "--profile", "full", "--yes"]);
let r = sb.mind(&["learn", "src", "--profile", "full", "--yes", "--json"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(!r.stdout.contains("resolving"), "{}", r.stdout);
json_doc(&r);
}