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
}
#[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_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
);
}
#[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 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("# prefix = \"prefix\""),
"scaffold must carry a [source] table and a generic commented prefix: {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_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 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 remeld_namespace_change_locked_when_items_installed() {
let sb = Sandbox::new();
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--yes"]).success,
"initial meld+install"
);
assert!(
sb.claude_home.join("skills/review").exists(),
"item installs unprefixed first"
);
let r = sb.mind(&["meld", &spec, "--namespace", "jk", "--yes"]);
assert!(
!r.success,
"re-meld --namespace with installed items must fail: {} {}",
r.stdout, r.stderr
);
assert!(
r.stderr.contains("namespace") || r.stderr.contains("installed"),
"error must mention namespace lock: {}",
r.stderr
);
assert!(
sb.claude_home.join("skills/review").exists(),
"existing unprefixed link must survive the refused re-meld"
);
assert!(
std::fs::symlink_metadata(sb.claude_home.join("skills/jk:review")).is_err(),
"the prefixed link must not exist after a refused re-meld"
);
let recall = sb.mind(&["recall"]).stdout;
assert!(
!recall.contains("jk:review"),
"recall must still show the original unprefixed name: {recall}"
);
}
#[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 rows: serde_json::Value = serde_json::from_str(&r.stdout).expect("probe --json array");
let rows = rows.as_array().expect("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_n_is_short_for_no_tui() {
let sb = melded();
let short = sb.mind(&["probe", "-n"]);
let long = sb.mind(&["probe", "--no-tui"]);
assert!(short.success, "{}", short.stderr);
assert_eq!(
short.stdout, long.stdout,
"`-n` must match `--no-tui` output"
);
assert!(short.stdout.contains("skill:review"), "{}", short.stdout);
assert!(short.stdout.contains("agent:dev"), "{}", short.stdout);
assert!(short.stdout.contains("rule:style"), "{}", 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_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 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_stdout_is_empty() {
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
);
assert!(
r.stdout.trim().is_empty(),
"error path must produce no stdout; got: {:?}",
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
);
}
#[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("as: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 remeld_namespace_change_allowed_when_no_items_installed() {
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"]);
assert!(
r.success,
"re-meld --namespace with no installed items must succeed: {} {}",
r.stdout, r.stderr
);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:jk:review"),
"probe must show the new 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 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") && r.stderr.contains("review"),
"expected unguarded-ref warning for skill referent: {}",
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.\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["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.\n",
);
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec, "--as", "jk"]);
assert!(r.success, "{}", 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(&["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 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 source named"), "{}", 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 source named"), "{}", 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",
"--dangerously-skip-install-hook-check"
])
.success,
"install greet (with its uninstall hook)"
);
assert!(
fail.mind(&[
"learn",
"skill:other",
"--dangerously-skip-install-hook-check"
])
.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",
"--dangerously-skip-install-hook-check",
]);
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("as: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_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_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 detach_is_an_alias_for_unmeld() {
let sb = melded();
assert!(sb.mind(&["detach", "agents"]).success);
assert!(
sb.mind(&["recall", "--sources"])
.stdout
.contains("no sources melded")
);
}
#[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_target_is_an_alias_for_lobes() {
let sb = Sandbox::new();
let home = sb.base.join("viaTarget").display().to_string();
assert!(sb.mind(&["config", "target", "add", &home]).success);
assert!(
sb.mind(&["config", "target", "list"])
.stdout
.contains(&home)
);
}
#[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 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"), "{}", 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");
assert!(ok.mind(&["meld", &ok.source_spec()]).success);
}
#[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}"
);
}
#[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);
assert!(
items.stdout.trim_start().starts_with('['),
"{}",
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 = sb.mind(&["recall", "--sources", "--json"]).stdout;
assert!(srcs.trim_start().starts_with('['), "{srcs}");
assert!(srcs.contains("\"url\""), "{srcs}");
let fresh = Sandbox::new();
assert_eq!(
fresh.mind(&["recall", "--json"]).stdout.trim(),
"[]",
"an empty registry must emit []"
);
}
#[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);
assert!(r.stdout.trim_start().starts_with('['), "{}", r.stdout);
assert!(r.stdout.contains("\"installed\""), "{}", r.stdout);
assert!(r.stdout.contains("\"name\": \"review\""), "{}", r.stdout);
assert!(r.stdout.contains("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_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);
assert!(
r.stdout.trim_start().starts_with('['),
"probe --json must produce a JSON 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}");
assert!(clean.contains("\"issues\""), "{clean}");
assert!(clean.contains("\"items\""), "{clean}");
std::fs::remove_file(sb.claude_home.join("skills/review")).unwrap();
let broken = sb.mind(&["introspect", "--json"]).stdout;
assert!(broken.contains("\"missing-link\""), "{broken}");
}
#[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(&["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(&["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);
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"
);
}
#[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", "detect"]).success);
assert!(sb.mind(&["learn", "scan"]).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"
);
}
#[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", "--dangerously-skip-install-hook-check"]);
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", "--dangerously-skip-install-hook-check"]);
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_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);
}
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 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 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", "/tmp/some-home"],
&[("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
);
let after = sb.mind_env(
&["config", "lobes", "list"],
&[("MIND_POLICY_FILE", policy.as_str())],
);
assert!(
!after.stdout.contains("/tmp/some-home"),
"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 must not be re-melded: {}",
r.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_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_dangerously_skip_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, "--dangerously-skip-install-hook-check"]);
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, "--dangerously-skip-install-hook-check"]);
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",
"--dangerously-skip-install-hook-check",
]);
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, "--dangerously-skip-install-hook-check"])
.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_reruns_hook_after_source_advances() {
let sb = sandbox_with_declared_hook("agents", "touch hookran");
let spec = sb.source_spec();
assert!(
sb.mind(&["meld", &spec, "--dangerously-skip-install-hook-check"])
.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", "--dangerously-skip-install-hook-check"]);
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", "--dangerously-skip-install-hook-check"]);
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, "--dangerously-skip-install-hook-check"])
.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", "--dangerously-skip-install-hook-check"]);
assert!(
with_flag.success,
"sync --upgrade --dangerously-skip-install-hook-check 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,
"--dangerously-skip-install-hook-check"
])
.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",
"--dangerously-skip-install-hook-check",
]);
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", "--dangerously-skip-install-hook-check"]);
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 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,
"--dangerously-skip-install-hook-check"
])
.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",
"--dangerously-skip-install-hook-check",
]);
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", "--dangerously-skip-install-hook-check"]);
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, "--dangerously-skip-install-hook-check"])
.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", "--dangerously-skip-install-hook-check"],
&[("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]
fn help_lists_upgrade_and_evolve_not_self_update() {
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 NOT contain 'self-update': {}",
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",
"--dangerously-skip-install-hook-check"
])
.success
);
assert!(marker.exists(), "re-meld must run the pending hook");
std::fs::remove_file(&marker).unwrap();
assert!(
sb.mind(&[
"meld",
&spec,
"--link-only",
"--dangerously-skip-install-hook-check"
])
.success
);
assert!(
!marker.exists(),
"a hook already run at this commit is not re-offered"
);
assert!(
sb.mind(&[
"meld",
&spec,
"--link-only",
"--force",
"--dangerously-skip-install-hook-check"
])
.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_output_is_mirrored_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",
"--dangerously-skip-install-hook-check",
]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("====== (hook-stdout: build) ======"),
"the stdout separator frame must appear in mind's output: {}",
r.stdout
);
assert!(
r.stdout.contains("HELLO-FROM-HOOK"),
"the hook's stdout must be mirrored to mind's output: {}",
r.stdout
);
assert!(
r.stdout.contains("====== (end hook: build) ======"),
"the closing divider must separate the hook output from what follows: {}",
r.stdout
);
}
#[test]
fn install_hook_stderr_is_framed_and_mirrored() {
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",
"--dangerously-skip-install-hook-check",
]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
assert!(
r.stdout.contains("====== (hook-stderr: warn) ======"),
"the stderr separator frame must appear in mind's output: {}",
r.stdout
);
assert!(
r.stdout.contains("OOPS"),
"the hook's stderr must be mirrored to mind's output: {}",
r.stdout
);
assert!(
r.stdout.contains("====== (end hook: warn) ======"),
"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,
"--dangerously-skip-install-hook-check",
"--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,
"--dangerously-skip-install-hook-check",
"--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, "--dangerously-skip-install-hook-check"]);
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",
"--dangerously-skip-install-hook-check",
]);
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}"),
"--dangerously-skip-install-hook-check",
]);
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,
"--dangerously-skip-install-hook-check",
"--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,
"--dangerously-skip-install-hook-check",
]);
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", "--dangerously-skip-install-hook-check"]);
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",
"--dangerously-skip-install-hook-check",
]);
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_everywhere() {
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(&format!("{s}/tool/detect/detect run")),
"{run_sh}"
);
let lib_sh = std::fs::read_to_string(store.join("tool/detect/lib.sh")).unwrap();
assert!(
lib_sh.contains(&format!("exec {s}/tool/shard/shard")),
"{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_goes_to_stderr_and_stdout_stays_clean() {
let sb = melded();
let r = sb.mind(&["learn", "does-not-exist", "--json"]);
assert!(!r.success, "unknown item must fail");
assert!(
r.stderr.contains("no item matches"),
"error must go to stderr: {}",
r.stderr
);
assert!(
r.stdout.trim().is_empty(),
"no JSON (or prose) must be written to stdout on error: {:?}",
r.stdout
);
}
#[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_two_objects_one_per_action() {
let sb = melded();
assert!(sb.mind(&["learn", "skill:review"]).success);
let r = sb.mind(&["sync", "--upgrade", "--json"]);
assert!(r.success, "sync --upgrade --json failed: {}", r.stderr);
assert!(
serde_json::from_str::<serde_json::Value>(r.stdout.trim()).is_err(),
"sync --upgrade --json is expected to emit two objects, not one value: {}",
r.stdout
);
let actions: Vec<serde_json::Value> = serde_json::Deserializer::from_str(&r.stdout)
.into_iter::<serde_json::Value>()
.map(|d| d.expect("each chunk must be valid JSON"))
.collect();
assert_eq!(
actions.len(),
2,
"exactly two JSON objects (one per logical action): {}",
r.stdout
);
assert_eq!(actions[0]["action"], "sync", "{}", r.stdout);
assert_eq!(actions[1]["action"], "upgrade", "{}", r.stdout);
}
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",
"--dangerously-skip-install-hook-check",
]);
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",
"--dangerously-skip-install-hook-check",
]);
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",
"--dangerously-skip-install-hook-check"
])
.success
);
let r = sb.mind(&[
"forget",
"skill:greet",
"--dangerously-skip-install-hook-check",
]);
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",
"--dangerously-skip-install-hook-check"
])
.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",
"--dangerously-skip-install-hook-check"
])
.success
);
let r = sb.mind(&[
"forget",
"skill:greet",
"--dangerously-skip-install-hook-check",
]);
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",
"--dangerously-skip-install-hook-check"
])
.success
);
let r = sb.mind(&[
"unmeld",
"agents",
"-y",
"--dangerously-skip-install-hook-check",
]);
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",
"--dangerously-skip-install-hook-check"
])
.success
);
assert!(sb.base.join("markers/installed").exists());
std::fs::remove_dir_all(sb.base.join("markers")).unwrap();
assert!(
sb.mind(&[
"forget",
"skill:greet",
"--dangerously-skip-install-hook-check"
])
.success
);
assert!(
sb.base.join("markers/uninstalled").exists(),
"uninstall hook fires on removal"
);
let r = sb.mind(&[
"learn",
"skill:greet",
"--dangerously-skip-install-hook-check",
]);
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",
"--dangerously-skip-install-hook-check"
])
.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", "--dangerously-skip-install-hook-check"]);
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 an install hook"),
"review must list the install hook: {all}"
);
assert!(
all.contains("declares an uninstall hook"),
"review must list the uninstall hook: {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[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[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[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.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, "--dangerously-skip-install-hook-check"]);
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, "--dangerously-skip-install-hook-check"]);
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
);
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 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, "--dangerously-skip-install-hook-check"]);
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, "--dangerously-skip-install-hook-check"]);
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_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", "-n", "--kind", "skill", "review"]);
assert!(
r.success,
"probe -n --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 rows: Vec<serde_json::Value> = serde_json::from_str(&r.stdout).expect("must be valid JSON");
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 rows: Vec<serde_json::Value> = serde_json::from_str(&r.stdout).expect("must be valid JSON");
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 rows: Vec<serde_json::Value> = serde_json::from_str(&r.stdout).expect("must be valid JSON");
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, not emit JSON: {} {}",
json.stdout, json.stderr
);
assert!(
json.stderr.contains("not installed"),
"must report NotInstalled: {}",
json.stderr
);
assert!(
json.stdout.trim().is_empty(),
"no JSON should be emitted for an uninstalled 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 rows: Vec<serde_json::Value> =
serde_json::from_str(&probe.stdout).expect("probe JSON invalid");
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
);
assert!(
r.stderr.contains("needs confirmation") || r.stderr.contains("ConfirmationRequired"),
"must return ConfirmationRequired: stderr={}",
r.stderr
);
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("as = \"jk\""),
"dump must record the effective prefix as `as = \"jk\"`: {}",
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("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") || combined.contains("command"),
"skipped-components note must name a kind: {combined}"
);
}
#[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_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
);
assert!(
sources.stdout.contains("alpha"),
"alpha sub-source must appear: {}",
sources.stdout
);
assert!(
sources.stdout.contains("beta"),
"beta sub-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
);
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)"
);
}
#[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();
assert!(catalog.mind(&["meld", &spec]).success);
let jsrc = catalog.mind(&["recall", "--sources", "--json"]);
assert!(jsrc.success, "{}", jsrc.stderr);
assert!(
jsrc.stdout.contains("1.1.1"),
"the marketplace entry version must win and appear in JSON: {}",
jsrc.stdout
);
assert!(
!jsrc.stdout.contains("9.9.9"),
"the plugin.json version must be overridden by the entry version: {}",
jsrc.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 sub-source 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"),
"the sync re-walk must register the newly-listed marketplace entry (DSC-57): {}",
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
);
}