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 {
Sandbox::build("agents", true)
}
fn named(name: &str) -> Sandbox {
Sandbox::build(name, true)
}
fn bare(name: &str) -> Sandbox {
Sandbox::build(name, false)
}
fn from_example(name: &str) -> Sandbox {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-it-{}-{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"),
};
let example = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join(name);
copy_dir(&example, &source);
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"]);
sb
}
fn from_root_mindfile() -> Sandbox {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-it-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let source = base.join("mind");
let sb = Sandbox {
base: base.clone(),
source: source.clone(),
mind_home: base.join("home"),
claude_home: base.join("claude"),
};
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
std::fs::create_dir_all(&source).unwrap();
let nested_a = base.join("anthropics-skills");
write(
&nested_a.join("skills/astand/SKILL.md"),
"---\nname: astand\ndescription: stand-in for a curated skill\n---\n# astand\n",
);
git(&nested_a, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&nested_a, &["config", "user.email", "t@t"]);
git(&nested_a, &["config", "user.name", "t"]);
git(&nested_a, &["add", "-A"]);
git(&nested_a, &["commit", "-qm", "initial"]);
let nested_b = base.join("awesome-claude-skills");
write(
&nested_b.join("README.md"),
"# awesome (stand-in, no items)\n",
);
git(&nested_b, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&nested_b, &["config", "user.email", "t@t"]);
git(&nested_b, &["config", "user.name", "t"]);
git(&nested_b, &["add", "-A"]);
git(&nested_b, &["commit", "-qm", "initial"]);
let mindfile = std::fs::read_to_string(root.join("mind.toml")).unwrap();
let mindfile = mindfile
.replace(
"https://github.com/anthropics/skills",
nested_a.to_str().unwrap(),
)
.replace(
"https://github.com/ComposioHQ/awesome-claude-skills",
nested_b.to_str().unwrap(),
);
write(&source.join("mind.toml"), &mindfile);
copy_dir(&root.join("examples/hello"), &source.join("examples/hello"));
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"]);
sb
}
fn build(name: &str, with_fixture: bool) -> Sandbox {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-it-{}-{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"),
};
if with_fixture {
write(
&source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\n",
);
write(
&source.join("agents/dev.md"),
"---\nname: dev\ndescription: Implements a spec with tests\n---\n# dev agent\n",
);
write(
&source.join("rules/style.md"),
"---\ndescription: ASCII only\n---\n# style rule\n",
);
} else {
write(&source.join("README.md"), "# registry\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"]);
sb
}
fn mind(&self, args: &[&str]) -> Run {
self.run(args, None, &[])
}
fn mind_with_input(&self, args: &[&str], input: Option<&str>) -> Run {
self.run(args, input, &[])
}
fn mind_env(&self, args: &[&str], envs: &[(&str, &str)]) -> Run {
self.run(args, None, envs)
}
fn mind_cwd(&self, args: &[&str], cwd: &Path) -> Run {
let out = Command::new(env!("CARGO_BIN_EXE_mind"))
.args(args)
.current_dir(cwd)
.env("MIND_HOME", &self.mind_home)
.env("CLAUDE_HOME", &self.claude_home)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.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 run(&self, args: &[&str], input: Option<&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)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::piped());
for (k, v) in envs {
cmd.env(k, v);
}
let mut child = cmd.spawn().expect("spawn mind");
if let Some(text) = input {
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(text.as_bytes())
.unwrap();
}
let out = child.wait_with_output().expect("wait 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 edit_source(&self) {
write(
&self.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\nedited\n",
);
git(&self.source, &["commit", "-aqm", "edit review"]);
}
fn write_and_commit(&self, rel: &str, contents: &str) {
write(&self.source.join(rel), contents);
git(&self.source, &["add", "-A"]);
git(&self.source, &["commit", "-qm", "fixture"]);
}
fn remove_and_commit(&self, rel: &str) {
std::fs::remove_file(self.source.join(rel)).unwrap();
git(&self.source, &["add", "-A"]);
git(&self.source, &["commit", "-qm", "remove"]);
}
fn source_spec(&self) -> String {
self.source.to_string_lossy().into_owned()
}
fn base_name(&self) -> String {
self.base
.file_name()
.unwrap()
.to_string_lossy()
.into_owned()
}
}
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 copy_dir(src: &Path, dst: &Path) {
std::fs::create_dir_all(dst).unwrap();
for entry in std::fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let from = entry.path();
let to = dst.join(entry.file_name());
if from.is_dir() {
copy_dir(&from, &to);
} else {
std::fs::copy(&from, &to).unwrap();
}
}
}
fn git(dir: &Path, args: &[&str]) {
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:?} failed in {dir:?}");
}
fn assert_no_review_temp(mind_home: &Path) {
let tdir = mind_home.join(".tmp");
if !tdir.is_dir() {
return;
}
for entry in std::fs::read_dir(&tdir).unwrap().flatten() {
let name = entry.file_name();
assert!(
!name.to_string_lossy().starts_with("review-"),
"leftover review temp dir: {:?}",
entry.path()
);
}
}
fn melded() -> Sandbox {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {}", r.stderr);
sb
}
fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', r"'\''"))
}
fn shell_split(s: &str) -> Vec<String> {
let chars: Vec<char> = s.chars().collect();
let n = chars.len();
let mut i = 0;
let mut tokens = Vec::new();
let mut cur = String::new();
let mut in_token = false;
while i < n {
let c = chars[i];
if c.is_whitespace() {
if in_token {
tokens.push(std::mem::take(&mut cur));
in_token = false;
}
i += 1;
continue;
}
in_token = true;
if c == '\'' {
i += 1;
while i < n && chars[i] != '\'' {
cur.push(chars[i]);
i += 1;
}
assert!(i < n, "unterminated single quote in {s:?}");
i += 1;
} else if c == '\\' && i + 1 < n && chars[i + 1] == '\'' {
cur.push('\'');
i += 2;
} else {
cur.push(c);
i += 1;
}
}
if in_token {
tokens.push(cur);
}
tokens
}
fn remedy_argv(text: &str) -> Vec<String> {
let line = text
.lines()
.find(|l| l.contains("run `mind ") || l.contains("run\n`mind "))
.unwrap_or_else(|| panic!("no `run \\`mind ...\\`` remedy line in: {text:?}"));
let start = line.find("`mind ").expect("checked above") + 1;
let rest = &line[start..];
let end = rest.find('`').unwrap_or(rest.len());
let cmd = &rest[..end];
let mut argv = shell_split(cmd);
assert_eq!(
argv.first().map(String::as_str),
Some("mind"),
"the remedy must be a `mind` invocation: {cmd:?}"
);
argv.remove(0);
argv
}
#[test]
fn remeld_ignored_flags_note_shell_quotes_a_metachar_alias() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let alias = "it's; touch pwned; echo $(whoami)";
let first = sb.mind(&["meld", &spec, "--as", alias, "--register-only"]);
assert!(
first.success,
"first meld failed: {} {}",
first.stdout, first.stderr
);
let again = sb.mind(&[
"meld",
&spec,
"--as",
alias,
"--register-only",
"--add-root",
".claude",
]);
assert!(
again.success,
"a re-meld is not an error: {} {}",
again.stdout, again.stderr
);
let source_name = format!("local/{}/agents@{alias}", sb.base_name());
let quoted = shell_quote(&source_name);
assert!(
again.stdout.contains(&format!("mind unmeld {quoted}")),
"the remedy must shell-quote the source identity: {}",
again.stdout
);
assert!(
!again.stdout.contains(&format!("mind unmeld {source_name}")),
"the raw identity must never be spliced in unquoted: {}",
again.stdout
);
let argv = remedy_argv(&again.stdout);
let args: Vec<&str> = argv.iter().map(String::as_str).collect();
let r = sb.mind(&args);
assert!(
r.success,
"the round-tripped remedy must succeed: {} {}",
r.stdout, r.stderr
);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"the round-tripped `mind unmeld` must have removed exactly the aliased instance"
);
}
#[test]
fn learn_not_found_hint_shell_quotes_a_metachar_query() {
let sb = melded();
let query = "a'; touch pwned; echo $(whoami)";
let r = sb.mind(&["learn", query]);
assert!(
!r.success,
"learn must fail for an unknown item: {}",
r.stdout
);
let quoted = shell_quote(query);
assert!(
r.stderr.contains(&format!("mind probe {quoted}")),
"the hint must shell-quote the query: {}",
r.stderr
);
assert!(
!r.stderr.contains(&format!("mind probe {query}")),
"the raw query must never be spliced in unquoted: {}",
r.stderr
);
}
#[test]
fn unmeld_unlink_only_remaining_items_hint_shell_quotes_a_metachar_alias() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let alias = "it's a source";
assert!(sb.mind(&["meld", &spec, "--as", alias]).success);
let source_name = format!("local/{}/agents@{alias}", sb.base_name());
let learn_all = sb.mind(&["learn", "*"]);
assert!(
learn_all.success,
"learn * failed: {} {}",
learn_all.stdout, learn_all.stderr
);
let r = sb.mind(&["unmeld", &source_name, "--unlink-only"]);
assert!(r.success, "unmeld failed: {} {}", r.stdout, r.stderr);
let quoted = shell_quote(&format!("{source_name}#*"));
assert!(
r.stdout.contains(&format!("mind forget {quoted}")),
"the remedy must shell-quote the source identity: {}",
r.stdout
);
assert!(
!r.stdout.contains(&format!("mind forget '{source_name}#*'")),
"the identity must never be framed in bare single quotes: {}",
r.stdout
);
let argv = remedy_argv(&r.stdout);
let mut args: Vec<&str> = argv.iter().map(String::as_str).collect();
args.push("--yes");
let forget = sb.mind(&args);
assert!(
forget.success,
"the round-tripped remedy must succeed: {} {}",
forget.stdout, forget.stderr
);
assert!(
!sb.mind(&["recall"]).stdout.contains("installed @"),
"the round-tripped `mind forget` must have removed every item"
);
}
#[test]
fn recall_not_installed_hint_shell_quotes_an_item_name_with_a_quote() {
let sb = Sandbox::bare("quoteitems");
sb.write_and_commit(
"skills/rev'iew/SKILL.md",
"---\ndescription: a quote-carrying item\n---\n# skill\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
let r = sb.mind(&["meld", &spec, "--register-only"]);
assert!(r.success, "re-meld failed: {} {}", r.stdout, r.stderr);
let quoted = shell_quote("skill:rev'iew");
assert!(
r.stdout.contains(&format!("mind learn {quoted}")),
"the hint must shell-quote the item key: {}",
r.stdout
);
assert!(
!r.stdout.contains("mind learn 'skill:rev'iew'"),
"the raw item key must never be framed in bare single quotes: {}",
r.stdout
);
}
#[test]
fn meld_default_non_tty_install_note_shell_quotes_a_metachar_alias() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let alias = "it's; touch pwned";
let r = sb.mind(&["meld", &spec, "--as", alias]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("nothing installed"),
"must note nothing was installed (not a TTY): {}",
r.stdout
);
let source_name = format!("local/{}/agents@{alias}", sb.base_name());
let quoted = shell_quote(&format!("{source_name}#*"));
assert!(
r.stdout.contains(&format!("mind learn {quoted}")),
"the note must shell-quote the source identity: {}",
r.stdout
);
assert!(
!r.stdout.contains(&format!("mind learn '{source_name}#*'")),
"the identity must never be framed in bare single quotes: {}",
r.stdout
);
let argv = remedy_argv(&r.stdout);
let args: Vec<&str> = argv.iter().map(String::as_str).collect();
let learn = sb.mind(&args);
assert!(
learn.success,
"the round-tripped remedy must succeed: {} {}",
learn.stdout, learn.stderr
);
assert!(
sb.mind(&["recall"]).stdout.contains("installed @"),
"the round-tripped `mind learn` must have installed the item(s)"
);
}
#[test]
fn curator_add_roots_warning_shell_quotes_a_metachar_nested_source_name() {
let nested = Sandbox::bare("it's a nested source");
nested.write_and_commit(
"skills/base/SKILL.md",
"---\nname: base\ndescription: base skill\n---\n# base\n",
);
nested.write_and_commit(
"contrib/skills/addon/SKILL.md",
"---\nname: addon\ndescription: add-root contributed skill\n---\n# addon\n",
);
nested.write_and_commit("mind.toml", "[source]\ndescription = \"onboarded\"\n");
let registry = Sandbox::bare("addroot-registry-quote-test");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\nsource = \"{}\"\nadd-roots = [\"contrib\"]\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
assert!(
r.stderr.contains("mind meld") && r.stderr.contains("--add-root"),
"the DSC-60 warning must offer the consumer-side add-root remedy: {}",
r.stderr
);
let start = r.stderr.find("`mind meld ").expect("remedy present") + 1;
let rest = &r.stderr[start..];
let end = rest.find('`').unwrap_or(rest.len());
let cmd = &rest[..end];
assert!(
cmd.contains(r"'\''"),
"the nested source's quote-carrying name must be shell-quoted \
(the '\\'' idiom must appear) in: {cmd:?}"
);
}
#[test]
fn meld_registers_source_and_lists_items() {
let sb = melded();
let r = sb.mind(&["recall", "--sources"]);
assert!(r.success);
assert!(r.stdout.contains("agents"), "sources: {}", r.stdout);
}
#[test]
fn meld_of_a_source_declaring_no_hooks_runs_and_discloses_none() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--register-only"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let out = format!("{}{}", r.stdout, r.stderr);
for marker in ["hook", "Hook", "would run", "run this command"] {
assert!(
!out.contains(marker),
"a source declaring no hooks must not mention `{marker}`: {out}"
);
}
}
#[test]
fn meld_with_zero_discovered_items_reports_guidance() {
let sb = Sandbox::bare("zero-item-repo");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(
r.success,
"a zero-item meld is not a hard error: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("(0 item(s))"),
"must still report the 0 item(s) count: {}",
r.stdout
);
assert!(
r.stdout.contains("! melded"),
"a zero-item meld must use the warning glyph, not the success one: {}",
r.stdout
);
for hint in [
"skills/<name>/SKILL.md",
"agents/<name>.md",
"rules/<name>.md",
"tools/<name>/",
"--root",
"--add-root",
"--flat-skills",
] {
assert!(
r.stdout.contains(hint),
"guidance must mention `{hint}`: {}",
r.stdout
);
}
}
#[test]
fn meld_with_add_root_discovering_items_suppresses_zero_item_note() {
let sb = Sandbox::bare("addroot-fixes-zero");
sb.write_and_commit(
".claude/skills/foo/SKILL.md",
"---\nname: foo\ndescription: foo\n---\n# foo\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--add-root", ".claude"]);
assert!(
r.success,
"meld --add-root failed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("(1 item(s))") && r.stdout.contains("+ melded"),
"with --add-root discovering the item, the note must not fire: {}",
r.stdout
);
}
#[test]
fn remeld_notes_the_discovery_flags_it_ignored() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let first = sb.mind(&["meld", &spec, "--register-only"]);
assert!(first.success, "first meld failed: {}", first.stderr);
let again = sb.mind(&[
"meld",
&spec,
"--register-only",
"--add-root",
".claude",
"--flat-skills",
]);
assert!(
again.success,
"a re-meld is not an error: {} {}",
again.stdout, again.stderr
);
assert!(
again.stdout.contains("--add-root") && again.stdout.contains("--flat-skills"),
"the note must name each ignored flag: {}",
again.stdout
);
assert!(
again.stdout.contains("ignored") && again.stdout.contains("unmeld"),
"the note must say they were ignored and what to do instead: {}",
again.stdout
);
assert!(
!again.stdout.contains("--pin"),
"the ignored-flags note must not name --pin now that a re-meld honors it (CLI-209): {}",
again.stdout
);
let quiet = sb.mind(&["meld", &spec, "--register-only"]);
assert!(
!quiet.stdout.contains("ignored"),
"a re-meld with no discovery flags must print no note: {}",
quiet.stdout
);
}
#[test]
fn bare_mind_prints_help_on_stdout_and_exits_zero() {
let sb = Sandbox::new();
let r = sb.mind(&[]);
assert!(
r.success,
"bare `mind` must exit 0: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("Usage") && r.stdout.contains("meld"),
"bare `mind` must print help on stdout: {}",
r.stdout
);
}
#[test]
fn learn_of_a_source_name_hints_at_learn_all() {
let sb = melded();
let source = format!("local/{}/agents", sb.base_name());
let r = sb.mind(&["learn", &source]);
assert!(!r.success, "learn of a source name is still an error");
assert!(
r.stderr.contains("--all") && r.stderr.contains(&source),
"the hint must name the source and `learn --all`: {}",
r.stderr
);
}
#[test]
fn meld_yes_installs_all_source_items() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld --yes failed: {} {}", r.stdout, r.stderr);
let recall = sb.mind(&["recall"]);
for item in ["review", "dev", "style"] {
assert!(
recall.stdout.contains(item),
"{item} should be installed after `meld --yes`: {}",
recall.stdout
);
}
}
#[test]
fn meld_link_only_registers_without_installing() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
assert!(
sb.mind(&["recall", "--sources"]).stdout.contains("agents"),
"the source must be registered"
);
assert!(
!sb.mind(&["recall"]).stdout.contains("installed @"),
"--link-only must not install any items"
);
}
#[test]
fn meld_default_non_tty_registers_only_and_notes_install() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
assert!(
sb.mind(&["recall", "--sources"]).stdout.contains("agents"),
"the source must be registered"
);
assert!(
!sb.mind(&["recall"]).stdout.contains("installed @"),
"a non-TTY default meld must not install items"
);
assert!(
r.stdout.contains("learn") && r.stdout.contains("#*"),
"it should note how to install later: {}",
r.stdout
);
assert!(
r.stdout.contains("nothing installed"),
"the note must say explicitly that nothing was installed: {}",
r.stdout
);
}
#[test]
fn meld_uses_declared_prefix_when_installing() {
let sb = Sandbox::new();
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--yes"]).success,
"meld of a prefixed source should succeed"
);
let recall = sb.mind(&["recall"]).stdout;
assert!(
recall.contains("jk:review"),
"items must carry the declared prefix: {recall}"
);
}
#[test]
fn meld_as_empty_overrides_a_declared_prefix() {
let sb = Sandbox::new();
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--as", "", "--yes"]).success,
"meld --as '' should succeed"
);
let recall = sb.mind(&["recall"]).stdout;
assert!(recall.contains("review"), "items must install: {recall}");
assert!(
!recall.contains("jk:"),
"the declared prefix must be overridden to none: {recall}"
);
}
#[test]
fn meld_namespace_empty_overrides_a_declared_prefix() {
let sb = Sandbox::new();
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--namespace", "", "--yes"])
.success,
"meld --namespace '' should succeed"
);
let recall = sb.mind(&["recall"]).stdout;
assert!(recall.contains("review"), "items must install: {recall}");
assert!(
!recall.contains("jk:"),
"the declared prefix must be overridden to none: {recall}"
);
}
#[test]
fn meld_with_no_arg_melds_the_current_directory() {
let sb = Sandbox::new();
let r = sb.mind_cwd(&["meld", "--link-only"], &sb.source);
assert!(
r.success,
"no-arg meld of the cwd failed: {} {}",
r.stdout, r.stderr
);
let sources = sb.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("agents"),
"the current directory must be registered as a source: {sources}"
);
}
#[test]
fn local_source_is_read_from_its_working_tree() {
let sb = Sandbox::bare("worktree-src");
sb.write_and_commit("skills/a/SKILL.md", "---\ndescription: a\n---\n# a\n");
write(
&sb.source.join("mind.toml"),
"[source]\ndescription = \"live working tree\"\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
assert!(
!clone_dir_of(&sb, "worktree-src").exists(),
"a linked local source must not be cloned"
);
let sources = sb.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("live working tree"),
"the untracked mind.toml must be read from the working tree: {sources}"
);
assert!(
sb.mind(&["unmeld", "worktree-src", "--unlink-only"])
.success
);
assert!(
sb.source.join("skills/a/SKILL.md").exists(),
"unmeld must not delete the linked working tree"
);
}
#[test]
fn recall_degrades_past_a_vanished_linked_source_instead_of_hard_failing() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
std::fs::remove_dir_all(&sb.source).unwrap();
let r = sb.mind(&["recall"]);
assert!(
r.success,
"recall must degrade past a vanished linked source, not hard-fail: {}\n{}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("gone") || r.stderr.contains("unmeld"),
"must warn about the vanished source with an actionable hint: {}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(sources.success);
assert!(
sources.stdout.contains("agents"),
"the source itself must still be listed: {}",
sources.stdout
);
let p = sb.mind(&["probe"]);
assert!(
p.success,
"probe must degrade past a vanished linked source: {}\n{}",
p.stdout, p.stderr
);
}
#[test]
fn introspect_reports_a_vanished_linked_source_as_an_issue_not_a_hard_error() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
std::fs::remove_dir_all(&sb.source).unwrap();
let r = sb.mind(&["introspect"]);
assert!(
r.success,
"introspect must not hard-fail: {}\n{}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("gone") || r.stdout.contains("unmeld"),
"introspect must report the vanished source as an actionable issue: {}",
r.stdout
);
}
fn mind_in(sb: &Sandbox, args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> Run {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mind"));
cmd.args(args)
.current_dir(cwd)
.env("MIND_HOME", &sb.mind_home)
.env("CLAUDE_HOME", &sb.claude_home)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
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(),
}
}
#[test]
fn recall_still_lists_the_healthy_sources_items_when_another_source_is_gone() {
let sb = melded(); let broken = Sandbox::bare("gone-src");
broken.write_and_commit(
"skills/ghost/SKILL.md",
"---\nname: ghost\ndescription: ghost skill\n---\n# ghost\n",
);
assert!(
sb.mind(&["meld", &broken.source_spec(), "--register-only"])
.success
);
std::fs::remove_dir_all(&broken.source).unwrap();
let r = sb.mind(&["recall"]);
assert!(r.success, "recall must degrade: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("review") && r.stdout.contains("dev"),
"the healthy source's items must still be listed: {}",
r.stdout
);
assert!(
!r.stdout.contains("ghost"),
"the gone source contributes nothing: {}",
r.stdout
);
assert!(
r.stderr.contains("gone-src"),
"the warning must name the skipped source: {}",
r.stderr
);
let p = sb.mind(&["probe", "review", "--no-tui"]);
assert!(p.success, "probe must degrade: {} {}", p.stdout, p.stderr);
assert!(
p.stdout.contains("review"),
"probe must still search the sources that did scan: {}",
p.stdout
);
}
#[test]
fn hostile_frontmatter_description_is_sanitized_everywhere() {
let sb = Sandbox::bare("hostile-desc");
sb.write_and_commit(
"skills/evil/SKILL.md",
"---\nname: evil\ndescription: \"\x1b[31mRED\x1b[0m \u{202E}oot now\"\n---\n# evil\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let probe = sb.mind(&["probe", "--no-tui"]);
assert!(probe.success, "{}", probe.stderr);
assert!(
!probe.stdout.contains('\x1b') && !probe.stdout.contains('\u{202E}'),
"probe --no-tui must not leak raw ANSI/bidi: {:?}",
probe.stdout
);
assert!(
probe.stdout.contains("RED") && probe.stdout.contains("oot now"),
"the surrounding text must still be shown: {}",
probe.stdout
);
let probe_json = sb.mind(&["probe", "--json"]);
assert!(probe_json.success, "{}", probe_json.stderr);
assert!(
!probe_json.stdout.contains('\x1b') && !probe_json.stdout.contains('\u{202E}'),
"probe --json must not leak raw ANSI/bidi (serde only escapes ANSI, \
not the bidi override): {:?}",
probe_json.stdout
);
assert!(sb.mind(&["learn", "skill:evil"]).success);
let recall = sb.mind(&["recall", "skill:evil"]);
assert!(recall.success, "{}", recall.stderr);
assert!(
!recall.stdout.contains('\x1b') && !recall.stdout.contains('\u{202E}'),
"recall must not leak raw ANSI/bidi: {:?}",
recall.stdout
);
let recall_json = sb.mind(&["recall", "skill:evil", "--json"]);
assert!(recall_json.success, "{}", recall_json.stderr);
assert!(
!recall_json.stdout.contains('\x1b') && !recall_json.stdout.contains('\u{202E}'),
"recall --json must not leak raw ANSI/bidi: {:?}",
recall_json.stdout
);
}
#[test]
fn hostile_mind_toml_source_description_is_sanitized_everywhere() {
let sb = Sandbox::bare("hostile-source-desc");
sb.write_and_commit(
"mind.toml",
"[source]\ndescription = \"\\u001b[31mRED\\u001b[0m \\u202Eoot now\"\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let sources = sb.mind(&["recall", "--sources"]);
assert!(sources.success, "{}", sources.stderr);
assert!(
!sources.stdout.contains('\x1b') && !sources.stdout.contains('\u{202E}'),
"recall --sources must not leak raw ANSI/bidi from [source].description: {:?}",
sources.stdout
);
let sources_json = sb.mind(&["recall", "--sources", "--json"]);
assert!(sources_json.success, "{}", sources_json.stderr);
assert!(
!sources_json.stdout.contains('\x1b') && !sources_json.stdout.contains('\u{202E}'),
"recall --sources --json must not leak raw ANSI/bidi: {:?}",
sources_json.stdout
);
sb.write_and_commit(
"mind.toml",
"[source]\ndescription = \"\\u001b[32mGREEN\\u001b[0m \\u202Eoot again\"\n",
);
assert!(sb.mind(&["sync"]).success);
let after_sync = sb.mind(&["recall", "--sources"]);
assert!(
!after_sync.stdout.contains('\x1b') && !after_sync.stdout.contains('\u{202E}'),
"a re-synced [source].description must stay sanitized: {:?}",
after_sync.stdout
);
}
#[test]
fn hostile_installed_item_is_sanitized_at_recall_introspect_and_forget() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let manifest_path = sb.mind_home.join("manifest.json");
let raw = std::fs::read_to_string(&manifest_path).unwrap();
let mut doc: serde_json::Value = serde_json::from_str(&raw).unwrap();
let review = doc["items"]["skill:review"].clone();
assert!(!review.is_null(), "the review entry must exist: {doc}");
let hostile_name = format!("re{}vie\u{202E}w", '\x07');
let hostile_key = format!("skill:{hostile_name}");
let mut hostile = review.clone();
hostile["name"] = serde_json::Value::String(hostile_name.clone());
hostile["bare_name"] = serde_json::Value::String(hostile_name.clone());
hostile["store"] = serde_json::Value::String(format!("store/skill/{hostile_name}"));
hostile["links"] = serde_json::Value::Array(vec![]);
doc["items"][&hostile_key] = hostile;
std::fs::write(&manifest_path, serde_json::to_vec_pretty(&doc).unwrap()).unwrap();
let recall = sb.mind(&["recall"]);
assert!(recall.success, "{}", recall.stderr);
assert!(
!recall.stdout.contains('\x1b') && !recall.stdout.contains('\u{202E}'),
"recall must not leak raw ANSI/bidi from an installed item name: {:?}",
recall.stdout
);
assert!(
recall.stdout.contains("skill:review"),
"recall must still show the sanitized key (twice: the real item and \
the orphan row): {:?}",
recall.stdout
);
let recall_json = sb.mind(&["recall", "--json"]);
assert!(recall_json.success, "{}", recall_json.stderr);
assert!(
!recall_json.stdout.contains('\x1b') && !recall_json.stdout.contains('\u{202E}'),
"recall --json must not leak raw ANSI/bidi: {:?}",
recall_json.stdout
);
assert!(
recall_json.stdout.contains("skill:review"),
"recall --json must still show the sanitized key: {:?}",
recall_json.stdout
);
for args in [
vec!["recall", hostile_key.as_str()],
vec!["recall", hostile_key.as_str(), "--json"],
] {
let out = sb.mind(&args);
assert!(out.success, "{args:?}: {}", out.stderr);
assert!(
!out.stdout.contains('\x1b') && !out.stdout.contains('\u{202E}'),
"{args:?} must not leak raw ANSI/bidi: {:?}",
out.stdout
);
assert!(
out.stdout.contains("review"),
"{args:?} must still show the sanitized name: {:?}",
out.stdout
);
}
let introspect = sb.mind(&["introspect"]);
assert!(
!introspect.stdout.contains('\x1b') && !introspect.stdout.contains('\u{202E}'),
"introspect must not leak raw ANSI/bidi: {:?}",
introspect.stdout
);
assert!(
introspect.stdout.contains("skill:review"),
"introspect must still show the sanitized key: {:?}",
introspect.stdout
);
assert!(
introspect.stdout.contains("no longer present"),
"introspect must still report the removed-upstream finding: {:?}",
introspect.stdout
);
let introspect_json = sb.mind(&["introspect", "--json"]);
assert!(introspect_json.success, "{}", introspect_json.stderr);
assert!(
!introspect_json.stdout.contains('\x1b') && !introspect_json.stdout.contains('\u{202E}'),
"introspect --json must not leak raw ANSI/bidi: {:?}",
introspect_json.stdout
);
let introspect_v = parse_json(&introspect_json.stdout);
let found = introspect_v["issues"]
.as_array()
.unwrap()
.iter()
.any(|i| i["target"] == "skill:review" && i["kind"] == "removed-upstream");
assert!(
found,
"introspect --json must report the sanitized target: {introspect_v}"
);
let forget_json = sb.mind(&["forget", &hostile_key, "--json", "--yes"]);
assert!(forget_json.success, "{}", forget_json.stderr);
let forget_v = parse_json(&forget_json.stdout);
assert_eq!(
forget_v["removed"].as_array().map(|a| a.len()),
Some(1),
"forget --json must record the removed item: {forget_v}"
);
assert_eq!(
forget_v["removed"][0], "skill:review",
"the removed[] entry must be the sanitized key: {forget_v}"
);
assert!(sb.claude_home.join("skills/review/SKILL.md").exists());
}
#[test]
fn hostile_installed_item_name_is_sanitized_in_recall_tree() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let manifest_path = sb.mind_home.join("manifest.json");
let raw = std::fs::read_to_string(&manifest_path).unwrap();
let mut doc: serde_json::Value = serde_json::from_str(&raw).unwrap();
let review = doc["items"]["skill:review"].clone();
assert!(!review.is_null());
let hostile_name = format!("re{}vie\u{202E}w", '\x07');
let hostile_key = format!("skill:{hostile_name}");
let mut hostile = review.clone();
hostile["name"] = serde_json::Value::String(hostile_name.clone());
hostile["bare_name"] = serde_json::Value::String(hostile_name.clone());
hostile["store"] = serde_json::Value::String(format!("store/skill/{hostile_name}"));
hostile["links"] = serde_json::Value::Array(vec![]);
doc["items"][&hostile_key] = hostile;
std::fs::write(&manifest_path, serde_json::to_vec_pretty(&doc).unwrap()).unwrap();
let tree = sb.mind(&["recall", &hostile_key, "--tree"]);
assert!(tree.success, "{}", tree.stderr);
assert!(
!tree.stdout.contains('\x07') && !tree.stdout.contains('\u{202E}'),
"recall --tree must not leak raw control/bidi bytes: {:?}",
tree.stdout
);
assert!(
tree.stdout.contains("review"),
"recall --tree must still show the sanitized name: {:?}",
tree.stdout
);
let tree_json = sb.mind(&["recall", &hostile_key, "--tree", "--json"]);
assert!(tree_json.success, "{}", tree_json.stderr);
assert!(
!tree_json.stdout.contains('\x07') && !tree_json.stdout.contains('\u{202E}'),
"recall --tree --json must not leak raw control/bidi bytes: {:?}",
tree_json.stdout
);
let v = parse_json(&tree_json.stdout);
assert_eq!(
v["key"], "skill:review",
"the --tree --json node's key must be the sanitized key: {v}"
);
}
#[test]
fn hostile_unmanaged_item_name_is_skipped_via_unm9() {
let sb = melded();
let unmanaged_name = format!("hand{}ma\u{202E}de", '\x07');
let unmanaged_bare = format!("skill:hand{}ma\u{202E}de", '\x07');
let dir = sb.claude_home.join("skills").join(&unmanaged_name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
)
.unwrap();
let forget_unmanaged = sb.mind(&["forget", &unmanaged_bare]);
assert!(
!forget_unmanaged.success,
"a hostile derived name must never resolve as a forgettable item: {}",
forget_unmanaged.stdout
);
let warning_line = forget_unmanaged
.stderr
.lines()
.find(|l| l.starts_with("warning:"))
.unwrap_or_else(|| {
panic!(
"expected a scan warning line: {:?}",
forget_unmanaged.stderr
)
});
assert!(
!warning_line.contains('\x1b') && !warning_line.contains('\u{202E}'),
"the scan's own warning must not leak raw ANSI/bidi: {:?}",
warning_line
);
assert!(
warning_line.contains("unsafe") && warning_line.contains("handmade"),
"the scan's own warning must name the cause and the sanitized name: {:?}",
warning_line
);
assert!(
forget_unmanaged
.stderr
.to_lowercase()
.contains("not installed")
|| forget_unmanaged
.stderr
.to_lowercase()
.contains("notinstalled"),
"the ref must fail to resolve, having been skipped out of the scan: {:?}",
forget_unmanaged.stderr
);
assert!(dir.exists(), "a skipped entry must be left in place");
let forget_yes = sb.mind(&["forget", &unmanaged_bare, "--yes"]);
assert!(
!forget_yes.success,
"--yes cannot forget an entry that was never a candidate: {}",
forget_yes.stdout
);
assert!(dir.exists(), "the unmanaged item must not be removed");
}
#[test]
fn hostile_named_item_is_skipped_at_scan_not_installed() {
let sb = Sandbox::bare("hostile-name-scan");
let name = format!("re{}[31mvie\u{202E}w", '\x1b');
sb.write_and_commit(
&format!("skills/{name}/SKILL.md"),
"---\ndescription: a skill\n---\n# skill\n",
);
let meld = sb.mind(&["meld", &sb.source_spec()]);
assert!(
meld.success,
"meld itself must still succeed: {}",
meld.stderr
);
assert!(
meld.stderr.contains("unsafe item name"),
"meld must warn that the hostile-named item was skipped: {}",
meld.stderr
);
let recall = sb.mind(&["recall"]);
assert!(
!recall.stdout.contains("skill:review"),
"the hostile-named item must not appear in the catalog at all: {}",
recall.stdout
);
}
#[test]
fn hostile_discover_glob_error_is_sanitized_and_names_the_dsc81_rejection() {
let sb = Sandbox::bare("hostile-glob");
sb.write_and_commit(
"mind.toml",
"[discover]\nskills = { include = [\"/abs\\u001b[31mINJ\\u001b[0m\\u202E/*/SKILL.md\"] }\n",
);
let meld = sb.mind(&["meld", &sb.source_spec()]);
assert!(
!meld.success,
"an absolute discover glob must be rejected (DSC-81)"
);
assert!(
meld.stderr.contains("absolute") || meld.stderr.contains("must be relative"),
"the failure must specifically name the absolute-glob rejection, not \
some other meld failure: {}",
meld.stderr
);
let meld_json = sb.mind(&["meld", &sb.source_spec(), "--json"]);
assert!(!meld_json.success);
let v = parse_json(&meld_json.stdout);
assert_eq!(
v["error"]["kind"], "mind-toml",
"expected the mind-toml error kind: {v}"
);
let message = v["error"]["message"].as_str().unwrap_or_default();
assert!(
message.contains("absolute") || message.contains("must be relative"),
"the --json error message must also name the absolute-glob rejection: {v}"
);
assert!(
!meld.stderr.contains('\x1b') && !meld.stderr.contains('\u{202E}'),
"the rejected glob must be echoed stripped on stderr: {:?}",
meld.stderr
);
assert!(
!message.contains('\x1b') && !message.contains('\u{202E}'),
"the rejected glob must be echoed stripped in --json: {message:?}"
);
assert!(
message.contains("INJ"),
"the sanitized pattern must still be shown: {message:?}"
);
}
#[test]
fn a_stray_item_manifest_drops_its_item_not_every_melded_source() {
let sb = melded();
let stray = Sandbox::bare("stray-item-manifest");
stray.write_and_commit(
"skills/vendored/SKILL.md",
"---\nname: vendored\ndescription: a vendored sub-source\n---\n# vendored\n",
);
stray.write_and_commit(
"skills/vendored/mind.toml",
"[source]\ndescription = \"a source manifest in an item dir\"\n",
);
stray.write_and_commit(
"skills/healthy/SKILL.md",
"---\nname: healthy\ndescription: a normal sibling\n---\n# healthy\n",
);
let meld = sb.mind(&["meld", &stray.source_spec(), "--register-only"]);
assert!(
meld.success,
"one unreadable item manifest must not fail the meld: {} {}",
meld.stdout, meld.stderr
);
let probe = sb.mind(&["probe"]);
assert!(
probe.success,
"probe must still run: {} {}",
probe.stdout, probe.stderr
);
assert!(
probe.stdout.contains("skill:review"),
"the other source's items must still be listed: {}",
probe.stdout
);
assert!(
probe.stdout.contains("skill:healthy"),
"the stray source's other items must still be listed: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("vendored"),
"the item with the unreadable manifest must not be offered: {}",
probe.stdout
);
assert!(
probe.stderr.contains("vendored") && probe.stderr.contains("mind.toml"),
"the warning must name the item and its manifest: {}",
probe.stderr
);
assert!(
probe.stderr.contains("[[hooks]]"),
"the warning must name the one-table rule (HOOK-131): {}",
probe.stderr
);
let recall = sb.mind(&["recall"]);
assert!(recall.success, "recall must still run: {}", recall.stderr);
let learn_dropped = sb.mind(&["learn", "vendored", "--yes"]);
assert!(
!learn_dropped.success,
"a dropped item must not be installable: {}",
learn_dropped.stdout
);
let learn_ok = sb.mind(&["learn", "healthy", "--yes"]);
assert!(
learn_ok.success,
"a healthy sibling must still install: {} {}",
learn_ok.stdout, learn_ok.stderr
);
}
#[test]
fn hostile_item_manifest_parse_error_is_sanitized() {
let sb = melded();
let hostile = Sandbox::bare("hostile-item-manifest");
hostile.write_and_commit(
"skills/trap/SKILL.md",
"---\nname: trap\ndescription: trap\n---\n# trap\n",
);
hostile.write_and_commit(
"skills/trap/mind.toml",
"\u{1b}[31mINJ\u{1b}[0m\u{202E} = broken\n",
);
assert!(
sb.mind(&["meld", &hostile.source_spec(), "--register-only"])
.success
);
let probe = sb.mind(&["probe"]);
assert!(probe.success, "probe must still run: {}", probe.stderr);
assert!(
!probe.stderr.contains('\x1b') && !probe.stderr.contains('\u{202E}'),
"the quoted input line must be echoed stripped: {:?}",
probe.stderr
);
assert!(
probe.stderr.contains("INJ"),
"the sanitized snippet must still be shown: {:?}",
probe.stderr
);
assert!(
!probe.stdout.contains("trap"),
"the item must be dropped: {}",
probe.stdout
);
}
#[test]
fn hostile_root_mind_toml_parse_error_is_sanitized() {
let sb = Sandbox::bare("hostile-root-manifest");
sb.write_and_commit("mind.toml", "\u{1b}[31mINJ\u{1b}[0m\u{202E} = broken\n");
let meld = sb.mind(&["meld", &sb.source_spec()]);
assert!(!meld.success, "an unparsable mind.toml must be refused");
assert!(
meld.stderr.contains("invalid mind.toml"),
"the failure must name the file it could not parse: {}",
meld.stderr
);
assert!(
!meld.stderr.contains('\x1b') && !meld.stderr.contains('\u{202E}'),
"the quoted input line must be echoed stripped: {:?}",
meld.stderr
);
assert!(
meld.stderr.contains("INJ"),
"the sanitized snippet must still be shown: {:?}",
meld.stderr
);
}
#[test]
fn an_unprefixed_source_shipping_a_colon_name_is_warned_about() {
let sb = Sandbox::bare("forged-namespace");
sb.write_and_commit(
"commands/acme:deploy.md",
"---\ndescription: deploy the thing\n---\n# deploy\n",
);
let meld = sb.mind(&["meld", &sb.source_spec(), "--register-only"]);
assert!(meld.success, "the item is still offered: {}", meld.stderr);
assert!(
meld.stderr.contains("acme:deploy") && meld.stderr.contains("no namespace prefix"),
"meld must warn that an unprefixed source spelled a namespace: {}",
meld.stderr
);
let learn = sb.mind(&["learn", "command:acme:deploy", "--dry-run"]);
assert!(learn.success, "{}", learn.stderr);
assert!(
learn.stderr.contains("no namespace prefix"),
"learn must warn over the closure it would install: {}",
learn.stderr
);
let probe = sb.mind(&["probe"]);
assert!(probe.success, "{}", probe.stderr);
assert!(
probe.stdout.contains("command:acme:deploy"),
"the name installs as written, warning or not: {}",
probe.stdout
);
assert!(
!probe.stderr.contains("no namespace prefix"),
"a read-only surface must not warn: {}",
probe.stderr
);
let recall = sb.mind(&["recall"]);
assert!(
!recall.stderr.contains("no namespace prefix"),
"a read-only surface must not warn: {}",
recall.stderr
);
let prefixed = Sandbox::bare("real-namespace");
prefixed.write_and_commit("mind.toml", "[source]\nprefix = \"acme\"\n");
prefixed.write_and_commit(
"commands/deploy.md",
"---\ndescription: deploy the thing\n---\n# deploy\n",
);
let sb2 = Sandbox::bare("consumer");
assert!(
sb2.mind(&["meld", &prefixed.source_spec(), "--register-only"])
.success
);
let probe2 = sb2.mind(&["probe"]);
assert!(
probe2.stdout.contains("command:acme:deploy"),
"the prefixed source produces the same effective name: {}",
probe2.stdout
);
assert!(
!probe2.stderr.contains("no namespace prefix"),
"a real prefix must not be warned about: {}",
probe2.stderr
);
}
#[test]
fn learn_still_resolves_an_item_when_another_source_is_gone() {
let sb = melded();
let broken = Sandbox::bare("gone-for-learn");
broken.write_and_commit(
"skills/ghost/SKILL.md",
"---\nname: ghost\ndescription: ghost skill\n---\n# ghost\n",
);
assert!(
sb.mind(&["meld", &broken.source_spec(), "--register-only"])
.success
);
std::fs::remove_dir_all(&broken.source).unwrap();
let r = sb.mind(&["learn", "review"]);
assert!(
r.success,
"learn must still resolve items from the healthy source: {} {}",
r.stdout, r.stderr
);
assert!(
sb.claude_home.join("skills/review/SKILL.md").exists(),
"the item must actually be installed"
);
}
#[test]
fn dump_still_hard_fails_on_a_vanished_linked_source() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
std::fs::remove_dir_all(&sb.source).unwrap();
let out_path = sb.base.join("dumped.toml");
let out_str = out_path.to_string_lossy().into_owned();
let r = sb.mind(&["dump", "--output", &out_str]);
assert!(
!r.success,
"dump must refuse a partial reproduction: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
format!("{}{}", r.stdout, r.stderr).contains("gone"),
"dump must name the vanished working tree as the cause: stdout={} stderr={}",
r.stdout,
r.stderr
);
assert!(
!out_path.exists(),
"no partial dump file may be left behind: {out_path:?}"
);
}
#[test]
fn meld_of_a_nonexistent_local_path_still_hard_fails_and_registers_nothing() {
let sb = Sandbox::bare("meld-guard");
let missing = sb.base.join("no-such-dir").to_string_lossy().into_owned();
let r = sb.mind(&["meld", &missing]);
assert!(
!r.success,
"meld of a nonexistent local path must fail: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("not a directory"),
"the error must name the cause: {}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
!sources.stdout.contains("no-such-dir"),
"nothing may be registered: {}",
sources.stdout
);
}
#[test]
fn dsc93_does_not_soften_a_top_level_meld_of_a_missing_sources_tree_path() {
let sb = Sandbox::bare("dsc93-toplevel");
let inside = sb
.mind_home
.join("sources/local/someone/never-cloned")
.to_string_lossy()
.into_owned();
let r = sb.mind(&["meld", &inside]);
assert!(
!r.success,
"a top-level meld of a missing path inside the sources tree must still \
hard-fail: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("not a directory"),
"the failure must name the missing path: {}",
r.stderr
);
assert!(
!r.stderr.contains("resolves inside mind's own sources tree"),
"the DSC-93 skip rationale must not be applied to a path the user named \
directly: {}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
!sources.stdout.contains("never-cloned"),
"nothing may be registered: {}",
sources.stdout
);
}
#[test]
fn remelding_a_vanished_linked_source_names_it_rather_than_reporting_zero_items() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
std::fs::remove_dir_all(&sb.source).unwrap();
let r = sb.mind(&["meld", &spec, "--register-only"]);
let combined = format!("{}{}", r.stdout, r.stderr);
assert!(
combined.contains("gone") || combined.contains("not a directory"),
"re-melding a vanished source must name the vanished working tree rather than \
reporting a healthy source with no items: stdout={} stderr={}",
r.stdout,
r.stderr
);
assert!(
!r.stdout.contains("0 item(s)"),
"a vanished source must not be reported as an empty but healthy source: {}",
r.stdout
);
assert!(
!r.success,
"the scan error must fail the command, not be degraded into a success: {} {}",
r.stdout, r.stderr
);
}
#[test]
fn a_relative_local_meld_keeps_working_after_the_caller_moves() {
let sb = Sandbox::new();
let r = sb.mind_cwd(&["meld", "./agents", "--register-only"], &sb.base);
assert!(
r.success,
"a relative local meld must succeed: {} {}",
r.stdout, r.stderr
);
let sources_json = std::fs::read_to_string(sb.mind_home.join("sources.json")).unwrap();
assert!(
sources_json.contains(&sb.source.to_string_lossy().into_owned()),
"the absolute path must be recorded: {sources_json}"
);
assert!(
!sources_json.contains("\"./agents\""),
"the literal relative spec must not be persisted: {sources_json}"
);
assert!(
!sources_json.contains("\"local/local/agents\""),
"the owner must be the real parent dir, not the 'local' placeholder: {sources_json}"
);
let elsewhere = sb.mind_home.clone();
let recall = sb.mind_cwd(&["recall"], &elsewhere);
assert!(
recall.success && recall.stdout.contains("review"),
"the source must still be readable from another cwd: {} {}",
recall.stdout,
recall.stderr
);
assert!(
!recall.stderr.contains("gone"),
"the source must not be reported as a vanished working tree: {}",
recall.stderr
);
let learn = sb.mind_cwd(&["learn", "review"], &elsewhere);
assert!(
learn.success,
"learn from another cwd must succeed: {} {}",
learn.stdout, learn.stderr
);
assert!(sb.claude_home.join("skills/review/SKILL.md").exists());
}
#[test]
fn review_reads_a_relative_two_segment_directory_as_a_local_path() {
let sb = Sandbox::bare("cli214");
let fake_dir = fake_git_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let workdir = sb.base.join("workdir");
write(
&workdir.join("skills/greet/SKILL.md"),
"---\nname: greet\ndescription: Greet the user\n---\n# greet\n",
);
let r = mind_in(
&sb,
&["review", "skills/greet"],
&workdir,
&[("PATH", &new_path)],
);
assert!(
r.success,
"review of an existing relative directory must succeed locally: {} {}",
r.stdout, r.stderr
);
assert!(
!format!("{}{}", r.stdout, r.stderr).contains("Authentication failed"),
"review must not have attempted a clone: {} {}",
r.stdout,
r.stderr
);
assert!(
r.stderr.contains("reviewed as that local path")
&& r.stderr.contains("mind review 'github:skills/greet'"),
"review must say which reading it took and how to force the remote one: {}",
r.stderr
);
assert_no_review_temp(&sb.mind_home);
}
#[test]
fn review_of_a_shadowed_directory_emits_only_the_note_for_the_reading_it_took() {
let sb = Sandbox::bare("cli214-notes");
let fake_dir = fake_git_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let workdir = sb.base.join("workdir");
write(
&workdir.join("skills/greet/SKILL.md"),
"---\nname: greet\ndescription: Greet the user\n---\n# greet\n",
);
let r = mind_in(
&sb,
&["review", "skills/greet"],
&workdir,
&[("PATH", &new_path)],
);
assert!(
r.stderr.contains("reviewed as that local path"),
"the CLI-214 note must be present: {}",
r.stderr
);
assert!(
!r.stderr.contains("use './skills/greet'"),
"the CLI-215 note contradicts the reading review actually took: {}",
r.stderr
);
}
#[test]
fn a_curated_entry_that_shadows_a_local_directory_is_noted_exactly_once() {
let sb = Sandbox::named("shadow-curator");
sb.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"skill\"\nname = \"review\"\npath = \"skills/review\"\n\n\
[[discover.sources]]\nsource = \"skills/greet\"\n\
on-auth-failure = { action = \"skip\" }\n",
);
let fake_dir = fake_git_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let workdir = sb.base.join("standhere");
write(
&workdir.join("skills/greet/SKILL.md"),
"---\nname: greet\ndescription: a decoy that shadows the curated entry\n---\n# greet\n",
);
let spec = sb.source_spec();
let r = mind_in(
&sb,
&["meld", &spec, "--yes"],
&workdir,
&[("PATH", &new_path)],
);
let combined = format!("{}{}", r.stdout, r.stderr);
assert_eq!(
combined.matches("is also a directory here").count(),
1,
"a curated entry that shadows a local directory must be noted exactly \
once (the recursion's real parse), not once more by the DSC-93 \
classification: stdout={} stderr={}",
r.stdout,
r.stderr
);
}
#[test]
fn a_top_level_shadowing_spec_is_noted_exactly_once() {
let sb = Sandbox::bare("top-shadow");
let fake_dir = fake_git_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let workdir = sb.base.join("standhere");
write(
&workdir.join("skills/greet/SKILL.md"),
"---\nname: greet\ndescription: a decoy that shadows the spec\n---\n# greet\n",
);
let r = mind_in(
&sb,
&["meld", "skills/greet", "--register-only"],
&workdir,
&[("PATH", &new_path)],
);
let combined = format!("{}{}", r.stdout, r.stderr);
assert_eq!(
combined.matches("is also a directory here").count(),
1,
"a shadowing top-level spec must be noted exactly once (by the parse \
that decides what to clone), not once per identity lookup: stdout={} \
stderr={}",
r.stdout,
r.stderr
);
assert!(
combined.contains("use './skills/greet'"),
"the one note must still name the local-path form (CLI-215): {combined}"
);
}
#[test]
fn review_registry_selector_still_beats_a_same_named_local_directory() {
let sb = melded(); let decoy = sb.base.join("decoy");
write(
&decoy.join("agents/mind.toml"),
"this is not = = toml {{{\n",
);
write(
&decoy.join("agents/skills/impostor/SKILL.md"),
"---\nname: impostor\ndescription: not the melded source\n---\n# impostor\n",
);
let r = mind_in(&sb, &["review", "agents"], &decoy, &[]);
assert!(
r.success,
"the registry match must win over the same-named local directory (reviewing the decoy \
would hard-fail on its malformed mind.toml): {} {}",
r.stdout, r.stderr
);
assert!(
!format!("{}{}", r.stdout, r.stderr).contains("impostor"),
"the decoy's items must never be reviewed: {} {}",
r.stdout,
r.stderr
);
let d = mind_in(&sb, &["review", "./agents"], &decoy, &[]);
assert!(
!d.success,
"control: the decoy directory really is a broken source: {} {}",
d.stdout, d.stderr
);
}
#[test]
fn meld_notes_a_shadowing_local_directory_before_it_clones() {
let sb = Sandbox::bare("cli215");
let fake_dir = fake_git_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let workdir = sb.base.join("workdir");
write(
&workdir.join("skills/greet/SKILL.md"),
"---\nname: greet\ndescription: Greet the user\n---\n# greet\n",
);
let r = mind_in(
&sb,
&["meld", "skills/greet"],
&workdir,
&[("PATH", &new_path)],
);
assert!(
r.stderr.contains("is also a directory here") && r.stderr.contains("'./skills/greet'"),
"meld must note the shadowing directory and the './' form: {}",
r.stderr
);
assert!(
!r.success,
"the remote reading must still be taken (and fail against the stub git): {} {}",
r.stdout, r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
!sources.stdout.contains("greet"),
"nothing may be registered from the failed remote reading: {}",
sources.stdout
);
}
#[test]
fn meld_prints_no_shadow_note_when_no_such_directory_exists() {
let sb = Sandbox::bare("cli215-quiet");
let fake_dir = fake_git_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let workdir = sb.base.join("empty-workdir");
std::fs::create_dir_all(&workdir).unwrap();
let r = mind_in(
&sb,
&["meld", "skills/greet"],
&workdir,
&[("PATH", &new_path)],
);
assert!(
!r.stderr.contains("is also a directory here"),
"no shadow note when nothing is shadowed: {}",
r.stderr
);
}
fn nested_repo(dir: &Path, skill: &str) {
write(
&dir.join(format!("skills/{skill}/SKILL.md")),
&format!("---\nname: {skill}\ndescription: {skill} skill\n---\n# {skill}\n"),
);
git(dir, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(dir, &["config", "user.email", "t@t"]);
git(dir, &["config", "user.name", "t"]);
git(dir, &["add", "-A"]);
git(dir, &["commit", "-qm", "initial"]);
}
#[test]
fn meld_resolves_a_curated_relative_nested_source_against_the_curator_not_the_cwd() {
let sb = Sandbox::bare("curator");
nested_repo(&sb.base.join("explicit"), "greet");
sb.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"../explicit\"\n",
);
let elsewhere = sb.base.join("far/away");
std::fs::create_dir_all(&elsewhere).unwrap();
assert!(
!elsewhere.join("../explicit").is_dir(),
"the decoy cwd must not resolve '../explicit' to anything"
);
let spec = sb.source_spec();
let r = sb.mind_cwd(&["meld", &spec, "--register-only"], &elsewhere);
assert!(
r.success,
"melding the curator must succeed: {} {}",
r.stdout, r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("explicit"),
"the relative nested source must be registered even though the consumer stood elsewhere: \
{}\nmeld stderr: {}",
sources.stdout,
r.stderr
);
let recall = sb.mind(&["recall"]);
assert!(
recall.stdout.contains("greet"),
"the nested source's item must be offered: {}",
recall.stdout
);
}
#[test]
fn sync_rewalk_resolves_a_newly_curated_relative_nested_source_against_the_curator() {
let sb = Sandbox::bare("curator-sync");
nested_repo(&sb.base.join("first"), "alpha");
sb.write_and_commit("mind.toml", "[[discover.sources]]\nsource = \"../first\"\n");
let elsewhere = sb.base.join("far/away");
std::fs::create_dir_all(&elsewhere).unwrap();
let spec = sb.source_spec();
assert!(
sb.mind_cwd(&["meld", &spec, "--register-only"], &elsewhere)
.success
);
nested_repo(&sb.base.join("second"), "beta");
sb.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"../first\"\n[[discover.sources]]\nsource = \"../second\"\n",
);
let s = sb.mind_cwd(&["sync"], &elsewhere);
assert!(s.success, "sync must succeed: {} {}", s.stdout, s.stderr);
let sources = sb.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("second"),
"sync's re-walk must register the newly curated relative nested source: {sources}\n\
sync stdout: {}",
s.stdout
);
}
#[test]
fn init_source_reports_refs_scaffolds_toml_and_templates() {
let sb = Sandbox::new();
let repo = sb.base.join("authoring");
write(
&repo.join("skills/review/SKILL.md"),
"---\ndescription: review\n---\n# review\nHand off to dev, then see {{ns:style}}.\n",
);
write(
&repo.join("agents/dev.md"),
"---\nname: dev\ndescription: dev\n---\n# dev\n",
);
write(
&repo.join("rules/style.md"),
"---\ndescription: style\n---\n# style\n",
);
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir]);
assert!(r.success, "init-source failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("review") && r.stdout.contains("dev") && r.stdout.contains("style"),
"items and references must be reported: {}",
r.stdout
);
assert!(
!r.stdout.contains("advisory [unguarded-reference]"),
"no prefix => no unguarded-reference advisory (INIT-9): {}",
r.stdout
);
let scaffold = std::fs::read_to_string(repo.join("mind.toml")).unwrap();
assert!(
scaffold.contains("[source]") && scaffold.contains("# namespace = \"prefix\""),
"scaffold must carry a [source] table and a generic commented namespace: {scaffold}"
);
assert!(
!sb.mind_home.join("sources.json").exists(),
"init-source must not write to the store"
);
let toml_before = std::fs::read_to_string(repo.join("mind.toml")).unwrap();
assert!(sb.mind(&["init-source", dir]).success);
assert_eq!(
std::fs::read_to_string(repo.join("mind.toml")).unwrap(),
toml_before,
"an existing mind.toml must not be overwritten"
);
let t = sb.mind(&["init-source", dir, "--template"]);
assert!(
t.success,
"init-source --template failed: {} {}",
t.stdout, t.stderr
);
let review = std::fs::read_to_string(repo.join("skills/review/SKILL.md")).unwrap();
assert!(
review.contains("{{ns:dev}}"),
"the bare `dev` reference must be templated: {review}"
);
assert!(
review.contains("{{ns:style}}"),
"the existing token must survive: {review}"
);
assert!(
!review.contains("to dev,"),
"the bare `dev` must be replaced, not duplicated: {review}"
);
}
#[test]
fn init_source_oversized_pre_existing_mind_toml_is_refused() {
let sb = Sandbox::new();
let repo = sb.base.join("authoring");
write(
&repo.join("mind.toml"),
"[source]\ndescription = \"small\"\n",
);
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir, "--max-metadata-size", "1"]);
assert!(
!r.success,
"a 1-byte cap must refuse init-source's read of an existing mind.toml: {} {}",
r.stdout, r.stderr
);
let combined = format!("{}{}", r.stdout, r.stderr);
assert!(
combined.contains("mind.toml"),
"error must name mind.toml: {combined}"
);
}
#[test]
fn init_source_template_skips_a_bare_mention_in_a_non_markdown_file() {
let sb = Sandbox::new();
let repo = sb.base.join("authoring-md-gate");
write(
&repo.join("skills/review/SKILL.md"),
"---\ndescription: review\n---\n# review\n",
);
write(
&repo.join("skills/review/run.sh"),
"#!/bin/sh\n# hand off to dev\necho dev\n",
);
write(
&repo.join("skills/review/notes.markdown"),
"hand off to dev when done\n",
);
write(
&repo.join("agents/dev.md"),
"---\nname: dev\ndescription: dev\n---\n# dev\n",
);
let dir = repo.to_str().unwrap();
let t = sb.mind(&["init-source", dir, "--template"]);
assert!(
t.success,
"init-source --template failed: {} {}",
t.stdout, t.stderr
);
let script = std::fs::read_to_string(repo.join("skills/review/run.sh")).unwrap();
assert!(
script.contains("hand off to dev") && !script.contains("{{ns:dev}}"),
"a bare mention in a non-markdown file must not be templated: {script}"
);
let notes = std::fs::read_to_string(repo.join("skills/review/notes.markdown")).unwrap();
assert!(
notes.contains("{{ns:dev}}"),
"a bare mention in a recognized non-`.md` markdown extension (.markdown) \
must still be templated: {notes}"
);
}
#[test]
fn a_workflows_unresolvable_token_fails_the_install_and_leaves_nothing() {
let sb = Sandbox::new();
sb.write_and_commit(
"workflows/stray.js",
"export const meta = {\n name: 'stray',\n description: 'Stray token',\n}\n\
agent(`see {{ns:nonesuch}} for the format`)\n",
);
assert!(
sb.mind(&["meld", &sb.source_spec(), "--register-only"])
.success
);
let learn = sb.mind(&["learn", "workflow:stray", "--yes"]);
assert!(
!learn.success,
"an unresolvable token in a workflow's .js must fail the install, not \
install as literal text: {}\n{}",
learn.stdout, learn.stderr
);
let all = format!("{}{}", learn.stdout, learn.stderr);
assert!(
all.contains("nonesuch"),
"the failure must name the bad referent: {all}"
);
assert!(
!sb.mind_home.join("store/workflow/stray").exists(),
"a failed install must leave no store copy"
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("workflows/stray.js")).is_err(),
"and no link"
);
let ok = sb.mind(&["learn", "skill:review", "--yes"]);
assert!(
ok.success,
"a sibling item must still install after the refusal: {}\n{}",
ok.stdout, ok.stderr
);
}
#[test]
fn init_source_template_leaves_a_workflows_js_alone() {
let sb = Sandbox::new();
let repo = sb.base.join("authoring-workflow-gate");
let js = "export const meta = {\n name: 'ship-it',\n description: 'ship',\n}\n\
// hand off to dev when the build is green\nagent('ask dev to review')\n";
write(&repo.join("workflows/ship-it.js"), js);
write(
&repo.join("skills/review/SKILL.md"),
"---\ndescription: review\n---\n# review\nhand off to dev when done\n",
);
write(
&repo.join("agents/dev.md"),
"---\nname: dev\ndescription: dev\n---\n# dev\n",
);
let dir = repo.to_str().unwrap();
let t = sb.mind(&["init-source", dir, "--template"]);
assert!(
t.success,
"init-source --template failed: {} {}",
t.stdout, t.stderr
);
let after = std::fs::read_to_string(repo.join("workflows/ship-it.js")).unwrap();
assert_eq!(
after, js,
"--template must leave a workflow's .js byte-identical (INIT-5), even \
though install DOES expand tokens there (WF-25)"
);
let skill = std::fs::read_to_string(repo.join("skills/review/SKILL.md")).unwrap();
assert!(
skill.contains("{{ns:dev}}"),
"the markdown control must be templated: {skill}"
);
}
#[test]
fn init_source_flags_helper_script_duplicated_across_items() {
let sb = Sandbox::new();
let repo = sb.base.join("authoring");
write(
&repo.join("skills/a/SKILL.md"),
"---\ndescription: a\n---\n# a\n",
);
write(&repo.join("skills/a/helper.sh"), "#!/bin/sh\necho shared\n");
write(
&repo.join("skills/b/SKILL.md"),
"---\ndescription: b\n---\n# b\n",
);
write(&repo.join("skills/b/helper.sh"), "#!/bin/sh\necho shared\n");
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir]);
assert!(r.success, "init-source failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("advisory [duplicate-tooling]") && r.stdout.contains("helper.sh"),
"init-source must surface the duplicate-tooling advisory like review: {}",
r.stdout
);
}
#[test]
fn review_with_no_target_reviews_the_current_directory() {
let sb = Sandbox::new();
let r = sb.mind_cwd(&["review"], &sb.source);
assert!(
r.success,
"a bare `review` of the current directory should succeed for a clean source: {} {}",
r.stdout, r.stderr
);
}
#[test]
fn review_json_answers_with_a_document_not_the_text_output() {
let sb = Sandbox::new();
let plain = sb.mind_cwd(&["review"], &sb.source);
let json = sb.mind_cwd(&["--json", "review"], &sb.source);
assert!(
plain.success && json.success,
"{} {}",
plain.stderr,
json.stderr
);
assert_ne!(
plain.stdout, json.stdout,
"`--json` must change review's output; it is no longer an exclusion"
);
let doc: serde_json::Value = serde_json::from_str(json.stdout.trim())
.unwrap_or_else(|e| panic!("review --json must be one document: {e}: {:?}", json.stdout));
assert_eq!(doc["action"], "review", "{doc}");
}
#[test]
fn init_source_json_flag_is_ignored_and_still_prints_plain_text() {
let sb = Sandbox::bare("init-json");
let dir = sb.base.join("scaffold-target");
std::fs::create_dir_all(&dir).unwrap();
let dir_str = dir.to_str().unwrap();
let plain = sb.mind(&["init-source", dir_str]);
let dir2 = sb.base.join("scaffold-target-json");
std::fs::create_dir_all(&dir2).unwrap();
let dir2_str = dir2.to_str().unwrap();
let json = sb.mind(&["--json", "init-source", dir2_str]);
assert!(
plain.success && json.success,
"{} {}",
plain.stderr,
json.stderr
);
assert!(
serde_json::from_str::<serde_json::Value>(json.stdout.trim()).is_err(),
"init-source --json must not accidentally look like a JSON document: {:?}",
json.stdout
);
assert!(
json.stdout.contains("mind.toml") || json.stdout.contains("wrote"),
"init-source --json must still print its ordinary human report: {:?}",
json.stdout
);
}
#[test]
fn remeld_of_an_uninstalled_source_offers_to_install() {
let sb = melded(); let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "re-meld should not error: {}", r.stderr);
assert!(
r.stdout.contains("already melded"),
"re-meld must report the source is already melded: {}",
r.stdout
);
assert!(
r.stdout.contains("to install"),
"with items uninstalled, re-meld must offer to install them: {}",
r.stdout
);
}
#[test]
fn remeld_of_an_installed_source_shows_item_status() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--yes"]).success,
"initial meld+install"
);
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "re-meld should not error: {}", r.stderr);
assert!(
r.stdout.contains("already melded"),
"re-meld must report the source is already melded: {}",
r.stdout
);
assert!(
r.stdout.contains("skill:review") && r.stdout.contains("installed @"),
"re-meld of a fully installed source must show item status with commits: {}",
r.stdout
);
}
#[test]
fn meld_as_different_prefix_creates_coexisting_instance() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--yes"]).success,
"initial bare meld+install"
);
assert!(
sb.claude_home.join("skills/review").exists(),
"unprefixed skill installs first"
);
let r = sb.mind(&["meld", &spec, "--namespace", "jk", "--register-only"]);
assert!(
r.success,
"second meld under a new prefix must succeed: {} {}",
r.stdout, r.stderr
);
let srcs = sb.mind(&["recall", "--sources"]).stdout;
assert!(
srcs.contains("@jk"),
"the prefixed instance must be registered under its @jk identity: {srcs}"
);
let base = sb.base_name();
assert!(
srcs.matches(&format!("local/{base}/agents")).count() >= 2,
"both the bare and @jk source instances must be listed: {srcs}"
);
let l = sb.mind(&["learn", "jk:review"]);
assert!(
l.success,
"learn the prefixed skill: {} {}",
l.stdout, l.stderr
);
assert!(
sb.claude_home.join("skills/review").exists(),
"the unprefixed skill survives"
);
assert!(
sb.claude_home.join("skills/jk:review").exists(),
"the prefixed instance installs its skill alongside"
);
}
#[test]
fn as_instances_get_independent_clone_dirs() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(
sb.mind(&[
"meld",
&spec,
"--as",
"dev",
"--pin",
"HEAD",
"--register-only"
])
.success,
"first aliased pinned meld"
);
assert!(
sb.mind(&[
"meld",
&spec,
"--as",
"test",
"--pin",
"HEAD",
"--register-only"
])
.success,
"second aliased pinned meld"
);
let base = sb.base_name();
let dev_clone = sb
.mind_home
.join(format!("sources/local/{base}/agents@dev"));
let test_clone = sb
.mind_home
.join(format!("sources/local/{base}/agents@test"));
assert!(
dev_clone.join("skills/review/SKILL.md").exists(),
"the @dev instance has its own clone at {dev_clone:?}"
);
assert!(
test_clone.join("skills/review/SKILL.md").exists(),
"the @test instance has its own clone at {test_clone:?}"
);
assert_ne!(dev_clone, test_clone, "the two clones are distinct dirs");
}
#[test]
fn probe_lists_all_three_kinds() {
let sb = melded();
let r = sb.mind(&["probe"]);
assert!(r.success);
assert!(r.stdout.contains("skill:review"), "{}", r.stdout);
assert!(r.stdout.contains("agent:dev"), "{}", r.stdout);
assert!(r.stdout.contains("rule:style"), "{}", r.stdout);
}
#[test]
fn probe_filters_by_substring() {
let sb = melded();
let r = sb.mind(&["probe", "review"]);
assert!(r.stdout.contains("skill:review"));
assert!(!r.stdout.contains("agent:dev"), "{}", r.stdout);
}
#[test]
fn probe_matches_description_text() {
let sb = melded();
let r = sb.mind(&["probe", "bugs"]);
assert!(r.success, "probe failed: {}", r.stderr);
assert!(
r.stdout.contains("skill:review"),
"expected skill:review in output: {}",
r.stdout
);
assert!(
!r.stdout.contains("agent:dev"),
"unexpected agent:dev in output: {}",
r.stdout
);
}
#[test]
fn probe_query_is_case_insensitive() {
let sb = melded();
let r = sb.mind(&["probe", "REVIEW"]);
assert!(r.success, "probe failed: {}", r.stderr);
assert!(
r.stdout.contains("skill:review"),
"expected skill:review in output: {}",
r.stdout
);
}
#[test]
fn probe_description_query_composes_with_kind_filter() {
let sb = melded();
let r = sb.mind(&["probe", "--kind", "agent", "spec"]);
assert!(r.success, "probe failed: {}", r.stderr);
assert!(
r.stdout.contains("agent:dev"),
"expected agent:dev in output: {}",
r.stdout
);
assert!(
!r.stdout.contains("skill:review"),
"unexpected skill:review in output: {}",
r.stdout
);
}
#[test]
fn probe_description_query_composes_with_source_filter() {
let agents = melded();
let tools = Sandbox::named("tools");
tools.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Deploy onto kubernetes clusters\n---\n# review skill\n",
);
assert!(
agents.mind(&["meld", &tools.source_spec()]).success,
"meld of second source failed"
);
let in_tools = agents.mind(&["probe", "--source", "tools", "kubernetes"]);
assert!(in_tools.success, "probe failed: {}", in_tools.stderr);
assert!(
in_tools.stdout.contains("skill:review"),
"expected skill:review from tools: {}",
in_tools.stdout
);
assert!(
in_tools.stdout.contains("tools"),
"expected the tools source column: {}",
in_tools.stdout
);
let in_agents = agents.mind(&["probe", "--source", "agents", "kubernetes"]);
assert!(in_agents.success, "probe failed: {}", in_agents.stderr);
assert!(
!in_agents.stdout.contains("skill:review"),
"kubernetes must not match any agents item: {}",
in_agents.stdout
);
assert!(
in_agents.stdout.contains("no items match"),
"expected an empty-result note: {}",
in_agents.stdout
);
}
fn melded_two_sources() -> (Sandbox, Sandbox) {
let agents = melded();
let tools = Sandbox::bare("tools");
tools.write_and_commit(
"skills/deploy/SKILL.md",
"---\nname: deploy\ndescription: Ship the build\n---\n# deploy skill\n",
);
assert!(
agents.mind(&["meld", &tools.source_spec()]).success,
"meld of second source failed"
);
(agents, tools)
}
#[test]
fn probe_source_glob_narrows_to_matching_sources() {
let (sb, _tools) = melded_two_sources();
let only_agents = sb.mind(&["probe", "--no-tui", "--source", "*agents"]);
assert!(only_agents.success, "{}", only_agents.stderr);
assert!(
only_agents.stdout.contains("skill:review"),
"expected the agents source's item: {}",
only_agents.stdout
);
assert!(
!only_agents.stdout.contains("skill:deploy"),
"the tools source's item must be excluded: {}",
only_agents.stdout
);
let only_tools = sb.mind(&["probe", "--no-tui", "--source", "*tools"]);
assert!(only_tools.success, "{}", only_tools.stderr);
assert!(
only_tools.stdout.contains("skill:deploy"),
"expected the tools source's item: {}",
only_tools.stdout
);
assert!(
!only_tools.stdout.contains("skill:review"),
"the agents source's item must be excluded: {}",
only_tools.stdout
);
}
#[test]
fn recall_source_glob_narrows_to_matching_sources() {
let (sb, _tools) = melded_two_sources();
let only_agents = sb.mind(&["recall", "--source", "*agents"]);
assert!(only_agents.success, "{}", only_agents.stderr);
assert!(
only_agents.stdout.contains("review"),
"expected the agents source's item: {}",
only_agents.stdout
);
assert!(
!only_agents.stdout.contains("deploy"),
"the tools source's item must be excluded: {}",
only_agents.stdout
);
}
#[test]
fn probe_source_glob_matching_nothing_is_empty() {
let (sb, _tools) = melded_two_sources();
let r = sb.mind(&["probe", "--no-tui", "--source", "*nope"]);
assert!(r.success, "{}", r.stderr);
assert!(
!r.stdout.contains("skill:review") && !r.stdout.contains("skill:deploy"),
"no items should be listed: {}",
r.stdout
);
}
#[test]
fn probe_source_glob_composes_with_json() {
let (sb, _tools) = melded_two_sources();
let r = sb.mind(&["probe", "--no-tui", "--source", "*agents", "--json"]);
assert!(r.success, "{}", r.stderr);
let envelope: serde_json::Value =
serde_json::from_str(&r.stdout).expect("probe --json envelope");
assert_eq!(
envelope["schema"], 1,
"schema field must be 1: {}",
r.stdout
);
let rows = envelope["items"].as_array().expect("items array");
assert!(
rows.iter().any(|row| row["name"] == "review"),
"agents item present in json: {}",
r.stdout
);
assert!(
!rows.iter().any(|row| row["name"] == "deploy"),
"tools item excluded from json: {}",
r.stdout
);
}
#[test]
fn probe_source_glob_composes_with_kind_and_query() {
let (sb, _tools) = melded_two_sources();
let r = sb.mind(&[
"probe", "--no-tui", "--source", "*agents", "--kind", "skill", "review",
]);
assert!(r.success, "{}", r.stderr);
assert!(
r.stdout.contains("skill:review"),
"the item matching source+kind+query must be shown: {}",
r.stdout
);
assert!(
!r.stdout.contains("rule:style"),
"--kind must exclude the rule: {}",
r.stdout
);
assert!(
!r.stdout.contains("agent:dev"),
"--kind must exclude the agent: {}",
r.stdout
);
assert!(
!r.stdout.contains("skill:deploy"),
"--source must exclude the other source's skill: {}",
r.stdout
);
let none = sb.mind(&[
"probe", "--no-tui", "--source", "*agents", "--kind", "skill", "deploy",
]);
assert!(none.success, "{}", none.stderr);
assert!(
!none.stdout.contains("skill:"),
"the query must still exclude non-matching items in the selected source/kind: {}",
none.stdout
);
}
#[test]
fn recall_sources_ignores_source_filter_glob() {
let (sb, _tools) = melded_two_sources();
let agents_full = format!("{}/agents", sb.base_name());
let r = sb.mind(&["recall", "--sources", "--source", "*agents"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains(&agents_full),
"the agents source must be listed: {}",
r.stdout
);
assert!(
r.stdout.contains("/tools"),
"the non-matching tools source must STILL be listed (filter ignored): {}",
r.stdout
);
assert!(
r.stderr.contains("ignored with --sources"),
"a note that the filter is ignored must be printed: {}",
r.stderr
);
}
#[test]
fn probe_no_tui_is_long_only() {
let sb = melded();
let long = sb.mind(&["probe", "--no-tui"]);
assert!(long.success, "{}", long.stderr);
assert!(long.stdout.contains("skill:review"), "{}", long.stdout);
assert!(long.stdout.contains("agent:dev"), "{}", long.stdout);
assert!(long.stdout.contains("rule:style"), "{}", long.stdout);
let short = sb.mind(&["probe", "-n"]);
assert!(!short.success, "probe -n should fail: {}", short.stdout);
}
#[test]
fn probe_query_matches_name_in_one_item_and_description_in_another() {
let sb = Sandbox::named("dual");
sb.write_and_commit(
"skills/audit/SKILL.md",
"---\nname: audit\ndescription: Inspect changes carefully\n---\n# audit\n",
);
sb.write_and_commit(
"agents/dev.md",
"---\nname: dev\ndescription: Run an audit before shipping\n---\n# dev\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let r = sb.mind(&["probe", "audit"]);
assert!(r.success, "probe failed: {}", r.stderr);
assert!(
r.stdout.contains("skill:audit"),
"expected skill:audit (name match): {}",
r.stdout
);
assert!(
r.stdout.contains("agent:dev"),
"expected agent:dev (description match): {}",
r.stdout
);
}
#[test]
fn probe_matches_substring_in_middle_of_word() {
let sb = Sandbox::named("frag");
sb.write_and_commit(
"agents/dev.md",
"---\nname: dev\ndescription: Performs refactoring of modules\n---\n# dev\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let r = sb.mind(&["probe", "factor"]);
assert!(r.success, "probe failed: {}", r.stderr);
assert!(
r.stdout.contains("agent:dev"),
"expected mid-word substring match: {}",
r.stdout
);
}
#[test]
fn learn_installs_and_creates_symlink() {
let sb = melded();
let r = sb.mind(&["learn", "review"]);
assert!(r.success, "{}", r.stderr);
assert!(r.stdout.contains("learned skill:review"));
let link = sb.claude_home.join("skills/review");
let meta = std::fs::symlink_metadata(&link).expect("symlink should exist");
assert!(
meta.file_type().is_symlink(),
"expected a symlink at {link:?}"
);
}
#[test]
fn learn_pin_on_a_plain_ref_is_ignored_with_a_note() {
let sb = melded();
let r = sb.mind(&["learn", "review", "--pin"]);
assert!(
r.success,
"learn --pin on a plain ref should still install: {}",
r.stderr
);
assert!(
r.stderr.contains("--pin is ignored"),
"expected the ignored-pin note on stderr, got: {}",
r.stderr
);
assert!(
sb.claude_home.join("skills/review").exists(),
"the skill must still be installed despite the ignored --pin"
);
}
#[test]
fn learn_force_overwrites_a_conflicting_target() {
let sb = melded();
let link = sb.claude_home.join("rules/style.md");
write(&link, "the user's own file\n");
let r = sb.mind(&["learn", "style"]);
assert!(
!r.success,
"learn must refuse to clobber a foreign target: {}",
r.stdout
);
assert!(
r.stderr.contains("not managed by mind"),
"expected a clobber error: {}",
r.stderr
);
assert!(
!std::fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink(),
"the user's file must be left untouched without --force"
);
let f = sb.mind(&["learn", "style", "--force"]);
assert!(
f.success,
"learn --force should overwrite: {} {}",
f.stdout, f.stderr
);
assert!(f.stdout.contains("learned rule:style"), "{}", f.stdout);
assert!(
std::fs::symlink_metadata(&link)
.expect("link should exist")
.file_type()
.is_symlink(),
"--force must replace the file with mind's symlink"
);
}
#[test]
fn overwrite_accepts_every_conflict_of_a_meld_and_a_learn() {
let plant = |sb: &Sandbox| {
write(&sb.claude_home.join("rules/style.md"), "the user's rule\n");
write(
&sb.claude_home.join("skills/review/SKILL.md"),
"the user's skill\n",
);
};
let is_link = |p: std::path::PathBuf| {
std::fs::symlink_metadata(&p)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
};
let sb = Sandbox::new();
plant(&sb);
let r = sb.mind(&["meld", &sb.source_spec(), "--yes"]);
assert!(
!r.success,
"meld must refuse a foreign target: {}",
r.stdout
);
assert!(r.stderr.contains("not managed by mind"), "{}", r.stderr);
assert!(!is_link(sb.claude_home.join("rules/style.md")));
let sb = Sandbox::new();
plant(&sb);
let r = sb.mind(&["meld", &sb.source_spec(), "--yes", "--overwrite"]);
assert!(r.success, "meld --overwrite: {} {}", r.stdout, r.stderr);
assert!(is_link(sb.claude_home.join("rules/style.md")));
assert!(is_link(sb.claude_home.join("skills/review")));
let sb = melded();
plant(&sb);
let r = sb.mind(&["learn", "style", "--overwrite"]);
assert!(r.success, "learn --overwrite: {} {}", r.stdout, r.stderr);
assert!(is_link(sb.claude_home.join("rules/style.md")));
}
#[test]
fn remeld_overwrite_does_not_rerun_hooks_that_already_ran() {
let sb = Sandbox::bare("remeld-overwrite");
let marker = sb.base.join("hook-ran");
let m = marker.to_str().unwrap().to_owned();
sb.write_and_commit(
"mind.toml",
&format!("[[hooks]]\nrun = \"touch {m}\"\nevent = \"install\"\n"),
);
let spec = sb.source_spec();
let meld = |extra: &str| {
let mut args = vec![
"meld",
spec.as_str(),
"--link-only",
"--accept-hooks=install,update,uninstall",
];
if !extra.is_empty() {
args.push(extra);
}
assert!(sb.mind(&args).success);
};
meld("");
assert!(marker.exists(), "the first meld runs the hook");
std::fs::remove_file(&marker).unwrap();
meld("--overwrite");
assert!(
!marker.exists(),
"--overwrite must not re-run a hook already run at this commit"
);
}
#[test]
fn recall_lists_and_shows_item_details() {
let sb = melded();
sb.mind(&["learn", "review"]);
let list = sb.mind(&["recall"]);
assert!(list.stdout.contains("skill:review"));
let detail = sb.mind(&["recall", "skill:review"]);
assert!(detail.stdout.contains("source "), "{}", detail.stdout);
assert!(detail.stdout.contains("/agents"), "{}", detail.stdout);
assert!(detail.stdout.contains("hash"), "{}", detail.stdout);
}
#[test]
fn learn_unknown_item_errors() {
let sb = melded();
let r = sb.mind(&["learn", "does-not-exist"]);
assert!(!r.success);
assert!(r.stderr.contains("no item matches"), "{}", r.stderr);
}
#[test]
fn introspect_is_clean_after_learn() {
let sb = melded();
sb.mind(&["learn", "review"]);
let r = sb.mind(&["introspect"]);
assert!(r.success);
assert!(r.stdout.contains("all good"), "{}", r.stdout);
}
#[test]
fn upgrade_reports_nothing_when_up_to_date() {
let sb = melded();
sb.mind(&["learn", "review"]);
let r = sb.mind(&["upgrade"]);
assert!(r.stdout.contains("up to date"), "{}", r.stdout);
}
#[test]
fn upgrade_reports_delta_and_declining_changes_nothing() {
let sb = melded();
sb.mind(&["learn", "review"]);
sb.edit_source();
sb.mind(&["sync"]);
let report = sb.mind_with_input(&["upgrade"], Some("n\n"));
assert!(report.stdout.contains("skill:review"), "{}", report.stdout);
assert!(report.stdout.contains("hash"), "{}", report.stdout);
assert!(report.stdout.contains("->"), "{}", report.stdout);
assert!(report.stdout.contains("aborted"), "{}", report.stdout);
let before = sb.mind(&["recall", "skill:review"]).stdout;
let again = sb.mind_with_input(&["upgrade"], Some("n\n"));
assert!(again.stdout.contains("aborted"));
assert_eq!(before, sb.mind(&["recall", "skill:review"]).stdout);
}
#[test]
fn upgrade_prompt_defaults_to_yes_on_bare_enter() {
let sb = melded();
sb.mind(&["learn", "review"]);
let before = sb.mind(&["recall", "skill:review"]).stdout;
sb.edit_source();
sb.mind(&["sync"]);
let applied = sb.mind_with_input(&["upgrade"], Some("\n"));
assert!(applied.success, "{}", applied.stderr);
assert!(
applied.stdout.contains("upgraded skill:review"),
"a bare Enter must apply the upgrade: {}",
applied.stdout
);
assert_ne!(
before,
sb.mind(&["recall", "skill:review"]).stdout,
"the installed commit should have advanced"
);
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\nedited again\n",
);
sb.mind(&["sync"]);
let eof = sb.mind_with_input(&["upgrade"], Some(""));
assert!(
eof.stdout.contains("aborted"),
"EOF must decline: {}",
eof.stdout
);
}
#[test]
fn upgrade_yes_applies_and_is_then_idempotent() {
let sb = melded();
sb.mind(&["learn", "review"]);
let before = sb.mind(&["recall", "skill:review"]).stdout;
sb.edit_source();
sb.mind(&["sync"]);
let applied = sb.mind(&["upgrade", "--yes"]);
assert!(applied.success, "{}", applied.stderr);
assert!(
applied.stdout.contains("upgraded skill:review"),
"{}",
applied.stdout
);
let after = sb.mind(&["recall", "skill:review"]).stdout;
assert_ne!(before, after, "commit/hash should have advanced");
let idem = sb.mind(&["upgrade"]);
assert!(idem.stdout.contains("up to date"), "{}", idem.stdout);
}
#[test]
fn forget_removes_symlink_and_manifest_entry() {
let sb = melded();
sb.mind(&["learn", "review"]);
let r = sb.mind(&["forget", "review"]);
assert!(r.success, "{}", r.stderr);
let link = sb.claude_home.join("skills/review");
assert!(
std::fs::symlink_metadata(&link).is_err(),
"symlink should be gone"
);
assert!(
!sb.mind(&["recall", "review"]).success,
"review should no longer be installed"
);
}
#[test]
fn forget_unknown_item_errors() {
let sb = melded();
let r = sb.mind(&["forget", "review"]);
assert!(!r.success);
assert!(r.stderr.contains("not installed"), "{}", r.stderr);
}
#[test]
fn forget_bare_name_is_ambiguous_across_kinds_and_qualifier_resolves() {
let sb = Sandbox::bare("dup");
sb.write_and_commit(
"skills/dup/SKILL.md",
"---\nname: dup\ndescription: skill dup\n---\n# dup skill\n",
);
sb.write_and_commit(
"agents/dup.md",
"---\nname: dup\ndescription: agent dup\n---\n# dup agent\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
assert!(sb.mind(&["learn", "skill:dup"]).success);
assert!(sb.mind(&["learn", "agent:dup"]).success);
let bare = sb.mind(&["forget", "dup"]);
assert!(!bare.success);
assert!(bare.stderr.contains("ambiguous"), "{}", bare.stderr);
assert!(!sb.mind(&["recall", "dup"]).success);
let wrong = sb.mind(&["forget", "other/repo#skill:dup"]);
assert!(!wrong.success);
assert!(wrong.stderr.contains("not installed"), "{}", wrong.stderr);
assert!(sb.mind(&["forget", "skill:dup"]).success);
assert!(
sb.mind(&["recall", "agent:dup"]).success,
"agent:dup remains installed"
);
assert!(
!sb.mind(&["recall", "skill:dup"]).success,
"skill:dup uninstalled"
);
}
#[test]
fn learn_refuses_to_clobber_a_user_file() {
let sb = melded();
let target = sb.claude_home.join("skills/review");
write(&target.join("MINE.md"), "do not delete me");
let r = sb.mind(&["learn", "review"]);
assert!(!r.success, "learn should refuse to overwrite a user file");
assert!(
r.stderr.contains("managed by mind") || r.stderr.contains("already exists"),
"{}",
r.stderr
);
assert!(target.join("MINE.md").exists(), "user file was deleted");
assert!(!sb.mind(&["recall"]).stdout.contains("installed @"));
}
#[test]
fn relearn_replaces_minds_own_symlink() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let again = sb.mind(&["learn", "review"]);
assert!(again.success, "{}", again.stderr);
}
#[test]
fn probe_surfaces_frontmatter_descriptions() {
let sb = melded();
let r = sb.mind(&["probe"]);
assert!(r.success);
assert!(
r.stdout.contains("Review the diff for bugs"),
"expected skill description in probe output: {}",
r.stdout
);
assert!(
r.stdout.contains("Implements a spec with tests"),
"{}",
r.stdout
);
}
#[test]
fn recall_detail_shows_description() {
let sb = melded();
sb.mind(&["learn", "review"]);
let r = sb.mind(&["recall", "skill:review"]);
assert!(
r.stdout.contains("desc Review the diff for bugs"),
"{}",
r.stdout
);
}
#[test]
fn mind_toml_is_authoritative_and_overrides_link_and_description() {
let sb = Sandbox::new();
sb.write_and_commit(
"guidelines/style.md",
"---\ndescription: from frontmatter\n---\n# house style\n",
);
sb.write_and_commit(
"mind.toml",
concat!(
"[source]\n",
"description = \"a test library\"\n\n",
"[[items]]\n",
"kind = \"rule\"\n",
"name = \"style\"\n",
"path = \"guidelines/style.md\"\n",
"link = \"rules/custom-style.md\"\n",
"description = \"override wins\"\n",
),
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let probe = sb.mind(&["probe"]);
assert!(probe.stdout.contains("rule:style"), "{}", probe.stdout);
assert!(!probe.stdout.contains("skill:review"), "{}", probe.stdout);
assert!(probe.stdout.contains("override wins"), "{}", probe.stdout);
assert!(
!probe.stdout.contains("from frontmatter"),
"{}",
probe.stdout
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("a test library"),
"{}",
sources.stdout
);
assert!(sb.mind(&["learn", "style"]).success);
let link = sb.claude_home.join("rules/custom-style.md");
let meta = std::fs::symlink_metadata(&link).expect("custom link should exist");
assert!(meta.file_type().is_symlink());
}
#[test]
fn mind_toml_discover_globs_find_items() {
let sb = Sandbox::new();
sb.write_and_commit(
"packages/foo/SKILL.md",
"---\ndescription: a glob-found skill\n---\n# foo\n",
);
sb.write_and_commit(
"mind.toml",
"[discover]\nskills = { include = [\"packages/*/SKILL.md\"] }\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let probe = sb.mind(&["probe"]);
assert!(probe.stdout.contains("skill:foo"), "{}", probe.stdout);
assert!(
probe.stdout.contains("a glob-found skill"),
"{}",
probe.stdout
);
assert!(!probe.stdout.contains("skill:review"), "{}", probe.stdout);
}
#[test]
fn mind_toml_discover_exclude_drops_matches() {
let sb = Sandbox::new();
sb.write_and_commit(
"packages/foo/SKILL.md",
"---\ndescription: foo\n---\n# foo\n",
);
sb.write_and_commit(
"packages/internal-x/SKILL.md",
"---\ndescription: internal\n---\n# internal\n",
);
sb.write_and_commit(
"mind.toml",
concat!(
"[discover.skills]\n",
"include = [\"packages/*/SKILL.md\"]\n",
"exclude = [\"packages/internal-*/SKILL.md\"]\n",
),
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let probe = sb.mind(&["probe"]);
assert!(probe.stdout.contains("skill:foo"), "{}", probe.stdout);
assert!(
!probe.stdout.contains("skill:internal-x"),
"{}",
probe.stdout
);
}
#[test]
fn super_source_recursively_melds_listed_sources() {
let tools = Sandbox::named("tools"); let registry = Sandbox::bare("registry"); registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
tools.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(r.success, "{}", r.stderr);
let probe = registry.mind(&["probe"]);
assert!(probe.stdout.contains("skill:review"), "{}", probe.stdout);
let sources = registry.mind(&["recall", "--sources"]);
assert!(sources.stdout.contains("tools"), "{}", sources.stdout);
assert!(sources.stdout.contains("registry"), "{}", sources.stdout);
assert!(registry.mind(&["learn", "review"]).success);
}
#[test]
fn super_source_applies_nested_alias() {
let tools = Sandbox::named("tools");
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\", as = \"tl\" }}]\n",
tools.source_spec()
),
);
let spec = registry.source_spec();
assert!(registry.mind(&["meld", &spec]).success);
let probe = registry.mind(&["probe"]);
assert!(probe.stdout.contains("skill:tl:review"), "{}", probe.stdout);
}
#[test]
fn on_auth_failure_field_accepted_in_super_source() {
let tools = Sandbox::named("tools");
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\", on-auth-failure = {{ action = \"skip\", message = \"Configure credentials: https://example.com/auth\" }} }}]\n",
tools.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(
r.success,
"meld of a super-source with on-auth-failure must succeed: {}",
r.stderr
);
let probe = registry.mind(&["probe"]);
assert!(probe.stdout.contains("skill:review"), "{}", probe.stdout);
let sources = registry.mind(&["recall", "--sources"]);
assert!(sources.stdout.contains("tools"), "{}", sources.stdout);
}
#[test]
fn on_auth_failure_invalid_action_rejected_in_super_source() {
let tools = Sandbox::named("tools");
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\", on-auth-failure = {{ action = \"warn\" }} }}]\n",
tools.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(
!r.success,
"an invalid on-auth-failure action must fail the meld"
);
assert!(
r.stderr.contains("on-auth-failure") || r.stderr.contains("expected 'error' or 'skip'"),
"error must explain the invalid action: {}",
r.stderr
);
}
fn fake_git_bin_dir(dir: &Path) -> PathBuf {
let real_git = String::from_utf8(
std::process::Command::new("which")
.arg("git")
.output()
.expect("which git")
.stdout,
)
.expect("utf8")
.trim()
.to_string();
let bin_dir = dir.join("fake-git-bin");
std::fs::create_dir_all(&bin_dir).unwrap();
let script = format!(
"#!/bin/sh\nif [ \"$1\" = \"clone\" ]; then\n for a; do\n case \"$a\" in\n https://*)\n echo \"fatal: Authentication failed for '$a'\" >&2\n exit 128\n ;;\n esac\n done\nfi\nexec \"{real_git}\" \"$@\"\n"
);
let script_path = bin_dir.join("git");
std::fs::write(&script_path, &script).unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755)).unwrap();
bin_dir
}
fn prepend_path(extra_dir: &Path) -> String {
let current = std::env::var("PATH").unwrap_or_default();
if current.is_empty() {
extra_dir.display().to_string()
} else {
format!("{}:{}", extra_dir.display(), current)
}
}
#[test]
fn on_auth_failure_skip_continues_meld() {
let registry = Sandbox::bare("registry");
let fake_dir = fake_git_bin_dir(®istry.base);
let new_path = prepend_path(&fake_dir);
registry.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-repo\"\non-auth-failure = { action = \"skip\" }\n",
);
let spec = registry.source_spec();
let r = registry.mind_env(&["meld", &spec], &[("PATH", &new_path)]);
assert!(
r.success,
"skip action must not abort meld: stderr={}",
r.stderr
);
let sources = registry.mind(&["recall", "--sources"]);
assert!(
!sources.stdout.contains("private-repo"),
"skipped source must not be registered: {}",
sources.stdout
);
assert!(
r.stderr.contains("unable to meld source") && r.stderr.contains("skipping"),
"auth failure warning must appear on stderr: {}",
r.stderr
);
}
#[test]
fn on_auth_failure_skip_json_output_has_skipped_array() {
let registry = Sandbox::bare("registry");
let fake_dir = fake_git_bin_dir(®istry.base);
let new_path = prepend_path(&fake_dir);
registry.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-repo\"\non-auth-failure = { action = \"skip\" }\n",
);
let spec = registry.source_spec();
let r = registry.mind_env(&["meld", &spec, "--json"], &[("PATH", &new_path)]);
assert!(
r.success,
"skip action --json must exit zero: stderr={}",
r.stderr
);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "meld", "action field: {}", r.stdout);
assert_eq!(v["outcome"], "melded", "outcome field: {}", r.stdout);
let skipped = v["skipped"].as_array().expect("skipped must be an array");
assert_eq!(skipped.len(), 1, "exactly one skipped entry: {}", r.stdout);
assert_eq!(
skipped[0]["reason"], "auth_failure",
"reason must be auth_failure: {}",
r.stdout
);
assert!(
skipped[0]["source"]
.as_str()
.unwrap_or("")
.contains("private-repo"),
"source must name the skipped repo: {}",
r.stdout
);
assert!(
r.stderr.contains("unable to meld source"),
"warning must appear on stderr under --json too: {}",
r.stderr
);
}
#[test]
fn on_auth_failure_error_fails_meld() {
let registry = Sandbox::bare("registry");
let fake_dir = fake_git_bin_dir(®istry.base);
let new_path = prepend_path(&fake_dir);
registry.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-repo\"\non-auth-failure = { action = \"error\" }\n",
);
let spec = registry.source_spec();
let r = registry.mind_env(&["meld", &spec], &[("PATH", &new_path)]);
assert!(
!r.success,
"error action must cause non-zero exit: stdout={} stderr={}",
r.stdout, r.stderr
);
}
#[test]
fn on_auth_failure_error_prints_message() {
let registry = Sandbox::bare("registry");
let fake_dir = fake_git_bin_dir(®istry.base);
let new_path = prepend_path(&fake_dir);
registry.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-repo\"\non-auth-failure = { action = \"error\", message = \"Configure credentials.\" }\n",
);
let spec = registry.source_spec();
let r = registry.mind_env(&["meld", &spec], &[("PATH", &new_path)]);
assert!(!r.success, "error action must fail: {}", r.stderr);
assert!(
r.stderr.contains("unable to meld source"),
"standard auth-failure line must appear: {}",
r.stderr
);
assert!(
r.stderr.contains("Configure credentials."),
"curator message must appear on stderr: {}",
r.stderr
);
let rj = registry.mind_env(&["meld", &spec, "--json"], &[("PATH", &new_path)]);
assert!(!rj.success, "error action --json must fail: {}", rj.stderr);
assert!(
rj.stderr.contains("unable to meld source"),
"standard auth-failure line must appear under --json: {}",
rj.stderr
);
assert!(
rj.stderr.contains("Configure credentials."),
"curator message must appear on stderr under --json: {}",
rj.stderr
);
}
#[test]
fn on_auth_failure_error_json_emits_error_envelope() {
let registry = Sandbox::bare("registry");
let fake_dir = fake_git_bin_dir(®istry.base);
let new_path = prepend_path(&fake_dir);
registry.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-repo\"\non-auth-failure = { action = \"error\" }\n",
);
let spec = registry.source_spec();
let r = registry.mind_env(&["meld", &spec, "--json"], &[("PATH", &new_path)]);
assert!(
!r.success,
"error action --json must exit non-zero: {}",
r.stderr
);
let v = parse_json(&r.stdout);
assert_eq!(v["schema"], 1, "schema must be 1: {}", r.stdout);
assert!(
v["error"]["kind"].is_string(),
"error envelope must have a kind: {}",
r.stdout
);
assert!(
v["error"]["message"]
.as_str()
.is_some_and(|s| !s.is_empty()),
"error envelope must have a non-empty message: {}",
r.stdout
);
assert!(
r.stderr.contains("unable to meld source"),
"auth-failure warning must appear on stderr: {}",
r.stderr
);
}
#[test]
fn on_auth_failure_absent_propagates_as_generic_error() {
let registry = Sandbox::bare("registry");
let fake_dir = fake_git_bin_dir(®istry.base);
let new_path = prepend_path(&fake_dir);
registry.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/unconfigured-private\"\n",
);
let spec = registry.source_spec();
let r = registry.mind_env(&["meld", &spec], &[("PATH", &new_path)]);
assert!(
!r.success,
"auth failure without on-auth-failure must fail: {}",
r.stderr
);
assert!(
!r.stderr.contains("unable to meld source"),
"no structured auth-failure message without on-auth-failure config: {}",
r.stderr
);
}
#[test]
fn on_auth_failure_multiple_nested_sources_all_skipped() {
let toml_body = "[[discover.sources]]\nsource = \"https://example.com/owner/private-one\"\non-auth-failure = { action = \"skip\" }\n\n[[discover.sources]]\nsource = \"https://example.com/owner/private-two\"\non-auth-failure = { action = \"skip\" }\n";
{
let registry = Sandbox::bare("registry");
let fake_dir = fake_git_bin_dir(®istry.base);
let new_path = prepend_path(&fake_dir);
registry.write_and_commit("mind.toml", toml_body);
let spec = registry.source_spec();
let r = registry.mind_env(&["meld", &spec], &[("PATH", &new_path)]);
assert!(
r.success,
"two skipped nested sources must not abort meld: {}",
r.stderr
);
assert_eq!(
r.stderr.matches("unable to meld source").count(),
2,
"one warning per skipped source: {}",
r.stderr
);
}
{
let registry = Sandbox::bare("registry");
let fake_dir = fake_git_bin_dir(®istry.base);
let new_path = prepend_path(&fake_dir);
registry.write_and_commit("mind.toml", toml_body);
let spec = registry.source_spec();
let rj = registry.mind_env(&["meld", &spec, "--json"], &[("PATH", &new_path)]);
assert!(rj.success, "meld --json must succeed: {}", rj.stderr);
let v = parse_json(&rj.stdout);
let skipped = v["skipped"].as_array().expect("skipped must be an array");
assert_eq!(
skipped.len(),
2,
"two skipped sources must produce two skipped entries: {}",
rj.stdout
);
for entry in skipped {
assert_eq!(
entry["reason"], "auth_failure",
"each entry must have reason auth_failure: {}",
rj.stdout
);
}
}
}
#[test]
fn on_auth_failure_skip_during_sync_rewalk() {
let registry = Sandbox::bare("registry");
let fake_dir = fake_git_bin_dir(®istry.base);
let new_path = prepend_path(&fake_dir);
registry.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-repo\"\non-auth-failure = { action = \"skip\" }\n",
);
let spec = registry.source_spec();
let rm = registry.mind_env(&["meld", &spec], &[("PATH", &new_path)]);
assert!(rm.success, "initial meld must succeed: {}", rm.stderr);
let sources_after_meld = registry.mind(&["recall", "--sources"]).stdout;
assert!(
!sources_after_meld.contains("private-repo"),
"private-repo must not be registered after skipped initial meld: {}",
sources_after_meld
);
let rs = registry.mind_env(&["sync"], &[("PATH", &new_path)]);
assert!(
rs.success,
"sync must complete when nested auth-failure has action=skip: stderr={}",
rs.stderr
);
let sources_after_sync = registry.mind(&["recall", "--sources"]).stdout;
assert!(
!sources_after_sync.contains("private-repo"),
"skipped source must remain unregistered after sync: {}",
sources_after_sync
);
assert!(
rs.stderr.contains("unable to meld source") && rs.stderr.contains("skipping"),
"auth-failure skip warning must appear on stderr during sync: {}",
rs.stderr
);
}
#[test]
fn on_auth_failure_skip_sync_json_has_skipped_array() {
let registry = Sandbox::bare("registry");
let fake_dir = fake_git_bin_dir(®istry.base);
let new_path = prepend_path(&fake_dir);
registry.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-repo\"\non-auth-failure = { action = \"skip\" }\n",
);
let spec = registry.source_spec();
let rm = registry.mind_env(&["meld", &spec], &[("PATH", &new_path)]);
assert!(rm.success, "initial meld must succeed: {}", rm.stderr);
let rs = registry.mind_env(&["sync", "--json"], &[("PATH", &new_path)]);
assert!(
rs.success,
"sync --json must exit zero with skip: {}",
rs.stderr
);
let v = parse_json(&rs.stdout);
assert_eq!(v["action"], "sync", "action field: {}", rs.stdout);
assert_eq!(v["outcome"], "synced", "outcome field: {}", rs.stdout);
let skipped = v["skipped"].as_array().expect("skipped must be an array");
assert_eq!(
skipped.len(),
1,
"one skipped entry in sync result: {}",
rs.stdout
);
assert_eq!(
skipped[0]["reason"], "auth_failure",
"reason must be auth_failure: {}",
rs.stdout
);
assert!(
skipped[0]["source"]
.as_str()
.unwrap_or("")
.contains("private-repo"),
"source must name the skipped repo: {}",
rs.stdout
);
assert!(
rs.stderr.contains("unable to meld source"),
"auth-failure warning must appear on stderr under sync --json: {}",
rs.stderr
);
}
#[test]
fn on_auth_failure_descendant_failure_propagates() {
let a = Sandbox::bare("source_a");
a.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-b\"\n",
);
let a_spec = a.source_spec();
let t = Sandbox::bare("super_t");
let toml = format!(
"[[discover.sources]]\nsource = \"{a_spec}\"\non-auth-failure = {{ action = \"skip\" }}\n"
);
t.write_and_commit("mind.toml", &toml);
let t_spec = t.source_spec();
let fake_dir = fake_git_bin_dir(&t.base);
let new_path = prepend_path(&fake_dir);
let r = t.mind_env(&["meld", &t_spec], &[("PATH", &new_path)]);
assert!(
!r.success,
"B's auth failure must exit non-zero, not be absorbed by A's on-auth-failure: stderr={}",
r.stderr
);
assert!(
!r.stderr.contains("unable to meld source"),
"B's failure must not be reported as A's auth failure: {}",
r.stderr
);
let sources = t.mind(&["recall", "--sources"]);
assert!(
!sources.stdout.contains("source_a"),
"A must not remain registered after the failed meld: {}",
sources.stdout
);
}
#[test]
fn on_auth_failure_descendant_failure_propagates_during_sync() {
let a = Sandbox::bare("source_a");
let a_spec = a.source_spec();
let t = Sandbox::bare("super_t");
let toml = format!(
"[[discover.sources]]\nsource = \"{a_spec}\"\non-auth-failure = {{ action = \"skip\" }}\n"
);
t.write_and_commit("mind.toml", &toml);
let t_spec = t.source_spec();
let rm = t.mind(&["meld", &t_spec]);
assert!(rm.success, "initial meld must succeed: {}", rm.stderr);
let sources_after_meld = t.mind(&["recall", "--sources"]).stdout;
assert!(
sources_after_meld.contains("source_a"),
"A must be registered after meld: {}",
sources_after_meld
);
a.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-b\"\n",
);
let fake_dir = fake_git_bin_dir(&t.base);
let new_path = prepend_path(&fake_dir);
let rs = t.mind_env(&["sync"], &[("PATH", &new_path)]);
assert!(
!rs.success,
"B's auth failure during sync re-walk must exit non-zero: stderr={}",
rs.stderr
);
assert!(
!rs.stderr.contains("unable to meld source"),
"B's failure must not be attributed to A via the DSC-69 message: {}",
rs.stderr
);
}
#[test]
fn on_auth_failure_descendant_failure_propagates_during_sync_with_error_action() {
let a = Sandbox::bare("source_a");
let a_spec = a.source_spec();
let t = Sandbox::bare("super_t");
let toml = format!(
"[[discover.sources]]\nsource = \"{a_spec}\"\non-auth-failure = {{ action = \"error\", message = \"A-LEVEL-CREDS-HINT\" }}\n"
);
t.write_and_commit("mind.toml", &toml);
let t_spec = t.source_spec();
let rm = t.mind(&["meld", &t_spec]);
assert!(rm.success, "initial meld must succeed: {}", rm.stderr);
assert!(
t.mind(&["recall", "--sources"]).stdout.contains("source_a"),
"A must be registered after meld"
);
a.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-b\"\n",
);
let fake_dir = fake_git_bin_dir(&t.base);
let new_path = prepend_path(&fake_dir);
let rs = t.mind_env(&["sync"], &[("PATH", &new_path)]);
assert!(
!rs.success,
"B's auth failure during sync re-walk must exit non-zero with A's error action: stderr={}",
rs.stderr
);
assert!(
!rs.stderr.contains("unable to meld source"),
"B's failure must not be reported via A's DSC-69 auth-failure line: {}",
rs.stderr
);
assert!(
!rs.stderr.contains("A-LEVEL-CREDS-HINT"),
"A's curator message must not be printed for B's descendant failure: {}",
rs.stderr
);
}
#[test]
fn on_auth_failure_descendant_failure_during_sync_json_is_well_formed() {
let a = Sandbox::bare("source_a");
let a_spec = a.source_spec();
let t = Sandbox::bare("super_t");
let toml = format!(
"[[discover.sources]]\nsource = \"{a_spec}\"\non-auth-failure = {{ action = \"skip\" }}\n"
);
t.write_and_commit("mind.toml", &toml);
let t_spec = t.source_spec();
let rm = t.mind(&["meld", &t_spec]);
assert!(rm.success, "initial meld must succeed: {}", rm.stderr);
a.write_and_commit(
"mind.toml",
"[[discover.sources]]\nsource = \"https://example.com/owner/private-b\"\n",
);
let fake_dir = fake_git_bin_dir(&t.base);
let new_path = prepend_path(&fake_dir);
let rs = t.mind_env(&["sync", "--json"], &[("PATH", &new_path)]);
assert!(
!rs.success,
"descendant failure under sync --json must exit non-zero: stderr={}",
rs.stderr
);
let out = rs.stdout.trim();
if !out.is_empty() {
let _: serde_json::Value =
serde_json::from_str(out).expect("any sync --json stdout must be well-formed JSON");
}
assert!(
!rs.stderr.contains("unable to meld source"),
"B's failure must not be reported via A's DSC-69 auth-failure line: {}",
rs.stderr
);
}
const UNREACHABLE_SOURCE: &str = "/nonexistent/mind-test-source-does-not-exist";
#[test]
fn meld_nested_clone_failure_degrades_gracefully() {
let tools = Sandbox::named("tools");
let super_src = Sandbox::named("super");
let toml = format!(
"[[discover.sources]]\nsource = \"{}\"\n\n[[discover.sources]]\nsource = \"{}\"\n",
tools.source_spec(),
UNREACHABLE_SOURCE
);
super_src.write_and_commit("mind.toml", &toml);
let spec = super_src.source_spec();
let r = super_src.mind(&["meld", &spec]);
assert!(
r.success,
"a non-auth nested clone failure must not abort the meld: stderr={}",
r.stderr
);
let sources = super_src.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("tools"),
"the reachable nested source must remain registered: {sources}"
);
assert!(
sources.contains("super"),
"the primary super-source must remain registered: {sources}"
);
assert!(
!sources.contains("mind-test-source-does-not-exist"),
"the unreachable nested source must not be registered: {sources}"
);
assert!(
r.stderr.contains("skipping") && r.stderr.contains("clone"),
"a clone-failure warning must appear on stderr: {}",
r.stderr
);
}
#[test]
fn meld_nested_clone_failure_json_skipped() {
let tools = Sandbox::named("tools");
let super_src = Sandbox::named("super");
let toml = format!(
"[[discover.sources]]\nsource = \"{}\"\n\n[[discover.sources]]\nsource = \"{}\"\n",
tools.source_spec(),
UNREACHABLE_SOURCE
);
super_src.write_and_commit("mind.toml", &toml);
let spec = super_src.source_spec();
let r = super_src.mind(&["meld", &spec, "--json"]);
assert!(r.success, "meld --json must exit zero: stderr={}", r.stderr);
let v = parse_json(&r.stdout);
let skipped = v["skipped"].as_array().expect("skipped must be an array");
assert_eq!(
skipped.len(),
1,
"exactly one skipped entry expected: {}",
r.stdout
);
assert_eq!(
skipped[0]["reason"], "clone_failure",
"reason must be clone_failure: {}",
r.stdout
);
assert!(
skipped[0]["source"]
.as_str()
.unwrap_or("")
.contains("mind-test-source-does-not-exist"),
"the skipped entry must name the unreachable source: {}",
r.stdout
);
}
#[test]
fn meld_curator_all_nested_fail_hard_fails() {
let super_src = Sandbox::bare("curator");
let toml = format!("[[discover.sources]]\nsource = \"{UNREACHABLE_SOURCE}\"\n");
super_src.write_and_commit("mind.toml", &toml);
let spec = super_src.source_spec();
let r = super_src.mind(&["meld", &spec]);
assert!(
!r.success,
"a curator with zero own items and all nested sources failing must hard-fail: stdout={} stderr={}",
r.stdout, r.stderr
);
}
#[test]
fn meld_curator_one_nested_succeeds_passes() {
let tools = Sandbox::named("tools");
let super_src = Sandbox::bare("curator");
let toml = format!(
"[[discover.sources]]\nsource = \"{}\"\n\n[[discover.sources]]\nsource = \"{}\"\n",
tools.source_spec(),
UNREACHABLE_SOURCE
);
super_src.write_and_commit("mind.toml", &toml);
let spec = super_src.source_spec();
let r = super_src.mind(&["meld", &spec]);
assert!(
r.success,
"a curator with one nested source registered must succeed: stderr={}",
r.stderr
);
let sources = super_src.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("curator"),
"the curator must be registered: {sources}"
);
assert!(
sources.contains("tools"),
"the reachable nested source must be registered: {sources}"
);
}
#[test]
fn meld_nested_clone_failure_descendant_propagates() {
let a = Sandbox::bare("source_a");
a.write_and_commit(
"mind.toml",
&format!("[[discover.sources]]\nsource = \"{UNREACHABLE_SOURCE}\"\n"),
);
let a_spec = a.source_spec();
let t = Sandbox::named("super_t");
t.write_and_commit(
"mind.toml",
&format!("[[discover.sources]]\nsource = \"{a_spec}\"\n"),
);
let t_spec = t.source_spec();
let r = t.mind(&["meld", &t_spec]);
assert!(
!r.success,
"A's descendant failure must propagate as a hard error, not be absorbed as a skip: stderr={}",
r.stderr
);
assert!(
!r.stderr.contains("skipping 'source_a'"),
"A's failure must not be misattributed as a clone_failure skip of A: {}",
r.stderr
);
let sources = t.mind(&["recall", "--sources"]).stdout;
assert!(
!sources.contains("source_a"),
"A must not remain registered after the failed meld: {sources}"
);
}
#[test]
fn meld_curator_all_nested_fail_error_message() {
let super_src = Sandbox::bare("curator-msg");
let toml = format!("[[discover.sources]]\nsource = \"{UNREACHABLE_SOURCE}\"\n");
super_src.write_and_commit("mind.toml", &toml);
let spec = super_src.source_spec();
let r = super_src.mind(&["meld", &spec]);
assert!(
!r.success,
"curator with all nested failing must exit non-zero: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("produced no discoverable items")
|| r.stderr.contains("no items of its own"),
"CuratorAllNestedFailed error message must appear on stderr: {}",
r.stderr
);
assert!(
r.stderr.contains("curator-msg"),
"error message must name the super-source: {}",
r.stderr
);
}
#[test]
fn meld_non_curator_all_nested_fail_still_succeeds() {
let super_src = Sandbox::named("non-curator");
let toml = format!("[[discover.sources]]\nsource = \"{UNREACHABLE_SOURCE}\"\n");
super_src.write_and_commit("mind.toml", &toml);
let spec = super_src.source_spec();
let r = super_src.mind(&["meld", &spec]);
assert!(
r.success,
"a source with own items must succeed even when all nested sources fail: stderr={}",
r.stderr
);
let sources = super_src.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("non-curator"),
"primary source must be registered: {sources}"
);
assert!(
!sources.contains("mind-test-source-does-not-exist"),
"the unreachable nested source must not be registered: {sources}"
);
assert!(
r.stderr.contains("skipping") && r.stderr.contains("clone"),
"a clone-failure warning must appear even for a non-curator: {}",
r.stderr
);
}
#[test]
fn sync_nested_clone_failure_is_skipped() {
let super_src = Sandbox::named("rewalk-host");
let spec = super_src.source_spec();
let r = super_src.mind(&["meld", &spec]);
assert!(r.success, "initial meld must succeed: {}", r.stderr);
let toml = format!("[[discover.sources]]\nsource = \"{UNREACHABLE_SOURCE}\"\n");
super_src.write_and_commit("mind.toml", &toml);
let rs = super_src.mind(&["sync"]);
assert!(
rs.success,
"sync must exit zero when a newly-listed nested source fails to clone: stderr={}",
rs.stderr
);
assert!(
rs.stderr.contains("skipping") && rs.stderr.contains("clone"),
"a clone-failure warning must appear on stderr during sync re-walk: {}",
rs.stderr
);
let sources = super_src.mind(&["recall", "--sources"]).stdout;
assert!(
!sources.contains("mind-test-source-does-not-exist"),
"unreachable source must not be registered after sync re-walk: {sources}"
);
}
#[test]
fn sync_nested_clone_failure_json_has_skipped_entry() {
let super_src = Sandbox::named("rewalk-json");
let spec = super_src.source_spec();
let r = super_src.mind(&["meld", &spec]);
assert!(r.success, "initial meld must succeed: {}", r.stderr);
let toml = format!("[[discover.sources]]\nsource = \"{UNREACHABLE_SOURCE}\"\n");
super_src.write_and_commit("mind.toml", &toml);
let rs = super_src.mind(&["sync", "--json"]);
assert!(
rs.success,
"sync --json must exit zero on nested clone failure during re-walk: stderr={}",
rs.stderr
);
let v = parse_json(&rs.stdout);
assert_eq!(v["action"], "sync", "action field: {}", rs.stdout);
let skipped = v["skipped"].as_array().expect("skipped must be an array");
assert_eq!(
skipped.len(),
1,
"exactly one skipped entry expected in sync --json result: {}",
rs.stdout
);
assert_eq!(
skipped[0]["reason"], "clone_failure",
"reason must be clone_failure: {}",
rs.stdout
);
assert!(
skipped[0]["source"]
.as_str()
.unwrap_or("")
.contains("mind-test-source-does-not-exist"),
"skipped entry must name the unreachable source: {}",
rs.stdout
);
}
#[test]
fn super_source_meld_is_cycle_safe() {
let a = Sandbox::bare("aa");
let b = Sandbox::bare("bb");
a.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
b.source_spec()
),
);
b.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
a.source_spec()
),
);
let spec = a.source_spec();
let r = a.mind(&["meld", &spec]);
assert!(r.success, "{}", r.stderr);
}
#[test]
fn super_source_meld_breaks_multi_level_cycle() {
let a = Sandbox::bare("aa");
let b = Sandbox::bare("bb");
let c = Sandbox::bare("cc");
a.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
b.source_spec()
),
);
b.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
c.source_spec()
),
);
c.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
a.source_spec()
),
);
let spec = a.source_spec();
let r = a.mind(&["meld", &spec]);
assert!(r.success, "a cyclic chain must terminate: {}", r.stderr);
assert_eq!(
r.stdout.matches("melding").count(),
3,
"each source melds exactly once: {}",
r.stdout
);
let recall = a.mind(&["recall", "--sources", "--json"]).stdout;
for name in ["aa", "bb", "cc"] {
assert_eq!(
recall.matches(&format!("\"repo\": \"{name}\"")).count(),
1,
"{name} must be registered exactly once: {recall}"
);
}
}
#[test]
fn super_source_meld_does_not_auto_install_nested_items() {
let tools = Sandbox::named("tools"); let registry = Sandbox::bare("registry"); registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
tools.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(r.success, "{}", r.stderr);
assert!(
registry.mind(&["probe"]).stdout.contains("skill:review"),
"the curated source's items must be available"
);
assert!(
!registry.claude_home.join("skills/review").exists(),
"a curated super-source must not auto-install the nested chain's items"
);
assert!(registry.mind(&["learn", "review"]).success);
assert!(registry.claude_home.join("skills/review").exists());
}
#[test]
fn meld_recursive_installs_nested_items() {
let tools = Sandbox::named("tools"); let registry = Sandbox::bare("registry"); registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
tools.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec, "--recursive", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
registry.claude_home.join("skills/review").exists(),
"the nested source's items must install with --recursive"
);
}
#[test]
fn meld_recursive_short_flag_installs_nested_items() {
let tools = Sandbox::named("tools");
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
tools.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec, "-r", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
registry.claude_home.join("skills/review").exists(),
"-r must install the nested source's items"
);
}
#[test]
fn remeld_recursive_installs_nested_chain() {
let tools = Sandbox::named("tools");
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
tools.source_spec()
),
);
let spec = registry.source_spec();
assert!(registry.mind(&["meld", &spec]).success);
assert!(!registry.claude_home.join("skills/review").exists());
let r = registry.mind(&["meld", &spec, "--recursive", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
registry.claude_home.join("skills/review").exists(),
"a re-meld must honor --recursive"
);
}
#[test]
fn meld_installs_curator_flagged_nested_source_without_recursive() {
let want = Sandbox::bare("want"); want.write_and_commit(
"skills/want-skill/SKILL.md",
"---\nname: want-skill\ndescription: wanted\n---\n# want\n",
);
let skip = Sandbox::bare("skip"); skip.write_and_commit(
"skills/skip-skill/SKILL.md",
"---\nname: skip-skill\ndescription: skipped\n---\n# skip\n",
);
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\", install = true }}, {{ source = \"{}\" }}]\n",
want.source_spec(),
skip.source_spec()
),
);
let r = registry.mind(&["meld", ®istry.source_spec(), "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let sources = registry.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("/want") && sources.contains("/skip"),
"both nested sources should be registered: {sources}"
);
assert!(
registry.claude_home.join("skills/want-skill").exists(),
"the install=true nested source's item must be installed"
);
assert!(
!registry.claude_home.join("skills/skip-skill").exists(),
"the unflagged nested source's item must not be auto-installed"
);
}
#[test]
fn meld_recursive_installs_even_unflagged_nested_sources() {
let want = Sandbox::bare("want");
want.write_and_commit(
"skills/want-skill/SKILL.md",
"---\nname: want-skill\ndescription: wanted\n---\n# want\n",
);
let skip = Sandbox::bare("skip");
skip.write_and_commit(
"skills/skip-skill/SKILL.md",
"---\nname: skip-skill\ndescription: skipped\n---\n# skip\n",
);
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\", install = true }}, {{ source = \"{}\" }}]\n",
want.source_spec(),
skip.source_spec()
),
);
let r = registry.mind(&["meld", ®istry.source_spec(), "--recursive", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
registry.claude_home.join("skills/want-skill").exists()
&& registry.claude_home.join("skills/skip-skill").exists(),
"--recursive installs every nested source regardless of the install flag"
);
}
#[test]
fn meld_super_source_suggests_probe() {
let tools = Sandbox::named("tools");
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
tools.source_spec()
),
);
let r = registry.mind(&["meld", ®istry.source_spec()]);
assert!(r.success, "{}", r.stderr);
assert!(
r.stdout.contains("mind probe"),
"melding a curated super-source should suggest probe: {}",
r.stdout
);
let plain = Sandbox::named("plain");
let r2 = plain.mind(&["meld", &plain.source_spec()]);
assert!(
!r2.stdout.contains("mind probe"),
"a normal source must not get the probe hint: {}",
r2.stdout
);
}
#[test]
fn sync_rewalks_super_source_for_new_nested_sources() {
let a = Sandbox::bare("aa"); let b = Sandbox::named("bb"); let c = Sandbox::named("cc"); a.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}]\n",
b.source_spec()
),
);
let spec = a.source_spec();
assert!(a.mind(&["meld", &spec]).success);
let before = a.mind(&["recall", "--sources"]).stdout;
assert!(before.contains("/bb"), "{before}");
assert!(!before.contains("/cc"), "cc not yet listed: {before}");
a.write_and_commit(
"mind.toml",
&format!(
"[discover]\nsources = [{{ source = \"{}\" }}, {{ source = \"{}\" }}]\n",
b.source_spec(),
c.source_spec()
),
);
let r = a.mind(&["sync"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
a.mind(&["recall", "--sources"]).stdout.contains("/cc"),
"sync must register the newly-listed nested source"
);
}
#[test]
fn invalid_mind_toml_errors_clearly() {
let sb = Sandbox::new();
sb.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"spell\"\nname = \"x\"\npath = \"x\"\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(!r.success);
assert!(r.stderr.contains("unknown item kind"), "{}", r.stderr);
}
#[test]
fn mind_toml_rejects_unknown_fields() {
let sb = Sandbox::new();
sb.write_and_commit("mind.toml", "[source]\nbogus = \"x\"\n");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(!r.success);
assert!(r.stderr.contains("invalid mind.toml"), "{}", r.stderr);
}
#[test]
fn meld_as_prefixes_names_links_and_refs() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
let probe = sb.mind(&["probe"]);
assert!(probe.stdout.contains("skill:jk:review"), "{}", probe.stdout);
assert!(probe.stdout.contains("agent:jk:dev"), "{}", probe.stdout);
assert!(!probe.stdout.contains("skill:review"), "{}", probe.stdout);
assert!(sb.mind(&["learn", "jk:review"]).success);
let link = sb.claude_home.join("skills/jk:review");
assert!(
std::fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink()
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("namespace:jk"),
"{}",
sources.stdout
);
}
#[test]
fn meld_namespace_flag_sets_prefix_and_as_alias_still_works() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--namespace", "jk"]).success);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:jk:review"),
"--namespace must set prefix: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("skill:review"),
"bare name must not appear when prefixed: {}",
probe.stdout
);
let sb2 = Sandbox::new();
let spec2 = sb2.source_spec();
assert!(sb2.mind(&["meld", &spec2, "-N", "zz"]).success);
let probe2 = sb2.mind(&["probe"]);
assert!(
probe2.stdout.contains("skill:zz:review"),
"-N short form must set prefix: {}",
probe2.stdout
);
let sb3 = Sandbox::new();
let spec3 = sb3.source_spec();
assert!(sb3.mind(&["meld", &spec3, "--as", "qq"]).success);
let probe3 = sb3.mind(&["probe"]);
assert!(
probe3.stdout.contains("skill:qq:review"),
"--as deprecated alias must still set prefix: {}",
probe3.stdout
);
}
#[test]
fn review_namespace_flag_evaluates_under_prefix() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec, "--namespace", "jk"]);
assert!(
r.success,
"review --namespace must exit 0 for clean source: {} {}",
r.stdout, r.stderr
);
let r2 = sb.mind(&["review", &spec, "-N", "jk"]);
assert!(
r2.success,
"review -N must exit 0 for clean source: {} {}",
r2.stdout, r2.stderr
);
}
#[test]
fn meld_namespace_on_bare_source_forks_a_new_instance() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--link-only"]).success,
"initial link-only meld"
);
let r = sb.mind(&["meld", &spec, "--namespace", "jk", "--link-only"]);
assert!(
r.success,
"meld --namespace on a bare source must succeed: {} {}",
r.stdout, r.stderr
);
let srcs = sb.mind(&["recall", "--sources"]).stdout;
let base = sb.base_name();
assert!(
srcs.contains(&format!("local/{base}/agents@jk")),
"the @jk instance must be registered: {srcs}"
);
assert!(
srcs.matches(&format!("local/{base}/agents")).count() >= 2,
"the bare source must still coexist with @jk: {srcs}"
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:jk:review"),
"probe must show the @jk instance's prefixed name: {}",
probe.stdout
);
}
#[test]
fn mind_toml_prefix_auto_applies_and_alias_overrides() {
let sb = Sandbox::new();
sb.write_and_commit("mind.toml", "[source]\nprefix = \"ag\"\n");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let probe = sb.mind(&["probe"]);
assert!(probe.stdout.contains("skill:ag:review"), "{}", probe.stdout);
let sb2 = Sandbox::new();
sb2.write_and_commit("mind.toml", "[source]\nprefix = \"ag\"\n");
let spec2 = sb2.source_spec();
assert!(sb2.mind(&["meld", &spec2, "--as", "zz"]).success);
let probe2 = sb2.mind(&["probe"]);
assert!(
probe2.stdout.contains("skill:zz:review"),
"{}",
probe2.stdout
);
assert!(!probe2.stdout.contains("ag:review"), "{}", probe2.stdout);
}
#[test]
fn ns_token_expands_to_prefixed_reference_on_install() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nDelegate to the {{ns:dev}} agent.\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:lead", "--yes"]).success);
let store = sb.mind_home.join("store/agent/jk:lead");
let body = std::fs::read_to_string(&store).expect("installed agent file");
assert!(
body.contains("the dev agent"),
"expected bare agent ref: {body}"
);
assert!(!body.contains("{{ns:dev}}"), "token should be gone: {body}");
assert!(
sb.claude_home.join("agents/lead.md").exists(),
"agent should link as agents/lead.md"
);
}
#[test]
fn ns_token_expands_to_bare_reference_without_prefix() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nDelegate to the {{ns:dev}} agent.\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
assert!(sb.mind(&["learn", "lead", "--yes"]).success);
let body = std::fs::read_to_string(sb.mind_home.join("store/agent/lead")).unwrap();
assert!(body.contains("the dev agent"), "{body}");
assert!(!body.contains("{{ns:"), "{body}");
}
#[test]
fn bad_ns_reference_errors_on_install() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nsee {{ns:ghost}}\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let r = sb.mind(&["learn", "lead"]);
assert!(!r.success);
assert!(r.stderr.contains("does not match any item"), "{}", r.stderr);
}
#[test]
fn bad_ns_token_in_non_markdown_file_installs_clean_but_still_fails_in_markdown() {
let sb = Sandbox::new();
sb.write_and_commit(
"skills/review/run.sh",
"#!/bin/sh\n# hand off to {{ns:ghost}}\necho ok\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let r = sb.mind(&["learn", "review", "--yes"]);
assert!(
r.success,
"a bad {{{{ns:}}}} token outside markdown must not fail install: {} {}",
r.stdout, r.stderr
);
let script = std::fs::read_to_string(sb.mind_home.join("store/skill/review/run.sh")).unwrap();
assert!(
script.contains("{{ns:ghost}}"),
"the token must be left exactly as written outside markdown: {script}"
);
let sb2 = Sandbox::new();
sb2.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: review\n---\nhand off to {{ns:ghost}}\n",
);
let spec2 = sb2.source_spec();
assert!(sb2.mind(&["meld", &spec2]).success);
let r2 = sb2.mind(&["learn", "review"]);
assert!(
!r2.success,
"the same bad token in a markdown file must still fail install"
);
}
#[test]
fn path_token_outside_markdown_does_not_expand() {
let sb = Sandbox::new();
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review the diff for bugs\n---\nSee {{self}}/run.sh.\n",
);
sb.write_and_commit(
"skills/review/run.sh",
"#!/bin/sh\n# lives at {{self}}\necho ok\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
assert!(sb.mind(&["learn", "review", "--yes"]).success);
let md = std::fs::read_to_string(sb.mind_home.join("store/skill/review/SKILL.md")).unwrap();
assert!(
md.contains("/store/skill/review/run.sh"),
"the markdown file's {{{{self}}}} token must expand: {md}"
);
let script = std::fs::read_to_string(sb.mind_home.join("store/skill/review/run.sh")).unwrap();
assert!(
script.contains("{{self}}"),
"the non-markdown file's token must be left exactly as written: {script}"
);
}
#[test]
fn tokens_expand_in_every_markdown_extension_and_no_other() {
let sb = Sandbox::new();
let token = "see {{ns:dev}} and {{self}}\n";
for name in [
"A.markdown",
"B.mkd",
"C.mdown",
"D.MD",
"E.Markdown",
"F.mdx",
"G.txt",
"NOTES",
".mkd",
] {
sb.write_and_commit(&format!("skills/review/{name}"), token);
}
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:review", "--yes"]).success);
let store = sb.mind_home.join("store/skill/jk:review");
for name in ["A.markdown", "B.mkd", "C.mdown", "D.MD", "E.Markdown"] {
let got = std::fs::read_to_string(store.join(name)).unwrap();
assert!(
!got.contains("{{"),
"{name} is markdown: every token must expand: {got}"
);
assert!(got.contains("see dev and "), "{name}: {got}");
}
for name in ["F.mdx", "G.txt", "NOTES", ".mkd"] {
assert_eq!(
std::fs::read_to_string(store.join(name)).unwrap(),
token,
"{name} is not markdown: its tokens must be left exactly as written"
);
}
}
#[test]
fn meld_as_warns_about_unguarded_prose_references() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nDelegate to the review skill.\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--as", "jk"]);
assert!(r.success, "{}", r.stderr);
assert!(
!r.stderr.contains("references sibling(s) in prose"),
"expected no unguarded-ref warning without --verbose: {}",
r.stderr
);
let sb2 = Sandbox::new();
sb2.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nDelegate to the review skill.\n",
);
let spec2 = sb2.source_spec();
let r2 = sb2.mind(&["--verbose", "meld", &spec2, "--as", "jk"]);
assert!(r2.success, "{}", r2.stderr);
assert!(
r2.stderr.contains("references sibling(s) in prose") && r2.stderr.contains("review"),
"expected unguarded-ref warning under --verbose: {}",
r2.stderr
);
}
#[test]
fn meld_as_does_not_warn_for_a_sibling_mention_inside_a_code_span() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nRun `review` locally first.\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["--verbose", "meld", &spec, "--as", "jk"]);
assert!(r.success, "{}", r.stderr);
assert!(
!r.stderr.contains("references sibling(s) in prose"),
"a code-span mention is not a reference and must not be flagged: {}",
r.stderr
);
}
#[test]
fn no_warning_when_unprefixed() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nDelegate to the dev agent, then run the review skill.\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["--verbose", "meld", &spec]); assert!(r.success);
assert!(
!r.stderr.contains("references sibling(s) in prose"),
"{}",
r.stderr
);
}
#[test]
fn prefixed_agent_links_under_bare_harness_name() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:dev"]).success);
assert!(
sb.mind_home.join("store/agent/jk:dev").exists(),
"store should be at jk:dev"
);
assert!(
sb.claude_home.join("agents/dev.md").exists(),
"link should be agents/dev.md"
);
assert!(
!sb.claude_home.join("agents/jk:dev.md").exists(),
"no link should exist at the prefixed path"
);
}
#[test]
fn prefixed_agent_manifest_key_uses_effective_name() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:dev"]).success);
let r = sb.mind(&["recall"]);
assert!(r.success, "{}", r.stderr);
assert!(
r.stdout.contains("jk:dev"),
"recall should show jk:dev: {}",
r.stdout
);
}
#[test]
fn agent_collision_is_refused_at_learn() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
assert!(sb.mind(&["learn", "dev"]).success);
let other = Sandbox::bare("other");
other.write_and_commit(
"agents/coder.md",
"---\nname: dev\ndescription: another dev\n---\n# dev\n",
);
assert!(sb.mind(&["meld", &other.source_spec()]).success);
let r = sb.mind(&["learn", "coder"]);
assert!(!r.success, "colliding agent install must be refused");
assert!(
r.stderr.contains("conflict"),
"expected collision error message: {}",
r.stderr
);
}
#[test]
fn meld_warns_when_incoming_agent_would_collide() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
assert!(sb.mind(&["learn", "dev"]).success);
let other = Sandbox::bare("other");
other.write_and_commit(
"agents/coder.md",
"---\nname: dev\ndescription: another dev\n---\n# dev\n",
);
let r = sb.mind(&["meld", &other.source_spec()]);
assert!(
r.success,
"meld should succeed even on collision: {}",
r.stderr
);
assert!(
r.stderr.contains("would collide"),
"expected advisory collision warning: {}",
r.stderr
);
}
#[test]
fn no_agent_collision_when_reinstalling_same_source() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
assert!(sb.mind(&["learn", "dev"]).success);
let r = sb.mind(&["learn", "dev"]);
assert!(
r.success,
"re-learn of same agent should succeed: {}",
r.stderr
);
}
#[test]
fn agent_token_expands_bare_under_prefix() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nDelegate to {{ns:dev}} for coding.\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:lead", "--yes"]).success);
let store = sb.mind_home.join("store/agent/jk:lead");
let body = std::fs::read_to_string(&store).expect("store file");
assert!(
body.contains("dev") && !body.contains("jk:dev"),
"agent token should expand bare: {body}"
);
}
#[test]
fn unguarded_ref_warning_skips_agent_only_sibling_names() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nDelegate to the dev agent, then run the review skill.\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["--verbose", "meld", &spec, "--as", "jk"]);
assert!(r.success, "{}", r.stderr);
assert!(
r.stderr.contains("references sibling(s) in prose") && r.stderr.contains("review"),
"non-agent sibling `review` must be flagged under --verbose: {}",
r.stderr
);
assert!(
!r.stderr.contains("dev"),
"agent-only sibling should not trigger unguarded-ref warning: {}",
r.stderr
);
}
#[test]
fn unprefixed_agent_links_under_frontmatter_name_not_filename() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/coder.md",
"---\nname: reviewer\ndescription: reviews code\n---\n# reviewer\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
assert!(sb.mind(&["learn", "coder"]).success);
assert!(
sb.claude_home.join("agents/reviewer.md").exists(),
"link should use the frontmatter name"
);
assert!(
!sb.claude_home.join("agents/coder.md").exists(),
"no link at the filename path"
);
assert!(sb.mind_home.join("store/agent/coder").exists());
}
#[test]
fn upgrade_moves_agent_link_when_frontmatter_name_changes() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
assert!(sb.mind(&["learn", "dev"]).success);
assert!(sb.claude_home.join("agents/dev.md").exists());
sb.write_and_commit(
"agents/dev.md",
"---\nname: lead\ndescription: now the lead\n---\n# lead agent\n",
);
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["upgrade", "--yes"]);
assert!(r.success, "{}", r.stderr);
assert!(
sb.claude_home.join("agents/lead.md").exists(),
"link should move to the new harness name"
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("agents/dev.md")).is_err(),
"the old harness-name link must not be left orphaned"
);
assert!(sb.mind_home.join("store/agent/dev").exists());
}
#[test]
fn introspect_fix_recreates_bare_agent_link() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:dev"]).success);
let link = sb.claude_home.join("agents/dev.md");
assert!(link.exists());
std::fs::remove_file(&link).unwrap();
assert!(sb.mind(&["introspect", "--fix"]).success);
assert!(
link.exists(),
"introspect --fix must recreate the bare link"
);
assert!(
!sb.claude_home.join("agents/jk:dev.md").exists(),
"the recreated link must be bare, not prefixed"
);
}
#[test]
fn forget_removes_bare_agent_link() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:dev"]).success);
assert!(sb.claude_home.join("agents/dev.md").exists());
assert!(sb.mind(&["forget", "jk:dev"]).success);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("agents/dev.md")).is_err(),
"forget must remove the bare agent link"
);
assert!(!sb.mind_home.join("store/agent/jk:dev").exists());
}
#[test]
fn cross_kind_shadow_name_still_warns_in_prose_under_prefix() {
let sb = Sandbox::new();
sb.write_and_commit(
"skills/shared/SKILL.md",
"---\nname: shared\ndescription: shared skill\n---\n# shared\n",
);
sb.write_and_commit(
"agents/shared.md",
"---\nname: shared\ndescription: shared agent\n---\n# shared\n",
);
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nHand off to shared for the work.\n",
);
let r = sb.mind(&["--verbose", "meld", &sb.source_spec(), "--as", "jk"]);
assert!(r.success, "{}", r.stderr);
assert!(
r.stderr.contains("shared"),
"a name shadowed across agent+skill must still be flagged: {}",
r.stderr
);
}
#[test]
fn upgrade_treats_a_prefix_change_as_a_rename() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success); assert!(sb.mind(&["learn", "review"]).success);
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["upgrade", "--yes"]);
assert!(r.success, "{}", r.stderr);
assert!(
r.stdout.contains("rename"),
"report should flag rename: {}",
r.stdout
);
assert!(
r.stdout
.contains("upgraded skill:review -> skill:jk:review"),
"{}",
r.stdout
);
let recall = sb.mind(&["recall"]);
assert!(
recall.stdout.contains("skill:jk:review"),
"{}",
recall.stdout
);
assert!(!recall.stdout.contains("skill:review"), "{}", recall.stdout);
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err());
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/jk:review")).is_ok());
assert!(!sb.mind_home.join("store/skill/review").exists());
assert!(sb.mind_home.join("store/skill/jk:review").exists());
}
#[test]
fn upgrade_rename_refuses_to_evict_a_different_sources_item() {
let x = Sandbox::bare("x");
x.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: X review\n---\n# X review\n",
);
x.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
let y = Sandbox::bare("y");
y.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Y review\n---\n# Y review\n",
);
assert!(x.mind(&["meld", &x.source_spec()]).success);
assert!(x.mind(&["meld", &y.source_spec()]).success);
assert!(x.mind(&["learn", "review"]).success);
assert!(x.mind(&["learn", "jk:review"]).success);
x.write_and_commit("mind.toml", "[source]\n");
assert!(x.mind(&["sync"]).success);
let r = x.mind(&["upgrade", "--yes"]);
assert!(
!r.success,
"a colliding rename must be refused, not applied: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("skill:review") || r.stderr.contains("ambiguous"),
"the refusal should name the colliding key: {}",
r.stderr
);
let store_content =
std::fs::read_to_string(x.mind_home.join("store/skill/review/SKILL.md")).unwrap();
assert!(
store_content.contains("Y review"),
"Y's store copy must not be evicted by X's refused rename: {store_content}"
);
assert!(
std::fs::symlink_metadata(x.claude_home.join("skills/review")).is_ok(),
"Y's link must remain in place"
);
assert!(x.mind(&["recall", "skill:jk:review"]).success);
let manifest = std::fs::read_to_string(x.mind_home.join("manifest.json")).unwrap_or_default();
assert!(
manifest.contains("\"skill:jk:review\"") && manifest.contains("\"skill:review\""),
"both items must remain recorded in the manifest: {manifest}"
);
}
#[test]
fn upgrade_rename_within_batch_collision_is_refused() {
let x = Sandbox::bare("x");
x.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: X review\n---\n# X review\n",
);
x.write_and_commit("mind.toml", "[source]\nprefix = \"jx\"\n");
let y = Sandbox::bare("y");
y.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Y review\n---\n# Y review\n",
);
y.write_and_commit("mind.toml", "[source]\nprefix = \"jy\"\n");
assert!(x.mind(&["meld", &x.source_spec()]).success);
assert!(x.mind(&["meld", &y.source_spec()]).success);
assert!(x.mind(&["learn", "jx:review"]).success);
assert!(x.mind(&["learn", "jy:review"]).success);
x.write_and_commit("mind.toml", "[source]\n");
y.write_and_commit("mind.toml", "[source]\n");
assert!(x.mind(&["sync"]).success);
let r = x.mind(&["upgrade", "--yes"]);
assert!(
!r.success,
"two same-batch renames converging on one key must be refused: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("skill:review"),
"the refusal must name the colliding key: {}",
r.stderr
);
assert!(
r.stdout.contains("upstream changes"),
"the pending report must still print before the collision aborts \
the batch: {}",
r.stdout
);
let r_json = x.mind(&["upgrade", "--yes", "--json"]);
assert!(!r_json.success);
let v = parse_json(&r_json.stdout);
assert_eq!(
v["error"]["kind"], "upgrade-rename-collision",
"expected the LIFE-46 error kind: {v}"
);
let x_store =
std::fs::read_to_string(x.mind_home.join("store/skill/jx:review/SKILL.md")).unwrap();
assert!(x_store.contains("X review"), "X's store copy must survive");
let y_store =
std::fs::read_to_string(x.mind_home.join("store/skill/jy:review/SKILL.md")).unwrap();
assert!(y_store.contains("Y review"), "Y's store copy must survive");
let manifest = std::fs::read_to_string(x.mind_home.join("manifest.json")).unwrap_or_default();
assert!(
manifest.contains("\"skill:jx:review\"") && manifest.contains("\"skill:jy:review\""),
"both items must remain recorded: {manifest}"
);
assert!(
!manifest.contains("\"skill:review\""),
"neither rename may have been applied: {manifest}"
);
}
#[test]
fn upgrade_rename_collision_and_coincident_links_together_still_refuses_cleanly() {
let x = Sandbox::bare("x3");
x.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: X review\n---\n# X review\n",
);
x.write_and_commit(
"mind.toml",
concat!(
"[[items]]\n",
"kind = \"skill\"\n",
"name = \"review\"\n",
"path = \"skills/review\"\n",
"link = \"skills/shared-review\"\n",
),
);
let y = Sandbox::bare("y3");
y.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Y review\n---\n# Y review\n",
);
y.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
assert!(x.mind(&["meld", &x.source_spec()]).success);
assert!(x.mind(&["meld", &y.source_spec()]).success);
assert!(x.mind(&["learn", "review"]).success); assert!(x.mind(&["learn", "jk:review"]).success);
let shared_link = x.claude_home.join("skills/shared-review");
assert!(
std::fs::symlink_metadata(&shared_link).is_ok(),
"X's explicit link override must exist after learn"
);
x.write_and_commit(
"mind.toml",
concat!(
"[source]\n",
"prefix = \"jk\"\n\n",
"[[items]]\n",
"kind = \"skill\"\n",
"name = \"review\"\n",
"path = \"skills/review\"\n",
"link = \"skills/shared-review\"\n",
),
);
assert!(x.mind(&["sync"]).success);
let r = x.mind(&["upgrade", "--yes"]);
assert!(
!r.success,
"the colliding rename must be refused even with coincident links: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
std::fs::symlink_metadata(&shared_link).is_ok(),
"X's link must survive the refused rename untouched"
);
let y_store =
std::fs::read_to_string(x.mind_home.join("store/skill/jk:review/SKILL.md")).unwrap();
assert!(
y_store.contains("Y review"),
"Y's store copy must not be evicted: {y_store}"
);
assert!(x.mind(&["recall", "skill:review"]).success);
assert!(x.mind(&["recall", "skill:jk:review"]).success);
let manifest = std::fs::read_to_string(x.mind_home.join("manifest.json")).unwrap_or_default();
assert!(
manifest.contains("\"skill:jk:review\"") && manifest.contains("\"skill:review\""),
"both items must remain recorded in the manifest: {manifest}"
);
}
#[test]
fn upgrade_rename_keeps_the_just_created_link_when_old_and_new_paths_coincide() {
let sb = melded();
assert!(sb.mind(&["learn", "dev"]).success);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("agents/dev.md")).is_ok(),
"the agent link must exist after learn"
);
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["upgrade", "--yes"]);
assert!(r.success, "{}", r.stderr);
assert!(r.stdout.contains("rename"), "{}", r.stdout);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("agents/dev.md")).is_ok(),
"the link must survive the rename: the new install's link must not be \
removed by the old item's uninstall"
);
let recall = sb.mind(&["recall"]);
assert!(recall.stdout.contains("agent:jk:dev"), "{}", recall.stdout);
}
#[test]
fn upgrade_mid_batch_failure_still_saves_the_earlier_successful_upgrades() {
let sb = melded();
assert!(sb.mind(&["learn", "agent:dev"]).success);
assert!(sb.mind(&["learn", "skill:review"]).success);
let before_manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(sb.mind_home.join("manifest.json")).unwrap())
.unwrap();
let before_hash = before_manifest["items"]["agent:dev"]["hash"].clone();
sb.write_and_commit(
"mind.toml",
concat!(
"[[items]]\n",
"kind = \"skill\"\n",
"name = \"review\"\n",
"path = \"skills/review\"\n",
"install = \"exit 3\"\n",
"\n",
"[[items]]\n",
"kind = \"agent\"\n",
"name = \"dev\"\n",
"path = \"agents/dev.md\"\n",
"\n",
"[[items]]\n",
"kind = \"rule\"\n",
"name = \"style\"\n",
"path = \"rules/style.md\"\n",
),
);
sb.write_and_commit(
"agents/dev.md",
"---\nname: dev\ndescription: Implements a spec with tests (v2)\n---\n# dev agent v2\n",
);
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review the diff for bugs (v2)\n---\n# review skill v2\n",
);
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&[
"upgrade",
"--yes",
"--accept-hooks=install,update,uninstall",
]);
assert!(
!r.success,
"the batch must fail on review's install hook: {} {}",
r.stdout, r.stderr
);
let manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(sb.mind_home.join("manifest.json")).unwrap())
.unwrap();
let old_hash = manifest["items"]["agent:dev"]["hash"].clone();
assert_ne!(
old_hash, before_hash,
"sanity: agent:dev's manifest hash must have advanced past the \
pre-upgrade hash for this test to be meaningful"
);
let store_content = std::fs::read_to_string(sb.mind_home.join("store/agent/dev")).unwrap();
assert!(
store_content.contains("v2"),
"agent:dev's store copy must hold the new content: {store_content}"
);
}
#[test]
fn upgrade_mid_batch_failure_still_saves_earlier_upgrades_under_json() {
let sb = melded();
assert!(sb.mind(&["learn", "agent:dev"]).success);
assert!(sb.mind(&["learn", "skill:review"]).success);
let before_manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(sb.mind_home.join("manifest.json")).unwrap())
.unwrap();
let before_hash = before_manifest["items"]["agent:dev"]["hash"].clone();
sb.write_and_commit(
"mind.toml",
concat!(
"[[items]]\n",
"kind = \"skill\"\n",
"name = \"review\"\n",
"path = \"skills/review\"\n",
"install = \"exit 3\"\n",
"\n",
"[[items]]\n",
"kind = \"agent\"\n",
"name = \"dev\"\n",
"path = \"agents/dev.md\"\n",
"\n",
"[[items]]\n",
"kind = \"rule\"\n",
"name = \"style\"\n",
"path = \"rules/style.md\"\n",
),
);
sb.write_and_commit(
"agents/dev.md",
"---\nname: dev\ndescription: Implements a spec with tests (v2)\n---\n# dev agent v2\n",
);
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review the diff for bugs (v2)\n---\n# review skill v2\n",
);
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&[
"--json",
"upgrade",
"--yes",
"--accept-hooks=install,update,uninstall",
]);
assert!(
!r.success,
"the batch must fail on review's install hook: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout must be a JSON error envelope, got {e}: {:?}",
r.stdout
)
});
assert_eq!(v["schema"], 1, "{}", r.stdout);
assert!(v["error"].is_object(), "{}", r.stdout);
let manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(sb.mind_home.join("manifest.json")).unwrap())
.unwrap();
let old_hash = manifest["items"]["agent:dev"]["hash"].clone();
assert_ne!(
old_hash, before_hash,
"agent:dev's manifest hash must have advanced even though the whole \
command failed under --json: {manifest:#}"
);
let store_content = std::fs::read_to_string(sb.mind_home.join("store/agent/dev")).unwrap();
assert!(
store_content.contains("v2"),
"agent:dev's store copy must hold the new content: {store_content}"
);
}
#[test]
fn unmeld_unlink_only_keeps_installed_items() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let r = sb.mind(&["unmeld", "agents", "--unlink-only"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded")
);
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_ok());
assert!(
sb.mind(&["recall", "review"]).success,
"the item remains installed"
);
assert!(
r.stdout.contains("remain installed") && r.stdout.contains("mind forget"),
"unlink-only must list orphaned items and suggest forget: {}",
r.stdout
);
}
#[test]
fn unmeld_forgets_items_by_default() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let r = sb.mind(&["unmeld", "agents"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err(),
"the item link must be removed by default"
);
assert!(
!sb.mind(&["recall", "review"]).success,
"the item must be uninstalled by default"
);
assert!(
sb.source.exists(),
"unmeld must not delete the linked local working tree at {}",
sb.source.display()
);
}
#[test]
fn unmeld_unknown_source_errors() {
let sb = Sandbox::new();
let r = sb.mind(&["unmeld", "nope"]);
assert!(!r.success);
assert!(
r.stderr.contains("no melded source matches"),
"{}",
r.stderr
);
}
#[test]
fn sources_with_same_basename_coexist() {
let a = Sandbox::new();
let b = Sandbox::new(); assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &b.source_spec()]).success);
let a_full = format!("{}/agents", a.base_name());
let b_full = format!("{}/agents", b.base_name());
let sources = a.mind(&["recall", "--sources"]).stdout;
assert!(sources.contains(&a_full), "{sources}");
assert!(sources.contains(&b_full), "{sources}");
let bare = a.mind(&["learn", "review"]);
assert!(!bare.success);
assert!(bare.stderr.contains("ambiguous"), "{}", bare.stderr);
let r = a.mind(&["learn", &format!("{a_full}#review")]);
assert!(r.success, "{}", r.stderr);
}
#[test]
fn unmeld_full_name_resolves_basename_collision() {
let a = Sandbox::new();
let b = Sandbox::new();
assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &b.source_spec()]).success);
let amb = a.mind(&["unmeld", "agents"]);
assert!(!amb.success);
assert!(amb.stderr.contains("multiple sources"), "{}", amb.stderr);
assert!(
a.mind(&["unmeld", &format!("{}/agents", a.base_name())])
.success
);
assert!(a.mind(&["unmeld", "agents"]).success);
}
#[test]
fn unmeld_glob_removes_only_the_matching_source() {
let a = Sandbox::named("foo");
let agents = Sandbox::named("agents");
assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &agents.source_spec()]).success);
assert!(
a.mind(&["learn", &format!("{}/agents#review", agents.base_name())])
.success
);
let r = a.mind(&["unmeld", "*agents"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let sources = a.mind(&["recall", "--sources"]).stdout;
assert!(sources.contains("foo"), "foo must remain melded: {sources}");
assert!(
!sources.contains(&format!("{}/agents", agents.base_name())),
"the agents source must be unmelded: {sources}"
);
assert!(
std::fs::symlink_metadata(a.claude_home.join("skills/review")).is_err(),
"the agents source's item link must be removed"
);
}
#[test]
fn unmeld_glob_matching_several_lists_and_removes_with_yes() {
let a = Sandbox::named("agents");
let b = Sandbox::named("agents");
assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &b.source_spec()]).success);
let a_full = format!("{}/agents", a.base_name());
let b_full = format!("{}/agents", b.base_name());
let refused = a.mind(&["unmeld", "*agents"]);
assert!(!refused.success, "must refuse: {}", refused.stdout);
assert!(
refused.stderr.contains("needs confirmation"),
"{}",
refused.stderr
);
assert!(
refused.stdout.contains(&a_full) && refused.stdout.contains(&b_full),
"both matched sources must be listed: {}",
refused.stdout
);
let still = a.mind(&["recall", "--sources"]).stdout;
assert!(
still.contains(&a_full) && still.contains(&b_full),
"both sources must survive a refused unmeld: {still}"
);
let r = a.mind(&["unmeld", "*agents", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
a.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"every matching source must be unmelded"
);
}
#[test]
fn unmeld_glob_matching_no_source_errors() {
let sb = melded();
let r = sb.mind(&["unmeld", "*nope"]);
assert!(!r.success);
assert!(
r.stderr.contains("no melded source matches"),
"{}",
r.stderr
);
}
#[test]
fn unmeld_plain_ambiguous_suffix_still_errors() {
let a = Sandbox::named("agents");
let b = Sandbox::named("agents");
assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &b.source_spec()]).success);
let amb = a.mind(&["unmeld", "agents"]);
assert!(!amb.success);
assert!(amb.stderr.contains("multiple sources"), "{}", amb.stderr);
}
#[test]
fn unmeld_glob_unlink_only_over_several_keeps_items() {
let a = Sandbox::named("agents");
let b = Sandbox::named("agents");
assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &b.source_spec()]).success);
let a_full = format!("{}/agents", a.base_name());
let b_full = format!("{}/agents", b.base_name());
assert!(
a.mind(&["learn", &format!("{a_full}#skill:review")])
.success,
"install review from the first source"
);
assert!(
a.mind(&["learn", &format!("{b_full}#agent:dev")]).success,
"install dev from the second source"
);
let r = a.mind(&["unmeld", "*agents", "--unlink-only", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let sources = a.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("no sources melded"),
"every matched source must be unmelded: {sources}"
);
assert!(
std::fs::symlink_metadata(a.claude_home.join("skills/review")).is_ok(),
"the first source's item link must be kept under --unlink-only"
);
assert!(
std::fs::symlink_metadata(a.claude_home.join("agents/dev.md")).is_ok(),
"the second source's item link must be kept under --unlink-only"
);
assert!(
r.stdout.matches("item(s) remain installed").count() >= 2,
"the orphaned-items note must appear for each unmelded source: {}",
r.stdout
);
assert!(
r.stdout.contains("mind forget"),
"the forget suggestion must be shown: {}",
r.stdout
);
}
#[test]
fn unmeld_glob_single_match_honors_item_count_confirmation() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
assert!(sb.mind(&["learn", "dev"]).success);
let refused = sb.mind(&["unmeld", "*agents"]);
assert!(
!refused.success,
"a single-match glob must still honor the item-count confirmation: {}",
refused.stdout
);
assert!(
refused.stderr.contains("needs confirmation"),
"{}",
refused.stderr
);
assert!(
!refused.stdout.contains("would remove 1 source"),
"a single match must not show the multi-source listing: {}",
refused.stdout
);
assert!(sb.mind(&["recall", "review"]).success, "item remains");
assert!(
sb.mind(&["recall", "--sources"]).stdout.contains("agents"),
"the source survives a refused single-match glob"
);
let r = sb.mind(&["unmeld", "*agents", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"the matched source must be unmelded"
);
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err());
assert!(std::fs::symlink_metadata(sb.claude_home.join("agents/dev.md")).is_err());
}
#[test]
fn unmeld_glob_aborts_remaining_sources_when_one_item_hook_fails() {
let fail = Sandbox::bare("glob-abort");
fail.write_and_commit(
"skills/greet/SKILL.md",
"---\ndescription: greet the user\n---\n# greet\n",
);
fail.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"skill\"\nname = \"greet\"\npath = \"skills/greet\"\nuninstall = \"exit 1\"\n",
);
let good = Sandbox::bare("glob-abort");
good.write_and_commit(
"skills/other/SKILL.md",
"---\ndescription: another skill\n---\n# other\n",
);
assert!(
fail.mind(&["meld", &fail.source_spec(), "--link-only"])
.success
);
assert!(
fail.mind(&["meld", &good.source_spec(), "--link-only"])
.success
);
assert!(
fail.mind(&[
"learn",
"skill:greet",
"--accept-hooks=install,update,uninstall"
])
.success,
"install greet (with its uninstall hook)"
);
assert!(
fail.mind(&[
"learn",
"skill:other",
"--accept-hooks=install,update,uninstall"
])
.success,
"install other from the good source"
);
let fail_full = format!("{}/glob-abort", fail.base_name());
let good_full = format!("{}/glob-abort", good.base_name());
let r = fail.mind(&[
"unmeld",
"*glob-abort",
"--yes",
"--accept-hooks=install,update,uninstall",
]);
assert!(
!r.success,
"a required item uninstall-hook failure must fail the whole unmeld: {} {}",
r.stdout, r.stderr
);
let sources = fail.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains(&fail_full),
"the source whose item uninstall hook failed must stay melded: {sources}"
);
assert!(
fail.mind(&["recall", "skill:greet"]).success,
"the item is kept when its required uninstall hook fails"
);
assert!(
sources.contains(&good_full),
"a matched source after the failing one must be left unprocessed (still melded): {sources}"
);
assert!(
fail.mind(&["recall", "skill:other"]).success,
"the unprocessed source's item must still be installed"
);
}
#[test]
fn sync_reports_up_to_date_then_updated() {
let sb = melded();
assert!(sb.mind(&["sync"]).stdout.contains("up to date"));
sb.edit_source();
assert!(sb.mind(&["sync"]).stdout.contains("updated"));
}
#[test]
fn sync_with_no_sources_is_ok() {
let sb = Sandbox::new();
let r = sb.mind(&["sync"]);
assert!(r.success);
assert!(r.stdout.contains("no sources melded"), "{}", r.stdout);
}
#[test]
fn introspect_reports_missing_link() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
std::fs::remove_file(sb.claude_home.join("skills/review")).unwrap();
let r = sb.mind(&["introspect"]);
assert!(r.stdout.contains("symlink missing"), "{}", r.stdout);
}
#[test]
fn introspect_reports_drift_after_source_change() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
sb.edit_source();
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["introspect"]);
assert!(r.stdout.contains("upstream changed"), "{}", r.stdout);
}
#[test]
fn introspect_reports_namespace_change() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["introspect"]);
assert!(r.stdout.contains("namespace changed"), "{}", r.stdout);
}
#[test]
fn failed_upgrade_preserves_the_previous_version() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nDelegate to {{ns:dev}}.\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:lead", "--yes"]).success);
let store = sb.mind_home.join("store/agent/jk:lead");
assert!(std::fs::read_to_string(&store).unwrap().contains("dev"));
sb.write_and_commit(
"agents/lead.md",
"---\nname: lead\ndescription: lead\n---\nDelegate to {{ns:ghost}}.\n",
);
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["upgrade", "--yes"]);
assert!(!r.success, "upgrade should fail on the bad reference");
assert!(r.stderr.contains("does not match any item"), "{}", r.stderr);
let body = std::fs::read_to_string(&store).expect("old store copy should remain");
assert!(body.contains("dev"), "old version should be intact: {body}");
assert!(std::fs::symlink_metadata(sb.claude_home.join("agents/lead.md")).is_ok());
}
#[test]
fn removed_upstream_item_is_left_alone_and_flagged() {
let sb = melded();
assert!(sb.mind(&["learn", "dev"]).success);
sb.remove_and_commit("agents/dev.md");
assert!(sb.mind(&["sync"]).success);
let ev = sb.mind(&["upgrade", "--yes"]);
assert!(ev.success, "{}", ev.stderr);
assert!(ev.stdout.contains("up to date"), "{}", ev.stdout);
assert!(sb.mind(&["recall"]).stdout.contains("agent:dev"));
let ins = sb.mind(&["introspect"]);
assert!(ins.stdout.contains("no longer present"), "{}", ins.stdout);
}
#[test]
fn upgrade_item_filter_limits_to_one() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
assert!(sb.mind(&["learn", "dev"]).success);
sb.edit_source(); sb.write_and_commit(
"agents/dev.md",
"---\nname: dev\ndescription: Implements a spec with tests\n---\n# dev agent\nedited\n",
);
assert!(sb.mind(&["sync"]).success);
let ev = sb.mind(&["upgrade", "--yes", "review"]);
assert!(ev.success, "{}", ev.stderr);
assert!(ev.stdout.contains("upgraded skill:review"), "{}", ev.stdout);
assert!(!ev.stdout.contains("agent:dev"), "{}", ev.stdout);
let rest = sb.mind(&["upgrade"]);
assert!(rest.stdout.contains("agent:dev"), "{}", rest.stdout);
assert!(!rest.stdout.contains("skill:review"), "{}", rest.stdout);
}
#[test]
fn upgrade_glob_upgrades_multiple_items() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
assert!(sb.mind(&["learn", "dev"]).success);
sb.edit_source(); sb.write_and_commit(
"agents/dev.md",
"---\nname: dev\ndescription: Implements a spec with tests\n---\n# dev agent\nedited\n",
);
assert!(sb.mind(&["sync"]).success);
let ev = sb.mind(&["upgrade", "--yes", "skill:*"]);
assert!(ev.success, "{}", ev.stderr);
assert!(
ev.stdout.contains("upgraded skill:review"),
"skill:* must upgrade the skill: {}",
ev.stdout
);
assert!(
!ev.stdout.contains("upgraded agent:dev"),
"skill:* must not touch the agent: {}",
ev.stdout
);
let ev2 = sb.mind(&["upgrade", "--yes", "agents#*"]);
assert!(ev2.success, "{}", ev2.stderr);
assert!(
ev2.stdout.contains("upgraded agent:dev"),
"agents#* must upgrade the pending agent: {}",
ev2.stdout
);
}
#[test]
fn upgrade_glob_no_match_is_not_an_error() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
sb.edit_source();
assert!(sb.mind(&["sync"]).success);
let ev = sb.mind(&["upgrade", "--yes", "xyz*"]);
assert!(ev.success, "no-match glob must exit 0: {}", ev.stderr);
let pending = sb.mind(&["upgrade"]);
assert!(
pending.stdout.contains("skill:review"),
"item must remain pending: {}",
pending.stdout
);
}
#[test]
fn upgrade_namespaced_glob_upgrades_namespace() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:review"]).success);
assert!(sb.mind(&["learn", "jk:dev"]).success);
sb.edit_source();
sb.write_and_commit(
"agents/dev.md",
"---\nname: dev\ndescription: Implements a spec with tests\n---\n# dev agent\nedited\n",
);
assert!(sb.mind(&["sync"]).success);
let ev = sb.mind(&["upgrade", "--yes", "jk:*"]);
assert!(ev.success, "{}", ev.stderr);
assert!(
ev.stdout.contains("upgraded skill:jk:review"),
"jk:* must upgrade jk:review: {}",
ev.stdout
);
assert!(
ev.stdout.contains("upgraded agent:jk:dev"),
"jk:* must upgrade jk:dev: {}",
ev.stdout
);
}
#[test]
fn upgrade_exact_ref_still_works_after_glob_change() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
assert!(sb.mind(&["learn", "dev"]).success);
sb.edit_source();
sb.write_and_commit(
"agents/dev.md",
"---\nname: dev\ndescription: Implements a spec with tests\n---\n# dev agent\nedited\n",
);
assert!(sb.mind(&["sync"]).success);
let ev = sb.mind(&["upgrade", "--yes", "review"]);
assert!(ev.success, "{}", ev.stderr);
assert!(
ev.stdout.contains("upgraded skill:review"),
"exact ref must upgrade the item: {}",
ev.stdout
);
assert!(
!ev.stdout.contains("agent:dev"),
"exact ref must not touch other items: {}",
ev.stdout
);
let rest = sb.mind(&["upgrade"]);
assert!(
rest.stdout.contains("agent:dev"),
"dev must remain pending: {}",
rest.stdout
);
}
#[test]
fn upgrade_source_glob_isolates_to_named_source() {
let agents = melded(); assert!(agents.mind(&["learn", "review"]).success);
let tools = Sandbox::bare("tools");
tools.write_and_commit(
"skills/deploy/SKILL.md",
"---\nname: deploy\ndescription: Ship the build\n---\n# deploy skill\n",
);
assert!(
agents.mind(&["meld", &tools.source_spec()]).success,
"meld of the second source failed"
);
assert!(agents.mind(&["learn", "deploy"]).success);
agents.edit_source(); tools.write_and_commit(
"skills/deploy/SKILL.md",
"---\nname: deploy\ndescription: Ship the build\n---\n# deploy skill\nedited\n",
);
assert!(agents.mind(&["sync"]).success);
let ev = agents.mind(&["upgrade", "--yes", "tools#*"]);
assert!(ev.success, "{}", ev.stderr);
assert!(
ev.stdout.contains("upgraded skill:deploy"),
"tools#* must upgrade the tools source item: {}",
ev.stdout
);
assert!(
!ev.stdout.contains("skill:review"),
"tools#* must NOT touch the other source's pending item: {}",
ev.stdout
);
let rest = agents.mind(&["upgrade"]);
assert!(
rest.stdout.contains("skill:review"),
"the other source's item must remain pending: {}",
rest.stdout
);
assert!(
!rest.stdout.contains("skill:deploy"),
"the tools source item was already upgraded: {}",
rest.stdout
);
}
#[test]
fn upgrade_exact_ref_no_match_is_up_to_date_not_error() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
sb.edit_source(); assert!(sb.mind(&["sync"]).success);
let ev = sb.mind(&["upgrade", "--yes", "nonexistent"]);
assert!(
ev.success,
"exact no-match ref must exit 0, not error: {} {}",
ev.stdout, ev.stderr
);
assert!(
!ev.stdout.contains("upgraded skill:review"),
"a non-matching exact ref must not upgrade the pending item: {}",
ev.stdout
);
let rest = sb.mind(&["upgrade"]);
assert!(
rest.stdout.contains("skill:review"),
"the pending item must be untouched by the no-match ref: {}",
rest.stdout
);
}
#[test]
fn json_upgrade_glob_outcomes() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
sb.edit_source(); assert!(sb.mind(&["sync"]).success);
let none = sb.mind(&["upgrade", "--yes", "--json", "zzz*"]);
assert!(
none.success,
"no-match glob under --json failed: {}",
none.stderr
);
let v = parse_json(&none.stdout);
assert_eq!(v["action"], "upgrade", "{}", none.stdout);
assert_eq!(
v["outcome"], "up-to-date",
"a no-match glob must report up-to-date under --json: {}",
none.stdout
);
assert!(
!none.stdout.contains("up to date"),
"no prose under --json: {}",
none.stdout
);
let some = sb.mind(&["upgrade", "--yes", "--json", "skill:*"]);
assert!(
some.success,
"matching glob under --json failed: {}",
some.stderr
);
let v = parse_json(&some.stdout);
assert_eq!(v["action"], "upgrade", "{}", some.stdout);
assert_eq!(
v["outcome"], "upgraded",
"a matching glob must report upgraded under --json: {}",
some.stdout
);
assert_eq!(
v["installed"],
serde_json::json!(["skill:review"]),
"{}",
some.stdout
);
assert!(
!some.stdout.contains("upgraded skill"),
"no prose under --json: {}",
some.stdout
);
}
#[test]
fn mind_toml_unions_items_and_discover() {
let sb = Sandbox::new();
sb.write_and_commit(
"packages/foo/SKILL.md",
"---\ndescription: foo\n---\n# foo\n",
);
sb.write_and_commit(
"extra/special.md",
"---\nname: special\ndescription: x\n---\n# special\n",
);
sb.write_and_commit(
"mind.toml",
concat!(
"[[items]]\n",
"kind = \"agent\"\n",
"name = \"special\"\n",
"path = \"extra/special.md\"\n\n",
"[discover]\n",
"skills = { include = [\"packages/*/SKILL.md\"] }\n",
),
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let probe = sb.mind(&["probe"]).stdout;
assert!(probe.contains("agent:special"), "from [[items]]: {probe}");
assert!(probe.contains("skill:foo"), "from [discover]: {probe}");
}
#[test]
fn sync_preserves_consumer_alias() {
let sb = Sandbox::new();
assert!(sb.mind(&["meld", &sb.source_spec(), "--as", "jk"]).success);
assert!(sb.mind(&["sync"]).success);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("namespace:jk")
);
assert!(sb.mind(&["probe"]).stdout.contains("skill:jk:review"));
}
#[test]
fn learn_glob_installs_all_matches() {
let sb = melded();
assert!(sb.mind(&["learn", "*"]).success);
let recall = sb.mind(&["recall"]).stdout;
assert!(recall.contains("skill:review"), "{recall}");
assert!(recall.contains("agent:dev"), "{recall}");
assert!(recall.contains("rule:style"), "{recall}");
}
#[test]
fn learn_kind_glob_limits_to_kind() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:*"]).success);
assert!(
sb.mind(&["recall", "skill:review"]).success,
"skill installed"
);
assert!(
!sb.mind(&["recall", "agent:dev"]).success,
"agent not installed by a skill glob"
);
}
#[test]
fn learn_all_flag_installs_whole_source() {
let sb = melded();
let r = sb.mind(&["learn", "agents", "--all"]);
assert!(r.success, "{}", r.stderr);
let recall = sb.mind(&["recall"]).stdout;
assert!(recall.contains("skill:review"), "{recall}");
assert!(recall.contains("agent:dev"), "{recall}");
assert!(recall.contains("rule:style"), "{recall}");
}
#[test]
fn learn_all_flag_rejects_ref_with_hash() {
let sb = melded();
let r = sb.mind(&["learn", "agents#review", "--all"]);
assert!(!r.success, "expected failure: {}", r.stdout);
assert!(
!sb.mind(&["recall", "skill:review"]).success,
"nothing installed"
);
}
#[test]
fn learn_dry_run_installs_nothing() {
let sb = melded();
let r = sb.mind(&["learn", "*", "--dry-run"]);
assert!(r.success, "{}", r.stderr);
assert!(r.stdout.contains("would learn"), "{}", r.stdout);
assert!(
r.stdout.contains("skill:review"),
"plan should list items: {}",
r.stdout
);
assert!(!sb.mind(&["recall"]).stdout.contains("installed @"));
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err());
}
#[test]
fn learn_glob_collision_errors_and_installs_nothing() {
let a = Sandbox::new();
let b = Sandbox::new(); assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &b.source_spec()]).success);
let r = a.mind(&["learn", "*"]);
assert!(!r.success);
assert!(r.stderr.contains("ambiguous"), "{}", r.stderr);
assert!(!a.mind(&["recall"]).stdout.contains("installed @"));
}
#[test]
fn probe_marks_installed_and_shows_hash() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let probe = sb.mind(&["probe"]).stdout;
let review = probe.lines().find(|l| l.contains("skill:review")).unwrap();
assert!(
review.starts_with('*'),
"installed item should be marked: {review:?}"
);
let dev = probe.lines().find(|l| l.contains("agent:dev")).unwrap();
assert!(
!dev.starts_with('*'),
"uninstalled item should not be marked: {dev:?}"
);
assert!(
review
.split_whitespace()
.any(|t| t.len() == 8 && t.chars().all(|c| c.is_ascii_hexdigit())),
"expected a short hash: {review:?}"
);
}
#[test]
fn probe_columns_align_with_long_names() {
let sb = Sandbox::new();
sb.write_and_commit(
"skills/consumer-experience-review/SKILL.md",
"---\ndescription: long-named skill\n---\n# x\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let probe = sb.mind(&["probe"]).stdout;
let cols: Vec<usize> = probe
.lines()
.filter(|l| l.contains("/agents"))
.map(|l| l.find("local/").expect("source column on every row"))
.collect();
assert!(cols.len() >= 2, "expected several rows: {probe}");
assert!(
cols.iter().all(|&c| c == cols[0]),
"source column misaligned: {cols:?}\n{probe}"
);
}
#[test]
fn learn_source_and_kind_glob_compose() {
let sb = melded();
assert!(sb.mind(&["learn", "agents#skill:*"]).success);
assert!(
sb.mind(&["recall", "skill:review"]).success,
"skill installed"
);
assert!(
!sb.mind(&["recall", "agent:dev"]).success,
"agent not installed by a skill glob"
);
}
#[test]
fn learn_partial_failure_persists_successes() {
let sb = Sandbox::new();
sb.write_and_commit(
"skills/zzz/SKILL.md",
"---\ndescription: bad\n---\nsee {{ns:ghost}}\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let r = sb.mind(&["learn", "skill:*"]);
assert!(!r.success, "should fail on the bad reference");
assert!(r.stderr.contains("does not match any item"), "{}", r.stderr);
let recall = sb.mind(&["recall"]).stdout;
assert!(
recall.contains("skill:review"),
"successes should persist: {recall}"
);
let ins = sb.mind(&["introspect"]).stdout;
assert!(
!ins.contains("symlink missing"),
"manifest/disk drift: {ins}"
);
}
fn dep_fixture() -> Sandbox {
let sb = Sandbox::bare("agents-and-skills");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review the diff\n---\n# review\nhand off to {{ns:reviewer}}\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviews changes\n---\n# reviewer agent\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
sb
}
#[test]
fn learn_yes_installs_referenced_dependency_closure() {
let sb = dep_fixture();
let r = sb.mind(&["learn", "skill:review", "--yes"]);
assert!(r.success, "{}", r.stderr);
let recall = sb.mind(&["recall"]).stdout;
assert!(
recall.contains("skill:review"),
"selected skill installed: {recall}"
);
assert!(
recall.contains("agent:reviewer"),
"referenced dependency pulled into the closure: {recall}"
);
}
#[test]
fn learn_whole_source_glob_pulls_no_extras() {
let sb = dep_fixture();
let r = sb.mind(&["learn", "agents-and-skills#*"]);
assert!(r.success, "{}", r.stderr);
let recall = sb.mind(&["recall"]).stdout;
assert!(recall.contains("skill:review"), "{recall}");
assert!(recall.contains("agent:reviewer"), "{recall}");
}
#[test]
fn learn_dependency_dry_run_renders_tree_and_installs_nothing() {
let sb = dep_fixture();
let r = sb.mind(&["learn", "skill:review", "--dry-run"]);
assert!(r.success, "{}", r.stderr);
assert!(r.stdout.contains("would learn"), "{}", r.stdout);
assert!(
r.stdout.contains("skill:review [selected]"),
"tree should head with the selected skill: {}",
r.stdout
);
assert!(
r.stdout.contains("agent:reviewer [dep]"),
"tree should mark the pulled-in dependency: {}",
r.stdout
);
assert!(!sb.mind(&["recall"]).stdout.contains("installed @"));
assert!(std::fs::symlink_metadata(sb.claude_home.join("agents/reviewer.md")).is_err());
}
#[test]
fn forget_does_not_remove_a_dependency() {
let sb = dep_fixture();
assert!(sb.mind(&["learn", "skill:review", "--yes"]).success);
assert!(sb.mind(&["forget", "skill:review"]).success);
assert!(
!sb.mind(&["recall", "skill:review"]).success,
"the forgotten skill is gone"
);
assert!(
sb.mind(&["recall", "agent:reviewer"]).success,
"the dependency stays installed"
);
}
#[test]
fn learn_installs_dependency_before_dependent() {
let sb = dep_fixture();
let r = sb.mind(&["learn", "skill:review", "--yes"]);
assert!(r.success, "{}", r.stderr);
let dep_line = r
.stdout
.lines()
.position(|l| l.starts_with("learned agent:reviewer "))
.unwrap_or_else(|| panic!("missing reviewer learned line: {}", r.stdout));
let dependent_line = r
.stdout
.lines()
.position(|l| l.starts_with("learned skill:review "))
.unwrap_or_else(|| panic!("missing review learned line: {}", r.stdout));
assert!(
dep_line < dependent_line,
"dependency must install before its dependent: {}",
r.stdout
);
}
#[test]
fn learn_dependency_prompt_decline_installs_nothing() {
let sb = dep_fixture();
let r = sb.mind_with_input(&["learn", "skill:review"], Some("n\n"));
assert!(r.success, "{}", r.stderr);
assert!(
r.stdout.contains("skill:review [selected]"),
"tree should head with the selected skill: {}",
r.stdout
);
assert!(
r.stdout.contains("agent:reviewer [dep]"),
"tree should mark the pulled-in dependency: {}",
r.stdout
);
assert!(
r.stdout.contains("cancelled; nothing installed"),
"decline should print the cancelled line: {}",
r.stdout
);
assert!(!sb.mind(&["recall"]).stdout.contains("installed @"));
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err());
assert!(std::fs::symlink_metadata(sb.claude_home.join("agents/reviewer.md")).is_err());
}
#[test]
fn learn_dependency_json_without_yes_refuses() {
let sb = dep_fixture();
let r = sb.mind(&["learn", "skill:review", "--json"]);
assert!(
!r.success,
"--json learn that pulls in a dependency must refuse without --yes: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("confirmation-required"),
"the json envelope should carry the confirmation-required kind: {}",
r.stdout
);
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err());
assert!(std::fs::symlink_metadata(sb.claude_home.join("agents/reviewer.md")).is_err());
assert!(
sb.mind(&["learn", "skill:review", "--json", "--yes"])
.success
);
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_ok());
}
#[test]
fn learn_dependency_prompt_defaults_to_no_on_eof() {
let sb = dep_fixture();
let r = sb.mind_with_input(&["learn", "skill:review"], Some(""));
assert!(r.success, "{}", r.stderr);
assert!(
r.stdout.contains("agent:reviewer [dep]"),
"tree should still render before the prompt: {}",
r.stdout
);
assert!(
r.stdout.contains("cancelled; nothing installed"),
"EOF should default to No: {}",
r.stdout
);
assert!(!sb.mind(&["recall"]).stdout.contains("installed @"));
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err());
assert!(std::fs::symlink_metadata(sb.claude_home.join("agents/reviewer.md")).is_err());
}
#[test]
fn learn_dependency_prompt_accept_installs_closure() {
let sb = dep_fixture();
let r = sb.mind_with_input(&["learn", "skill:review"], Some("y\n"));
assert!(r.success, "{}", r.stderr);
assert!(
r.stdout.contains("agent:reviewer [dep]"),
"tree should render before the prompt: {}",
r.stdout
);
let recall = sb.mind(&["recall"]).stdout;
assert!(
recall.contains("skill:review"),
"selected skill installed on confirm: {recall}"
);
assert!(
recall.contains("agent:reviewer"),
"dependency installed on confirm: {recall}"
);
}
#[test]
fn learn_pulls_dependency_referenced_in_non_skill_md_file() {
let sb = Sandbox::bare("nonmd-deps");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review the diff\n---\n# review\n",
);
sb.write_and_commit(
"skills/review/extra.md",
"see {{ns:reviewer}} for handoff\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviews changes\n---\n# reviewer agent\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let r = sb.mind(&["learn", "skill:review", "--yes"]);
assert!(r.success, "{}", r.stderr);
let recall = sb.mind(&["recall"]).stdout;
assert!(
recall.contains("skill:review"),
"selected skill installed: {recall}"
);
assert!(
recall.contains("agent:reviewer"),
"token in a non-SKILL.md file still pulls the dependency: {recall}"
);
}
#[test]
fn learn_ignores_ns_token_in_a_non_markdown_file_within_the_skill() {
let sb = Sandbox::bare("nonmd-no-deps");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review the diff\n---\n# review\n",
);
sb.write_and_commit(
"skills/review/run.sh",
"#!/bin/sh\n# see {{ns:reviewer}} for handoff\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviews changes\n---\n# reviewer agent\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let r = sb.mind(&["learn", "skill:review"]);
assert!(r.success, "{}", r.stderr);
let recall = sb.mind(&["recall"]).stdout;
assert!(
recall.contains("skill:review installed"),
"the skill must install with no dependency prompt in the way: {recall}"
);
assert!(
recall.contains("agent:reviewer available"),
"a {{{{ns:}}}} token in a non-markdown file must not pull in the \
sibling it names, so the agent stays uninstalled: {recall}"
);
}
#[test]
fn learn_dependency_already_installed_prompts_but_reinstalls_only_new() {
let sb = dep_fixture();
assert!(sb.mind(&["learn", "agent:reviewer", "--yes"]).success);
let r = sb.mind_with_input(&["learn", "skill:review"], Some("y\n"));
assert!(r.success, "{}", r.stderr);
assert!(
r.stdout.contains("agent:reviewer [installed]"),
"already-installed dep should be marked [installed]: {}",
r.stdout
);
assert!(
r.stdout.contains("learned skill:review "),
"the new skill installs: {}",
r.stdout
);
assert!(
!r.stdout.contains("learned agent:reviewer "),
"the already-installed dependency is not reinstalled: {}",
r.stdout
);
let recall = sb.mind(&["recall"]).stdout;
assert_eq!(
recall.matches("agent:reviewer").count(),
1,
"reviewer must not be duplicated: {recall}"
);
assert!(recall.contains("skill:review"), "{recall}");
}
#[test]
fn learn_closure_collision_via_pulled_dependency_aborts() {
let a = Sandbox::bare("coll-a");
a.write_and_commit(
"skills/areview/SKILL.md",
"---\nname: areview\ndescription: A review\n---\n# areview\nuse {{ns:reviewer}}\n",
);
a.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: A reviewer\n---\n# reviewer\n",
);
let b = Sandbox::bare("coll-b");
b.write_and_commit(
"skills/breview/SKILL.md",
"---\nname: breview\ndescription: B review\n---\n# breview\nuse {{ns:reviewer}}\n",
);
b.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: B reviewer\n---\n# reviewer\n",
);
assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &b.source_spec()]).success);
let r = a.mind(&["learn", "skill:*", "--yes"]);
assert!(!r.success, "closure collision should abort: {}", r.stdout);
assert!(
r.stderr.contains("ambiguous"),
"collision should be reported as ambiguous: {}",
r.stderr
);
assert!(!a.mind(&["recall"]).stdout.contains("installed @"));
}
#[test]
fn unlearn_is_an_alias_for_forget() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
assert!(sb.mind(&["unlearn", "review"]).success);
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err());
assert!(!sb.mind(&["recall"]).stdout.contains("installed @"));
}
#[test]
fn status_is_an_alias_for_recall() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let recall = sb.mind(&["recall"]);
let status = sb.mind(&["status"]);
assert!(status.success, "status alias runs: {}", status.stderr);
assert_eq!(
status.stdout, recall.stdout,
"`status` must produce the same output as `recall`"
);
assert!(sb.mind(&["status", "--sources"]).success);
}
#[test]
fn learn_links_into_all_configured_homes() {
let sb = Sandbox::new();
let home_a = sb.base.join("homeA");
let home_b = sb.base.join("homeB");
write(
&sb.mind_home.join("config.toml"),
&format!(
"lobes = [\"{}\", \"{}\"]\n",
home_a.display(),
home_b.display()
),
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(sb.mind(&["learn", "review"]).success);
assert!(std::fs::symlink_metadata(home_a.join("skills/review")).is_ok());
assert!(std::fs::symlink_metadata(home_b.join("skills/review")).is_ok());
assert!(sb.mind(&["forget", "review"]).success);
assert!(std::fs::symlink_metadata(home_a.join("skills/review")).is_err());
assert!(std::fs::symlink_metadata(home_b.join("skills/review")).is_err());
}
#[test]
fn learn_links_into_homes_from_env() {
let sb = Sandbox::new();
let home_a = sb.base.join("envA");
let home_b = sb.base.join("envB");
let homes = format!("{}:{}", home_a.display(), home_b.display());
let env = [("MIND_AGENT_HOMES", homes.as_str())];
assert!(sb.mind_env(&["meld", &sb.source_spec()], &env).success);
assert!(sb.mind_env(&["learn", "review"], &env).success);
assert!(std::fs::symlink_metadata(home_a.join("skills/review")).is_ok());
assert!(std::fs::symlink_metadata(home_b.join("skills/review")).is_ok());
}
#[test]
fn meld_with_ssh_config_still_melds_a_local_source() {
let sb = Sandbox::new();
write(&sb.mind_home.join("config.toml"), "ssh = true\n");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(
r.success,
"ssh-config meld of a local source should succeed: {}",
r.stderr
);
let json = std::fs::read_to_string(sb.mind_home.join("sources.json")).unwrap();
assert!(
json.contains(&spec),
"a local source URL must be unchanged under ssh=true: {json}"
);
assert!(
!json.contains("git@"),
"a local path must not be rewritten to git@: {json}"
);
}
#[test]
fn config_lobes_add_list_remove() {
let sb = Sandbox::new();
let home_a = sb.base.join("lobeA");
let home_b = sb.base.join("lobeB");
let (a, b) = (home_a.display().to_string(), home_b.display().to_string());
assert!(sb.mind(&["config", "lobes", "add", &a]).success);
assert!(sb.mind(&["config", "lobes", "add", &b]).success);
let list = sb.mind(&["config", "lobes", "list"]).stdout;
assert!(list.contains(&a), "{list}");
assert!(list.contains(&b), "{list}");
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(sb.mind(&["learn", "review"]).success);
assert!(std::fs::symlink_metadata(home_a.join("skills/review")).is_ok());
assert!(std::fs::symlink_metadata(home_b.join("skills/review")).is_ok());
assert!(sb.mind(&["config", "lobes", "remove", &a]).success);
let list2 = sb.mind(&["config", "lobes", "list"]).stdout;
assert!(!list2.contains(&a), "{list2}");
assert!(list2.contains(&b), "{list2}");
let bad = sb.mind(&["config", "lobes", "remove", &a]);
assert!(!bad.success);
assert!(
bad.stderr.contains("not a configured agent home"),
"{}",
bad.stderr
);
}
#[test]
fn config_show_creates_default_and_reports_lobes() {
let sb = Sandbox::new();
let cfg_path = sb.mind_home.join("config.toml");
assert!(!cfg_path.exists());
let show = sb.mind(&["config", "show"]);
assert!(show.success, "{}", show.stderr);
assert!(cfg_path.exists(), "config should be created on show");
assert!(show.stdout.contains("config.toml"), "{}", show.stdout);
assert!(show.stdout.contains("lobes"), "{}", show.stdout);
assert!(
show.stdout.contains(&sb.claude_home.display().to_string()),
"default lobe should be the claude home: {}",
show.stdout
);
let home = sb.base.join("shownLobe").display().to_string();
assert!(sb.mind(&["config", "lobes", "add", &home]).success);
assert!(sb.mind(&["config", "show"]).stdout.contains(&home));
}
#[test]
fn config_show_json_emits_one_document() {
let sb = Sandbox::new();
let home = sb.base.join("jsonLobe").display().to_string();
assert!(sb.mind(&["config", "lobes", "add", &home]).success);
let r = sb.mind(&["--json", "config", "show"]);
assert!(r.success, "config show --json: {} {}", r.stdout, r.stderr);
let doc: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"config show --json stdout must parse as a single JSON document \
({e}): {:?}",
r.stdout
)
});
assert!(
doc.get("config_file").is_some(),
"the config-show JSON object must be present, not silently dropped: {doc:#}"
);
assert!(
doc["lobes"]
.as_array()
.is_some_and(|a| a.iter().any(|l| l.as_str() == Some(home.as_str()))),
"the added lobe must appear in the JSON lobes array: {doc:#}"
);
}
#[test]
fn forget_glob_uninstalls_all_matches() {
let sb = melded();
assert!(sb.mind(&["learn", "*"]).success);
assert!(sb.mind(&["recall"]).stdout.contains("skill:review"));
assert!(sb.mind(&["forget", "skill:*"]).success);
assert!(
!sb.mind(&["recall", "skill:review"]).success,
"skill:review should be uninstalled"
);
assert!(
sb.mind(&["recall", "agent:dev"]).success,
"agent:dev should remain installed"
);
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err());
assert!(sb.mind(&["forget", "*", "--yes"]).success);
assert!(!sb.mind(&["recall"]).stdout.contains("installed @"));
let none = sb.mind(&["forget", "zzz*"]);
assert!(!none.success);
assert!(none.stderr.contains("not installed"), "{}", none.stderr);
}
#[test]
fn forget_confirms_before_removing_multiple_items() {
let sb = melded();
assert!(sb.mind(&["learn", "*"]).success);
let r = sb.mind(&["forget", "*"]);
assert!(
!r.success,
"a multi-item forget must refuse without --yes: {}",
r.stdout
);
assert!(
r.stdout.contains("would remove") && r.stdout.contains("skill:review"),
"it must list what would be removed: {}",
r.stdout
);
assert!(
r.stderr.contains("needs confirmation"),
"non-TTY refusal: {}",
r.stderr
);
assert!(
sb.mind(&["recall"]).stdout.contains("skill:review"),
"items must remain after a refused forget"
);
assert!(sb.mind(&["forget", "skill:review"]).success);
}
#[test]
fn unmeld_forgets_all_items_with_yes() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
assert!(sb.mind(&["learn", "dev"]).success);
let refused = sb.mind(&["unmeld", "agents"]);
assert!(
!refused.success,
"must refuse without --yes: {}",
refused.stdout
);
assert!(
refused.stderr.contains("needs confirmation"),
"{}",
refused.stderr
);
assert!(sb.mind(&["recall", "review"]).success, "item remains");
let r = sb.mind(&["unmeld", "agents", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(r.stdout.contains("removed"), "{}", r.stdout);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded")
);
assert!(std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_err());
assert!(std::fs::symlink_metadata(sb.claude_home.join("agents/dev.md")).is_err());
assert!(
sb.source.exists(),
"unmeld --yes must not delete the linked local working tree at {}",
sb.source.display()
);
}
#[test]
fn introspect_fix_relinks_missing_symlink() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let link = sb.claude_home.join("skills/review");
std::fs::remove_file(&link).unwrap();
let r = sb.mind(&["introspect", "--fix"]);
assert!(r.success, "{}", r.stderr);
assert!(r.stdout.contains("relinked"), "{}", r.stdout);
assert!(std::fs::symlink_metadata(&link).is_ok());
assert!(sb.mind(&["introspect"]).stdout.contains("all good"));
}
#[test]
fn sync_upgrade_refreshes_then_applies_upgrades() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let before = sb.mind(&["recall", "skill:review"]).stdout;
sb.edit_source();
let r = sb.mind_with_input(&["sync", "--upgrade"], Some("y\n"));
assert!(r.success, "{}", r.stderr);
assert!(r.stdout.contains("updated"), "sync ran: {}", r.stdout);
assert!(
r.stdout.contains("upgraded skill:review"),
"upgrade applied: {}",
r.stdout
);
let after = sb.mind(&["recall", "skill:review"]).stdout;
assert_ne!(before, after, "commit/hash should have advanced");
}
#[test]
fn probe_and_recall_filter_by_kind_and_source() {
let sb = melded();
let skills = sb.mind(&["probe", "--kind", "skill"]).stdout;
assert!(skills.contains("skill:review"), "{skills}");
assert!(!skills.contains("agent:dev"), "{skills}");
let by_source = sb.mind(&["probe", "--source", "agents"]).stdout;
assert!(by_source.contains("skill:review"), "{by_source}");
let no_source = sb.mind(&["probe", "--source", "nope"]).stdout;
assert!(!no_source.contains("skill:review"), "{no_source}");
assert!(sb.mind(&["learn", "*"]).success);
let only_agents = sb.mind(&["recall", "--kind", "agent"]).stdout;
assert!(only_agents.contains("agent:dev"), "{only_agents}");
assert!(!only_agents.contains("skill:review"), "{only_agents}");
let warned = sb.mind(&["recall", "--sources", "--kind", "skill"]);
assert!(warned.success, "{}", warned.stderr);
assert!(warned.stderr.contains("ignored"), "{}", warned.stderr);
}
#[test]
fn meld_rejects_source_requiring_a_newer_mind() {
let sb = Sandbox::new();
sb.write_and_commit("mind.toml", "[source]\nmin-mind-version = \"9.0\"\n");
let r = sb.mind(&["meld", &sb.source_spec()]);
assert!(!r.success, "should refuse a too-new source");
assert!(r.stderr.contains("requires mind >= 9.0"), "{}", r.stderr);
assert!(r.stderr.contains("mind evolve"), "{}", r.stderr);
assert!(r.stderr.contains("--yes"), "{}", r.stderr);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded")
);
let ok = Sandbox::new();
ok.write_and_commit("mind.toml", "[source]\nmin-mind-version = \"0.0.1\"\n");
let r = ok.mind(&["meld", &ok.source_spec()]);
assert!(r.success, "{}", r.stderr);
assert!(!r.stderr.contains("requires mind"), "{}", r.stderr);
}
#[test]
fn meld_yes_melds_a_source_requiring_a_newer_mind_with_a_warning() {
let sb = Sandbox::new();
sb.write_and_commit("mind.toml", "[source]\nmin-mind-version = \"9.0\"\n");
let r = sb.mind(&["meld", &sb.source_spec(), "--yes"]);
assert!(r.success, "{}", r.stderr);
assert!(
r.stderr.contains("may not work as expected"),
"{}",
r.stderr
);
assert!(r.stderr.contains("mind evolve"), "{}", r.stderr);
assert_eq!(
r.stderr.matches("requires mind >= 9.0").count(),
1,
"{}",
r.stderr
);
assert!(sb.claude_home.join("skills/review").exists());
let probe = sb.mind(&["probe", "--no-tui"]);
assert!(probe.success, "{}", probe.stderr);
assert!(probe.stdout.contains("review"), "{}", probe.stdout);
assert!(
probe.stderr.contains("requires mind >= 9.0"),
"{}",
probe.stderr
);
}
#[test]
fn meld_asks_before_melding_a_source_requiring_a_newer_mind() {
let sb = Sandbox::new();
sb.write_and_commit("mind.toml", "[source]\nmin-mind-version = \"9.0\"\n");
let spec = sb.source_spec();
let no = sb.run(&["meld", &spec], Some("\n"), &[("MIND_TTY", "1")]);
assert!(!no.success, "a bare Enter declines");
assert!(no.stdout.contains("anyway?"), "{}", no.stdout);
assert!(
no.stderr.contains("may not work as expected"),
"{}",
no.stderr
);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded")
);
let yes = sb.run(&["meld", &spec], Some("y\n"), &[("MIND_TTY", "1")]);
assert!(yes.success, "{}", yes.stderr);
assert!(
!sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded")
);
}
#[test]
fn meld_json_on_a_terminal_refuses_a_source_requiring_a_newer_mind_without_asking() {
let sb = Sandbox::new();
sb.write_and_commit("mind.toml", "[source]\nmin-mind-version = \"9.0\"\n");
let r = sb.run(
&["meld", &sb.source_spec(), "--json"],
Some("y\n"),
&[("MIND_TTY", "1")],
);
assert!(!r.success, "declined, as with no terminal");
assert!(!r.stdout.contains("anyway?"), "{}", r.stdout);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"the reply on stdin is never read"
);
}
#[test]
fn meld_names_the_version_gap_when_a_newer_mind_toml_does_not_parse() {
let sb = Sandbox::new();
sb.write_and_commit(
"mind.toml",
"[source]\nmin-mind-version = \"9.0\"\nfuture-key = true\n",
);
let r = sb.mind(&["meld", &sb.source_spec(), "--yes"]);
assert!(!r.success, "an unreadable mind.toml cannot be melded");
assert!(r.stderr.contains("requires mind >= 9.0"), "{}", r.stderr);
assert!(r.stderr.contains("future-key"), "{}", r.stderr);
assert!(r.stderr.contains("mind evolve"), "{}", r.stderr);
}
#[test]
fn config_is_created_with_default_lobe_on_first_use() {
let sb = Sandbox::new();
let cfg_path = sb.mind_home.join("config.toml");
assert!(!cfg_path.exists());
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(cfg_path.exists(), "meld should create the default config");
let body = std::fs::read_to_string(&cfg_path).unwrap();
assert!(body.contains("lobes"), "{body}");
assert!(
body.contains(&sb.claude_home.display().to_string()),
"default lobe should be the claude home: {body}"
);
}
#[test]
fn sync_continues_past_a_failed_source() {
let a = Sandbox::new(); let b = Sandbox::new(); assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &b.source_spec()]).success);
std::fs::remove_dir_all(&b.source).unwrap();
a.edit_source();
let r = a.mind(&["sync"]);
assert!(!r.success, "sync should exit non-zero when a source fails");
assert!(
r.stdout.contains("failed") || r.stderr.contains("failed"),
"broken source reported: {} / {}",
r.stdout,
r.stderr
);
assert!(r.stdout.contains("updated"), "healthy source: {}", r.stdout);
let sources = a.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains(&format!("{}/agents", a.base_name())),
"{sources}"
);
assert!(
sources.contains(&format!("{}/agents", b.base_name())),
"{sources}"
);
}
fn source_head(sb: &Sandbox) -> String {
let out = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&sb.source)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
}
fn recorded_commits(sb: &Sandbox) -> Vec<(String, String)> {
let json = std::fs::read_to_string(sb.mind_home.join("sources.json")).expect("sources.json");
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
v["sources"]
.as_array()
.unwrap()
.iter()
.map(|s| {
(
s["name"].as_str().unwrap().to_string(),
s["commit"].as_str().unwrap_or_default().to_string(),
)
})
.collect()
}
fn advanced_pinned_sources(count: usize) -> Vec<Sandbox> {
let sbs: Vec<Sandbox> = (0..count).map(|_| Sandbox::new()).collect();
for sb in &sbs {
let r = sbs[0].mind(&[
"meld",
&sb.source_spec(),
"--follow-branch",
"main",
"--register-only",
]);
assert!(r.success, "meld: {}", r.stderr);
}
for sb in &sbs {
sb.edit_source();
}
sbs
}
#[test]
fn sync_fetches_many_sources_and_reports_in_registry_order() {
let sbs = advanced_pinned_sources(10);
let r = sbs[0].mind(&["sync"]);
assert!(r.success, "sync: {} / {}", r.stdout, r.stderr);
let names: Vec<String> = recorded_commits(&sbs[0])
.into_iter()
.map(|(n, _)| n)
.collect();
let lines: Vec<&str> = r.stdout.lines().filter(|l| l.contains("syncing")).collect();
assert_eq!(lines.len(), sbs.len(), "{}", r.stdout);
for (line, name) in lines.iter().zip(&names) {
assert!(line.contains(name.as_str()), "order: {}", r.stdout);
assert!(line.contains("updated"), "fetched: {}", r.stdout);
}
let commits: Vec<String> = recorded_commits(&sbs[0])
.into_iter()
.map(|(_, c)| c)
.collect();
let heads: Vec<String> = sbs.iter().map(source_head).collect();
assert_eq!(commits, heads);
}
#[test]
fn upgrade_prepass_fetches_every_source() {
let sbs = advanced_pinned_sources(3);
let r = sbs[0].mind(&["upgrade", "--yes"]);
assert!(r.success, "upgrade: {} / {}", r.stdout, r.stderr);
let commits: Vec<String> = recorded_commits(&sbs[0])
.into_iter()
.map(|(_, c)| c)
.collect();
let heads: Vec<String> = sbs.iter().map(source_head).collect();
assert_eq!(commits, heads);
}
#[cfg(unix)]
fn prompt_requiring_git(sb: &Sandbox) -> (String, PathBuf) {
use std::os::unix::fs::PermissionsExt;
let real_git = std::env::split_paths(&std::env::var_os("PATH").unwrap())
.map(|d| d.join("git"))
.find(|p| p.is_file())
.expect("git on PATH");
let bin = sb.base.join("bin");
let log = sb.base.join("fetches.log");
std::fs::create_dir_all(&bin).unwrap();
let stub = bin.join("git");
std::fs::write(
&stub,
format!(
"#!/bin/sh\n\
if [ \"$1\" = fetch ]; then\n\
echo \"fetch prompt=${{GIT_TERMINAL_PROMPT:-on}}\" >> '{log}'\n\
if [ \"$GIT_TERMINAL_PROMPT\" = 0 ]; then\n\
echo \"fatal: could not read Username for 'x': terminal prompts disabled\" >&2\n\
exit 128\n\
fi\n\
fi\n\
exec '{git}' \"$@\"\n",
log = log.display(),
git = real_git.display()
),
)
.unwrap();
std::fs::set_permissions(&stub, std::fs::Permissions::from_mode(0o755)).unwrap();
let path = std::env::join_paths(
std::iter::once(bin).chain(std::env::split_paths(&std::env::var_os("PATH").unwrap())),
)
.unwrap();
(path.to_str().unwrap().to_string(), log)
}
#[cfg(unix)]
#[test]
fn sync_retries_a_prompt_requiring_fetch_with_prompts_allowed() {
let sbs = advanced_pinned_sources(2);
let (path, log) = prompt_requiring_git(&sbs[0]);
let r = sbs[0].mind_env(&["sync"], &[("PATH", path.as_str())]);
assert!(r.success, "sync: {} / {}", r.stdout, r.stderr);
let log = std::fs::read_to_string(log).unwrap();
assert!(log.contains("prompt=0"), "parallel pass ran: {log}");
let commits: Vec<String> = recorded_commits(&sbs[0])
.into_iter()
.map(|(_, c)| c)
.collect();
let heads: Vec<String> = sbs.iter().map(source_head).collect();
assert_eq!(commits, heads);
}
#[cfg(unix)]
#[test]
fn max_parallel_fetches_of_one_syncs_serially_with_prompts_allowed() {
let sbs = advanced_pinned_sources(3);
let (path, log) = prompt_requiring_git(&sbs[0]);
let r = sbs[0].mind_env(
&["sync"],
&[("PATH", path.as_str()), ("MIND_MAX_PARALLEL_FETCHES", "1")],
);
assert!(r.success, "sync: {} / {}", r.stdout, r.stderr);
let log = std::fs::read_to_string(log).unwrap();
assert_eq!(log.lines().count(), 3, "one fetch per source: {log}");
assert!(
!log.contains("prompt=0"),
"serial fetches may prompt: {log}"
);
let commits: Vec<String> = recorded_commits(&sbs[0])
.into_iter()
.map(|(_, c)| c)
.collect();
let heads: Vec<String> = sbs.iter().map(source_head).collect();
assert_eq!(commits, heads);
}
#[test]
fn recall_json_emits_items_and_sources() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let items = sb.mind(&["recall", "--json"]);
assert!(items.success, "{}", items.stderr);
let env: serde_json::Value =
serde_json::from_str(&items.stdout).expect("recall --json envelope");
assert_eq!(env["schema"], 1, "schema must be 1: {}", items.stdout);
assert!(
env["items"].is_array(),
"items key must be array: {}",
items.stdout
);
assert!(
items.stdout.contains("\"items\""),
"sources carry nested items: {}",
items.stdout
);
assert!(
items.stdout.contains("\"key\": \"skill:review\""),
"{}",
items.stdout
);
assert!(
items.stdout.contains("\"installed\": true"),
"the installed item is flagged: {}",
items.stdout
);
let one = sb.mind(&["recall", "skill:review", "--json"]).stdout;
assert!(one.trim_start().starts_with('{'), "{one}");
assert!(one.contains("\"hash\""), "{one}");
let srcs_r = sb.mind(&["recall", "--sources", "--json"]);
let srcs_env: serde_json::Value =
serde_json::from_str(&srcs_r.stdout).expect("recall --sources --json envelope");
assert_eq!(srcs_env["schema"], 1, "schema must be 1: {}", srcs_r.stdout);
assert!(
srcs_env["items"].is_array(),
"items key must be array: {}",
srcs_r.stdout
);
assert!(srcs_r.stdout.contains("\"url\""), "{}", srcs_r.stdout);
let fresh = Sandbox::new();
let empty_env: serde_json::Value =
serde_json::from_str(fresh.mind(&["recall", "--json"]).stdout.trim())
.expect("empty recall envelope");
assert_eq!(empty_env["schema"], 1);
assert_eq!(
empty_env["items"].as_array().map(|a| a.len()),
Some(0),
"empty recall must emit empty items array"
);
}
#[test]
fn probe_json_emits_rows() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let r = sb.mind(&["probe", "--json"]);
assert!(r.success, "{}", r.stderr);
let env: serde_json::Value = serde_json::from_str(&r.stdout).expect("probe --json envelope");
assert_eq!(env["schema"], 1, "schema must be 1: {}", r.stdout);
let rows = env["items"].as_array().expect("items must be array");
assert!(r.stdout.contains("\"installed\""), "{}", r.stdout);
assert!(r.stdout.contains("\"name\": \"review\""), "{}", r.stdout);
assert!(
rows.iter().any(|row| row["installed"] == true),
"{}",
r.stdout
);
}
fn seed_unmanaged(sb: &Sandbox) {
write(
&sb.claude_home.join("skills/handmade/SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
write(
&sb.claude_home.join("agents/custom.md"),
"---\nname: custom\n---\n# custom\n",
);
}
#[test]
fn recall_shows_unmanaged_lobe_items() {
let sb = melded();
seed_unmanaged(&sb);
let r = sb.mind(&["recall"]);
assert!(r.success, "{}", r.stderr);
assert!(
r.stdout.contains("unmanaged: not installed by mind"),
"recall must surface an unmanaged group: {}",
r.stdout
);
assert!(r.stdout.contains("skill:handmade"), "{}", r.stdout);
assert!(r.stdout.contains("agent:custom"), "{}", r.stdout);
}
#[test]
fn recall_excludes_managed_links_from_unmanaged() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let r = sb.mind(&["recall"]);
assert!(r.success, "{}", r.stderr);
assert!(
!r.stdout.contains("unmanaged: not installed by mind"),
"a mind-installed link must not be reported as unmanaged: {}",
r.stdout
);
}
#[test]
fn probe_lists_and_searches_unmanaged_items() {
let sb = melded();
seed_unmanaged(&sb);
let r = sb.mind(&["probe", "--no-tui"]);
assert!(r.success, "{}", r.stderr);
assert!(
r.stdout.contains("skill:handmade") && r.stdout.contains("(unmanaged)"),
"probe listing must mark the unmanaged item: {}",
r.stdout
);
let s = sb.mind(&["probe", "handmade", "--no-tui"]);
assert!(
s.stdout.contains("skill:handmade"),
"search must find the unmanaged item: {}",
s.stdout
);
let j = sb.mind(&["probe", "handmade", "--json"]);
assert!(
j.stdout.contains("\"unmanaged\": true"),
"json must flag the unmanaged row: {}",
j.stdout
);
}
#[test]
fn forget_unmanaged_removes_after_warning() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
assert!(skill.is_dir());
let r = sb.mind(&["forget", "skill:handmade", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("not managed by mind"),
"the removal must state it is unmanaged: {}",
r.stdout
);
assert!(!skill.exists(), "the unmanaged skill dir must be removed");
}
#[test]
fn forget_unmanaged_refuses_without_yes_in_non_tty() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let r = sb.mind(&["forget", "skill:handmade"]);
assert!(!r.success, "must refuse without --yes: {}", r.stdout);
assert!(
r.stdout.contains("not managed by mind"),
"must state it is unmanaged: {}",
r.stdout
);
assert!(skill.exists(), "nothing may be removed on refusal");
}
#[test]
fn forget_glob_never_sweeps_unmanaged() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let _ = sb.mind(&["forget", "*", "--yes"]);
assert!(
skill.exists(),
"a glob forget must never delete an unmanaged item"
);
}
#[test]
fn forget_unmanaged_bulk_kind_glob_removes_matching() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let agent = sb.claude_home.join("agents/custom.md");
write(&agent, "---\nname: custom\n---\n# custom\n");
let r = sb.mind(&["forget", "--unmanaged", "skill:*", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
r.stdout.to_lowercase().contains("not managed by mind"),
"must state items are not managed: {}",
r.stdout
);
assert!(!skill.exists(), "the unmanaged skill dir must be removed");
assert!(agent.exists(), "the unmanaged agent must be untouched");
}
#[test]
fn forget_unmanaged_bulk_no_ref_removes_all() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let agent = sb.claude_home.join("agents/custom.md");
write(&agent, "---\nname: custom\n---\n# custom\n");
let r = sb.mind(&["forget", "--unmanaged", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(!skill.exists(), "handmade skill must be removed");
assert!(!agent.exists(), "custom agent must be removed");
}
#[test]
fn forget_unmanaged_bulk_never_removes_managed_items() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
let managed_link = sb.claude_home.join("skills/review");
assert!(managed_link.exists(), "managed link must exist after learn");
let unmanaged_skill = sb.claude_home.join("skills/handmade");
write(
&unmanaged_skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let r = sb.mind(&["forget", "--unmanaged", "*", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(!unmanaged_skill.exists(), "unmanaged skill must be removed");
assert!(
managed_link.exists(),
"managed link must survive --unmanaged removal"
);
}
#[test]
fn forget_unmanaged_bulk_refuses_non_tty_without_yes() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let r = sb.mind(&["forget", "--unmanaged", "skill:*"]);
assert!(
!r.success,
"must refuse without --yes in non-TTY: {}",
r.stderr
);
assert!(skill.exists(), "nothing must be removed on refusal");
}
#[test]
fn forget_unmanaged_single_json_on_tty_without_yes_is_confirmation_required() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let r = sb.mind_env(
&["--json", "forget", "skill:handmade"],
&[("MIND_TTY", "1")],
);
assert!(
!r.success,
"--json on a TTY without --yes must refuse: stdout={} stderr={}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout must be a JSON error envelope, got {e}: {:?}",
r.stdout
)
});
assert_eq!(v["schema"], 1, "schema must be 1: {}", r.stdout);
assert_eq!(
v["error"]["kind"], "confirmation-required",
"kind must be confirmation-required: {}",
r.stdout
);
assert!(
skill.exists(),
"the user's own unmanaged file must NOT be deleted: {}",
r.stdout
);
}
#[test]
fn forget_unmanaged_bulk_json_on_tty_without_yes_is_confirmation_required() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let r = sb.mind_env(
&["--json", "forget", "--unmanaged", "skill:*"],
&[("MIND_TTY", "1")],
);
assert!(!r.success, "{} {}", r.stdout, r.stderr);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout must be a JSON error envelope, got {e}: {:?}",
r.stdout
)
});
assert_eq!(v["schema"], 1, "{}", r.stdout);
assert_eq!(v["error"]["kind"], "confirmation-required", "{}", r.stdout);
assert!(skill.exists(), "nothing must be removed: {}", r.stdout);
}
#[test]
fn forget_glob_json_on_tty_without_yes_is_confirmation_required() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
assert!(sb.mind(&["learn", "agent:dev"]).success);
let r = sb.mind_env(&["--json", "forget", "*"], &[("MIND_TTY", "1")]);
assert!(!r.success, "{} {}", r.stdout, r.stderr);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout must be a JSON error envelope, got {e}: {:?}",
r.stdout
)
});
assert_eq!(v["error"]["kind"], "confirmation-required", "{}", r.stdout);
assert!(
sb.mind(&["recall", "skill:review"]).success,
"nothing must have been removed"
);
assert!(sb.mind(&["recall", "agent:dev"]).success);
}
#[test]
fn unmeld_glob_json_on_tty_without_yes_is_confirmation_required() {
let (sb, _tools) = melded_two_sources();
let r = sb.mind_env(&["--json", "unmeld", "*"], &[("MIND_TTY", "1")]);
assert!(!r.success, "{} {}", r.stdout, r.stderr);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout must be a JSON error envelope, got {e}: {:?}",
r.stdout
)
});
assert_eq!(v["error"]["kind"], "confirmation-required", "{}", r.stdout);
let sources = sb.mind(&["recall", "--sources", "--json"]);
let sv: serde_json::Value = serde_json::from_str(sources.stdout.trim()).unwrap_or_else(|e| {
panic!(
"recall --sources --json must be JSON, got {e}: {:?}",
sources.stdout
)
});
assert_eq!(
sv["items"].as_array().map(|a| a.len()),
Some(2),
"both sources must remain melded, none removed: {}",
sources.stdout
);
}
#[test]
fn upgrade_json_on_tty_without_yes_is_confirmation_required() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
sb.edit_source();
assert!(sb.mind(&["sync"]).success);
let before = sb.mind(&["recall", "skill:review"]).stdout;
let r = sb.mind_env(&["--json", "upgrade"], &[("MIND_TTY", "1")]);
assert!(!r.success, "{} {}", r.stdout, r.stderr);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout must be a JSON error envelope, got {e}: {:?}",
r.stdout
)
});
assert_eq!(v["error"]["kind"], "confirmation-required", "{}", r.stdout);
assert_eq!(
before,
sb.mind(&["recall", "skill:review"]).stdout,
"nothing must have been upgraded"
);
}
#[test]
fn evolve_json_on_tty_without_yes_is_confirmation_required() {
let sb = Sandbox::bare("evolve-confirm");
let r = sb.mind_env(
&["--json", "evolve", "--version", "99.0.0"],
&[("MIND_TTY", "1")],
);
assert!(!r.success, "{} {}", r.stdout, r.stderr);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout must be a JSON error envelope, got {e}: {:?}",
r.stdout
)
});
assert_eq!(v["schema"], 1, "{}", r.stdout);
assert_eq!(v["error"]["kind"], "confirmation-required", "{}", r.stdout);
}
#[test]
fn sync_upgrade_yes_applies_without_a_prompt_reply() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
sb.edit_source();
let r = sb.mind(&["sync", "--upgrade", "--yes"]);
assert!(r.success, "{}", r.stderr);
assert!(
!r.stdout.contains("aborted"),
"sync --upgrade --yes must not fall back to the confirmation prompt: {}",
r.stdout
);
assert!(
r.stdout.contains("upgraded skill:review"),
"sync --upgrade --yes must apply the pending upgrade: {}",
r.stdout
);
}
#[test]
fn forget_unmanaged_bulk_no_match_is_not_installed() {
let sb = melded();
let r = sb.mind(&["forget", "--unmanaged", "nope*", "--yes"]);
assert!(!r.success, "must fail when no match: {}", r.stderr);
assert!(
r.stderr.contains("not installed") || r.stderr.contains("nope"),
"error must name the unmatched ref: {}",
r.stderr
);
}
#[test]
fn forget_unmanaged_bulk_json_lists_removed_keys() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let agent = sb.claude_home.join("agents/custom.md");
write(&agent, "---\nname: custom\n---\n# custom\n");
let r = sb.mind(&["forget", "--unmanaged", "*", "--yes", "--json"]);
assert!(r.success, "forget --unmanaged --json failed: {}", r.stderr);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "forget", "{}", r.stdout);
assert_eq!(v["target"], "*", "{}", r.stdout);
assert_eq!(v["outcome"], "removed", "{}", r.stdout);
assert_eq!(
v["removed"],
serde_json::json!(["skill:handmade", "agent:custom"]),
"removed keys must list every removed unmanaged item: {}",
r.stdout
);
assert!(
!r.stdout.contains("forgot") && !r.stdout.contains("not managed by mind"),
"human prose must be absent under --json: {}",
r.stdout
);
assert!(!has_ansi_escape(&r.stdout), "json stdout: {}", r.stdout);
assert!(!skill.exists() && !agent.exists(), "both must be removed");
}
#[test]
fn forget_unmanaged_bulk_short_y_skips_prompt() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let r = sb.mind(&["forget", "--unmanaged", "skill:*", "-y"]);
assert!(
r.success,
"-y must skip the prompt: {} {}",
r.stdout, r.stderr
);
assert!(
!skill.exists(),
"the unmanaged skill must be removed with -y"
);
}
#[test]
fn forget_unmanaged_bulk_via_unlearn_alias() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let r = sb.mind(&["unlearn", "--unmanaged", "--yes"]);
assert!(
r.success,
"unlearn alias must accept --unmanaged: {} {}",
r.stdout, r.stderr
);
assert!(
!skill.exists(),
"the unmanaged skill must be removed via unlearn"
);
}
#[test]
fn forget_unmanaged_bulk_kind_exact_name_removes_one() {
let sb = melded();
let skill = sb.claude_home.join("skills/shared");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# shared\n",
);
let agent = sb.claude_home.join("agents/shared.md");
write(&agent, "---\nname: shared\n---\n# shared\n");
let r = sb.mind(&["forget", "--unmanaged", "agent:shared", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(!agent.exists(), "the agent:shared must be removed");
assert!(
skill.exists(),
"the same-named skill must be untouched by an exact agent: ref"
);
}
#[test]
fn forget_unmanaged_bulk_bare_name_removes_all_kinds() {
let sb = melded();
let skill = sb.claude_home.join("skills/shared");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# shared\n",
);
let agent = sb.claude_home.join("agents/shared.md");
write(&agent, "---\nname: shared\n---\n# shared\n");
let r = sb.mind(&["forget", "--unmanaged", "shared", "--yes"]);
assert!(
r.success,
"a bare shared name must not error under --unmanaged: {} {}",
r.stdout, r.stderr
);
assert!(
!skill.exists() && !agent.exists(),
"both same-named unmanaged items must be removed"
);
}
#[test]
fn forget_unmanaged_bulk_source_qualified_is_not_installed() {
let sb = melded();
let skill = sb.claude_home.join("skills/handmade");
write(
&skill.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let r = sb.mind(&[
"forget",
"--unmanaged",
"owner/repo#skill:handmade",
"--yes",
]);
assert!(
!r.success,
"a source-qualified ref must not match an unmanaged item: {}",
r.stdout
);
assert!(
skill.exists(),
"nothing must be removed when the ref is source-qualified"
);
}
#[test]
fn forget_unmanaged_bulk_removes_from_all_lobes() {
let sb = Sandbox::new();
let home_a = sb.base.join("homeA");
let home_b = sb.base.join("homeB");
write(
&sb.mind_home.join("config.toml"),
&format!(
"lobes = [\"{}\", \"{}\"]\n",
home_a.display(),
home_b.display()
),
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let skill_a = home_a.join("skills/dup");
let skill_b = home_b.join("skills/dup");
write(
&skill_a.join("SKILL.md"),
"---\ndescription: mine\n---\n# dup\n",
);
write(
&skill_b.join("SKILL.md"),
"---\ndescription: mine\n---\n# dup\n",
);
let r = sb.mind(&["forget", "--unmanaged", "skill:dup", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(!skill_a.exists(), "lobe A copy must be removed");
assert!(!skill_b.exists(), "lobe B copy must be removed");
}
#[test]
fn forget_unmanaged_bulk_leaves_manifest_unchanged() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
let manifest = sb.mind_home.join("manifest.json");
let before = std::fs::read_to_string(&manifest).unwrap();
let unmanaged = sb.claude_home.join("skills/handmade");
write(
&unmanaged.join("SKILL.md"),
"---\ndescription: mine\n---\n# handmade\n",
);
let r = sb.mind(&["forget", "--unmanaged", "*", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(!unmanaged.exists(), "unmanaged item removed");
let after = std::fs::read_to_string(&manifest).unwrap();
assert_eq!(
before, after,
"the manifest must be byte-identical after --unmanaged removal"
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("skills/review")).is_ok(),
"the managed review link must survive"
);
}
#[test]
fn probe_fallback_on_non_tty_stdout_produces_listing() {
let sb = melded();
let r = sb.mind(&["probe"]);
assert!(r.success, "probe fallback should succeed: {}", r.stderr);
assert!(r.stdout.contains("skill:review"), "listing: {}", r.stdout);
assert!(r.stdout.contains("agent:dev"), "listing: {}", r.stdout);
assert!(r.stdout.contains("rule:style"), "listing: {}", r.stdout);
assert!(
!r.stdout.contains("\x1b[?1049h"),
"raw-mode alt-screen escape must not appear in fallback output"
);
}
#[test]
fn probe_no_tui_flag_produces_listing() {
let sb = melded();
let r = sb.mind(&["probe", "--no-tui"]);
assert!(r.success, "probe --no-tui should succeed: {}", r.stderr);
assert!(r.stdout.contains("skill:review"), "listing: {}", r.stdout);
assert!(r.stdout.contains("agent:dev"), "listing: {}", r.stdout);
}
#[test]
fn probe_json_flag_produces_json_not_tui() {
let sb = melded();
let r = sb.mind(&["probe", "--json"]);
assert!(r.success, "probe --json should succeed: {}", r.stderr);
let env: serde_json::Value =
serde_json::from_str(&r.stdout).expect("probe --json must produce valid JSON");
assert_eq!(
env["schema"], 1,
"probe --json must produce envelope: {}",
r.stdout
);
assert!(
env["items"].is_array(),
"probe --json envelope must have items array: {}",
r.stdout
);
assert!(
!r.stdout.contains("\x1b[?1049h"),
"probe --json must not enter alt-screen"
);
}
#[test]
fn probe_fallback_with_query_filters_listing() {
let sb = melded();
let r = sb.mind(&["probe", "--no-tui", "review"]);
assert!(
r.success,
"probe --no-tui query should succeed: {}",
r.stderr
);
assert!(r.stdout.contains("skill:review"), "listing: {}", r.stdout);
assert!(!r.stdout.contains("agent:dev"), "filtered: {}", r.stdout);
}
#[test]
fn probe_fallback_with_kind_filter_narrows_listing() {
let sb = melded();
let r = sb.mind(&["probe", "--no-tui", "--kind", "skill"]);
assert!(
r.success,
"probe --no-tui --kind should succeed: {}",
r.stderr
);
assert!(r.stdout.contains("skill:review"), "listing: {}", r.stdout);
assert!(!r.stdout.contains("agent:dev"), "filtered: {}", r.stdout);
assert!(!r.stdout.contains("rule:style"), "filtered: {}", r.stdout);
}
#[test]
fn probe_fallback_seed_query_with_no_tui() {
let sb = melded();
let r1 = sb.mind(&["probe", "review"]);
let r2 = sb.mind(&["probe", "--no-tui", "review"]);
assert!(r1.success);
assert!(r2.success);
assert_eq!(
r1.stdout, r2.stdout,
"--no-tui must not change filter behavior"
);
}
#[test]
fn probe_fallback_with_source_filter_narrows_listing() {
let sb = melded();
let matched = sb.mind(&["probe", "--no-tui", "--source", "agents"]);
assert!(
matched.success,
"probe --no-tui --source should succeed: {}",
matched.stderr
);
assert!(
matched.stdout.contains("skill:review"),
"matching source listing: {}",
matched.stdout
);
let unmatched = sb.mind(&["probe", "--no-tui", "--source", "nonesuch"]);
assert!(
unmatched.success,
"probe --no-tui --source nonesuch should succeed: {}",
unmatched.stderr
);
assert!(
!unmatched.stdout.contains("skill:review"),
"a non-matching --source must exclude items: {}",
unmatched.stdout
);
}
#[test]
fn probe_non_tty_returns_promptly_and_does_not_hang() {
use std::time::{Duration, Instant};
let sb = melded();
let mut child = Command::new(env!("CARGO_BIN_EXE_mind"))
.args(["probe"])
.env("MIND_HOME", &sb.mind_home)
.env("CLAUDE_HOME", &sb.claude_home)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn mind probe");
let deadline = Instant::now() + Duration::from_secs(10);
loop {
match child.try_wait().expect("try_wait") {
Some(status) => {
assert!(status.success(), "non-TTY probe should exit successfully");
break;
}
None => {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
panic!(
"non-TTY `mind probe` did not exit within 10s - it likely entered the TUI event loop instead of falling back"
);
}
std::thread::sleep(Duration::from_millis(25));
}
}
}
let out = child.wait_with_output().expect("collect output");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("skill:review"),
"fallback listing expected: {stdout}"
);
assert!(
!stdout.contains("\x1b[?1049h"),
"non-TTY probe must not enter the alt-screen"
);
}
#[test]
fn introspect_json_emits_report() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let clean = sb.mind(&["introspect", "--json"]).stdout;
assert!(clean.trim_start().starts_with('{'), "{clean}");
let v: serde_json::Value = serde_json::from_str(&clean).expect("valid JSON");
assert_eq!(v["schema"], 1, "schema field must be 1: {clean}");
assert!(v["issues"].is_array(), "issues must be an array: {clean}");
assert!(
v["sources"].is_number(),
"sources must be an integer count: {clean}"
);
assert!(
v["items"].is_number(),
"items must be an integer count: {clean}"
);
std::fs::remove_file(sb.claude_home.join("skills/review")).unwrap();
let broken = sb.mind(&["introspect", "--json"]).stdout;
let bv: serde_json::Value = serde_json::from_str(&broken).expect("valid JSON");
assert_eq!(
bv["schema"], 1,
"schema present on error report too: {broken}"
);
assert!(broken.contains("\"missing-link\""), "{broken}");
}
#[test]
fn introspect_survives_a_source_scan_failure_and_still_reports_the_healthy_source() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let broken = Sandbox::bare("broken-src");
broken.write_and_commit(
"skills/x/SKILL.md",
"---\nname: x\ndescription: x skill\n---\n# x\n",
);
assert!(sb.mind(&["meld", &broken.source_spec()]).success);
std::fs::remove_dir_all(&broken.source).unwrap();
sb.edit_source();
let r = sb.mind(&["introspect", "--json"]);
let v: serde_json::Value =
serde_json::from_str(&r.stdout).expect("introspect must still emit valid JSON");
let issues = v["issues"].as_array().expect("issues must be an array");
let scan_failed = issues
.iter()
.find(|i| i["kind"] == "source-scan-failed")
.unwrap_or_else(|| panic!("must report source-scan-failed: {}", r.stdout));
assert!(
scan_failed["target"]
.as_str()
.unwrap()
.contains("broken-src"),
"source-scan-failed must name the broken source: {}",
r.stdout
);
assert!(
issues
.iter()
.any(|i| i["kind"] == "drifted" && i["target"] == "skill:review"),
"the healthy source's drift must still be reported: {}",
r.stdout
);
}
#[test]
fn upgrade_names_an_unscannable_source_instead_of_only_up_to_date() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let broken = Sandbox::bare("broken-src-2");
broken.write_and_commit(
"skills/x/SKILL.md",
"---\nname: x\ndescription: x skill\n---\n# x\n",
);
assert!(sb.mind(&["meld", &broken.source_spec()]).success);
std::fs::remove_dir_all(&broken.source).unwrap();
let r = sb.mind(&["upgrade", "--yes"]);
let combined = format!("{}{}", r.stdout, r.stderr);
assert!(
!r.stdout
.trim()
.eq_ignore_ascii_case("everything is up to date"),
"must not silently report only up to date: stdout={}",
r.stdout
);
assert!(
combined.contains("broken-src-2"),
"must name the unscannable source: stdout={} stderr={}",
r.stdout,
r.stderr
);
assert!(
combined.contains("sync") || combined.contains("introspect"),
"must point at a way to diagnose the source: stdout={} stderr={}",
r.stdout,
r.stderr
);
}
#[test]
fn completions_emit_a_shell_script() {
let sb = Sandbox::new();
let r = sb.mind(&["completions", "bash"]);
assert!(r.success, "{}", r.stderr);
assert!(r.stdout.contains("_mind"), "{}", r.stdout);
assert!(r.stdout.contains("complete"), "{}", r.stdout);
assert!(!sb.mind(&["completions", "tcsh"]).success);
}
#[test]
fn relative_lobe_is_canonicalized_to_absolute() {
let sb = Sandbox::new();
write(&sb.mind_home.join("config.toml"), "lobes = [\"rellobe\"]\n");
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let r = sb.mind_cwd(&["learn", "review"], &sb.base);
assert!(r.success, "{}", r.stderr);
let link = sb.base.join("rellobe/skills/review");
assert!(
std::fs::symlink_metadata(&link).is_ok(),
"link should be created at the resolved absolute path {link:?}"
);
let detail = sb.mind(&["recall", "skill:review"]).stdout;
assert!(
detail.contains(&link.display().to_string()),
"recorded link should be the absolute path: {detail}"
);
assert!(sb.mind_cwd(&["forget", "review"], &sb.mind_home).success);
assert!(
std::fs::symlink_metadata(&link).is_err(),
"link should be gone"
);
}
#[test]
fn unguarded_ref_warning_scans_all_files_of_an_item() {
let sb = Sandbox::new();
sb.write_and_commit(
"skills/lead/SKILL.md",
"---\nname: lead\ndescription: lead skill\n---\n# lead\n",
);
sb.write_and_commit("skills/lead/NOTES.md", "Run the review skill first.\n");
let r = sb.mind(&["--verbose", "meld", &sb.source_spec(), "--as", "jk"]);
assert!(r.success, "{}", r.stderr);
assert!(
r.stderr.contains("skill:jk:lead") && r.stderr.contains("review"),
"warning should cite a sibling ref found in a non-SKILL.md file: {}",
r.stderr
);
}
#[test]
fn example_namespacing_expands_references() {
let jk = Sandbox::from_example("namespacing");
let meld = jk.mind(&["--verbose", "meld", &jk.source_spec(), "--as", "jk"]);
assert!(meld.success, "{}", meld.stderr);
assert!(
!meld.stderr.contains("references sibling(s) in prose"),
"all refs are tokens, so no warning: {}",
meld.stderr
);
assert!(jk.mind(&["learn", "jk:lead", "--yes"]).success);
let lead = std::fs::read_to_string(jk.mind_home.join("store/agent/jk:lead")).unwrap();
assert!(lead.contains("the dev agent"), "{lead}");
assert!(
!lead.contains("jk:dev"),
"agent token must not be prefixed: {lead}"
);
assert!(lead.contains("the jk:review skill"), "{lead}");
assert!(lead.contains("the jk:style rule"), "{lead}");
assert!(!lead.contains("{{ns:"), "tokens should be gone: {lead}");
assert!(jk.mind(&["learn", "jk:review", "--yes"]).success);
let review =
std::fs::read_to_string(jk.mind_home.join("store/skill/jk:review/SKILL.md")).unwrap();
assert!(review.contains("jk:style rule"), "{review}");
assert!(!review.contains("{{ns:"), "tokens should be gone: {review}");
let bare = Sandbox::from_example("namespacing");
assert!(bare.mind(&["meld", &bare.source_spec()]).success);
assert!(bare.mind(&["learn", "lead", "--yes"]).success);
let lead2 = std::fs::read_to_string(bare.mind_home.join("store/agent/lead")).unwrap();
assert!(lead2.contains("the dev agent"), "{lead2}");
assert!(lead2.contains("the review skill"), "{lead2}");
assert!(lead2.contains("the style rule"), "{lead2}");
assert!(!lead2.contains("{{ns:"), "{lead2}");
}
#[test]
fn example_starter_convention_discovery() {
let sb = Sandbox::from_example("starter");
let meld = sb.mind(&["meld", &sb.source_spec()]);
assert!(meld.success, "{}", meld.stderr);
let probe = sb.mind(&["probe"]);
assert!(probe.success, "{}", probe.stderr);
assert!(probe.stdout.contains("skill:greet"), "{}", probe.stdout);
assert!(probe.stdout.contains("agent:scribe"), "{}", probe.stdout);
assert!(probe.stdout.contains("rule:tone"), "{}", probe.stdout);
assert!(probe.stdout.contains("workflow:hello"), "{}", probe.stdout);
let by_desc = sb.mind(&["probe", "plain"]);
assert!(by_desc.success, "{}", by_desc.stderr);
assert!(by_desc.stdout.contains("rule:tone"), "{}", by_desc.stdout);
assert!(
!by_desc.stdout.contains("agent:scribe"),
"a description-only match should not list unrelated items: {}",
by_desc.stdout
);
assert!(sb.mind(&["learn", "greet"]).success);
assert!(
sb.mind_home.join("store/skill/greet/SKILL.md").exists(),
"greet should be copied into the store"
);
let learn_wf = sb.mind(&["learn", "hello"]);
assert!(learn_wf.success, "{}\n{}", learn_wf.stdout, learn_wf.stderr);
assert!(
sb.mind_home.join("store/workflow/hello").exists(),
"hello workflow should be copied into the store"
);
let wf_link = sb.claude_home.join("workflows/hello.js");
assert!(
std::fs::symlink_metadata(&wf_link)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false),
"hello workflow must be linked at workflows/hello.js: {:?}",
wf_link
);
}
#[test]
fn root_mindfile_exposes_hello() {
let sb = Sandbox::from_root_mindfile();
let meld = sb.mind(&["meld", &sb.source_spec()]);
assert!(meld.success, "{}", meld.stderr);
let probe = sb.mind(&["probe"]);
assert!(probe.success, "{}", probe.stderr);
assert!(
probe.stdout.contains("skill:hello-mind"),
"{}",
probe.stdout
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(sources.success, "{}", sources.stderr);
assert!(
sources.stdout.contains("anthropics-skills")
&& sources.stdout.contains("awesome-claude-skills"),
"both curated sources must be registered: {}",
sources.stdout
);
assert!(
probe.stdout.contains("skill:astand"),
"the curated stand-in's item must be available to browse: {}",
probe.stdout
);
assert!(
!sb.claude_home.join("skills/astand").exists(),
"a register-only curated item must NOT be installed on meld"
);
let learn = sb.mind(&["learn", "hello-mind"]);
assert!(learn.success, "{}", learn.stderr);
assert!(
sb.mind_home
.join("store/skill/hello-mind/SKILL.md")
.exists(),
"hello-mind should be copied into the store"
);
assert!(
sb.claude_home.join("skills/hello-mind").exists(),
"hello-mind should be linked into the agent home"
);
}
#[test]
fn example_policy_validates() {
let sb = Sandbox::new();
let policy = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/policy/policy.toml");
let r = sb.mind(&["review", "--policy", policy.to_str().unwrap()]);
assert!(
r.success,
"example policy must validate clean:\nstdout: {}\nstderr: {}",
r.stdout, r.stderr
);
}
#[test]
fn example_tooling_expands_path_tokens() {
let sb = Sandbox::from_example("tooling");
let meld = sb.mind(&["meld", &sb.source_spec()]);
assert!(meld.success, "{}", meld.stderr);
assert!(sb.mind(&["learn", "scan", "--yes"]).success);
let skill = std::fs::read_to_string(sb.mind_home.join("store/skill/scan/SKILL.md")).unwrap();
assert!(
skill.contains("store/tool/detect/detect.sh"),
"{{tools:detect}} expands to the tool entrypoint: {skill}"
);
assert!(
skill.contains("store/tool/detect/lib.sh"),
"{{path:tool:detect}} reaches a non-entrypoint file: {skill}"
);
assert!(
skill.contains("store/skill/scan"),
"{{self}} expands to the skill's own store dir: {skill}"
);
assert!(
!skill.contains("{{tools:") && !skill.contains("{{self") && !skill.contains("{{path:"),
"tokens should be gone: {skill}"
);
assert!(
sb.mind_home.join("store/tool/detect/detect.sh").exists(),
"the detect tool should be copied into the store"
);
assert!(
sb.mind_home.join("store/resource/markers.json").is_file(),
"the markers.json resource should be copied into the store"
);
assert!(
skill.contains("store/resource/markers.json") && !skill.contains("{{resources:"),
"{{resources:markers.json}} expands to the resource: {skill}"
);
let detect_sh =
std::fs::read_to_string(sb.mind_home.join("store/tool/detect/detect.sh")).unwrap();
assert!(
detect_sh.contains("{{tools:detect}}"),
"a token in a non-markdown file is left exactly as written: {detect_sh}"
);
let lib_sh = std::fs::read_to_string(sb.mind_home.join("store/tool/detect/lib.sh")).unwrap();
assert!(
lib_sh.contains("{{path:tool:detect}}") && lib_sh.contains("{{tools:detect}}"),
"a token in a non-markdown file is left exactly as written: {lib_sh}"
);
}
#[test]
fn example_hooks_lists_declared_hooks() {
let sb = Sandbox::new();
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/hooks");
let r = sb.mind(&["review", dir.to_str().unwrap()]);
assert!(r.success, "stdout: {}\nstderr: {}", r.stdout, r.stderr);
let out = format!("{}{}", r.stdout, r.stderr);
assert!(
out.contains("install hook"),
"discloses an install hook: {out}"
);
assert!(
out.contains("uninstall hook"),
"discloses the uninstall hook: {out}"
);
}
#[test]
fn example_monorepo_roots_discovery() {
let sb = Sandbox::from_example("monorepo");
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let probe = sb.mind(&["probe"]);
assert!(probe.success, "{}", probe.stderr);
assert!(
probe.stdout.contains("skill:deploy"),
"found under packages/web: {}",
probe.stdout
);
assert!(
probe.stdout.contains("agent:release"),
"found under packages/cli: {}",
probe.stdout
);
}
#[test]
fn example_explicit_inventory_offers_only_listed() {
let sb = Sandbox::from_example("explicit");
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let probe = sb.mind(&["probe"]);
assert!(probe.success, "{}", probe.stderr);
assert!(probe.stdout.contains("rule:style"), "{}", probe.stdout);
assert!(probe.stdout.contains("skill:scan"), "{}", probe.stdout);
assert!(
!probe.stdout.contains("internal"),
"an unlisted file is not offered: {}",
probe.stdout
);
}
#[test]
fn example_explicit_item_hooks_fire() {
let sb = Sandbox::from_example("explicit");
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let learn = sb.mind(&["learn", "scan", "--accept-hooks=install,update,uninstall"]);
assert!(learn.success, "{} {}", learn.stdout, learn.stderr);
assert!(
learn.stdout.contains("explicit-example: scan installed"),
"the install hook must fire at learn (HOOK-81): {}",
learn.stdout
);
let forget = sb.mind(&["forget", "scan", "--accept-hooks=install,update,uninstall"]);
assert!(forget.success, "{} {}", forget.stdout, forget.stderr);
assert!(
forget.stdout.contains("explicit-example: scan removed"),
"the uninstall hook must fire at forget (HOOK-82): {}",
forget.stdout
);
}
#[test]
fn example_discover_kind_globs() {
let sb = Sandbox::from_example("discover");
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let probe = sb.mind(&["probe"]);
assert!(probe.success, "{}", probe.stderr);
assert!(
probe.stdout.contains("skill:alpha"),
"skill glob matches packages/a/skills/alpha/SKILL.md, item = parent dir (DSC-33): {}",
probe.stdout
);
assert!(
probe.stdout.contains("agent:beta"),
"agent glob matches packages/b/agents/beta.md, item = stem (DSC-33): {}",
probe.stdout
);
assert!(
!probe.stdout.contains("secret"),
"internal/skills/secret/SKILL.md is dropped by the exclude glob (DSC-37): {}",
probe.stdout
);
assert_eq!(
probe.stdout.matches("skill:").count() + probe.stdout.matches("agent:").count(),
2,
"only the two glob-matched items are discovered: {}",
probe.stdout
);
}
#[test]
fn example_super_source_validates() {
let sb = Sandbox::new();
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/super-source");
let r = sb.mind(&["review", dir.to_str().unwrap()]);
assert!(
r.success,
"super-source example must validate clean:\nstdout: {}\nstderr: {}",
r.stdout, r.stderr
);
}
#[test]
fn example_super_source_curate_reports_up_to_date() {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-it-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let sb = Sandbox {
base: base.clone(),
source: base.clone(),
mind_home: base.join("mind"),
claude_home: base.join("claude"),
};
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/super-source");
let meld = sb.mind(&["meld", dir.to_str().unwrap(), "--yes"]);
assert!(
meld.success,
"super-source example meld failed:\nstdout: {}\nstderr: {}",
meld.stdout, meld.stderr
);
let r = sb.mind(&["curate", "--check"]);
assert!(r.success, "curate failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("up to date"),
"the shipped super-source example must be curate-clean right after melding: {}",
r.stdout
);
}
#[test]
fn example_drift_upgrade() {
let sb = Sandbox::from_example("drift");
let meld = sb.mind(&["meld", &sb.source_spec()]);
assert!(meld.success, "{}", meld.stderr);
assert!(sb.mind(&["learn", "audit"]).success);
let fresh = sb.mind(&["--ascii", "recall"]);
assert!(fresh.success, "{}", fresh.stderr);
assert!(
!fresh.stdout.contains("outdated"),
"freshly installed audit must not be outdated: {}",
fresh.stdout
);
write(
&sb.source.join("skills/audit/SKILL.md"),
"---\nname: audit\ndescription: Audit the change\n---\n# audit skill\nedited body\n",
);
git(&sb.source, &["commit", "-aqm", "edit audit"]);
assert!(sb.mind(&["sync"]).success);
let stale = sb.mind(&["--ascii", "recall"]);
assert!(stale.success, "{}", stale.stderr);
let line = stale
.stdout
.lines()
.find(|l| l.contains("skill:audit"))
.unwrap_or_else(|| panic!("no audit line in recall output: {}", stale.stdout));
assert_eq!(
line.trim_start().chars().next(),
Some('^'),
"an outdated install must lead with the `^` stale marker: {line:?}"
);
assert!(
line.contains("(outdated"),
"the stale line must carry the (outdated; run mind upgrade) text: {line:?}"
);
let ins = sb.mind(&["--ascii", "introspect"]);
assert!(
ins.stdout.contains("skill:audit") && ins.stdout.contains("upstream changed"),
"introspect must report audit's upstream change: {}",
ins.stdout
);
assert!(
ins.stdout.contains("issue(s) found") && !ins.stdout.contains("0 issue(s) found"),
"introspect must report a nonzero issue count: {}",
ins.stdout
);
let up = sb.mind(&["--ascii", "upgrade", "--yes"]);
assert!(up.success, "{} {}", up.stdout, up.stderr);
assert!(
up.stdout.contains("hash") && up.stdout.contains("->"),
"upgrade must report the hash delta with an arrow: {}",
up.stdout
);
assert!(
up.stdout.contains("commit"),
"upgrade must report the commit delta: {}",
up.stdout
);
assert!(
up.stdout.contains("upgraded skill:audit"),
"upgrade must apply audit under the same name: {}",
up.stdout
);
let after = sb.mind(&["--ascii", "recall"]);
assert!(after.success, "{}", after.stderr);
let line = after
.stdout
.lines()
.find(|l| l.contains("skill:audit"))
.unwrap_or_else(|| panic!("no audit line in recall output: {}", after.stdout));
assert_eq!(
line.trim_start().chars().next(),
Some('+'),
"a current install must lead with the `+` marker after upgrade: {line:?}"
);
assert!(
!line.contains("(outdated"),
"the line must not carry the outdated text after upgrade: {line:?}"
);
}
#[test]
fn example_multi_lobe_links_into_all_homes() {
let sb = Sandbox::from_example("multi-lobe");
let lobe_a = sb.base.join("lobe-a");
let lobe_b = sb.base.join("lobe-b");
write(
&sb.mind_home.join("config.toml"),
&format!(
"lobes = [\"{}\", \"{}\"]\n",
lobe_a.display(),
lobe_b.display()
),
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let learn = sb.mind(&["learn", "recap"]);
assert!(learn.success, "{} {}", learn.stdout, learn.stderr);
let link_a = lobe_a.join("skills/recap");
let link_b = lobe_b.join("skills/recap");
assert!(
std::fs::symlink_metadata(&link_a).is_ok(),
"recap must be linked into lobe A"
);
assert!(
std::fs::symlink_metadata(&link_b).is_ok(),
"recap must be linked into lobe B"
);
let store = sb.mind_home.join("store/skill/recap");
assert_eq!(
std::fs::canonicalize(&link_a).unwrap(),
std::fs::canonicalize(&store).unwrap(),
"lobe A link must point at the store copy"
);
assert_eq!(
std::fs::canonicalize(&link_b).unwrap(),
std::fs::canonicalize(&store).unwrap(),
"lobe B link must point at the store copy"
);
let forget = sb.mind(&["forget", "recap"]);
assert!(forget.success, "{} {}", forget.stdout, forget.stderr);
assert!(
std::fs::symlink_metadata(&link_a).is_err(),
"forget must remove the link from lobe A"
);
assert!(
std::fs::symlink_metadata(&link_b).is_err(),
"forget must remove the link from lobe B"
);
}
#[test]
fn example_absorb_claims_unmanaged_item() {
let sb = Sandbox::new();
write(
&sb.claude_home.join("skills/notes/SKILL.md"),
"---\ndescription: my personal notes skill\n---\n# notes\n",
);
let before = sb.mind(&["--ascii", "recall"]);
assert!(before.success, "{}", before.stderr);
assert!(
before.stdout.contains("unmanaged: not installed by mind"),
"recall must surface the unmanaged group before absorb: {}",
before.stdout
);
assert!(
before.stdout.contains("skill:notes"),
"recall must list notes as unmanaged before absorb: {}",
before.stdout
);
let target = sb.base.join("absorb-target");
std::fs::create_dir_all(&target).unwrap();
git(&target, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&target, &["config", "user.email", "t@t"]);
git(&target, &["config", "user.name", "t"]);
git(&target, &["commit", "-q", "--allow-empty", "-m", "init"]);
let target_spec = target.to_string_lossy().into_owned();
let absorb = sb.mind(&[
"--ascii",
"absorb",
"skill:notes",
"--to",
&target_spec,
"--yes",
]);
assert!(absorb.success, "{} {}", absorb.stdout, absorb.stderr);
assert!(
absorb
.stdout
.contains("absorbed skill:notes -> managed as skill:notes"),
"absorb must report the managed result: {}",
absorb.stdout
);
assert!(
target.join("skills/notes/SKILL.md").exists(),
"the absorbed file must live in the target at skills/notes/SKILL.md"
);
let lobe_path = sb.claude_home.join("skills/notes");
let meta = std::fs::symlink_metadata(&lobe_path).expect("lobe path must exist after absorb");
assert!(
meta.file_type().is_symlink(),
"the lobe path must be a managed symlink after absorb"
);
assert_eq!(
std::fs::canonicalize(&lobe_path).unwrap(),
std::fs::canonicalize(sb.mind_home.join("store/skill/notes")).unwrap(),
"the lobe link must point into the store"
);
let after = sb.mind(&["--ascii", "recall"]);
assert!(after.success, "{}", after.stderr);
let line = after
.stdout
.lines()
.find(|l| l.contains("skill:notes"))
.unwrap_or_else(|| panic!("no notes line in recall output: {}", after.stdout));
assert!(
line.contains("installed @"),
"notes must be a managed installed item after absorb: {line:?}"
);
assert!(
!after.stdout.contains("unmanaged: not installed by mind"),
"notes must no longer be reported as unmanaged after absorb: {}",
after.stdout
);
}
#[test]
fn man_page_renders_roff() {
let sb = Sandbox::new();
let r = sb.mind(&["man"]);
assert!(r.success, "{}", r.stderr);
assert!(r.stdout.contains(".TH"), "{}", r.stdout);
assert!(r.stdout.to_lowercase().contains("mind"), "{}", r.stdout);
assert!(
r.stdout.contains(".TH mind-meld") || r.stdout.contains(".TH MIND-MELD"),
"mind-meld page must follow the top-level page: {}",
&r.stdout[..r.stdout.len().min(2000)]
);
assert!(
r.stdout.contains(".TH mind-learn") || r.stdout.contains(".TH MIND-LEARN"),
"mind-learn page must follow the top-level page"
);
}
fn spawn_mind(
mind_home: &std::path::Path,
claude_home: &std::path::Path,
args: &[&str],
) -> std::process::Child {
Command::new(env!("CARGO_BIN_EXE_mind"))
.args(args)
.env("MIND_HOME", mind_home)
.env("CLAUDE_HOME", claude_home)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn mind")
}
#[test]
fn concurrent_mutating_commands_both_succeed_no_lost_update() {
let a = Sandbox::new();
let b = Sandbox::named("tools");
let mind_home = &a.mind_home;
let claude_home = &a.claude_home;
let a_spec = a.source_spec();
let b_spec = b.source_spec();
let mut child_a = spawn_mind(mind_home, claude_home, &["meld", &a_spec]);
let mut child_b = spawn_mind(mind_home, claude_home, &["meld", &b_spec]);
let status_a = child_a.wait().expect("wait a");
let status_b = child_b.wait().expect("wait b");
assert!(status_a.success(), "first meld failed");
assert!(status_b.success(), "second meld failed");
let sources = a.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("agents"),
"first source missing: {sources}"
);
assert!(
sources.contains("tools"),
"second source missing: {sources}"
);
}
#[test]
fn concurrent_learn_commands_both_effects_survive() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let mind_home = &sb.mind_home;
let claude_home = &sb.claude_home;
let mut child_a = spawn_mind(mind_home, claude_home, &["learn", "review"]);
let mut child_b = spawn_mind(mind_home, claude_home, &["learn", "dev"]);
let status_a = child_a.wait().expect("wait a");
let status_b = child_b.wait().expect("wait b");
assert!(status_a.success(), "learn review failed");
assert!(status_b.success(), "learn dev failed");
let recall = sb.mind(&["recall"]).stdout;
assert!(recall.contains("skill:review"), "review lost: {recall}");
assert!(recall.contains("agent:dev"), "dev lost: {recall}");
}
#[test]
fn three_concurrent_learns_no_lost_update() {
for _ in 0..15 {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let mind_home = &sb.mind_home;
let claude_home = &sb.claude_home;
let mut ca = spawn_mind(mind_home, claude_home, &["learn", "review"]);
let mut cb = spawn_mind(mind_home, claude_home, &["learn", "dev"]);
let mut cc = spawn_mind(mind_home, claude_home, &["learn", "style"]);
assert!(ca.wait().expect("wait a").success(), "learn review failed");
assert!(cb.wait().expect("wait b").success(), "learn dev failed");
assert!(cc.wait().expect("wait c").success(), "learn style failed");
let recall = sb.mind(&["recall"]);
assert!(recall.success, "recall failed: {}", recall.stderr);
assert!(
recall.stdout.contains("skill:review"),
"review lost: {}",
recall.stdout
);
assert!(
recall.stdout.contains("agent:dev"),
"dev lost: {}",
recall.stdout
);
assert!(
recall.stdout.contains("rule:style"),
"style lost: {}",
recall.stdout
);
}
}
#[test]
fn concurrent_reader_and_writer_reader_does_not_see_torn_file() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let mind_home = &sb.mind_home;
let claude_home = &sb.claude_home;
for _ in 0..40 {
let mut writer = spawn_mind(mind_home, claude_home, &["learn", "review"]);
let reader1 = spawn_mind(mind_home, claude_home, &["recall"]);
let reader2 = spawn_mind(mind_home, claude_home, &["recall", "--sources"]);
let ws = writer.wait().expect("wait writer");
let r1 = reader1.wait_with_output().expect("wait reader1");
let r2 = reader2.wait_with_output().expect("wait reader2");
assert!(ws.success(), "writer failed");
assert!(
r1.status.success(),
"recall errored during concurrent write: {}",
String::from_utf8_lossy(&r1.stderr)
);
assert!(
r2.status.success(),
"recall --sources errored during concurrent write: {}",
String::from_utf8_lossy(&r2.stderr)
);
let err1 = String::from_utf8_lossy(&r1.stderr);
assert!(
!err1.contains("expected") && !err1.to_lowercase().contains("json"),
"reader saw a torn/partial file: {err1}"
);
sb.mind(&["forget", "review"]);
}
}
#[test]
fn exclusive_lock_blocks_second_writer_until_first_completes() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let mut c1 = spawn_mind(&sb.mind_home, &sb.claude_home, &["meld", &spec]);
let mut c2 = spawn_mind(&sb.mind_home, &sb.claude_home, &["meld", &spec]);
let _ = c1.wait();
let _ = c2.wait();
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.success,
"recall failed after concurrent melds: {}",
sources.stderr
);
let entry_lines: Vec<_> = sources
.stdout
.lines()
.filter(|l| !l.trim().is_empty() && !l.contains("melded source"))
.collect();
assert_eq!(
entry_lines.len(),
1,
"expected exactly one source entry, got {}: {}",
entry_lines.len(),
sources.stdout
);
}
fn make_pinnable_repo(name: &str) -> (Sandbox, String, String) {
let sb = Sandbox::bare(name);
write(
&sb.source.join("agents/dev.md"),
"---\nname: dev\ndescription: dev agent v1\n---\n# dev v1\n",
);
git(&sb.source, &["add", "-A"]);
git(&sb.source, &["commit", "-qm", "initial"]);
let sha_v1 = {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&sb.source)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
};
git(&sb.source, &["tag", "v1.0"]);
git(&sb.source, &["branch", "stable"]);
write(
&sb.source.join("agents/dev.md"),
"---\nname: dev\ndescription: dev agent v2\n---\n# dev v2\n",
);
git(&sb.source, &["commit", "-aqm", "v2 commit"]);
let sha_v2 = {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&sb.source)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
};
(sb, sha_v1, sha_v2)
}
fn read_source_pin_json(sb: &Sandbox) -> String {
let json = std::fs::read_to_string(sb.mind_home.join("sources.json")).expect("sources.json");
let start = json.find("\"pin\":").expect("pin key in sources.json");
let after = &json[start..];
let obj_start = after.find('{').expect("pin object open brace");
let obj_str = &after[obj_start..];
let mut depth = 0usize;
let mut end = 0;
for (i, c) in obj_str.char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = i + 1;
break;
}
}
_ => {}
}
}
obj_str[..end].to_string()
}
fn read_source_commit(sb: &Sandbox) -> String {
let json = std::fs::read_to_string(sb.mind_home.join("sources.json")).expect("sources.json");
let key = "\"commit\": \"";
let start = json.find(key).expect("commit key") + key.len();
let end = json[start..].find('"').expect("closing quote") + start;
json[start..end].to_string()
}
#[test]
fn meld_follow_branch_clones_named_branch_and_persists_pin() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-follow");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--follow-branch", "stable"]);
assert!(r.success, "meld --follow-branch: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(commit, sha_v1, "follow-branch=stable should record sha_v1");
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("follow-branch"),
"pin kind should be follow-branch: {pin_json}"
);
assert!(
pin_json.contains("stable"),
"pin value should be stable: {pin_json}"
);
}
#[test]
fn meld_pin_tag_clones_at_tag_and_persists_pin() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-tag");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin-tag", "v1.0"]);
assert!(r.success, "meld --pin-tag: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(commit, sha_v1, "pin-tag=v1.0 should record sha_v1");
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"tag\""),
"pin kind should be tag: {pin_json}"
);
assert!(
pin_json.contains("v1.0"),
"pin value should be v1.0: {pin_json}"
);
}
#[test]
fn meld_pin_ref_clones_at_specific_commit_and_persists_pin() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-ref");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin-ref", &sha_v1]);
assert!(r.success, "meld --pin-ref: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(commit, sha_v1, "pin-ref should record sha_v1");
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\""),
"pin kind should be ref: {pin_json}"
);
assert!(
pin_json.contains(&sha_v1),
"pin value should be the sha: {pin_json}"
);
}
#[test]
fn meld_default_branch_pin_is_at_main_tip() {
let (sb, _sha_v1, sha_v2) = make_pinnable_repo("pintest-default");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld default: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(commit, sha_v2, "default branch should be at main tip");
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("default-branch"),
"pin kind should be default-branch: {pin_json}"
);
}
#[test]
fn meld_two_pin_flags_is_conflicting_pin_error() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--follow-branch", "main", "--pin-tag", "v1"]);
assert!(
!r.success,
"two pin flags must be rejected: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("conflicting pin flags"),
"expected the structured ConflictingPin error, got stderr={}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no sources melded"),
"nothing should be registered after a conflict error: {}",
sources.stdout
);
}
#[test]
fn meld_pin_head_freezes_default_tip() {
let (sb, _sha_v1, sha_v2) = make_pinnable_repo("pintest-pin-head");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin", "HEAD"]);
assert!(r.success, "meld --pin HEAD: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(commit, sha_v2, "--pin HEAD should freeze the main tip");
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\""),
"--pin HEAD should persist a ref pin: {pin_json}"
);
assert!(
pin_json.contains(&sha_v2),
"the frozen ref should be the tip sha: {pin_json}"
);
}
#[test]
fn meld_pin_requires_a_value() {
let (sb, _sha_v1, _sha_v2) = make_pinnable_repo("pintest-pin-noval");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin"]);
assert!(!r.success, "valueless --pin must be a usage error");
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no sources melded"),
"nothing registered after a valueless --pin: {}",
sources.stdout
);
}
#[test]
fn meld_pin_empty_branch_name_is_invalid_ref() {
let (sb, _sha_v1, _sha_v2) = make_pinnable_repo("pintest-pin-emptybranch");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin", "branch="]);
assert!(!r.success, "an empty branch name must fail");
assert_eq!(
source_count(&sb),
0,
"nothing registered after an empty branch name"
);
}
#[test]
fn meld_pin_abbreviated_sha_freezes_that_commit() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-pin-shortsha");
let spec = sb.source_spec();
let short = &sha_v1[..8];
let r = sb.mind(&["meld", &spec, "--pin", short]);
assert!(r.success, "meld --pin <short-sha>: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(
commit, sha_v1,
"short sha should resolve to the full commit"
);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\"") && pin_json.contains(&sha_v1),
"a short-sha freeze should persist the full-sha ref: {pin_json}"
);
}
#[test]
fn meld_pin_branch_value_freezes_that_branch_tip() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-pin-branch");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin", "stable"]);
assert!(r.success, "meld --pin stable: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(commit, sha_v1, "--pin stable should freeze stable's tip");
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\""),
"--pin <branch> should persist a ref, not follow-branch: {pin_json}"
);
assert!(
pin_json.contains(&sha_v1),
"the frozen ref should be stable's sha: {pin_json}"
);
}
#[test]
fn meld_pin_tag_value_freezes_tag_commit() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-pin-tagval");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin", "v1.0"]);
assert!(r.success, "meld --pin v1.0: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(commit, sha_v1, "--pin v1.0 should freeze the tag's commit");
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\""),
"--pin <tag> should persist a ref, not a tag pin: {pin_json}"
);
assert!(
pin_json.contains(&sha_v1) && !pin_json.contains("v1.0"),
"the ref value must be the resolved sha, not the literal tag: {pin_json}"
);
}
#[test]
fn meld_pin_sha_value_is_exact_ref() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-pin-sha");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin", &sha_v1]);
assert!(r.success, "meld --pin <sha>: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(commit, sha_v1, "--pin <sha> should record that sha");
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\"") && pin_json.contains(&sha_v1),
"--pin <sha> should persist a ref at that sha: {pin_json}"
);
}
#[test]
fn meld_pin_head_freezes_the_directive_selected_branch() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-pin-directive");
sb.write_and_commit("mind.toml", "[source]\nfollow-branch = \"stable\"\n");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin", "HEAD"]);
assert!(r.success, "meld --pin HEAD over directive: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(
commit, sha_v1,
"--pin HEAD should freeze stable's tip (the directive's branch)"
);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\""),
"the frozen directive branch should persist as a ref: {pin_json}"
);
}
#[test]
fn meld_pin_branch_equals_tracks_branch() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-follow-branch-spec");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin", "branch=stable"]);
assert!(r.success, "meld --pin branch=stable: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(commit, sha_v1, "--pin branch=stable records stable's tip");
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("follow-branch") && pin_json.contains("stable"),
"pin should be follow-branch=stable: {pin_json}"
);
}
#[test]
fn meld_pin_tag_equals_tracks_moving_tag() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-follow-tag-spec");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin", "tag=v1.0"]);
assert!(r.success, "meld --pin tag=v1.0: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(commit, sha_v1, "--pin tag=v1.0 records the tag's commit");
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"tag\"") && pin_json.contains("v1.0"),
"pin should be tag=v1.0: {pin_json}"
);
}
#[test]
fn meld_pin_bad_key_is_usage_error() {
let (sb, _sha_v1, _sha_v2) = make_pinnable_repo("pintest-pin-badkey");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin", "brnach=stable"]);
assert!(!r.success, "an unrecognized --pin key must fail");
assert!(
r.stderr.contains("invalid --pin value"),
"expected the BadPinSpec error, got: {}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no sources melded"),
"nothing registered after a bad --pin value: {}",
sources.stdout
);
}
#[test]
fn meld_pin_frozen_branch_does_not_advance_on_sync() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-pin-frozen-sync");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin", "stable"]);
assert!(r.success, "meld --pin stable: {}", r.stderr);
assert_eq!(read_source_commit(&sb), sha_v1, "frozen at stable's tip");
git(&sb.source, &["checkout", "-q", "stable"]);
write(
&sb.source.join("agents/dev.md"),
"---\nname: dev\ndescription: dev agent v3\n---\n# dev v3\n",
);
git(&sb.source, &["commit", "-aqm", "stable moves"]);
git(&sb.source, &["checkout", "-q", "main"]);
assert!(sb.mind(&["sync"]).success, "sync should succeed");
assert_eq!(
read_source_commit(&sb),
sha_v1,
"a frozen ref must not advance when its branch moves"
);
}
#[test]
fn meld_pin_freeze_satisfies_require_pinned_policy() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-pin-policy");
let policy = write_policy(
&sb,
"[sources]\npinned = true\nlock = true\nallow = [\"local/*/pintest-pin-policy\"]\n",
);
let spec = sb.source_spec();
let refused = sb.mind_env(
&["meld", &spec, "--pin", "branch=stable"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
!refused.success,
"following a branch must be refused under require-pinned: {}",
refused.stdout
);
assert!(
refused.stderr.contains("must be pinned"),
"refusal should be the require-pinned one, got: {}",
refused.stderr
);
assert_eq!(source_count(&sb), 0, "nothing registered on refusal");
let r = sb.mind_env(
&["meld", &spec, "--pin", "stable"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"a --pin freeze must satisfy require-pinned: {}",
r.stderr
);
assert_eq!(read_source_commit(&sb), sha_v1);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\""),
"the accepted pin must be a ref: {pin_json}"
);
}
#[test]
fn remeld_pin_repins_to_a_tag_and_changes_recorded_commit() {
let (sb, sha_v1, sha_v2) = make_pinnable_repo("pintest-remeld-tag");
let spec = sb.source_spec();
let first = sb.mind(&["meld", &spec, "--register-only"]);
assert!(first.success, "first meld failed: {}", first.stderr);
assert_eq!(
read_source_commit(&sb),
sha_v2,
"an unpinned meld records the default branch tip"
);
let r = sb.mind(&["meld", &spec, "--register-only", "--pin", "v1.0"]);
assert!(
r.success,
"re-meld --pin v1.0 failed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("re-pinned"),
"the source must report the re-pin: {}",
r.stdout
);
assert!(
!r.stdout.contains("--pin"),
"--pin must not appear in an ignored-flags note now that it is honored: {}",
r.stdout
);
assert_eq!(
read_source_commit(&sb),
sha_v1,
"the re-pin must record the tag's commit, not the prior default-branch tip"
);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\"") && pin_json.contains(&sha_v1),
"a --pin <ref> freeze must persist a ref pin at the resolved commit: {pin_json}"
);
}
#[test]
fn remeld_pin_to_the_same_value_is_a_noop() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-remeld-noop");
let spec = sb.source_spec();
let first = sb.mind(&["meld", &spec, "--register-only", "--pin", "v1.0"]);
assert!(first.success, "first meld failed: {}", first.stderr);
assert_eq!(read_source_commit(&sb), sha_v1);
let r = sb.mind(&["meld", &spec, "--register-only", "--pin", "v1.0"]);
assert!(
r.success,
"re-pin to the same tag failed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("already pinned"),
"re-pinning to the already-recorded value must say so: {}",
r.stdout
);
assert!(
!r.stdout.contains("re-pinned"),
"re-pinning to the already-recorded value must not report a change: {}",
r.stdout
);
assert_eq!(
read_source_commit(&sb),
sha_v1,
"a no-op re-pin must not change the recorded commit"
);
}
#[test]
fn remeld_pin_bad_value_fails_without_corrupting_source() {
let (sb, _sha_v1, sha_v2) = make_pinnable_repo("pintest-remeld-bad-pin");
let spec = sb.source_spec();
let first = sb.mind(&["meld", &spec, "--register-only"]);
assert!(first.success, "first meld failed: {}", first.stderr);
assert_eq!(read_source_commit(&sb), sha_v2);
let pin_before = read_source_pin_json(&sb);
let r = sb.mind(&[
"meld",
&spec,
"--register-only",
"--pin",
"does-not-exist-anywhere",
]);
assert!(
!r.success,
"a re-pin to a nonexistent ref must fail: {} {}",
r.stdout, r.stderr
);
assert_eq!(
read_source_commit(&sb),
sha_v2,
"a failed re-pin must not change the recorded commit"
);
assert_eq!(
read_source_pin_json(&sb),
pin_before,
"a failed re-pin must not change the recorded pin"
);
assert_eq!(source_count(&sb), 1, "the source must remain registered");
}
#[test]
fn remeld_pin_marks_installed_items_out_of_date_when_commit_moves() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let fresh = sb.mind(&["recall", "skill:review"]);
assert!(
!fresh.stdout.contains("out of date"),
"a fresh install must not be out of date: {}",
fresh.stdout
);
sb.edit_source();
let r = sb.mind(&["meld", &spec, "--pin", "HEAD"]);
assert!(
r.success,
"re-meld --pin HEAD failed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("re-pinned"),
"the source must report a re-pin (the commit moved): {}",
r.stdout
);
let after = sb.mind(&["recall", "skill:review"]);
assert!(
after.success,
"recall failed: {} {}",
after.stdout, after.stderr
);
assert!(
after.stdout.contains("out of date"),
"the installed item must be reported out of date after the pin moved \
the source's recorded commit: {}",
after.stdout
);
}
#[test]
fn meld_deprecated_aliases_map_and_conflict() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-deprecated-alias");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin-ref", &sha_v1]);
assert!(r.success, "deprecated --pin-ref: {}", r.stderr);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\"") && pin_json.contains(&sha_v1),
"--pin-ref should still persist a ref: {pin_json}"
);
let (sb2, _s1, _s2) = make_pinnable_repo("pintest-deprecated-conflict");
let spec2 = sb2.source_spec();
let r2 = sb2.mind(&["meld", &spec2, "--pin", "HEAD", "--follow-branch", "main"]);
assert!(!r2.success, "--pin with a deprecated alias must conflict");
assert!(
r2.stderr.contains("conflicting pin flags"),
"expected ConflictingPin, got: {}",
r2.stderr
);
}
#[test]
fn source_directive_follow_branch_applies_when_no_consumer_flag() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-directive-follow");
sb.write_and_commit("mind.toml", "[source]\nfollow-branch = \"stable\"\n");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld with directive: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(
commit, sha_v1,
"directive follow-branch=stable should land on sha_v1"
);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("follow-branch"),
"pin kind should be follow-branch: {pin_json}"
);
}
#[test]
fn consumer_flag_overrides_source_directive() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-override");
sb.write_and_commit("mind.toml", "[source]\nfollow-branch = \"stable\"\n");
let sha_main_tip = {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&sb.source)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
};
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--follow-branch", "main"]);
assert!(r.success, "meld override: {}", r.stderr);
let commit = read_source_commit(&sb);
assert_eq!(
commit, sha_main_tip,
"consumer --follow-branch main should override directive and land on main tip"
);
assert_ne!(
commit, sha_v1,
"directive must not take precedence over consumer flag"
);
}
#[test]
fn sync_follow_branch_advances_commit() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-sync-follow");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--follow-branch", "stable"])
.success
);
let before = read_source_commit(&sb);
assert_eq!(before, sha_v1);
write(
&sb.source.join("agents/dev.md"),
"---\nname: dev\ndescription: dev agent v3\n---\n# dev v3\n",
);
git(&sb.source, &["commit", "-aqm", "v3 on stable"]);
let sha_v3 = {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&sb.source)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
};
git(&sb.source, &["branch", "-f", "stable", &sha_v3]);
let r = sb.mind(&["sync"]);
assert!(r.success, "sync follow-branch: {}", r.stderr);
let after = read_source_commit(&sb);
assert_eq!(after, sha_v3, "follow-branch source should advance on sync");
}
#[test]
fn sync_pin_ref_stays_fixed() {
let (sb, sha_v1, _sha_v2) = make_pinnable_repo("pintest-sync-ref");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--pin-ref", &sha_v1]).success);
let before = read_source_commit(&sb);
assert_eq!(before, sha_v1);
write(
&sb.source.join("agents/dev.md"),
"---\nname: dev\ndescription: v99\n---\n# v99\n",
);
git(&sb.source, &["commit", "-aqm", "v99"]);
let r = sb.mind(&["sync"]);
assert!(r.success, "sync pin-ref: {}", r.stderr);
let after = read_source_commit(&sb);
assert_eq!(after, sha_v1, "pin-ref should be immutable across sync");
}
#[test]
fn sync_does_not_change_pin() {
let (sb, _sha_v1, _sha_v2) = make_pinnable_repo("pintest-sync-nopin");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--follow-branch", "stable"])
.success
);
let pin_before = read_source_pin_json(&sb);
sb.mind(&["sync"]);
let pin_after = read_source_pin_json(&sb);
assert_eq!(
pin_before, pin_after,
"sync must not modify the recorded pin"
);
assert!(pin_after.contains("follow-branch"), "{pin_after}");
assert!(pin_after.contains("stable"), "{pin_after}");
}
#[test]
fn source_directive_conflict_is_error() {
let sb = Sandbox::new();
sb.write_and_commit(
"mind.toml",
"[source]\nfollow-branch = \"main\"\npin-tag = \"v1.0\"\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(!r.success, "conflicting directives should fail meld");
assert!(
r.stderr.contains("conflicting pin") || r.stderr.contains("mind.toml"),
"expected pin conflict error: {}",
r.stderr
);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"nothing should be registered"
);
}
#[test]
fn existing_sources_json_without_pin_field_loads_as_default_branch() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let path = sb.mind_home.join("sources.json");
let json = std::fs::read_to_string(&path).unwrap();
let name_start = json.find("\"name\": \"").unwrap() + "\"name\": \"".len();
let name_end = json[name_start..].find('"').unwrap() + name_start;
let name_val = &json[name_start..name_end];
let url_start = json.find("\"url\": \"").unwrap() + "\"url\": \"".len();
let url_end = json[url_start..].find('"').unwrap() + url_start;
let url_val = &json[url_start..url_end];
let no_pin_json = format!(
r#"{{
"sources": [
{{
"name": "{name_val}",
"url": "{url_val}",
"host": "local",
"owner": "x",
"repo": "agents",
"commit": null
}}
]
}}"#
);
std::fs::write(&path, no_pin_json).unwrap();
let r = sb.mind(&["sync"]);
assert!(
r.success,
"sync on old sources.json (no pin field) should succeed: {}",
r.stderr
);
}
fn clone_dir_of(sb: &Sandbox, repo: &str) -> PathBuf {
sb.mind_home
.join("sources")
.join("local")
.join(sb.base_name())
.join(repo)
}
#[test]
fn meld_pin_ref_unresolvable_is_git_error_and_registers_nothing() {
let (sb, _v1, _v2) = make_pinnable_repo("pintest-bad-ref");
let spec = sb.source_spec();
let bogus = "0123456789abcdef0123456789abcdef01234567";
let r = sb.mind(&["meld", &spec, "--pin-ref", bogus]);
assert!(
!r.success,
"unresolvable --pin-ref must fail: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("git"),
"expected a git error, got stderr={}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no sources melded"),
"no source must be registered after an unresolvable pin: {}",
sources.stdout
);
let sources_json = sb.mind_home.join("sources.json");
if sources_json.exists() {
let json = std::fs::read_to_string(&sources_json).unwrap();
assert!(
!json.contains("pintest-bad-ref"),
"sources.json must not contain the failed source: {json}"
);
}
let clone = clone_dir_of(&sb, "pintest-bad-ref");
assert!(
!clone.exists(),
"an unresolvable pin must not leave a stray clone dir at {}",
clone.display()
);
}
#[test]
fn meld_pin_tag_unresolvable_is_git_error_and_registers_nothing() {
let (sb, _v1, _v2) = make_pinnable_repo("pintest-bad-tag");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin-tag", "v9.9-does-not-exist"]);
assert!(
!r.success,
"unresolvable --pin-tag must fail: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("git"),
"expected a git error, got stderr={}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no sources melded"),
"no source must be registered after an unresolvable tag pin: {}",
sources.stdout
);
let clone = clone_dir_of(&sb, "pintest-bad-tag");
assert!(
!clone.exists(),
"an unresolvable tag pin must not leave a stray clone dir at {}",
clone.display()
);
}
#[test]
fn sync_reclones_when_clone_dir_is_missing() {
let (sb, sha_v1, _v2) = make_pinnable_repo("pintest-sync-missing");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--pin-tag", "v1.0"]).success);
assert_eq!(read_source_commit(&sb), sha_v1);
let clone = clone_dir_of(&sb, "pintest-sync-missing");
assert!(clone.exists(), "clone should exist after meld");
std::fs::remove_dir_all(&clone).unwrap();
let r = sb.mind(&["sync"]);
assert!(
r.success,
"sync must recover a missing clone dir: {}",
r.stderr
);
assert_eq!(
read_source_commit(&sb),
sha_v1,
"re-clone on sync must honor the recorded pin"
);
assert!(
clone.join(".git").is_dir(),
"sync should have re-created the clone"
);
}
#[test]
fn pin_persists_across_repeated_syncs_while_commit_advances() {
let (sb, sha_v1, _v2) = make_pinnable_repo("pintest-multi-sync");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--follow-branch", "stable"])
.success
);
assert_eq!(read_source_commit(&sb), sha_v1);
let pin_initial = read_source_pin_json(&sb);
assert!(sb.mind(&["sync"]).success);
assert_eq!(read_source_commit(&sb), sha_v1);
assert_eq!(read_source_pin_json(&sb), pin_initial);
write(
&sb.source.join("agents/dev.md"),
"---\nname: dev\ndescription: stable v3\n---\n# stable v3\n",
);
git(&sb.source, &["commit", "-aqm", "v3 on stable"]);
let sha_v3 = {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&sb.source)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
};
git(&sb.source, &["branch", "-f", "stable", &sha_v3]);
assert!(sb.mind(&["sync"]).success);
assert_eq!(
read_source_commit(&sb),
sha_v3,
"follow-branch commit should advance across repeated syncs"
);
assert_eq!(
read_source_pin_json(&sb),
pin_initial,
"pin value must stay untouched across repeated syncs"
);
assert!(sb.mind(&["sync"]).success);
assert_eq!(read_source_commit(&sb), sha_v3);
assert_eq!(read_source_pin_json(&sb), pin_initial);
}
#[test]
fn source_directive_pin_tag_applies_when_no_consumer_flag() {
let (sb, sha_v1, _v2) = make_pinnable_repo("pintest-directive-tag");
sb.write_and_commit("mind.toml", "[source]\npin-tag = \"v1.0\"\n");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld with pin-tag directive: {}", r.stderr);
assert_eq!(
read_source_commit(&sb),
sha_v1,
"directive pin-tag=v1.0 should land on the tagged commit"
);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"tag\""),
"pin kind should be tag: {pin_json}"
);
assert!(
pin_json.contains("v1.0"),
"pin value should be v1.0: {pin_json}"
);
}
#[test]
fn source_directive_pin_ref_applies_when_no_consumer_flag() {
let (sb, sha_v1, _v2) = make_pinnable_repo("pintest-directive-ref");
sb.write_and_commit("mind.toml", &format!("[source]\npin-ref = \"{sha_v1}\"\n"));
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld with pin-ref directive: {}", r.stderr);
assert_eq!(
read_source_commit(&sb),
sha_v1,
"directive pin-ref should land on the named commit"
);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\""),
"pin kind should be ref: {pin_json}"
);
assert!(
pin_json.contains(&sha_v1),
"pin value should be the sha: {pin_json}"
);
}
#[test]
fn consumer_pin_ref_overrides_follow_branch_directive() {
let (sb, sha_v1, _v2) = make_pinnable_repo("pintest-cross-override");
sb.write_and_commit("mind.toml", "[source]\nfollow-branch = \"stable\"\n");
let spec = sb.source_spec();
let sha_main_tip = {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&sb.source)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
};
let r = sb.mind(&["meld", &spec, "--pin-ref", &sha_main_tip]);
assert!(r.success, "meld cross-kind override: {}", r.stderr);
assert_eq!(
read_source_commit(&sb),
sha_main_tip,
"consumer --pin-ref must override the follow-branch directive"
);
assert_ne!(
read_source_commit(&sb),
sha_v1,
"the stable directive must not win over the consumer ref"
);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"ref\""),
"persisted pin kind should be the consumer's ref, not follow-branch: {pin_json}"
);
}
#[test]
fn meld_rejects_unknown_source_pin_field() {
let (sb, _v1, _v2) = make_pinnable_repo("pintest-unknown-field");
sb.write_and_commit("mind.toml", "[source]\npin-branch = \"stable\"\n");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(
!r.success,
"an unknown [source] field must fail meld: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("mind.toml") || r.stderr.contains("pin-branch"),
"expected a mind.toml parse error naming the bad field: {}",
r.stderr
);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"nothing should be registered after a mind.toml parse error"
);
}
#[test]
fn sync_pin_tag_picks_up_moved_upstream_tag() {
let (sb, sha_v1, _v2) = make_pinnable_repo("pintest-moved-tag");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--pin-tag", "v1.0"]).success);
assert_eq!(read_source_commit(&sb), sha_v1, "pinned at v1.0 == sha_v1");
write(
&sb.source.join("agents/dev.md"),
"---\nname: dev\ndescription: retagged\n---\n# retagged\n",
);
git(&sb.source, &["commit", "-aqm", "retag target"]);
let sha_new = {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&sb.source)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
};
git(&sb.source, &["tag", "-f", "v1.0", &sha_new]);
let r = sb.mind(&["sync"]);
assert!(r.success, "sync after moving tag: {}", r.stderr);
assert_eq!(
read_source_commit(&sb),
sha_new,
"a re-pointed upstream tag must be picked up by sync (force-fetch)"
);
let pin_json = read_source_pin_json(&sb);
assert!(pin_json.contains("\"tag\""), "{pin_json}");
assert!(pin_json.contains("v1.0"), "{pin_json}");
}
fn read_source_roots_json(sb: &Sandbox) -> String {
let json = std::fs::read_to_string(sb.mind_home.join("sources.json")).expect("sources.json");
if let Some(start) = json.find("\"roots\":") {
let after = &json[start + "\"roots\":".len()..];
if let Some(arr_start) = after.find('[') {
let arr = &after[arr_start..];
let mut depth = 0usize;
let mut end = 0;
for (i, c) in arr.char_indices() {
match c {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
end = i + 1;
break;
}
}
_ => {}
}
}
return arr[..end].to_string();
}
}
"null".to_string()
}
#[test]
fn meld_root_persists_in_sources_json_and_probe_shows_subtree_items() {
let sb = Sandbox::bare("subtree");
sb.write_and_commit(
"sub/skills/deploy/SKILL.md",
"---\ndescription: deploy skill\n---\n# deploy\n",
);
sb.write_and_commit(
"sub/agents/ops.md",
"---\ndescription: ops agent\n---\n# ops\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--root", "sub"]);
assert!(r.success, "meld --root: {}", r.stderr);
let roots_json = read_source_roots_json(&sb);
assert!(
roots_json.contains("sub"),
"roots should be persisted: {roots_json}"
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:deploy"),
"subtree skill: {}",
probe.stdout
);
assert!(
probe.stdout.contains("agent:ops"),
"subtree agent: {}",
probe.stdout
);
}
#[test]
fn meld_root_on_authoritative_source_prints_note() {
let sb = Sandbox::bare("auth-source");
sb.write_and_commit(
"pkg/style.md",
"---\ndescription: style rule\n---\n# style\n",
);
sb.write_and_commit(
"mind.toml",
concat!(
"[[items]]\n",
"kind = \"rule\"\n",
"name = \"style\"\n",
"path = \"pkg/style.md\"\n",
),
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--root", "pkg"]);
assert!(
r.success,
"meld should succeed even with ignored --root: {}",
r.stderr
);
assert!(
r.stdout.contains("ignored") || r.stdout.contains("note"),
"expected an 'ignored' note: {}",
r.stdout
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("rule:style"),
"authoritative item still discovered: {}",
probe.stdout
);
}
#[test]
fn meld_root_nonexistent_dir_exits_nonzero() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--root", "does-not-exist"]);
assert!(!r.success, "meld with missing root must fail");
assert!(
r.stderr.contains("InvalidRoot") || r.stderr.contains("not a directory"),
"expected InvalidRoot error: {}",
r.stderr
);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"nothing should be registered after an invalid root"
);
}
#[test]
fn sync_preserves_roots() {
let sb = Sandbox::bare("roots-sync");
sb.write_and_commit(
"sub/skills/deploy/SKILL.md",
"---\ndescription: deploy\n---\n# deploy\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--root", "sub"]).success);
let roots_before = read_source_roots_json(&sb);
assert!(
roots_before.contains("sub"),
"roots should be set: {roots_before}"
);
assert!(sb.mind(&["sync"]).success);
let roots_after = read_source_roots_json(&sb);
assert_eq!(
roots_before, roots_after,
"sync must not modify the recorded roots"
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:deploy"),
"subtree item still visible after sync: {}",
probe.stdout
);
}
#[test]
fn two_root_flags_union_and_both_persist() {
let sb = Sandbox::bare("two-roots");
sb.write_and_commit(
"a/skills/alpha/SKILL.md",
"---\ndescription: alpha skill\n---\n# alpha\n",
);
sb.write_and_commit(
"b/agents/beta.md",
"---\ndescription: beta agent\n---\n# beta\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--root", "a", "--root", "b"]);
assert!(r.success, "meld --root a --root b: {}", r.stderr);
let roots_json = read_source_roots_json(&sb);
assert!(
roots_json.contains("\"a\""),
"root a persisted: {roots_json}"
);
assert!(
roots_json.contains("\"b\""),
"root b persisted: {roots_json}"
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:alpha"),
"root a item: {}",
probe.stdout
);
assert!(
probe.stdout.contains("agent:beta"),
"root b item: {}",
probe.stdout
);
}
#[test]
fn meld_absolute_root_exits_nonzero() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--root", "/tmp"]);
assert!(!r.success, "absolute root must fail");
assert!(
r.stderr.contains("InvalidRoot") || r.stderr.contains("not a directory"),
"expected InvalidRoot: {}",
r.stderr
);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"nothing registered after an absolute root"
);
}
#[test]
fn mindfile_roots_discovered_without_flag() {
let sb = Sandbox::bare("toml-roots");
sb.write_and_commit(
"toolbox/skills/pack/SKILL.md",
"---\ndescription: pack skill\n---\n# pack\n",
);
sb.write_and_commit("mind.toml", "[source]\nroots = [\"toolbox\"]\n");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld with roots in mind.toml: {}", r.stderr);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:pack"),
"item under toolbox/ should be found: {}",
probe.stdout
);
}
#[test]
fn review_clean_local_path_exits_zero() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec]);
assert!(
r.success,
"clean source should exit 0: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("no issues") || r.stdout.contains("publishable") || r.stderr.is_empty(),
"expected clean report: stdout={} stderr={}",
r.stdout,
r.stderr
);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"review must not register anything"
);
}
#[test]
fn review_malformed_mind_toml_exits_nonzero() {
let sb = Sandbox::new();
sb.write_and_commit("mind.toml", "[[[[bad toml");
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec]);
assert!(
!r.success,
"malformed mind.toml must exit non-zero: stdout={} stderr={}",
r.stdout, r.stderr
);
}
#[test]
fn review_unknown_item_kind_exits_nonzero() {
let sb = Sandbox::new();
sb.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"spell\"\nname = \"x\"\npath = \"x.md\"\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec]);
assert!(
!r.success,
"unknown kind must exit non-zero: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("unknown-kind") || r.stderr.contains("unknown item kind"),
"expected unknown-kind in output: stderr={}",
r.stderr
);
}
#[test]
fn review_bad_ns_token_exits_nonzero() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\ndescription: lead\n---\nDelegate to {{ns:nope}}.\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec]);
assert!(
!r.success,
"bad ns token must exit non-zero: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("bad-reference") || r.stderr.contains("does not resolve"),
"expected bad-reference in output: stderr={}",
r.stderr
);
}
#[test]
fn review_conflicting_pin_exits_nonzero() {
let sb = Sandbox::new();
sb.write_and_commit(
"mind.toml",
"[source]\nfollow-branch = \"main\"\npin-tag = \"v1.0\"\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec]);
assert!(
!r.success,
"conflicting pin must exit non-zero: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("conflicting-pin") || r.stderr.contains("conflicting pin"),
"expected conflicting-pin in output: stderr={}",
r.stderr
);
}
#[test]
fn review_missing_description_is_advisory_exit_zero() {
let sb = Sandbox::new();
sb.write_and_commit("agents/nodesc.md", "# no frontmatter here\nsome content\n");
let source_dir = sb.source.clone();
std::fs::remove_dir_all(source_dir.join("skills")).ok();
std::fs::remove_dir_all(source_dir.join("rules")).ok();
std::fs::remove_file(source_dir.join("agents/dev.md")).ok();
git(&source_dir, &["add", "-A"]);
git(&source_dir, &["commit", "-qm", "nodesc only"]);
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec]);
assert!(
r.success,
"missing description is advisory, must exit 0: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("missing-description") || r.stdout.contains("advisory"),
"expected advisory finding in stdout: {}",
r.stdout
);
}
#[test]
fn review_unguarded_ref_under_as_is_advisory_exit_zero() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\ndescription: lead\n---\nDelegate to the dev agent.\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec, "--as", "jk"]);
assert!(
r.success,
"unguarded ref advisory must exit 0: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("unguarded-reference") || r.stdout.contains("advisory"),
"expected advisory finding: stdout={}",
r.stdout
);
assert!(
!r.stderr.contains("error ["),
"must have no hard errors: stderr={}",
r.stderr
);
}
#[test]
fn review_melded_selector_resolves_via_registry() {
let sb = melded();
let r = sb.mind(&["review", "agents"]);
assert!(
r.success,
"review via registry selector must succeed: stdout={} stderr={}",
r.stdout, r.stderr
);
}
#[test]
fn review_with_prefix_flag_evaluates_under_that_namespace() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\ndescription: lead\n---\nDelegate to {{ns:dev}}.\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec, "--as", "jk"]);
assert!(
r.success,
"valid ns token with prefix must exit 0: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
!r.stderr.contains("bad-reference"),
"valid token must not produce bad-reference: stderr={}",
r.stderr
);
}
#[test]
fn review_local_path_target_is_left_intact() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let skill = sb.source.join("skills/review/SKILL.md");
let before = std::fs::read_to_string(&skill).unwrap();
let r = sb.mind(&["review", &spec]);
assert!(
r.success,
"clean local review should exit 0: {} {}",
r.stdout, r.stderr
);
assert!(sb.source.is_dir(), "local source dir must survive review");
let after = std::fs::read_to_string(&skill).unwrap();
assert_eq!(before, after, "review must not modify the local source");
assert_no_review_temp(&sb.mind_home);
}
#[test]
fn review_remote_spec_clone_failure_exits_nonzero_and_leaves_no_temp() {
let sb = Sandbox::new();
let r = sb.mind(&["review", "https://127.0.0.1:1/owner/repo"]);
assert!(
!r.success,
"a failed clone must exit non-zero: stdout={} stderr={}",
r.stdout, r.stderr
);
assert_no_review_temp(&sb.mind_home);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"failed review must not register anything"
);
}
#[test]
fn review_report_lists_every_advisory_finding() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\ndescription: lead\n---\nDelegate to the dev agent.\n",
);
sb.write_and_commit("agents/nodesc.md", "# no frontmatter\nbody\n");
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec, "--as", "jk"]);
assert!(
r.success,
"advisory-only review exits 0: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("missing-description"),
"missing-description advisory must be printed: {}",
r.stdout
);
assert!(
r.stdout.contains("unguarded-reference"),
"unguarded-reference advisory must be printed: {}",
r.stdout
);
assert!(
!r.stdout.contains("skill:review: no description"),
"clean item must not be flagged missing-description: {}",
r.stdout
);
}
#[test]
fn review_multiple_hard_errors_all_reported_and_counted() {
let sb = Sandbox::new();
sb.write_and_commit(
"agents/lead.md",
"---\ndescription: lead\n---\nDelegate to {{ns:nope}}.\n",
);
sb.write_and_commit(
"agents/boss.md",
"---\ndescription: boss\n---\nDefer to {{ns:alsonope}}.\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["review", &spec]);
assert!(
!r.success,
"multiple hard errors must exit non-zero: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("nope"),
"first bad ref reported: {}",
r.stderr
);
assert!(
r.stderr.contains("alsonope"),
"second bad ref reported: {}",
r.stderr
);
assert!(
r.stdout.contains("2 hard error(s)"),
"summary must count both hard errors: {}",
r.stdout
);
}
#[test]
fn review_target_and_policy_are_mutually_exclusive() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let policy_path = sb.base.join("policy.toml").to_string_lossy().into_owned();
let r = sb.mind(&["review", &spec, "--policy", &policy_path]);
assert!(
!r.success,
"review with both <target> and --policy must exit non-zero: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("cannot be used with"),
"clap conflict diagnostic must appear in stderr: {}",
r.stderr
);
}
#[test]
fn meld_pin_tag_uses_pinned_mindfile_for_authoritativeness_not_default_branch() {
let sb = Sandbox::bare("pinned-authoritativeness");
sb.write_and_commit(
"sub/skills/deploy/SKILL.md",
"---\ndescription: deploy skill\n---\n# deploy\n",
);
sb.write_and_commit(
"mind.toml",
"[source]\ndescription = \"non-authoritative at v1.0\"\n",
);
git(&sb.source, &["tag", "v1.0"]);
sb.write_and_commit(
"pkg/style.md",
"---\ndescription: style rule\n---\n# style\n",
);
sb.write_and_commit(
"mind.toml",
concat!(
"[source]\n",
"description = \"authoritative at main tip\"\n\n",
"[[items]]\n",
"kind = \"rule\"\n",
"name = \"style\"\n",
"path = \"pkg/style.md\"\n",
),
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin-tag", "v1.0", "--root", "sub"]);
assert!(r.success, "meld --pin-tag v1.0 --root sub: {}", r.stderr);
assert!(
!r.stdout.contains("ignored"),
"--root must be honored against the pinned non-authoritative mind.toml, \
not ignored against the default branch's authoritative one: {}",
r.stdout
);
let roots_json = read_source_roots_json(&sb);
assert!(
roots_json.contains("sub"),
"root from the pinned file must be persisted: {roots_json}"
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("non-authoritative at v1.0"),
"pinned [source].description should be recorded: {}",
sources.stdout
);
assert!(
!sources.stdout.contains("authoritative at main tip"),
"default branch description must not leak through: {}",
sources.stdout
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:deploy"),
"pinned subtree item should be discovered: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("rule:style"),
"default branch's authoritative item must not appear: {}",
probe.stdout
);
}
#[test]
fn meld_pin_tag_uses_pinned_mindfile_for_nested_discovery_not_default_branch() {
let sb = Sandbox::bare("pinned-nested-discovery");
sb.write_and_commit("agents/dev.md", "---\ndescription: dev agent\n---\n# dev\n");
sb.write_and_commit(
"mind.toml",
"[source]\ndescription = \"no nested sources at v1.0\"\n",
);
git(&sb.source, &["tag", "v1.0"]);
sb.write_and_commit(
"mind.toml",
concat!(
"[source]\n",
"description = \"nested at main tip\"\n\n",
"[[discover.sources]]\n",
"source = \"/nonexistent/nested/repo\"\n",
),
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin-tag", "v1.0"]);
assert!(
r.success,
"meld must use the pinned (no-nested) mind.toml and succeed: {} {}",
r.stdout, r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no nested sources at v1.0"),
"pinned source description should be present: {}",
sources.stdout
);
assert!(
!sources.stdout.contains("/nonexistent/nested/repo"),
"default branch's nested source must not be melded: {}",
sources.stdout
);
}
fn write_policy(sb: &Sandbox, body: &str) -> String {
let path = sb.base.join("policy.toml");
write(&path, body);
path.to_string_lossy().into_owned()
}
fn source_count(sb: &Sandbox) -> usize {
let path = sb.mind_home.join("sources.json");
let Ok(json) = std::fs::read_to_string(&path) else {
return 0;
};
json.matches("\"url\"").count()
}
#[test]
fn meld_refused_when_not_in_allow_and_locked() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow = [\"local/*/other-repo\"]\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"locked non-allowed meld must fail: {}",
r.stdout
);
assert!(
r.stderr.contains("not permitted") || r.stderr.contains("not permitted by the managed"),
"error should mention the source is not permitted: {}",
r.stderr
);
assert_eq!(source_count(&sb), 0, "registry must be unchanged");
let clone_dir = sb
.mind_home
.join("sources")
.join("local")
.join(sb.base_name())
.join("agents");
assert!(
!clone_dir.exists(),
"no clone should be left at {}",
clone_dir.display()
);
}
#[test]
fn meld_allowed_when_not_in_allow_but_unlocked() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\nlock = false\nallow = [\"local/*/other-repo\"]\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"unlocked non-allowed meld must succeed: {}",
r.stderr
);
assert!(
r.stderr.contains("advisory") || r.stderr.contains("not in the managed policy"),
"a warning should be printed: {}",
r.stderr
);
assert_eq!(source_count(&sb), 1, "source should be registered");
}
#[test]
fn policy_is_authoritative_over_explicit_user_meld() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(&sb, "[sources]\nlock = true\nallow = []\n");
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"policy must override the user's explicit meld request: {}",
r.stdout
);
assert!(
r.stderr.contains("not permitted"),
"refusal should be explained: {}",
r.stderr
);
assert_eq!(source_count(&sb), 0, "registry must be unchanged");
}
#[test]
fn meld_pinned_policy_refuses_floating_branch_and_allows_tag() {
let (sb, _sha_v1, _sha_v2) = make_pinnable_repo("pintest-policy");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\npinned = true\nlock = true\nallow = [\"local/*/pintest-policy\"]\n",
);
let floating = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!floating.success,
"pinned policy must refuse a default-branch meld: {}",
floating.stdout
);
assert!(
floating.stderr.contains("must be pinned"),
"refusal should mention pinning: {}",
floating.stderr
);
assert_eq!(source_count(&sb), 0, "nothing registered on refusal");
let tagged = sb.mind_env(
&["meld", &spec, "--pin-tag", "v1.0"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
tagged.success,
"pinned policy must accept a --pin-tag meld: {}",
tagged.stderr
);
assert_eq!(source_count(&sb), 1, "tagged source should be registered");
}
#[test]
fn learn_skips_disallowed_source_when_locked() {
let sb = melded(); let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow = [\"local/*/never-match\"]\n",
);
let r = sb.mind_env(
&["learn", "agent:dev"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"learn must not error when skipping disallowed sources: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("skipping") && r.stdout.contains("not permitted"),
"learn should report the skipped source: {}",
r.stdout
);
let recall = sb.mind_env(
&["recall", "agent:dev"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
!recall.success,
"the disallowed item must not be installed: {}",
recall.stdout
);
}
#[test]
fn sync_skips_disallowed_source_when_locked() {
let sb = melded();
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow = [\"local/*/never-match\"]\n",
);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"sync must not error on a skipped source: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("skipping") && r.stdout.contains("not permitted"),
"sync should report the skipped source: {}",
r.stdout
);
}
#[test]
fn sync_provisions_auto_meld_and_is_idempotent() {
let (sb, _v1, _v2) = make_pinnable_repo("automeld-src");
let spec = sb.source_spec();
let body = format!(
"[[sources.auto_meld]]\nrepo = \"{spec}\"\ntag = \"v1.0\"\n",
spec = spec.replace('\\', "\\\\")
);
let policy = write_policy(&sb, &body);
assert_eq!(source_count(&sb), 0, "registry starts empty");
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"auto-meld sync should succeed: {} {}",
r.stdout, r.stderr
);
assert_eq!(
source_count(&sb),
1,
"auto_meld entry should be provisioned into the registry: {}",
r.stdout
);
let r2 = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r2.success,
"second sync should succeed: {} {}",
r2.stdout, r2.stderr
);
assert_eq!(
source_count(&sb),
1,
"auto-meld provisioning must be idempotent: {}",
r2.stdout
);
}
#[test]
fn config_lobes_add_refused_when_lobes_locked() {
let sb = Sandbox::named("agents");
let policy = write_policy(&sb, "[lobes]\nlock = true\ntargets = [\"~/.claude\"]\n");
let candidate = sb.base.join("refused-lobe");
let candidate_str = candidate.to_string_lossy().into_owned();
let before = sb.mind_env(
&["config", "lobes", "list"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(before.success, "list before: {}", before.stderr);
let r = sb.mind_env(
&["config", "lobes", "add", &candidate_str],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(!r.success, "locked lobes add must be refused: {}", r.stdout);
assert!(
r.stderr.contains("lock") || r.stderr.contains("refused") || r.stderr.contains("pinned"),
"refusal should be explained: {}",
r.stderr
);
assert!(
!candidate.exists(),
"a refused lobe must not have its directory created (HARN-15 runs only \
after the POL-40 check): {candidate:?}"
);
let after = sb.mind_env(
&["config", "lobes", "list"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
!after.stdout.contains(&candidate_str),
"the refused lobe must not appear: {}",
after.stdout
);
}
#[test]
fn upgrade_skips_disallowed_source_when_locked() {
let sb = melded();
let learn = sb.mind(&["learn", "skill:review"]);
assert!(
learn.success,
"learn failed: {} {}",
learn.stdout, learn.stderr
);
let before = sb.mind(&["recall", "skill:review"]).stdout;
sb.edit_source();
let synced = sb.mind(&["sync"]);
assert!(
synced.success,
"sync failed: {} {}",
synced.stdout, synced.stderr
);
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow = [\"local/*/never-match\"]\n",
);
let r = sb.mind_env(
&["upgrade", "--yes"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"upgrade must not error when skipping disallowed sources: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("skipping") && r.stdout.contains("not permitted"),
"upgrade should report the skipped source: {}",
r.stdout
);
assert!(
!r.stdout.contains("upgraded"),
"the disallowed item must not be upgraded: {}",
r.stdout
);
let after = sb
.mind_env(
&["recall", "skill:review"],
&[("MIND_POLICY_FILE", policy.as_str())],
)
.stdout;
let commit_before = before.lines().find(|l| l.contains("commit")).unwrap_or("");
let commit_after = after.lines().find(|l| l.contains("commit")).unwrap_or("");
assert_eq!(
commit_before, commit_after,
"the skipped item's recorded commit must not advance: before={before} after={after}"
);
let hash_before = before.lines().find(|l| l.contains("hash")).unwrap_or("");
let hash_after = after.lines().find(|l| l.contains("hash")).unwrap_or("");
assert_eq!(
hash_before, hash_after,
"the skipped item's recorded hash must not advance: before={before} after={after}"
);
}
#[test]
fn upgrade_applies_allowed_source_while_skipping_disallowed() {
let sb = melded();
let learn = sb.mind(&["learn", "skill:review"]);
assert!(
learn.success,
"learn failed: {} {}",
learn.stdout, learn.stderr
);
sb.edit_source();
let synced = sb.mind(&["sync"]);
assert!(
synced.success,
"sync failed: {} {}",
synced.stdout, synced.stderr
);
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow = [\"local/*/agents\"]\n",
);
let r = sb.mind_env(
&["upgrade", "--yes"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(r.success, "upgrade failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("upgraded skill:review"),
"an allowed source must be upgraded: {}",
r.stdout
);
assert!(
!r.stdout.contains("skipping"),
"an allowed source must not be skipped: {}",
r.stdout
);
}
#[test]
fn sync_provisions_auto_meld_under_lock_and_is_idempotent() {
let (sb, _v1, _v2) = make_pinnable_repo("automeld-locked");
let spec = sb.source_spec();
let escaped = spec.replace('\\', "\\\\");
let raw_pat = sb.base.join("*").to_string_lossy().replace('\\', "\\\\");
let body = format!(
"[sources]\nlock = true\npinned = true\nallow = [\"{raw_pat}\", \"local/*/automeld-locked\"]\n\n[[sources.auto_meld]]\nrepo = \"{escaped}\"\ntag = \"v1.0\"\n"
);
let policy = write_policy(&sb, &body);
assert_eq!(source_count(&sb), 0, "registry starts empty");
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"locked+pinned auto-meld sync should succeed: {} {}",
r.stdout, r.stderr
);
assert_eq!(
source_count(&sb),
1,
"the allowed+pinned auto_meld entry should be provisioned under lock: {}",
r.stdout
);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"tag\"") && pin_json.contains("v1.0"),
"auto_meld entry should be provisioned at its declared tag pin: {pin_json}"
);
let r2 = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r2.success,
"second locked sync should succeed: {} {}",
r2.stdout, r2.stderr
);
assert_eq!(
source_count(&sb),
1,
"locked auto-meld provisioning must be idempotent: {}",
r2.stdout
);
}
#[test]
fn auto_meld_entry_already_melded_is_not_remelded() {
let (sb, _v1, _v2) = make_pinnable_repo("automeld-pre");
let spec = sb.source_spec();
let pre = sb.mind(&["meld", &spec, "--pin-tag", "v1.0"]);
assert!(
pre.success,
"pre-meld failed: {} {}",
pre.stdout, pre.stderr
);
assert_eq!(source_count(&sb), 1, "source melded once");
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ntag = \"v1.0\"\n");
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(r.success, "sync failed: {} {}", r.stdout, r.stderr);
assert_eq!(
source_count(&sb),
1,
"an already-melded auto_meld entry at the same pin must not be re-melded: {}",
r.stdout
);
assert!(
!r.stdout.contains("re-pinned"),
"matching pin must not emit a re-pinned message: {}",
r.stdout
);
let pin_json = read_source_pin_json(&sb);
assert!(
pin_json.contains("\"tag\"") && pin_json.contains("v1.0"),
"pin must remain at tag v1.0: {pin_json}"
);
}
#[test]
fn auto_meld_pin_bump_reconciles_on_sync() {
let (sb, _sha_v1, _sha_v2) = make_pinnable_repo("automeld-repin");
let spec = sb.source_spec();
git(&sb.source, &["tag", "v2.0"]);
let pre = sb.mind(&["meld", &spec, "--pin-tag", "v1.0"]);
assert!(pre.success, "pre-meld at v1.0 failed: {}", pre.stderr);
assert_eq!(source_count(&sb), 1, "one source registered");
let pin_before = read_source_pin_json(&sb);
assert!(
pin_before.contains("\"tag\"") && pin_before.contains("v1.0"),
"initial pin must be tag v1.0: {pin_before}"
);
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ntag = \"v2.0\"\n");
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"sync with bumped pin failed: {} {}",
r.stdout, r.stderr
);
assert_eq!(
source_count(&sb),
1,
"source count must remain 1 after re-pin"
);
assert!(
r.stdout.contains("re-pinned"),
"sync must report the re-pin: {}",
r.stdout
);
assert!(
r.stdout.contains("v1.0") && r.stdout.contains("v2.0"),
"re-pinned message must name old and new pins: {}",
r.stdout
);
let pin_after = read_source_pin_json(&sb);
assert!(
pin_after.contains("\"tag\"") && pin_after.contains("v2.0"),
"recorded pin must be updated to tag v2.0: {pin_after}"
);
assert!(
!pin_after.contains("v1.0"),
"old pin v1.0 must not remain in sources.json: {pin_after}"
);
}
#[test]
fn auto_meld_repin_default_branch_to_follow_branch_reconciles() {
let (sb, sha_v1, sha_v2) = make_pinnable_repo("automeld-db-to-branch");
let spec = sb.source_spec();
let pre = sb.mind(&["meld", &spec]);
assert!(
pre.success,
"pre-meld (default branch) failed: {}",
pre.stderr
);
assert_eq!(source_count(&sb), 1, "one source registered");
assert_eq!(
read_source_commit(&sb),
sha_v2,
"a default-branch meld records the main tip (sha_v2)"
);
let pin_before = read_source_pin_json(&sb);
assert!(
pin_before.contains("default-branch"),
"initial pin must be default-branch: {pin_before}"
);
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\nfollow_branch = \"stable\"\n");
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(r.success, "re-pin sync failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("re-pinned"),
"sync must report the re-pin: {}",
r.stdout
);
assert!(
r.stdout.contains("default branch") && r.stdout.contains("branch stable"),
"re-pin message must name the default-branch -> follow-branch transition: {}",
r.stdout
);
let pin_after = read_source_pin_json(&sb);
assert!(
pin_after.contains("follow-branch") && pin_after.contains("stable"),
"recorded pin must be follow-branch stable: {pin_after}"
);
assert_eq!(
read_source_commit(&sb),
sha_v1,
"the follow_branch (stable) tip (sha_v1) must be landed after re-pin"
);
assert_eq!(source_count(&sb), 1, "source stays registered exactly once");
}
#[test]
fn auto_meld_repin_lands_new_ref_and_second_sync_is_idempotent() {
let (sb, sha_v1, sha_v2) = make_pinnable_repo("automeld-repin-land");
let spec = sb.source_spec();
git(&sb.source, &["tag", "v2.0"]);
let pre = sb.mind(&["meld", &spec, "--pin-tag", "v1.0"]);
assert!(pre.success, "pre-meld at v1.0 failed: {}", pre.stderr);
assert_eq!(read_source_commit(&sb), sha_v1, "v1.0 pin lands sha_v1");
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ntag = \"v2.0\"\n");
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(r.success, "re-pin sync failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("re-pinned"),
"first sync must re-pin: {}",
r.stdout
);
assert_eq!(
read_source_commit(&sb),
sha_v2,
"the re-pinned v2.0 tag's commit (sha_v2) must be fetched and landed on the same sync"
);
let pin_after = read_source_pin_json(&sb);
assert!(
pin_after.contains("\"tag\"") && pin_after.contains("v2.0"),
"recorded pin must be persisted as tag v2.0: {pin_after}"
);
let r2 = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r2.success,
"second sync failed: {} {}",
r2.stdout, r2.stderr
);
assert!(
!r2.stdout.contains("re-pinned"),
"a matching pin on the second sync must not re-pin again: {}",
r2.stdout
);
assert_eq!(
read_source_commit(&sb),
sha_v2,
"the landed commit must remain at sha_v2 after the idempotent second sync"
);
assert_eq!(source_count(&sb), 1, "still exactly one source");
}
#[test]
fn auto_meld_repin_message_suppressed_under_json() {
let (sb, _sha_v1, sha_v2) = make_pinnable_repo("automeld-repin-json");
let spec = sb.source_spec();
git(&sb.source, &["tag", "v2.0"]);
let pre = sb.mind(&["meld", &spec, "--pin-tag", "v1.0"]);
assert!(pre.success, "pre-meld at v1.0 failed: {}", pre.stderr);
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ntag = \"v2.0\"\n");
let policy = write_policy(&sb, &body);
let r = sb.mind_env(
&["sync", "--json"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(r.success, "sync --json failed: {} {}", r.stdout, r.stderr);
assert!(
!r.stdout.contains("re-pinned"),
"the re-pin message must not be emitted under --json: {}",
r.stdout
);
let pin_after = read_source_pin_json(&sb);
assert!(
pin_after.contains("\"tag\"") && pin_after.contains("v2.0"),
"the pin must still be reconciled to v2.0 under --json: {pin_after}"
);
assert_eq!(
read_source_commit(&sb),
sha_v2,
"the new ref must still land under --json"
);
}
#[test]
fn auto_meld_repin_touches_only_the_drifting_source() {
let (sb, _sha_a_v1, sha_a_v2) = make_pinnable_repo("automeld-multi-a");
let spec_a = sb.source_spec();
git(&sb.source, &["tag", "v2.0"]);
let src_b = sb.base.join("automeld-multi-b");
write(
&src_b.join("agents/dev.md"),
"---\nname: dev\ndescription: dev b\n---\n# dev b\n",
);
git(&src_b, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&src_b, &["config", "user.email", "t@t"]);
git(&src_b, &["config", "user.name", "t"]);
git(&src_b, &["add", "-A"]);
git(&src_b, &["commit", "-qm", "initial"]);
git(&src_b, &["tag", "v1.0"]);
let spec_b = src_b.to_string_lossy().into_owned();
assert!(
sb.mind(&["meld", &spec_a, "--pin-tag", "v1.0"]).success,
"meld A at v1.0 failed"
);
assert!(
sb.mind(&["meld", &spec_b, "--pin-tag", "v1.0"]).success,
"meld B at v1.0 failed"
);
assert_eq!(source_count(&sb), 2, "two sources registered");
let esc_a = spec_a.replace('\\', "\\\\");
let esc_b = spec_b.replace('\\', "\\\\");
let body = format!(
"[[sources.auto_meld]]\nrepo = \"{esc_a}\"\ntag = \"v2.0\"\n\n[[sources.auto_meld]]\nrepo = \"{esc_b}\"\ntag = \"v1.0\"\n"
);
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(r.success, "sync failed: {} {}", r.stdout, r.stderr);
assert_eq!(
r.stdout.matches("re-pinned").count(),
1,
"exactly one source must be re-pinned: {}",
r.stdout
);
let a_name = format!("local/{}/automeld-multi-a", sb.base_name());
let b_name = format!("local/{}/automeld-multi-b", sb.base_name());
let repin_line = r
.stdout
.lines()
.find(|l| l.contains("re-pinned"))
.unwrap_or("");
assert!(
repin_line.contains(&a_name),
"the re-pin line must name the drifting source A: {repin_line}"
);
assert!(
!repin_line.contains(&b_name),
"the stable source B must not be re-pinned: {repin_line}"
);
assert_eq!(source_count(&sb), 2, "both sources remain registered");
assert_eq!(
read_source_commit(&sb),
sha_a_v2,
"the drifting source A must have landed its new v2.0 commit"
);
let sources_json = read_sources_json(&sb);
assert!(
sources_json.contains("v2.0") && sources_json.contains("v1.0"),
"A must be re-pinned to v2.0 while B keeps v1.0: {sources_json}"
);
}
#[test]
fn sync_auto_meld_provisioning_soft_fails_on_unreachable_entry() {
let sb = Sandbox::named("good-src-pol34");
let good_spec = sb.source_spec();
let meld = sb.mind(&["meld", &good_spec]);
assert!(meld.success, "meld good source failed: {}", meld.stderr);
assert_eq!(source_count(&sb), 1, "good source should be melded");
let bad_repo = "/nonexistent/path/that/cannot/be/cloned";
let escaped_good = good_spec.replace('\\', "\\\\");
let body = format!(
"[[sources.auto_meld]]\nrepo = \"{escaped_good}\"\n\n[[sources.auto_meld]]\nrepo = \"{bad_repo}\"\n"
);
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"sync must exit non-zero when an auto_meld entry fails to provision: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains(bad_repo) || r.stdout.contains(bad_repo),
"the failed entry must be named in the output: stderr={} stdout={}",
r.stderr,
r.stdout
);
assert_eq!(
source_count(&sb),
1,
"the already-melded source must still be registered after a provisioning failure"
);
}
#[test]
fn policy_min_mind_version_gate_surfaces_clean_error_end_to_end() {
let running = env!("CARGO_PKG_VERSION");
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"min-mind-version = \"999.0.0\"\n[future]\nunknown-key = 1\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"a too-new policy must fail closed: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("requires mind >=") && r.stderr.contains("999.0.0"),
"must surface the version gate error naming the required version: {}",
r.stderr
);
assert!(
r.stderr.contains("mind evolve"),
"must tell the user to run `mind evolve`: {}",
r.stderr
);
assert!(
!r.stderr.contains("unknown field") && !r.stderr.contains("unknown-key"),
"version gate must fire BEFORE the strict unknown-field parse: {}",
r.stderr
);
assert_eq!(
source_count(&sb),
0,
"no source registered when the gate fires"
);
assert_ne!(running, "999.0.0");
}
#[test]
fn auto_meld_install_true_installs_items_after_provisioning() {
let sb = Sandbox::named("am-install-src");
let spec = sb.source_spec();
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ninstall = true\n");
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"sync with install=true should succeed: stdout={} stderr={}",
r.stdout, r.stderr
);
let recall = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
recall.success,
"recall after sync+install should succeed: {}",
recall.stderr
);
assert!(
recall.stdout.contains("\"installed\": true"),
"items must be installed after auto_meld with install=true: {}",
recall.stdout
);
assert!(
!recall.stdout.contains("\"installed\": false"),
"no items should be uninstalled: {}",
recall.stdout
);
}
#[test]
fn auto_meld_install_absent_registers_only() {
let sb = Sandbox::named("am-register-only");
let spec = sb.source_spec();
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\n");
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"sync with default install should succeed: stdout={} stderr={}",
r.stdout, r.stderr
);
assert_eq!(source_count(&sb), 1, "source must be provisioned");
let recall = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
!recall.stdout.contains("\"installed\": true"),
"no items must be installed when install is absent: {}",
recall.stdout
);
}
#[test]
fn auto_meld_install_true_build_hook_skipped_by_default() {
let sb = Sandbox::bare("am-bld-skip");
let sentinel_name = "built.txt";
let toml = format!(
concat!(
"[[items]]\n",
"kind = \"tool\"\n",
"name = \"mytool\"\n",
"path = \"tools/mytool\"\n",
"build = \"touch {sentinel}\"\n",
),
sentinel = sentinel_name
);
write(
&sb.source.join("tools/mytool/TOOL.md"),
"---\ndescription: tool with build hook\n---\n# mytool\n",
);
write(&sb.source.join("mind.toml"), &toml);
git(&sb.source, &["add", "-A"]);
git(&sb.source, &["commit", "-qm", "add tool"]);
let spec = sb.source_spec();
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ninstall = true\n");
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"sync should succeed even with a build hook present: stdout={} stderr={}",
r.stdout, r.stderr
);
let store_path = sb.mind_home.join("store/tool/mytool");
assert!(
store_path.exists(),
"tool must be installed in the store: {store_path:?}"
);
let sentinel = store_path.join(sentinel_name);
assert!(
!sentinel.exists(),
"build hook must be skipped without run-build-hooks=true (POL-59): {sentinel:?}"
);
}
#[test]
fn auto_meld_install_true_build_hook_runs_when_enabled() {
let sb = Sandbox::bare("am-bld-run");
let sentinel_name = "built.txt";
let toml = format!(
concat!(
"[[items]]\n",
"kind = \"tool\"\n",
"name = \"mytool\"\n",
"path = \"tools/mytool\"\n",
"build = \"touch {sentinel}\"\n",
),
sentinel = sentinel_name
);
write(
&sb.source.join("tools/mytool/TOOL.md"),
"---\ndescription: tool with build hook\n---\n# mytool\n",
);
write(&sb.source.join("mind.toml"), &toml);
git(&sb.source, &["add", "-A"]);
git(&sb.source, &["commit", "-qm", "add tool"]);
let spec = sb.source_spec();
let escaped = spec.replace('\\', "\\\\");
let body = format!(
"[[sources.auto_meld]]\nrepo = \"{escaped}\"\ninstall = true\nrun-build-hooks = true\n"
);
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"sync with run-build-hooks=true should succeed: stdout={} stderr={}",
r.stdout, r.stderr
);
let sentinel = sb.mind_home.join("store/tool/mytool").join(sentinel_name);
assert!(
sentinel.exists(),
"build hook must run when run-build-hooks=true (POL-59): {sentinel:?}"
);
}
#[test]
fn auto_meld_install_item_failure_soft_fails_and_continues() {
let sb = Sandbox::bare("am-soft-fail");
let fail_toml = concat!(
"[[items]]\n",
"kind = \"tool\"\n",
"name = \"badtool\"\n",
"path = \"tools/badtool\"\n",
"build = \"exit 9\"\n",
"\n",
"[[items]]\n",
"kind = \"skill\"\n",
"name = \"review\"\n",
"path = \"skills/review\"\n",
);
write(
&sb.source.join("tools/badtool/TOOL.md"),
"---\ndescription: tool that fails to build\n---\n# badtool\n",
);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: review skill\n---\n# review\n",
);
write(&sb.source.join("mind.toml"), fail_toml);
git(&sb.source, &["add", "-A"]);
git(&sb.source, &["commit", "-qm", "add items"]);
let spec = sb.source_spec();
let escaped = spec.replace('\\', "\\\\");
let body = format!(
"[[sources.auto_meld]]\nrepo = \"{escaped}\"\ninstall = true\nrun-build-hooks = true\n"
);
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"sync must exit non-zero when an item install fails: stdout={} stderr={}",
r.stdout, r.stderr
);
let skill_store = sb.mind_home.join("store/skill/review");
assert!(
skill_store.exists(),
"the good skill must be installed despite the tool failure (POL-60): {skill_store:?}"
);
let tool_store = sb.mind_home.join("store/tool/badtool");
assert!(
!tool_store.exists(),
"the bad tool must not be in the store after a failed build hook: {tool_store:?}"
);
assert!(
r.stderr.contains("badtool") || r.stderr.contains("failed"),
"failed item must be named in stderr: {}",
r.stderr
);
}
#[test]
fn auto_meld_install_true_second_sync_is_idempotent() {
let sb = Sandbox::named("am-install-idem");
let spec = sb.source_spec();
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ninstall = true\n");
let policy = write_policy(&sb, &body);
let first = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
first.success,
"first sync must succeed: stdout={} stderr={}",
first.stdout, first.stderr
);
let recall1 = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
recall1.stdout.contains("\"installed\": true"),
"items must be installed after the first sync: {}",
recall1.stdout
);
let second = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
second.success,
"second sync must exit zero (idempotent): stdout={} stderr={}",
second.stdout, second.stderr
);
assert!(
!second.stderr.contains("item install failed"),
"second sync must not warn about any item install failure: {}",
second.stderr
);
assert_eq!(
source_count(&sb),
1,
"the source must remain singly registered after a second sync"
);
let recall2 = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
recall2.stdout.contains("\"installed\": true"),
"items must remain installed after the second sync: {}",
recall2.stdout
);
assert!(
!recall2.stdout.contains("\"installed\": false"),
"no item may become uninstalled after the second sync: {}",
recall2.stdout
);
}
#[test]
fn auto_meld_register_only_then_install_true_converges() {
let sb = Sandbox::named("am-converge");
let spec = sb.source_spec();
let escaped = spec.replace('\\', "\\\\");
let body_reg = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\n");
let policy_reg = write_policy(&sb, &body_reg);
let r1 = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy_reg.as_str())]);
assert!(
r1.success,
"register-only sync must succeed: stdout={} stderr={}",
r1.stdout, r1.stderr
);
assert_eq!(
source_count(&sb),
1,
"source must be registered after first sync"
);
let recall1 = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy_reg.as_str())],
);
assert!(
!recall1.stdout.contains("\"installed\": true"),
"items must NOT be installed after register-only sync: {}",
recall1.stdout
);
let body_ins = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ninstall = true\n");
let policy_ins = write_policy(&sb, &body_ins);
let r2 = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy_ins.as_str())]);
assert!(
r2.success,
"install=true sync at same pin must succeed: stdout={} stderr={}",
r2.stdout, r2.stderr
);
let recall2 = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy_ins.as_str())],
);
assert!(
recall2.stdout.contains("\"installed\": true"),
"items must be installed after flipping to install=true at same pin: {}",
recall2.stdout
);
assert!(
!recall2.stdout.contains("\"installed\": false"),
"no item may remain uninstalled after convergence sync: {}",
recall2.stdout
);
}
#[test]
fn auto_meld_repin_with_install_true_installs_at_new_ref() {
let (sb, _sha_v1, _sha_v2) = make_pinnable_repo("am-repin-install");
let spec = sb.source_spec();
git(&sb.source, &["tag", "v2.0"]);
let escaped = spec.replace('\\', "\\\\");
let body_v1 = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ntag = \"v1.0\"\n");
let policy_v1 = write_policy(&sb, &body_v1);
let r1 = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy_v1.as_str())]);
assert!(
r1.success,
"first sync (register at v1.0) must succeed: {} {}",
r1.stdout, r1.stderr
);
assert_eq!(source_count(&sb), 1, "one source registered");
let recall1 = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy_v1.as_str())],
);
assert!(
!recall1.stdout.contains("\"installed\": true"),
"nothing installed after register-only at v1.0: {}",
recall1.stdout
);
let body_v2 =
format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ntag = \"v2.0\"\ninstall = true\n");
let policy_v2 = write_policy(&sb, &body_v2);
let r2 = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy_v2.as_str())]);
assert!(
r2.success,
"re-pin+install sync must succeed: stdout={} stderr={}",
r2.stdout, r2.stderr
);
assert!(
r2.stdout.contains("re-pinned"),
"sync must report the re-pin: {}",
r2.stdout
);
assert_eq!(
source_count(&sb),
1,
"source count must remain 1 after re-pin+install"
);
let recall2 = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy_v2.as_str())],
);
assert!(
recall2.stdout.contains("\"installed\": true"),
"items must be installed after re-pin+install sync: {}",
recall2.stdout
);
assert!(
!recall2.stdout.contains("\"installed\": false"),
"no item may remain uninstalled after re-pin+install: {}",
recall2.stdout
);
}
#[test]
fn auto_meld_repin_install_persists_new_pin_and_lands_ref_durably() {
let (sb, _sha_v1, sha_v2) = make_pinnable_repo("am-repin-install-durable");
let spec = sb.source_spec();
git(&sb.source, &["tag", "v2.0"]);
let escaped = spec.replace('\\', "\\\\");
let body_v1 = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ntag = \"v1.0\"\n");
let policy_v1 = write_policy(&sb, &body_v1);
let r1 = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy_v1.as_str())]);
assert!(
r1.success,
"first sync (register at v1.0) must succeed: {} {}",
r1.stdout, r1.stderr
);
let pin_v1 = read_source_pin_json(&sb);
assert!(
pin_v1.contains("\"tag\"") && pin_v1.contains("v1.0"),
"initial recorded pin must be tag v1.0: {pin_v1}"
);
let body_v2 =
format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ntag = \"v2.0\"\ninstall = true\n");
let policy_v2 = write_policy(&sb, &body_v2);
let r2 = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy_v2.as_str())]);
assert!(
r2.success,
"re-pin+install sync must succeed: {} {}",
r2.stdout, r2.stderr
);
assert!(
r2.stdout.contains("re-pinned"),
"sync must report the re-pin: {}",
r2.stdout
);
let pin_after = read_source_pin_json(&sb);
assert!(
pin_after.contains("\"tag\"") && pin_after.contains("v2.0"),
"re-pinned tag v2.0 must be durably persisted to sources.json: {pin_after}"
);
assert!(
!pin_after.contains("v1.0"),
"old pin v1.0 must not remain on disk after re-pin+install: {pin_after}"
);
assert_eq!(
read_source_commit(&sb),
sha_v2,
"the re-pinned v2.0 commit (sha_v2) must be landed and persisted on disk"
);
assert_eq!(
source_count(&sb),
1,
"source must stay registered exactly once after re-pin+install"
);
let recall2 = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy_v2.as_str())],
);
assert!(
recall2.stdout.contains("\"installed\": true"),
"items must be installed after re-pin+install sync: {}",
recall2.stdout
);
let r3 = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy_v2.as_str())]);
assert!(
r3.success,
"third sync (same-pin + install) must succeed: {} {}",
r3.stdout, r3.stderr
);
assert!(
!r3.stdout.contains("re-pinned"),
"a matching pin must not re-pin again on the same-pin path: {}",
r3.stdout
);
let pin_final = read_source_pin_json(&sb);
assert!(
pin_final.contains("v2.0") && !pin_final.contains("v1.0"),
"the same-pin install pass must leave the persisted pin at v2.0: {pin_final}"
);
assert_eq!(
read_source_commit(&sb),
sha_v2,
"the landed commit must remain at sha_v2 after the same-pin+install sync"
);
let recall3 = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy_v2.as_str())],
);
assert!(
recall3.stdout.contains("\"installed\": true"),
"items must remain installed after the same-pin+install sync: {}",
recall3.stdout
);
assert!(
!recall3.stdout.contains("\"installed\": false"),
"no item may flip to uninstalled on the same-pin+install path: {}",
recall3.stdout
);
assert_eq!(
source_count(&sb),
1,
"source must stay registered exactly once after the same-pin+install sync"
);
}
#[test]
fn auto_meld_install_true_with_zero_items_succeeds() {
let sb = Sandbox::bare("am-install-empty");
let spec = sb.source_spec();
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\ninstall = true\n");
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"sync with install=true on an item-less source must succeed: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
!r.stderr.contains("item install failed"),
"an item-less source must not report any item failure: {}",
r.stderr
);
assert_eq!(
source_count(&sb),
1,
"the item-less source must still be registered"
);
}
#[test]
fn auto_meld_mixed_install_flags_install_only_the_opted_in_source() {
let sb = Sandbox::bare("am-mixed");
let src_a = sb.base.join("am-mixed-a");
write(
&src_a.join("skills/alpha/SKILL.md"),
"---\nname: alpha\ndescription: alpha skill\n---\n# alpha\n",
);
git(&src_a, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&src_a, &["config", "user.email", "t@t"]);
git(&src_a, &["config", "user.name", "t"]);
git(&src_a, &["add", "-A"]);
git(&src_a, &["commit", "-qm", "initial"]);
let spec_a = src_a.to_string_lossy().into_owned();
let src_b = sb.base.join("am-mixed-b");
write(
&src_b.join("skills/beta/SKILL.md"),
"---\nname: beta\ndescription: beta skill\n---\n# beta\n",
);
git(&src_b, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&src_b, &["config", "user.email", "t@t"]);
git(&src_b, &["config", "user.name", "t"]);
git(&src_b, &["add", "-A"]);
git(&src_b, &["commit", "-qm", "initial"]);
let spec_b = src_b.to_string_lossy().into_owned();
let esc_a = spec_a.replace('\\', "\\\\");
let esc_b = spec_b.replace('\\', "\\\\");
let body = format!(
"[[sources.auto_meld]]\nrepo = \"{esc_a}\"\ninstall = true\n\n[[sources.auto_meld]]\nrepo = \"{esc_b}\"\n"
);
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"mixed-policy sync must succeed: stdout={} stderr={}",
r.stdout, r.stderr
);
assert_eq!(
source_count(&sb),
2,
"both auto_meld sources must be registered"
);
assert!(
sb.claude_home.join("skills/alpha").exists(),
"alpha from the install=true source must be installed: {:?}",
sb.claude_home
);
assert!(
!sb.claude_home.join("skills/beta").exists(),
"beta from the register-only source must NOT be installed"
);
}
#[test]
fn auto_meld_run_build_hooks_without_install_is_inert() {
let sb = Sandbox::named("am-rbh-noinstall");
let spec = sb.source_spec();
let escaped = spec.replace('\\', "\\\\");
let body = format!("[[sources.auto_meld]]\nrepo = \"{escaped}\"\nrun-build-hooks = true\n");
let policy = write_policy(&sb, &body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"run-build-hooks without install must be a harmless no-op: stdout={} stderr={}",
r.stdout, r.stderr
);
assert_eq!(
source_count(&sb),
1,
"the source must be registered when only run-build-hooks is set"
);
let recall = sb.mind_env(
&["recall", "--json"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
!recall.stdout.contains("\"installed\": true"),
"no items may be installed when install is absent (run-build-hooks alone is inert): {}",
recall.stdout
);
}
#[test]
fn meld_pinned_policy_accepts_source_directive_tag() {
let (sb, sha_v1, _v2) = make_pinnable_repo("pindir-tag");
sb.write_and_commit("mind.toml", "[source]\npin-tag = \"v1.0\"\n");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\npinned = true\nlock = true\nallow = [\"local/*/pindir-tag\"]\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"a [source] tag directive must satisfy a pinned policy: {} {}",
r.stdout, r.stderr
);
assert_eq!(
source_count(&sb),
1,
"the directive-pinned source should register"
);
assert_eq!(
read_source_commit(&sb),
sha_v1,
"the directive tag pin should land on the tagged commit"
);
}
#[test]
fn meld_pinned_policy_refuses_source_directive_floating_branch() {
let (sb, _v1, _v2) = make_pinnable_repo("pindir-branch");
sb.write_and_commit("mind.toml", "[source]\nfollow-branch = \"stable\"\n");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\npinned = true\nlock = true\nallow = [\"local/*/pindir-branch\"]\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"a [source] follow-branch directive must be refused under a pinned policy: {}",
r.stdout
);
assert!(
r.stderr.contains("must be pinned"),
"refusal should mention pinning: {}",
r.stderr
);
assert_eq!(
source_count(&sb),
0,
"nothing registered on a floating refusal"
);
}
#[test]
fn config_lobes_add_allowed_when_lobes_not_locked() {
let sb = Sandbox::named("agents");
let policy = write_policy(&sb, "[lobes]\nlock = false\ntargets = [\"~/.claude\"]\n");
let lobe = sb.base.join("extra-home");
let lobe_str = lobe.to_string_lossy().into_owned();
let r = sb.mind_env(
&["config", "lobes", "add", &lobe_str],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"an unlocked lobes add must succeed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("added lobe"),
"the add should be reported: {}",
r.stdout
);
let after = sb.mind_env(
&["config", "lobes", "list"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
after.stdout.contains(&lobe_str),
"the added lobe must appear in the list: {}",
after.stdout
);
}
#[test]
fn config_lobes_add_allowed_with_no_lobes_section() {
let sb = Sandbox::named("agents");
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow = [\"local/*/agents\"]\n",
);
let lobe = sb.base.join("home-no-lobes-section");
let lobe_str = lobe.to_string_lossy().into_owned();
let r = sb.mind_env(
&["config", "lobes", "add", &lobe_str],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"lobes add must work when the policy has no [lobes] lock: {} {}",
r.stdout, r.stderr
);
assert!(r.stdout.contains("added lobe"), "{}", r.stdout);
}
#[test]
fn meld_refused_when_not_allowed_leaves_no_clone_and_no_registry() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow = [\"local/*/other-repo\"]\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(!r.success, "refused meld must fail: {}", r.stdout);
assert_eq!(source_count(&sb), 0, "registry must record nothing");
let clone_dir = sb
.mind_home
.join("sources")
.join("local")
.join(sb.base_name())
.join("agents");
assert!(
!clone_dir.exists(),
"no clone should survive a refusal at {}",
clone_dir.display()
);
let leaked = sb.claude_home.join("agents/dev.md");
assert!(
std::fs::symlink_metadata(&leaked).is_err(),
"no item should be installed on a refused meld"
);
}
#[test]
fn meld_unlocked_advisory_warning_text() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\nlock = false\nallow = [\"local/*/other-repo\"]\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"advisory meld must succeed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("allowlist") && r.stderr.contains("advisory"),
"warning should name the allowlist and mark it advisory: {}",
r.stderr
);
assert!(
r.stderr.contains("lock is false"),
"warning should explain the lock is off: {}",
r.stderr
);
assert_eq!(
source_count(&sb),
1,
"the advisory source is still registered"
);
}
fn sandbox_with_declared_hook(name: &str, cmd: &str) -> Sandbox {
let sb = Sandbox::named(name);
sb.write_and_commit("mind.toml", &format!("[source]\ninstall = \"{cmd}\"\n"));
sb
}
#[test]
fn meld_json_stdout_is_only_the_result_object_when_a_hook_is_skipped() {
let sb = sandbox_with_declared_hook("agents", "touch hookran");
let r = sb.mind(&["meld", &sb.source_spec(), "--json"]);
assert!(r.success, "meld --json: {} {}", r.stdout, r.stderr);
serde_json::from_str::<serde_json::Value>(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout under --json must parse as a single JSON document ({e}): {:?}",
r.stdout
)
});
assert!(
r.stderr.contains("note: skipped install hook"),
"the skip note must still be reported, on stderr: {}",
r.stderr
);
}
#[test]
fn learn_json_stdout_is_only_the_result_object_when_an_item_hook_is_skipped() {
let sb = Sandbox::named("agents");
sb.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"skill\"\nname = \"review\"\npath = \"skills/review\"\n\
install = \"touch itemhookran\"\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success, "meld");
let r = sb.mind(&["learn", "review", "--json"]);
assert!(r.success, "learn --json: {} {}", r.stdout, r.stderr);
serde_json::from_str::<serde_json::Value>(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout under --json must parse as a single JSON document ({e}): {:?}",
r.stdout
)
});
assert!(
r.stderr
.contains("note: skipped install hook for skill:review"),
"the per-item skip note must still be reported, on stderr: {}",
r.stderr
);
}
#[test]
fn json_does_not_swallow_the_stdout_of_verbs_whose_output_is_not_json() {
let sb = Sandbox::new();
let comp = sb.mind(&["--json", "completions", "bash"]);
assert!(comp.success, "completions --json: {}", comp.stderr);
assert!(
comp.stdout.contains("mind"),
"the completion script must still go to STDOUT under --json: stdout={:?} stderr={:?}",
comp.stdout,
comp.stderr
);
let man = sb.mind(&["--json", "man"]);
assert!(man.success, "man --json: {}", man.stderr);
assert!(
man.stdout.contains(".TH"),
"the roff page must still go to STDOUT under --json: stdout={:?} stderr={:?}",
man.stdout,
man.stderr
);
}
#[test]
fn json_stdout_is_exactly_one_document_across_the_verb_surface() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let lobe = sb.base.join("extra-lobe");
std::fs::create_dir_all(&lobe).unwrap();
let lobe_str = lobe.to_string_lossy().into_owned();
let steps: Vec<Vec<&str>> = vec![
vec!["meld", &spec, "--json"],
vec!["meld", &spec, "--json"], vec!["recall", "--json"],
vec!["recall", "--sources", "--json"],
vec!["probe", "--json", "--no-tui"],
vec!["learn", "review", "--json"],
vec!["learn", "review", "--json"], vec!["introspect", "--json"],
vec!["sync", "--json"],
vec!["upgrade", "--json", "--yes"],
vec!["config", "lobes", "add", "--json", &lobe_str],
vec!["config", "lobes", "remove", "--json", &lobe_str],
vec!["forget", "review", "--json", "--yes"],
vec!["unmeld", "agents", "--json", "--yes"],
];
let mut silent: Vec<String> = Vec::new();
for args in steps {
let r = sb.mind(&args);
let body = r.stdout.trim();
if body.is_empty() {
silent.push(args.join(" "));
continue;
}
serde_json::from_str::<serde_json::Value>(body).unwrap_or_else(|e| {
panic!(
"`mind {}` must leave stdout as exactly one JSON document ({e}); \
anything else belongs on stderr (CLI-217):\nstdout={:?}\nstderr={:?}",
args.join(" "),
r.stdout,
r.stderr
)
});
}
assert!(
silent.is_empty(),
"every --json invocation must answer with a document; these wrote \
nothing to stdout: {silent:?}"
);
}
#[test]
fn unmeld_json_stdout_is_only_the_result_object_when_an_uninstall_hook_is_skipped() {
let sb = Sandbox::named("agents");
sb.write_and_commit(
"mind.toml",
"[[hooks]]\nname = \"cleanup\"\nrun = \"echo clean\"\nevent = \"uninstall\"\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success, "meld");
let r = sb.mind(&["unmeld", "agents", "--json"]);
assert!(r.success, "unmeld --json: {} {}", r.stdout, r.stderr);
serde_json::from_str::<serde_json::Value>(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout under --json must parse as a single JSON document ({e}): {:?}",
r.stdout
)
});
assert!(
r.stderr.contains("note: skipped uninstall hook 'cleanup'"),
"the uninstall-hook skip note must still be reported, on stderr: {}",
r.stderr
);
}
#[test]
fn hook_skip_notes_stay_on_stdout_in_text_mode() {
let src = sandbox_with_declared_hook("agents", "touch hookran");
let meld = src.mind(&["meld", &src.source_spec()]);
assert!(meld.success, "meld: {} {}", meld.stdout, meld.stderr);
assert!(
meld.stdout.contains("note: skipped install hook "),
"the install-hook skip note belongs on STDOUT in text mode: stdout={} stderr={}",
meld.stdout,
meld.stderr
);
let un = Sandbox::named("agents");
un.write_and_commit(
"mind.toml",
"[[hooks]]\nname = \"cleanup\"\nrun = \"echo clean\"\nevent = \"uninstall\"\n",
);
assert!(un.mind(&["meld", &un.source_spec()]).success, "meld");
let r = un.mind(&["unmeld", "agents"]);
assert!(r.success, "unmeld: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("note: skipped uninstall hook 'cleanup'"),
"the uninstall-hook skip note belongs on STDOUT in text mode: stdout={} stderr={}",
r.stdout,
r.stderr
);
}
#[test]
fn item_hook_skip_note_stays_on_stdout_in_text_mode() {
let sb = Sandbox::named("agents");
sb.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"skill\"\nname = \"review\"\npath = \"skills/review\"\n\
install = \"touch itemhookran\"\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success, "meld");
let r = sb.mind(&["learn", "review"]);
assert!(r.success, "learn: {} {}", r.stdout, r.stderr);
assert!(
r.stdout
.contains("note: skipped install hook for skill:review"),
"the per-item skip note belongs on STDOUT in text mode: stdout={} stderr={}",
r.stdout,
r.stderr
);
}
#[test]
fn meld_json_stdout_is_only_the_result_object_when_a_hook_actually_runs() {
let sb = sandbox_with_declared_hook("agents", "printf 'hook-said-%s' this");
let r = sb.mind(&[
"meld",
&sb.source_spec(),
"--json",
"--accept-hooks=install,update,uninstall",
]);
assert!(r.success, "meld --json: {} {}", r.stdout, r.stderr);
serde_json::from_str::<serde_json::Value>(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout under --json must parse as a single JSON document ({e}): {:?}",
r.stdout
)
});
assert!(
r.stderr.contains("hook-said-this"),
"the hook's own output must still be reported, on stderr: {}",
r.stderr
);
assert!(
!r.stdout.contains("hook-said-this"),
"source-controlled hook output must never reach --json stdout: {}",
r.stdout
);
}
#[test]
fn remeld_json_stdout_is_one_document_when_it_installs_pending_items() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--register-only"]).success,
"register-only meld"
);
let r = sb.mind(&["meld", &spec, "--yes", "--json"]);
assert!(r.success, "re-meld --json: {} {}", r.stdout, r.stderr);
let doc: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout under --json must parse as a single JSON document ({e}): {:?}",
r.stdout
)
});
assert_eq!(
doc["schema"], 1,
"the one document must be a CLI-153 result envelope: {doc:#}"
);
assert_eq!(
doc["action"], "meld",
"a re-meld must answer with the invoked verb, not the nested one: {doc:#}"
);
assert_eq!(
doc["outcome"], "already-melded",
"CLI-153's re-meld outcome token: {doc:#}"
);
assert!(
doc["installed"]
.as_array()
.is_some_and(|a| a.iter().any(|k| k == "skill:review")),
"the pending items installed by this call must be listed: {doc:#}"
);
assert!(
sb.claude_home.join("skills/review").exists(),
"the pending items must actually have been installed: {doc:#}"
);
}
#[test]
fn remeld_json_answers_a_machine_caller_even_when_it_installs_nothing() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--register-only"]).success,
"register-only meld"
);
let r = sb.mind(&["meld", &spec, "--json"]);
assert!(r.success, "re-meld --json: {} {}", r.stdout, r.stderr);
let doc: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"a mutating verb must answer --json with a document, not silence \
({e}): stdout={:?} stderr={:?}",
r.stdout, r.stderr
)
});
assert_eq!(doc["action"], "meld", "{doc:#}");
assert_eq!(doc["outcome"], "already-melded", "{doc:#}");
assert!(
doc["pending_items"].as_u64().is_some_and(|n| n > 0),
"the un-installed items must be reported as pending (CLI-156's field on \
the fresh-meld branch): {doc:#}"
);
assert!(
!sb.claude_home.join("skills/review").exists(),
"nothing may be installed without --yes: {doc:#}"
);
}
#[test]
fn remeld_json_of_a_curator_emits_one_document_not_two() {
let sb = Sandbox::bare("json-curator");
nested_repo(&sb.base.join("curated-lib"), "greet");
let nested_abs = sb.base.join("curated-lib").to_string_lossy().into_owned();
sb.write_and_commit(
"mind.toml",
&format!("[[discover.sources]]\nsource = \"{nested_abs}\"\ninstall = true\n"),
);
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--register-only"]).success,
"register-only meld of the curator"
);
let r = sb.mind(&["meld", &spec, "--yes", "--json"]);
assert!(r.success, "re-meld --json: {} {}", r.stdout, r.stderr);
let doc: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout under --json must parse as a single JSON document ({e}): {:?}",
r.stdout
)
});
assert_eq!(doc["action"], "meld", "{doc:#}");
assert_eq!(doc["outcome"], "already-melded", "{doc:#}");
assert!(
doc["installed"]
.as_array()
.is_some_and(|a| a.iter().any(|k| k == "skill:greet")),
"the curated chain's install must be reported in meld's object: {doc:#}"
);
assert!(
sb.claude_home.join("skills/greet").exists(),
"the curated item must actually be installed: {doc:#}"
);
}
#[test]
fn meld_with_declared_hook_non_tty_skips_but_still_installs() {
let sb = sandbox_with_declared_hook("agents", "touch hookran");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld should still succeed: {}", r.stderr);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("agents"),
"source must be registered after a skipped hook: {}",
sources.stdout
);
assert!(
sb.mind(&["learn", "review"]).success,
"items must install even when the hook is skipped"
);
let marker = sb.source.clone().join("hookran");
assert!(
!marker.exists(),
"the install hook must not have run: {} exists",
marker.display()
);
let prefix = "note: skipped install hook ";
let suffix = "; its items may not work until it runs";
let reported = (r.stdout.contains(prefix) && r.stdout.contains(suffix))
|| (r.stderr.contains(prefix) && r.stderr.contains(suffix));
assert!(
reported,
"the skip must be reported with the exact note: {} {}",
r.stdout, r.stderr
);
let json = std::fs::read_to_string(sb.mind_home.join("sources.json")).unwrap();
assert!(
json.contains("touch hookran"),
"registry must record the hook command: {json}"
);
assert!(
json.contains("install_hooks") && json.contains("\"ran_at\": null"),
"a skipped hook must record in install_hooks with ran_at = null: {json}"
);
}
#[test]
fn meld_accept_hooks_runs_hook_and_records_it() {
let sb = sandbox_with_declared_hook("agents", "touch hookran");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"]);
assert!(r.success, "meld should succeed: {}", r.stderr);
let marker = sb.source.clone().join("hookran");
assert!(
marker.exists(),
"the install hook must have run in the clone: {} missing",
marker.display()
);
let json = std::fs::read_to_string(sb.mind_home.join("sources.json")).unwrap();
assert!(
json.contains("touch hookran"),
"registry must record the hook command: {json}"
);
assert!(
json.contains("install_hooks") && !json.contains("\"ran_at\": null"),
"a hook that ran must record a non-null ran_at in install_hooks: {json}"
);
}
#[test]
fn meld_hook_nonzero_exit_fails_and_registers_nothing() {
let sb = sandbox_with_declared_hook("agents", "exit 1");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"]);
assert!(
!r.success,
"a non-zero hook exit must fail the meld: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("install hook") && r.stderr.contains("failed"),
"stderr must report the failed install hook: {}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no sources melded"),
"no source must be registered after a failed hook: {}",
sources.stdout
);
let sources_json = sb.mind_home.join("sources.json");
if sources_json.exists() {
let json = std::fs::read_to_string(&sources_json).unwrap();
assert!(
!json.contains("\"repo\": \"agents\""),
"sources.json must not list the source after a failed hook: {json}"
);
}
assert!(
sb.source.exists(),
"a failed hook must not delete a linked source's working tree at {}",
sb.source.display()
);
}
#[test]
fn meld_install_hook_flag_supplies_hook_without_mind_toml() {
let sb = Sandbox::new(); let spec = sb.source_spec();
let r = sb.mind(&[
"meld",
&spec,
"--install-hook",
"touch hookran",
"--accept-hooks=install,update,uninstall",
]);
assert!(r.success, "meld should succeed: {}", r.stderr);
let marker = sb.source.clone().join("hookran");
assert!(
marker.exists(),
"the supplied hook must have run: {} missing",
marker.display()
);
let json = std::fs::read_to_string(sb.mind_home.join("sources.json")).unwrap();
assert!(
json.contains("touch hookran"),
"registry must record the supplied hook command: {json}"
);
assert!(
!json.contains("\"install_hook_commit\": null"),
"install_hook_commit must be non-null after the supplied hook ran: {json}"
);
}
#[test]
fn recall_sources_shows_install_hook_marker() {
let sb = sandbox_with_declared_hook("agents", "touch hookran");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"])
.success
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(sources.success, "recall failed: {}", sources.stderr);
assert!(
sources.stdout.contains(" hook]"),
"recall --sources must mark a source with the bracketed ` hook]` token: {}",
sources.stdout
);
}
#[test]
fn upgrade_source_hook_rerun_mid_batch_failure_saves_earlier_hook_runs() {
let a = sandbox_with_declared_hook("amarker", "true");
assert!(
a.mind(&[
"meld",
&a.source_spec(),
"--accept-hooks=install,update,uninstall"
])
.success
);
let b = Sandbox::bare("bfailer");
b.write_and_commit("mind.toml", "[source]\ninstall = \"exit 7\"\n");
assert!(a.mind(&["meld", &b.source_spec()]).success);
a.edit_source();
assert!(a.mind(&["sync"]).success);
let r = a.mind(&[
"upgrade",
"--yes",
"--accept-hooks=install,update,uninstall",
]);
assert!(
!r.success,
"must fail on B's hook: {} {}",
r.stdout, r.stderr
);
let sources_json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(a.mind_home.join("sources.json")).unwrap())
.unwrap();
let sources = sources_json["sources"].as_array().unwrap();
let a_entry = sources
.iter()
.find(|s| s["name"].as_str().unwrap().contains("amarker"))
.expect("source A recorded");
let ran_at = a_entry["install_hooks"][0]["ran_at"].as_str();
let commit = a_entry["commit"].as_str();
assert_eq!(
ran_at, commit,
"source A's hook re-run must be persisted before B's failure propagated: {a_entry:#}"
);
}
#[test]
fn remeld_hook_failure_mid_batch_saves_earlier_successful_hook_run() {
let sb = Sandbox::named("remeld-hooks");
sb.write_and_commit(
"mind.toml",
concat!(
"[[hooks]]\n",
"name = \"first\"\n",
"run = \"true\"\n",
"event = \"install\"\n",
"\n",
"[[hooks]]\n",
"name = \"second\"\n",
"run = \"exit 9\"\n",
"event = \"install\"\n",
),
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let r = sb.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"]);
assert!(
!r.success,
"the re-meld must fail on the second hook: {} {}",
r.stdout, r.stderr
);
let sources_json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(sb.mind_home.join("sources.json")).unwrap())
.unwrap();
let entry = &sources_json["sources"][0];
let first_ran_at = entry["install_hooks"][0]["ran_at"].as_str();
assert!(
first_ran_at.is_some(),
"the first hook's successful run must be persisted even though the \
second hook in the same pass failed: {entry:#}"
);
}
#[test]
fn upgrade_reruns_hook_after_source_advances() {
let sb = sandbox_with_declared_hook("agents", "touch hookran");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"])
.success,
"initial meld should run the hook and record commit C1"
);
let marker = sb.source.clone().join("hookran");
assert!(marker.exists(), "the hook should have run on meld");
std::fs::remove_file(&marker).unwrap();
sb.edit_source();
assert!(sb.mind(&["sync"]).success);
assert!(
!marker.exists(),
"sync alone must not re-run the hook (HOOK-11)"
);
let ev = sb.mind(&["upgrade", "-y", "--accept-hooks=install,update,uninstall"]);
assert!(ev.success, "upgrade failed: {} {}", ev.stdout, ev.stderr);
assert!(
marker.exists(),
"upgrade must re-run the hook after the source advanced: {} missing",
marker.display()
);
std::fs::remove_file(&marker).unwrap();
let again = sb.mind(&["upgrade", "-y", "--accept-hooks=install,update,uninstall"]);
assert!(again.success, "second upgrade failed: {}", again.stderr);
assert!(
!marker.exists(),
"upgrade must not re-run the hook when the source has not advanced"
);
}
#[test]
fn sync_upgrade_runs_hook_rerun_only_with_the_skip_check_flag() {
let sb = sandbox_with_declared_hook("agents", "touch hookran");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"])
.success,
"initial meld should run the hook and record commit C1"
);
let marker = sb.source.clone().join("hookran");
assert!(marker.exists(), "the hook should have run on meld");
std::fs::remove_file(&marker).unwrap();
sb.edit_source();
let no_flag = sb.mind(&["sync", "--upgrade"]);
assert!(
no_flag.success,
"sync --upgrade failed: {} {}",
no_flag.stdout, no_flag.stderr
);
assert!(
!marker.exists(),
"sync --upgrade without the flag must not re-run the hook (HOOK-22)"
);
let with_flag = sb.mind(&[
"sync",
"--upgrade",
"--accept-hooks=install,update,uninstall",
]);
assert!(
with_flag.success,
"sync --upgrade --accept-hooks=install,update,uninstall failed: {} {}",
with_flag.stdout, with_flag.stderr
);
assert!(
marker.exists(),
"sync --upgrade with the flag must re-run the hook unattended: {} missing",
marker.display()
);
}
#[test]
fn scoped_upgrade_does_not_rerun_unrelated_source_hook() {
let agents = sandbox_with_declared_hook("agents", "touch hookran");
let agents_spec = agents.source_spec();
assert!(
agents
.mind(&[
"meld",
&agents_spec,
"--accept-hooks=install,update,uninstall"
])
.success,
"initial meld of the hooked source should run the hook and record its commit"
);
let tools = Sandbox::named("tools");
assert!(
agents.mind(&["meld", &tools.source_spec()]).success,
"meld of the second (hook-free) source failed"
);
let learn = agents.mind(&["learn", "tools#skill:review"]);
assert!(
learn.success,
"learn failed: {} {}",
learn.stdout, learn.stderr
);
let marker = agents.source.clone().join("hookran");
assert!(
marker.exists(),
"the hook should have run on the initial meld"
);
std::fs::remove_file(&marker).unwrap();
agents.edit_source();
assert!(agents.mind(&["sync"]).success, "sync failed");
assert!(!marker.exists(), "sync alone must not re-run the hook");
let scoped = agents.mind(&[
"upgrade",
"tools#skill:review",
"-y",
"--accept-hooks=install,update,uninstall",
]);
assert!(
scoped.success,
"scoped upgrade failed: {} {}",
scoped.stdout, scoped.stderr
);
assert!(
!marker.exists(),
"a scoped upgrade of an unrelated item must not re-run the hooked source's hook: {} exists",
marker.display()
);
let unscoped = agents.mind(&["upgrade", "-y", "--accept-hooks=install,update,uninstall"]);
assert!(
unscoped.success,
"unscoped upgrade failed: {} {}",
unscoped.stdout, unscoped.stderr
);
assert!(
marker.exists(),
"an unscoped upgrade must re-run the hooked source's hook: {} missing",
marker.display()
);
}
#[test]
fn sync_source_upgrade_scopes_to_the_named_source_only() {
let alpha = Sandbox::bare("alpha");
alpha.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\n",
);
let beta = Sandbox::bare("beta");
beta.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\n",
);
assert!(alpha.mind(&["meld", &alpha.source_spec()]).success);
assert!(alpha.mind(&["meld", &beta.source_spec()]).success);
assert!(alpha.mind(&["learn", "skill:widget-a"]).success);
assert!(alpha.mind(&["learn", "skill:widget-b"]).success);
alpha.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\nedited\n",
);
beta.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\nedited\n",
);
let r = alpha.mind(&["sync", "alpha", "--upgrade", "--yes"]);
assert!(
r.success,
"sync alpha --upgrade failed: {} {}",
r.stdout, r.stderr
);
let a_store =
std::fs::read_to_string(alpha.mind_home.join("store/skill/widget-a/SKILL.md")).unwrap();
assert!(
a_store.contains("edited"),
"alpha's item must be upgraded by `sync alpha --upgrade`: {a_store}"
);
let b_store =
std::fs::read_to_string(alpha.mind_home.join("store/skill/widget-b/SKILL.md")).unwrap();
assert!(
!b_store.contains("edited"),
"beta's item must NOT be upgraded by a `sync alpha --upgrade` that \
only named alpha: {b_store}"
);
let recall = alpha.mind(&["recall"]);
assert!(recall.success, "{}", recall.stderr);
assert!(
recall.stdout.contains("widget-b") && recall.stdout.contains("outdated"),
"beta's item must still be reported outdated: {}",
recall.stdout
);
}
#[test]
fn upgrade_source_syncs_and_upgrades_only_the_named_source() {
let alpha = Sandbox::bare("alpha");
alpha.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\n",
);
let beta = Sandbox::bare("beta");
beta.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\n",
);
for spec in [alpha.source_spec(), beta.source_spec()] {
let m = alpha.mind(&["meld", &spec, "--follow-branch", "main", "--register-only"]);
assert!(m.success, "{} {}", m.stdout, m.stderr);
}
assert!(alpha.mind(&["learn", "skill:widget-a"]).success);
assert!(alpha.mind(&["learn", "skill:widget-b"]).success);
alpha.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\nedited\n",
);
beta.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\nedited\n",
);
let r = alpha.mind(&["upgrade", "--source", "alpha", "--yes", "--json"]);
assert!(
r.success,
"upgrade --source alpha failed: {} {}",
r.stdout, r.stderr
);
let v = parse_json(&r.stdout);
assert_eq!(v["outcome"], "upgraded", "{}", r.stdout);
let a_store =
std::fs::read_to_string(alpha.mind_home.join("store/skill/widget-a/SKILL.md")).unwrap();
assert!(
a_store.contains("edited"),
"alpha's item must upgrade: {a_store}"
);
let b_store =
std::fs::read_to_string(alpha.mind_home.join("store/skill/widget-b/SKILL.md")).unwrap();
assert!(
!b_store.contains("edited"),
"beta's item must not upgrade under `--source alpha`: {b_store}"
);
let stale = alpha.mind(&["upgrade", "--source", "beta", "--no-sync", "--json"]);
assert!(stale.success, "{} {}", stale.stdout, stale.stderr);
assert_eq!(
parse_json(&stale.stdout)["outcome"],
"up-to-date",
"`--source alpha` must not have fetched beta: {}",
stale.stdout
);
let b = alpha.mind(&["upgrade", "--source", "beta", "--yes", "--json"]);
assert!(b.success, "{} {}", b.stdout, b.stderr);
assert_eq!(parse_json(&b.stdout)["outcome"], "upgraded", "{}", b.stdout);
}
#[test]
fn upgrade_source_filter_matching_nothing_is_source_not_found() {
let sb = Sandbox::bare("alpha");
sb.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let r = sb.mind(&["upgrade", "--source", "nope", "--yes"]);
assert!(!r.success, "an unmatched --source must fail: {}", r.stdout);
assert!(
r.stderr.contains("nope"),
"error must name the filter: {}",
r.stderr
);
let bad = sb.mind(&["upgrade", "--source", "[bad", "--yes"]);
assert!(!bad.success, "a malformed glob must fail: {}", bad.stdout);
assert!(
bad.stderr.contains("not a valid glob selector"),
"expected invalid-pattern error: {}",
bad.stderr
);
}
#[test]
fn upgrade_source_with_item_names_only_the_intersection() {
let one = Sandbox::bare("skills");
one.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\n",
);
let two = Sandbox::bare("skills");
two.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\n",
);
assert!(one.mind(&["meld", &one.source_spec()]).success);
assert!(one.mind(&["meld", &two.source_spec()]).success);
assert!(one.mind(&["learn", "skill:widget-a"]).success);
assert!(one.mind(&["learn", "skill:widget-b"]).success);
one.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\nedited\n",
);
two.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\nedited\n",
);
let wide = one.mind(&["upgrade", "skills#*", "--no-sync"]);
assert!(wide.stdout.contains("matched 2 sources"), "{}", wide.stdout);
let both = one.mind(&["upgrade", "skills#*", "--source", "skills", "--no-sync"]);
assert!(
both.stdout
.contains("the filters \"skills#*\" and \"skills\" together matched 2 sources"),
"{}",
both.stdout
);
let one_id = format!("{}/skills", one.base_name());
let r = one.mind(&["upgrade", "skills#*", "--source", &one_id, "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let a_store =
std::fs::read_to_string(one.mind_home.join("store/skill/widget-a/SKILL.md")).unwrap();
assert!(a_store.contains("edited"), "{a_store}");
let b_store =
std::fs::read_to_string(one.mind_home.join("store/skill/widget-b/SKILL.md")).unwrap();
assert!(!b_store.contains("edited"), "{b_store}");
assert!(
!r.stdout.contains("matched 2 sources"),
"the disclosure must not name a source outside the intersection: {}",
r.stdout
);
}
#[test]
fn sync_multi_match_filter_upgrade_names_every_matched_source_before_consent() {
let one = Sandbox::bare("skills");
one.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\n",
);
let two = Sandbox::bare("skills");
two.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\n",
);
assert!(one.mind(&["meld", &one.source_spec()]).success);
assert!(one.mind(&["meld", &two.source_spec()]).success);
assert!(one.mind(&["learn", "skill:widget-a"]).success);
assert!(one.mind(&["learn", "skill:widget-b"]).success);
one.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\nedited\n",
);
two.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\nedited\n",
);
let one_id = format!("{}/skills", one.base_name());
let two_id = format!("{}/skills", two.base_name());
let r = one.mind(&["sync", "skills", "--upgrade"]);
assert!(
r.stdout.contains("matched 2 sources"),
"the disclosure's discriminating substring must appear on stdout: {}",
r.stdout
);
assert!(
r.stdout.contains(&one_id) && r.stdout.contains(&two_id),
"the confirmation must name every matched source ({one_id}, {two_id}): {}",
r.stdout
);
let disclosure_pos = r.stdout.find("matched 2 sources").expect("checked above");
let prompt_pos = r
.stdout
.find("apply these upgrades?")
.expect("the confirm prompt must still be reached (both sources have a pending upgrade)");
assert!(
disclosure_pos < prompt_pos,
"the disclosure must precede the confirm prompt: {}",
r.stdout
);
let unapplied =
std::fs::read_to_string(one.mind_home.join("store/skill/widget-a/SKILL.md")).unwrap();
assert!(
!unapplied.contains("edited"),
"nothing may be applied without consent: {unapplied}"
);
let j = one.mind(&["sync", "skills", "--upgrade", "--json"]);
assert!(!j.success);
let v = parse_json(&j.stdout);
assert_eq!(
v["error"]["kind"], "confirmation-required",
"expected the LIFE-45 refusal: {v}"
);
let action = v["error"]["message"].as_str().unwrap_or_default();
assert!(
action.contains(&one_id) && action.contains(&two_id),
"the --json refusal must name every matched source: {action}"
);
let single = one.mind(&["sync", &one_id, "--upgrade"]);
assert!(
single.stdout.contains("apply these upgrades?"),
"the single-match run must still reach the confirm prompt \
(positive anchor for the negative assertion below): {}",
single.stdout
);
assert!(
!(single.stdout.contains("matched") && single.stdout.contains("sources;")),
"a single-match filter must not grow the confirmation: {}",
single.stdout
);
let applied = one.mind(&["sync", "skills", "--upgrade", "--yes"]);
assert!(
applied.success,
"sync skills --upgrade --yes failed: {} {}",
applied.stdout, applied.stderr
);
for item in ["widget-a", "widget-b"] {
let store =
std::fs::read_to_string(one.mind_home.join(format!("store/skill/{item}/SKILL.md")))
.unwrap();
assert!(
store.contains("edited"),
"{item} must be upgraded by the multi-match filter: {store}"
);
}
}
#[test]
fn upgrade_item_ref_filter_names_every_matched_source_before_consent() {
let one = Sandbox::bare("skills");
one.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\n",
);
let two = Sandbox::bare("skills");
two.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\n",
);
assert!(one.mind(&["meld", &one.source_spec()]).success);
assert!(one.mind(&["meld", &two.source_spec()]).success);
assert!(one.mind(&["learn", "skill:widget-a"]).success);
assert!(one.mind(&["learn", "skill:widget-b"]).success);
one.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\nedited\n",
);
two.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\nedited\n",
);
let one_id = format!("{}/skills", one.base_name());
let two_id = format!("{}/skills", two.base_name());
let r = one.mind(&["upgrade", "skills#*"]);
assert!(
r.stdout.contains("matched 2 sources"),
"the disclosure's discriminating substring must appear on stdout: {}",
r.stdout
);
assert!(
r.stdout.contains(&one_id) && r.stdout.contains(&two_id),
"the disclosure must name every matched source ({one_id}, {two_id}): {}",
r.stdout
);
let disclosure_pos = r.stdout.find("matched 2 sources").expect("checked above");
let prompt_pos = r
.stdout
.find("apply these upgrades?")
.expect("both sources have a pending upgrade, so the confirm prompt must be reached");
assert!(
disclosure_pos < prompt_pos,
"the disclosure must precede the confirm prompt: {}",
r.stdout
);
let unapplied =
std::fs::read_to_string(one.mind_home.join("store/skill/widget-a/SKILL.md")).unwrap();
assert!(
!unapplied.contains("edited"),
"nothing may be applied without consent: {unapplied}"
);
}
#[test]
fn upgrade_disclosure_fires_before_the_hook_rerun_pass_and_under_yes() {
let one = sandbox_with_declared_hook("skills", "touch hookran-one");
let two = sandbox_with_declared_hook("skills", "touch hookran-two");
one.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\n",
);
two.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\n",
);
assert!(
one.mind(&[
"meld",
&one.source_spec(),
"--accept-hooks=install,update,uninstall"
])
.success
);
assert!(
one.mind(&[
"meld",
&two.source_spec(),
"--accept-hooks=install,update,uninstall"
])
.success
);
assert!(one.mind(&["learn", "skill:widget-a"]).success);
assert!(one.mind(&["learn", "skill:widget-b"]).success);
let marker_one = one.source.join("hookran-one");
let marker_two = two.source.join("hookran-two");
assert!(marker_one.exists() && marker_two.exists());
std::fs::remove_file(&marker_one).unwrap();
std::fs::remove_file(&marker_two).unwrap();
one.edit_source();
two.edit_source();
let r = one.mind(&[
"upgrade",
"skills#*",
"--yes",
"--accept-hooks=install,update,uninstall",
]);
assert!(
r.success,
"upgrade 'skills#*' --yes failed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("matched 2 sources"),
"--yes must not suppress the multi-source disclosure: {}",
r.stdout
);
let disclosure_pos = r.stdout.find("matched 2 sources").expect("checked above");
let hook_marker_pos = r.stdout.find("install hook for").expect(
"both sources' hooks are due to re-run under --accept-hooks=install,update,uninstall",
);
assert!(
disclosure_pos < hook_marker_pos,
"the disclosure must precede the first hook-execution marker: {}",
r.stdout
);
assert!(marker_one.exists() && marker_two.exists());
}
#[test]
fn upgrade_disclosure_fires_even_with_nothing_pending() {
let one = Sandbox::bare("skills");
one.write_and_commit(
"skills/widget-a/SKILL.md",
"---\nname: widget-a\ndescription: a\n---\n# widget a\n",
);
let two = Sandbox::bare("skills");
two.write_and_commit(
"skills/widget-b/SKILL.md",
"---\nname: widget-b\ndescription: b\n---\n# widget b\n",
);
assert!(one.mind(&["meld", &one.source_spec()]).success);
assert!(one.mind(&["meld", &two.source_spec()]).success);
assert!(one.mind(&["learn", "skill:widget-a"]).success);
assert!(one.mind(&["learn", "skill:widget-b"]).success);
let r = one.mind(&["upgrade", "skills#*"]);
assert!(
r.success,
"upgrade 'skills#*' failed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("matched 2 sources"),
"the disclosure must fire even with nothing pending: {}",
r.stdout
);
assert!(
r.stdout.contains("up to date"),
"the run must still report up to date: {}",
r.stdout
);
}
#[test]
fn sync_source_upgrade_does_not_rerun_an_unrelated_sources_hook() {
let agents = sandbox_with_declared_hook("agents", "touch hookran");
let agents_spec = agents.source_spec();
assert!(
agents
.mind(&[
"meld",
&agents_spec,
"--accept-hooks=install,update,uninstall"
])
.success,
"initial meld of the hooked source should run the hook and record its commit"
);
let tools = Sandbox::named("tools");
assert!(
agents.mind(&["meld", &tools.source_spec()]).success,
"meld of the second (hook-free) source failed"
);
let marker = agents.source.clone().join("hookran");
assert!(marker.exists(), "the hook should have run on initial meld");
std::fs::remove_file(&marker).unwrap();
agents.edit_source();
let r = agents.mind(&[
"sync",
"tools",
"--upgrade",
"--yes",
"--accept-hooks=install,update,uninstall",
]);
assert!(
r.success,
"sync tools --upgrade failed: {} {}",
r.stdout, r.stderr
);
assert!(
!marker.exists(),
"`sync tools --upgrade` must not re-run the unrelated `agents` source's \
install hook: {} exists",
marker.display()
);
let r2 = agents.mind(&[
"sync",
"agents",
"--upgrade",
"--yes",
"--accept-hooks=install,update,uninstall",
]);
assert!(
r2.success,
"sync agents --upgrade failed: {} {}",
r2.stdout, r2.stderr
);
assert!(
marker.exists(),
"`sync agents --upgrade` (naming the hooked source) must re-run its \
install hook: {} missing",
marker.display()
);
}
#[test]
fn upgrade_source_does_not_rerun_an_unrelated_sources_hook() {
let agents = sandbox_with_declared_hook("agents", "touch hookran");
let agents_spec = agents.source_spec();
assert!(
agents
.mind(&[
"meld",
&agents_spec,
"--accept-hooks=install,update,uninstall"
])
.success
);
let tools = Sandbox::named("tools");
assert!(agents.mind(&["meld", &tools.source_spec()]).success);
let marker = agents.source.clone().join("hookran");
assert!(marker.exists(), "the hook should have run on initial meld");
std::fs::remove_file(&marker).unwrap();
agents.edit_source();
let r = agents.mind(&[
"upgrade",
"--source",
"tools",
"--yes",
"--accept-hooks=install,update,uninstall",
]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
!marker.exists(),
"`upgrade --source tools` must not re-run agents' hook"
);
let r2 = agents.mind(&[
"upgrade",
"--source",
"agents",
"--yes",
"--accept-hooks=install,update,uninstall",
]);
assert!(r2.success, "{} {}", r2.stdout, r2.stderr);
assert!(
marker.exists(),
"`upgrade --source agents` must re-run its hook"
);
}
#[test]
fn glob_scoped_upgrade_does_not_rerun_unrelated_source_hook() {
let agents = sandbox_with_declared_hook("agents", "touch hookran");
let agents_spec = agents.source_spec();
assert!(
agents
.mind(&[
"meld",
&agents_spec,
"--accept-hooks=install,update,uninstall"
])
.success,
"initial meld of the hooked source should run the hook and record its commit"
);
let tools = Sandbox::named("tools");
assert!(
agents.mind(&["meld", &tools.source_spec()]).success,
"meld of the second (hook-free) source failed"
);
let learn = agents.mind(&["learn", "tools#skill:review"]);
assert!(
learn.success,
"learn failed: {} {}",
learn.stdout, learn.stderr
);
let marker = agents.source.clone().join("hookran");
assert!(
marker.exists(),
"the hook should have run on the initial meld"
);
std::fs::remove_file(&marker).unwrap();
agents.edit_source();
assert!(agents.mind(&["sync"]).success, "sync failed");
assert!(!marker.exists(), "sync alone must not re-run the hook");
let scoped = agents.mind(&[
"upgrade",
"tools#*",
"-y",
"--accept-hooks=install,update,uninstall",
]);
assert!(
scoped.success,
"glob-scoped upgrade failed: {} {}",
scoped.stdout, scoped.stderr
);
assert!(
!marker.exists(),
"a glob-scoped upgrade of an unrelated source must not re-run the hooked source's hook: {} exists",
marker.display()
);
let unscoped = agents.mind(&["upgrade", "-y", "--accept-hooks=install,update,uninstall"]);
assert!(
unscoped.success,
"unscoped upgrade failed: {} {}",
unscoped.stdout, unscoped.stderr
);
assert!(
marker.exists(),
"an unscoped upgrade must re-run the hooked source's hook: {} missing",
marker.display()
);
}
#[test]
fn upgrade_skips_disallowed_source_hook_when_locked() {
let sb = sandbox_with_declared_hook("agents", "touch hookran");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"])
.success,
"initial meld should run the hook and record its commit"
);
let marker = sb.source.clone().join("hookran");
assert!(marker.exists(), "the hook should have run on meld");
std::fs::remove_file(&marker).unwrap();
sb.edit_source();
assert!(sb.mind(&["sync"]).success, "sync failed");
assert!(!marker.exists(), "sync alone must not re-run the hook");
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow = [\"local/*/never-match\"]\n",
);
let r = sb.mind_env(
&["upgrade", "-y", "--accept-hooks=install,update,uninstall"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"upgrade must not error when skipping a disallowed source's hook: {} {}",
r.stdout, r.stderr
);
assert!(
!marker.exists(),
"a policy-disallowed source's hook must not re-run: {} exists",
marker.display()
);
assert!(
r.stdout.contains("skipping install hook for")
&& r.stdout
.contains("not permitted by the managed policy's allowlist"),
"the skipped hook must be reported: {}",
r.stdout
);
}
#[test]
fn evolve_check_with_explicit_version_reports_update_and_changes_nothing() {
let sb = Sandbox::new(); let r = sb.mind(&["evolve", "--check", "--version", "9.9.9"]);
assert!(
r.success,
"evolve --check --version 9.9.9 should succeed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("9.9.9"),
"expected target version 9.9.9 in output: {}",
r.stdout
);
assert!(
r.stdout.contains("available"),
"expected 'available' in output: {}",
r.stdout
);
assert!(
!sb.mind_home.join("sources.json").exists(),
"no sources.json should be written by evolve --check"
);
assert!(
!sb.mind_home.join("manifest.json").exists(),
"no manifest.json should be written by evolve --check"
);
}
#[test]
fn evolve_check_at_current_version_reports_up_to_date() {
let sb = Sandbox::new();
let current = env!("CARGO_PKG_VERSION");
let r = sb.mind(&["evolve", "--check", "--version", current]);
assert!(
r.success,
"evolve --check --version {current} should succeed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("up to date"),
"expected 'up to date' in output for version {current}: {}",
r.stdout
);
}
#[test]
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn evolve_check_reports_resolved_target_triple() {
let sb = Sandbox::new();
let r = sb.mind(&["evolve", "--check", "--version", "9.9.9"]);
assert!(
r.success,
"evolve --check --version 9.9.9 should succeed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("x86_64-unknown-linux-musl"),
"expected the resolved musl target triple in the --check report: {}",
r.stdout
);
assert!(
r.stdout.contains("9.9.9") && r.stdout.contains("available"),
"existing report wording must be preserved: {}",
r.stdout
);
}
#[test]
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn evolve_check_json_includes_target_triple_key() {
let sb = Sandbox::new();
let r = sb.mind(&["evolve", "--check", "--version", "9.9.9", "--json"]);
assert!(
r.success,
"evolve --check --version 9.9.9 --json should succeed: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"evolve --check --json stdout must be valid JSON ({e}): {}",
r.stdout
)
});
assert_eq!(
v.get("target_triple").and_then(|t| t.as_str()),
Some("x86_64-unknown-linux-musl"),
"target_triple must be the resolved musl artifact: {}",
r.stdout
);
assert_eq!(v.get("action").and_then(|a| a.as_str()), Some("evolve"));
assert_eq!(v.get("target").and_then(|t| t.as_str()), Some("9.9.9"));
assert_eq!(v.get("outcome").and_then(|o| o.as_str()), Some("available"));
}
#[test]
fn help_lists_upgrade_evolve_and_self_update_alias() {
let sb = Sandbox::new();
let r = sb.mind(&["--help"]);
assert!(
r.success,
"mind --help should succeed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("upgrade"),
"help must list the 'upgrade' subcommand: {}",
r.stdout
);
assert!(
r.stdout.contains("evolve"),
"help must list the 'evolve' subcommand: {}",
r.stdout
);
assert!(
r.stdout.contains("self-update"),
"help must list the 'self-update' visible alias: {}",
r.stdout
);
}
#[test]
fn evolve_disabled_by_policy_rejects_both_check_and_run() {
let sb = Sandbox::new();
let policy = write_policy(&sb, "[binary]\nself-update = false\n");
let r = sb.mind_env(
&["evolve", "--check", "--version", "9.9.9"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
!r.success,
"evolve --check must be refused when self-update = false: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("disabled by the managed policy"),
"error must name the policy: {}",
r.stderr
);
assert!(
!r.stdout.contains("warning:") && !r.stdout.contains("upper bound"),
"disabled policy must not reach/print the skew warning: {}",
r.stdout
);
let r = sb.mind_env(
&["evolve", "--yes", "--version", "9.9.9"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
!r.success,
"evolve --yes must be refused when self-update = false: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("disabled by the managed policy"),
"run error must also name the policy: {}",
r.stderr
);
assert!(
!r.stdout.contains("warning:") && !r.stdout.contains("upper bound"),
"disabled policy (run mode) must not reach/print the skew warning: {}",
r.stdout
);
}
#[test]
fn evolve_refuses_a_homebrew_managed_binary_and_check_points_at_brew() {
let sb = Sandbox::new();
let cellar_bin = sb.base.join("brew/Cellar/mind/0.0.0/bin");
std::fs::create_dir_all(&cellar_bin).unwrap();
let cellar_exe = cellar_bin.join("mind");
std::fs::copy(env!("CARGO_BIN_EXE_mind"), &cellar_exe).unwrap();
let prefix_bin = sb.base.join("brew/bin");
std::fs::create_dir_all(&prefix_bin).unwrap();
let linked = prefix_bin.join("mind");
std::os::unix::fs::symlink(&cellar_exe, &linked).unwrap();
let run = |args: &[&str]| {
let mut attempts = 0;
let out = loop {
let res = Command::new(&linked)
.args(args)
.env("MIND_HOME", &sb.mind_home)
.env("CLAUDE_HOME", &sb.claude_home)
.env("HTTPS_PROXY", "http://127.0.0.1:9")
.env("https_proxy", "http://127.0.0.1:9")
.stdin(Stdio::null())
.output();
match res {
Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy && attempts < 50 => {
attempts += 1;
std::thread::sleep(std::time::Duration::from_millis(20));
}
other => break other.expect("run linked mind"),
}
};
Run {
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
success: out.status.success(),
}
};
let r = run(&["evolve", "--yes", "--to", "9.9.9"]);
assert!(!r.success, "evolve must refuse: {} {}", r.stdout, r.stderr);
assert!(
r.stderr.contains("managed by Homebrew") && r.stderr.contains("brew upgrade mind"),
"refusal must name Homebrew and the remedy: {}",
r.stderr
);
assert!(
r.stderr.contains("Cellar"),
"refusal must name the resolved Cellar path: {}",
r.stderr
);
let r = run(&["--json", "evolve", "--yes", "--to", "9.9.9"]);
assert!(!r.success);
assert!(
r.stdout.contains("homebrew-managed") || r.stderr.contains("homebrew-managed"),
"--json refusal must carry the homebrew-managed kind: {} {}",
r.stdout,
r.stderr
);
let r = run(&["evolve", "--check", "--to", "9.9.9"]);
assert!(
r.success,
"--check must still report: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("brew upgrade mind"),
"--check must point at brew upgrade: {}",
r.stdout
);
let r = sb.mind(&["evolve", "--check", "--to", "9.9.9"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(!r.stdout.contains("Homebrew"), "{}", r.stdout);
}
#[test]
fn evolve_pinned_by_policy_check_is_offline_and_names_pin() {
let sb = Sandbox::new();
let policy = write_policy(&sb, "[binary]\nself-update = \"0.1.0\"\n");
let r = sb.mind_env(
&["evolve", "--check"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"evolve --check with a pin below current must succeed (not-downgrading path): {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("0.1.0"),
"check output must name the pinned version: {}",
r.stdout
);
}
#[test]
fn evolve_pinned_mismatched_version_arg_fails() {
let sb = Sandbox::new();
let policy = write_policy(&sb, "[binary]\nself-update = \"0.14.0\"\n");
let r = sb.mind_env(
&["evolve", "--check", "--version", "0.15.0"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
!r.success,
"mismatched --version must be refused by a pinned policy: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("0.14.0"),
"error must name the policy pin: {}",
r.stderr
);
assert!(
r.stderr.contains("conflicts") || r.stderr.contains("conflict"),
"error must say 'conflicts': {}",
r.stderr
);
}
#[test]
fn evolve_pinned_matching_version_arg_succeeds() {
let sb = Sandbox::new();
let policy = write_policy(&sb, "[binary]\nself-update = \"0.1.0\"\n");
let r = sb.mind_env(
&["evolve", "--check", "--version", "0.1.0"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"matching --version must be accepted by a pinned policy: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("0.1.0"),
"output must name the pinned version: {}",
r.stdout
);
}
#[test]
fn evolve_self_update_true_allows_evolve() {
let sb = Sandbox::new();
let policy = write_policy(&sb, "[binary]\nself-update = true\n");
let r = sb.mind_env(
&["evolve", "--check", "--version", "9.9.9"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"self-update = true must allow evolve: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("9.9.9"),
"output must name the target version: {}",
r.stdout
);
}
#[test]
fn evolve_policy_pin_below_running_emits_skew_warning_human_mode() {
let sb = Sandbox::new();
let policy = write_policy(&sb, "[binary]\nself-update = \"0.1.0\"\n");
let r = sb.mind_env(
&["evolve", "--check"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"evolve --check with a pin below current must exit 0: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("warning:"),
"check mode must print a warning when running > policy pin: {}",
r.stdout
);
assert!(
r.stdout.contains("0.1.0"),
"warning must name the policy pin: {}",
r.stdout
);
assert!(
r.stdout.contains("upper bound") || r.stdout.contains("does not downgrade"),
"warning must say the pin is an upper bound or does not downgrade: {}",
r.stdout
);
let r2 = sb.mind_env(&["evolve"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r2.success,
"evolve (no --check) with a pin below current must exit 0: {} {}",
r2.stdout, r2.stderr
);
assert!(
r2.stdout.contains("warning:"),
"non-check evolve must also print the skew warning: {}",
r2.stdout
);
}
#[test]
fn evolve_policy_pin_skew_json_no_warning_on_stdout_outcome_is_not_downgrading() {
let sb = Sandbox::new();
let policy = write_policy(&sb, "[binary]\nself-update = \"0.1.0\"\n");
let r = sb.mind_env(
&["evolve", "--check", "--json"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"evolve --check --json with a pin below current must exit 0: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"evolve --json stdout must be valid JSON ({e}): {}",
r.stdout
)
});
assert_eq!(
v.get("action").and_then(|a| a.as_str()),
Some("evolve"),
"structured action must be 'evolve': {}",
r.stdout
);
assert_eq!(
v.get("outcome").and_then(|o| o.as_str()),
Some("not-downgrading"),
"JSON outcome must be 'not-downgrading': {}",
r.stdout
);
assert!(
!r.stdout.contains("warning:"),
"JSON mode must not print the human warning on stdout: {}",
r.stdout
);
assert!(
!r.stdout.contains("upper bound"),
"JSON mode must not leak the skew-warning wording onto stdout: {}",
r.stdout
);
}
#[test]
fn evolve_policy_pin_skew_non_check_json_no_warning_valid_json() {
let sb = Sandbox::new();
let policy = write_policy(&sb, "[binary]\nself-update = \"0.1.0\"\n");
let r = sb.mind_env(
&["evolve", "--json"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"evolve --json (non-check) with a pin below current must exit 0: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"non-check evolve --json stdout must be valid JSON ({e}): {}",
r.stdout
)
});
assert_eq!(
v.get("outcome").and_then(|o| o.as_str()),
Some("not-downgrading"),
"non-check JSON outcome must be 'not-downgrading': {}",
r.stdout
);
assert!(
!r.stdout.contains("warning:") && !r.stdout.contains("upper bound"),
"non-check JSON mode must not print the skew warning on stdout: {}",
r.stdout
);
}
#[test]
fn evolve_policy_pin_equal_to_running_no_skew_warning() {
let sb = Sandbox::new();
let running = env!("CARGO_PKG_VERSION");
let base = running.split('-').next().unwrap();
let policy = write_policy(&sb, &format!("[binary]\nself-update = \"{base}\"\n"));
let r = sb.mind_env(
&["evolve", "--check"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
r.success,
"evolve --check with pin at the running base must exit 0: {} {}",
r.stdout, r.stderr
);
assert!(
!r.stdout.contains("warning:"),
"no skew warning when the pin is not below the running version: {}",
r.stdout
);
if running.contains('-') {
assert!(
r.stdout.contains("available"),
"a dev build must be offered its base release: {}",
r.stdout
);
} else {
assert!(
r.stdout.contains("up to date"),
"output must say 'up to date' when pin == running: {}",
r.stdout
);
}
}
#[test]
fn evolve_explicit_version_below_running_no_policy_no_skew_warning() {
let sb = Sandbox::new();
let r = sb.mind(&["evolve", "--check", "--version", "0.1.0"]);
assert!(
r.success,
"evolve --check --version 0.1.0 (below current, no policy) must exit 0: {} {}",
r.stdout, r.stderr
);
assert!(
!r.stdout.contains("warning:"),
"no skew warning when pin is from --version with no policy: {}",
r.stdout
);
assert!(
r.stdout.contains("not downgrading"),
"output must say 'not downgrading': {}",
r.stdout
);
}
#[test]
fn remeld_reoffers_pending_install_hooks_and_force_reruns() {
let sb = Sandbox::bare("remeld-hook");
let marker = sb.base.join("hook-ran");
let m = marker.to_str().unwrap().to_owned();
sb.write_and_commit(
"mind.toml",
&format!("[[hooks]]\nrun = \"touch {m}\"\nevent = \"install\"\n"),
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
assert!(!marker.exists(), "hook skipped on the non-TTY meld");
assert!(
sb.mind(&[
"meld",
&spec,
"--link-only",
"--accept-hooks=install,update,uninstall"
])
.success
);
assert!(marker.exists(), "re-meld must run the pending hook");
std::fs::remove_file(&marker).unwrap();
assert!(
sb.mind(&[
"meld",
&spec,
"--link-only",
"--accept-hooks=install,update,uninstall"
])
.success
);
assert!(
!marker.exists(),
"a hook already run at this commit is not re-offered"
);
assert!(
sb.mind(&[
"meld",
&spec,
"--link-only",
"--force",
"--accept-hooks=install,update,uninstall"
])
.success
);
assert!(marker.exists(), "--force must re-run an already-run hook");
}
#[test]
fn recall_status_view_marks_install_state() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let out = sb.mind(&["recall"]).stdout;
assert!(out.contains("agents"), "source header: {out}");
assert!(
out.contains("skill:review") && out.contains("installed @"),
"an installed item is marked installed with its commit: {out}"
);
assert!(
out.contains("agent:dev") && out.contains("available"),
"a not-installed item is marked available: {out}"
);
}
#[test]
fn install_hook_stdout_streams_to_mind_stdout() {
let sb = Sandbox::bare("hook-output");
sb.write_and_commit(
"mind.toml",
"[[hooks]]\nrun = \"echo HELLO-FROM-HOOK\"\nname = \"build\"\nevent = \"install\"\n",
);
let spec = sb.source_spec();
let r = sb.mind(&[
"meld",
&spec,
"--link-only",
"--accept-hooks=install,update,uninstall",
]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("====== (hook: build) ======"),
"the opening frame must appear in mind's output: {}",
r.stdout
);
assert!(
r.stdout.contains("HELLO-FROM-HOOK"),
"the hook's stdout must reach mind's stdout: {}",
r.stdout
);
assert!(
!r.stdout.contains("hook-stdout:") && !r.stdout.contains("hook-stderr:"),
"the per-stream capture frames are gone: {}",
r.stdout
);
assert!(
r.stdout.contains("====== (end hook: build, exit 0, "),
"the closing divider must separate the hook output from what follows: {}",
r.stdout
);
}
#[test]
fn install_hook_stderr_goes_to_mind_stderr_not_stdout() {
let sb = Sandbox::bare("hook-stderr");
sb.write_and_commit(
"mind.toml",
"[[hooks]]\nrun = \"echo OOPS 1>&2\"\nname = \"warn\"\nevent = \"install\"\n",
);
let spec = sb.source_spec();
let r = sb.mind(&[
"meld",
&spec,
"--link-only",
"--accept-hooks=install,update,uninstall",
]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("====== (hook: warn) ======"),
"the frame still opens on stdout, where mind's own output is: {}",
r.stdout
);
assert!(
r.stderr.contains("OOPS"),
"the hook's stderr must reach mind's stderr: {}",
r.stderr
);
assert!(
!r.stdout.contains("OOPS"),
"and must NOT be copied onto stdout as the captured form did: {}",
r.stdout
);
assert!(
r.stdout.contains("====== (end hook: warn, exit 0, "),
"the closing divider must separate the hook output from what follows: {}",
r.stdout
);
}
#[test]
fn meld_runs_multiple_install_hooks_with_dangerous_flag() {
let sb = Sandbox::bare("multi-hook");
let marker1 = sb.base.join("marker1");
let marker2 = sb.base.join("marker2");
let m1 = marker1.to_str().unwrap().to_owned();
let m2 = marker2.to_str().unwrap().to_owned();
let toml = format!(
"[[hooks]]\nrun = \"touch {m1}\"\nevent = \"install\"\n\n\
[[hooks]]\nrun = \"touch {m2}\"\nevent = \"install\"\n"
);
sb.write_and_commit("mind.toml", &toml);
let spec = sb.source_spec();
let r = sb.mind(&[
"meld",
&spec,
"--accept-hooks=install,update,uninstall",
"--link-only",
]);
assert!(
r.success,
"meld with two install hooks should succeed: {} {}",
r.stdout, r.stderr
);
assert!(
marker1.exists(),
"first install hook must have run (marker1 missing): {}",
marker1.display()
);
assert!(
marker2.exists(),
"second install hook must have run (marker2 missing): {}",
marker2.display()
);
let sources = sb.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("multi-hook"),
"source must be registered after both hooks ran: {sources}"
);
}
#[test]
fn meld_non_tty_skips_install_hooks_and_still_registers_source() {
let sb = Sandbox::bare("multi-hook-skip");
let marker1 = sb.base.join("skip-marker1");
let marker2 = sb.base.join("skip-marker2");
let m1 = marker1.to_str().unwrap().to_owned();
let m2 = marker2.to_str().unwrap().to_owned();
let toml = format!(
"[[hooks]]\nrun = \"touch {m1}\"\nevent = \"install\"\n\n\
[[hooks]]\nrun = \"touch {m2}\"\nevent = \"install\"\n"
);
sb.write_and_commit("mind.toml", &toml);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(
r.success,
"meld should still succeed on non-TTY skip: {} {}",
r.stdout, r.stderr
);
assert!(
!marker1.exists(),
"hook must not have run in non-TTY mode (marker1 exists)"
);
assert!(
!marker2.exists(),
"hook must not have run in non-TTY mode (marker2 exists)"
);
let combined = format!("{}{}", r.stdout, r.stderr);
assert!(
combined.contains("note: skipped install hook "),
"non-TTY skip must print a note starting with 'note: skipped install hook ': {combined}"
);
let sources = sb.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("multi-hook-skip"),
"source must be registered even when hooks are skipped: {sources}"
);
}
#[test]
fn optional_install_hook_failure_aborts_meld() {
let sb = Sandbox::bare("optional-hook-fail");
let toml = "[[hooks]]\nrun = \"exit 1\"\nevent = \"install\"\noptional = true\n";
sb.write_and_commit("mind.toml", toml);
let spec = sb.source_spec();
let r = sb.mind(&[
"meld",
&spec,
"--accept-hooks=install,update,uninstall",
"--link-only",
]);
assert!(
!r.success,
"an optional hook failure must abort the meld: {} {}",
r.stdout, r.stderr
);
assert!(
!sb.mind(&["recall", "--sources"])
.stdout
.contains("optional-hook-fail"),
"the source must not be registered after a failed optional hook"
);
}
#[test]
fn required_install_hook_failure_aborts_meld() {
let sb = Sandbox::bare("required-fail");
let toml = "[[hooks]]\nrun = \"exit 1\"\nevent = \"install\"\n";
sb.write_and_commit("mind.toml", toml);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"]);
assert!(
!r.success,
"meld must fail when a required install hook exits non-zero: {} {}",
r.stdout, r.stderr
);
let sources = sb.mind(&["recall", "--sources"]).stdout;
assert!(
!sources.contains("required-fail"),
"source must not be registered after a required hook failure: {sources}"
);
}
#[test]
fn unmeld_runs_uninstall_hook_with_dangerous_flag() {
let sb = Sandbox::bare("uninstall-hook");
let uninstall_marker = sb.base.join("uninstall-ran");
let m = uninstall_marker.to_str().unwrap().to_owned();
let toml = format!("[[hooks]]\nrun = \"touch {m}\"\nevent = \"uninstall\"\n");
sb.write_and_commit("mind.toml", &toml);
let spec = sb.source_spec();
let meld = sb.mind(&["meld", &spec, "--link-only"]);
assert!(
meld.success,
"meld should succeed: {} {}",
meld.stdout, meld.stderr
);
assert!(
!uninstall_marker.exists(),
"uninstall hook must not run at meld time"
);
let unmeld = sb.mind(&[
"unmeld",
"uninstall-hook",
"--accept-hooks=install,update,uninstall",
]);
assert!(
unmeld.success,
"unmeld should succeed: {} {}",
unmeld.stdout, unmeld.stderr
);
assert!(
uninstall_marker.exists(),
"uninstall hook must have run at unmeld: marker missing at {}",
uninstall_marker.display()
);
let sources = sb.mind(&["recall", "--sources"]).stdout;
assert!(
!sources.contains("uninstall-hook"),
"source must be removed after unmeld: {sources}"
);
}
#[test]
fn unmeld_uninstall_hook_override_replaces_declared() {
let sb = Sandbox::bare("uninstall-override");
let declared_marker = sb.base.join("declared-ran");
let override_marker = sb.base.join("override-ran");
let dm = declared_marker.to_str().unwrap().to_owned();
let om = override_marker.to_str().unwrap().to_owned();
let toml = format!("[[hooks]]\nrun = \"touch {dm}\"\nevent = \"uninstall\"\n");
sb.write_and_commit("mind.toml", &toml);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success, "meld");
let unmeld = sb.mind(&[
"unmeld",
"uninstall-override",
"--uninstall-hook",
&format!("touch {om}"),
"--accept-hooks=install,update,uninstall",
]);
assert!(
unmeld.success,
"unmeld --uninstall-hook should succeed: {} {}",
unmeld.stdout, unmeld.stderr
);
assert!(
override_marker.exists(),
"the override uninstall hook must run: {}",
override_marker.display()
);
assert!(
!declared_marker.exists(),
"the declared uninstall hook must not run when overridden"
);
assert!(
!sb.mind(&["recall", "--sources"])
.stdout
.contains("uninstall-override"),
"source must be removed"
);
}
#[test]
fn unmeld_non_tty_skips_uninstall_hook_but_still_removes_source() {
let sb = Sandbox::bare("uninstall-skip");
let uninstall_marker = sb.base.join("uninstall-skip-ran");
let m = uninstall_marker.to_str().unwrap().to_owned();
let toml = format!("[[hooks]]\nrun = \"touch {m}\"\nevent = \"uninstall\"\n");
sb.write_and_commit("mind.toml", &toml);
let spec = sb.source_spec();
let meld = sb.mind(&["meld", &spec, "--link-only"]);
assert!(
meld.success,
"meld should succeed: {} {}",
meld.stdout, meld.stderr
);
let unmeld = sb.mind(&["unmeld", "uninstall-skip"]);
assert!(
unmeld.success,
"unmeld should succeed even when hook is skipped: {} {}",
unmeld.stdout, unmeld.stderr
);
assert!(
!uninstall_marker.exists(),
"uninstall hook must not run in non-TTY mode without the dangerous flag"
);
let sources = sb.mind(&["recall", "--sources"]).stdout;
assert!(
!sources.contains("uninstall-skip"),
"source must be removed even when uninstall hook is skipped: {sources}"
);
}
#[test]
fn init_source_scaffold_offers_hook_examples() {
let sb = Sandbox::new();
let repo = sb.base.join("new-source");
write(
&repo.join("skills/greet/SKILL.md"),
"---\nname: greet\ndescription: A greeting skill\n---\n# greet\n",
);
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir]);
assert!(
r.success,
"init-source should succeed: {} {}",
r.stdout, r.stderr
);
let scaffold =
std::fs::read_to_string(repo.join("mind.toml")).expect("init-source must create mind.toml");
assert!(
scaffold.contains("[[hooks]]"),
"scaffold must contain [[hooks]] examples: {scaffold}"
);
let has_install_comment = scaffold
.lines()
.any(|l| l.trim_start().starts_with('#') && l.contains("event") && l.contains("install"));
assert!(
has_install_comment,
"scaffold must have a commented event = \"install\" line: {scaffold}"
);
let has_uninstall_comment = scaffold
.lines()
.any(|l| l.trim_start().starts_with('#') && l.contains("event") && l.contains("uninstall"));
assert!(
has_uninstall_comment,
"scaffold must have a commented event = \"uninstall\" line: {scaffold}"
);
let has_optional_comment = scaffold
.lines()
.any(|l| l.trim_start().starts_with('#') && l.contains("optional") && l.contains("true"));
assert!(
has_optional_comment,
"scaffold must have a commented optional = true line: {scaffold}"
);
}
#[test]
fn recall_sources_marks_multi_hook_source() {
let sb = Sandbox::bare("hook-report");
let marker1 = sb.base.join("report-marker1");
let marker2 = sb.base.join("report-marker2");
let m1 = marker1.to_str().unwrap().to_owned();
let m2 = marker2.to_str().unwrap().to_owned();
let toml = format!(
"[[hooks]]\nrun = \"touch {m1}\"\nevent = \"install\"\n\n\
[[hooks]]\nrun = \"touch {m2}\"\nevent = \"install\"\n"
);
sb.write_and_commit("mind.toml", &toml);
let spec = sb.source_spec();
assert!(
sb.mind(&[
"meld",
&spec,
"--accept-hooks=install,update,uninstall",
"--link-only"
])
.success,
"meld should succeed"
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.success,
"recall --sources failed: {}",
sources.stderr
);
assert!(
sources.stdout.contains(" hooks(2)"),
"recall --sources must mark a two-hook source with ' hooks(2)': {}",
sources.stdout
);
}
#[test]
fn pinned_local_meld_hook_failure_leaves_no_orphan_clone() {
let sb = sandbox_with_declared_hook("agents", "exit 1");
let spec = sb.source_spec();
let sha = {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&sb.source)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
};
let r = sb.mind(&[
"meld",
&spec,
"--pin-ref",
&sha,
"--accept-hooks=install,update,uninstall",
]);
assert!(
!r.success,
"hook failure must fail the meld: {} {}",
r.stdout, r.stderr
);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"source must not be registered after a failed hook"
);
let sources_tree = sb.mind_home.join("sources");
if sources_tree.exists() {
let clone = sources_tree
.join("local")
.join(sb.base_name())
.join("agents");
assert!(
!clone.exists(),
"pinned-local clone must be removed on hook failure, found orphan at {}",
clone.display()
);
}
assert!(
sb.source.exists(),
"working tree must survive a failed pinned-local meld: {}",
sb.source.display()
);
}
#[test]
fn upgrade_pending_filter_treats_none_ran_at_as_always_pending() {
let sb = Sandbox::bare("upgrade-pending");
let marker = sb.base.join("upgrade-pending-ran");
let m = marker.to_str().unwrap().to_owned();
let toml = format!("[[hooks]]\nrun = \"touch {m}\"\nevent = \"install\"\n");
sb.write_and_commit("mind.toml", &toml);
let spec = sb.source_spec();
let meld = sb.mind(&["meld", &spec, "--link-only"]);
assert!(
meld.success,
"meld should succeed: {} {}",
meld.stdout, meld.stderr
);
assert!(
!marker.exists(),
"hook must not run at meld time (non-TTY skip)"
);
let json = std::fs::read_to_string(sb.mind_home.join("sources.json")).unwrap();
assert!(
json.contains("\"ran_at\": null"),
"registry must record ran_at=null for the skipped hook: {json}"
);
let upgrade = sb.mind(&["upgrade", "--accept-hooks=install,update,uninstall"]);
assert!(
upgrade.success,
"upgrade should succeed: {} {}",
upgrade.stdout, upgrade.stderr
);
assert!(
marker.exists(),
"upgrade must re-run a hook with ran_at=null (none-pending filter): marker absent"
);
}
#[test]
fn unmeld_confirm_decline_leaves_source_melded_and_hook_not_run() {
let sb = Sandbox::bare("unmeld-confirm-order");
let sentinel = sb.base.join("uninstall-ran");
let s = sentinel.to_str().unwrap().to_owned();
let hook_toml = format!("[[hooks]]\nrun = \"touch {s}\"\nevent = \"uninstall\"\n");
sb.write_and_commit("mind.toml", &hook_toml);
sb.write_and_commit(
"agents/dev.md",
"---\nname: dev\ndescription: dev\n---\n# dev\n",
);
sb.write_and_commit(
"agents/ops.md",
"---\nname: ops\ndescription: ops\n---\n# ops\n",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success, "meld");
assert!(sb.mind(&["learn", "agent:dev"]).success, "learn dev");
assert!(sb.mind(&["learn", "agent:ops"]).success, "learn ops");
let r = sb.mind(&["unmeld", "unmeld-confirm-order"]);
assert!(
!r.success,
"unmeld without --yes must fail in non-TTY: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("needs confirmation"),
"must report ConfirmationRequired: {}",
r.stderr
);
assert!(
!sentinel.exists(),
"uninstall hook must not run before the multi-item confirmation gate: sentinel exists"
);
let sources = sb.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("unmeld-confirm-order"),
"source must remain melded after a declined confirm: {sources}"
);
}
#[test]
fn unmeld_failing_uninstall_hook_leaves_source_melded() {
let sb = Sandbox::bare("failing-uninstall-hook");
let toml = "[[hooks]]\nrun = \"exit 1\"\nevent = \"uninstall\"\n";
sb.write_and_commit("mind.toml", toml);
sb.write_and_commit(
"agents/dev.md",
"---\nname: dev\ndescription: dev\n---\n# dev\n",
);
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--link-only"]).success,
"meld should succeed"
);
assert!(sb.mind(&["learn", "agent:dev"]).success, "learn dev");
let r = sb.mind(&[
"unmeld",
"failing-uninstall-hook",
"--accept-hooks=install,update,uninstall",
]);
assert!(
!r.success,
"unmeld must fail when uninstall hook exits non-zero: {} {}",
r.stdout, r.stderr
);
let sources = sb.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("failing-uninstall-hook"),
"source must remain melded after a failed uninstall hook: {sources}"
);
assert!(
!sb.mind(&["recall", "agent:dev"]).success,
"the item is torn down before the source uninstall hook fires (HOOK-87)"
);
}
fn tool_source() -> Sandbox {
let sb = Sandbox::bare("agents");
write(
&sb.source.join("tools/shard/shard"),
"#!/bin/sh\necho shard\n",
);
write(
&sb.source.join("tools/detect/detect"),
"#!/bin/sh\necho detect\n",
);
write(
&sb.source.join("tools/detect/lib.sh"),
"exec {{tools:shard}} \"$@\"\n",
);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: review\n---\nrun {{self}}/run.sh\ndetect {{tools:detect}} .\nlib {{path:tool:detect}}/lib.sh\n",
);
write(
&sb.source.join("skills/review/run.sh"),
"#!/bin/sh\n{{tools:detect}} run\n",
);
git(&sb.source, &["add", "-A"]);
git(&sb.source, &["commit", "-qm", "tools"]);
sb
}
#[test]
fn tool_installs_store_only_and_tokens_expand_in_markdown_only() {
let sb = tool_source();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let store = sb.mind_home.join("store");
assert!(store.join("tool/detect/detect").is_file());
assert!(store.join("tool/shard/shard").is_file());
assert!(
!sb.claude_home.join("tools").exists(),
"a tool must not be linked into an agent home"
);
assert!(!sb.claude_home.join("skills/detect").exists());
let link = sb.claude_home.join("skills/review");
assert!(
std::fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink(),
"the skill links as usual"
);
let s = store.display().to_string();
let skill_md = std::fs::read_to_string(store.join("skill/review/SKILL.md")).unwrap();
assert!(
skill_md.contains(&format!("run {s}/skill/review/run.sh")),
"{skill_md}"
);
assert!(
skill_md.contains(&format!("detect {s}/tool/detect/detect .")),
"{skill_md}"
);
assert!(
skill_md.contains(&format!("lib {s}/tool/detect/lib.sh")),
"{skill_md}"
);
let run_sh = std::fs::read_to_string(store.join("skill/review/run.sh")).unwrap();
assert!(
run_sh.contains("{{tools:detect}} run"),
"a non-markdown file's token must not expand: {run_sh}"
);
let lib_sh = std::fs::read_to_string(store.join("tool/detect/lib.sh")).unwrap();
assert!(
lib_sh.contains("exec {{tools:shard}} \"$@\""),
"a non-markdown file's token must not expand: {lib_sh}"
);
}
#[test]
fn tool_prefix_applies_to_store_and_tokens() {
let sb = tool_source();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--as", "jk", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let store = sb.mind_home.join("store");
assert!(store.join("tool/jk:detect/detect").is_file());
let skill_md = std::fs::read_to_string(store.join("skill/jk:review/SKILL.md")).unwrap();
assert!(
skill_md.contains(&format!("{}/tool/jk:detect/detect", store.display())),
"{skill_md}"
);
}
#[test]
fn tool_with_explicit_link_is_surfaced() {
let sb = Sandbox::bare("agents");
write(&sb.source.join("tools/detect/detect"), "#!/bin/sh\n");
write(
&sb.source.join("mind.toml"),
"[[items]]\nkind = \"tool\"\nname = \"detect\"\npath = \"tools/detect\"\nlink = \"agents/detect\"\n",
);
git(&sb.source, &["add", "-A"]);
git(&sb.source, &["commit", "-qm", "linked-tool"]);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
let link = sb.claude_home.join("agents/detect");
assert!(
std::fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink(),
"an explicit link surfaces the tool in the agent home"
);
}
#[test]
fn review_flags_tooling_references() {
let sb = Sandbox::bare("agents");
write(&sb.source.join("tools/detect/detect"), "#!/bin/sh\n");
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: review\n---\n\
run {{tools:nope}} .\n\
also ~/.claude/skills/review/resources/pr.py\n\
mention the detect tool\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(
!r.success,
"an unresolved path token is a hard error: {}",
r.stdout
);
assert!(
r.stderr.contains("bad-reference"),
"expected a bad-reference hard finding: {}",
r.stderr
);
assert!(
r.stdout.contains("hardcoded-path") && r.stdout.contains("{{self}}/resources/pr.py"),
"expected a hardcoded-path advisory suggesting the token: {}",
r.stdout
);
assert!(
r.stdout.contains("bare-tool-reference"),
"expected a bare-tool-reference advisory: {}",
r.stdout
);
}
#[test]
fn review_hardcoded_path_classifies_and_detects_env_forms() {
let sb = Sandbox::bare("agents");
write(&sb.source.join("tools/detect/detect"), "#!/bin/sh\n");
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: review\n---\n\
own ~/.claude/skills/review/resources/pr.py\n\
tool $HOME/.mind/store/tool/detect/detect run\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(
r.success,
"advisory-only review exits zero: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("hardcodes its own resource path")
&& r.stdout.contains("this works but assumes")
&& r.stdout.contains("{{self}}/resources/pr.py"),
"own-resource classification: {}",
r.stdout
);
assert!(
r.stdout.contains("hardcodes a shared tool path")
&& r.stdout.contains("will not resolve")
&& r.stdout.contains("{{tools:detect}}"),
"shared-tool classification via $HOME form: {}",
r.stdout
);
}
#[test]
fn review_flags_helper_script_duplicated_across_items() {
let sb = Sandbox::bare("agents");
write(
&sb.source.join("skills/a/SKILL.md"),
"---\nname: a\ndescription: a\n---\n# a\n",
);
write(
&sb.source.join("skills/a/helper.sh"),
"#!/bin/sh\necho shared\n",
);
write(
&sb.source.join("skills/a/only.sh"),
"#!/bin/sh\necho unique\n",
);
write(
&sb.source.join("skills/b/SKILL.md"),
"---\nname: b\ndescription: b\n---\n# b\n",
);
write(
&sb.source.join("skills/b/helper.sh"),
"#!/bin/sh\necho shared\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(
r.success,
"an advisory-only review exits zero: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("duplicate-tooling") && r.stdout.contains("helper.sh"),
"expected a duplicate-tooling advisory naming the file: {}",
r.stdout
);
assert!(
r.stdout.contains("skill:a") && r.stdout.contains("skill:b"),
"the finding names both carriers: {}",
r.stdout
);
assert!(
r.stdout.contains("both are valid"),
"duplicate-tooling must frame the copy as an optional, valid choice: {}",
r.stdout
);
assert!(
!r.stdout.contains("only.sh"),
"a non-duplicated script must not be flagged: {}",
r.stdout
);
}
#[test]
fn review_does_not_flag_duplicated_markdown() {
let sb = Sandbox::bare("agents");
write(
&sb.source.join("skills/a/SKILL.md"),
"---\nname: a\ndescription: a\n---\n# shared heading\n",
);
write(&sb.source.join("skills/a/NOTES.md"), "same notes\n");
write(
&sb.source.join("skills/b/SKILL.md"),
"---\nname: b\ndescription: b\n---\n# shared heading\n",
);
write(&sb.source.join("skills/b/NOTES.md"), "same notes\n");
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
!r.stdout.contains("duplicate-tooling"),
"duplicated markdown must not be flagged: {}",
r.stdout
);
}
#[test]
fn review_fix_rewrites_local_copy() {
let sb = Sandbox::bare("agents");
let skill = sb.source.join("skills/review/SKILL.md");
write(
&skill,
"---\nname: review\ndescription: review\n---\n\
run ~/.claude/skills/review/run.sh; hand off to dev\n",
);
write(
&sb.source.join("agents/dev.md"),
"---\nname: dev\ndescription: dev\n---\n# dev\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target, "--fix"]);
assert!(
r.success,
"advisory-only fix must exit zero: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("fixed"),
"must report the fixed file: {}",
r.stdout
);
let rewritten = std::fs::read_to_string(&skill).unwrap();
assert!(
rewritten.contains("{{self}}/run.sh"),
"hardcoded path rewritten to a token: {rewritten}"
);
assert!(
rewritten.contains("{{ns:dev}}"),
"bare sibling name templatized: {rewritten}"
);
}
#[test]
fn review_fix_refuses_a_registry_target() {
let sb = melded();
let r = sb.mind(&["review", "agents", "--fix"]);
assert!(
!r.success,
"--fix against a melded selector must refuse: {}",
r.stdout
);
assert!(
r.stderr.contains("fix-not-local"),
"expected a fix-not-local refusal: {}",
r.stderr
);
}
#[test]
fn two_sources_same_names_coexist_under_a_prefix() {
let a = Sandbox::new();
let b = Sandbox::new();
assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &b.source_spec(), "--as", "zz"]).success);
let la = a.mind(&["learn", "review"]);
assert!(la.success, "learn review: {} {}", la.stdout, la.stderr);
let lb = a.mind(&["learn", "zz:review"]);
assert!(lb.success, "learn zz:review: {} {}", lb.stdout, lb.stderr);
let recall = a.mind(&["recall"]).stdout;
assert!(recall.contains("skill:review"), "{recall}");
assert!(recall.contains("skill:zz:review"), "{recall}");
assert!(
a.mind_home.join("store/skill/review").is_dir(),
"a's store copy"
);
assert!(
a.mind_home.join("store/skill/zz:review").is_dir(),
"b's store copy"
);
for link in ["skills/review", "skills/zz:review"] {
assert!(
std::fs::symlink_metadata(a.claude_home.join(link))
.unwrap()
.file_type()
.is_symlink(),
"expected a symlink at {link}"
);
}
}
#[test]
fn unprefixed_same_name_second_install_is_a_noop_first_wins() {
let a = Sandbox::new();
let b = Sandbox::new();
b.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: BRAVO review\n---\n# review b\n",
);
assert!(a.mind(&["meld", &a.source_spec()]).success);
assert!(a.mind(&["meld", &b.source_spec()]).success);
let a_full = format!("{}/agents", a.base_name());
let b_full = format!("{}/agents", b.base_name());
assert!(a.mind(&["learn", &format!("{a_full}#review")]).success);
let second = a.mind(&["learn", &format!("{b_full}#review")]);
assert!(second.success, "second install: {}", second.stderr);
let installed =
std::fs::read_to_string(a.mind_home.join("store/skill/review/SKILL.md")).unwrap();
assert!(
installed.contains("Review the diff for bugs") && !installed.contains("BRAVO review"),
"the first install must remain (no overwrite): {installed}"
);
}
fn parse_json(stdout: &str) -> serde_json::Value {
serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("stdout is not valid JSON ({e}): {stdout:?}"))
}
fn has_ansi_escape(s: &str) -> bool {
s.contains('\u{1b}')
}
#[test]
fn json_learn_emits_result_object_and_no_prose() {
let sb = melded();
let pre = sb.mind(&["--json", "learn", "skill:review"]);
assert!(pre.success, "learn --json failed: {}", pre.stderr);
let v = parse_json(&pre.stdout);
assert_eq!(v["action"], "learn", "{}", pre.stdout);
assert_eq!(v["target"], "skill:review", "{}", pre.stdout);
assert_eq!(v["outcome"], "installed", "{}", pre.stdout);
assert_eq!(
v["installed"],
serde_json::json!(["skill:review"]),
"{}",
pre.stdout
);
assert!(
!pre.stdout.contains("learned"),
"human prose `learned` must not appear under --json: {}",
pre.stdout
);
assert!(!has_ansi_escape(&pre.stdout), "json stdout: {}", pre.stdout);
let sb2 = melded();
let post = sb2.mind(&["learn", "skill:review", "--json"]);
assert!(
post.success,
"learn --json (suffix) failed: {}",
post.stderr
);
assert_eq!(
parse_json(&post.stdout),
v,
"flag position must not change the JSON: pre={} post={}",
pre.stdout,
post.stdout
);
}
#[test]
fn json_forget_emits_removed_object_and_no_prose() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
let r = sb.mind(&["forget", "skill:review", "--json"]);
assert!(r.success, "forget --json failed: {}", r.stderr);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "forget", "{}", r.stdout);
assert_eq!(v["target"], "skill:review", "{}", r.stdout);
assert_eq!(v["outcome"], "removed", "{}", r.stdout);
assert_eq!(
v["removed"],
serde_json::json!(["skill:review"]),
"{}",
r.stdout
);
assert!(
!r.stdout.contains("forgot"),
"human prose `forgot` must not appear under --json: {}",
r.stdout
);
assert!(!has_ansi_escape(&r.stdout), "json stdout: {}", r.stdout);
}
#[test]
fn json_meld_emits_result_object_and_no_prose() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["--json", "meld", &spec]);
assert!(r.success, "meld --json failed: {} {}", r.stdout, r.stderr);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "meld", "{}", r.stdout);
assert_eq!(v["outcome"], "melded", "{}", r.stdout);
assert!(
v["target"].is_string() && !v["target"].as_str().unwrap().is_empty(),
"meld target must name the source: {}",
r.stdout
);
assert!(
!r.stdout.contains("learn") && !r.stdout.contains("melded source"),
"default-meld prose must not appear under --json: {}",
r.stdout
);
assert!(!has_ansi_escape(&r.stdout), "json stdout: {}", r.stdout);
}
#[test]
fn json_remeld_already_melded_is_a_single_object() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--yes"]).success, "meld+install");
let r = sb.mind(&["meld", &spec, "--json"]);
assert!(r.success, "re-meld --json failed: {}", r.stderr);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "meld", "{}", r.stdout);
assert_eq!(v["outcome"], "already-melded", "{}", r.stdout);
assert!(
!r.stdout.contains("already melded") && !r.stdout.contains("to install"),
"re-meld prose must not appear under --json: {}",
r.stdout
);
}
#[test]
fn json_sync_emits_result_object_and_no_prose() {
let sb = melded();
let r = sb.mind(&["sync", "--json"]);
assert!(r.success, "sync --json failed: {}", r.stderr);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "sync", "{}", r.stdout);
assert_eq!(v["outcome"], "synced", "{}", r.stdout);
assert!(v["count"].is_number(), "sync count: {}", r.stdout);
assert!(
!r.stdout.contains("syncing") && !r.stdout.contains("up to date"),
"sync prose must not appear under --json: {}",
r.stdout
);
assert!(!has_ansi_escape(&r.stdout), "json stdout: {}", r.stdout);
}
#[test]
fn json_sync_no_op_on_empty_registry() {
let sb = Sandbox::new();
let r = sb.mind(&["sync", "--json"]);
assert!(r.success, "sync --json on empty registry: {}", r.stderr);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "sync", "{}", r.stdout);
assert_eq!(v["outcome"], "no-op", "{}", r.stdout);
assert!(
!r.stdout.contains("no sources"),
"no-op prose must not appear under --json: {}",
r.stdout
);
}
#[test]
fn json_upgrade_up_to_date_is_an_object() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
let r = sb.mind(&["upgrade", "--json"]);
assert!(r.success, "upgrade --json failed: {}", r.stderr);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "upgrade", "{}", r.stdout);
assert_eq!(v["outcome"], "up-to-date", "{}", r.stdout);
assert!(
!r.stdout.contains("up to date"),
"upgrade prose must not appear under --json: {}",
r.stdout
);
}
#[test]
fn json_upgrade_applies_and_reports_upgraded() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
sb.edit_source();
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["upgrade", "--yes", "--json"]);
assert!(r.success, "upgrade --yes --json failed: {}", r.stderr);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "upgrade", "{}", r.stdout);
assert_eq!(v["outcome"], "upgraded", "{}", r.stdout);
assert_eq!(
v["installed"],
serde_json::json!(["skill:review"]),
"{}",
r.stdout
);
assert!(
!r.stdout.contains("upgraded skill"),
"upgrade prose must not appear under --json: {}",
r.stdout
);
}
#[test]
fn json_unmeld_emits_result_object() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--yes"]).success, "meld+install");
let name = "agents";
let r = sb.mind(&["unmeld", name, "--yes", "--json"]);
assert!(r.success, "unmeld --json failed: {} {}", r.stdout, r.stderr);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "unmeld", "{}", r.stdout);
assert!(
v["target"]
.as_str()
.is_some_and(|t| t.ends_with(&format!("/{name}")) || t == name),
"unmeld target must name the source: {}",
r.stdout
);
assert_eq!(v["outcome"], "removed", "{}", r.stdout);
assert!(
!r.stdout.contains("unmelded"),
"unmeld prose must not appear under --json: {}",
r.stdout
);
}
#[test]
fn json_lobe_add_and_remove_emit_objects() {
let sb = Sandbox::new();
let extra = sb.base.join("extra-lobe");
let extra_s = extra.to_string_lossy().into_owned();
let added = sb.mind(&["config", "lobes", "add", &extra_s, "--json"]);
assert!(added.success, "lobe add --json failed: {}", added.stderr);
let v = parse_json(&added.stdout);
assert_eq!(v["action"], "lobe-add", "{}", added.stdout);
assert_eq!(v["outcome"], "added", "{}", added.stdout);
let again = sb.mind(&["config", "lobes", "add", &extra_s, "--json"]);
assert!(again.success, "{}", again.stderr);
assert_eq!(
parse_json(&again.stdout)["outcome"],
"no-op",
"{}",
again.stdout
);
let removed = sb.mind(&["config", "lobes", "remove", &extra_s, "--json"]);
assert!(
removed.success,
"lobe remove --json failed: {}",
removed.stderr
);
let rv = parse_json(&removed.stdout);
assert_eq!(rv["action"], "lobe-remove", "{}", removed.stdout);
assert_eq!(rv["outcome"], "removed", "{}", removed.stdout);
}
#[test]
fn json_learn_dry_run_lists_nothing_installed_as_prose() {
let sb = melded();
let r = sb.mind(&["learn", "skill:review", "--dry-run", "--json"]);
assert!(r.success, "learn --dry-run --json failed: {}", r.stderr);
let v = parse_json(&r.stdout);
assert_eq!(v["action"], "learn", "{}", r.stdout);
assert_eq!(v["outcome"], "dry-run", "{}", r.stdout);
assert!(
!r.stdout.contains("would learn"),
"dry-run prose must not appear under --json: {}",
r.stdout
);
assert!(
!sb.mind(&["recall"]).stdout.contains("installed @"),
"dry-run must not install anything"
);
}
#[test]
fn json_error_emits_envelope_on_stdout_not_stderr() {
let sb = melded();
let r = sb.mind(&["learn", "does-not-exist", "--json"]);
assert!(!r.success, "unknown item must fail");
let v = parse_json(&r.stdout);
assert_eq!(v["schema"], 1, "schema must be 1: {}", r.stdout);
let err = &v["error"];
assert_eq!(
err["kind"], "item-not-found",
"kind must be item-not-found: {}",
r.stdout
);
assert!(
err["message"]
.as_str()
.map(|s| s.contains("does-not-exist"))
.unwrap_or(false),
"message must contain the query: {}",
r.stdout
);
assert!(
!r.stderr.contains("error:"),
"main error handler must not write to stderr under --json: {}",
r.stderr
);
}
#[test]
fn json_error_envelope_schema_and_kind_fields() {
let sb = melded();
let r = sb.mind(&["--json", "unmeld", "no-such-source"]);
assert!(!r.success, "unmeld unknown source must fail");
let v = parse_json(&r.stdout);
assert_eq!(v["schema"], 1, "schema field must be 1: {}", r.stdout);
let err = &v["error"];
assert_eq!(
err["kind"], "source-not-found",
"kind must be source-not-found: {}",
r.stdout
);
let msg = err["message"].as_str().unwrap_or("");
assert!(!msg.is_empty(), "message must be non-empty: {}", r.stdout);
assert!(
msg.contains("no-such-source"),
"message must contain the source name: {}",
r.stdout
);
assert!(
!r.stderr.contains("error:"),
"no stderr from main handler under --json: {}",
r.stderr
);
}
#[test]
fn clap_usage_error_is_not_json_enveloped() {
let sb = Sandbox::new();
let r = sb.mind(&["--no-such-flag"]);
assert!(!r.success, "unknown flag must fail");
assert!(
r.stdout.trim().is_empty(),
"clap usage errors must not produce stdout output: {:?}",
r.stdout
);
assert!(
!r.stderr.is_empty(),
"clap must write the usage error to stderr: {:?}",
r.stderr
);
}
#[test]
fn non_tty_output_is_plain_ascii_with_no_escapes() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
for args in [
vec!["recall"],
vec!["recall", "--sources"],
vec!["recall", "skill:review"],
vec!["probe"],
vec!["introspect"],
vec!["upgrade"],
] {
let r = sb.mind(&args);
assert!(
!has_ansi_escape(&r.stdout),
"non-TTY stdout for `{args:?}` must contain no ANSI escapes: {:?}",
r.stdout
);
}
}
#[test]
fn no_color_env_forces_plain_ascii() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
let r = sb.mind_env(&["recall"], &[("NO_COLOR", "1")]);
assert!(r.success, "recall failed: {}", r.stderr);
assert!(
!has_ansi_escape(&r.stdout),
"NO_COLOR must force plain ASCII: {:?}",
r.stdout
);
let empty = sb.mind_env(&["recall"], &[("NO_COLOR", "")]);
assert!(
!has_ansi_escape(&empty.stdout),
"empty NO_COLOR must still force plain ASCII: {:?}",
empty.stdout
);
}
#[test]
fn ascii_flag_forces_plain_output() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
let pre = sb.mind(&["--ascii", "recall"]);
assert!(pre.success, "--ascii recall failed: {}", pre.stderr);
assert!(!has_ansi_escape(&pre.stdout), "{:?}", pre.stdout);
let post = sb.mind(&["recall", "--ascii"]);
assert!(!has_ansi_escape(&post.stdout), "{:?}", post.stdout);
}
#[test]
fn ascii_fallback_glyphs_are_present_in_plain_mode() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
let recall = sb.mind(&["recall"]).stdout;
assert!(
recall.contains("installed @"),
"installed marker (ASCII fallback) must show `installed @`: {recall}"
);
assert!(
recall.contains("available"),
"available marker (ASCII fallback) must show `available`: {recall}"
);
for glyph in ['✓', '○', '✗', '●'] {
assert!(
!recall.contains(glyph),
"Unicode glyph {glyph:?} must not appear in plain mode: {recall}"
);
}
let probe = sb.mind(&["probe", "review"]).stdout;
assert!(
probe.contains('*'),
"probe must mark the installed item with the `*` ASCII bullet: {probe}"
);
assert!(
!probe.contains('●'),
"probe must not emit the Unicode bullet in plain mode: {probe}"
);
}
#[test]
fn every_reachable_verb_emits_valid_json_under_json_flag() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let meld = sb.mind(&["meld", &spec, "--json"]);
assert!(meld.success, "{}", meld.stderr);
assert!(parse_json(&meld.stdout).is_object(), "{}", meld.stdout);
let learn = sb.mind(&["learn", "skill:review", "--json"]);
assert!(learn.success, "{}", learn.stderr);
assert!(parse_json(&learn.stdout).is_object(), "{}", learn.stdout);
let sync = sb.mind(&["sync", "--json"]);
assert!(sync.success, "{}", sync.stderr);
assert!(parse_json(&sync.stdout).is_object(), "{}", sync.stdout);
let upgrade = sb.mind(&["upgrade", "--json"]);
assert!(upgrade.success, "{}", upgrade.stderr);
assert!(
parse_json(&upgrade.stdout).is_object(),
"{}",
upgrade.stdout
);
let forget = sb.mind(&["forget", "skill:review", "--json"]);
assert!(forget.success, "{}", forget.stderr);
assert!(parse_json(&forget.stdout).is_object(), "{}", forget.stdout);
let unmeld = sb.mind(&["unmeld", "agents", "--json"]);
assert!(unmeld.success, "{}", unmeld.stderr);
assert!(parse_json(&unmeld.stdout).is_object(), "{}", unmeld.stdout);
}
#[test]
fn json_sync_upgrade_emits_one_document_for_the_invoked_verb() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review code (v2)\n---\n# review v2\n",
);
let r = sb.mind(&["sync", "--upgrade", "--json", "--yes"]);
assert!(
r.success,
"sync --upgrade --json --yes failed: {}",
r.stderr
);
let doc: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"sync --upgrade --json must leave stdout as exactly one JSON \
document ({e}): {:?}",
r.stdout
)
});
assert_eq!(
doc["action"], "sync",
"the one document answers the invoked verb: {doc:#}"
);
assert!(
doc["installed"]
.as_array()
.is_some_and(|a| a.iter().any(|k| k == "skill:review")),
"the --upgrade pass's applied items must be folded into sync's object: {doc:#}"
);
}
#[test]
fn json_upgrade_verb_still_emits_its_own_object() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
let clean = sb.mind(&["upgrade", "--json", "--yes"]);
assert!(clean.success, "upgrade --json: {}", clean.stderr);
let doc: serde_json::Value = serde_json::from_str(clean.stdout.trim()).unwrap_or_else(|e| {
panic!(
"upgrade --json must emit one object ({e}): {:?}",
clean.stdout
)
});
assert_eq!(doc["action"], "upgrade", "{doc:#}");
assert_eq!(doc["outcome"], "up-to-date", "{doc:#}");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review code (v2)\n---\n# review v2\n",
);
let applied = sb.mind(&["upgrade", "--json", "--yes"]);
assert!(applied.success, "upgrade --json: {}", applied.stderr);
let doc: serde_json::Value = serde_json::from_str(applied.stdout.trim()).unwrap_or_else(|e| {
panic!(
"upgrade --json must emit one object ({e}): {:?}",
applied.stdout
)
});
assert_eq!(doc["outcome"], "upgraded", "{doc:#}");
assert!(
doc["installed"]
.as_array()
.is_some_and(|a| a.iter().any(|k| k == "skill:review")),
"the upgraded item must be listed: {doc:#}"
);
}
fn sandbox_with_item_hook_cmds(name: &str, install: &str, uninstall: &str) -> Sandbox {
let sb = Sandbox::bare(name);
write(
&sb.source.join("skills/greet/SKILL.md"),
"---\ndescription: greet the user\n---\n# greet\n",
);
let toml = format!(
concat!(
"[[items]]\n",
"kind = \"skill\"\n",
"name = \"greet\"\n",
"path = \"skills/greet\"\n",
"install = \"{install}\"\n",
"uninstall = \"{uninstall}\"\n",
),
install = install,
uninstall = uninstall,
);
sb.write_and_commit(
"skills/greet/SKILL.md",
"---\ndescription: greet the user\n---\n# greet\n",
);
sb.write_and_commit("mind.toml", &toml);
sb
}
fn sandbox_with_item_hooks(name: &str) -> Sandbox {
let sb = Sandbox::bare(name);
let markers = sb.base.join("markers");
let m = markers.display();
let install = format!("touch built-here && mkdir -p '{m}' && touch '{m}/installed'");
let uninstall = format!("mkdir -p '{m}' && touch '{m}/uninstalled'");
write(
&sb.source.join("skills/greet/SKILL.md"),
"---\ndescription: greet the user\n---\n# greet\n",
);
let toml = format!(
concat!(
"[[items]]\n",
"kind = \"skill\"\n",
"name = \"greet\"\n",
"path = \"skills/greet\"\n",
"install = \"{install}\"\n",
"uninstall = \"{uninstall}\"\n",
),
install = install,
uninstall = uninstall,
);
sb.write_and_commit(
"skills/greet/SKILL.md",
"---\ndescription: greet the user\n---\n# greet\n",
);
sb.write_and_commit("mind.toml", &toml);
sb
}
#[test]
fn learn_runs_item_install_hook_in_store_dir() {
let sb = sandbox_with_item_hooks("agents");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let r = sb.mind(&[
"learn",
"skill:greet",
"--accept-hooks=install,update,uninstall",
]);
assert!(r.success, "learn should succeed: {} {}", r.stdout, r.stderr);
assert!(
sb.mind_home.join("store/skill/greet/SKILL.md").exists(),
"the skill must be installed"
);
assert!(
sb.mind_home.join("store/skill/greet/built-here").exists(),
"install hook must run in the item's store directory"
);
assert!(
sb.base.join("markers/installed").exists(),
"the install hook's side effect must have run"
);
}
#[test]
fn learn_without_flag_skips_item_install_hook_in_non_tty() {
let sb = sandbox_with_item_hooks("agents");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let r = sb.mind(&["learn", "skill:greet"]);
assert!(
r.success,
"learn should still succeed: {} {}",
r.stdout, r.stderr
);
assert!(
sb.mind_home.join("store/skill/greet/SKILL.md").exists(),
"the item must install even though the hook is skipped"
);
assert!(
!sb.base.join("markers/installed").exists(),
"a non-TTY learn must skip the install hook"
);
assert!(
r.stdout.contains("skipped install hook"),
"the skip must be reported: {}",
r.stdout
);
}
#[test]
fn learn_item_install_hook_failure_rolls_back_the_install() {
let sb = sandbox_with_item_hook_cmds("agents", "exit 1", "true");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let r = sb.mind(&[
"learn",
"skill:greet",
"--accept-hooks=install,update,uninstall",
]);
assert!(
!r.success,
"a failing install hook must fail learn: {}",
r.stdout
);
assert!(
!sb.mind_home.join("store/skill/greet").exists(),
"the store copy must be removed on rollback"
);
assert!(
!sb.claude_home.join("skills/greet").exists(),
"the link must be removed on rollback"
);
let manifest = std::fs::read_to_string(sb.mind_home.join("manifest.json")).unwrap_or_default();
assert!(
!manifest.contains("greet"),
"a rolled-back item must not be recorded in the manifest: {manifest}"
);
}
#[test]
fn forget_runs_item_uninstall_hook() {
let sb = sandbox_with_item_hooks("agents");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
assert!(
sb.mind(&[
"learn",
"skill:greet",
"--accept-hooks=install,update,uninstall"
])
.success
);
let r = sb.mind(&[
"forget",
"skill:greet",
"--accept-hooks=install,update,uninstall",
]);
assert!(
r.success,
"forget should succeed: {} {}",
r.stdout, r.stderr
);
assert!(
sb.base.join("markers/uninstalled").exists(),
"the uninstall hook must run at forget"
);
assert!(
!sb.mind_home.join("store/skill/greet").exists(),
"the item must be removed after its uninstall hook"
);
}
#[test]
fn forget_without_flag_skips_item_uninstall_hook_in_non_tty() {
let sb = sandbox_with_item_hooks("agents");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
assert!(
sb.mind(&[
"learn",
"skill:greet",
"--accept-hooks=install,update,uninstall"
])
.success
);
let r = sb.mind(&["forget", "skill:greet"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
!sb.base.join("markers/uninstalled").exists(),
"a non-TTY forget must skip the uninstall hook"
);
assert!(
!sb.mind_home.join("store/skill/greet").exists(),
"the item is still removed when the hook is skipped"
);
assert!(
r.stdout.contains("skipped uninstall hook"),
"the skip must be reported: {}",
r.stdout
);
}
#[test]
fn forget_item_uninstall_hook_failure_leaves_item_installed() {
let sb = sandbox_with_item_hook_cmds("agents", "true", "exit 1");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
assert!(
sb.mind(&[
"learn",
"skill:greet",
"--accept-hooks=install,update,uninstall"
])
.success
);
let r = sb.mind(&[
"forget",
"skill:greet",
"--accept-hooks=install,update,uninstall",
]);
assert!(
!r.success,
"a failing uninstall hook must fail forget: {}",
r.stdout
);
assert!(
sb.mind_home.join("store/skill/greet/SKILL.md").exists(),
"the item must remain installed when its uninstall hook fails"
);
}
#[test]
fn unmeld_runs_item_uninstall_hook() {
let sb = sandbox_with_item_hooks("agents");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
assert!(
sb.mind(&[
"learn",
"skill:greet",
"--accept-hooks=install,update,uninstall"
])
.success
);
let r = sb.mind(&[
"unmeld",
"agents",
"-y",
"--accept-hooks=install,update,uninstall",
]);
assert!(
r.success,
"unmeld should succeed: {} {}",
r.stdout, r.stderr
);
assert!(
sb.base.join("markers/uninstalled").exists(),
"the item uninstall hook must run at unmeld"
);
assert!(
!sb.mind_home.join("store/skill/greet").exists(),
"the item must be removed at unmeld"
);
}
#[test]
fn item_install_hook_reruns_on_reinstall() {
let sb = sandbox_with_item_hooks("agents");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
assert!(
sb.mind(&[
"learn",
"skill:greet",
"--accept-hooks=install,update,uninstall"
])
.success
);
assert!(sb.base.join("markers/installed").exists());
std::fs::remove_dir_all(sb.base.join("markers")).unwrap();
assert!(
sb.mind(&[
"forget",
"skill:greet",
"--accept-hooks=install,update,uninstall"
])
.success
);
assert!(
sb.base.join("markers/uninstalled").exists(),
"uninstall hook fires on removal"
);
let r = sb.mind(&[
"learn",
"skill:greet",
"--accept-hooks=install,update,uninstall",
]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(
sb.base.join("markers/installed").exists(),
"the install hook must re-run on reinstall (HOOK-84)"
);
}
#[test]
fn in_place_upgrade_reruns_install_hook_but_not_uninstall_hook() {
let sb = sandbox_with_item_hooks("agents");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
assert!(
sb.mind(&[
"learn",
"skill:greet",
"--accept-hooks=install,update,uninstall"
])
.success
);
std::fs::remove_dir_all(sb.base.join("markers")).unwrap();
sb.write_and_commit(
"skills/greet/SKILL.md",
"---\ndescription: greet the user\n---\n# greet v2\n",
);
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["upgrade", "-y", "--accept-hooks=install,update,uninstall"]);
assert!(
r.success,
"upgrade should succeed: {} {}",
r.stdout, r.stderr
);
assert!(
sb.base.join("markers/installed").exists(),
"the install hook must re-run on an in-place upgrade (HOOK-81)"
);
assert!(
!sb.base.join("markers/uninstalled").exists(),
"an in-place upgrade must NOT run the uninstall hook (HOOK-82)"
);
}
#[test]
fn review_lists_item_install_and_uninstall_hooks() {
let sb = sandbox_with_item_hooks("agents");
let r = sb.mind(&["review", &sb.source_spec()]);
let all = format!("{}{}", r.stdout, r.stderr);
assert!(
all.contains("item-hook"),
"review must emit item-hook advisories: {all}"
);
assert!(
all.contains("declares a required install hook"),
"review must list the install hook and whether it is required: {all}"
);
assert!(
all.contains("declares a required uninstall hook"),
"review must list the uninstall hook and whether it is required: {all}"
);
}
#[test]
fn recall_marks_item_outdated_after_in_place_content_edit() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let r = sb.mind(&["recall"]);
assert!(r.success, "recall failed: {} {}", r.stdout, r.stderr);
assert!(
!r.stdout.contains("outdated"),
"freshly installed item must not be outdated: {}",
r.stdout
);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\nmodified content\n",
);
let r = sb.mind(&["recall"]);
assert!(r.success, "recall failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("outdated"),
"recall must mark the item outdated after an in-place content edit: {}",
r.stdout
);
}
#[test]
fn recall_status_view_uses_stale_marker_for_outdated_item() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let r = sb.mind(&["recall"]);
assert!(r.success, "recall failed: {} {}", r.stdout, r.stderr);
let line = r
.stdout
.lines()
.find(|l| l.contains("skill:review"))
.unwrap_or_else(|| panic!("no review line in recall output: {}", r.stdout));
assert_eq!(
line.trim_start().chars().next(),
Some('+'),
"a current install must lead with the `+` marker: {line:?}"
);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\nmodified content\n",
);
let r = sb.mind(&["recall"]);
assert!(r.success, "recall failed: {} {}", r.stdout, r.stderr);
let line = r
.stdout
.lines()
.find(|l| l.contains("skill:review"))
.unwrap_or_else(|| panic!("no review line in recall output: {}", r.stdout));
assert_eq!(
line.trim_start().chars().next(),
Some('^'),
"an outdated install must lead with the `^` stale marker: {line:?}"
);
}
#[test]
fn source_status_uses_stale_marker_for_outdated_item() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\nmodified content\n",
);
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "re-meld failed: {} {}", r.stdout, r.stderr);
let line = r
.stdout
.lines()
.find(|l| l.contains("skill:review"))
.unwrap_or_else(|| panic!("no review line in source_status output: {}", r.stdout));
assert_eq!(
line.trim_start().chars().next(),
Some('^'),
"an outdated install must lead with the `^` stale marker: {line:?}"
);
}
#[test]
fn recall_item_detail_shows_out_of_date_after_content_edit() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\nmodified content\n",
);
let r = sb.mind(&["recall", "skill:review"]);
assert!(r.success, "recall failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("out of date"),
"recall <item> must show out-of-date note after content edit: {}",
r.stdout
);
}
#[test]
fn recall_does_not_mark_unedited_item_outdated() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let r = sb.mind(&["recall"]);
assert!(r.success, "recall failed: {} {}", r.stdout, r.stderr);
assert!(
!r.stdout.contains("outdated"),
"unedited item must not be marked outdated: {}",
r.stdout
);
let r = sb.mind(&["recall", "skill:review"]);
assert!(r.success, "recall failed: {} {}", r.stdout, r.stderr);
assert!(
!r.stdout.contains("out of date"),
"recall <item> must not show out-of-date for unedited item: {}",
r.stdout
);
}
#[test]
fn probe_marks_installed_item_outdated_after_in_place_content_edit() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let r = sb.mind(&["probe", "--no-tui"]);
assert!(r.success, "probe failed: {} {}", r.stdout, r.stderr);
assert!(
!r.stdout.contains("outdated"),
"freshly installed items must not be outdated in probe: {}",
r.stdout
);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\nchanged\n",
);
let r = sb.mind(&["probe", "--no-tui"]);
assert!(r.success, "probe failed: {} {}", r.stdout, r.stderr);
let review = r
.stdout
.lines()
.find(|l| l.contains("skill:review"))
.unwrap_or_else(|| panic!("no review row in probe: {}", r.stdout));
assert!(
review.contains("outdated"),
"probe must mark the drifted item outdated: {review:?}\n{}",
r.stdout
);
let dev = r
.stdout
.lines()
.find(|l| l.contains("agent:dev"))
.unwrap_or_else(|| panic!("no dev row in probe: {}", r.stdout));
assert!(
!dev.contains("outdated"),
"an unedited item must not be marked outdated in probe: {dev:?}"
);
}
#[test]
fn remeld_source_status_marks_item_outdated_after_in_place_content_edit() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\nremeld-edit\n",
);
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "re-meld failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("already melded"),
"expected the already-melded status view: {}",
r.stdout
);
assert!(
r.stdout.contains("outdated"),
"source_status via re-meld must mark the drifted item outdated: {}",
r.stdout
);
}
#[test]
fn recall_does_not_mark_item_outdated_after_commit_only_advance() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let r = sb.mind(&["recall"]);
assert!(
!r.stdout.contains("outdated"),
"freshly installed item must not be outdated: {}",
r.stdout
);
sb.write_and_commit("CHANGES.md", "unrelated change\n");
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["recall"]);
assert!(r.success, "recall failed: {} {}", r.stdout, r.stderr);
assert!(
!r.stdout.contains("outdated"),
"recall must NOT mark the item outdated after a commit-only advance (content unchanged): {}",
r.stdout
);
let r = sb.mind(&["upgrade", "--yes"]);
assert!(r.success, "upgrade failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("everything is up to date"),
"upgrade must report everything up to date after a commit-only advance: {}",
r.stdout
);
}
#[test]
fn recall_json_is_unchanged_by_drift() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let before_status = sb.mind(&["recall", "--json"]);
let before_detail = sb.mind(&["recall", "skill:review", "--json"]);
assert!(before_status.success && before_detail.success);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\njson-drift\n",
);
let after_status = sb.mind(&["recall", "--json"]);
let after_detail = sb.mind(&["recall", "skill:review", "--json"]);
assert!(after_status.success && after_detail.success);
assert_eq!(
before_status.stdout, after_status.stdout,
"recall --json status output must not change with drift"
);
assert_eq!(
before_detail.stdout, after_detail.stdout,
"recall <item> --json output must not change with drift"
);
assert!(
!after_status.stdout.contains("outdated") && !after_status.stdout.contains("out of date"),
"JSON must carry no human out-of-date marker: {}",
after_status.stdout
);
}
#[test]
fn probe_does_not_mark_item_outdated_after_commit_only_advance() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let r = sb.mind(&["probe", "--no-tui"]);
assert!(r.success, "probe failed: {} {}", r.stdout, r.stderr);
assert!(
!r.stdout.contains("outdated"),
"freshly installed items must not be outdated in probe: {}",
r.stdout
);
sb.write_and_commit("NOTES.md", "unrelated\n");
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["probe", "--no-tui"]);
assert!(r.success, "probe failed: {} {}", r.stdout, r.stderr);
assert!(
!r.stdout.contains("outdated"),
"probe must NOT mark item outdated after commit-only advance: {}",
r.stdout
);
let r = sb.mind(&["recall"]);
assert!(r.success, "recall failed: {} {}", r.stdout, r.stderr);
assert!(
!r.stdout.contains("outdated"),
"recall must NOT mark item outdated after commit-only advance: {}",
r.stdout
);
let r = sb.mind(&["upgrade", "--yes"]);
assert!(r.success, "upgrade failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("everything is up to date"),
"upgrade must report everything up to date after commit-only advance: {}",
r.stdout
);
}
#[test]
fn recall_still_marks_item_outdated_after_commit_with_content_change() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let r = sb.mind(&["recall"]);
assert!(
!r.stdout.contains("outdated"),
"freshly installed item must not be outdated: {}",
r.stdout
);
sb.edit_source();
assert!(sb.mind(&["sync"]).success);
let r = sb.mind(&["recall"]);
assert!(r.success, "recall failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("outdated"),
"recall must mark item outdated when commit also changed content: {}",
r.stdout
);
}
#[test]
fn recall_and_probe_mark_item_outdated_on_rename_without_content_change() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let recall = sb.mind(&["recall"]);
assert!(
!recall.stdout.contains("outdated"),
"fresh install must not be outdated in recall: {}",
recall.stdout
);
let probe = sb.mind(&["probe", "--no-tui"]);
assert!(
!probe.stdout.contains("outdated"),
"fresh install must not be outdated in probe: {}",
probe.stdout
);
let detail = sb.mind(&["recall", "skill:review"]);
assert!(
!detail.stdout.contains("out of date"),
"fresh install single-item must not be out of date: {}",
detail.stdout
);
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
assert!(sb.mind(&["sync"]).success, "sync failed");
let probe = sb.mind(&["probe", "--no-tui"]);
assert!(probe.success, "probe failed: {}", probe.stderr);
assert!(
probe.stdout.contains("outdated"),
"probe must mark a renamed item outdated: {}",
probe.stdout
);
let detail = sb.mind(&["recall", "skill:review"]);
assert!(detail.success, "recall detail failed: {}", detail.stderr);
assert!(
detail.stdout.contains("out of date"),
"recall single-item detail must report a renamed item out of date: {}",
detail.stdout
);
let up = sb.mind(&["upgrade", "--yes"]);
assert!(up.success, "upgrade failed: {} {}", up.stdout, up.stderr);
assert!(
!up.stdout.contains("everything is up to date"),
"upgrade must NOT report up to date when an effective name changed: {}",
up.stdout
);
assert!(
up.stdout.contains("rename")
&& up.stdout.contains("review -> ")
&& up.stdout.contains("jk:review"),
"upgrade must report the rename review -> jk:review: {}",
up.stdout
);
}
#[test]
fn recall_status_view_marks_renamed_item_outdated() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
assert!(sb.mind(&["sync"]).success, "sync failed");
let recall = sb.mind(&["recall"]);
assert!(recall.success, "recall failed: {}", recall.stderr);
assert!(
recall.stdout.contains("outdated"),
"recall status view must mark a renamed (effective-name-changed) item \
outdated to agree with probe/detail/upgrade: {}",
recall.stdout
);
assert!(
!recall.stdout.contains("removed upstream"),
"a pure namespace rename must not be reported as removed upstream: {}",
recall.stdout
);
}
#[test]
fn all_four_surfaces_agree_on_hash_drift() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff for bugs\n---\n# review skill\nfour-surface-drift\n",
);
let recall = sb.mind(&["recall"]);
assert!(
recall.stdout.contains("outdated"),
"recall status must flag hash drift: {}",
recall.stdout
);
let detail = sb.mind(&["recall", "skill:review"]);
assert!(
detail.stdout.contains("out of date"),
"recall single-item detail must flag hash drift: {}",
detail.stdout
);
let probe = sb.mind(&["probe", "--no-tui"]);
assert!(
probe.stdout.contains("outdated"),
"probe must flag hash drift: {}",
probe.stdout
);
let up = sb.mind(&["upgrade", "--yes"]);
assert!(up.success, "upgrade failed: {} {}", up.stdout, up.stderr);
assert!(
!up.stdout.contains("everything is up to date"),
"upgrade must act on the drifted item: {}",
up.stdout
);
let recall = sb.mind(&["recall"]);
assert!(
!recall.stdout.contains("outdated"),
"recall must be clean after upgrade: {}",
recall.stdout
);
let detail = sb.mind(&["recall", "skill:review"]);
assert!(
!detail.stdout.contains("out of date"),
"recall detail must be clean after upgrade: {}",
detail.stdout
);
let probe = sb.mind(&["probe", "--no-tui"]);
assert!(
!probe.stdout.contains("outdated"),
"probe must be clean after upgrade: {}",
probe.stdout
);
}
#[test]
fn json_outputs_carry_no_outdated_marker_under_rename_drift() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
assert!(sb.mind(&["sync"]).success, "sync failed");
let recall = sb.mind(&["recall", "--json"]);
let detail = sb.mind(&["recall", "skill:review", "--json"]);
let probe = sb.mind(&["probe", "--json"]);
assert!(recall.success && detail.success && probe.success);
for (label, body) in [
("recall --json", &recall.stdout),
("recall detail --json", &detail.stdout),
("probe --json", &probe.stdout),
] {
let _: serde_json::Value =
serde_json::from_str(body).unwrap_or_else(|e| panic!("{label} not valid JSON: {e}"));
assert!(
!body.contains("outdated") && !body.contains("out of date"),
"{label} must carry no human out-of-date marker: {body}"
);
}
}
#[test]
fn recall_status_renamed_item_appears_exactly_once_no_orphan_dup() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--yes"]).success, "meld failed");
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
assert!(sb.mind(&["sync"]).success, "sync failed");
let recall = sb.mind(&["recall"]);
assert!(recall.success, "recall failed: {}", recall.stderr);
let review_lines: Vec<&str> = recall
.stdout
.lines()
.filter(|l| l.contains("skill:review") || l.contains("skill:jk:review"))
.collect();
assert_eq!(
review_lines.len(),
1,
"the renamed item must appear on exactly one row (no orphan dup), got: {:#?}",
review_lines
);
assert!(
review_lines[0].contains("outdated"),
"the single review row must be the outdated row: {}",
review_lines[0]
);
assert!(
!recall.stdout.contains("removed upstream"),
"a pure rename must not be flagged removed upstream: {}",
recall.stdout
);
}
#[test]
fn recall_json_renamed_item_installed_once_not_orphaned() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--yes"]).success, "meld failed");
let before = parse_json(&sb.mind(&["recall", "--json"]).stdout);
let source_commit = before["items"][0]["commit"].as_str().unwrap().to_string();
sb.write_and_commit("mind.toml", "[source]\nprefix = \"jk\"\n");
assert!(sb.mind(&["sync"]).success, "sync failed");
let j = parse_json(&sb.mind(&["recall", "--json"]).stdout);
let items = j["items"][0]["items"].as_array().expect("items array");
let review_rows: Vec<&serde_json::Value> = items
.iter()
.filter(|r| {
let k = r["key"].as_str().unwrap_or("");
k == "skill:review" || k == "skill:jk:review"
})
.collect();
assert_eq!(
review_rows.len(),
1,
"the renamed skill must be emitted exactly once in recall --json: {items:#?}"
);
let row = review_rows[0];
assert_eq!(
row["key"].as_str(),
Some("skill:jk:review"),
"the renamed item must carry its new effective key: {row}"
);
assert_eq!(
row["installed"].as_bool(),
Some(true),
"the renamed item must resolve installed by stable identity: {row}"
);
assert_eq!(
row["commit"].as_str(),
Some(source_commit.as_str()),
"the renamed item must carry its install commit: {row}"
);
assert!(
row.get("orphaned").is_none(),
"the renamed item must not be flagged orphaned: {row}"
);
assert!(
!items.iter().any(|r| r.get("orphaned").is_some()),
"no catalog-matched item may be reported orphaned under a pure rename: {items:#?}"
);
let detail = sb.mind(&["recall", "skill:review", "--json"]);
assert!(
detail.success,
"recall detail --json failed: {}",
detail.stderr
);
let d = parse_json(&detail.stdout);
assert_eq!(
d["name"].as_str(),
Some("review"),
"the single-item lookup resolves by the recorded (old) name: {d}"
);
}
#[test]
fn removed_upstream_still_flagged_in_recall_human_and_json() {
let sb = melded();
assert!(sb.mind(&["learn", "dev"]).success, "learn dev failed");
sb.remove_and_commit("agents/dev.md");
assert!(sb.mind(&["sync"]).success, "sync failed");
let recall = sb.mind(&["recall"]);
assert!(recall.success, "recall failed: {}", recall.stderr);
assert!(
recall.stdout.contains("agent:dev"),
"the removed item must still be listed: {}",
recall.stdout
);
assert!(
recall.stdout.contains("removed upstream"),
"a genuinely removed item must be flagged removed upstream: {}",
recall.stdout
);
let j = parse_json(&sb.mind(&["recall", "--json"]).stdout);
let items = j["items"][0]["items"].as_array().expect("items array");
let dev = items
.iter()
.find(|r| r["key"].as_str() == Some("agent:dev"))
.expect("the removed agent must be present in recall --json");
assert_eq!(
dev["installed"].as_bool(),
Some(true),
"the removed-upstream item is still installed: {dev}"
);
assert_eq!(
dev["orphaned"].as_bool(),
Some(true),
"the removed-upstream item must be flagged orphaned in JSON: {dev}"
);
assert!(
!items
.iter()
.any(|r| r["key"].as_str() == Some("skill:review") && r.get("orphaned").is_some()),
"a still-cataloged item must not be orphaned: {items:#?}"
);
}
#[test]
fn same_bare_name_across_sources_does_not_cross_match_on_removal() {
let a = Sandbox::new();
let b = Sandbox::new();
assert!(a.mind(&["meld", &a.source_spec()]).success, "meld a");
assert!(
a.mind(&["meld", &b.source_spec(), "--as", "zz"]).success,
"meld b as zz"
);
assert!(a.mind(&["learn", "review"]).success, "learn review (a)");
assert!(
a.mind(&["learn", "zz:review"]).success,
"learn zz:review (b)"
);
a.remove_and_commit("skills/review/SKILL.md");
assert!(a.mind(&["sync"]).success, "sync failed");
let recall = a.mind(&["recall"]);
assert!(recall.success, "recall failed: {}", recall.stderr);
let removed_lines: Vec<&str> = recall
.stdout
.lines()
.filter(|l| l.contains("removed upstream"))
.collect();
assert_eq!(
removed_lines.len(),
1,
"exactly one item (A's review) must be removed upstream: {:#?}",
removed_lines
);
assert!(
removed_lines[0].contains("skill:review") && !removed_lines[0].contains("zz:review"),
"the removed-upstream row must be A's review, not B's zz:review: {}",
removed_lines[0]
);
let jj = parse_json(&a.mind(&["recall", "--json"]).stdout);
let sources = jj["items"].as_array().expect("sources array");
let mut saw_review_orphan = false;
let mut saw_zz_review_ok = false;
for s in sources {
for r in s["items"].as_array().unwrap() {
match r["key"].as_str() {
Some("skill:review") => {
assert_eq!(
r["orphaned"].as_bool(),
Some(true),
"A's review must be orphaned: {r}"
);
saw_review_orphan = true;
}
Some("skill:zz:review") => {
assert!(
r.get("orphaned").is_none(),
"B's zz:review must not be orphaned: {r}"
);
assert_eq!(
r["installed"].as_bool(),
Some(true),
"B's zz:review must stay installed: {r}"
);
saw_zz_review_ok = true;
}
_ => {}
}
}
}
assert!(
saw_review_orphan && saw_zz_review_ok,
"both A's orphaned review and B's intact zz:review must be present: {jj:#?}"
);
}
#[test]
fn unmanaged_listing_unaffected_by_orphan_detection() {
let sb = melded();
assert!(sb.mind(&["learn", "dev"]).success, "learn dev failed");
write(
&sb.claude_home.join("skills/handmade/SKILL.md"),
"---\nname: handmade\ndescription: hand written\n---\n# handmade\n",
);
sb.remove_and_commit("agents/dev.md");
assert!(sb.mind(&["sync"]).success, "sync failed");
let recall = sb.mind(&["recall"]);
assert!(recall.success, "recall failed: {}", recall.stderr);
assert!(
recall.stdout.contains("unmanaged: not installed by mind"),
"the unmanaged group must still be shown: {}",
recall.stdout
);
assert!(
recall.stdout.contains("skill:handmade"),
"the unmanaged item must still be listed: {}",
recall.stdout
);
assert!(
recall.stdout.contains("agent:dev") && recall.stdout.contains("removed upstream"),
"the removed-upstream mind item must still be flagged alongside unmanaged: {}",
recall.stdout
);
assert!(
!recall.stdout.contains("handmade") || !recall_handmade_is_in_a_source(&recall.stdout),
"the unmanaged item must not be classified as a source's removed-upstream item: {}",
recall.stdout
);
}
fn recall_handmade_is_in_a_source(stdout: &str) -> bool {
stdout
.lines()
.any(|l| l.contains("handmade") && l.contains("removed upstream"))
}
fn read_sources_json(sb: &Sandbox) -> String {
std::fs::read_to_string(sb.mind_home.join("sources.json")).expect("sources.json")
}
fn make_unonboarded_nested(name: &str) -> Sandbox {
let sb = Sandbox::bare(name);
write(
&sb.source.join("pkg/skills/widget/SKILL.md"),
"---\nname: widget\ndescription: A curated widget skill\n---\n# widget\n",
);
git(&sb.source, &["add", "-A"]);
git(&sb.source, &["commit", "-qm", "pkg layout"]);
git(&sb.source, &["branch", "stable"]);
sb
}
#[test]
fn curator_applies_follow_branch_roots_and_hook_when_nested_has_no_mind_toml() {
let nested = make_unonboarded_nested("widgets");
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
follow-branch = \"stable\"\n\
roots = [\"pkg\"]\n\n\
[[discover.sources.hooks]]\n\
run = \"touch curated-hookran\"\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
let probe = registry.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:widget"),
"curator roots must govern discovery so the pkg-only item is found: {}",
probe.stdout
);
let json = read_sources_json(®istry);
assert!(
json.contains("follow-branch") && json.contains("stable"),
"the nested source's pin must be recorded as follow-branch=stable: {json}"
);
let nested_clone = registry
.mind_home
.join("sources/local")
.join(nested.base_name())
.join("widgets");
let marker = nested_clone.join("curated-hookran");
assert!(
marker.exists(),
"the curator-supplied hook must have run in the nested clone: {} missing",
marker.display()
);
assert!(
json.contains("touch curated-hookran"),
"the curator hook command must be recorded on the nested source: {json}"
);
}
#[test]
fn curator_values_ignored_with_warning_when_nested_has_mind_toml() {
let nested = make_unonboarded_nested("onboarded");
nested.write_and_commit("mind.toml", "[source]\ndescription = \"onboarded\"\n");
git(&nested.source, &["branch", "-f", "stable"]);
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
follow-branch = \"stable\"\n\
roots = [\"pkg\"]\n\n\
[[discover.sources.hooks]]\n\
run = \"touch curated-hookran\"\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
assert!(
r.stderr.contains("ships its own mind.toml")
&& r.stderr.contains("ignored")
&& r.stderr.contains("onboarded"),
"a DSC-60 warning must be emitted naming the onboarded source: {}",
r.stderr
);
assert!(
!r.stderr.contains("--add-root"),
"no add-roots hint should be printed when the curator entry set no \
add-roots: {}",
r.stderr
);
let json = read_sources_json(®istry);
assert!(
json.contains("follow-branch") && json.contains("stable"),
"the curator follow-branch must apply (authoritative DSC-65), recorded as follow-branch=stable: {json}"
);
let probe = registry.mind(&["probe"]);
assert!(
!probe.stdout.contains("skill:widget"),
"the curator roots must be suppressed: the pkg-only item must not appear: {}",
probe.stdout
);
let nested_clone = registry
.mind_home
.join("sources/local")
.join(nested.base_name())
.join("onboarded");
assert!(
!nested_clone.join("curated-hookran").exists(),
"the curator hook must be suppressed (no marker)"
);
assert!(
!json.contains("touch curated-hookran"),
"the curator hook command must not be recorded when suppressed: {json}"
);
}
#[test]
fn curator_add_roots_applied_when_nested_has_no_mind_toml() {
let nested = Sandbox::bare("addroot-widgets");
nested.write_and_commit(
"skills/base/SKILL.md",
"---\nname: base\ndescription: base skill\n---\n# base\n",
);
nested.write_and_commit(
"contrib/skills/addon/SKILL.md",
"---\nname: addon\ndescription: add-root contributed skill\n---\n# addon\n",
);
let registry = Sandbox::bare("addroot-registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\nsource = \"{}\"\nadd-roots = [\"contrib\"]\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
let probe = registry.mind(&["probe"]).stdout;
assert!(
probe.contains("skill:base"),
"the base item (plain convention scan) must be discovered: {probe}"
);
assert!(
probe.contains("skill:addon"),
"curator add-roots must apply when the nested source has no mind.toml: {probe}"
);
}
#[test]
fn curator_add_roots_ignored_with_warning_when_nested_has_mind_toml() {
let nested = Sandbox::bare("addroot-onboarded");
nested.write_and_commit(
"skills/base/SKILL.md",
"---\nname: base\ndescription: base skill\n---\n# base\n",
);
nested.write_and_commit(
"contrib/skills/addon/SKILL.md",
"---\nname: addon\ndescription: add-root contributed skill\n---\n# addon\n",
);
nested.write_and_commit("mind.toml", "[source]\ndescription = \"onboarded\"\n");
let registry = Sandbox::bare("addroot-registry-gated");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\nsource = \"{}\"\nadd-roots = [\"contrib\"]\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
assert!(
r.stderr.contains("ships its own mind.toml")
&& r.stderr.contains("ignored")
&& r.stderr.contains("onboarded"),
"a DSC-60 warning must be emitted naming the onboarded source: {}",
r.stderr
);
assert!(
r.stderr.contains("add-roots")
&& r.stderr.contains("mind meld")
&& r.stderr.contains("--add-root")
&& r.stderr.contains("not gated"),
"the DSC-60 warning must tell the consumer they can apply add-roots \
themselves with `mind meld <source> --add-root <dir>`: {}",
r.stderr
);
let probe = registry.mind(&["probe"]).stdout;
assert!(
probe.contains("skill:base"),
"the base item (plain convention scan) must still be discovered: {probe}"
);
assert!(
!probe.contains("skill:addon"),
"curator add-roots must be gated (ignored) when the nested source ships \
its own mind.toml: {probe}"
);
}
#[test]
fn consumer_pin_flag_overrides_curator_follow_branch() {
let nested = make_unonboarded_nested("pinned");
git(&nested.source, &["tag", "v1"]);
let spec = nested.source_spec();
let r = nested.mind(&["meld", &spec, "--pin-tag", "v1"]);
assert!(r.success, "meld --pin-tag should succeed: {}", r.stderr);
let json = read_sources_json(&nested);
assert!(
json.contains("\"kind\": \"tag\"") && json.contains("v1"),
"a consumer pin flag must win and record a tag pin: {json}"
);
assert!(
!json.contains("follow-branch"),
"a consumer pin flag must override any follow-branch (no follow-branch pin recorded): {json}"
);
}
#[test]
fn curator_empty_roots_list_discovers_nothing() {
let nested = make_unonboarded_nested("emptyroots");
nested.write_and_commit(
"skills/toplevel/SKILL.md",
"---\nname: toplevel\ndescription: A root-level skill\n---\n# toplevel\n",
);
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
roots = []\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
let probe = registry.mind(&["probe"]);
assert!(
!probe.stdout.contains("skill:widget"),
"empty curator roots must scan nothing (pkg item must not appear): {}",
probe.stdout
);
assert!(
!probe.stdout.contains("skill:toplevel"),
"empty curator roots must scan nothing, not even the repo root (toplevel must not appear): {}",
probe.stdout
);
}
#[test]
fn curator_hooks_do_not_leak_across_nested_entries() {
let first = make_unonboarded_nested("alpha");
let second = make_unonboarded_nested("beta");
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
follow-branch = \"stable\"\n\
roots = [\"pkg\"]\n\n\
[[discover.sources.hooks]]\n\
run = \"touch alpha-marker\"\n\n\
[[discover.sources]]\n\
source = \"{}\"\n\
follow-branch = \"stable\"\n\
roots = [\"pkg\"]\n\n\
[[discover.sources.hooks]]\n\
run = \"touch beta-marker\"\n",
first.source_spec(),
second.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
let alpha_clone = registry
.mind_home
.join("sources/local")
.join(first.base_name())
.join("alpha");
let beta_clone = registry
.mind_home
.join("sources/local")
.join(second.base_name())
.join("beta");
assert!(
alpha_clone.join("alpha-marker").exists(),
"alpha's own hook must run in alpha's clone"
);
assert!(
beta_clone.join("beta-marker").exists(),
"beta's own hook must run in beta's clone"
);
assert!(
!alpha_clone.join("beta-marker").exists(),
"beta's hook leaked into alpha's clone"
);
assert!(
!beta_clone.join("alpha-marker").exists(),
"alpha's hook leaked into beta's clone"
);
let json = read_sources_json(®istry);
assert!(
json.contains("touch alpha-marker") && json.contains("touch beta-marker"),
"each nested source records its own hook command: {json}"
);
}
#[test]
fn curator_values_suppressed_when_nested_declares_own_pin_roots_hooks() {
let nested = make_unonboarded_nested("selfdeclared");
nested.write_and_commit(
"mind.toml",
"[source]\n\
description = \"self-declared\"\n\
follow-branch = \"own\"\n\
roots = [\"pkg\"]\n\n\
[[hooks]]\n\
run = \"touch source-own-hook\"\n",
);
git(&nested.source, &["branch", "own"]);
git(&nested.source, &["branch", "-f", "stable"]);
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
follow-branch = \"stable\"\n\
roots = [\"nonexistent\"]\n\n\
[[discover.sources.hooks]]\n\
run = \"touch curator-hook\"\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec, "--accept-hooks=install,update,uninstall"]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
assert!(
r.stderr.contains("ships its own mind.toml")
&& r.stderr.contains("ignored")
&& r.stderr.contains("selfdeclared"),
"a DSC-60 warning must name the onboarded source: {}",
r.stderr
);
let json = read_sources_json(®istry);
let probe = registry.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:widget"),
"the source's own roots = [pkg] must govern, finding its item: {}",
probe.stdout
);
assert!(
json.contains("follow-branch") && json.contains("\"stable\""),
"the curator follow-branch=stable must win (DSC-65 authoritative): {json}"
);
assert!(
!json.contains("\"own\""),
"the source's own follow-branch=own must be overridden by the curator pin: {json}"
);
let nested_clone = registry
.mind_home
.join("sources/local")
.join(nested.base_name())
.join("selfdeclared");
assert!(
nested_clone.join("source-own-hook").exists(),
"the source's own declared hook must run"
);
assert!(
!nested_clone.join("curator-hook").exists(),
"the curator hook must be suppressed"
);
assert!(
json.contains("touch source-own-hook") && !json.contains("touch curator-hook"),
"only the source's own hook command is recorded: {json}"
);
}
#[test]
fn curator_pin_ref_authoritative_overrides_source_own_pin() {
let nested = make_unonboarded_nested("pinref-target");
nested.write_and_commit(
"mind.toml",
"[source]\ndescription = \"onboarded\"\nfollow-branch = \"own\"\n",
);
git(&nested.source, &["branch", "own"]);
let sha_output = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&nested.source)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("git rev-parse HEAD");
let sha = String::from_utf8_lossy(&sha_output.stdout)
.trim()
.to_string();
assert!(!sha.is_empty(), "could not capture HEAD commit sha");
let registry = Sandbox::bare("pinref-registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
pin-ref = \"{sha}\"\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
assert!(
!r.stderr.contains("ignored"),
"no DSC-60 warning should fire for a pin-only curator entry (no gated fields): {}",
r.stderr
);
let json = read_sources_json(®istry);
assert!(
json.contains("\"kind\": \"ref\"") && json.contains(&sha),
"the curator pin-ref must be recorded as the ref pin: {json}"
);
assert!(
!json.contains("follow-branch"),
"the source's own follow-branch must be overridden by the curator pin-ref: {json}"
);
}
#[test]
fn curator_hook_skipped_under_non_tty_without_skip_flag() {
let nested = make_unonboarded_nested("skiphook");
let registry = Sandbox::bare("registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
follow-branch = \"stable\"\n\
roots = [\"pkg\"]\n\n\
[[discover.sources.hooks]]\n\
run = \"touch curated-hookran\"\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(
r.success,
"meld should still succeed: {} {}",
r.stdout, r.stderr
);
let nested_clone = registry
.mind_home
.join("sources/local")
.join(nested.base_name())
.join("skiphook");
assert!(
!nested_clone.join("curated-hookran").exists(),
"a non-TTY meld without the skip flag must NOT run the curator hook"
);
assert!(
r.stdout.contains("skipped install hook") || r.stderr.contains("skipped install hook"),
"the skip must be announced: {} {}",
r.stdout,
r.stderr
);
let json = read_sources_json(®istry);
assert!(
json.contains("follow-branch") && json.contains("stable"),
"roots/follow-branch still apply even when the hook is skipped: {json}"
);
}
#[test]
fn sync_rewalk_applies_curator_follow_branch_to_new_nested() {
let registry = Sandbox::bare("registry");
let first = make_unonboarded_nested("present"); let later = make_unonboarded_nested("arriving");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
roots = [\"pkg\"]\n",
first.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(
r.success,
"initial meld should succeed: {} {}",
r.stdout, r.stderr
);
let before = registry.mind(&["recall", "--sources"]).stdout;
assert!(
!before.contains("/arriving"),
"the new nested source must not be registered before sync: {before}"
);
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
roots = [\"pkg\"]\n\n\
[[discover.sources]]\n\
source = \"{}\"\n\
follow-branch = \"stable\"\n\
roots = [\"pkg\"]\n",
first.source_spec(),
later.source_spec()
),
);
let r = registry.mind(&["sync"]);
assert!(r.success, "sync should succeed: {} {}", r.stdout, r.stderr);
assert!(
registry
.mind(&["recall", "--sources"])
.stdout
.contains("/arriving"),
"sync must register the newly-listed nested source"
);
let json = read_sources_json(®istry);
assert!(
json.contains("arriving") && json.contains("follow-branch") && json.contains("stable"),
"sync's re-walk must apply the curator follow-branch to the new nested source: {json}"
);
let probe = registry.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:widget"),
"curator roots must govern discovery for a sync-discovered nested source: {}",
probe.stdout
);
}
#[test]
fn curator_pin_tag_authoritative_overrides_source_own_pin() {
let nested = make_unonboarded_nested("pintag-target");
nested.write_and_commit(
"mind.toml",
"[source]\ndescription = \"onboarded\"\nfollow-branch = \"own\"\n",
);
git(&nested.source, &["branch", "own"]);
git(&nested.source, &["tag", "rel-1"]);
let registry = Sandbox::bare("pintag-registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
pin-tag = \"rel-1\"\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
assert!(
!r.stderr.contains("ignored"),
"no DSC-60 warning should fire for a pin-only (pin-tag) curator entry: {}",
r.stderr
);
let json = read_sources_json(®istry);
assert!(
json.contains("\"kind\": \"tag\"") && json.contains("rel-1"),
"the curator pin-tag must be recorded as the tag pin: {json}"
);
assert!(
!json.contains("follow-branch"),
"the source's own follow-branch must be overridden by the curator pin-tag: {json}"
);
}
#[test]
fn curator_conflicting_pin_directives_is_meld_error() {
let nested = make_unonboarded_nested("conflict-target");
let registry = Sandbox::bare("conflict-registry");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
follow-branch = \"stable\"\n\
pin-ref = \"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\"\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(
!r.success,
"a nested entry with two pin directives must fail at meld: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("conflicting pin"),
"the meld error must mention conflicting pin directives: {}",
r.stderr
);
let recall = registry.mind(&["recall", "--sources"]).stdout;
assert!(
!recall.contains("/conflict-target"),
"the conflicting nested source must not be registered: {recall}"
);
}
#[test]
fn learn_requires_frontmatter_pulls_dependency_closure() {
let sb = Sandbox::bare("req-closure");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:reviewer\n---\n# review skill\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviewer agent\n---\n# reviewer\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let r = sb.mind(&["learn", "skill:review", "--yes"]);
assert!(r.success, "learn must succeed: {}", r.stderr);
let recall = sb.mind(&["recall"]).stdout;
assert!(
recall.contains("skill:review"),
"selected skill must be installed: {recall}"
);
assert!(
recall.contains("agent:reviewer"),
"requires entry must pull the dependency into the closure: {recall}"
);
let dep_line = r
.stdout
.lines()
.position(|l| l.starts_with("learned agent:reviewer "));
let dep_line = dep_line.unwrap_or_else(|| panic!("no reviewer learned line: {}", r.stdout));
let skill_line = r
.stdout
.lines()
.position(|l| l.starts_with("learned skill:review "));
let skill_line = skill_line.unwrap_or_else(|| panic!("no review learned line: {}", r.stdout));
assert!(
dep_line < skill_line,
"requires dep must install before its dependent: {}",
r.stdout
);
}
#[test]
fn learn_requires_union_with_token_dep_deduped() {
let sb = Sandbox::bare("req-dedup");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:reviewer\n---\n# review\nhandoff to {{ns:reviewer}}\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviewer\n---\n# reviewer\n",
);
sb.write_and_commit("rules/style.md", "---\ndescription: style\n---\n# style\n");
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let r = sb.mind(&["learn", "skill:review", "--yes"]);
assert!(r.success, "{}", r.stderr);
let reviewer_count = r
.stdout
.lines()
.filter(|l| l.contains("agent:reviewer"))
.count();
assert_eq!(
reviewer_count, 1,
"dedup: agent:reviewer must appear once in the install output: {}",
r.stdout
);
}
#[test]
fn learn_requires_typo_is_bad_reference_error() {
let sb = Sandbox::bare("req-typo");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:nonexistent\n---\n# review skill\n",
);
sb.write_and_commit(
"agents/helper.md",
"---\ndescription: helper\n---\n# helper\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
let r = sb.mind(&["learn", "skill:review", "--yes"]);
assert!(
!r.success,
"learn with unresolved requires must fail: {} {}",
r.stdout, r.stderr
);
let combined = format!("{} {}", r.stdout, r.stderr);
assert!(
combined.contains("nonexistent")
|| combined.contains("bad")
|| combined.contains("reference"),
"error output must mention the bad entry: {combined}"
);
}
#[test]
fn learn_requires_bad_reference_names_specific_cause() {
let xs = Sandbox::bare("req-crosssource");
xs.write_and_commit(
"skills/main/SKILL.md",
"---\nname: main\ndescription: Main\nrequires: other/repo#agent:helper\n---\n# main\n",
);
xs.write_and_commit("rules/style.md", "---\ndescription: style\n---\n# style\n");
assert!(xs.mind(&["meld", &xs.source_spec()]).success, "meld failed");
let r = xs.mind(&["learn", "skill:main", "--yes"]);
assert!(
!r.success,
"cross-source requires must fail: {} {}",
r.stdout, r.stderr
);
let combined = format!("{} {}", r.stdout, r.stderr);
assert!(
combined.contains("crosses sources"),
"must name the cross-source cause, not a blanket miss: {combined}"
);
let am = Sandbox::bare("req-ambiguous");
am.write_and_commit(
"skills/main/SKILL.md",
"---\nname: main\ndescription: Main\nrequires: dup\n---\n# main\n",
);
am.write_and_commit(
"skills/dup/SKILL.md",
"---\nname: dup\ndescription: Dup skill\n---\n# dup\n",
);
am.write_and_commit(
"agents/dup.md",
"---\nname: dup\ndescription: Dup agent\n---\n# dup\n",
);
assert!(am.mind(&["meld", &am.source_spec()]).success, "meld failed");
let r = am.mind(&["learn", "skill:main", "--yes"]);
assert!(
!r.success,
"ambiguous requires must fail: {} {}",
r.stdout, r.stderr
);
let combined = format!("{} {}", r.stdout, r.stderr);
assert!(
combined.contains("ambiguous across kinds"),
"must name the ambiguity cause, not a blanket miss: {combined}"
);
}
#[test]
fn learn_requires_resolves_against_own_source_not_a_sibling_source() {
let a = Sandbox::bare("alpha");
a.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:helper\n---\n# review\n",
);
a.write_and_commit("rules/style.md", "---\ndescription: style\n---\n# style\n");
let b = Sandbox::bare("beta");
b.write_and_commit(
"agents/helper.md",
"---\nname: helper\ndescription: Helper\n---\n# helper\n",
);
assert!(a.mind(&["meld", &a.source_spec()]).success, "meld A failed");
assert!(a.mind(&["meld", &b.source_spec()]).success, "meld B failed");
let r = a.mind(&["learn", "skill:review", "--yes"]);
assert!(
!r.success,
"a requires entry must not resolve against another source's agent: {} {}",
r.stdout, r.stderr
);
let combined = format!("{} {}", r.stdout, r.stderr);
assert!(
combined.contains("helper") || combined.contains("bad") || combined.contains("reference"),
"error must name the unresolved cross-source entry: {combined}"
);
}
#[test]
fn review_requires_resolves_per_source_in_a_multi_source_registry() {
let a = Sandbox::bare("alpha");
a.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:helper\n---\n# review\n",
);
let b = Sandbox::bare("beta");
b.write_and_commit(
"agents/helper.md",
"---\nname: helper\ndescription: Helper\n---\n# helper\n",
);
assert!(a.mind(&["meld", &a.source_spec()]).success, "meld A failed");
assert!(a.mind(&["meld", &b.source_spec()]).success, "meld B failed");
let r = a.mind(&["review", "alpha"]);
assert!(
!r.success,
"review of alpha must fail: its requires must not resolve against beta: {} {}",
r.stdout, r.stderr
);
let combined = format!("{} {}", r.stdout, r.stderr);
assert!(
combined.contains("bad-reference") && combined.contains("helper"),
"must report alpha's unresolved cross-source requires: {combined}"
);
}
#[test]
fn review_requires_typo_is_hard_finding() {
let sb = Sandbox::bare("review-req-typo");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:nonexistent\n---\n# review skill\n",
);
let r = sb.mind(&["review", &sb.source_spec()]);
assert!(
!r.success,
"review with unresolved requires must exit non-zero: {} {}",
r.stdout, r.stderr
);
let combined = format!("{} {}", r.stdout, r.stderr);
assert!(
combined.contains("bad-reference"),
"must report a bad-reference finding: {combined}"
);
assert!(
combined.contains("nonexistent"),
"bad-reference message must name the offending entry: {combined}"
);
}
fn dep60_fixture() -> Sandbox {
let sb = Sandbox::bare("dep60-agents");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:reviewer\n---\n# review skill\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviewer agent\n---\n# reviewer\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(
sb.mind(&["learn", "skill:review", "--yes"]).success,
"fixture: learn should succeed"
);
sb
}
#[test]
fn forget_single_item_with_dependents_refuses_non_tty_without_force() {
let sb = dep60_fixture();
let r = sb.mind(&["forget", "agent:reviewer"]);
assert!(
!r.success,
"forget of a depended-on item must refuse in non-TTY: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("needs confirmation"),
"must report ConfirmationRequired: {}",
r.stderr
);
assert!(
sb.mind(&["recall", "agent:reviewer"]).success,
"the item must still be installed after refused forget"
);
}
#[test]
fn forget_single_item_with_dependents_lists_them() {
let sb = dep60_fixture();
let r = sb.mind(&["forget", "agent:reviewer"]);
assert!(!r.success);
assert!(
r.stdout.contains("skill:review"),
"output must name the dependent: {}",
r.stdout
);
}
#[test]
fn forget_single_item_with_dependents_proceeds_with_yes() {
let sb = dep60_fixture();
let r = sb.mind(&["forget", "--yes", "agent:reviewer"]);
assert!(
r.success,
"forget --yes must proceed: {} {}",
r.stdout, r.stderr
);
assert!(
!sb.mind(&["recall", "agent:reviewer"]).success,
"item must be removed after forget --yes"
);
assert!(
sb.mind(&["recall", "skill:review"]).success,
"dependent must remain installed (DEP-50)"
);
}
#[test]
fn forget_single_item_with_dependents_proceeds_with_force() {
let sb = dep60_fixture();
let r = sb.mind(&["forget", "--force", "agent:reviewer"]);
assert!(
r.success,
"forget --force must proceed: {} {}",
r.stdout, r.stderr
);
assert!(
!sb.mind(&["recall", "agent:reviewer"]).success,
"item must be removed after forget --force"
);
assert!(
sb.mind(&["recall", "skill:review"]).success,
"dependent must remain installed (DEP-50)"
);
}
#[test]
fn forget_single_item_no_dependents_removes_without_extra_prompt() {
let sb = dep60_fixture();
let r = sb.mind(&["forget", "skill:review"]);
assert!(
r.success,
"forget with no dependents must not prompt: {} {}",
r.stdout, r.stderr
);
assert!(
!sb.mind(&["recall", "skill:review"]).success,
"skill must be removed"
);
assert!(
sb.mind(&["recall", "agent:reviewer"]).success,
"reviewer must remain installed"
);
}
#[test]
fn forget_glob_path_uses_existing_cli42_confirmation_not_dep60() {
let sb = dep60_fixture();
let r = sb.mind(&["forget", "*"]);
assert!(!r.success, "multi-item forget must refuse: {}", r.stderr);
assert!(
r.stderr.contains("needs confirmation"),
"must report ConfirmationRequired: {}",
r.stderr
);
assert!(
r.stdout.contains("would remove"),
"must show CLI-42 count message: {}",
r.stdout
);
assert!(
sb.mind(&["recall", "agent:reviewer"]).success,
"reviewer still installed"
);
assert!(
sb.mind(&["recall", "skill:review"]).success,
"review still installed"
);
}
fn dep61_fixture() -> Sandbox {
let sb = Sandbox::bare("dep61-agents");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:reviewer\n---\n# review skill\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviewer agent\n---\n# reviewer\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(
sb.mind(&["learn", "skill:review", "--yes"]).success,
"fixture: learn should succeed"
);
sb
}
#[test]
fn recall_tree_renders_dependency_forest() {
let sb = dep61_fixture();
let r = sb.mind(&["recall", "--tree"]);
assert!(
r.success,
"recall --tree must succeed: {} {}",
r.stdout, r.stderr
);
let out = &r.stdout;
assert!(
out.contains("skill:review"),
"forest must include skill:review: {out}"
);
assert!(
out.contains("agent:reviewer"),
"forest must include agent:reviewer: {out}"
);
assert!(
out.lines().any(|l| l.starts_with("- skill:review")),
"skill:review must be a root: {out}"
);
assert!(
out.lines().any(|l| l.starts_with(" - agent:reviewer")),
"agent:reviewer must be nested under skill:review: {out}"
);
}
#[test]
fn recall_tree_item_scopes_to_subtree() {
let sb = dep61_fixture();
let r = sb.mind(&["recall", "skill:review", "--tree"]);
assert!(
r.success,
"recall <item> --tree must succeed: {} {}",
r.stdout, r.stderr
);
let out = &r.stdout;
assert!(
out.lines().any(|l| l.starts_with("- skill:review")),
"subtree root must be skill:review: {out}"
);
assert!(
out.contains("agent:reviewer"),
"subtree must include the dependency: {out}"
);
}
#[test]
fn recall_tree_dependency_only_item_is_not_a_root() {
let sb = dep61_fixture();
let r = sb.mind(&["recall", "--tree"]);
assert!(r.success, "recall --tree must succeed");
let out = &r.stdout;
assert!(
!out.lines().any(|l| l.starts_with("- agent:reviewer")),
"agent:reviewer must not be a primary root: {out}"
);
}
fn dep62_fixture() -> Sandbox {
let sb = Sandbox::bare("dep62-agents");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:reviewer\n---\n# review skill\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviewer agent\n---\n# reviewer\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
sb
}
#[test]
fn probe_non_interactive_nests_dependency_under_dependent() {
let sb = dep62_fixture();
let r = sb.mind(&["probe", "--no-tui", "--kind", "skill", "review"]);
assert!(
r.success,
"probe --no-tui --kind skill must succeed: {} {}",
r.stdout, r.stderr
);
let out = &r.stdout;
assert!(
out.contains("skill:review"),
"skill:review must appear in output: {out}"
);
let review_pos = out.lines().position(|l| l.contains("skill:review"));
let reviewer_pos = out.lines().position(|l| l.contains("agent:reviewer"));
assert!(review_pos.is_some(), "skill:review must appear: {out}");
assert!(
reviewer_pos.is_some(),
"agent:reviewer dependency must appear in output: {out}"
);
assert!(
reviewer_pos.unwrap() > review_pos.unwrap(),
"agent:reviewer (dependency) must come after skill:review: {out}"
);
let reviewer_line = out.lines().find(|l| l.contains("agent:reviewer")).unwrap();
assert!(
reviewer_line.starts_with(" "),
"dependency line must be indented: {reviewer_line:?}"
);
}
#[test]
fn probe_json_includes_dependencies_field() {
let sb = dep62_fixture();
let r = sb.mind(&["probe", "--json", "review"]);
assert!(
r.success,
"probe --json must succeed: {} {}",
r.stdout, r.stderr
);
let env: serde_json::Value = serde_json::from_str(&r.stdout).expect("must be valid JSON");
let rows = env["items"].as_array().expect("items must be array");
let review_row = rows
.iter()
.find(|row| row["name"] == "review")
.expect("skill:review must be in JSON output");
let deps = review_row["dependencies"]
.as_array()
.expect("dependencies must be an array");
assert!(
deps.iter().any(|d| d == "agent:reviewer"),
"dependencies must include agent:reviewer: {deps:?}"
);
}
#[test]
fn probe_json_item_with_no_deps_omits_dependencies_field() {
let sb = dep62_fixture();
let r = sb.mind(&["probe", "--json", "reviewer"]);
assert!(
r.success,
"probe --json must succeed: {} {}",
r.stdout, r.stderr
);
let env: serde_json::Value = serde_json::from_str(&r.stdout).expect("must be valid JSON");
let rows = env["items"].as_array().expect("items must be array");
let reviewer_row = rows
.iter()
.find(|row| row["name"] == "reviewer")
.expect("agent:reviewer must be in JSON output");
let deps = reviewer_row.get("dependencies");
assert!(
deps.is_none() || deps.unwrap().as_array().is_some_and(|a| a.is_empty()),
"dependencies field must be absent or empty for an item with no deps: {reviewer_row}"
);
}
fn dep_chain_fixture() -> Sandbox {
let sb = Sandbox::bare("dep-chain");
sb.write_and_commit(
"skills/a/SKILL.md",
"---\nname: a\ndescription: A\nrequires: agent:b\n---\n# a skill\n",
);
sb.write_and_commit(
"agents/b.md",
"---\nname: b\ndescription: B\nrequires: rule:c\n---\n# b agent\n",
);
sb.write_and_commit(
"rules/c.md",
"---\nname: c\ndescription: C\n---\n# c rule\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(
sb.mind(&["learn", "dep-chain#*"]).success,
"fixture: whole-source learn should install all three"
);
sb
}
#[test]
fn forget_transitive_lists_only_direct_dependent_and_no_cascade() {
let sb = dep_chain_fixture();
let refused = sb.mind(&["forget", "agent:b"]);
assert!(
!refused.success,
"forget of a depended-on middle item must refuse: {} {}",
refused.stdout, refused.stderr
);
assert!(
refused.stderr.contains("needs confirmation"),
"must report ConfirmationRequired: {}",
refused.stderr
);
assert!(
refused.stdout.contains("skill:a"),
"the direct dependent skill:a must be listed: {}",
refused.stdout
);
assert!(
!refused.stdout.lines().any(|l| l.trim() == "rule:c"),
"rule:c (a dependency of b, not a dependent) must not be listed as a dependent: {}",
refused.stdout
);
assert!(sb.mind(&["recall", "agent:b"]).success, "b still installed");
let done = sb.mind(&["forget", "--yes", "agent:b"]);
assert!(done.success, "forget --yes must proceed: {}", done.stderr);
assert!(
!sb.mind(&["recall", "agent:b"]).success,
"b must be removed"
);
assert!(
sb.mind(&["recall", "skill:a"]).success,
"dependent a must remain (no upward cascade)"
);
assert!(
sb.mind(&["recall", "rule:c"]).success,
"dependency c must remain (no downward cascade, DEP-50)"
);
}
#[test]
fn forget_dependent_warning_fires_on_union_of_requires_and_token_edges() {
let sb = Sandbox::bare("dep-union");
sb.write_and_commit(
"agents/target.md",
"---\nname: target\ndescription: Target\n---\n# target\n",
);
sb.write_and_commit(
"skills/via-requires/SKILL.md",
"---\nname: via-requires\ndescription: R\nrequires: agent:target\n---\n# via-requires\n",
);
sb.write_and_commit(
"skills/via-token/SKILL.md",
"---\nname: via-token\ndescription: T\n---\n# via-token\nhand off to {{ns:target}}\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(
sb.mind(&["learn", "dep-union#*"]).success,
"fixture: whole-source learn should install all three"
);
let r = sb.mind(&["forget", "agent:target"]);
assert!(
!r.success,
"forget of the doubly-depended item must refuse: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("skill:via-requires"),
"the requires-edge dependent must be listed: {}",
r.stdout
);
assert!(
r.stdout.contains("skill:via-token"),
"the token-edge dependent must be listed: {}",
r.stdout
);
}
#[test]
fn forget_force_does_not_bypass_cli42_multi_item_confirmation() {
let sb = dep60_fixture();
let r = sb.mind(&["forget", "--force", "*"]);
assert!(
!r.success,
"forget --force over a multi-match glob must still refuse: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("needs confirmation"),
"must report ConfirmationRequired (CLI-42, not bypassed by --force): {}",
r.stderr
);
assert!(
r.stdout.contains("would remove"),
"must show the CLI-42 count message: {}",
r.stdout
);
assert!(
sb.mind(&["recall", "agent:reviewer"]).success,
"reviewer must remain installed"
);
assert!(
sb.mind(&["recall", "skill:review"]).success,
"review must remain installed"
);
}
#[test]
fn recall_tree_item_with_no_dependencies_prints_just_that_item() {
let sb = dep61_fixture();
let r = sb.mind(&["recall", "agent:reviewer", "--tree"]);
assert!(
r.success,
"recall <leaf> --tree must succeed: {} {}",
r.stdout, r.stderr
);
let lines: Vec<&str> = r.stdout.lines().filter(|l| !l.is_empty()).collect();
assert_eq!(
lines,
vec!["- agent:reviewer"],
"a dependency-free item's subtree must be just that item: {:?}",
r.stdout
);
}
fn dep_cycle_fixture() -> Sandbox {
let sb = Sandbox::bare("dep-cycle");
sb.write_and_commit(
"skills/loop-a/SKILL.md",
"---\nname: loop-a\ndescription: A\nrequires: skill:loop-b\n---\n# loop-a\n",
);
sb.write_and_commit(
"skills/loop-b/SKILL.md",
"---\nname: loop-b\ndescription: B\nrequires: skill:loop-a\n---\n# loop-b\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
assert!(
sb.mind(&["learn", "dep-cycle#*"]).success,
"fixture: whole-source learn should install both cycle members"
);
sb
}
#[test]
fn recall_tree_cyclic_installed_pair_renders_every_item() {
let sb = dep_cycle_fixture();
let r = sb.mind(&["recall", "--tree"]);
assert!(
r.success,
"recall --tree over a cycle must succeed: {} {}",
r.stdout, r.stderr
);
let out = &r.stdout;
assert!(
out.contains("skill:loop-a"),
"loop-a must appear in the forest: {out}"
);
assert!(
out.contains("skill:loop-b"),
"loop-b must appear in the forest: {out}"
);
assert!(
out.contains("(cycle)"),
"the cycle must be rendered as a marked back-edge: {out}"
);
}
#[test]
fn probe_json_resolves_dependency_to_prefixed_effective_key() {
let sb = Sandbox::bare("dep-prefix");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:reviewer\n---\n# review\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviewer\n---\n# reviewer\n",
);
assert!(sb.mind(&["meld", &sb.source_spec(), "--as", "jk"]).success);
let r = sb.mind(&["probe", "--json", "review"]);
assert!(
r.success,
"probe --json must succeed: {} {}",
r.stdout, r.stderr
);
let env: serde_json::Value = serde_json::from_str(&r.stdout).expect("must be valid JSON");
let rows = env["items"].as_array().expect("items must be array");
let review_row = rows
.iter()
.find(|row| row["name"] == "jk:review")
.expect("skill:jk:review must be in JSON output (prefixed effective name)");
let deps = review_row["dependencies"]
.as_array()
.expect("dependencies must be an array");
assert!(
deps.iter().any(|d| d == "agent:jk:reviewer"),
"dependency key must be the prefixed effective key agent:jk:reviewer, not bare: {deps:?}"
);
assert!(
!deps.iter().any(|d| d == "agent:reviewer"),
"the bare (unprefixed) dependency key must NOT appear: {deps:?}"
);
}
#[test]
fn recall_tree_json_emits_json_array_with_dependency_nested() {
let sb = dep61_fixture();
let r = sb.mind(&["recall", "--tree", "--json"]);
assert!(
r.success,
"recall --tree --json must succeed: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim())
.unwrap_or_else(|e| panic!("output must be valid JSON: {e}\n{}", r.stdout));
assert!(v.is_array(), "output must be a JSON array: {v}");
let arr = v.as_array().unwrap();
let root = arr
.iter()
.find(|n| n["key"] == "skill:review")
.unwrap_or_else(|| panic!("must have skill:review as root: {arr:?}"));
assert!(
root.get("dependencies").is_some(),
"root node must have dependencies field: {root}"
);
assert!(
root.get("cycle").is_none(),
"root node must not have cycle field: {root}"
);
let deps = root["dependencies"].as_array().unwrap();
let reviewer = deps
.iter()
.find(|n| n["key"] == "agent:reviewer")
.unwrap_or_else(|| panic!("agent:reviewer must be in dependencies: {deps:?}"));
assert!(
reviewer.get("cycle").is_none(),
"reviewer node must not be a cycle: {reviewer}"
);
let reviewer_deps = reviewer["dependencies"]
.as_array()
.expect("reviewer must have dependencies field");
assert!(
reviewer_deps.is_empty(),
"reviewer is a leaf, so dependencies must be empty: {reviewer_deps:?}"
);
}
#[test]
fn recall_tree_json_item_emits_single_object_not_array() {
let sb = dep61_fixture();
let r = sb.mind(&["recall", "skill:review", "--tree", "--json"]);
assert!(
r.success,
"recall <item> --tree --json must succeed: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim())
.unwrap_or_else(|e| panic!("output must be valid JSON: {e}\n{}", r.stdout));
assert!(
v.is_object(),
"scoped recall --tree --json must emit an object: {v}"
);
assert_eq!(
v["key"], "skill:review",
"object key must be skill:review: {v}"
);
let deps = v["dependencies"]
.as_array()
.expect("root object must have dependencies");
assert_eq!(deps.len(), 1, "skill:review has one dependency: {deps:?}");
assert_eq!(deps[0]["key"], "agent:reviewer");
}
#[test]
fn recall_tree_json_leaf_item_has_empty_dependencies() {
let sb = dep61_fixture();
let r = sb.mind(&["recall", "agent:reviewer", "--tree", "--json"]);
assert!(
r.success,
"recall <leaf> --tree --json must succeed: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim())
.unwrap_or_else(|e| panic!("output must be valid JSON: {e}\n{}", r.stdout));
assert!(v.is_object(), "must be an object: {v}");
assert_eq!(v["key"], "agent:reviewer");
let deps = v["dependencies"]
.as_array()
.expect("leaf object must have dependencies field (not absent)");
assert!(
deps.is_empty(),
"leaf must have empty dependencies array: {deps:?}"
);
}
#[test]
fn recall_tree_json_with_prefix_uses_effective_keys() {
let sb = Sandbox::bare("dep63-prefix");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:reviewer\n---\n# review\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviewer\n---\n# reviewer\n",
);
assert!(
sb.mind(&["meld", "--as", "pfx", &sb.source_spec()]).success,
"meld with prefix must succeed"
);
assert!(
sb.mind(&["learn", "pfx:review", "--yes"]).success,
"learn with prefix must succeed"
);
let r = sb.mind(&["recall", "--tree", "--json"]);
assert!(
r.success,
"recall --tree --json with prefix must succeed: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim())
.unwrap_or_else(|e| panic!("must be valid JSON: {e}\n{}", r.stdout));
assert!(v.is_array());
let arr = v.as_array().unwrap();
let root = arr
.iter()
.find(|n| n["key"] == "skill:pfx:review")
.unwrap_or_else(|| panic!("root must be skill:pfx:review: {arr:?}"));
let deps = root["dependencies"].as_array().unwrap();
assert_eq!(deps.len(), 1, "one dep: {deps:?}");
assert_eq!(
deps[0]["key"], "agent:pfx:reviewer",
"dep must use effective prefixed key: {deps:?}"
);
}
#[test]
fn recall_tree_with_sources_resolves_to_sources_path_with_note() {
let sb = dep61_fixture();
let r = sb.mind(&["recall", "--tree", "--sources"]);
assert!(
r.success,
"recall --tree --sources must succeed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("--tree") && r.stderr.contains("ignored with --sources"),
"a note that --tree is ignored with --sources must be emitted: {}",
r.stderr
);
assert!(
r.stdout.contains("dep61-agents"),
"the sources listing (not a dependency forest) must be shown: {}",
r.stdout
);
assert!(
!r.stdout
.lines()
.any(|l| l.starts_with(" - agent:reviewer")),
"must not render the dependency forest under --sources: {}",
r.stdout
);
}
#[test]
fn recall_tree_json_not_installed_item_errors_like_non_json() {
let sb = dep61_fixture();
let json = sb.mind(&["recall", "skill:nope", "--tree", "--json"]);
assert!(
!json.success,
"recall <uninstalled> --tree --json must fail: {} {}",
json.stdout, json.stderr
);
let v = parse_json(&json.stdout);
assert_eq!(v["schema"], 1, "schema must be 1: {}", json.stdout);
assert_eq!(
v["error"]["kind"], "not-installed",
"kind must be not-installed: {}",
json.stdout
);
assert!(
v["error"]["message"]
.as_str()
.map(|s| s.contains("not installed") || s.contains("nope"))
.unwrap_or(false),
"message must mention the missing item: {}",
json.stdout
);
let human = sb.mind(&["recall", "skill:nope", "--tree"]);
assert!(
!human.success,
"non-json recall <uninstalled> --tree must also fail: {} {}",
human.stdout, human.stderr
);
assert!(
human.stderr.contains("not installed"),
"non-json form must also report NotInstalled: {}",
human.stderr
);
}
#[test]
fn recall_tree_json_installed_but_orphaned_item_falls_back_to_normal_node() {
let sb = melded();
assert!(sb.mind(&["learn", "dev"]).success, "learn dev failed");
sb.remove_and_commit("agents/dev.md");
assert!(sb.mind(&["sync"]).success, "sync failed");
let r = sb.mind(&["recall", "agent:dev", "--tree", "--json"]);
assert!(
r.success,
"recall <orphaned> --tree --json must succeed via the fallback: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim())
.unwrap_or_else(|e| panic!("output must be valid JSON: {e}\n{:?}", r.stdout));
assert!(
v.is_object(),
"fallback must emit a single object, not an array: {v}"
);
assert_eq!(
v["key"], "agent:dev",
"fallback node key must be the item key: {v}"
);
let deps = v["dependencies"]
.as_array()
.expect("fallback node must carry an (empty) dependencies array");
assert!(
deps.is_empty(),
"an orphaned item has no graph edges, so dependencies must be empty: {deps:?}"
);
assert!(
v.get("cycle").is_none(),
"the fallback node must not be a cycle leaf: {v}"
);
}
#[test]
fn recall_tree_json_empty_manifest_emits_empty_array() {
let sb = dep61_fixture_unlearned();
let r = sb.mind(&["recall", "--tree", "--json"]);
assert!(
r.success,
"recall --tree --json over an empty manifest must succeed: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim())
.unwrap_or_else(|e| panic!("output must be valid JSON: {e}\n{:?}", r.stdout));
assert!(
v.as_array().is_some_and(|a| a.is_empty()),
"empty manifest must yield an empty JSON array: {v}"
);
}
fn dep61_fixture_unlearned() -> Sandbox {
let sb = Sandbox::bare("dep63-empty");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\nname: review\ndescription: Review\nrequires: agent:reviewer\n---\n# review\n",
);
sb.write_and_commit(
"agents/reviewer.md",
"---\nname: reviewer\ndescription: Reviewer\n---\n# reviewer\n",
);
assert!(sb.mind(&["meld", &sb.source_spec()]).success);
sb
}
#[test]
fn recall_tree_json_cyclic_pair_every_item_present_with_cycle_leaf() {
let sb = dep_cycle_fixture();
let r = sb.mind(&["recall", "--tree", "--json"]);
assert!(
r.success,
"recall --tree --json over a cycle must succeed: {} {}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim())
.unwrap_or_else(|e| panic!("output must be valid JSON: {e}\n{:?}", r.stdout));
let arr = v.as_array().expect("forest must be a JSON array");
fn collect(node: &serde_json::Value, out: &mut std::collections::HashSet<String>) {
if let Some(k) = node["key"].as_str() {
out.insert(k.to_string());
}
if let Some(children) = node["dependencies"].as_array() {
for c in children {
collect(c, out);
}
}
}
let mut seen = std::collections::HashSet::new();
for root in arr {
collect(root, &mut seen);
}
assert!(
seen.contains("skill:loop-a") && seen.contains("skill:loop-b"),
"every installed cycle member must appear in the JSON forest: {seen:?}"
);
fn has_cycle_leaf(node: &serde_json::Value) -> bool {
if node["cycle"] == serde_json::Value::Bool(true) {
assert!(
node.get("dependencies").is_none(),
"a cycle leaf must omit dependencies: {node}"
);
return true;
}
node["dependencies"]
.as_array()
.is_some_and(|cs| cs.iter().any(has_cycle_leaf))
}
assert!(
arr.iter().any(has_cycle_leaf),
"the cycle must surface as a {{cycle:true}} leaf, not infinite nesting: {v}"
);
}
#[test]
fn recall_tree_json_and_probe_json_agree_on_direct_dependencies() {
let sb = dep61_fixture();
let tree = sb.mind(&["recall", "skill:review", "--tree", "--json"]);
assert!(
tree.success,
"recall --tree --json must succeed: {}",
tree.stderr
);
let tv: serde_json::Value = serde_json::from_str(tree.stdout.trim())
.unwrap_or_else(|e| panic!("recall tree JSON invalid: {e}\n{:?}", tree.stdout));
let mut tree_deps: Vec<String> = tv["dependencies"]
.as_array()
.expect("subtree object must have dependencies")
.iter()
.map(|n| n["key"].as_str().unwrap().to_string())
.collect();
tree_deps.sort();
let probe = sb.mind(&["probe", "--json", "review"]);
assert!(probe.success, "probe --json must succeed: {}", probe.stderr);
let probe_env: serde_json::Value =
serde_json::from_str(&probe.stdout).expect("probe JSON invalid");
let rows = probe_env["items"].as_array().expect("items must be array");
let review_row = rows
.iter()
.find(|row| row["name"] == "review")
.expect("skill:review must be a probe row");
let mut probe_deps: Vec<String> = review_row
.get("dependencies")
.and_then(|d| d.as_array())
.map(|a| a.iter().map(|d| d.as_str().unwrap().to_string()).collect())
.unwrap_or_default();
probe_deps.sort();
assert_eq!(
tree_deps, probe_deps,
"recall --tree --json and probe --json must agree on skill:review's direct deps"
);
assert_eq!(
tree_deps,
vec!["agent:reviewer".to_string()],
"both forms must report exactly agent:reviewer: {tree_deps:?}"
);
}
#[test]
fn forget_json_without_yes_when_dependents_exist_is_confirmation_required() {
let sb = dep60_fixture();
let r = sb.mind(&["--json", "forget", "agent:reviewer"]);
assert!(
!r.success,
"forget --json without --yes must refuse when dependents exist: stdout={} stderr={}",
r.stdout, r.stderr
);
let v = parse_json(&r.stdout);
assert_eq!(v["schema"], 1, "schema must be 1: {}", r.stdout);
assert_eq!(
v["error"]["kind"], "confirmation-required",
"kind must be confirmation-required: {}",
r.stdout
);
assert!(
sb.mind(&["recall", "agent:reviewer"]).success,
"agent:reviewer must remain installed after json refusal"
);
}
#[test]
fn forget_json_with_yes_when_dependents_exist_proceeds() {
let sb = dep60_fixture();
let r = sb.mind(&["--json", "--yes", "forget", "agent:reviewer"]);
assert!(
r.success,
"forget --json --yes must proceed: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
!sb.mind(&["recall", "agent:reviewer"]).success,
"agent:reviewer must be removed after forget --json --yes"
);
}
#[test]
fn meld_pin_tag_leading_dash_is_invalid_ref() {
let sb = Sandbox::bare("pin-inject-tag");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin-tag=-x"]);
assert!(
!r.success,
"--pin-tag=-x must fail: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("invalid ref") || r.stderr.contains("InvalidRef"),
"must report InvalidRef: stderr={}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no sources melded"),
"no source must be registered after invalid pin: {}",
sources.stdout
);
}
#[test]
fn meld_pin_ref_leading_dash_is_invalid_ref() {
let sb = Sandbox::bare("pin-inject-ref");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin-ref=--upload-pack=evil"]);
assert!(
!r.success,
"--pin-ref=--upload-pack=evil must fail: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("invalid ref") || r.stderr.contains("InvalidRef"),
"must report InvalidRef: stderr={}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no sources melded"),
"no source must be registered: {}",
sources.stdout
);
}
#[test]
fn meld_follow_branch_leading_dash_is_invalid_ref() {
let sb = Sandbox::bare("pin-inject-branch");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--follow-branch=-evil"]);
assert!(
!r.success,
"--follow-branch=-evil must fail: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("invalid ref") || r.stderr.contains("InvalidRef"),
"must report InvalidRef: stderr={}",
r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no sources melded"),
"no source must be registered: {}",
sources.stdout
);
}
#[test]
fn meld_pin_tag_space_separated_leading_dash_is_rejected_before_git() {
let sb = Sandbox::bare("pin-inject-space");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--pin-tag", "-x"]);
assert!(
!r.success,
"--pin-tag -x (space form) must fail: stdout={} stderr={}",
r.stdout, r.stderr
);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("no sources melded"),
"no source must be registered after a rejected space-form pin: {}",
sources.stdout
);
}
#[test]
fn meld_json_with_yes_installs_and_emits_single_object() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["--json", "meld", &spec, "--yes"]);
assert!(
r.success,
"meld --yes --json must succeed: stdout={} stderr={}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"meld --yes --json stdout must be one valid JSON object, got error {e}: '{}'",
r.stdout
)
});
assert_eq!(v["action"], "meld", "action must be 'meld': {v}");
assert_eq!(v["outcome"], "melded", "outcome must be 'melded': {v}");
let installed = v["installed"]
.as_array()
.expect("installed must be an array");
assert!(
!installed.is_empty(),
"installed must not be empty when --yes is given: {v}"
);
}
#[test]
fn meld_json_no_yes_non_tty_emits_single_object_with_pending() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind_with_input(&["--json", "meld", &spec], Some(""));
assert!(
r.success,
"meld --json (no --yes) must succeed: stdout={} stderr={}",
r.stdout, r.stderr
);
let v: serde_json::Value = serde_json::from_str(r.stdout.trim()).unwrap_or_else(|e| {
panic!(
"meld --json stdout must be one valid JSON object, got error {e}: '{}'",
r.stdout
)
});
assert_eq!(v["action"], "meld", "action must be 'meld': {v}");
assert_eq!(v["outcome"], "melded", "outcome must be 'melded': {v}");
let pending = v["pending_items"].as_u64().unwrap_or(0);
assert!(
pending >= 1,
"pending_items must be >= 1 when items exist but --yes was not given: {v}"
);
}
#[test]
fn relearn_already_installed_signals_noop() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r1 = sb.mind(&["meld", &spec, "--yes"]);
assert!(r1.success, "initial meld+learn must succeed: {}", r1.stderr);
let r2 = sb.mind(&["learn", "skill:review"]);
assert!(
r2.success,
"re-learn must still exit 0: stdout={} stderr={}",
r2.stdout, r2.stderr
);
assert!(
r2.stdout.contains("already installed") || r2.stdout.contains("nothing to do"),
"re-learn human output must signal noop: '{}'",
r2.stdout
);
let r3 = sb.mind(&["--json", "learn", "skill:review"]);
assert!(
r3.success,
"re-learn --json must exit 0: stdout={} stderr={}",
r3.stdout, r3.stderr
);
let v: serde_json::Value = serde_json::from_str(r3.stdout.trim()).unwrap_or_else(|e| {
panic!(
"re-learn --json stdout must be one valid JSON object, got error {e}: '{}'",
r3.stdout
)
});
assert_eq!(
v["outcome"], "up-to-date",
"json outcome for re-learn must be 'up-to-date', not 'installed': {v}"
);
}
fn make_flat_source(name: &str, mindfile: Option<&str>) -> Sandbox {
let sb = Sandbox::bare(name);
write(
&sb.source.join("alpha/SKILL.md"),
"---\nname: alpha\ndescription: Alpha flat skill\n---\n# alpha\n",
);
write(
&sb.source.join("beta/SKILL.md"),
"---\nname: beta\ndescription: Beta flat skill\n---\n# beta\n",
);
write(
&sb.source.join("agents/dev.md"),
"---\nname: dev\ndescription: A dev agent\n---\n# dev\n",
);
if let Some(toml) = mindfile {
write(&sb.source.join("mind.toml"), toml);
}
git(&sb.source, &["add", "-A"]);
git(&sb.source, &["commit", "-qm", "flat layout"]);
sb
}
#[test]
fn meld_flat_skills_flag_discovers_root_level_skill_dirs() {
let sb = make_flat_source("flatsrc", None);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--flat-skills", "--yes"]);
assert!(
r.success,
"meld --flat-skills failed: {} {}",
r.stdout, r.stderr
);
let recall = sb.mind(&["recall"]);
for item in ["alpha", "beta", "dev"] {
assert!(
recall.stdout.contains(item),
"flat skill/agent '{item}' must be discovered: {}",
recall.stdout
);
}
let json = read_sources_json(&sb);
assert!(
json.contains("\"flat_skills\": true"),
"the --flat-skills override must be persisted on the source: {json}"
);
}
#[test]
fn meld_without_flat_skills_skips_root_level_skill_dirs() {
let sb = make_flat_source("flatsrc-control", None);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = sb.mind(&["probe"]);
assert!(
!probe.stdout.contains("skill:alpha") && !probe.stdout.contains("skill:beta"),
"root-level skill dirs must NOT be discovered without flat-skills: {}",
probe.stdout
);
assert!(
probe.stdout.contains("agent:dev"),
"the conventional agents/ item must still be discovered: {}",
probe.stdout
);
}
#[test]
fn source_flat_skills_directive_discovers_without_flag() {
let sb = make_flat_source("flatsrc-declared", Some("[source]\nflat-skills = true\n"));
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:alpha") && probe.stdout.contains("skill:beta"),
"[source].flat-skills must enable flat discovery without a flag: {}",
probe.stdout
);
}
#[test]
fn meld_flat_skills_ignored_for_authoritative_mindfile() {
let toml = "[[items]]\nkind = \"skill\"\nname = \"alpha\"\npath = \"alpha\"\n";
let sb = make_flat_source("flatsrc-auth", Some(toml));
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--flat-skills", "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("--flat-skills is ignored"),
"an authoritative mind.toml must note that --flat-skills is ignored: {}",
r.stdout
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:alpha") && !probe.stdout.contains("skill:beta"),
"authoritative mind.toml must ignore the flat layout: {}",
probe.stdout
);
}
#[test]
fn curator_flat_skills_applies_when_nested_has_no_mind_toml() {
let nested = make_flat_source("flat-nested", None);
let registry = Sandbox::bare("registry-flat");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
flat-skills = true\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
let probe = registry.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:alpha") && probe.stdout.contains("skill:beta"),
"curator flat-skills must govern discovery of the un-onboarded nested source: {}",
probe.stdout
);
}
#[test]
fn curator_flat_skills_ignored_with_warning_when_nested_has_mind_toml() {
let nested = make_flat_source(
"flat-onboarded",
Some("[source]\ndescription = \"onboarded\"\n"),
);
let registry = Sandbox::bare("registry-flat-gated");
registry.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\n\
source = \"{}\"\n\
flat-skills = true\n",
nested.source_spec()
),
);
let spec = registry.source_spec();
let r = registry.mind(&["meld", &spec]);
assert!(r.success, "meld should succeed: {} {}", r.stdout, r.stderr);
assert!(
r.stderr.contains("ships its own mind.toml")
&& r.stderr.contains("ignored")
&& r.stderr.contains("flat-onboarded"),
"a DSC-60 warning must be emitted naming the onboarded source: {}",
r.stderr
);
let probe = registry.mind(&["probe"]);
assert!(
!probe.stdout.contains("skill:alpha") && !probe.stdout.contains("skill:beta"),
"curator flat-skills must be suppressed: root-level skill dirs must not appear: {}",
probe.stdout
);
assert!(
probe.stdout.contains("agent:dev"),
"the nested source's conventional agents/ item must still be discovered: {}",
probe.stdout
);
}
#[test]
fn dump_emits_flat_skills_for_flat_source() {
let flat = make_flat_source("dump-flat", None);
let flat_spec = flat.source_spec();
assert!(
flat.mind(&["meld", &flat_spec, "--flat-skills", "--yes"])
.success
);
let dump = flat.mind(&["dump"]);
assert!(dump.success, "dump failed: {} {}", dump.stdout, dump.stderr);
assert!(
dump.stdout.contains("flat-skills = true"),
"dump must emit flat-skills = true for a flat source: {}",
dump.stdout
);
let normal = Sandbox::new();
let normal_spec = normal.source_spec();
assert!(normal.mind(&["meld", &normal_spec, "--yes"]).success);
let dump2 = normal.mind(&["dump"]);
assert!(
!dump2.stdout.contains("flat-skills"),
"a non-flat source must emit no flat-skills key: {}",
dump2.stdout
);
}
#[test]
fn meld_as_reserved_kind_word_is_rejected() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--as", "skill"]);
assert!(
!r.success,
"meld --as skill must fail: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("reserved") || r.stderr.contains("kind"),
"error must mention the reserved-kind-word problem: {}",
r.stderr
);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded"),
"source must not be registered after a rejected meld"
);
let sb2 = Sandbox::new();
let spec2 = sb2.source_spec();
let r2 = sb2.mind(&["meld", &spec2, "--as", "agent"]);
assert!(
!r2.success,
"meld --as agent must fail: stdout={} stderr={}",
r2.stdout, r2.stderr
);
assert!(
r2.stderr.contains("reserved") || r2.stderr.contains("kind"),
"error must mention the reserved-kind-word problem: {}",
r2.stderr
);
}
#[test]
fn upgrade_migrates_dash_separator_to_colon_by_stable_identity() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(
sb.mind(&["learn", "jk:review"]).success,
"learn must accept the colon-separated effective name"
);
let new_store = sb.mind_home.join("store/skill/jk:review");
let new_link = sb.claude_home.join("skills/jk:review");
assert!(
new_store.exists(),
"store must be at jk:review after install"
);
assert!(
std::fs::symlink_metadata(&new_link).is_ok(),
"symlink must be at skills/jk:review after install"
);
let manifest_path = sb.mind_home.join("manifest.json");
let manifest_text = std::fs::read_to_string(&manifest_path).unwrap();
let manifest_old = manifest_text
.replace("\"skill:jk:review\"", "\"skill:jk-review\"")
.replace("\"jk:review\"", "\"jk-review\"")
.replace("store/skill/jk:review", "store/skill/jk-review")
.replace("skills/jk:review", "skills/jk-review");
std::fs::write(&manifest_path, &manifest_old).unwrap();
let old_store = sb.mind_home.join("store/skill/jk-review");
std::fs::rename(&new_store, &old_store).unwrap();
std::fs::remove_file(&new_link).unwrap();
let old_link = sb.claude_home.join("skills/jk-review");
std::os::unix::fs::symlink(&old_store, &old_link).unwrap();
let up = sb.mind(&["upgrade", "--yes"]);
assert!(
up.success,
"upgrade must succeed on a separator-migration rename: {} {}",
up.stdout, up.stderr
);
assert!(
up.stdout.contains("jk-review") && up.stdout.contains("jk:review"),
"upgrade must report the rename jk-review -> jk:review: {}",
up.stdout
);
assert!(
sb.mind_home.join("store/skill/jk:review").exists(),
"store must be at jk:review after separator migration"
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("skills/jk:review")).is_ok(),
"symlink must be at skills/jk:review after separator migration"
);
assert!(
!sb.mind_home.join("store/skill/jk-review").exists(),
"old jk-review store must be removed after migration"
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("skills/jk-review")).is_err(),
"old jk-review symlink must be removed after migration"
);
let recall = sb.mind(&["recall"]).stdout;
assert!(
recall.contains("skill:jk:review"),
"recall must show skill:jk:review after migration: {recall}"
);
assert!(
!recall.contains("skill:jk-review"),
"recall must not show the old skill:jk-review after migration: {recall}"
);
}
#[test]
fn prefixed_effective_name_resolves_for_recall_upgrade_and_forget() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(
sb.mind(&["learn", "jk:review"]).success,
"learn must accept the colon-separated effective name"
);
let recall = sb.mind(&["recall", "jk:review"]);
assert!(
recall.success && recall.stdout.contains("jk:review"),
"recall jk:review must resolve the installed item: {} {}",
recall.stdout,
recall.stderr
);
let upgrade = sb.mind(&["upgrade", "jk:review", "--yes"]);
assert!(
upgrade.success,
"upgrade jk:review must resolve the installed item: {} {}",
upgrade.stdout, upgrade.stderr
);
assert!(
!upgrade.stderr.contains("not installed") && !upgrade.stdout.contains("not installed"),
"upgrade jk:review must not report the prefixed ref as not installed: {} {}",
upgrade.stdout,
upgrade.stderr
);
let forget = sb.mind(&["forget", "jk:review"]);
assert!(
forget.success,
"forget jk:review must resolve and remove the installed item: {} {}",
forget.stdout, forget.stderr
);
assert!(
!sb.mind_home.join("store/skill/jk:review").exists(),
"forget jk:review must remove the store copy"
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("skills/jk:review")).is_err(),
"forget jk:review must remove the lobe symlink"
);
let again = sb.mind(&["forget", "jk:review"]);
assert!(
!again.success
&& (again.stderr.contains("not installed") || again.stderr.contains("jk:review")),
"a second forget jk:review must report it as not installed: {} {}",
again.stdout,
again.stderr
);
}
#[test]
fn prefixed_lobe_symlink_resolves_through_colon_path() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:review"]).success);
let store_file = sb.mind_home.join("store/skill/jk:review/SKILL.md");
let link_file = sb.claude_home.join("skills/jk:review/SKILL.md");
let link_dir = sb.claude_home.join("skills/jk:review");
assert!(
std::fs::symlink_metadata(&link_dir)
.unwrap()
.file_type()
.is_symlink(),
"the prefixed lobe entry must be a symlink"
);
let via_store = std::fs::read_to_string(&store_file).expect("store file readable");
let via_link =
std::fs::read_to_string(&link_file).expect("file readable through the colon-bearing link");
assert_eq!(
via_link, via_store,
"content read through the `:` symlink must equal the store copy"
);
assert!(
via_link.contains("review skill"),
"the resolved file must be the review SKILL.md: {via_link}"
);
}
#[test]
fn dump_records_prefix_and_bare_install_items() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
assert!(sb.mind(&["learn", "jk:review"]).success);
let dump = sb.mind(&["dump"]);
assert!(dump.success, "dump failed: {} {}", dump.stdout, dump.stderr);
assert!(
dump.stdout.contains("namespace = \"jk\""),
"dump must record the effective prefix as `namespace = \"jk\"` (DSC-78): {}",
dump.stdout
);
assert!(
dump.stdout.contains("\"skill:review\""),
"install-items must list the BARE ref skill:review: {}",
dump.stdout
);
assert!(
!dump.stdout.contains("skill:jk:review") && !dump.stdout.contains("jk:review"),
"install-items must NOT carry the prefixed effective name: {}",
dump.stdout
);
let parsed: Result<toml::Value, _> = toml::from_str(&dump.stdout);
assert!(
parsed.is_ok(),
"dump output must be valid TOML: {:?}\n{}",
parsed.err(),
dump.stdout
);
}
#[test]
fn remeld_as_reserved_kind_word_is_rejected() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--as", "jk"]).success);
for word in ["rule", "tool"] {
let r = sb.mind(&["meld", &spec, "--as", word]);
assert!(
!r.success,
"re-meld --as {word} must fail: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("reserved") || r.stderr.contains("kind"),
"error must mention the reserved-kind-word problem for {word}: {}",
r.stderr
);
}
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:jk:review"),
"the original prefix must survive a rejected re-meld: {}",
probe.stdout
);
}
#[test]
fn marketplace_plugin_meld_discovers_skill_and_agent() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--link-only"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:acme-tools:greet"),
"plugin skill must appear in probe: {}",
probe.stdout
);
assert!(
probe.stdout.contains("agent:acme-tools:helper"),
"plugin agent must appear in probe: {}",
probe.stdout
);
assert!(
probe.stdout.contains("command:acme-tools:hello"),
"plugin command must appear in probe: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("rule:"),
"no rule items from a plugin: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("tool:"),
"no tool items from a plugin: {}",
probe.stdout
);
}
#[test]
fn marketplace_plugin_learn_installs_and_links() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let r = sb.mind(&["learn", "acme-tools:greet"]);
assert!(r.success, "learn failed: {} {}", r.stdout, r.stderr);
let link = sb.claude_home.join("skills/acme-tools:greet");
assert!(
std::fs::symlink_metadata(&link)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false),
"symlink must exist at claude_home/skills/acme-tools:greet"
);
}
#[test]
fn marketplace_plugin_skipped_components_note() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
combined.contains("not installed (no mind equivalent)"),
"meld must print the skipped-components note: {combined}"
);
assert!(
combined.contains("hook"),
"skipped-components note must name the unsupported kind: {combined}"
);
let note = combined
.lines()
.find(|l| l.contains("not installed (no mind equivalent)"))
.expect("the note line");
assert!(
!note.contains("command"),
"a plugin's commands are installed, so the skipped note must not name \
them: {note}"
);
assert!(
!note.contains("workflow"),
"a plugin's workflows are installed, so the skipped note must not name \
them: {note}"
);
}
#[test]
fn marketplace_plugin_command_installs_and_links() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let r = sb.mind(&["learn", "command:acme-tools:hello"]);
assert!(r.success, "learn command failed: {} {}", r.stdout, r.stderr);
let link = sb.claude_home.join("commands/acme-tools:hello.md");
assert!(
std::fs::symlink_metadata(&link)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false),
"the plugin command must be linked at commands/acme-tools:hello.md"
);
assert!(
std::fs::read_to_string(&link)
.expect("read the linked command")
.contains("Greet the current project"),
"the linked file must be the plugin's command"
);
}
#[test]
fn marketplace_plugin_workflow_installs_and_links() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("workflow:acme-tools:deploy"),
"the plugin workflow must appear in probe: {}",
probe.stdout
);
let r = sb.mind(&["learn", "workflow:acme-tools:deploy"]);
assert!(
r.success,
"learn workflow failed: {} {}",
r.stdout, r.stderr
);
let link = sb.claude_home.join("workflows/acme-tools:deploy.js");
assert!(
std::fs::symlink_metadata(&link)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false),
"the plugin workflow must be linked at workflows/acme-tools:deploy.js"
);
let body = std::fs::read_to_string(&link).expect("read the linked workflow");
assert!(
body.contains("name: 'acme-tools:deploy'"),
"the token must expand to the harness's <plugin>:<meta.name> spelling: {body}"
);
assert!(
!r.stderr.contains("the harness resolves it as"),
"an expanded token agrees with the installed name, so no WF-24 \
divergence warning: {}",
r.stderr
);
}
#[test]
fn marketplace_plugin_manifest_workflows_key_is_ignored() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--link-only"]);
assert!(
r.success,
"an unknown component-override key must not fail the meld: {} {}",
r.stdout, r.stderr
);
let manifest = std::fs::read_to_string(sb.source.join(".claude-plugin/plugin.json"))
.expect("read the fixture manifest");
assert!(
manifest.contains("\"workflows\""),
"the fixture must actually declare the override key: {manifest}"
);
assert!(
sb.source.join("elsewhere/workflows/release.js").is_file(),
"the declared path must be real, so this proves mind is choosing not \
to follow it rather than failing to find anything there"
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("workflow:acme-tools:deploy"),
"the convention workflows/ dir is scanned, not the declared path: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("workflow:acme-tools:release")
&& !probe.stdout.contains("workflow:release"),
"the workflow at the declared (but ignored) path must not appear: {}",
probe.stdout
);
assert!(
!sb.mind_home
.join("store/workflow/acme-tools:release")
.exists()
&& !sb.mind_home.join("store/workflow/release").exists(),
"the ignored-path workflow must never be installed"
);
for r#ref in ["release", "acme-tools:release", "workflow:release"] {
let learn = sb.mind(&["learn", r#ref, "--yes"]);
assert!(
!learn.success,
"`learn {ref}` must not resolve an item at the ignored path: {}\n{}",
learn.stdout,
learn.stderr,
r#ref = r#ref
);
}
assert!(
std::fs::symlink_metadata(sb.claude_home.join("workflows/release.js")).is_err()
&& std::fs::symlink_metadata(sb.claude_home.join("workflows/acme-tools:release.js"))
.is_err(),
"nothing may link for the workflow at the declared path"
);
}
#[test]
fn marketplace_plugin_ignored_override_path_holds_a_real_workflow() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--register-only", "--add-root", "elsewhere"]);
assert!(r.success, "meld --add-root: {}\n{}", r.stdout, r.stderr);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("workflow:acme-tools:release"),
"the workflow at the declared path is a real, discoverable item once a \
scan root reaches it: {}",
probe.stdout
);
assert!(
probe.stdout.contains("workflow:acme-tools:deploy"),
"and the plugin manifest's own items survive the added root (DSC-84): {}",
probe.stdout
);
let by_desc = sb.mind(&["probe", "--no-tui", "declared"]);
assert!(
by_desc.stdout.contains("workflow:acme-tools:release"),
"release's meta.description must be read, so the fixture is a real \
workflow and not an unreadable file: {}",
by_desc.stdout
);
let learn = sb.mind(&["learn", "workflow:acme-tools:release", "--yes"]);
assert!(learn.success, "learn: {}\n{}", learn.stdout, learn.stderr);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("workflows/acme-tools:release.js")).is_ok(),
"and it installs and links like any other workflow: {}",
learn.stdout
);
}
#[test]
fn marketplace_plugin_name_is_default_prefix_for_skills() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:acme-tools:greet"),
"skill must be prefixed with the plugin name: {}",
probe.stdout
);
let r = sb.mind(&["learn", "acme-tools:helper"]);
assert!(r.success, "learn agent failed: {} {}", r.stdout, r.stderr);
assert!(
sb.claude_home.join("agents/helper.md").exists(),
"agent link must be at agents/helper.md (bare harness name, not prefixed)"
);
assert!(
!sb.claude_home.join("agents/acme-tools:helper.md").exists(),
"no prefixed agent link must exist"
);
}
#[test]
fn marketplace_plugin_namespace_override_sets_prefix() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--namespace", "z", "--link-only"])
.success
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:z:greet"),
"consumer --namespace must override the plugin-name prefix: {}",
probe.stdout
);
let r = sb.mind(&["learn", "z:helper"]);
assert!(r.success, "learn agent failed: {} {}", r.stdout, r.stderr);
assert!(
sb.claude_home.join("agents/helper.md").exists(),
"agent link must be bare (agents/helper.md) even with a consumer namespace"
);
assert!(
!sb.claude_home.join("agents/z:helper.md").exists(),
"no prefixed agent link must exist"
);
}
#[test]
fn marketplace_plugin_namespace_empty_clears_prefix() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--namespace", "", "--link-only"])
.success
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:greet"),
"empty --namespace must clear the plugin-name prefix (skill is bare): {}",
probe.stdout
);
assert!(
!probe.stdout.contains("skill:acme-tools:greet"),
"plugin-name prefix must not appear after --namespace '': {}",
probe.stdout
);
}
#[test]
fn marketplace_plugin_description_and_version_recorded() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("Acme developer tools plugin"),
"plugin description must appear in recall --sources: {}",
sources.stdout
);
let jsrc = sb.mind(&["recall", "--sources", "--json"]);
assert!(jsrc.success, "{}", jsrc.stderr);
assert!(
jsrc.stdout.contains("1.0.0"),
"plugin_version must be present in JSON output: {}",
jsrc.stdout
);
}
#[test]
fn marketplace_plugin_description_strips_bidi_directional_and_zero_width() {
let sb = Sandbox::bare("quote-plugin");
sb.write_and_commit(
".claude-plugin/plugin.json",
"{\"name\":\"quoteplugin\",\"version\":\"1.0\",\
\"description\":\"Evil \u{202E}RTL\u{200E} zerowidth\u{200B} end\"}",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("Evil RTL zerowidth end"),
"the sanitized description text must survive: {}",
sources.stdout
);
assert!(
!sources.stdout.contains('\u{202E}')
&& !sources.stdout.contains('\u{200E}')
&& !sources.stdout.contains('\u{200B}'),
"the bidi override, directional mark, and zero-width character must \
all be stripped from plain-text output: {:?}",
sources.stdout
);
let jsrc = sb.mind(&["recall", "--sources", "--json"]);
assert!(jsrc.success, "{}", jsrc.stderr);
assert!(
jsrc.stdout.contains("Evil RTL zerowidth end"),
"the sanitized description text must survive in --json: {}",
jsrc.stdout
);
assert!(
!jsrc.stdout.contains('\u{202E}')
&& !jsrc.stdout.contains('\u{200E}')
&& !jsrc.stdout.contains('\u{200B}'),
"the bidi override, directional mark, and zero-width character must \
all be stripped from --json output: {:?}",
jsrc.stdout
);
}
#[test]
fn marketplace_catalog_melds_in_repo_plugins() {
let sb = Sandbox::from_example("marketplace-catalog");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("marketplace-catalog"),
"catalog source must appear: {}",
sources.stdout
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:alpha:one"),
"alpha's skill must appear in probe: {}",
probe.stdout
);
assert!(
probe.stdout.contains("agent:beta:two"),
"beta's agent must appear in probe (prefixed effective name): {}",
probe.stdout
);
assert!(
probe.stdout.contains("command:beta:ship"),
"beta's command must appear in probe: {}",
probe.stdout
);
assert!(
probe.stdout.contains("workflow:beta:ship-it"),
"beta's workflow must appear in probe: {}",
probe.stdout
);
let r = sb.mind(&["learn", "alpha:one"]);
assert!(
r.success,
"learn alpha:one failed: {} {}",
r.stdout, r.stderr
);
assert!(
sb.claude_home.join("skills/alpha:one").exists(),
"skill alpha:one must be symlinked into the lobe"
);
let r = sb.mind(&["learn", "beta:two"]);
assert!(
r.success,
"learn beta:two failed: {} {}",
r.stdout, r.stderr
);
assert!(
sb.claude_home.join("agents/two.md").exists(),
"beta's agent link must be at agents/two.md (bare harness name)"
);
let r = sb.mind(&["learn", "command:beta:ship"]);
assert!(
r.success,
"learn beta's command failed: {} {}",
r.stdout, r.stderr
);
assert!(
sb.claude_home.join("commands/beta:ship.md").exists(),
"beta's command link must be at commands/beta:ship.md"
);
let r = sb.mind(&["learn", "workflow:beta:ship-it"]);
assert!(
r.success,
"learn beta's workflow failed: {} {}",
r.stdout, r.stderr
);
let link = sb.claude_home.join("workflows/beta:ship-it.js");
assert!(
link.exists(),
"beta's workflow link must be at workflows/beta:ship-it.js"
);
let body = std::fs::read_to_string(&link).expect("read the linked workflow");
assert!(
body.contains("name: 'beta:ship-it'"),
"the token must expand to the entry-prefixed name: {body}"
);
}
#[test]
fn marketplace_catalog_probe_hint_fires() {
let sb = Sandbox::from_example("marketplace-catalog");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
combined.contains("mind probe"),
"meld of a marketplace catalog must print the probe hint: {combined}"
);
}
#[test]
fn marketplace_catalog_external_plugin_registers() {
let extplugin = Sandbox::bare("extplugin");
extplugin.write_and_commit(
".claude-plugin/plugin.json",
r#"{"name":"ext","version":"0.1","description":"External test plugin"}"#,
);
extplugin.write_and_commit(
"skills/extskill/SKILL.md",
"---\nname: extskill\ndescription: External skill\n---\n# extskill\n",
);
let catalog = Sandbox::bare("ext-catalog");
let ext_url = format!("file://{}", extplugin.source_spec());
catalog.write_and_commit(
".claude-plugin/marketplace.json",
&format!(r#"{{"name":"ext-market","plugins":[{{"name":"ext","source":"{ext_url}"}}]}}"#),
);
let cat_spec = catalog.source_spec();
let r = catalog.mind(&["meld", &cat_spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let sources = catalog.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("extplugin"),
"external plugin sub-source must be registered: {}",
sources.stdout
);
let probe = catalog.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:ext:extskill"),
"external plugin's skill must appear in probe: {}",
probe.stdout
);
}
#[test]
fn marketplace_yes_auto_installs_in_repo_plugins() {
let sb = Sandbox::from_example("marketplace-catalog");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--yes"]);
assert!(r.success, "meld --yes failed: {} {}", r.stdout, r.stderr);
assert!(
sb.claude_home.join("skills/alpha:one").exists(),
"alpha's skill must be auto-installed under --yes: lobe = {}",
sb.claude_home.display()
);
assert!(
sb.claude_home.join("agents/two.md").exists(),
"beta's agent must be auto-installed under --yes (bare harness name)"
);
}
#[test]
fn marketplace_external_plugin_installs_only_under_recursive() {
let extplugin = Sandbox::bare("extplugin2");
extplugin.write_and_commit(
".claude-plugin/plugin.json",
r#"{"name":"ext","version":"0.1","description":"External test plugin"}"#,
);
extplugin.write_and_commit(
"skills/extskill/SKILL.md",
"---\nname: extskill\ndescription: External skill\n---\n# extskill\n",
);
let catalog = Sandbox::bare("ext-catalog2");
let ext_url = format!("file://{}", extplugin.source_spec());
catalog.write_and_commit(
".claude-plugin/marketplace.json",
&format!(r#"{{"name":"ext-market","plugins":[{{"name":"ext","source":"{ext_url}"}}]}}"#),
);
let cat_spec = catalog.source_spec();
let r = catalog.mind(&["meld", &cat_spec, "--yes"]);
assert!(r.success, "meld --yes failed: {} {}", r.stdout, r.stderr);
assert!(
!catalog.claude_home.join("skills/ext:extskill").exists(),
"external plugin must be register-only without --recursive"
);
let r = catalog.mind(&["meld", &cat_spec, "--recursive", "--yes"]);
assert!(
r.success,
"remeld --recursive --yes failed: {} {}",
r.stdout, r.stderr
);
assert!(
catalog.claude_home.join("skills/ext:extskill").exists(),
"external plugin's skill must install under --recursive"
);
}
#[test]
fn marketplace_entry_name_wins_over_plugin_json_name() {
let catalog = Sandbox::bare("mkt8-catalog");
catalog.write_and_commit(
"plugins/myplugin/.claude-plugin/plugin.json",
r#"{"name":"original-name","version":"0.1"}"#,
);
catalog.write_and_commit(
"plugins/myplugin/skills/theskill/SKILL.md",
"---\nname: theskill\ndescription: A skill\n---\n# theskill\n",
);
catalog.write_and_commit(
".claude-plugin/marketplace.json",
r#"{"name":"test","plugins":[{"name":"override-name","source":"./plugins/myplugin"}]}"#,
);
let spec = catalog.source_spec();
assert!(catalog.mind(&["meld", &spec]).success);
let probe = catalog.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:override-name:theskill"),
"entry name must override plugin.json name as the prefix: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("skill:original-name:theskill"),
"plugin.json name must not appear as prefix when entry overrides it: {}",
probe.stdout
);
}
#[test]
fn marketplace_unsafe_in_repo_path_fails_meld() {
let catalog = Sandbox::bare("unsafe-mkt");
catalog.write_and_commit(
".claude-plugin/marketplace.json",
r#"{"name":"bad","plugins":[{"name":"evil","source":"../escape"}]}"#,
);
let spec = catalog.source_spec();
let r = catalog.mind(&["meld", &spec]);
assert!(!r.success, "unsafe path must cause meld to fail");
assert!(
r.stderr.contains("unsafe") || r.stderr.contains("..") || r.stderr.contains("escape"),
"error must describe the path safety violation: {}",
r.stderr
);
}
#[test]
fn marketplace_malformed_plugin_json_fails_meld() {
let sb = Sandbox::bare("bad-plugin-json");
sb.write_and_commit(".claude-plugin/plugin.json", "{not valid json at all");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(!r.success, "malformed plugin.json must cause meld to fail");
assert!(
r.stderr.contains("plugin.json") || r.stderr.contains("invalid"),
"error must indicate the invalid plugin.json: {}",
r.stderr
);
}
#[test]
fn marketplace_malformed_marketplace_json_fails_meld() {
let sb = Sandbox::bare("bad-market-json");
sb.write_and_commit(".claude-plugin/marketplace.json", "{bad json");
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(
!r.success,
"malformed marketplace.json must cause meld to fail"
);
assert!(
r.stderr.contains("marketplace.json") || r.stderr.contains("invalid"),
"error must indicate the invalid marketplace.json: {}",
r.stderr
);
}
#[test]
fn marketplace_ansi_escape_stripped_in_recall_sources() {
let sb = Sandbox::bare("ansi-plugin");
sb.write_and_commit(
".claude-plugin/plugin.json",
"{\"name\":\"ansi-p\",\"description\":\"\\u001b[31mred text\\u001b[0m safe\"}",
);
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
!sources.stdout.contains('\x1b'),
"ANSI escape bytes must be stripped from recall --sources output: {:?}",
sources.stdout
);
assert!(
sources.stdout.contains("safe") || sources.stdout.contains("red text"),
"stripped text must still be visible: {}",
sources.stdout
);
}
#[test]
fn marketplace_plugin_origin_label_in_recall_sources() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--link-only"]).success);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("origin:claude-plugin"),
"recall --sources must label a plugin source with origin:claude-plugin: {}",
sources.stdout
);
}
#[test]
fn marketplace_catalog_origin_label_in_recall_sources() {
let sb = Sandbox::from_example("marketplace-catalog");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success);
let sources = sb.mind(&["recall", "--sources"]);
assert!(
sources.stdout.contains("origin:claude-marketplace"),
"recall --sources must label a marketplace source with origin:claude-marketplace: {}",
sources.stdout
);
}
#[test]
fn mind_toml_suppresses_plugin_manifest_with_note() {
let sb = Sandbox::bare("toml-wins");
sb.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"skill\"\nname = \"toml-skill\"\npath = \"skills/toml-skill/SKILL.md\"\n",
);
sb.write_and_commit(
"skills/toml-skill/SKILL.md",
"---\nname: toml-skill\ndescription: From mind.toml\n---\n# toml\n",
);
sb.write_and_commit(
".claude-plugin/plugin.json",
r#"{"name":"plugin-name","version":"1.0","description":"Plugin desc"}"#,
);
sb.write_and_commit(
"skills/plugin-skill/SKILL.md",
"---\nname: plugin-skill\ndescription: From plugin\n---\n# plugin\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld must succeed: {} {}", r.stdout, r.stderr);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
combined.contains("authoritative mind.toml")
|| combined.contains(".claude-plugin/ manifest is ignored"),
"meld must print the advisory note about mind.toml suppressing plugin manifest: {combined}"
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:toml-skill"),
"mind.toml item must appear in probe: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("skill:plugin-skill"),
"plugin item must not appear (mind.toml is authoritative): {}",
probe.stdout
);
assert!(
!probe.stdout.contains("skill:plugin-name:"),
"plugin-name prefix must not appear: {}",
probe.stdout
);
}
#[test]
fn marketplace_plugin_does_not_emit_claude_plugin_manifest() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--yes"]).success);
let store_dir = sb.mind_home.join("store");
if store_dir.is_dir() {
for entry in std::fs::read_dir(&store_dir).unwrap().flatten() {
let p = entry.path();
assert!(
!p.join(".claude-plugin").exists(),
"store entry must not contain .claude-plugin: {:?}",
p
);
}
}
assert!(
!sb.claude_home.join(".claude-plugin").exists(),
"lobe must not contain a .claude-plugin dir"
);
let dump = sb.mind(&["dump"]);
assert!(
!dump.stdout.contains(".claude-plugin"),
"dump output must contain no .claude-plugin reference: {}",
dump.stdout
);
}
#[test]
fn marketplace_entry_version_wins_over_plugin_json_version() {
let catalog = Sandbox::bare("mkt8-version");
catalog.write_and_commit(
"plugins/myplugin/.claude-plugin/plugin.json",
r#"{"name":"myplugin","version":"9.9.9"}"#,
);
catalog.write_and_commit(
"plugins/myplugin/skills/theskill/SKILL.md",
"---\nname: theskill\ndescription: A skill\n---\n# theskill\n",
);
catalog.write_and_commit(
".claude-plugin/marketplace.json",
r#"{"name":"test","plugins":[{"name":"override","source":"./plugins/myplugin","version":"1.1.1"}]}"#,
);
let spec = catalog.source_spec();
let r = catalog.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = catalog.mind(&["probe", "--no-tui"]);
assert!(
probe.stdout.contains("override:theskill"),
"skill must appear under entry name as prefix (MKT-8): {}",
probe.stdout
);
}
#[test]
fn mind_toml_suppresses_marketplace_manifest_with_note() {
let catalog = Sandbox::bare("mkt2-market-suppress");
catalog.write_and_commit(
"rules/my-rule.md",
"---\ndescription: my rule\n---\n# rule\n",
);
catalog.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"rule\"\nname = \"my-rule\"\npath = \"rules/my-rule.md\"\n",
);
catalog.write_and_commit(
"plugins/embedded/.claude-plugin/plugin.json",
r#"{"name":"embedded","version":"0.1"}"#,
);
catalog.write_and_commit(
"plugins/embedded/skills/embskill/SKILL.md",
"---\nname: embskill\ndescription: embedded skill\n---\n# embskill\n",
);
catalog.write_and_commit(
".claude-plugin/marketplace.json",
r#"{"name":"cat","plugins":[{"name":"embedded","source":"./plugins/embedded"}]}"#,
);
let spec = catalog.source_spec();
let r = catalog.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
combined.contains("authoritative mind.toml")
|| combined.contains(".claude-plugin/ manifest is ignored"),
"meld must print the advisory note for a suppressed marketplace manifest: {combined}"
);
let sources = catalog.mind(&["recall", "--sources"]);
assert!(
!sources.stdout.contains("embedded"),
"the marketplace's in-repo plugin must NOT be melded when mind.toml is authoritative: {}",
sources.stdout
);
let probe = catalog.mind(&["probe"]);
assert!(
probe.stdout.contains("rule:my-rule"),
"the mind.toml rule must be present: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("embskill"),
"the suppressed plugin's skill must not appear: {}",
probe.stdout
);
}
#[test]
fn marketplace_two_plugins_same_agent_name_collide() {
let catalog = Sandbox::bare("mkt-agent-collide");
catalog.write_and_commit("plugins/a/.claude-plugin/plugin.json", r#"{"name":"pa"}"#);
catalog.write_and_commit(
"plugins/a/agents/from-a.md",
"---\nname: shared\ndescription: agent from A\n---\n# a\n",
);
catalog.write_and_commit("plugins/b/.claude-plugin/plugin.json", r#"{"name":"pb"}"#);
catalog.write_and_commit(
"plugins/b/agents/from-b.md",
"---\nname: shared\ndescription: agent from B\n---\n# b\n",
);
catalog.write_and_commit(
".claude-plugin/marketplace.json",
r#"{"name":"cat","plugins":[{"name":"pa","source":"./plugins/a"},{"name":"pb","source":"./plugins/b"}]}"#,
);
let spec = catalog.source_spec();
assert!(catalog.mind(&["meld", &spec]).success);
let r1 = catalog.mind(&["learn", "pa:from-a"]);
assert!(
r1.success,
"first agent must install: {} {}",
r1.stdout, r1.stderr
);
assert!(
catalog.claude_home.join("agents/shared.md").exists(),
"first agent must link at agents/shared.md"
);
let r2 = catalog.mind(&["learn", "pb:from-b"]);
assert!(
!r2.success,
"a second agent colliding at agents/shared.md must be refused (NS-41): {} {}",
r2.stdout, r2.stderr
);
assert!(
r2.stderr.to_lowercase().contains("collid") || r2.stderr.to_lowercase().contains("shared"),
"the error must describe the agent collision: {}",
r2.stderr
);
}
#[test]
fn marketplace_sync_rewalk_registers_new_entry() {
let catalog = Sandbox::bare("mkt-sync-rewalk");
catalog.write_and_commit(
"plugins/first/.claude-plugin/plugin.json",
r#"{"name":"first"}"#,
);
catalog.write_and_commit(
"plugins/first/skills/oneskill/SKILL.md",
"---\nname: oneskill\ndescription: skill one\n---\n# one\n",
);
catalog.write_and_commit(
".claude-plugin/marketplace.json",
r#"{"name":"cat","plugins":[{"name":"first","source":"./plugins/first"}]}"#,
);
let spec = catalog.source_spec();
assert!(catalog.mind(&["meld", &spec]).success);
let before = catalog.mind(&["recall", "--sources"]);
assert!(
!before.stdout.contains("second"),
"second must not exist before it is added: {}",
before.stdout
);
catalog.write_and_commit(
"plugins/second/.claude-plugin/plugin.json",
r#"{"name":"second"}"#,
);
catalog.write_and_commit(
"plugins/second/skills/twoskill/SKILL.md",
"---\nname: twoskill\ndescription: skill two\n---\n# two\n",
);
catalog.write_and_commit(
".claude-plugin/marketplace.json",
r#"{"name":"cat","plugins":[{"name":"first","source":"./plugins/first"},{"name":"second","source":"./plugins/second"}]}"#,
);
let sync = catalog.mind(&["sync"]);
assert!(sync.success, "sync failed: {} {}", sync.stdout, sync.stderr);
let after = catalog.mind(&["recall", "--sources"]);
assert!(
!after.stdout.contains("/second") && !after.stdout.contains("second "),
"second must not be a registered sub-source (MKT-14): {}",
after.stdout
);
let probe = catalog.mind(&["probe"]);
assert!(
probe.stdout.contains("twoskill"),
"the newly-added plugin's skill must be discoverable after sync: {}",
probe.stdout
);
}
#[test]
fn nested_source_plugin_inherits_plugin_name_as_namespace() {
let plugin_repo = Sandbox::bare("mkt12-plugin");
plugin_repo.write_and_commit(
"skills/greet/SKILL.md",
"---\nname: greet\ndescription: Plugin greet skill\n---\n# greet\n",
);
plugin_repo.write_and_commit(
".claude-plugin/plugin.json",
r#"{"name":"myplugin","version":"1.0"}"#,
);
let super_src = Sandbox::bare("mkt12-super");
super_src.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\nsource = \"{}\"\n",
plugin_repo.source_spec()
),
);
let spec = super_src.source_spec();
let r = super_src.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = super_src.mind(&["probe", "--no-tui"]);
assert!(
probe.stdout.contains("myplugin:greet"),
"nested plugin source must use plugin name as default prefix (MKT-12): {}",
probe.stdout
);
}
#[test]
fn nested_source_marketplace_preserves_per_plugin_namespacing() {
let market_repo = Sandbox::bare("mkt13-market");
market_repo.write_and_commit(
"plugins/alpha/.claude-plugin/plugin.json",
r#"{"name":"alpha"}"#,
);
market_repo.write_and_commit(
"plugins/alpha/skills/search/SKILL.md",
"---\nname: search\ndescription: Alpha search skill\n---\n# search\n",
);
market_repo.write_and_commit(
"plugins/beta/.claude-plugin/plugin.json",
r#"{"name":"beta"}"#,
);
market_repo.write_and_commit(
"plugins/beta/skills/search/SKILL.md",
"---\nname: search\ndescription: Beta search skill\n---\n# search\n",
);
market_repo.write_and_commit(
".claude-plugin/marketplace.json",
r#"{"name":"TestMarket","plugins":[{"name":"alpha","source":"./plugins/alpha"},{"name":"beta","source":"./plugins/beta"}]}"#,
);
let super_src = Sandbox::bare("mkt13-super");
super_src.write_and_commit(
"mind.toml",
&format!(
"[[discover.sources]]\nsource = \"{}\"\n",
market_repo.source_spec()
),
);
let spec = super_src.source_spec();
let r = super_src.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = super_src.mind(&["probe", "--no-tui"]);
assert!(
probe.stdout.contains("alpha:search"),
"alpha's skill must be namespaced under 'alpha' through the nested chain (MKT-13): {}",
probe.stdout
);
assert!(
probe.stdout.contains("beta:search"),
"beta's skill must be namespaced under 'beta' through the nested chain (MKT-13): {}",
probe.stdout
);
}
#[test]
fn skill_collision_non_interactive_errors_with_guidance() {
let src_a = Sandbox::bare("ns43-a");
src_a.write_and_commit(
"skills/deploy/SKILL.md",
"---\nname: deploy\ndescription: Deploy skill A\n---\n# deploy\n",
);
let r = src_a.mind(&["meld", &src_a.source_spec(), "--yes"]);
assert!(r.success, "meld source-a failed: {} {}", r.stdout, r.stderr);
let src_b = Sandbox::bare("ns43-b");
src_b.write_and_commit(
"skills/deploy/SKILL.md",
"---\nname: deploy\ndescription: Deploy skill B\n---\n# deploy\n",
);
let r = src_a.mind(&["meld", &src_b.source_spec(), "--yes"]);
assert!(
!r.success,
"colliding meld must fail in non-interactive mode (NS-45)"
);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
combined.contains("collision")
|| combined.contains("conflict")
|| combined.contains("namespace"),
"error must describe the collision and suggest --namespace (NS-43 NS-45): {combined}"
);
let r = src_a.mind(&["meld", &src_b.source_spec(), "--namespace", "sb", "--yes"]);
assert!(
r.success,
"meld with --namespace must resolve the collision: {} {}",
r.stdout, r.stderr
);
let probe = src_a.mind(&["probe", "--no-tui"]);
assert!(
probe.stdout.contains("sb:deploy"),
"namespaced skill sb:deploy must appear in probe: {}",
probe.stdout
);
}
#[test]
fn skill_collision_same_source_remeld_is_not_a_collision() {
let src = Sandbox::bare("ns43-remeld");
src.write_and_commit(
"skills/deploy/SKILL.md",
"---\nname: deploy\ndescription: Deploy skill\n---\n# deploy\n",
);
let r = src.mind(&["meld", &src.source_spec(), "--yes"]);
assert!(
r.success,
"first meld must succeed: {} {}",
r.stdout, r.stderr
);
let r = src.mind(&["unmeld", "ns43-remeld", "--unlink-only"]);
assert!(
r.success,
"unmeld --unlink-only must succeed: {} {}",
r.stdout, r.stderr
);
let r = src.mind(&["meld", &src.source_spec(), "--yes"]);
assert!(
r.success,
"re-meld of same source must succeed (NS-43 same-source skip): {} {}",
r.stdout, r.stderr
);
}
#[test]
fn skill_collision_with_namespace_set_does_not_collide() {
let src_a = Sandbox::bare("ns43-ns-a");
src_a.write_and_commit(
"skills/deploy/SKILL.md",
"---\nname: deploy\ndescription: Deploy skill A\n---\n# deploy\n",
);
let r = src_a.mind(&["meld", &src_a.source_spec(), "--yes"]);
assert!(r.success, "meld source-a failed: {} {}", r.stdout, r.stderr);
let src_b = Sandbox::bare("ns43-ns-b");
src_b.write_and_commit(
"skills/deploy/SKILL.md",
"---\nname: deploy\ndescription: Deploy skill B\n---\n# deploy\n",
);
let r = src_a.mind(&["meld", &src_b.source_spec(), "--namespace", "sb", "--yes"]);
assert!(
r.success,
"meld with --namespace must succeed when effective names are distinct (NS-43): {} {}",
r.stdout, r.stderr
);
let probe = src_a.mind(&["probe", "--no-tui"]);
assert!(
probe.stdout.contains("sb:deploy"),
"namespaced sb:deploy must appear in probe: {}",
probe.stdout
);
}
#[test]
fn marketplace_same_root_multiple_plugins_with_skills_array() {
let sb = Sandbox::bare("mkt14-same-root");
sb.write_and_commit(
"skills/xlsx/SKILL.md",
"---\nname: xlsx\ndescription: xlsx\n---\n# xlsx\n",
);
sb.write_and_commit(
"skills/docx/SKILL.md",
"---\nname: docx\ndescription: docx\n---\n# docx\n",
);
sb.write_and_commit(
"skills/art/SKILL.md",
"---\nname: art\ndescription: art\n---\n# art\n",
);
sb.write_and_commit(
"skills/design/SKILL.md",
"---\nname: design\ndescription: design\n---\n# design\n",
);
sb.write_and_commit(
"skills/claude-api/SKILL.md",
"---\nname: claude-api\ndescription: api\n---\n# api\n",
);
sb.write_and_commit(
".claude-plugin/marketplace.json",
r#"{
"name": "anthropic-agent-skills",
"plugins": [
{"name": "document-skills", "source": "./", "skills": ["./skills/xlsx", "./skills/docx"]},
{"name": "example-skills", "source": "./", "skills": ["./skills/art", "./skills/design"]},
{"name": "api-skills", "source": "./", "skills": ["./skills/claude-api"]}
]
}"#,
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = sb.mind(&["probe", "--no-tui"]);
assert!(
probe.stdout.contains("document-skills:xlsx"),
"expected document-skills:xlsx: {}",
probe.stdout
);
assert!(
probe.stdout.contains("document-skills:docx"),
"expected document-skills:docx: {}",
probe.stdout
);
assert!(
probe.stdout.contains("example-skills:art"),
"expected example-skills:art: {}",
probe.stdout
);
assert!(
probe.stdout.contains("example-skills:design"),
"expected example-skills:design: {}",
probe.stdout
);
assert!(
probe.stdout.contains("api-skills:claude-api"),
"expected api-skills:claude-api: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("example-skills:xlsx"),
"xlsx must not appear under example-skills: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("document-skills:art"),
"art must not appear under document-skills: {}",
probe.stdout
);
}
#[test]
fn marketplace_forget_removes_plugin_installed_item() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--link-only"]).success,
"meld --link-only must succeed"
);
let learn = sb.mind(&["learn", "acme-tools:greet"]);
assert!(
learn.success,
"learn must succeed: {} {}",
learn.stdout, learn.stderr
);
let r = sb.mind(&["forget", "skill:acme-tools:greet"]);
assert!(r.success, "forget must succeed: {} {}", r.stdout, r.stderr);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("skills/acme-tools:greet")).is_err(),
"symlink must be removed after forget"
);
assert!(
!sb.mind(&["recall", "skill:acme-tools:greet"]).success,
"recall must fail after forget (item no longer installed)"
);
}
#[test]
fn marketplace_upgrade_plugin_sourced_item() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--link-only"]).success,
"meld --link-only must succeed"
);
let learn = sb.mind(&["learn", "acme-tools:greet"]);
assert!(
learn.success,
"learn must succeed: {} {}",
learn.stdout, learn.stderr
);
let before = sb.mind(&["recall", "skill:acme-tools:greet"]).stdout;
sb.write_and_commit(
"skills/greet/SKILL.md",
"---\nname: greet\ndescription: Greet\n---\n# greet (updated)\n",
);
assert!(
sb.mind(&["sync"]).success,
"sync must succeed after source update"
);
let r = sb.mind(&["upgrade", "--yes"]);
assert!(
r.success,
"upgrade --yes must succeed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("upgraded skill:acme-tools:greet"),
"upgrade must report the item as upgraded: {}",
r.stdout
);
let after = sb.mind(&["recall", "skill:acme-tools:greet"]).stdout;
assert_ne!(
before, after,
"recall output must differ after upgrade (hash must advance)"
);
}
#[test]
fn marketplace_introspect_fix_relinks_plugin_item() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--link-only"]).success,
"meld --link-only must succeed"
);
let learn = sb.mind(&["learn", "acme-tools:greet"]);
assert!(
learn.success,
"learn must succeed: {} {}",
learn.stdout, learn.stderr
);
let link = sb.claude_home.join("skills/acme-tools:greet");
assert!(
std::fs::symlink_metadata(&link).is_ok(),
"symlink must exist before the test removes it"
);
std::fs::remove_file(&link).unwrap();
let r = sb.mind(&["introspect", "--fix"]);
assert!(
r.success,
"introspect --fix must succeed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("relinked"),
"introspect --fix must report the link was recreated: {}",
r.stdout
);
assert!(
std::fs::symlink_metadata(&link).is_ok(),
"symlink must be restored by introspect --fix"
);
assert!(
sb.mind(&["introspect"]).stdout.contains("all good"),
"introspect must be clean after --fix"
);
}
#[test]
fn marketplace_recall_shows_installed_plugin_item() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--link-only"]).success,
"meld --link-only must succeed"
);
let learn = sb.mind(&["learn", "acme-tools:greet"]);
assert!(
learn.success,
"learn must succeed: {} {}",
learn.stdout, learn.stderr
);
let list = sb.mind(&["recall"]);
assert!(
list.success,
"recall must succeed: {} {}",
list.stdout, list.stderr
);
assert!(
list.stdout.contains("skill:acme-tools:greet"),
"recall must list the installed plugin skill by its prefixed name: {}",
list.stdout
);
}
#[test]
fn marketplace_source_only_mind_toml_composes_with_plugin() {
let sb = Sandbox::bare("compose-toml");
sb.write_and_commit("mind.toml", "[source]\nprefix = \"custom\"\n");
sb.write_and_commit(
".claude-plugin/plugin.json",
r#"{"name":"plugin-name","version":"1.0"}"#,
);
sb.write_and_commit(
"skills/greet/SKILL.md",
"---\nname: greet\ndescription: Greet\n---\n# greet\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--link-only"]);
assert!(
r.success,
"meld --link-only must succeed: {} {}",
r.stdout, r.stderr
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:custom:greet"),
"mind.toml prefix must win over plugin name: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("skill:plugin-name:greet"),
"plugin name must not be used as prefix when mind.toml supplies one: {}",
probe.stdout
);
}
#[test]
fn marketplace_mind_toml_roots_suppress_manifest_own_items_with_note() {
let sb = Sandbox::bare("mkt15-roots");
sb.write_and_commit(".claude-plugin/plugin.json", "{\n \"name\": \"acme\"\n}\n");
sb.write_and_commit(
"skills/greet/SKILL.md",
"---\nname: greet\ndescription: plugin greet\n---\n# greet\n",
);
sb.write_and_commit("mind.toml", "[source]\nroots = [\"pkg\"]\n");
sb.write_and_commit(
"pkg/skills/own/SKILL.md",
"---\nname: own\ndescription: convention own skill\n---\n# own\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--link-only"]);
assert!(r.success, "meld must succeed: {} {}", r.stdout, r.stderr);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
combined.contains("plugin components are ignored"),
"meld must note the manifest's own-item layer is ignored: {combined}"
);
let probe = sb.mind(&["probe"]).stdout;
assert!(
probe.contains("skill:own"),
"convention skill under the declared root must be discovered: {probe}"
);
assert!(
!probe.contains("greet"),
"the plugin manifest's own items must be suppressed by the roots layout: {probe}"
);
}
#[test]
fn marketplace_consumer_root_flag_suppresses_manifest_with_note() {
let sb = Sandbox::bare("mkt15-consumer-root");
sb.write_and_commit(".claude-plugin/plugin.json", "{\n \"name\": \"acme\"\n}\n");
sb.write_and_commit(
"skills/greet/SKILL.md",
"---\nname: greet\ndescription: plugin greet\n---\n# greet\n",
);
sb.write_and_commit(
"pkg/skills/own/SKILL.md",
"---\nname: own\ndescription: convention own skill\n---\n# own\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--root", "pkg", "--link-only"]);
assert!(r.success, "meld must succeed: {} {}", r.stdout, r.stderr);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
combined.contains("plugin components are ignored")
&& combined.contains("--root/--flat-skills"),
"meld must note the manifest is ignored and name the consumer flag: {combined}"
);
let probe = sb.mind(&["probe"]).stdout;
assert!(
probe.contains("skill:own"),
"convention skill under the consumer root must be discovered: {probe}"
);
assert!(
!probe.contains("greet"),
"the plugin manifest's own items must be suppressed by --root: {probe}"
);
}
#[test]
fn marketplace_and_curator_compose_via_discover_sources() {
let curator = Sandbox::bare("curator");
let nested = Sandbox::named("curated"); curator.write_and_commit(
".claude-plugin/marketplace.json",
r#"{ "name": "M", "plugins": [ {"name": "toolkit", "source": "./plugins/toolkit"} ] }"#,
);
curator.write_and_commit(
"plugins/toolkit/skills/format/SKILL.md",
"---\nname: format\ndescription: fmt\n---\n# format\n",
);
curator.write_and_commit(
"mind.toml",
&format!(
"[source]\ndescription = \"marketplace + curator\"\n\n[discover]\nsources = [{{ source = \"{}\" }}]\n",
nested.source_spec()
),
);
let spec = curator.source_spec();
let r = curator.mind(&["meld", &spec, "--link-only"]);
assert!(r.success, "meld must succeed: {} {}", r.stdout, r.stderr);
let probe = curator.mind(&["probe"]).stdout;
assert!(
probe.contains("toolkit:format"),
"the marketplace in-repo plugin item must be discovered (not suppressed by [discover].sources): {probe}"
);
let sources = curator.mind(&["recall", "--sources"]).stdout;
assert!(
sources.contains("/curated"),
"the curated nested source must be registered alongside the marketplace: {sources}"
);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
!combined.contains("manifest's plugin components are ignored")
&& !combined.contains("manifest is ignored"),
"a [discover].sources-only mind.toml must not suppress the manifest: {combined}"
);
}
#[test]
fn example_marketplace_curator_validates() {
let sb = Sandbox::new();
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/marketplace-curator");
let r = sb.mind(&["review", dir.to_str().unwrap()]);
assert!(
r.success,
"marketplace-curator example must validate clean:\nstdout: {}\nstderr: {}",
r.stdout, r.stderr
);
}
#[test]
fn marketplace_plugin_with_only_unsupported_components() {
let sb = Sandbox::bare("no-items-plugin");
sb.write_and_commit(
".claude-plugin/plugin.json",
r#"{"name":"hooks-only","version":"1.0"}"#,
);
sb.write_and_commit("hooks/post.sh", "#!/bin/sh\n");
sb.write_and_commit("commands/do.sh", "#!/bin/sh\n");
sb.write_and_commit(
"commands/frontend/component.md",
"---\ndescription: nested\n---\n# component\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(
r.success,
"meld of a plugin with only unsupported components must succeed: {} {}",
r.stdout, r.stderr
);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
combined.contains("not installed (no mind equivalent)"),
"meld must print the skipped-components note: {combined}"
);
assert!(
combined.contains("2 unmapped commands/ entries"),
"the note must count the commands/ entries the flat .md scan left \
behind, or MKT-4's 'never a silent drop' is false: {combined}"
);
let probe = sb.mind(&["probe"]);
assert!(
!probe.stdout.contains("skill:")
&& !probe.stdout.contains("agent:")
&& !probe.stdout.contains("rule:")
&& !probe.stdout.contains("command:")
&& !probe.stdout.contains("tool:"),
"a hooks-only plugin must contribute zero items to probe: {}",
probe.stdout
);
}
#[test]
fn marketplace_plugin_with_only_commands_installs_them_without_a_skipped_note() {
let sb = Sandbox::bare("commands-only-plugin");
sb.write_and_commit(
".claude-plugin/plugin.json",
r#"{"name":"cmds","version":"1.0"}"#,
);
sb.write_and_commit(
"commands/ship.md",
"---\ndescription: Ship it\n---\n# ship\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
!combined.contains("not installed (no mind equivalent)"),
"a plugin whose commands all install must print no skipped note: {combined}"
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("command:cmds:ship"),
"the plugin's command must be offered: {}",
probe.stdout
);
}
#[test]
fn marketplace_reserved_kind_word_plugin_name_installs_bare() {
let sb = Sandbox::bare("reserved-name-plugin");
sb.write_and_commit(
".claude-plugin/plugin.json",
r#"{"name":"skill","version":"1.0"}"#,
);
sb.write_and_commit(
"skills/greet/SKILL.md",
"---\nname: greet\ndescription: Greet\n---\n# greet\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--link-only"]);
assert!(
r.success,
"meld of a plugin named with a reserved kind word must succeed: {} {}",
r.stdout, r.stderr
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:greet"),
"item must install bare when plugin name is a reserved kind word: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("skill:skill:greet"),
"reserved kind word must not be used as prefix: {}",
probe.stdout
);
}
#[test]
fn marketplace_external_plugin_learnable_on_demand() {
let extplugin = Sandbox::bare("extplugin-ondemand");
extplugin.write_and_commit(
".claude-plugin/plugin.json",
r#"{"name":"ext","version":"0.1"}"#,
);
extplugin.write_and_commit(
"skills/extskill/SKILL.md",
"---\nname: extskill\ndescription: External skill\n---\n# extskill\n",
);
let catalog = Sandbox::bare("ext-catalog-ondemand");
let ext_url = format!("file://{}", extplugin.source_spec());
catalog.write_and_commit(
".claude-plugin/marketplace.json",
&format!(r#"{{"name":"cat","plugins":[{{"name":"ext","source":"{ext_url}"}}]}}"#),
);
let cat_spec = catalog.source_spec();
let r = catalog.mind(&["meld", &cat_spec]);
assert!(r.success, "meld must succeed: {} {}", r.stdout, r.stderr);
assert!(
!catalog.claude_home.join("skills/ext:extskill").exists(),
"external plugin skill must not be installed after plain meld"
);
let probe = catalog.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:ext:extskill"),
"external plugin skill must be discoverable after plain meld: {}",
probe.stdout
);
let learn = catalog.mind(&["learn", "ext:extskill"]);
assert!(
learn.success,
"learn of external plugin skill must succeed: {} {}",
learn.stdout, learn.stderr
);
assert!(
catalog.claude_home.join("skills/ext:extskill").exists(),
"external plugin skill must be linked after learn"
);
}
#[test]
fn marketplace_dump_of_plugin_source_contains_no_claude_plugin_ref() {
let sb = Sandbox::from_example("marketplace-plugin");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--yes"]).success,
"meld --yes must succeed"
);
let dump = sb.mind(&["dump"]);
assert!(
dump.success,
"dump must succeed: {} {}",
dump.stdout, dump.stderr
);
assert!(
!dump.stdout.is_empty(),
"dump output must be non-empty for a plugin source"
);
assert!(
!dump.stdout.contains(".claude-plugin"),
"dump must not contain any .claude-plugin reference (MKT-11): {}",
dump.stdout
);
}
#[test]
fn marketplace_upgrade_catalog_sub_source_item() {
let sb = Sandbox::from_example("marketplace-catalog");
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success, "meld must succeed");
let learn = sb.mind(&["learn", "alpha:one"]);
assert!(
learn.success,
"learn alpha:one must succeed: {} {}",
learn.stdout, learn.stderr
);
let before = sb.mind(&["recall", "skill:alpha:one"]).stdout;
sb.write_and_commit(
"plugins/alpha/skills/one/SKILL.md",
"---\nname: one\ndescription: Alpha skill (updated)\n---\n# one updated\n",
);
assert!(
sb.mind(&["sync"]).success,
"sync must succeed after sub-source update"
);
let r = sb.mind(&["upgrade", "--yes"]);
assert!(
r.success,
"upgrade --yes must succeed: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("upgraded skill:alpha:one"),
"upgrade must report the sub-source item as upgraded: {}",
r.stdout
);
let after = sb.mind(&["recall", "skill:alpha:one"]).stdout;
assert_ne!(
before, after,
"recall output must differ after upgrade (hash must advance)"
);
}
#[test]
fn init_source_marketplace_generates_correct_json() {
let sb = Sandbox::new();
let repo = sb.base.join("my-plugin");
write(
&repo.join("skills/review/SKILL.md"),
"---\ndescription: review\n---\n# review\n",
);
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir, "--marketplace"]);
assert!(
r.success,
"init-source --marketplace failed: {} {}",
r.stdout, r.stderr
);
let mkt_path = repo.join(".claude-plugin/marketplace.json");
assert!(
mkt_path.exists(),
".claude-plugin/marketplace.json must be created"
);
let content = std::fs::read_to_string(&mkt_path).unwrap();
let v: serde_json::Value = serde_json::from_str(&content).expect("valid JSON");
assert_eq!(
v["name"], "my-plugin",
"top-level name must be the dir basename"
);
let plugin = &v["plugins"][0];
assert_eq!(plugin["name"], "my-plugin", "plugin entry name");
assert_eq!(plugin["source"], ".", "plugin source must be '.'");
assert!(
plugin.get("skills").is_none(),
"skills key must be absent without --flat-skills: {content}"
);
}
#[test]
fn init_source_marketplace_uses_mindtoml_prefix() {
let sb = Sandbox::new();
let repo = sb.base.join("irrelevant-dirname");
write(
&repo.join("skills/build/SKILL.md"),
"---\ndescription: build\n---\n# build\n",
);
write(
&repo.join("mind.toml"),
"[source]\nprefix = \"mypkg\"\ndescription = \"A great plugin\"\n",
);
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir, "--marketplace"]);
assert!(r.success, "failed: {} {}", r.stdout, r.stderr);
let content = std::fs::read_to_string(repo.join(".claude-plugin/marketplace.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&content).expect("valid JSON");
assert_eq!(
v["name"], "mypkg",
"prefix from mind.toml must override dirname"
);
assert_eq!(v["plugins"][0]["name"], "mypkg", "plugin entry name");
assert_eq!(
v["plugins"][0]["description"], "A great plugin",
"description from mind.toml [source].description"
);
}
#[test]
fn init_source_marketplace_namespace_flag_overrides() {
let sb = Sandbox::new();
let repo = sb.base.join("some-dir");
write(
&repo.join("skills/build/SKILL.md"),
"---\ndescription: build\n---\n# build\n",
);
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir, "--marketplace", "--namespace", "foo"]);
assert!(r.success, "failed: {} {}", r.stdout, r.stderr);
let content = std::fs::read_to_string(repo.join(".claude-plugin/marketplace.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&content).expect("valid JSON");
assert_eq!(v["name"], "foo", "name must be the --namespace value");
assert_eq!(v["plugins"][0]["name"], "foo", "plugin entry name");
let toml = std::fs::read_to_string(repo.join("mind.toml")).unwrap();
assert!(
toml.contains("namespace = \"foo\""),
"mind.toml must carry namespace = \"foo\": {toml}"
);
}
#[test]
fn init_source_marketplace_no_overwrite() {
let sb = Sandbox::new();
let repo = sb.base.join("existing-mkt");
write(
&repo.join("skills/build/SKILL.md"),
"---\ndescription: build\n---\n# build\n",
);
let sentinel = r#"{"name":"sentinel","plugins":[]}"#;
write(&repo.join(".claude-plugin/marketplace.json"), sentinel);
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir, "--marketplace"]);
assert!(
r.success,
"must exit 0 even when file exists: {} {}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("already exists"),
"must print 'already exists' message: {}",
r.stdout
);
let content = std::fs::read_to_string(repo.join(".claude-plugin/marketplace.json")).unwrap();
assert_eq!(content, sentinel, "file must be left unchanged");
}
#[test]
fn init_source_flat_skills_sets_mindtoml_key() {
let sb = Sandbox::new();
let repo_a = sb.base.join("flat-a");
write(
&repo_a.join("review/SKILL.md"),
"---\ndescription: review\n---\n# review\n",
);
let r = sb.mind(&["init-source", repo_a.to_str().unwrap(), "--flat-skills"]);
assert!(r.success, "case A failed: {} {}", r.stdout, r.stderr);
let toml_a = std::fs::read_to_string(repo_a.join("mind.toml")).unwrap();
assert!(
toml_a.contains("flat-skills = true"),
"case A: flat-skills must be set: {toml_a}"
);
let repo_b = sb.base.join("flat-b");
write(
&repo_b.join("review/SKILL.md"),
"---\ndescription: review\n---\n# review\n",
);
write(
&repo_b.join("mind.toml"),
"[source]\ndescription = \"existing\"\n",
);
let r = sb.mind(&["init-source", repo_b.to_str().unwrap(), "--flat-skills"]);
assert!(r.success, "case B failed: {} {}", r.stdout, r.stderr);
let toml_b = std::fs::read_to_string(repo_b.join("mind.toml")).unwrap();
assert!(
toml_b.contains("flat-skills = true"),
"case B: flat-skills must be inserted: {toml_b}"
);
assert!(
toml_b.contains("description = \"existing\""),
"case B: existing content must be preserved: {toml_b}"
);
let repo_c = sb.base.join("flat-c");
write(
&repo_c.join("review/SKILL.md"),
"---\ndescription: review\n---\n# review\n",
);
write(
&repo_c.join("mind.toml"),
"[source]\nflat-skills = false\ndescription = \"existing\"\n",
);
let r = sb.mind(&["init-source", repo_c.to_str().unwrap(), "--flat-skills"]);
assert!(r.success, "case C failed: {} {}", r.stdout, r.stderr);
let toml_c = std::fs::read_to_string(repo_c.join("mind.toml")).unwrap();
assert!(
toml_c.contains("flat-skills = true"),
"case C: flat-skills must be true: {toml_c}"
);
assert!(
!toml_c.contains("flat-skills = false"),
"case C: old value must be gone: {toml_c}"
);
}
#[test]
fn init_source_flat_skills_marketplace_skills_array() {
let sb = Sandbox::new();
let repo = sb.base.join("flat-mkt");
write(
&repo.join("review/SKILL.md"),
"---\ndescription: review\n---\n# review\n",
);
write(
&repo.join("build/SKILL.md"),
"---\ndescription: build\n---\n# build\n",
);
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir, "--flat-skills", "--marketplace"]);
assert!(r.success, "failed: {} {}", r.stdout, r.stderr);
let content = std::fs::read_to_string(repo.join(".claude-plugin/marketplace.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&content).expect("valid JSON");
let skills = v["plugins"][0]["skills"]
.as_array()
.expect("skills array must be present with --flat-skills");
let skill_strings: Vec<&str> = skills.iter().filter_map(|s| s.as_str()).collect();
assert!(
skill_strings.contains(&"review"),
"skills must include 'review': {skill_strings:?}"
);
assert!(
skill_strings.contains(&"build"),
"skills must include 'build': {skill_strings:?}"
);
let toml = std::fs::read_to_string(repo.join("mind.toml")).unwrap();
assert!(
toml.contains("flat-skills = true"),
"mind.toml must have flat-skills = true: {toml}"
);
}
#[test]
fn init_source_namespace_writes_namespace_key() {
let sb = Sandbox::new();
let repo = sb.base.join("ns-test");
write(
&repo.join("skills/scan/SKILL.md"),
"---\ndescription: scan\n---\n# scan\n",
);
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir, "--namespace", "bar"]);
assert!(r.success, "failed: {} {}", r.stdout, r.stderr);
let toml = std::fs::read_to_string(repo.join("mind.toml")).unwrap();
assert!(
toml.contains("namespace = \"bar\""),
"mind.toml must have namespace = \"bar\": {toml}"
);
assert!(
toml.contains("[source]"),
"must still have [source] header: {toml}"
);
}
#[test]
fn init_source_marketplace_placeholder_description() {
let sb = Sandbox::new();
let repo_a = sb.base.join("placeholder-a");
write(
&repo_a.join("skills/plan/SKILL.md"),
"---\ndescription: plan\n---\n# plan\n",
);
let r = sb.mind(&["init-source", repo_a.to_str().unwrap(), "--marketplace"]);
assert!(r.success, "case A failed: {} {}", r.stdout, r.stderr);
let content_a =
std::fs::read_to_string(repo_a.join(".claude-plugin/marketplace.json")).unwrap();
let v_a: serde_json::Value = serde_json::from_str(&content_a).expect("valid JSON");
assert_eq!(
v_a["plugins"][0]["description"], "TODO: describe this plugin",
"case A: description must be the placeholder when no mind.toml: {content_a}"
);
let repo_b = sb.base.join("placeholder-b");
write(
&repo_b.join("skills/plan/SKILL.md"),
"---\ndescription: plan\n---\n# plan\n",
);
write(&repo_b.join("mind.toml"), "[source]\ndescription = \"\"\n");
let r = sb.mind(&["init-source", repo_b.to_str().unwrap(), "--marketplace"]);
assert!(r.success, "case B failed: {} {}", r.stdout, r.stderr);
let content_b =
std::fs::read_to_string(repo_b.join(".claude-plugin/marketplace.json")).unwrap();
let v_b: serde_json::Value = serde_json::from_str(&content_b).expect("valid JSON");
assert_eq!(
v_b["plugins"][0]["description"], "TODO: describe this plugin",
"case B: description must be the placeholder when description is empty: {content_b}"
);
}
#[test]
fn init_source_namespace_updates_existing_mindtoml() {
let sb = Sandbox::new();
let repo_a = sb.base.join("ns-existing-a");
write(
&repo_a.join("skills/deploy/SKILL.md"),
"---\ndescription: deploy\n---\n# deploy\n",
);
write(
&repo_a.join("mind.toml"),
"[source]\ndescription = \"my source\"\n",
);
let r = sb.mind(&[
"init-source",
repo_a.to_str().unwrap(),
"--namespace",
"mypkg",
]);
assert!(r.success, "case A failed: {} {}", r.stdout, r.stderr);
let toml_a = std::fs::read_to_string(repo_a.join("mind.toml")).unwrap();
assert!(
toml_a.contains("namespace = \"mypkg\""),
"case A: namespace must be inserted: {toml_a}"
);
assert!(
toml_a.contains("description = \"my source\""),
"case A: existing content must be preserved: {toml_a}"
);
let repo_b = sb.base.join("ns-existing-b");
write(
&repo_b.join("skills/deploy/SKILL.md"),
"---\ndescription: deploy\n---\n# deploy\n",
);
write(
&repo_b.join("mind.toml"),
"[source]\nprefix = \"old\"\ndescription = \"my source\"\n",
);
let r = sb.mind(&[
"init-source",
repo_b.to_str().unwrap(),
"--namespace",
"new",
]);
assert!(r.success, "case B failed: {} {}", r.stdout, r.stderr);
let toml_b = std::fs::read_to_string(repo_b.join("mind.toml")).unwrap();
assert!(
toml_b.contains("namespace = \"new\""),
"case B: namespace must be updated to new: {toml_b}"
);
assert!(
!toml_b.contains("prefix = \"old\""),
"case B: old prefix key must be gone: {toml_b}"
);
assert!(
toml_b.contains("description = \"my source\""),
"case B: other content must be preserved: {toml_b}"
);
}
#[test]
fn init_source_namespace_overrides_existing_prefix_in_marketplace() {
let sb = Sandbox::new();
let repo = sb.base.join("ns-override");
write(
&repo.join("skills/lint/SKILL.md"),
"---\ndescription: lint\n---\n# lint\n",
);
write(
&repo.join("mind.toml"),
"[source]\nprefix = \"old\"\ndescription = \"lint tools\"\n",
);
let dir = repo.to_str().unwrap();
let r = sb.mind(&["init-source", dir, "--marketplace", "--namespace", "new"]);
assert!(r.success, "failed: {} {}", r.stdout, r.stderr);
let content = std::fs::read_to_string(repo.join(".claude-plugin/marketplace.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&content).expect("valid JSON");
assert_eq!(
v["name"], "new",
"top-level name must be the --namespace value: {content}"
);
assert_eq!(
v["plugins"][0]["name"], "new",
"plugin entry name must be --namespace value: {content}"
);
assert_eq!(
v["plugins"][0]["description"], "lint tools",
"description from existing mind.toml must be preserved: {content}"
);
let toml = std::fs::read_to_string(repo.join("mind.toml")).unwrap();
assert!(
toml.contains("namespace = \"new\""),
"mind.toml namespace must be updated to 'new': {toml}"
);
assert!(
!toml.contains("prefix = \"old\""),
"old prefix key must no longer be present: {toml}"
);
}
#[test]
fn skill_collision_yes_flag_forces_noninteractive_error() {
let src_a = Sandbox::bare("h1-yes-a");
src_a.write_and_commit(
"skills/h1skill/SKILL.md",
"---\nname: h1skill\ndescription: H1 skill A\n---\n# h1skill\n",
);
let r = src_a.mind(&["meld", &src_a.source_spec(), "--yes"]);
assert!(r.success, "meld source-a failed: {} {}", r.stdout, r.stderr);
let src_b = Sandbox::bare("h1-yes-b");
src_b.write_and_commit(
"skills/h1skill/SKILL.md",
"---\nname: h1skill\ndescription: H1 skill B\n---\n# h1skill\n",
);
let r = src_a.mind(&["meld", &src_b.source_spec(), "--yes"]);
assert!(
!r.success,
"--yes meld with collision must fail non-interactively (NS-45): {} {}",
r.stdout, r.stderr
);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
combined.contains("collision")
|| combined.contains("conflict")
|| combined.contains("namespace"),
"--yes collision error must mention collision/namespace (NS-43 NS-45): {combined}"
);
}
#[test]
fn recall_default_view_uses_namespace_prefix_for_alias() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec, "--namespace", "jk"]).success);
let recall = sb.mind(&["recall"]).stdout;
assert!(
recall.contains("namespace:jk"),
"default recall must show 'namespace:jk' not 'as:jk': {recall}"
);
assert!(
!recall.contains("as:jk"),
"default recall must not use the old 'as:' prefix: {recall}"
);
}
#[test]
fn sync_rewalk_respects_namespace_key_in_mindfile() {
let nested = Sandbox::named("dsc78-n");
let super_src = Sandbox::bare("dsc78-sup");
super_src.write_and_commit("mind.toml", "[source]\ndescription = \"super\"\n");
let r = super_src.mind(&["meld", &super_src.source_spec()]);
assert!(
r.success,
"meld super-source failed: {} {}",
r.stdout, r.stderr
);
let before = super_src.mind(&["recall", "--sources"]).stdout;
assert!(
!before.contains("/dsc78-n"),
"nested must not be registered before sync re-walk: {before}"
);
super_src.write_and_commit(
"mind.toml",
&format!(
"[source]\ndescription = \"super\"\n\
[[discover.sources]]\nsource = \"{}\"\nnamespace = \"pfx\"\n",
nested.source_spec()
),
);
let r = super_src.mind(&["sync"]);
assert!(r.success, "sync failed: {} {}", r.stdout, r.stderr);
let after = super_src.mind(&["recall", "--sources"]).stdout;
assert!(
after.contains("/dsc78-n"),
"nested source must be registered after sync re-walk: {after}"
);
assert!(
after.contains("namespace:pfx"),
"nested source must carry namespace:pfx alias after sync re-walk (DSC-78): {after}"
);
}
#[test]
fn meld_register_only_is_canonical() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--register-only"]).success,
"--register-only must succeed"
);
let skill_link = sb.claude_home.join("skills/review");
assert!(
!skill_link.exists(),
"--register-only must not install items: lobe symlink exists at {skill_link:?}"
);
}
#[test]
fn unmeld_keep_items_is_canonical() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let r = sb.mind(&["unmeld", "agents", "--keep-items"]);
assert!(r.success, "--keep-items must succeed: {}", r.stderr);
assert!(
sb.claude_home.join("skills/review").exists()
|| sb.claude_home.join("skills/review/SKILL.md").exists(),
"--keep-items must preserve the installed item"
);
}
#[test]
fn mutation_result_schema_field() {
let sb = melded();
let r = sb.mind(&["learn", "review", "--json"]);
assert!(r.success, "{}", r.stderr);
let v: serde_json::Value =
serde_json::from_str(&r.stdout).expect("learn --json must produce JSON");
assert_eq!(
v["schema"], 1,
"mutation result must carry schema:1: {}",
r.stdout
);
assert!(
v["action"].is_string(),
"action must be present: {}",
r.stdout
);
}
#[test]
fn upgrade_no_sync_flag_is_accepted() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let r = sb.mind(&["upgrade", "--no-sync"]);
assert!(
!r.stderr.contains("unexpected argument"),
"--no-sync must be a recognized flag: {}",
r.stderr
);
assert!(
!r.stderr.contains("error: unrecognized"),
"--no-sync must be a recognized flag: {}",
r.stderr
);
}
#[test]
fn upgrade_syncs_before_computing_delta() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--follow-branch", "main", "--yes"])
.success,
"meld --follow-branch failed"
);
assert!(sb.mind(&["learn", "review"]).success, "learn review failed");
sb.write_and_commit(
"skills/review/SKILL.md",
"---\ndescription: updated skill\n---\n# review v2\n",
);
let no_sync = sb.mind(&["upgrade", "--no-sync", "--yes"]);
assert!(
!no_sync.stderr.contains("error"),
"--no-sync must not error: {}",
no_sync.stderr
);
let with_sync = sb.mind(&["upgrade", "--yes"]);
assert!(
with_sync.stdout.contains("review") || with_sync.stderr.contains("review"),
"upgrade with sync must reference the changed item: out={} err={}",
with_sync.stdout,
with_sync.stderr
);
}
#[test]
fn mind_default_lobe_env_var_overrides_claude_home() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let alt_lobe = sb.mind_home.join("alt_lobe");
std::fs::create_dir_all(&alt_lobe).expect("create alt_lobe");
let alt_lobe_str = alt_lobe.to_str().unwrap().to_string();
assert!(
sb.mind_env(
&["meld", &spec, "--register-only"],
&[("MIND_DEFAULT_LOBE", alt_lobe_str.as_str())]
)
.success,
"register-only meld with MIND_DEFAULT_LOBE failed"
);
let r = sb.mind_env(
&["learn", "review"],
&[("MIND_DEFAULT_LOBE", alt_lobe_str.as_str())],
);
assert!(
r.success,
"learn with MIND_DEFAULT_LOBE failed: {}",
r.stderr
);
let in_alt =
alt_lobe.join("skills/review").exists() || alt_lobe.join("skills/review/SKILL.md").exists();
let in_claude = sb.claude_home.join("skills/review").exists()
|| sb.claude_home.join("skills/review/SKILL.md").exists();
assert!(
in_alt,
"item must be linked in MIND_DEFAULT_LOBE: {alt_lobe:?}"
);
assert!(
!in_claude,
"item must NOT be linked in CLAUDE_HOME when MIND_DEFAULT_LOBE is set: {:?}",
sb.claude_home
);
}
#[test]
fn visible_aliases_present_in_help() {
let sb = Sandbox::new();
let r = sb.mind(&["--help"]);
assert!(r.success, "--help failed: {}", r.stderr);
let out = r.stdout;
for alias in &[
"add",
"install",
"uninstall",
"update",
"search",
"list",
"doctor",
] {
assert!(
out.contains(alias),
"visible alias `{alias}` must appear in --help: {out}"
);
}
assert!(
!out.contains("detach"),
"`detach` must not appear in --help (hidden alias): {out}"
);
assert!(
!out.contains("target"),
"`target` must not appear in --help (hidden alias): {out}"
);
}
#[test]
fn meld_help_mentions_install_default() {
let sb = Sandbox::new();
let r = sb.mind(&["meld", "--help"]);
assert!(r.success, "meld --help failed: {}", r.stderr);
let out = r.stdout + &r.stderr;
assert!(
out.contains("install") || out.contains("Install"),
"meld --help must mention install: {out}"
);
}
#[test]
fn unmeld_long_help_leads_with_uninstall() {
let sb = Sandbox::new();
let r = sb.mind(&["unmeld", "--help"]);
assert!(r.success, "unmeld --help failed: {}", r.stderr);
let out = r.stdout + &r.stderr;
assert!(
out.contains("uninstall") || out.contains("Uninstalls"),
"unmeld --help must mention uninstall: {out}"
);
assert!(
out.contains("--keep-items"),
"unmeld --help must mention --keep-items: {out}"
);
}
#[test]
fn exit_codes_distinguish_runtime_and_usage_errors() {
let sb = Sandbox::new();
let ok = Command::new(env!("CARGO_BIN_EXE_mind"))
.args(["recall"])
.env("MIND_HOME", &sb.mind_home)
.env("CLAUDE_HOME", &sb.claude_home)
.output()
.expect("run mind");
assert_eq!(ok.status.code(), Some(0), "recall in a fresh home exits 0");
let runtime = Command::new(env!("CARGO_BIN_EXE_mind"))
.args(["learn", "no-such-item"])
.env("MIND_HOME", &sb.mind_home)
.env("CLAUDE_HOME", &sb.claude_home)
.output()
.expect("run mind");
assert_eq!(
runtime.status.code(),
Some(1),
"a MindError exits 1: {}",
String::from_utf8_lossy(&runtime.stderr)
);
let usage = Command::new(env!("CARGO_BIN_EXE_mind"))
.args(["probe", "--definitely-not-a-flag"])
.env("MIND_HOME", &sb.mind_home)
.env("CLAUDE_HOME", &sb.claude_home)
.output()
.expect("run mind");
assert_eq!(
usage.status.code(),
Some(2),
"a clap parse failure exits 2: {}",
String::from_utf8_lossy(&usage.stderr)
);
}
#[test]
fn removed_aliases_are_usage_errors() {
let sb = Sandbox::new();
let detach = sb.mind(&["detach", "whatever"]);
assert!(
!detach.success,
"`mind detach` must fail: {}",
detach.stdout
);
let target = sb.mind(&["config", "target", "list"]);
assert!(
!target.success,
"`mind config target` must fail: {}",
target.stdout
);
}
#[test]
fn self_update_alias_works() {
let sb = Sandbox::new();
let r = sb.mind(&["self-update", "--help"]);
assert!(
r.success || r.stderr.contains("evolve") || r.stdout.contains("evolve"),
"self-update alias must be recognized: out={} err={}",
r.stdout,
r.stderr
);
}
#[test]
fn all_mutating_verbs_carry_schema_1_in_json_envelope() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let meld_r = sb.mind(&["meld", &spec, "--json"]);
assert!(meld_r.success, "meld --json failed: {}", meld_r.stderr);
assert_eq!(
parse_json(&meld_r.stdout)["schema"],
1,
"meld --json must carry schema:1: {}",
meld_r.stdout
);
let sync_r = sb.mind(&["sync", "--json"]);
assert!(sync_r.success, "sync --json failed: {}", sync_r.stderr);
assert_eq!(
parse_json(&sync_r.stdout)["schema"],
1,
"sync --json must carry schema:1: {}",
sync_r.stdout
);
assert!(
sb.mind(&["learn", "skill:review"]).success,
"learn review failed"
);
let forget_r = sb.mind(&["forget", "skill:review", "--json"]);
assert!(
forget_r.success,
"forget --json failed: {}",
forget_r.stderr
);
assert_eq!(
parse_json(&forget_r.stdout)["schema"],
1,
"forget --json must carry schema:1: {}",
forget_r.stdout
);
assert!(
sb.mind(&["learn", "skill:review"]).success,
"learn review failed"
);
let upgrade_r = sb.mind(&["upgrade", "--json"]);
assert!(
upgrade_r.success,
"upgrade --json failed: {}",
upgrade_r.stderr
);
assert_eq!(
parse_json(&upgrade_r.stdout)["schema"],
1,
"upgrade --json must carry schema:1: {}",
upgrade_r.stdout
);
let unmeld_r = sb.mind(&["unmeld", "agents", "--json"]);
assert!(
unmeld_r.success,
"unmeld --json failed: {}",
unmeld_r.stderr
);
assert_eq!(
parse_json(&unmeld_r.stdout)["schema"],
1,
"unmeld --json must carry schema:1: {}",
unmeld_r.stdout
);
}
#[test]
fn short_n_is_usage_error_on_meld_review_and_init_source() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let meld_n = sb.mind(&["meld", "-n", &spec]);
assert!(
!meld_n.success,
"meld -n must be rejected as an unknown flag: {}",
meld_n.stderr
);
let review_n = sb.mind(&["review", "-n", &spec]);
assert!(
!review_n.success,
"review -n must be rejected as an unknown flag: {}",
review_n.stderr
);
let init_n = sb.mind(&["init-source", "-n"]);
assert!(
!init_n.success,
"init-source -n must be rejected as an unknown flag: {}",
init_n.stderr
);
assert!(sb.mind(&["meld", &spec]).success, "meld without -n failed");
let learn_n = sb.mind(&["learn", "-n", "skill:review"]);
assert!(
learn_n.success,
"learn -n must still work as --dry-run: {}",
learn_n.stderr
);
}
#[test]
fn deprecated_flag_aliases_are_hidden_from_help() {
let sb = Sandbox::new();
let meld_help = sb.mind(&["meld", "--help"]).stdout;
assert!(
!meld_help.contains("link-only"),
"--link-only must not appear in meld --help (hidden alias): {meld_help}"
);
assert!(
meld_help.contains("register-only"),
"--register-only must appear in meld --help (canonical name): {meld_help}"
);
let unmeld_help = sb.mind(&["unmeld", "--help"]).stdout;
assert!(
!unmeld_help.contains("unlink-only"),
"--unlink-only must not appear in unmeld --help (hidden alias): {unmeld_help}"
);
assert!(
unmeld_help.contains("keep-items"),
"--keep-items must appear in unmeld --help (canonical name): {unmeld_help}"
);
}
#[test]
fn verb_aliases_dispatch_to_correct_commands() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let add_r = sb.mind(&["add", &spec]);
assert!(
add_r.success,
"mind add must dispatch to meld: {}",
add_r.stderr
);
assert!(
sb.mind(&["recall", "--sources"]).stdout.contains("agents"),
"mind add must register the source"
);
let install_r = sb.mind(&["install", "skill:review"]);
assert!(
install_r.success,
"mind install must dispatch to learn: {}",
install_r.stderr
);
let list_r = sb.mind(&["list"]);
assert!(
list_r.success,
"mind list must dispatch to recall: {}",
list_r.stderr
);
assert!(
list_r.stdout.contains("skill:review"),
"mind list must show the installed item: {}",
list_r.stdout
);
let search_r = sb.mind(&["search", "--no-tui"]);
assert!(
search_r.success,
"mind search must dispatch to probe: {}",
search_r.stderr
);
assert!(
search_r.stdout.contains("review"),
"mind search must list catalog items: {}",
search_r.stdout
);
let update_r = sb.mind(&["update"]);
assert!(
update_r.success,
"mind update must dispatch to sync: {}",
update_r.stderr
);
let doctor_r = sb.mind(&["doctor"]);
assert!(
doctor_r.success,
"mind doctor must dispatch to introspect: {}",
doctor_r.stderr
);
let uninstall_r = sb.mind(&["uninstall", "skill:review"]);
assert!(
uninstall_r.success,
"mind uninstall must dispatch to forget: {}",
uninstall_r.stderr
);
let recall_after = sb.mind(&["recall"]).stdout;
assert!(
!recall_after.contains("installed @"),
"mind uninstall must have removed skill:review (no installed-at marker): {recall_after}"
);
}
#[test]
fn sync_upgrade_is_noted_as_deprecated_in_help() {
let sb = Sandbox::new();
let r = sb.mind(&["sync", "--help"]);
assert!(r.success, "sync --help failed: {}", r.stderr);
let out = r.stdout + &r.stderr;
assert!(
out.to_lowercase().contains("deprecated") || out.to_lowercase().contains("prefer"),
"sync --help must note that --upgrade is deprecated: {out}"
);
}
#[test]
fn upgrade_sync_first_outcome_with_drift_example() {
let sb = Sandbox::from_example("drift");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--follow-branch", "main", "--register-only"])
.success,
"meld --follow-branch failed"
);
assert!(sb.mind(&["learn", "audit"]).success, "learn audit failed");
write(
&sb.source.join("skills/audit/SKILL.md"),
"---\nname: audit\ndescription: Updated audit skill\n---\n# audit v2 body\n",
);
git(&sb.source, &["commit", "-aqm", "update audit"]);
let no_sync = sb.mind(&["upgrade", "--no-sync", "--json"]);
assert!(
no_sync.success,
"--no-sync must succeed: {}",
no_sync.stderr
);
let ns = parse_json(&no_sync.stdout);
assert_eq!(
ns["outcome"], "up-to-date",
"--no-sync on stale clone must report up-to-date (no delta visible from old clone): {}",
no_sync.stdout
);
let with_sync = sb.mind(&["upgrade", "--yes", "--json"]);
assert!(
with_sync.success,
"upgrade with sync must succeed: {}",
with_sync.stderr
);
let ws = parse_json(&with_sync.stdout);
assert_eq!(
ws["outcome"], "upgraded",
"upgrade with sync must detect and apply the upstream change: {}",
with_sync.stdout
);
let after = sb.mind(&["recall"]);
assert!(
!after.stdout.contains("outdated"),
"item must be current after sync-first upgrade: {}",
after.stdout
);
}
fn fake_git_proxy_bin_dir(dir: &Path) -> PathBuf {
let real_git = String::from_utf8(
std::process::Command::new("which")
.arg("git")
.output()
.expect("which git")
.stdout,
)
.expect("utf8")
.trim()
.to_string();
let bin_dir = dir.join("fake-git-proxy-bin");
std::fs::create_dir_all(&bin_dir).unwrap();
let script = format!(
"#!/bin/sh\nif [ \"$1\" = \"clone\" ]; then\n for a; do\n case \"$a\" in\n https://*)\n echo \"fatal: unable to access '$a': Received HTTP code 407 from proxy after CONNECT\" >&2\n exit 128\n ;;\n esac\n done\nfi\nexec \"{real_git}\" \"$@\"\n"
);
let script_path = bin_dir.join("git");
std::fs::write(&script_path, &script).unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755)).unwrap();
bin_dir
}
#[test]
fn meld_top_level_auth_failure_prints_ssh_hint() {
let sb = Sandbox::bare("auth-hint-test");
let fake_dir = fake_git_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let r = sb.mind_env(
&["meld", "https://example.com/owner/private-repo"],
&[("PATH", &new_path)],
);
assert!(!r.success, "meld must fail on auth error: {}", r.stderr);
assert!(
r.stderr.contains("git@example.com:owner/private-repo"),
"auth hint must include the SSH URL: {}",
r.stderr
);
assert!(
r.stderr.contains("ssh = true"),
"auth hint must mention `ssh = true` config option: {}",
r.stderr
);
assert!(
r.stderr.contains("credential helper"),
"auth hint must mention credential helper: {}",
r.stderr
);
}
#[test]
fn meld_top_level_proxy_failure_prints_proxy_hint() {
let sb = Sandbox::bare("proxy-hint-test");
let fake_dir = fake_git_proxy_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let r = sb.mind_env(
&["meld", "https://example.com/owner/repo"],
&[("PATH", &new_path)],
);
assert!(!r.success, "meld must fail on proxy error: {}", r.stderr);
assert!(
r.stderr.contains("HTTPS_PROXY"),
"proxy hint must mention HTTPS_PROXY: {}",
r.stderr
);
assert!(
r.stderr.contains("http.proxy"),
"proxy hint must mention http.proxy: {}",
r.stderr
);
assert!(
!r.stderr.contains("credential helper"),
"auth hint must not appear for a proxy failure: {}",
r.stderr
);
}
#[test]
fn meld_top_level_clone_error_leads_with_git_stderr() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--follow-branch", "nonexistent-branch"]);
assert!(!r.success, "meld must fail: {}", r.stderr);
assert!(
r.stderr.contains("fatal:")
|| r.stderr.contains("error:")
|| r.stderr.contains("not found"),
"git stderr must appear in output: {}",
r.stderr
);
assert!(
!r.stderr.contains(" command: git clone"),
"explicit command line must be absent without --verbose: {}",
r.stderr
);
assert!(
!r.stderr.contains(" store: "),
"explicit store path line must be absent without --verbose: {}",
r.stderr
);
}
#[test]
fn meld_top_level_clone_error_verbose_shows_command_and_store() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&[
"meld",
"--verbose",
&spec,
"--follow-branch",
"nonexistent-branch",
]);
assert!(!r.success, "meld must fail: {}", r.stderr);
assert!(
r.stderr.contains(" command: git clone"),
"explicit command line must appear under --verbose: {}",
r.stderr
);
assert!(
r.stderr.contains(" store: "),
"explicit store path line must appear under --verbose: {}",
r.stderr
);
}
#[test]
fn learn_not_found_with_sources_hints_probe_not_sync() {
let sb = melded();
let r = sb.mind(&["learn", "does-not-exist"]);
assert!(!r.success, "learn must fail for unknown item: {}", r.stderr);
assert!(
r.stderr.contains("probe 'does-not-exist'"),
"hint must point at `mind probe <query>`: {}",
r.stderr
);
assert!(
!r.stderr.contains("sync"),
"with sources, no line in stderr may mention sync: {}",
r.stderr
);
}
#[test]
fn meld_json_clone_error_message_contains_git_cause() {
let sb = Sandbox::bare("json-git-cause-test");
let fake_dir = fake_git_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let r = sb.mind_env(
&["--json", "meld", "https://example.com/owner/repo"],
&[("PATH", &new_path)],
);
assert!(!r.success, "meld must fail: {}", r.stdout);
assert!(
r.stderr.is_empty(),
"no stderr output expected in --json mode: {}",
r.stderr
);
let v = parse_json(&r.stdout);
let msg = v["error"]["message"]
.as_str()
.expect("error.message must be a string");
assert!(
msg.contains("Authentication failed") || msg.contains("fatal:"),
"message must contain the git error cause, not a placeholder: {msg}"
);
assert!(
!msg.contains("<no stderr>"),
"message must not contain `<no stderr>` placeholder: {msg}"
);
}
#[test]
fn meld_human_clone_error_no_no_stderr_literal() {
let sb = Sandbox::bare("human-no-stderr-literal-test");
let fake_dir = fake_git_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let r = sb.mind_env(
&["meld", "https://example.com/owner/repo"],
&[("PATH", &new_path)],
);
assert!(!r.success, "meld must fail: {}", r.stderr);
assert!(
!r.stderr.contains("<no stderr>"),
"the literal `<no stderr>` must not appear in human output: {}",
r.stderr
);
assert!(
r.stderr.contains("Authentication failed") || r.stderr.contains("fatal:"),
"git's stderr must appear in human output: {}",
r.stderr
);
}
fn fake_git_ansi_bin_dir(dir: &std::path::Path) -> std::path::PathBuf {
let real_git = String::from_utf8(
std::process::Command::new("which")
.arg("git")
.output()
.expect("which git")
.stdout,
)
.expect("utf8")
.trim()
.to_string();
let bin_dir = dir.join("fake-git-ansi-bin");
std::fs::create_dir_all(&bin_dir).unwrap();
let script = format!(
"#!/bin/sh\nif [ \"$1\" = \"clone\" ]; then\n printf 'fatal: \\033[31mANSI\\033[0m injection \\xe2\\x80\\xae bidi\\n' >&2\n exit 128\nfi\nexec \"{real_git}\" \"$@\"\n"
);
let script_path = bin_dir.join("git");
std::fs::write(&script_path, &script).unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755)).unwrap();
bin_dir
}
#[test]
fn meld_clone_error_strips_ansi_from_git_stderr() {
let sb = Sandbox::bare("ansi-strip-test");
let fake_dir = fake_git_ansi_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let r = sb.mind_env(
&["meld", "https://example.com/owner/repo"],
&[("PATH", &new_path)],
);
assert!(!r.success, "meld must fail: {}", r.stderr);
assert!(
r.stderr.contains("ANSI"),
"the text content must still appear: {}",
r.stderr
);
assert!(
!has_ansi_escape(&r.stderr),
"no ANSI escape sequence must appear in the printed stderr: {:?}",
r.stderr
);
}
fn fake_git_ansi_fetch_bin_dir(dir: &std::path::Path) -> std::path::PathBuf {
let real_git = String::from_utf8(
std::process::Command::new("which")
.arg("git")
.output()
.expect("which git")
.stdout,
)
.expect("utf8")
.trim()
.to_string();
let bin_dir = dir.join("fake-git-ansi-fetch-bin");
std::fs::create_dir_all(&bin_dir).unwrap();
let script = format!(
"#!/bin/sh\nif [ \"$1\" = \"fetch\" ]; then\n printf 'fatal: \\033[31mANSIFETCH\\033[0m injection \\xe2\\x80\\xae bidi\\n' >&2\n exit 128\nfi\nexec \"{real_git}\" \"$@\"\n"
);
let script_path = bin_dir.join("git");
std::fs::write(&script_path, &script).unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755)).unwrap();
bin_dir
}
#[test]
fn sync_per_source_error_strips_ansi_from_git_stderr() {
let (sb, _sha_v1, _sha_v2) = make_pinnable_repo("sync-ansi-strip-test");
let spec = sb.source_spec();
let melded = sb.mind(&["meld", &spec, "--pin-tag", "v1.0"]);
assert!(melded.success, "meld --pin-tag v1.0: {}", melded.stderr);
let fake_dir = fake_git_ansi_fetch_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let r = sb.mind_env(&["sync"], &[("PATH", &new_path)]);
assert!(
!r.success,
"sync must fail when the source's fetch fails: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("ANSIFETCH"),
"the git error text content must still appear: {}",
r.stderr
);
assert!(
!has_ansi_escape(&r.stderr),
"no ANSI escape sequence may appear in the printed sync stderr: {:?}",
r.stderr
);
assert!(
!r.stderr.contains('\u{202e}'),
"the U+202E bidi-override char must not survive into printed stderr: {:?}",
r.stderr
);
}
#[test]
fn meld_policy_refused_before_git_clone_no_clone_dir() {
let sb = Sandbox::bare("pol36-no-clone-test");
let fake_dir = fake_git_bin_dir(&sb.base);
let new_path = prepend_path(&fake_dir);
let policy = write_policy(&sb, "[sources]\nlock = true\nallow = []\n");
let r = sb.mind_env(
&["meld", "https://example.com/owner/refused-repo"],
&[("PATH", &new_path), ("MIND_POLICY_FILE", policy.as_str())],
);
assert!(!r.success, "locked meld must be refused: {}", r.stdout);
assert!(
r.stderr.contains("not permitted"),
"stderr must report SourceNotAllowed: {}",
r.stderr
);
assert!(
!r.stderr.contains("Authentication failed"),
"git clone must not have run: policy check must fire first: {}",
r.stderr
);
let sources_root = sb.mind_home.join("sources");
let clone_exists = sources_root.exists()
&& std::fs::read_dir(&sources_root)
.map(|d| d.count() > 0)
.unwrap_or(false);
assert!(
!clone_exists,
"no clone directory should exist after a refused meld"
);
}
#[test]
fn meld_policy_refused_prints_policy_file_hint() {
let sb = Sandbox::bare("pol37-hint-test");
let policy = write_policy(&sb, "[sources]\nlock = true\nallow = []\n");
let spec = sb.source_spec();
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(!r.success, "locked meld must be refused: {}", r.stdout);
assert!(
r.stderr.contains("policy") && r.stderr.contains("policy.toml"),
"hint must mention the policy file path: {}",
r.stderr
);
}
#[test]
fn allow_local_false_refuses_local_path_meld_even_with_permissive_pattern() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow-local = false\nallow = [\"local/*/*\"]\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"allow-local = false under lock must refuse a local-path meld: {}",
r.stdout
);
assert!(
r.stderr.contains("allow-local"),
"error must name the allow-local = false reason: {}",
r.stderr
);
assert!(
!r.stderr
.contains("not permitted by the managed policy's allowlist"),
"error must not be the generic SourceNotAllowed message: {}",
r.stderr
);
assert_eq!(source_count(&sb), 0, "registry must be unchanged");
let clone_dir = sb
.mind_home
.join("sources")
.join("local")
.join(sb.base_name())
.join("agents");
assert!(
!clone_dir.exists(),
"no clone should be left at {}",
clone_dir.display()
);
}
#[test]
fn allow_local_false_under_no_lock_does_not_refuse() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(&sb, "[sources]\nlock = false\nallow-local = false\n");
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"allow-local = false with lock = false must not refuse: {}",
r.stderr
);
assert_eq!(source_count(&sb), 1, "source should be registered");
}
#[test]
fn allow_local_true_still_requires_allow_pattern_under_lock() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow-local = true\nallow = [\"local/*/other-repo\"]\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"allow-local = true with lock must still enforce allow patterns: {}",
r.stdout
);
assert!(
r.stderr.contains("not permitted"),
"error should be the standard SourceNotAllowed message: {}",
r.stderr
);
assert_eq!(source_count(&sb), 0, "registry must be unchanged");
}
#[test]
fn allow_local_true_default_allows_local_meld_matching_pattern() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow = [\"local/*/agents\"]\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
r.success,
"default allow-local (true) with matching pattern must succeed: {}",
r.stderr
);
assert_eq!(source_count(&sb), 1, "source should be registered");
}
#[test]
fn allow_local_false_refuses_file_url_meld() {
let sb = Sandbox::named("agents");
let spec = format!("file://{}", sb.source_spec());
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow-local = false\nallow = [\"local/*/*\"]\n",
);
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"allow-local = false under lock must refuse a file:// meld: {}",
r.stdout
);
assert!(
r.stderr.contains("allow-local"),
"error must name the allow-local = false reason: {}",
r.stderr
);
assert!(
!r.stderr
.contains("not permitted by the managed policy's allowlist"),
"error must be LocalMeldForbidden, not the generic SourceNotAllowed: {}",
r.stderr
);
assert_eq!(source_count(&sb), 0, "registry must be unchanged");
}
#[test]
fn allow_local_false_meld_json_error_envelope_has_kind_slug() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(
&sb,
"[sources]\nlock = true\nallow-local = false\nallow = [\"local/*/*\"]\n",
);
let r = sb.mind_env(
&["--json", "meld", &spec],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(!r.success, "meld must fail: {}", r.stdout);
let v = parse_json(&r.stdout);
assert_eq!(v["schema"], 1, "schema must be 1: {}", r.stdout);
assert_eq!(
v["error"]["kind"].as_str(),
Some("local-meld-forbidden"),
"kind slug must be local-meld-forbidden: {}",
r.stdout
);
let msg = v["error"]["message"]
.as_str()
.expect("error.message must be a string");
assert!(
msg.contains("allow-local"),
"message must name the allow-local reason, not a placeholder: {msg}"
);
assert_eq!(source_count(&sb), 0, "registry must be unchanged");
}
#[test]
fn allow_local_false_no_lock_emits_no_allow_local_warning() {
let sb = Sandbox::named("agents");
let spec = sb.source_spec();
let policy = write_policy(&sb, "[sources]\nlock = false\nallow-local = false\n");
let r = sb.mind_env(&["meld", &spec], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(r.success, "lock = false must not refuse: {}", r.stderr);
assert!(
!r.stderr.contains("allow-local"),
"no allow-local warning must be emitted when lock is false: {}",
r.stderr
);
assert_eq!(source_count(&sb), 1, "source should be registered");
}
#[test]
fn auto_meld_partial_failure_does_not_persist_partial_entry() {
let sb = Sandbox::new(); let first_spec = sb.source_spec();
let nested = sb.base.join("nested-src");
write(
&nested.join("skills/real-skill/SKILL.md"),
"---\nname: real-skill\ndescription: a real skill\n---\n# real skill\n",
);
git(&nested, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&nested, &["config", "user.email", "t@t"]);
git(&nested, &["config", "user.name", "t"]);
git(&nested, &["add", "-A"]);
git(&nested, &["commit", "-qm", "initial"]);
let super_src = sb.base.join("super-src");
let nested_str = nested.to_string_lossy();
let super_toml = format!(
"[[discover.sources]]\nsource = \"{nested_str}\"\ninstall-items = [\"skill:nonexistent-skill\"]\n"
);
write(&super_src.join("mind.toml"), &super_toml);
git(&super_src, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&super_src, &["config", "user.email", "t@t"]);
git(&super_src, &["config", "user.name", "t"]);
git(&super_src, &["add", "-A"]);
git(&super_src, &["commit", "-qm", "initial"]);
let super_str = super_src.to_string_lossy();
let first_str = first_spec.as_str();
let policy_body = format!(
"[[sources.auto_meld]]\nrepo = \"{first_str}\"\n\n[[sources.auto_meld]]\nrepo = \"{super_str}\"\n"
);
let policy = write_policy(&sb, &policy_body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"sync must fail due to provisioning failure: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("nonexistent-skill") && r.stderr.contains("does not match"),
"failure must be the DSC-63 bad-reference error (fires post-push): {}",
r.stderr
);
assert_eq!(
source_count(&sb),
1,
"only the first source should be persisted; the failed entry must be rolled back: stderr={}",
r.stderr
);
}
#[test]
fn auto_meld_rollback_on_dsc64_validate_failure() {
let sb = Sandbox::new();
let first_spec = sb.source_spec();
let nested = sb.base.join("nested-dsc64-src");
write(
&nested.join("skills/real-skill/SKILL.md"),
"---\nname: real-skill\ndescription: a real skill\n---\n# real skill\n",
);
git(&nested, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&nested, &["config", "user.email", "t@t"]);
git(&nested, &["config", "user.name", "t"]);
git(&nested, &["add", "-A"]);
git(&nested, &["commit", "-qm", "initial"]);
let super_src = sb.base.join("super-dsc64-src");
let nested_str = nested.to_string_lossy();
let super_toml = format!(
"[[discover.sources]]\nsource = \"{nested_str}\"\ninstall = true\ninstall-items = [\"skill:real-skill\"]\n"
);
write(&super_src.join("mind.toml"), &super_toml);
git(&super_src, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&super_src, &["config", "user.email", "t@t"]);
git(&super_src, &["config", "user.name", "t"]);
git(&super_src, &["add", "-A"]);
git(&super_src, &["commit", "-qm", "initial"]);
let super_str = super_src.to_string_lossy();
let first_str = first_spec.as_str();
let policy_body = format!(
"[[sources.auto_meld]]\nrepo = \"{first_str}\"\n\n[[sources.auto_meld]]\nrepo = \"{super_str}\"\n"
);
let policy = write_policy(&sb, &policy_body);
let r = sb.mind_env(&["sync"], &[("MIND_POLICY_FILE", policy.as_str())]);
assert!(
!r.success,
"sync must fail on the DSC-64 provisioning error: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("mutually exclusive"),
"failure must be the DSC-64 validate error (fires after the super push): {}",
r.stderr
);
assert_eq!(
source_count(&sb),
1,
"the DSC-64-failing entry must be rolled back, leaving only the first: stderr={}",
r.stderr
);
}
#[test]
fn no_sources_melded_phrasing_sync() {
let sb = Sandbox::bare("cli187-sync-test");
let r = sb.mind(&["sync"]);
assert!(r.success, "sync with no sources must succeed: {}", r.stderr);
assert!(
r.stdout.contains("no sources melded")
&& r.stdout.contains("mind meld <owner/repo>")
&& r.stdout.contains("to add one"),
"sync empty message must match the standard phrasing: {}",
r.stdout
);
}
#[test]
fn no_sources_melded_phrasing_recall_sources() {
let sb = Sandbox::bare("cli187-recall-sources-test");
let r = sb.mind(&["recall", "--sources"]);
assert!(
r.success,
"recall --sources with no sources must succeed: {}",
r.stderr
);
assert!(
r.stdout.contains("no sources melded")
&& r.stdout.contains("mind meld <owner/repo>")
&& r.stdout.contains("to add one"),
"recall --sources empty message must match the standard phrasing: {}",
r.stdout
);
}
#[test]
fn no_sources_melded_phrasing_recall() {
let sb = Sandbox::bare("cli187-recall-test");
let r = sb.mind(&["recall"]);
assert!(
r.success,
"recall with no sources must succeed: {}",
r.stderr
);
assert!(
r.stdout.contains("no sources melded")
&& r.stdout.contains("mind meld <owner/repo>")
&& r.stdout.contains("to add one"),
"recall empty message must match the standard phrasing: {}",
r.stdout
);
}
#[test]
fn no_sources_melded_phrasing_probe() {
let sb = Sandbox::bare("cli187-probe-test");
let r = sb.mind(&["probe", "anything"]);
assert!(
r.success,
"probe with no sources must succeed: {}",
r.stderr
);
assert!(
r.stdout.contains("no sources melded")
&& r.stdout.contains("mind meld <owner/repo>")
&& r.stdout.contains("to add one"),
"probe empty message must match the standard phrasing: {}",
r.stdout
);
}
fn marketplace_with_unlisted_community() -> Sandbox {
let sb = Sandbox::bare("mkt-addroot");
sb.write_and_commit(
".claude-plugin/marketplace.json",
r#"{"name":"Cat","plugins":[{"name":"kit","source":"./plugins/kit"}]}"#,
);
sb.write_and_commit(
"plugins/kit/skills/foo/SKILL.md",
"---\ndescription: listed skill\n---\n# foo\n",
);
sb.write_and_commit(
"community/extra/SKILL.md",
"---\ndescription: unlisted flat skill\n---\n# extra\n",
);
sb.write_and_commit(
"community/skills/bonus/SKILL.md",
"---\ndescription: unlisted containered skill\n---\n# bonus\n",
);
sb
}
#[test]
fn add_root_composes_with_marketplace_manifest() {
let sb = marketplace_with_unlisted_community();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--add-root", "community", "--register-only"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
!combined.contains("manifest's plugin components are ignored"),
"--add-root must not suppress the manifest: {combined}"
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:kit:foo"),
"manifest item must stay offered under its plugin namespace: {}",
probe.stdout
);
assert!(
probe.stdout.contains("skill:extra"),
"unlisted flat skill under the added root must be offered: {}",
probe.stdout
);
assert!(
probe.stdout.contains("skill:bonus"),
"unlisted containered skill under the added root must be offered: {}",
probe.stdout
);
let r = sb.mind(&["learn", "extra"]);
assert!(r.success, "learn extra failed: {} {}", r.stdout, r.stderr);
assert!(
sb.claude_home.join("skills/extra").exists(),
"add-root skill must link into the lobe"
);
}
#[test]
fn add_root_on_single_plugin_source_inherits_plugin_prefix() {
let sb = Sandbox::bare("plugin-addroot");
sb.write_and_commit(".claude-plugin/plugin.json", r#"{"name":"kit"}"#);
sb.write_and_commit(
"skills/foo/SKILL.md",
"---\ndescription: plugin skill\n---\n# foo\n",
);
sb.write_and_commit(
"community/extra/SKILL.md",
"---\ndescription: unlisted flat skill\n---\n# extra\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--add-root", "community", "--register-only"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:kit:foo"),
"plugin item must carry the plugin-name prefix: {}",
probe.stdout
);
assert!(
probe.stdout.contains("skill:kit:extra"),
"add-root item must share the plugin-name prefix: {}",
probe.stdout
);
}
#[test]
fn add_root_items_carry_source_prefix_not_plugin_prefix() {
let sb = marketplace_with_unlisted_community();
let spec = sb.source_spec();
let r = sb.mind(&[
"meld",
&spec,
"--namespace",
"jk",
"--add-root",
"community",
"--register-only",
]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:jk:kit:foo"),
"manifest item must carry outer + per-plugin prefix: {}",
probe.stdout
);
assert!(
probe.stdout.contains("skill:jk:extra"),
"add-root item must carry only the source prefix: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("skill:jk:kit:extra"),
"add-root item must not inherit a per-plugin prefix: {}",
probe.stdout
);
}
#[test]
fn add_root_composes_with_authoritative_mind_toml() {
let sb = Sandbox::bare("auth-addroot");
sb.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"skill\"\nname = \"declared\"\npath = \"lib/declared\"\n",
);
sb.write_and_commit(
"lib/declared/SKILL.md",
"---\ndescription: declared skill\n---\n# declared\n",
);
sb.write_and_commit(
"extra/skills/undeclared/SKILL.md",
"---\ndescription: undeclared skill\n---\n# undeclared\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--add-root", "extra", "--register-only"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let combined = format!("{}\n{}", r.stdout, r.stderr);
assert!(
!combined.contains("--add-root is ignored") && !combined.contains("is ignored"),
"--add-root must not be ignored for an authoritative mind.toml: {combined}"
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:declared"),
"declared item must stay offered: {}",
probe.stdout
);
assert!(
probe.stdout.contains("skill:undeclared"),
"add-root skill must compose with the authoritative inventory: {}",
probe.stdout
);
}
#[test]
fn add_root_extends_plain_convention_scan() {
let sb = Sandbox::new();
sb.write_and_commit(
"community/skills/side/SKILL.md",
"---\ndescription: side skill\n---\n# side\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--add-root", "community", "--register-only"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:review") && probe.stdout.contains("skill:side"),
"repo-root and add-root items must union: {}",
probe.stdout
);
}
#[test]
fn add_root_dedups_paths_the_manifest_already_contributed() {
let sb = marketplace_with_unlisted_community();
let spec = sb.source_spec();
let r = sb.mind(&[
"meld",
&spec,
"--add-root",
"plugins/kit",
"--register-only",
]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:kit:foo"),
"the manifest entry must win the overlap: {}",
probe.stdout
);
assert!(
!probe.stdout.contains("skill:foo\n") && probe.stdout.matches("foo").count() == 1,
"the overlapping skill must not be offered twice / bare: {}",
probe.stdout
);
}
#[test]
fn add_root_distinct_path_collision_is_duplicate_item() {
let sb = Sandbox::new(); sb.write_and_commit(
"extra/skills/review/SKILL.md",
"---\ndescription: colliding skill\n---\n# review\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--add-root", "extra", "--register-only"]);
assert!(
!r.success,
"colliding add-root item must fail the meld: {}",
r.stdout
);
assert!(
r.stderr.contains("review"),
"the DuplicateItem error must name the item: {}",
r.stderr
);
}
#[test]
fn add_root_invalid_root_is_error() {
let sb = Sandbox::new();
let spec = sb.source_spec();
let r = sb.mind(&[
"meld",
&spec,
"--add-root",
"no-such-dir",
"--register-only",
]);
assert!(!r.success, "a missing add-root dir must fail: {}", r.stdout);
assert!(
r.stderr.contains("scan root") && r.stderr.contains("no-such-dir"),
"the InvalidRoot error must name the root: {}",
r.stderr
);
}
#[test]
fn sync_source_selector_scopes_to_one_source() {
let sb = Sandbox::named("alpha");
let beta = sb.base.join("beta");
write(
&beta.join("skills/scan/SKILL.md"),
"---\nname: scan\ndescription: b\n---\n# scan\n",
);
git(&beta, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(&beta, &["config", "user.email", "t@t"]);
git(&beta, &["config", "user.name", "t"]);
git(&beta, &["add", "-A"]);
git(&beta, &["commit", "-qm", "initial"]);
let beta_spec = beta.to_string_lossy().into_owned();
assert!(
sb.mind(&["meld", &sb.source_spec(), "--register-only"])
.success
);
assert!(sb.mind(&["meld", &beta_spec, "--register-only"]).success);
let one = sb.mind(&["sync", "alpha"]);
assert!(one.success, "sync alpha failed: {}", one.stderr);
assert!(
one.stdout.contains("alpha"),
"sync alpha must refresh alpha: {}",
one.stdout
);
assert!(
!one.stdout.contains("beta"),
"sync alpha must NOT refresh beta: {}",
one.stdout
);
let bad = sb.mind(&["sync", "no-such-source"]);
assert!(
!bad.success,
"an unknown selector must fail: {}",
bad.stdout
);
assert!(
bad.stderr.contains("no melded source matches"),
"unknown selector must be SourceNotFound: {}",
bad.stderr
);
let all = sb.mind(&["sync"]);
assert!(all.success, "bare sync failed: {}", all.stderr);
assert!(
all.stdout.contains("alpha") && all.stdout.contains("beta"),
"bare sync must fetch both sources: {}",
all.stdout
);
}
#[test]
fn sync_malformed_glob_selector_reports_invalid_pattern() {
let sb = Sandbox::named("alpha");
assert!(
sb.mind(&["meld", &sb.source_spec(), "--register-only"])
.success
);
let r = sb.mind(&["sync", "[bad"]);
assert!(!r.success, "malformed glob must fail: {}", r.stdout);
assert!(
r.stderr.contains("not a valid glob selector"),
"expected invalid-pattern error, got stderr={} stdout={}",
r.stderr,
r.stdout
);
assert!(
!r.stderr.contains("no source named"),
"malformed glob must NOT surface as SourceNotFound: {}",
r.stderr
);
}
#[test]
fn re_pin_to_older_sha_then_upgrade_installs_older_content() {
let (sb, sha_v1, sha_v2) = make_pinnable_repo("downgrade");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--pin", &sha_v2]).success,
"meld --pin v2 must succeed"
);
assert!(sb.mind(&["learn", "dev"]).success, "learn dev failed");
let link = sb.claude_home.join("agents/dev.md");
let at_v2 = std::fs::read_to_string(&link).expect("installed agent");
assert!(
at_v2.contains("dev v2"),
"the installed agent must start at v2: {at_v2}"
);
let store_path = sb.mind_home.join("store/agent/dev");
let store_at_v2 = std::fs::read_to_string(&store_path).expect("store copy at v2");
assert!(store_at_v2.contains("dev v2"), "{store_at_v2}");
let manifest_v2: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(sb.mind_home.join("manifest.json")).unwrap())
.unwrap();
let hash_v2 = manifest_v2["items"]["agent:dev"]["hash"].clone();
assert!(hash_v2.is_string(), "sanity: hash recorded at v2");
assert!(
sb.mind(&["meld", &spec, "--pin", &sha_v1]).success,
"re-pin to v1 must succeed"
);
let up = sb.mind(&["upgrade", "dev", "--yes"]);
assert!(up.success, "downgrade upgrade failed: {}", up.stderr);
let at_v1 = std::fs::read_to_string(&link).expect("installed agent after downgrade");
assert!(
at_v1.contains("dev v1"),
"the agent must be rolled back to v1: {at_v1}"
);
assert!(
!at_v1.contains("dev v2"),
"the v2 content must be gone after the downgrade: {at_v1}"
);
let store_at_v1 = std::fs::read_to_string(&store_path).expect("store copy after downgrade");
assert!(
store_at_v1.contains("dev v1") && !store_at_v1.contains("dev v2"),
"the store copy must roll back to v1, not just the link: {store_at_v1}"
);
let manifest_v1: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(sb.mind_home.join("manifest.json")).unwrap())
.unwrap();
let hash_v1 = manifest_v1["items"]["agent:dev"]["hash"].clone();
assert_ne!(
hash_v1, hash_v2,
"the manifest's recorded hash must advance to the v1 content's hash, \
not stay pinned at v2's: {manifest_v1:#}"
);
}
fn installed_keys(sb: &Sandbox) -> Vec<String> {
let m: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(sb.mind_home.join("manifest.json")).unwrap())
.unwrap();
let mut keys: Vec<String> = m["items"]
.as_object()
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
keys.sort();
keys
}
#[test]
fn forget_dropped_removes_only_items_removed_upstream() {
let sb = melded();
assert!(sb.mind(&["learn", "dev"]).success);
assert!(sb.mind(&["learn", "review"]).success);
sb.remove_and_commit("agents/dev.md");
assert!(sb.mind(&["sync"]).success, "sync failed");
let r = sb.mind(&["forget", "--dropped"]);
assert!(
r.success,
"forget --dropped failed: {} {}",
r.stdout, r.stderr
);
assert!(r.stdout.contains("forgot agent:dev"), "{}", r.stdout);
assert_eq!(installed_keys(&sb), vec!["skill:review".to_string()]);
assert!(
!sb.claude_home.join("agents/dev.md").exists(),
"the dropped item's link must be removed"
);
assert!(sb.claude_home.join("skills/review").exists());
}
#[test]
fn forget_dropped_takes_items_of_an_unmelded_source_and_confirms_several() {
let sb = melded();
assert!(sb.mind(&["learn", "dev"]).success);
assert!(sb.mind(&["learn", "review"]).success);
assert!(sb.mind(&["unmeld", "agents", "--keep-items"]).success);
let r = sb.mind(&["forget", "--dropped"]);
assert!(!r.success, "a multi-item forget must confirm: {}", r.stdout);
assert!(
r.stdout.contains("forget would remove 2 item(s)"),
"{}",
r.stdout
);
assert_eq!(installed_keys(&sb).len(), 2, "nothing removed on refusal");
let r = sb.mind(&["forget", "--dropped", "--yes"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(installed_keys(&sb).is_empty(), "{}", r.stdout);
}
#[test]
fn forget_dropped_is_narrowed_by_a_ref() {
let sb = melded();
assert!(sb.mind(&["learn", "dev"]).success);
assert!(sb.mind(&["learn", "review"]).success);
assert!(sb.mind(&["unmeld", "agents", "--keep-items"]).success);
let r = sb.mind(&["forget", "--dropped", "agent:*"]);
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert_eq!(installed_keys(&sb), vec!["skill:review".to_string()]);
}
#[test]
fn forget_dropped_with_nothing_dropped_removes_nothing() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let r = sb.mind(&["forget", "--dropped"]);
assert!(r.success, "{}", r.stderr);
assert!(r.stdout.contains("no dropped items"), "{}", r.stdout);
assert_eq!(installed_keys(&sb), vec!["skill:review".to_string()]);
let r = sb.mind(&["forget", "--dropped", "--json"]);
assert!(r.success, "{}", r.stderr);
let v: serde_json::Value = serde_json::from_str(&r.stdout).expect("JSON");
assert_eq!(v["outcome"], "no-op", "{v}");
}
#[test]
fn forget_dropped_keeps_items_of_a_source_whose_clone_is_missing() {
let sb = melded();
assert!(sb.mind(&["learn", "review"]).success);
let moved = sb.base.join("moved");
std::fs::rename(&sb.source, &moved).unwrap();
let r = sb.mind(&["forget", "--dropped"]);
std::fs::rename(&moved, &sb.source).unwrap();
assert!(r.success, "{} {}", r.stdout, r.stderr);
assert!(r.stdout.contains("no dropped items"), "{}", r.stdout);
assert_eq!(installed_keys(&sb), vec!["skill:review".to_string()]);
}