use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
struct Sandbox {
base: PathBuf,
source: PathBuf,
mind_home: PathBuf,
claude_home: PathBuf,
}
struct Run {
stdout: String,
stderr: String,
success: bool,
}
impl Sandbox {
fn new() -> Sandbox {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-ign-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let source = base.join("lib");
let sb = Sandbox {
base: base.clone(),
source: source.clone(),
mind_home: base.join("mind"),
claude_home: base.join("claude"),
};
write(&source.join("README.md"), "# fixture\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 spec(&self) -> String {
self.source.to_string_lossy().into_owned()
}
fn mind(&self, args: &[&str]) -> Run {
let out = Command::new(env!("CARGO_BIN_EXE_mind"))
.args(args)
.env("MIND_HOME", &self.mind_home)
.env("CLAUDE_HOME", &self.claude_home)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null())
.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 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 store(&self, kind: &str, name: &str) -> PathBuf {
self.mind_home.join("store").join(kind).join(name)
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.base);
}
}
fn write(path: &Path, contents: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, contents).unwrap();
}
fn git(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 root_skill_sandbox() -> Sandbox {
let sb = Sandbox::new();
sb.write_and_commit(
"SKILL.md",
"---\ndescription: A skill at the repo root\n---\n# root-skill\n",
);
sb.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"skill\"\nname = \"root-skill\"\npath = \".\"\n",
);
sb
}
#[test]
fn a_repo_root_item_does_not_install_the_vcs_directory() {
let sb = root_skill_sandbox();
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let store = sb.store("skill", "root-skill");
assert!(
store.join("SKILL.md").is_file(),
"the skill itself must be installed"
);
assert!(
!store.join(".git").exists(),
"the source's .git must not be copied into the store"
);
assert!(
store.join("README.md").is_file(),
"ordinary files at the item root are still installed"
);
}
#[test]
fn a_repo_root_item_does_not_drift_when_the_source_repo_gets_a_commit() {
let sb = root_skill_sandbox();
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let before = sb.mind(&["introspect"]);
assert!(
before.stdout.contains("all good"),
"a fresh install must be clean: {}",
before.stdout
);
sb.write_and_commit("notes.txt", "unrelated\n");
std::fs::remove_file(sb.source.join("notes.txt")).unwrap();
git(&sb.source, &["add", "-A"]);
git(&sb.source, &["commit", "-qm", "revert"]);
let after = sb.mind(&["introspect"]);
assert!(
after.stdout.contains("all good"),
"commits that leave the item's content unchanged must not drift it: {}",
after.stdout
);
let recall = sb.mind(&["recall", "skill:root-skill"]);
assert!(
!recall.stdout.contains("out of date"),
"recall must not report drift either: {}",
recall.stdout
);
}
#[test]
fn a_declared_source_level_list_excludes_from_both_the_copy_and_the_hash() {
let sb = Sandbox::new();
sb.write_and_commit("skills/review/SKILL.md", "---\ndescription: r\n---\n# r\n");
sb.write_and_commit("skills/review/scratch/notes.md", "draft\n");
sb.write_and_commit("skills/review/keep.md", "kept\n");
sb.write_and_commit("mind.toml", "[source]\nignore = [\"scratch/\"]\n");
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let store = sb.store("skill", "review");
assert!(store.join("keep.md").is_file(), "unmatched files install");
assert!(
!store.join("scratch").exists(),
"an ignored directory is not copied"
);
sb.write_and_commit("skills/review/scratch/notes.md", "edited draft\n");
let after = sb.mind(&["introspect"]);
assert!(
after.stdout.contains("all good"),
"an ignored file's content must not affect the hash: {}",
after.stdout
);
sb.write_and_commit("skills/review/keep.md", "changed\n");
let drifted = sb.mind(&["introspect"]);
assert!(
!drifted.stdout.contains("all good"),
"a real content change must still drift: {}",
drifted.stdout
);
}
#[test]
fn an_item_level_list_replaces_the_source_level_one() {
let sb = Sandbox::new();
sb.write_and_commit("a/SKILL.md", "---\ndescription: a\n---\n# a\n");
sb.write_and_commit("a/src-ignored.md", "x\n");
sb.write_and_commit("a/own-ignored.md", "y\n");
sb.write_and_commit(
"mind.toml",
"[source]\nignore = [\"src-ignored.md\"]\n\n\
[[items]]\nkind = \"skill\"\nname = \"a\"\npath = \"a\"\nignore = [\"own-ignored.md\"]\n",
);
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let store = sb.store("skill", "a");
assert!(
!store.join("own-ignored.md").exists(),
"the item's own list applies"
);
assert!(
store.join("src-ignored.md").is_file(),
"the source list is REPLACED, not merged, so this is installed"
);
}
#[test]
fn an_item_may_not_ignore_its_own_anchor_file() {
let sb = Sandbox::new();
sb.write_and_commit("a/SKILL.md", "---\ndescription: a\n---\n# a\n");
sb.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"skill\"\nname = \"a\"\npath = \"a\"\nignore = [\"SKILL.md\"]\n",
);
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(!r.success, "must be refused: {}", r.stdout);
assert!(
r.stderr.contains("SKILL.md") && r.stderr.contains("cannot ignore"),
"the error must name the anchor: {}",
r.stderr
);
}
#[test]
fn an_unusable_ignore_pattern_is_refused_at_scan() {
for (pattern, expect) in [
("/absolute", "not relative"),
("../outside", "'..' component"),
("[bad", "not a valid glob"),
] {
let sb = Sandbox::new();
sb.write_and_commit("skills/a/SKILL.md", "---\ndescription: a\n---\n# a\n");
sb.write_and_commit(
"mind.toml",
&format!("[source]\nignore = [\"{pattern}\"]\n"),
);
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(!r.success, "{pattern:?} must be refused: {}", r.stdout);
assert!(
r.stderr.contains(expect),
"{pattern:?} must report {expect:?}: {}",
r.stderr
);
}
}
#[test]
fn expanding_an_ignored_file_is_a_hard_error() {
let sb = Sandbox::new();
sb.write_and_commit(
"skills/a/SKILL.md",
"---\ndescription: a\nexpand: gen/run.py\n---\n# a\n",
);
sb.write_and_commit("skills/a/gen/run.py", "# {{self}}\n");
sb.write_and_commit("mind.toml", "[source]\nignore = [\"gen/\"]\n");
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(!r.success, "must be refused: {}", r.stdout);
assert!(
r.stderr.contains("gen/run.py") && r.stderr.contains("expand"),
"the error must name both directives: {}",
r.stderr
);
}
#[test]
fn expanding_a_file_under_a_builtin_ignored_directory_names_the_builtin() {
let sb = Sandbox::new();
sb.write_and_commit(
"skills/a/SKILL.md",
"---\ndescription: a\nexpand: .git/config\n---\n# a\n",
);
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(!r.success, "must be refused: {}", r.stdout);
assert!(
r.stderr.contains("'.git'"),
"the error must name the specific built-in that caused the exclusion: {}",
r.stderr
);
assert!(
!r.stderr.contains("a built-in ignore"),
"the generic placeholder must not appear when the built-in is nameable: {}",
r.stderr
);
}
#[test]
fn an_ignored_file_is_invisible_to_the_reference_scan() {
let sb = Sandbox::new();
sb.write_and_commit("skills/a/SKILL.md", "---\ndescription: a\n---\n# a\n");
sb.write_and_commit(
"skills/a/scratch/draft.md",
"handoff to {{ns:nonexistent}}\n",
);
sb.write_and_commit("mind.toml", "[source]\nignore = [\"scratch/\"]\n");
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(
r.success,
"a token in an ignored file must not fail the install: {} {}",
r.stdout, r.stderr
);
assert!(
sb.store("skill", "a").join("SKILL.md").is_file(),
"the item must actually be installed: {}",
r.stdout
);
assert!(
!sb.store("skill", "a").join("scratch").exists(),
"and the file is not installed"
);
}
#[test]
fn ignores_do_not_change_which_items_are_discovered() {
let sb = Sandbox::new();
sb.write_and_commit("skills/one/SKILL.md", "---\ndescription: 1\n---\n# 1\n");
sb.write_and_commit("skills/two/SKILL.md", "---\ndescription: 2\n---\n# 2\n");
sb.write_and_commit(
"mind.toml",
"[source]\nignore = [\"two/\", \"skills/two/\"]\n",
);
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let probe = sb.mind(&["probe"]);
assert!(
probe.stdout.contains("skill:one") && probe.stdout.contains("skill:two"),
"both items are still discovered and installable: {}",
probe.stdout
);
assert!(
sb.store("skill", "two").join("SKILL.md").is_file(),
"and the second item installed normally"
);
}
#[test]
fn ignores_travel_with_the_source_so_a_re_meld_reproduces_them() {
let sb = Sandbox::new();
sb.write_and_commit("skills/a/SKILL.md", "---\ndescription: a\n---\n# a\n");
sb.write_and_commit("skills/a/scratch/x.md", "draft\n");
sb.write_and_commit("mind.toml", "[source]\nignore = [\"scratch/\"]\n");
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let first = sb.mind(&["recall", "skill:a"]);
let hash_line = first
.stdout
.lines()
.find(|l| l.contains("hash"))
.expect("recall shows a hash")
.to_string();
assert!(sb.mind(&["unmeld", "lib", "--yes"]).success);
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let second = sb.mind(&["recall", "skill:a"]);
assert!(
second.stdout.contains(hash_line.trim()),
"the reproduced install must hash identically:\nfirst: {hash_line}\nsecond: {}",
second.stdout
);
assert!(!sb.store("skill", "a").join("scratch").exists());
}
#[test]
fn dropping_a_vcs_directory_from_an_installed_item_is_an_ordinary_upgrade() {
let sb = root_skill_sandbox();
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let manifest_path = sb.mind_home.join("manifest.json");
let raw = std::fs::read_to_string(&manifest_path).unwrap();
let mut doc: serde_json::Value = serde_json::from_str(&raw).unwrap();
let item = &mut doc["items"]["skill:root-skill"];
assert!(!item.is_null(), "the installed item must exist: {doc}");
item["hash"] = serde_json::Value::String("pretend-pre-ign-2-hash".to_string());
std::fs::write(&manifest_path, serde_json::to_vec_pretty(&doc).unwrap()).unwrap();
let store = sb.store("skill", "root-skill");
std::fs::create_dir_all(store.join(".git")).unwrap();
std::fs::write(store.join(".git").join("HEAD"), "ref: refs/heads/main\n").unwrap();
let recall = sb.mind(&["recall", "skill:root-skill"]);
assert!(
recall.stdout.contains("out of date"),
"the hand-edited hash must read as drift: {}",
recall.stdout
);
let upgrade = sb.mind(&["upgrade", "--yes"]);
assert!(upgrade.success, "upgrade failed: {}", upgrade.stderr);
assert!(
upgrade.stdout.contains("root-skill"),
"upgrade must report the item: {}",
upgrade.stdout
);
assert!(
!store.join(".git").exists(),
"the reinstalled store copy must not carry the VCS directory back"
);
let after = sb.mind(&["introspect"]);
assert!(
after.stdout.contains("all good"),
"the install is stable across the upgrade: {}",
after.stdout
);
}
#[test]
fn a_tool_with_no_tool_md_may_still_ignore_a_file() {
let sb = Sandbox::new();
sb.write_and_commit("tools/mytool/run.sh", "#!/bin/sh\necho hi\n");
sb.write_and_commit("tools/mytool/README.md", "internal notes\n");
sb.write_and_commit("mind.toml", "[source]\nignore = [\"*.md\"]\n");
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(
r.success,
"a tool with no TOOL.md must not be refused for ignoring *.md: {} {}",
r.stdout, r.stderr
);
let store = sb.store("tool", "mytool");
assert!(
store.join("run.sh").is_file(),
"the tool's own file installs"
);
assert!(
!store.join("README.md").exists(),
"the ignored README must not be copied"
);
}
#[test]
fn a_dot_git_file_is_ignored_like_a_dot_git_directory() {
let sb = Sandbox::new();
sb.write_and_commit("skills/a/SKILL.md", "---\ndescription: a\n---\n# a\n");
write(
&sb.source.join("skills/a/.git"),
"gitdir: ../../.git/modules/a\n",
);
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(r.success, "meld failed: {} {}", r.stdout, r.stderr);
let store = sb.store("skill", "a");
assert!(store.join("SKILL.md").is_file());
assert!(
!store.join(".git").exists(),
"a `.git` FILE must be excluded the same as a `.git` directory"
);
}
#[test]
fn changing_the_ignore_list_across_installs_is_an_ordinary_upgrade() {
let sb = Sandbox::new();
sb.write_and_commit("skills/a/SKILL.md", "---\ndescription: a\n---\n# a\n");
sb.write_and_commit("skills/a/docs/guide.md", "guide\n");
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let store = sb.store("skill", "a");
assert!(
store.join("docs/guide.md").is_file(),
"docs/ installs before any ignore list is declared"
);
sb.write_and_commit("mind.toml", "[source]\nignore = [\"docs/\"]\n");
let recall = sb.mind(&["recall", "skill:a"]);
assert!(
recall.stdout.contains("out of date"),
"a newly-declared ignore list must read as drift: {}",
recall.stdout
);
let upgrade = sb.mind(&["upgrade", "--yes"]);
assert!(upgrade.success, "upgrade failed: {}", upgrade.stderr);
assert!(
!store.join("docs").exists(),
"the reinstalled copy must honor the newly-declared ignore list"
);
}
#[test]
fn an_item_link_item_inherits_the_source_level_ignore_list() {
let sb = Sandbox::new();
sb.write_and_commit("skills/review/SKILL.md", "---\ndescription: r\n---\n# r\n");
sb.write_and_commit("skills/review/scratch/notes.md", "draft\n");
sb.write_and_commit("mind.toml", "[source]\nignore = [\"scratch/\"]\n");
let link = format!("file://{}/tree/main/skills/review", sb.source.display());
let r = sb.mind(&["learn", &link]);
assert!(r.success, "learn failed: {} {}", r.stdout, r.stderr);
let store = sb.store("skill", "review");
assert!(store.join("SKILL.md").is_file());
assert!(
!store.join("scratch").exists(),
"an item-link item must inherit the source-level ignore list"
);
}
#[test]
fn an_add_root_item_inherits_the_source_level_ignore_list() {
let sb = Sandbox::new();
sb.write_and_commit(
"extra/skills/foo/SKILL.md",
"---\ndescription: foo\n---\n# foo\n",
);
sb.write_and_commit("extra/skills/foo/scratch/notes.md", "draft\n");
sb.write_and_commit("mind.toml", "[source]\nignore = [\"scratch/\"]\n");
let r = sb.mind(&["meld", &sb.spec(), "--add-root", "extra", "--yes"]);
assert!(
r.success,
"meld --add-root failed: {} {}",
r.stdout, r.stderr
);
let store = sb.store("skill", "foo");
assert!(
store.join("SKILL.md").is_file(),
"the add-root item must install"
);
assert!(
!store.join("scratch").exists(),
"an add-root item must inherit the source-level ignore list"
);
}
#[test]
fn an_item_may_declare_an_explicit_empty_ignore_list_overriding_the_source() {
let sb = Sandbox::new();
sb.write_and_commit("a/SKILL.md", "---\ndescription: a\n---\n# a\n");
sb.write_and_commit("a/scratch/notes.md", "draft\n");
sb.write_and_commit(
"mind.toml",
"[source]\nignore = [\"scratch/\"]\n\n\
[[items]]\nkind = \"skill\"\nname = \"a\"\npath = \"a\"\nignore = []\n",
);
assert!(sb.mind(&["meld", &sb.spec(), "--yes"]).success);
let store = sb.store("skill", "a");
assert!(
store.join("scratch").exists(),
"an explicit empty ignore list must override, not inherit, the source list"
);
}
#[test]
fn a_bad_source_level_ignore_pattern_is_blamed_on_the_source_declaration() {
let sb = Sandbox::new();
sb.write_and_commit("skills/a/SKILL.md", "---\ndescription: a\n---\n# a\n");
sb.write_and_commit("mind.toml", "[source]\nignore = [\"/absolute\"]\n");
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(!r.success, "must be refused: {}", r.stdout);
assert!(
r.stderr.contains("mind.toml [source].ignore"),
"the error must blame the source declaration, not the item: {}",
r.stderr
);
assert!(
!r.stderr.contains("skill:a:"),
"the error must not misattribute the source-level pattern to the item: {}",
r.stderr
);
}
#[test]
fn a_bad_item_level_ignore_pattern_is_blamed_on_the_item() {
let sb = Sandbox::new();
sb.write_and_commit("a/SKILL.md", "---\ndescription: a\n---\n# a\n");
sb.write_and_commit(
"mind.toml",
"[[items]]\nkind = \"skill\"\nname = \"a\"\npath = \"a\"\nignore = [\"/absolute\"]\n",
);
let r = sb.mind(&["meld", &sb.spec(), "--yes"]);
assert!(!r.success, "must be refused: {}", r.stdout);
assert!(
r.stderr.contains("skill:a:"),
"the error must blame the declaring item: {}",
r.stderr
);
}