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-rh-{}-{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()
}
}
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();
}
#[test]
fn review_source_install_emits_deprecated_field_advisory() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("mind.toml"),
"[source]\ninstall = \"make build\"\n",
);
write(
&sb.source.join("agents/dev.md"),
"---\ndescription: dev agent\n---\n# dev\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(
r.success,
"deprecated-field is advisory; review must exit 0: stdout={} stderr={}",
r.stdout, r.stderr
);
assert!(
r.stdout.contains("deprecated-field"),
"expected deprecated-field advisory in stdout: {}",
r.stdout
);
assert!(
r.stdout.contains("[[hooks]]"),
"deprecated-field advisory must mention [[hooks]]: {}",
r.stdout
);
assert!(
r.stdout.contains("event = \"install\""),
"deprecated-field advisory must mention event = \"install\": {}",
r.stdout
);
assert!(
r.stdout.contains("make build"),
"deprecated-field advisory must echo the command: {}",
r.stdout
);
}
#[test]
fn review_source_install_emits_both_install_hook_and_deprecated_field() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("mind.toml"),
"[source]\ninstall = \"npm install\"\n",
);
write(
&sb.source.join("agents/dev.md"),
"---\ndescription: dev agent\n---\n# dev\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(r.success, "advisory-only review exits 0: {}", r.stdout);
assert!(
r.stdout.contains("install-hook"),
"install-hook advisory must still be present: {}",
r.stdout
);
assert!(
r.stdout.contains("deprecated-field"),
"deprecated-field advisory must also be present: {}",
r.stdout
);
}
#[test]
fn review_hooks_table_only_no_deprecated_field() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("mind.toml"),
"[[hooks]]\nrun = \"npm install\"\nevent = \"install\"\n",
);
write(
&sb.source.join("agents/dev.md"),
"---\ndescription: dev agent\n---\n# dev\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(r.success, "advisory-only review exits 0: {}", r.stdout);
assert!(
!r.stdout.contains("deprecated-field"),
"[[hooks]]-only source must not emit deprecated-field: {}",
r.stdout
);
}
#[test]
fn review_whitespace_source_install_emits_no_deprecated_field() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("mind.toml"),
"[source]\ninstall = \" \"\n",
);
write(
&sb.source.join("agents/dev.md"),
"---\ndescription: dev agent\n---\n# dev\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(r.success, "advisory-only review exits 0: {}", r.stdout);
assert!(
!r.stdout.contains("deprecated-field"),
"whitespace-only [source].install must not emit deprecated-field: {}",
r.stdout
);
assert!(
!r.stdout.contains("install-hook"),
"whitespace-only install is absent, so no install-hook advisory: {}",
r.stdout
);
}
#[test]
fn review_hardcoded_path_other_item_carries_install_hook_safe_note() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("agents/dev.md"),
"---\ndescription: dev agent\n---\n# dev\n",
);
write(
&sb.source.join("skills/review/SKILL.md"),
"---\ndescription: review\n---\nuse ~/.claude/agents/dev.md for context\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(r.success, "advisory-only: {}", r.stdout);
assert!(
r.stdout.contains("hardcoded-path"),
"expected hardcoded-path advisory: {}",
r.stdout
);
assert!(
r.stdout.contains("intentional") || r.stdout.contains("safe"),
"hardcoded-path OtherItem advisory must note install-hook-safe: {}",
r.stdout
);
assert!(
r.stdout.contains("fragile"),
"hardcoded-path OtherItem advisory must still say fragile: {}",
r.stdout
);
}
#[test]
fn review_hardcoded_path_other_item_no_suggestion_still_install_hook_safe() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("skills/review/SKILL.md"),
"---\ndescription: review\n---\nload ~/.claude/agents/ghost.md for context\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(r.success, "advisory-only: {}", r.stdout);
assert!(
r.stdout.contains("hardcoded-path"),
"expected hardcoded-path advisory: {}",
r.stdout
);
assert!(
r.stdout.contains("intentional") || r.stdout.contains("safe"),
"no-suggestion OtherItem advisory must still note install-hook-safe: {}",
r.stdout
);
assert!(
!r.stdout.contains("; use {{"),
"a non-sibling OtherItem path should carry no token suggestion: {}",
r.stdout
);
}
#[test]
fn review_bare_tool_reference_carries_install_hook_safe_note() {
let sb = Sandbox::new("agents");
write(&sb.source.join("tools/detect/detect"), "#!/bin/sh\n");
write(
&sb.source.join("skills/review/SKILL.md"),
"---\ndescription: review\n---\nFirst run the detect helper, then review.\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(r.success, "advisory-only: {}", r.stdout);
assert!(
r.stdout.contains("bare-tool-reference"),
"expected bare-tool-reference advisory: {}",
r.stdout
);
assert!(
r.stdout.contains("intentional") || r.stdout.contains("safe"),
"bare-tool-reference advisory must note install-hook-safe: {}",
r.stdout
);
}
#[test]
fn review_hardcoded_path_own_resource_wording_unchanged() {
let sb = Sandbox::new("agents");
write(
&sb.source.join("skills/review/SKILL.md"),
"---\ndescription: review\n---\nrun ~/.claude/skills/review/resources/pr.py here\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(r.success, "advisory-only: {}", r.stdout);
assert!(
r.stdout.contains("hardcodes its own resource path"),
"OwnResource arm must keep existing wording: {}",
r.stdout
);
assert!(
r.stdout.contains("this works but assumes"),
"OwnResource arm must keep works-but-assumes wording: {}",
r.stdout
);
assert!(
r.stdout.contains("{{self}}/resources/pr.py"),
"OwnResource arm must suggest the token: {}",
r.stdout
);
}
#[test]
fn review_hardcoded_path_shared_tool_wording_distinct_from_other_item() {
let sb = Sandbox::new("agents");
write(&sb.source.join("tools/detect/detect"), "#!/bin/sh\n");
write(
&sb.source.join("skills/review/SKILL.md"),
"---\ndescription: review\n---\nrun ~/.mind/store/tool/detect/detect to analyze\n",
);
let target = sb.source_spec();
let r = sb.mind(&["review", &target]);
assert!(r.success, "advisory-only: {}", r.stdout);
let hardcoded_line = r
.stdout
.lines()
.find(|l| l.contains("hardcoded-path"))
.unwrap_or_else(|| panic!("expected hardcoded-path advisory: {}", r.stdout));
assert!(
hardcoded_line.contains("store-only"),
"SharedTool hardcoded-path advisory must say store-only: {hardcoded_line}"
);
assert!(
hardcoded_line.contains("never linked"),
"SharedTool hardcoded-path advisory must say never linked: {hardcoded_line}"
);
assert!(
!hardcoded_line.contains("intentional") && !hardcoded_line.contains("safe"),
"SharedTool hardcoded-path advisory must not carry the install-hook-safe note: \
{hardcoded_line}"
);
}