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(name: &str) -> Self {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-rhash-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let source = base.join(name);
Sandbox {
base: base.clone(),
source,
mind_home: base.join("mind"),
claude_home: base.join("claude"),
}
}
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 source_spec(&self) -> String {
self.source.to_string_lossy().into_owned()
}
fn git_init(&self) {
git(
&self.source,
&["-c", "init.defaultBranch=main", "init", "-q"],
);
git(&self.source, &["config", "user.email", "t@t"]);
git(&self.source, &["config", "user.name", "t"]);
git(&self.source, &["add", "-A"]);
git(&self.source, &["commit", "-qm", "initial"]);
}
}
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:?}");
}
#[test]
fn recall_flags_outdated_when_source_content_cannot_be_hashed() {
let sb = Sandbox::new("agents");
let skill_dir = sb.source.join("skills/review");
write(
&skill_dir.join("SKILL.md"),
"---\nname: review\ndescription: review the diff\n---\n# review\n",
);
sb.git_init();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success, "meld failed");
let learned = sb.mind(&["learn", "review"]);
assert!(learned.success, "learn failed: {}", learned.stderr);
let before = sb.mind(&["recall"]);
assert!(before.success, "recall failed: {}", before.stderr);
assert!(
before.stdout.contains("review"),
"recall should list the learned skill: {}",
before.stdout
);
assert!(
!before.stdout.contains("outdated"),
"a freshly learned item must not be marked outdated: {}",
before.stdout
);
let dangling = skill_dir.join("broken-link");
std::os::unix::fs::symlink(skill_dir.join("does-not-exist"), &dangling)
.expect("create dangling symlink");
let after = sb.mind(&["recall"]);
assert!(
after.success,
"recall must not abort the listing on a hash error: stdout={} stderr={}",
after.stdout, after.stderr
);
assert!(
after.stdout.contains("review"),
"the item must still be listed (best-effort marker, no abort): {}",
after.stdout
);
assert!(
after.stdout.contains("outdated"),
"a hash-computation error must count as drift and flag the item: {}",
after.stdout
);
}
fn sandbox_with_unhashable_learned_skill() -> Sandbox {
let sb = Sandbox::new("agents");
let skill_dir = sb.source.join("skills/review");
write(
&skill_dir.join("SKILL.md"),
"---\nname: review\ndescription: review the diff\n---\n# review\n",
);
sb.git_init();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success, "meld failed");
let learned = sb.mind(&["learn", "review"]);
assert!(learned.success, "learn failed: {}", learned.stderr);
let dangling = skill_dir.join("broken-link");
std::os::unix::fs::symlink(skill_dir.join("does-not-exist"), &dangling)
.expect("create dangling symlink");
sb
}
#[test]
fn recall_single_item_flags_outdated_when_source_unhashable() {
let sb = sandbox_with_unhashable_learned_skill();
let clean = Sandbox::new("agents");
let clean_dir = clean.source.join("skills/review");
write(
&clean_dir.join("SKILL.md"),
"---\nname: review\ndescription: review the diff\n---\n# review\n",
);
clean.git_init();
let clean_spec = clean.source_spec();
assert!(clean.mind(&["meld", &clean_spec]).success, "meld failed");
assert!(clean.mind(&["learn", "review"]).success, "learn failed");
let clean_detail = clean.mind(&["recall", "skill:review"]);
assert!(clean_detail.success, "clean recall failed");
assert!(
clean_detail.stdout.contains("review"),
"clean detail should show the item: {}",
clean_detail.stdout
);
assert!(
!clean_detail.stdout.contains("out of date"),
"a freshly learned item must not be out of date: {}",
clean_detail.stdout
);
let r = sb.mind(&["recall", "skill:review"]);
assert!(
r.success,
"single-item recall must not abort on a hash error: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("review"),
"the item detail must still print: {}",
r.stdout
);
assert!(
r.stdout.contains("out of date"),
"a hash error must mark the single-item detail out of date: {}",
r.stdout
);
}
#[test]
fn probe_flags_outdated_when_source_unhashable() {
let clean = Sandbox::new("agents");
write(
&clean.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: review the diff\n---\n# review\n",
);
clean.git_init();
let clean_spec = clean.source_spec();
assert!(clean.mind(&["meld", &clean_spec]).success, "meld failed");
assert!(clean.mind(&["learn", "review"]).success, "learn failed");
let clean_probe = clean.mind(&["probe", "review"]);
assert!(clean_probe.success, "clean probe failed");
assert!(
clean_probe.stdout.contains("review") && !clean_probe.stdout.contains("outdated"),
"a freshly learned item must probe as not outdated: {}",
clean_probe.stdout
);
let sb = sandbox_with_unhashable_learned_skill();
let r = sb.mind(&["probe", "review"]);
assert!(
r.success,
"probe must not abort on a hash error: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("review"),
"probe must still list the matching item: {}",
r.stdout
);
assert!(
r.stdout.contains("outdated"),
"a hash error must flag the probe row outdated: {}",
r.stdout
);
}
#[test]
fn remeld_source_status_flags_outdated_when_source_unhashable() {
let clean = Sandbox::new("agents");
write(
&clean.source.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: review the diff\n---\n# review\n",
);
clean.git_init();
let clean_spec = clean.source_spec();
assert!(clean.mind(&["meld", &clean_spec]).success, "meld failed");
assert!(clean.mind(&["learn", "review"]).success, "learn failed");
let clean_remeld = clean.mind(&["meld", &clean_spec]);
assert!(clean_remeld.success, "clean re-meld failed");
assert!(
clean_remeld.stdout.contains("review") && !clean_remeld.stdout.contains("outdated"),
"a freshly learned item must re-meld as not outdated: {}",
clean_remeld.stdout
);
let sb = sandbox_with_unhashable_learned_skill();
let spec = sb.source_spec();
let r = sb.mind(&["meld", &spec]);
assert!(
r.success,
"re-meld status view must not abort on a hash error: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("review"),
"the source status view must still list the item: {}",
r.stdout
);
assert!(
r.stdout.contains("outdated"),
"a hash error must flag the source-status row outdated: {}",
r.stdout
);
}
#[test]
fn unmeld_malformed_glob_reports_invalid_pattern_not_source_not_found() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("agents/dev.md"),
"---\ndescription: dev agent\n---\n# dev\n",
);
sb.git_init();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success, "meld failed");
let r = sb.mind(&["unmeld", "[bad", "--yes"]);
assert!(!r.success, "malformed glob must fail: {}", r.stdout);
assert!(
r.stderr.contains("not a valid glob selector"),
"expected invalid-pattern error, got stderr={} stdout={}",
r.stderr,
r.stdout
);
assert!(
!r.stderr.contains("no source named"),
"malformed glob must NOT surface as SourceNotFound: {}",
r.stderr
);
}
#[test]
fn recall_source_filter_malformed_glob_reports_invalid_pattern() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("agents/dev.md"),
"---\ndescription: dev agent\n---\n# dev\n",
);
sb.git_init();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success, "meld failed");
let r = sb.mind(&["recall", "--source", "[bad"]);
assert!(
!r.success,
"malformed --source glob must fail: {}",
r.stdout
);
assert!(
r.stderr.contains("not a valid glob selector"),
"expected invalid-pattern error, got stderr={} stdout={}",
r.stderr,
r.stdout
);
}
#[test]
fn probe_source_filter_malformed_glob_reports_invalid_pattern() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("agents/dev.md"),
"---\ndescription: dev agent\n---\n# dev\n",
);
sb.git_init();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success, "meld failed");
let r = sb.mind(&["probe", "--source", "[bad"]);
assert!(
!r.success,
"malformed --source glob must fail: {}",
r.stdout
);
assert!(
r.stderr.contains("not a valid glob selector"),
"expected invalid-pattern error, got stderr={} stdout={}",
r.stderr,
r.stdout
);
}
#[test]
fn unmeld_valid_glob_still_matches() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("agents/dev.md"),
"---\ndescription: dev agent\n---\n# dev\n",
);
sb.git_init();
let spec = sb.source_spec();
assert!(sb.mind(&["meld", &spec]).success, "meld failed");
let r = sb.mind(&["unmeld", "*", "--yes"]);
assert!(
r.success,
"a valid glob must still match and unmeld: stdout={} stderr={}",
r.stdout, r.stderr
);
}