use std::path::Path;
use serde::Deserialize;
use crate::error::{ItemKind, MindError, Result};
use crate::git::validate_ref_value;
use crate::source::Pin;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HookEvent {
Install,
Update,
Uninstall,
}
impl HookEvent {
pub fn as_str(self) -> &'static str {
match self {
HookEvent::Install => "install",
HookEvent::Update => "update",
HookEvent::Uninstall => "uninstall",
}
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Hook {
pub run: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub optional: bool,
#[serde(default)]
pub event: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedHook {
pub run: String,
pub name: Option<String>,
pub optional: bool,
pub event: HookEvent,
}
impl ResolvedHook {
pub fn label(&self) -> &str {
match self.name.as_deref() {
Some(n) if !n.trim().is_empty() => n,
_ => &self.run,
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MindToml {
#[serde(default)]
pub source: SourceMeta,
#[serde(default)]
pub items: Vec<ItemDecl>,
pub discover: Option<Discover>,
#[serde(default)]
pub hooks: Vec<Hook>,
pub profiles: Option<crate::profile::ProfileTable>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SourceMeta {
pub description: Option<String>,
pub install: Option<String>,
#[serde(rename = "namespace", alias = "prefix")]
pub prefix: Option<String>,
#[serde(rename = "min-mind-version")]
pub min_mind_version: Option<String>,
#[serde(rename = "follow-branch")]
pub follow_branch: Option<String>,
#[serde(rename = "pin-tag")]
pub pin_tag: Option<String>,
#[serde(rename = "pin-ref")]
pub pin_ref: Option<String>,
#[serde(default)]
pub roots: Option<Vec<String>>,
#[serde(default)]
pub ignore: Option<Vec<String>>,
#[serde(rename = "flat-skills", default)]
pub flat_skills: bool,
}
impl SourceMeta {
pub fn pin_directive(&self, toml_path: &Path) -> Result<Option<Pin>> {
let mut set: Vec<(&str, Pin)> = Vec::new();
if let Some(b) = &self.follow_branch {
validate_ref_value(b)?;
set.push(("follow-branch", Pin::FollowBranch(b.clone())));
}
if let Some(t) = &self.pin_tag {
validate_ref_value(t)?;
set.push(("pin-tag", Pin::Tag(t.clone())));
}
if let Some(r) = &self.pin_ref {
validate_ref_value(r)?;
set.push(("pin-ref", Pin::Ref(r.clone())));
}
match set.len() {
0 => Ok(None),
1 => Ok(Some(set.remove(0).1)),
_ => {
let names: Vec<&str> = set.iter().map(|(k, _)| *k).collect();
Err(MindError::MindToml {
path: toml_path.to_path_buf(),
msg: format!(
"conflicting pin directives: {}; declare at most one of follow-branch, pin-tag, pin-ref",
names.join(", ")
),
})
}
}
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ItemDecl {
pub kind: String,
pub name: String,
pub path: String,
pub link: Option<String>,
pub description: Option<String>,
pub bin: Option<String>,
pub build: Option<String>,
pub install: Option<String>,
pub update: Option<String>,
pub uninstall: Option<String>,
#[serde(default)]
pub hooks: Vec<Hook>,
#[serde(default)]
pub ignore: Option<Vec<String>>,
}
impl ItemDecl {
pub fn resolved_item_hooks(&self, toml_path: &std::path::Path) -> Result<Vec<ResolvedHook>> {
let mut out: Vec<ResolvedHook> = Vec::new();
for (scalar, event) in [
(&self.install, HookEvent::Install),
(&self.update, HookEvent::Update),
(&self.uninstall, HookEvent::Uninstall),
] {
if let Some(cmd) = scalar {
let trimmed = cmd.trim();
if !trimmed.is_empty() {
out.push(ResolvedHook {
run: trimmed.to_owned(),
name: None,
optional: false,
event,
});
}
}
}
out.extend(self.resolved_item_array_hooks(toml_path)?);
Ok(out)
}
pub fn declares_hooks(&self) -> bool {
let scalar = [&self.install, &self.update, &self.uninstall]
.into_iter()
.flatten()
.any(|c| !c.trim().is_empty());
scalar || self.hooks.iter().any(|h| !h.run.trim().is_empty())
}
fn resolved_item_array_hooks(&self, toml_path: &std::path::Path) -> Result<Vec<ResolvedHook>> {
resolve_hook_array(&self.hooks, toml_path, &format!("item '{}'", self.name))
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ItemToml {
#[serde(default)]
pub hooks: Vec<Hook>,
#[serde(default)]
profiles: Option<toml::Value>,
}
impl ItemToml {
pub fn load(item_dir: &Path) -> Result<Option<ItemToml>> {
let file = item_dir.join("mind.toml");
let text = match crate::error::read_capped_metadata(&file) {
Ok(text) => text,
Err(MindError::Io { source, .. }) if source.kind() == std::io::ErrorKind::NotFound => {
return Ok(None);
}
Err(err) => return Err(err),
};
let parsed: ItemToml = toml::from_str(&text).map_err(|e| MindError::MindToml {
path: sanitized_path(&file),
msg: format!(
"not readable as an item manifest, whose only table is `[[hooks]]` (any other \
key is rejected): {}",
crate::sanitize::strip_ansi(&e.to_string())
),
})?;
if parsed.profiles.is_some() {
return Err(MindError::MindToml {
path: sanitized_path(&file),
msg: "[profiles] is not allowed in an item manifest; profiles are read from the \
source's root mind.toml only"
.to_string(),
});
}
Ok(Some(parsed))
}
pub fn resolved_hooks(&self, path: &Path) -> Result<Vec<ResolvedHook>> {
resolve_hook_array(&self.hooks, path, "item manifest")
}
}
fn resolve_hook_array(
hooks: &[Hook],
toml_path: &std::path::Path,
location: &str,
) -> Result<Vec<ResolvedHook>> {
let mut out: Vec<ResolvedHook> = Vec::new();
for hook in hooks {
let run = hook.run.trim();
if run.is_empty() {
continue; }
let event = match hook.event.as_deref() {
None | Some("install") => HookEvent::Install,
Some("update") => HookEvent::Update,
Some("uninstall") => HookEvent::Uninstall,
Some(e) => {
let e = crate::sanitize::strip_ansi(e);
return Err(MindError::MindToml {
path: sanitized_path(toml_path),
msg: format!(
"{location}: unknown hook event '{e}'; expected 'install', 'update', or 'uninstall'"
),
});
}
};
out.push(ResolvedHook {
run: run.to_owned(),
name: hook.name.clone(),
optional: hook.optional,
event,
});
}
Ok(out)
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Discover {
#[serde(default)]
pub skills: KindGlobs,
#[serde(default)]
pub agents: KindGlobs,
#[serde(default)]
pub rules: KindGlobs,
#[serde(default)]
pub commands: KindGlobs,
#[serde(default)]
pub workflows: KindGlobs,
#[serde(default)]
pub tools: KindGlobs,
#[serde(default)]
pub resources: KindGlobs,
#[serde(default)]
pub sources: Vec<NestedSource>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KindGlobs {
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AuthFailureAction {
Skip,
Error,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OnAuthFailure {
pub action: AuthFailureAction,
#[serde(default)]
pub message: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NestedSource {
pub source: String,
#[serde(rename = "namespace", default)]
pub namespace: Option<String>,
#[serde(rename = "as", default)]
pub alias: Option<String>,
#[serde(default)]
pub kind: Option<String>,
#[serde(default)]
pub install: bool,
#[serde(rename = "install-items", default)]
pub install_items: Option<Vec<String>>,
#[serde(default)]
pub profiles: Option<Vec<String>>,
#[serde(rename = "default-profile", default)]
pub default_profile: Option<bool>,
#[serde(rename = "follow-branch", default)]
pub follow_branch: Option<String>,
#[serde(rename = "pin-tag", default)]
pub pin_tag: Option<String>,
#[serde(rename = "pin-ref", default)]
pub pin_ref: Option<String>,
#[serde(default)]
pub roots: Option<Vec<String>>,
#[serde(rename = "add-roots", default)]
pub add_roots: Option<Vec<String>>,
#[serde(rename = "flat-skills", default)]
pub flat_skills: bool,
#[serde(default)]
pub hooks: Vec<Hook>,
#[serde(rename = "on-auth-failure", default)]
pub on_auth_failure: Option<OnAuthFailure>,
}
impl NestedSource {
pub fn effective_alias(&self) -> Option<String> {
self.namespace.clone().or_else(|| self.alias.clone())
}
pub fn item_kind(&self, toml_path: &Path) -> Result<Option<ItemKind>> {
let Some(raw) = self
.kind
.as_deref()
.map(str::trim)
.filter(|k| !k.is_empty())
else {
return Ok(None);
};
ItemKind::parse(raw)
.map(Some)
.ok_or_else(|| MindError::MindToml {
path: toml_path.to_path_buf(),
msg: format!(
"nested source '{}': unknown kind '{}'; expected agent, rule, or command",
crate::sanitize::strip_ansi(&self.source),
crate::sanitize::strip_ansi(raw)
),
})
}
pub fn validate(&self, toml_path: &Path) -> Result<()> {
if self.install
&& self
.install_items
.as_ref()
.is_some_and(|items| !items.is_empty())
{
return Err(MindError::MindToml {
path: toml_path.to_path_buf(),
msg: format!(
"nested source '{}': install = true and install-items are mutually exclusive; \
use install-items alone to offer a subset, or install = true to offer all",
self.source
),
});
}
if let Some(profiles) = &self.profiles {
if self.install && !profiles.is_empty() {
return Err(MindError::MindToml {
path: toml_path.to_path_buf(),
msg: format!(
"nested source '{}': install = true and profiles are mutually exclusive; \
use profiles alone to offer those profiles, or install = true to offer \
the source's default",
crate::sanitize::strip_ansi(&self.source)
),
});
}
for name in profiles {
if let Some(reason) = crate::profile::name_problem(name) {
return Err(MindError::MindToml {
path: toml_path.to_path_buf(),
msg: format!(
"nested source '{}': profile name '{}' {reason}",
crate::sanitize::strip_ansi(&self.source),
crate::sanitize::strip_ansi(name)
),
});
}
}
}
Ok(())
}
pub fn pin_directive(&self, toml_path: &Path) -> Result<Option<Pin>> {
let mut set: Vec<(&str, Pin)> = Vec::new();
if let Some(b) = &self.follow_branch {
validate_ref_value(b)?;
set.push(("follow-branch", Pin::FollowBranch(b.clone())));
}
if let Some(t) = &self.pin_tag {
validate_ref_value(t)?;
set.push(("pin-tag", Pin::Tag(t.clone())));
}
if let Some(r) = &self.pin_ref {
validate_ref_value(r)?;
set.push(("pin-ref", Pin::Ref(r.clone())));
}
match set.len() {
0 => Ok(None),
1 => Ok(Some(set.remove(0).1)),
_ => {
let names: Vec<&str> = set.iter().map(|(k, _)| *k).collect();
Err(MindError::MindToml {
path: toml_path.to_path_buf(),
msg: format!(
"nested source '{}': conflicting pin directives: {}; declare at most one of follow-branch, pin-tag, pin-ref",
self.source,
names.join(", ")
),
})
}
}
}
pub fn resolved_hooks(&self, toml_path: &std::path::Path) -> Result<Vec<ResolvedHook>> {
resolve_hook_array(
&self.hooks,
toml_path,
&format!("nested source '{}'", self.source),
)
}
}
impl Discover {
fn kind_globs(&self) -> [&KindGlobs; 7] {
[
&self.skills,
&self.agents,
&self.rules,
&self.commands,
&self.workflows,
&self.tools,
&self.resources,
]
}
pub fn has_item_globs(&self) -> bool {
self.kind_globs().iter().any(|g| !g.include.is_empty())
}
}
fn validate_discover_patterns(discover: &Discover, toml_path: &Path) -> Result<()> {
let all_globs: Vec<&str> = discover
.kind_globs()
.into_iter()
.flat_map(|g| g.include.iter().chain(g.exclude.iter()))
.map(String::as_str)
.collect();
for pattern in all_globs {
let pp = std::path::Path::new(pattern);
let shown = crate::sanitize::strip_ansi(pattern);
if pp.is_absolute() || pattern.starts_with('~') {
return Err(MindError::MindToml {
path: toml_path.to_path_buf(),
msg: format!(
"discover glob '{shown}' is absolute; globs must be relative to the repo root"
),
});
}
use std::path::Component;
if pp.components().any(|c| c == Component::ParentDir) {
return Err(MindError::MindToml {
path: toml_path.to_path_buf(),
msg: format!(
"discover glob '{shown}' contains '..'; globs must not escape the repo root"
),
});
}
}
Ok(())
}
pub fn is_plausible_version(s: &str) -> bool {
!s.is_empty()
&& s.split('.')
.all(|c| !c.is_empty() && c.bytes().all(|b| b.is_ascii_digit()))
}
pub fn is_plausible_release_tag(s: &str) -> bool {
let (rest, build) = match s.split_once('+') {
Some((rest, build)) => (rest, Some(build)),
None => (s, None),
};
let (base, prerelease) = match rest.split_once('-') {
Some((base, pre)) => (base, Some(pre)),
None => (rest, None),
};
if !is_plausible_version(base) {
return false;
}
if let Some(pre) = prerelease
&& !is_valid_semver_identifiers(pre)
{
return false;
}
if let Some(build) = build
&& !is_valid_semver_identifiers(build)
{
return false;
}
true
}
fn is_valid_semver_identifiers(s: &str) -> bool {
!s.is_empty()
&& s.split('.')
.all(|c| !c.is_empty() && c.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-'))
}
fn validate_version_string(val: &str, field: &str, path: &Path) -> Result<()> {
let bad = |reason: &str| {
Err(MindError::MindToml {
path: path.to_path_buf(),
msg: format!(
"field '{field}' must be a dotted numeric version \
(e.g. \"1\", \"0.7\", \"2.3.1\"); got {val:?} ({reason})"
),
})
};
if val.is_empty() {
return bad("empty string");
}
for component in val.split('.') {
if component.is_empty() {
return bad("empty component");
}
if !component.bytes().all(|b| b.is_ascii_digit()) {
return bad(&format!(
"component {component:?} is not a non-negative integer"
));
}
}
Ok(())
}
pub fn version_at_least(running: &str, required: &str) -> bool {
let parse = |v: &str| -> Vec<u64> {
let end = v.find(['-', '+']).unwrap_or(v.len());
v[..end]
.split('.')
.map(|c| c.trim().parse::<u64>().unwrap_or(0))
.collect()
};
let r = parse(running);
let req = parse(required);
for i in 0..r.len().max(req.len()) {
let a = r.get(i).copied().unwrap_or(0);
let b = req.get(i).copied().unwrap_or(0);
if a != b {
return a > b;
}
}
true
}
pub fn running_release_version() -> &'static str {
let v = env!("CARGO_PKG_VERSION");
&v[..v.find(['-', '+']).unwrap_or(v.len())]
}
pub fn unmet_min_version(mindfile: Option<&MindToml>) -> Option<&str> {
mindfile
.and_then(|m| m.source.min_mind_version.as_deref())
.filter(|required| !version_at_least(env!("CARGO_PKG_VERSION"), required))
}
fn probe_min_version(text: &str) -> Option<String> {
let table: toml::Table = toml::from_str(text).ok()?;
let v = table.get("source")?.get("min-mind-version")?.as_str()?;
is_plausible_version(v).then(|| v.to_string())
}
fn sanitized_path(path: &Path) -> std::path::PathBuf {
std::path::PathBuf::from(crate::sanitize::strip_ansi(&path.display().to_string()))
}
impl MindToml {
pub fn load(root: &Path) -> Result<Option<MindToml>> {
let file = root.join("mind.toml");
let text = match crate::error::read_capped_metadata(&file) {
Ok(text) => text,
Err(MindError::Io { source, .. }) if source.kind() == std::io::ErrorKind::NotFound => {
return Ok(None);
}
Err(err) => return Err(err),
};
let mut parsed: MindToml = toml::from_str(&text).map_err(|e| {
let msg = crate::sanitize::strip_ansi(&e.to_string());
match probe_min_version(&text) {
Some(required) if !version_at_least(env!("CARGO_PKG_VERSION"), &required) => {
MindError::MindTomlTooNew {
path: sanitized_path(&file),
required,
running: env!("CARGO_PKG_VERSION").to_string(),
msg,
}
}
_ => MindError::Toml {
path: sanitized_path(&file),
msg,
},
}
})?;
if let Some(v) = &parsed.source.min_mind_version {
validate_version_string(v, "min-mind-version", &file)?;
}
if let Some(p) = &parsed.source.prefix {
crate::namespace::validate_prefix(p)?;
}
if let Some(discover) = &parsed.discover {
validate_discover_patterns(discover, &file)?;
}
if let Some(profiles) = &parsed.profiles {
crate::profile::validate_table(profiles, &sanitized_path(&file))?;
}
if let Some(discover) = &mut parsed.discover {
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let root_abs = std::path::PathBuf::from(crate::source::absolutize_against(
&cwd,
&root.to_string_lossy(),
));
for ns in &mut discover.sources {
if crate::source::is_relative_local_spec(&ns.source) {
ns.source = crate::source::absolutize_against(&root_abs, &ns.source);
}
}
}
Ok(Some(parsed))
}
pub fn is_authoritative(&self) -> bool {
!self.items.is_empty() || self.discover.as_ref().is_some_and(|d| d.has_item_globs())
}
pub fn declares_scan_layout(&self) -> bool {
self.source.roots.is_some() || self.source.flat_skills
}
pub fn resolved_hooks(&self, toml_path: &std::path::Path) -> Result<Vec<ResolvedHook>> {
let mut out: Vec<ResolvedHook> = Vec::new();
if let Some(cmd) = &self.source.install {
let trimmed = cmd.trim();
if !trimmed.is_empty() {
out.push(ResolvedHook {
run: trimmed.to_owned(),
name: None,
optional: false,
event: HookEvent::Install,
});
}
}
out.extend(resolve_hook_array(&self.hooks, toml_path, "[source]")?);
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn root_mindfile_curates_skill_libraries_register_only() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let mt = MindToml::load(root)
.expect("root mind.toml must parse")
.expect("root mind.toml must exist");
assert!(
!mt.is_authoritative(),
"a bare [discover].sources list must keep convention discovery (DSC-35)"
);
let sources = &mt
.discover
.as_ref()
.expect("must declare [discover]")
.sources;
let urls: Vec<&str> = sources.iter().map(|s| s.source.as_str()).collect();
assert!(
urls.contains(&"https://github.com/anthropics/skills"),
"must curate anthropics/skills: {urls:?}"
);
assert!(
urls.contains(&"https://github.com/ComposioHQ/awesome-claude-skills"),
"must curate ComposioHQ/awesome-claude-skills: {urls:?}"
);
for s in sources {
assert!(
!s.install,
"curated source '{}' must be register-only (install = false)",
s.source
);
assert!(
s.install_items.is_none(),
"curated source '{}' must not pin an install subset",
s.source
);
}
}
#[test]
fn declares_scan_layout_covers_roots_and_flat_skills_but_not_sources() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let cases: &[(&str, bool, bool)] = &[
("[source]\nroots = [\"pkg\"]\n", true, false),
("[source]\nroots = []\n", true, false),
("[source]\nflat-skills = true\n", true, false),
("[source]\nflat-skills = false\n", false, false),
(
"[source]\n[discover]\nsources = [{ source = \"owner/repo\" }]\n",
false,
false,
),
("[source]\ndescription = \"x\"\n", false, false),
(
"[[items]]\nkind = \"rule\"\nname = \"style\"\npath = \"r.md\"\n",
false,
true,
),
(
"[source]\nroots = [\"pkg\"]\n[discover]\nsources = [{ source = \"owner/repo\" }]\n",
true,
false,
),
];
for (body, want_layout, want_auth) in cases {
let n = N.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("mind-mkt15-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("mind.toml"), body).unwrap();
let mt = MindToml::load(&dir)
.expect("must parse")
.expect("must exist");
assert_eq!(
mt.declares_scan_layout(),
*want_layout,
"declares_scan_layout mismatch for body: {body:?}"
);
assert_eq!(
mt.is_authoritative(),
*want_auth,
"is_authoritative mismatch for body: {body:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn nested_relative_local_source_resolves_against_the_mind_toml_directory_not_cwd() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-dsc92-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let curator = base.join("curator");
let sibling = base.join("explicit");
std::fs::create_dir_all(&curator).unwrap();
std::fs::create_dir_all(&sibling).unwrap();
std::fs::write(
curator.join("mind.toml"),
"[discover]\nsources = [{ source = \"../explicit\" }]\n",
)
.unwrap();
let mt = MindToml::load(&curator)
.expect("must parse")
.expect("file exists");
let ns = &mt
.discover
.as_ref()
.expect("must declare [discover]")
.sources[0];
assert_eq!(
ns.source,
sibling.to_string_lossy(),
"the relative nested source must resolve against the mind.toml's own directory \
({curator:?}), not the test process's cwd"
);
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn nested_absolute_or_remote_source_is_left_unchanged() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("mind-dsc92-noop-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("mind.toml"),
"[discover]\nsources = [\
{ source = \"owner/repo\" }, \
{ source = \"/already/absolute\" }\
]\n",
)
.unwrap();
let mt = MindToml::load(&dir)
.expect("must parse")
.expect("file exists");
let sources = &mt
.discover
.as_ref()
.expect("must declare [discover]")
.sources;
assert_eq!(
sources[0].source, "owner/repo",
"a remote spec is untouched"
);
assert_eq!(
sources[1].source, "/already/absolute",
"an already-absolute local path is untouched"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn every_relative_nested_source_is_rewritten_and_sibling_fields_are_preserved() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-dsc92-many-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let curator = base.join("curator");
std::fs::create_dir_all(&curator).unwrap();
std::fs::write(
curator.join("mind.toml"),
"[discover]\nsources = [\
{ source = \"owner/repo\" }, \
{ source = \"./inner/one\", namespace = \"one\", install = true }, \
{ source = \"../sibling/two\" }\
]\n",
)
.unwrap();
let mt = MindToml::load(&curator)
.expect("must parse")
.expect("file exists");
let sources = &mt
.discover
.as_ref()
.expect("must declare [discover]")
.sources;
assert_eq!(sources[0].source, "owner/repo", "remote entry untouched");
assert_eq!(
sources[1].source,
curator.join("inner/one").to_string_lossy(),
"a './' entry resolves against the mind.toml's directory"
);
assert_eq!(
sources[2].source,
base.join("sibling/two").to_string_lossy(),
"a later '../' entry must be rewritten too, not just the first entry"
);
assert_eq!(sources[1].namespace.as_deref(), Some("one"));
assert!(sources[1].install, "install flag must survive the rewrite");
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn a_relative_nested_source_is_rewritten_even_when_the_target_does_not_exist() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::SeqCst);
let curator =
std::env::temp_dir().join(format!("mind-dsc92-ghost-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&curator);
std::fs::create_dir_all(&curator).unwrap();
std::fs::write(
curator.join("mind.toml"),
"[discover]\nsources = [{ source = \"./never-created\" }]\n",
)
.unwrap();
let mt = MindToml::load(&curator)
.expect("must parse")
.expect("file exists");
assert_eq!(
mt.discover.as_ref().unwrap().sources[0].source,
curator.join("never-created").to_string_lossy(),
"the rewrite must not be gated on the target existing"
);
let _ = std::fs::remove_dir_all(&curator);
}
#[test]
fn version_comparison_orders_dotted_components() {
assert!(version_at_least("0.2.0", "0.2"));
assert!(version_at_least("0.2", "0.2.0"));
assert!(version_at_least("1.0.0", "0.9.9"));
assert!(version_at_least("0.10.0", "0.9.0"));
assert!(!version_at_least("0.1.0", "0.2"));
assert!(!version_at_least("0.1.0", "0.1.1"));
assert!(version_at_least("0.2.0-rc1", "0.2"));
}
#[test]
fn version_comparison_strips_prerelease_and_build_suffix_before_parsing() {
assert!(version_at_least("0.22.1-dev", "0.22.1"));
assert!(version_at_least("0.23.1-dev", "0.23.1"));
assert!(version_at_least("0.22.1+build.5", "0.22.1"));
assert!(version_at_least("0.22.1-dev", "0.22.0"));
assert!(!version_at_least("0.22.1-dev", "0.22.2"));
assert!(!version_at_least("0.21.9-dev", "0.22.0"));
}
#[test]
fn version_comparison_strips_a_dotted_prerelease_suffix_from_the_whole_string() {
assert!(
version_at_least("1.0.0", "1.0.0-rc.2"),
"1.0.0 must satisfy >= 1.0.0-rc.2 (both parse as [1,0,0]); \
per-component stripping would wrongly refuse this"
);
assert!(version_at_least("1.0.0-rc.2", "1.0.0"));
assert!(!version_at_least("1.0.0-rc.2", "1.0.1"));
assert!(!version_at_least("0.9.0-rc.2", "1.0.0"));
}
#[test]
fn is_plausible_version_unaffected_by_the_release_tag_sibling() {
for v in ["1", "0.7", "2.3.1", "10.0.1"] {
assert!(is_plausible_version(v), "{v:?} must still be plausible");
}
for v in ["1.2.3-rc1", "1.2.3+build", "", "1..0", "1.x", "abc", "1/2"] {
assert!(
!is_plausible_version(v),
"{v:?} must still be rejected by is_plausible_version"
);
}
}
#[test]
fn is_plausible_release_tag_accepts_dotted_numeric_and_semver_prerelease_build() {
for v in [
"1",
"0.7",
"2.3.1",
"1.2.3-rc1",
"1.2.3-rc.2",
"1.2.3-alpha-1",
"1.2.3+build.5",
"1.2.3-rc1+build.5",
"1.2.3-0",
] {
assert!(is_plausible_release_tag(v), "{v:?} must be accepted");
}
}
#[test]
fn is_plausible_release_tag_rejects_path_segments_and_traversal() {
assert!(!is_plausible_release_tag(
"1/../../../../attacker/mind/releases/download/v1"
));
assert!(!is_plausible_release_tag("../etc/passwd"));
assert!(!is_plausible_release_tag("1.0.0/evil"));
assert!(!is_plausible_release_tag("1.0.0-../.."));
assert!(!is_plausible_release_tag("1.0.0-rc1/../.."));
assert!(!is_plausible_release_tag("1.0.0-rc1/etc/passwd"));
assert!(!is_plausible_release_tag("1.0.0+../.."));
assert!(!is_plausible_release_tag("1.0.0+build/../.."));
assert!(!is_plausible_release_tag("1.0.0-rc1\\..\\.."));
assert!(!is_plausible_release_tag("1.0.0-rc 1"));
assert!(!is_plausible_release_tag("1.0.0-rc\n1"));
assert!(!is_plausible_release_tag("1.0.0-rc\x001"));
assert!(!is_plausible_release_tag(""));
assert!(!is_plausible_release_tag("-rc1"));
assert!(!is_plausible_release_tag("v1.2.3"));
assert!(!is_plausible_release_tag("1.x.0"));
}
#[test]
fn is_plausible_release_tag_rejects_further_boundary_cases() {
assert!(!is_plausible_release_tag("1.0.0-.."));
assert!(!is_plausible_release_tag("1.0.0+.."));
assert!(!is_plausible_release_tag("1.2.3-"));
assert!(!is_plausible_release_tag("1.2.3+"));
assert!(!is_plausible_release_tag("1.2.3+a+b"));
assert!(
is_plausible_release_tag("1.2.3+a"),
"sanity: a clean single build identifier is accepted"
);
assert!(!is_plausible_release_tag("1.2.3-rc\u{e9}"));
}
#[test]
fn min_mind_version_well_formed_parses_ok() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
for v in ["0.7", "2", "1.2.3", "0", "10.0.1"] {
let n = N.fetch_add(1, Ordering::SeqCst);
let dir =
std::env::temp_dir().join(format!("mind-dsc40-ok-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("mind.toml"),
format!("[source]\nmin-mind-version = \"{v}\"\n"),
)
.unwrap();
let result = MindToml::load(&dir);
assert!(
result.is_ok(),
"min-mind-version = {v:?} must parse OK, got: {result:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn unparseable_mind_toml_names_an_unmet_min_version() {
let load = |name: &str, body: &str| {
let dir =
std::env::temp_dir().join(format!("mind-dsc117-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("mind.toml"), body).unwrap();
let r = MindToml::load(&dir);
let _ = std::fs::remove_dir_all(&dir);
r
};
let err = load(
"new",
"[source]\nmin-mind-version = \"99.0\"\nfuture-key = true\n",
)
.unwrap_err();
match &err {
MindError::MindTomlTooNew { required, msg, .. } => {
assert_eq!(required, "99.0");
assert!(msg.contains("future-key"), "{msg}");
}
other => panic!("expected MindTomlTooNew, got {other:?}"),
}
assert!(err.to_string().contains("mind evolve"), "{err}");
let err = load(
"old",
"[source]\nmin-mind-version = \"0.0.1\"\nfuture-key = true\n",
)
.unwrap_err();
assert!(matches!(err, MindError::Toml { .. }), "{err:?}");
let err = load("none", "[source]\nfuture-key = true\n").unwrap_err();
assert!(matches!(err, MindError::Toml { .. }), "{err:?}");
}
#[test]
fn unmet_min_version_compares_against_the_running_binary() {
let with = |v: &str| -> MindToml {
toml::from_str(&format!("[source]\nmin-mind-version = \"{v}\"\n")).unwrap()
};
assert_eq!(unmet_min_version(Some(&with("99.0"))), Some("99.0"));
assert_eq!(unmet_min_version(Some(&with("0.0.1"))), None);
assert_eq!(
unmet_min_version(Some(&with(running_release_version()))),
None
);
assert_eq!(unmet_min_version(None), None);
}
#[test]
fn min_mind_version_malformed_is_mind_toml_error() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
for v in ["0.3-beta", "abc", "", "1.x", "0.", ".1", "1..0"] {
let n = N.fetch_add(1, Ordering::SeqCst);
let dir =
std::env::temp_dir().join(format!("mind-dsc40-bad-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("mind.toml"),
format!("[source]\nmin-mind-version = \"{v}\"\n"),
)
.unwrap();
let err = MindToml::load(&dir).unwrap_err();
match err {
MindError::MindToml { msg, .. } => {
assert!(
msg.contains("min-mind-version"),
"error must name the field for {v:?}: {msg}"
);
if !v.is_empty() {
assert!(
msg.contains(v),
"error must include the bad value {v:?}: {msg}"
);
}
}
other => panic!("expected MindError::MindToml for {v:?}, got: {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn declared_reserved_kind_prefix_is_rejected_on_load() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
for word in ["skill", "agent", "rule", "command", "tool"] {
let n = N.fetch_add(1, Ordering::SeqCst);
let dir =
std::env::temp_dir().join(format!("mind-ns25-bad-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("mind.toml"),
format!("[source]\nprefix = \"{word}\"\n"),
)
.unwrap();
let err = MindToml::load(&dir).unwrap_err();
assert!(
matches!(err, MindError::ReservedPrefix { ref prefix } if prefix == word),
"expected ReservedPrefix for declared prefix {word:?}, got: {err:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn declared_normal_prefix_loads_ok() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("mind-ns25-ok-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("mind.toml"), "[source]\nprefix = \"jk\"\n").unwrap();
let mt = MindToml::load(&dir)
.expect("normal prefix must load")
.expect("file exists");
assert_eq!(mt.source.prefix.as_deref(), Some("jk"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn normal_sized_mind_toml_is_unaffected_by_the_cap() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("mind-dsc91-ok-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("mind.toml"),
"[source]\ndescription = \"a perfectly normal source\"\n",
)
.unwrap();
let mt = MindToml::load(&dir)
.expect("normal-sized mind.toml must load fine")
.expect("file exists");
assert_eq!(
mt.source.description.as_deref(),
Some("a perfectly normal source")
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn oversized_mind_toml_is_refused_naming_the_file() {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("mind-dsc91-big-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let toml_path = dir.join("mind.toml");
let file = std::fs::File::create(&toml_path).unwrap();
file.set_len(crate::error::METADATA_SIZE_LIMIT + 1).unwrap();
drop(file);
let err = MindToml::load(&dir).expect_err("oversized mind.toml must be refused");
match &err {
MindError::MetadataTooLarge { path, .. } => {
assert_eq!(path, &toml_path, "error must name the offending file");
}
other => panic!("expected MetadataTooLarge, got: {other:?}"),
}
assert!(
err.to_string().contains(&toml_path.display().to_string()),
"error message must name the file: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn source_install_hook_parses() {
let toml = r#"
[source]
description = "tools"
install = "make build && make install"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
assert_eq!(
parsed.source.install.as_deref(),
Some("make build && make install")
);
let none: MindToml = toml::from_str("[source]\ndescription = \"x\"\n").unwrap();
assert_eq!(none.source.install, None);
}
#[test]
fn pin_directive_none_when_no_fields_set() {
let meta = SourceMeta::default();
let pin = meta
.pin_directive(Path::new("mind.toml"))
.expect("should not error");
assert!(pin.is_none(), "no directive set => None");
}
#[test]
fn pin_directive_follow_branch() {
let meta = SourceMeta {
follow_branch: Some("develop".into()),
..Default::default()
};
let pin = meta
.pin_directive(Path::new("mind.toml"))
.expect("no error");
assert_eq!(pin, Some(Pin::FollowBranch("develop".into())));
}
#[test]
fn pin_directive_tag() {
let meta = SourceMeta {
pin_tag: Some("v2.0".into()),
..Default::default()
};
let pin = meta
.pin_directive(Path::new("mind.toml"))
.expect("no error");
assert_eq!(pin, Some(Pin::Tag("v2.0".into())));
}
#[test]
fn pin_directive_ref() {
let meta = SourceMeta {
pin_ref: Some("abc123".into()),
..Default::default()
};
let pin = meta
.pin_directive(Path::new("mind.toml"))
.expect("no error");
assert_eq!(pin, Some(Pin::Ref("abc123".into())));
}
#[test]
fn pin_directive_conflict_is_an_error() {
let meta = SourceMeta {
follow_branch: Some("main".into()),
pin_tag: Some("v1.0".into()),
..Default::default()
};
let result = meta.pin_directive(Path::new("/repo/mind.toml"));
assert!(result.is_err(), "conflicting directives must error");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("conflicting pin"),
"expected 'conflicting pin' in: {err_msg}"
);
}
#[test]
fn hooks_parse_fields_correctly() {
let toml = r#"
[[hooks]]
run = "make build"
name = "Build step"
optional = true
event = "install"
[[hooks]]
run = "make clean"
event = "uninstall"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
assert_eq!(parsed.hooks.len(), 2);
let h0 = &parsed.hooks[0];
assert_eq!(h0.run, "make build");
assert_eq!(h0.name.as_deref(), Some("Build step"));
assert!(h0.optional);
assert_eq!(h0.event.as_deref(), Some("install"));
let h1 = &parsed.hooks[1];
assert_eq!(h1.run, "make clean");
assert_eq!(h1.name, None);
assert!(!h1.optional);
assert_eq!(h1.event.as_deref(), Some("uninstall"));
}
#[test]
fn hooks_preserve_declaration_order() {
let toml = r#"
[[hooks]]
run = "first"
[[hooks]]
run = "second"
[[hooks]]
run = "third"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let dummy = Path::new("mind.toml");
let resolved = parsed.resolved_hooks(dummy).expect("resolve");
assert_eq!(resolved.len(), 3);
assert_eq!(resolved[0].run, "first");
assert_eq!(resolved[1].run, "second");
assert_eq!(resolved[2].run, "third");
}
#[test]
fn legacy_install_folds_in_as_first_required_install_hook() {
let toml = r#"
[source]
install = "make legacy"
[[hooks]]
run = "npm install"
event = "install"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let dummy = Path::new("mind.toml");
let resolved = parsed.resolved_hooks(dummy).expect("resolve");
assert_eq!(resolved.len(), 2);
assert_eq!(resolved[0].run, "make legacy");
assert_eq!(resolved[0].event, HookEvent::Install);
assert!(!resolved[0].optional);
assert_eq!(resolved[0].name, None);
assert_eq!(resolved[1].run, "npm install");
assert_eq!(resolved[1].event, HookEvent::Install);
}
#[test]
fn legacy_install_only_no_hooks_table() {
let toml = r#"
[source]
install = "make build && make install"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let dummy = Path::new("mind.toml");
let resolved = parsed.resolved_hooks(dummy).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].run, "make build && make install");
assert_eq!(resolved[0].event, HookEvent::Install);
assert!(!resolved[0].optional);
}
#[test]
fn a_commands_only_discover_is_authoritative_and_validated() {
let toml = r#"
[discover]
commands = { include = ["packages/*/commands/*.md"] }
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
assert!(
parsed.is_authoritative(),
"commands globs are item globs, so they make the file authoritative"
);
for bad in ["/etc/commands/*.md", "../outside/commands/*.md"] {
let text = format!("[discover]\ncommands = {{ include = [\"{bad}\"] }}\n");
let parsed: MindToml = toml::from_str(&text).expect("parse");
let err = validate_discover_patterns(
parsed.discover.as_ref().expect("discover"),
Path::new("mind.toml"),
)
.expect_err("an unconfined commands glob must be rejected at load");
assert!(
err.to_string().contains("discover glob"),
"the rejection must name the glob: {err}"
);
}
}
#[test]
fn update_event_resolves_and_is_its_own_event() {
let toml = r#"
[[hooks]]
run = "make install"
[[hooks]]
run = "make upgrade"
name = "Upgrade"
event = "update"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let resolved = parsed
.resolved_hooks(Path::new("mind.toml"))
.expect("resolve");
assert_eq!(resolved.len(), 2);
assert_eq!(resolved[0].event, HookEvent::Install);
assert_eq!(resolved[1].event, HookEvent::Update);
assert_eq!(resolved[1].run, "make upgrade");
assert_eq!(resolved[1].label(), "Upgrade");
assert_eq!(HookEvent::Update.as_str(), "update");
}
#[test]
fn unknown_event_names_the_legal_set_including_update() {
let toml = r#"
[[hooks]]
run = "make build"
event = "upgrade"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let err = parsed
.resolved_hooks(Path::new("mind.toml"))
.expect_err("an unknown event is a schema error")
.to_string();
assert!(err.contains("'upgrade'"), "must name the bad value: {err}");
assert!(
err.contains("'install', 'update', or 'uninstall'"),
"must name the legal set: {err}"
);
}
#[test]
fn item_scalar_update_folds_in_between_install_and_uninstall() {
let decl = ItemDecl {
kind: "skill".into(),
name: "scanner".into(),
path: "skills/scanner".into(),
link: None,
description: None,
bin: None,
build: None,
install: Some("setup.sh".into()),
update: Some("migrate.sh".into()),
uninstall: Some("teardown.sh".into()),
hooks: Vec::new(),
ignore: None,
};
let resolved = decl
.resolved_item_hooks(Path::new("mind.toml"))
.expect("resolve");
let events: Vec<HookEvent> = resolved.iter().map(|h| h.event).collect();
assert_eq!(
events,
vec![HookEvent::Install, HookEvent::Update, HookEvent::Uninstall]
);
assert_eq!(resolved[1].run, "migrate.sh");
}
#[test]
fn declares_hooks_is_true_only_for_a_non_blank_declaration() {
let bare = |install: Option<&str>, hooks: Vec<Hook>| ItemDecl {
kind: "skill".into(),
name: "scanner".into(),
path: "skills/scanner".into(),
link: None,
description: None,
bin: None,
build: None,
install: install.map(str::to_string),
update: None,
uninstall: None,
hooks,
ignore: None,
};
assert!(
!bare(None, Vec::new()).declares_hooks(),
"a declaration that says nothing about hooks leaves the item free to \
declare its own"
);
assert!(
!bare(Some(" "), Vec::new()).declares_hooks(),
"a blank command is absent (HOOK-3), so it declares nothing"
);
assert!(bare(Some("setup.sh"), Vec::new()).declares_hooks());
assert!(
bare(
None,
vec![Hook {
run: "setup.sh".into(),
name: None,
optional: false,
event: None,
}],
)
.declares_hooks()
);
}
#[test]
fn item_manifest_reads_hooks_and_rejects_source_keys() {
let dir =
std::env::temp_dir().join(format!("mind-item-toml-{}-{}", std::process::id(), line!()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("mind.toml"),
"[[hooks]]\nrun = \"setup.sh\"\n\n[[hooks]]\nrun = \"migrate.sh\"\nevent = \"update\"\n",
)
.unwrap();
let parsed = ItemToml::load(&dir)
.expect("an item manifest parses")
.expect("present");
let hooks = parsed
.resolved_hooks(&dir.join("mind.toml"))
.expect("resolve");
assert_eq!(hooks.len(), 2);
assert_eq!(hooks[0].event, HookEvent::Install);
assert_eq!(hooks[1].event, HookEvent::Update);
let manifest = dir.join("mind.toml");
std::fs::write(
&manifest,
"[source]\ndescription = \"nope\"\n\n[[hooks]]\nrun = \"setup.sh\"\n",
)
.unwrap();
let err = ItemToml::load(&dir).expect_err("a source key must be rejected");
assert!(
err.to_string().contains("source"),
"the error must name the offending key: {err}"
);
assert!(
err.to_string().contains(&manifest.display().to_string()),
"the error must name the manifest it failed to read: {err}"
);
std::fs::write(
&manifest,
"[[items]]\nkind = \"skill\"\nname = \"x\"\npath = \"skills/x\"\n",
)
.unwrap();
let err = ItemToml::load(&dir).expect_err("an [[items]] table must be rejected");
assert!(
err.to_string().contains("items"),
"the error must name the offending key: {err}"
);
assert!(
err.to_string().contains(&manifest.display().to_string()),
"the error must name the manifest it failed to read: {err}"
);
std::fs::remove_file(dir.join("mind.toml")).unwrap();
assert!(ItemToml::load(&dir).expect("absent is ok").is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn default_event_is_install() {
let toml = r#"
[[hooks]]
run = "setup.sh"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let dummy = Path::new("mind.toml");
let resolved = parsed.resolved_hooks(dummy).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].event, HookEvent::Install);
}
#[test]
fn explicit_uninstall_event_resolves() {
let toml = r#"
[[hooks]]
run = "teardown.sh"
event = "uninstall"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let dummy = Path::new("mind.toml");
let resolved = parsed.resolved_hooks(dummy).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].event, HookEvent::Uninstall);
}
#[test]
fn unknown_event_returns_mind_toml_error() {
let toml = r#"
[[hooks]]
run = "do-something.sh"
event = "build"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let toml_path = Path::new("/repo/mind.toml");
let result = parsed.resolved_hooks(toml_path);
assert!(result.is_err(), "unknown event must error");
let err = result.unwrap_err();
match err {
MindError::MindToml { path, msg } => {
assert_eq!(path, toml_path);
assert!(
msg.contains("build"),
"error message should mention the bad value: {msg}"
);
assert!(
msg.contains("install") && msg.contains("uninstall"),
"error message should mention valid values: {msg}"
);
}
other => panic!("expected MindError::MindToml, got: {other:?}"),
}
}
#[test]
fn empty_run_in_hooks_is_dropped() {
let toml = r#"
[[hooks]]
run = ""
[[hooks]]
run = " "
[[hooks]]
run = "real-command"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let dummy = Path::new("mind.toml");
let resolved = parsed.resolved_hooks(dummy).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].run, "real-command");
}
#[test]
fn whitespace_legacy_install_contributes_nothing() {
let toml = r#"
[source]
install = " "
[[hooks]]
run = "npm ci"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let dummy = Path::new("mind.toml");
let resolved = parsed.resolved_hooks(dummy).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].run, "npm ci");
}
#[test]
fn resolved_hook_label_returns_name_when_set() {
let hook = ResolvedHook {
run: "make build".into(),
name: Some("Build step".into()),
optional: false,
event: HookEvent::Install,
};
assert_eq!(hook.label(), "Build step");
}
#[test]
fn resolved_hook_label_falls_back_to_command() {
let hook = ResolvedHook {
run: "make build".into(),
name: None,
optional: false,
event: HookEvent::Install,
};
assert_eq!(hook.label(), "make build");
}
#[test]
fn label_treats_empty_and_whitespace_name_as_absent() {
let make_hook = |name: Option<&str>| ResolvedHook {
run: "make build".into(),
name: name.map(str::to_owned),
optional: false,
event: HookEvent::Install,
};
assert_eq!(make_hook(Some("Build")).label(), "Build");
assert_eq!(make_hook(Some("")).label(), "make build");
assert_eq!(make_hook(Some(" ")).label(), "make build");
assert_eq!(make_hook(None).label(), "make build");
}
#[test]
fn hooks_whitespace_run_is_trimmed() {
let toml = r#"
[[hooks]]
run = " npm install "
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let dummy = Path::new("mind.toml");
let resolved = parsed.resolved_hooks(dummy).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].run, "npm install");
}
fn first_item_hooks(toml: &str) -> Result<Vec<ResolvedHook>> {
let parsed: MindToml = toml::from_str(toml).expect("parse");
let item = &parsed.items[0];
item.resolved_item_hooks(Path::new("/repo/mind.toml"))
}
#[test]
fn item_hooks_array_parses_and_preserves_order() {
let toml = r#"
[[items]]
kind = "tool"
name = "helper"
path = "tools/helper"
[[items.hooks]]
run = "first"
name = "Step one"
[[items.hooks]]
run = "second"
optional = true
event = "install"
[[items.hooks]]
run = "teardown"
event = "uninstall"
"#;
let resolved = first_item_hooks(toml).expect("resolve");
assert_eq!(resolved.len(), 3);
assert_eq!(resolved[0].run, "first");
assert_eq!(resolved[0].name.as_deref(), Some("Step one"));
assert!(!resolved[0].optional);
assert_eq!(resolved[0].event, HookEvent::Install);
assert_eq!(resolved[1].run, "second");
assert!(resolved[1].optional);
assert_eq!(resolved[1].event, HookEvent::Install);
assert_eq!(resolved[2].run, "teardown");
assert_eq!(resolved[2].event, HookEvent::Uninstall);
}
#[test]
fn item_scalar_install_uninstall_fold_in_ahead_of_array() {
let toml = r#"
[[items]]
kind = "tool"
name = "helper"
path = "tools/helper"
install = "scalar-install"
uninstall = "scalar-uninstall"
[[items.hooks]]
run = "array-install"
event = "install"
[[items.hooks]]
run = "array-uninstall"
event = "uninstall"
"#;
let resolved = first_item_hooks(toml).expect("resolve");
assert_eq!(resolved.len(), 4);
assert_eq!(resolved[0].run, "scalar-install");
assert_eq!(resolved[0].event, HookEvent::Install);
assert!(!resolved[0].optional);
assert_eq!(resolved[0].name, None);
assert_eq!(resolved[1].run, "scalar-uninstall");
assert_eq!(resolved[1].event, HookEvent::Uninstall);
assert!(!resolved[1].optional);
assert_eq!(resolved[2].run, "array-install");
assert_eq!(resolved[3].run, "array-uninstall");
}
#[test]
fn item_scalar_only_yields_one_required_hook_each() {
let toml = r#"
[[items]]
kind = "rule"
name = "style"
path = "guidelines/style.md"
install = "set-up"
uninstall = "tear-down"
"#;
let resolved = first_item_hooks(toml).expect("resolve");
assert_eq!(resolved.len(), 2);
assert_eq!(resolved[0].run, "set-up");
assert_eq!(resolved[0].event, HookEvent::Install);
assert_eq!(resolved[1].run, "tear-down");
assert_eq!(resolved[1].event, HookEvent::Uninstall);
}
#[test]
fn item_hooks_unknown_event_is_mind_toml_error() {
let toml = r#"
[[items]]
kind = "tool"
name = "helper"
path = "tools/helper"
[[items.hooks]]
run = "do-something"
event = "build"
"#;
let err = first_item_hooks(toml).unwrap_err();
match err {
MindError::MindToml { path, msg } => {
assert_eq!(path, Path::new("/repo/mind.toml"));
assert!(msg.contains("build"), "names the bad value: {msg}");
assert!(
msg.contains("install") && msg.contains("uninstall"),
"names the legal set: {msg}"
);
assert!(msg.contains("helper"), "names the offending item: {msg}");
}
other => panic!("expected MindError::MindToml, got: {other:?}"),
}
}
#[test]
fn item_hooks_empty_run_is_dropped() {
let toml = r#"
[[items]]
kind = "tool"
name = "helper"
path = "tools/helper"
[[items.hooks]]
run = ""
[[items.hooks]]
run = " "
[[items.hooks]]
run = "real-command"
"#;
let resolved = first_item_hooks(toml).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].run, "real-command");
}
#[test]
fn item_scalar_whitespace_contributes_nothing() {
let toml = r#"
[[items]]
kind = "tool"
name = "helper"
path = "tools/helper"
install = " "
[[items.hooks]]
run = "array-install"
"#;
let resolved = first_item_hooks(toml).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].run, "array-install");
}
#[test]
fn nested_source_parses_all_curator_fields() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
namespace = "or"
install = true
follow-branch = "main"
roots = ["packages", "tools"]
[[discover.sources.hooks]]
run = "make setup"
name = "Setup"
optional = true
event = "install"
[[discover.sources.hooks]]
run = "make teardown"
event = "uninstall"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
assert_eq!(ns.source, "github:owner/repo");
assert_eq!(ns.namespace.as_deref(), Some("or"));
assert_eq!(ns.effective_alias(), Some("or".to_string()));
assert!(ns.install);
assert_eq!(ns.follow_branch.as_deref(), Some("main"));
assert_eq!(
ns.roots.as_deref(),
Some(&["packages".to_owned(), "tools".to_owned()][..])
);
assert_eq!(ns.hooks.len(), 2);
let h0 = &ns.hooks[0];
assert_eq!(h0.run, "make setup");
assert_eq!(h0.name.as_deref(), Some("Setup"));
assert!(h0.optional);
assert_eq!(h0.event.as_deref(), Some("install"));
let h1 = &ns.hooks[1];
assert_eq!(h1.run, "make teardown");
assert_eq!(h1.event.as_deref(), Some("uninstall"));
}
#[test]
fn nested_source_deny_unknown_fields_still_rejects_typo() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
follow_branch = "main"
"#;
let result: std::result::Result<MindToml, _> = toml::from_str(toml);
assert!(
result.is_err(),
"underscore variant of follow-branch must be rejected"
);
}
#[test]
fn nested_source_unknown_top_level_key_rejected() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
typo-key = "value"
"#;
let result: std::result::Result<MindToml, _> = toml::from_str(toml);
assert!(result.is_err(), "unknown keys must be rejected");
}
#[test]
fn namespace_key_parses_as_canonical_form() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
namespace = "pfx"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
assert_eq!(ns.namespace.as_deref(), Some("pfx"));
assert!(ns.alias.is_none());
assert_eq!(ns.effective_alias(), Some("pfx".to_string()));
}
#[test]
fn as_key_still_accepted_as_legacy_alias() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
as = "pfx"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
assert_eq!(ns.alias.as_deref(), Some("pfx"));
assert!(ns.namespace.is_none());
assert_eq!(ns.effective_alias(), Some("pfx".to_string()));
}
#[test]
fn namespace_takes_precedence_over_as_when_both_present() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
namespace = "ns-wins"
as = "as-loses"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
assert_eq!(ns.effective_alias(), Some("ns-wins".to_string()));
}
#[test]
fn effective_alias_returns_none_when_neither_set() {
let toml = "[[discover.sources]]\nsource = \"github:owner/repo\"\n";
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
assert!(ns.namespace.is_none());
assert!(ns.alias.is_none());
assert_eq!(ns.effective_alias(), None);
}
#[test]
fn nested_source_pin_directive_returns_follow_branch_pin() {
let ns = NestedSource {
source: "x".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: Some("develop".into()),
pin_tag: None,
pin_ref: None,
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
assert_eq!(
ns.pin_directive(Path::new("mind.toml")).expect("no error"),
Some(Pin::FollowBranch("develop".into()))
);
}
#[test]
fn nested_source_pin_directive_none_when_unset() {
let ns = NestedSource {
source: "x".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: None,
pin_tag: None,
pin_ref: None,
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
assert!(
ns.pin_directive(Path::new("mind.toml"))
.expect("no error")
.is_none()
);
}
#[test]
fn nested_source_resolved_hooks_order_preserved() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
[[discover.sources.hooks]]
run = "first"
[[discover.sources.hooks]]
run = "second"
[[discover.sources.hooks]]
run = "third"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let resolved = ns.resolved_hooks(Path::new("mind.toml")).expect("resolve");
assert_eq!(resolved.len(), 3);
assert_eq!(resolved[0].run, "first");
assert_eq!(resolved[1].run, "second");
assert_eq!(resolved[2].run, "third");
}
#[test]
fn nested_source_resolved_hooks_default_event_is_install() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
[[discover.sources.hooks]]
run = "setup.sh"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let resolved = ns.resolved_hooks(Path::new("mind.toml")).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].event, HookEvent::Install);
}
#[test]
fn nested_source_resolved_hooks_explicit_uninstall() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
[[discover.sources.hooks]]
run = "teardown.sh"
event = "uninstall"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let resolved = ns.resolved_hooks(Path::new("mind.toml")).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].event, HookEvent::Uninstall);
}
#[test]
fn nested_source_resolved_hooks_unknown_event_is_mind_toml_error() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
[[discover.sources.hooks]]
run = "do-something"
event = "build"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let toml_path = Path::new("/repo/mind.toml");
let err = ns.resolved_hooks(toml_path).unwrap_err();
match err {
MindError::MindToml { path, msg } => {
assert_eq!(path, toml_path);
assert!(msg.contains("build"), "names the bad value: {msg}");
assert!(
msg.contains("install") && msg.contains("uninstall"),
"names the legal set: {msg}"
);
}
other => panic!("expected MindError::MindToml, got: {other:?}"),
}
}
#[test]
fn nested_source_resolved_hooks_blank_run_dropped() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
[[discover.sources.hooks]]
run = ""
[[discover.sources.hooks]]
run = " "
[[discover.sources.hooks]]
run = "real-command"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let resolved = ns.resolved_hooks(Path::new("mind.toml")).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].run, "real-command");
}
#[test]
fn nested_source_resolved_hooks_run_trimmed() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
[[discover.sources.hooks]]
run = " npm install "
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let resolved = ns.resolved_hooks(Path::new("mind.toml")).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].run, "npm install");
}
#[test]
fn nested_source_no_legacy_install_fold_in() {
let ns = NestedSource {
source: "x".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: None,
pin_tag: None,
pin_ref: None,
roots: None,
add_roots: None,
hooks: vec![Hook {
run: "array-only".into(),
name: None,
optional: false,
event: None,
}],
flat_skills: false,
on_auth_failure: None,
};
let resolved = ns.resolved_hooks(Path::new("mind.toml")).expect("resolve");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].run, "array-only");
}
#[test]
fn nested_source_follow_branch_pin_directive_is_always_follow_branch_variant() {
for branch in ["main", "develop", "v1", "release/2.0", "abc123"] {
let ns = NestedSource {
source: "x".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: Some(branch.into()),
pin_tag: None,
pin_ref: None,
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
match ns.pin_directive(Path::new("mind.toml")).expect("no error") {
Some(Pin::FollowBranch(b)) => assert_eq!(b, branch),
other => panic!("expected Pin::FollowBranch({branch:?}); got {other:?}"),
}
}
}
#[test]
fn multiple_nested_sources_keep_independent_hook_arrays() {
let toml = r#"
[[discover.sources]]
source = "github:owner/first"
[[discover.sources.hooks]]
run = "first-hook"
[[discover.sources]]
source = "github:owner/second"
[[discover.sources.hooks]]
run = "second-hook"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let sources = &parsed.discover.as_ref().unwrap().sources;
assert_eq!(sources.len(), 2);
let dummy = Path::new("mind.toml");
let first = sources[0].resolved_hooks(dummy).expect("resolve");
let second = sources[1].resolved_hooks(dummy).expect("resolve");
assert_eq!(first.len(), 1, "first entry sees only its own hook");
assert_eq!(first[0].run, "first-hook");
assert_eq!(second.len(), 1, "second entry sees only its own hook");
assert_eq!(second[0].run, "second-hook");
assert!(
first.iter().all(|h| h.run != "second-hook"),
"second entry's hook leaked into the first"
);
assert!(
second.iter().all(|h| h.run != "first-hook"),
"first entry's hook leaked into the second"
);
}
#[test]
fn nested_source_explicit_empty_roots_is_some_empty_not_none() {
let explicit = r#"
[[discover.sources]]
source = "github:owner/repo"
roots = []
"#;
let parsed: MindToml = toml::from_str(explicit).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
assert_eq!(
ns.roots.as_deref(),
Some(&[][..]),
"explicit roots = [] must be Some(empty), not None"
);
let unset = r#"
[[discover.sources]]
source = "github:owner/repo"
"#;
let parsed: MindToml = toml::from_str(unset).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
assert!(
ns.roots.is_none(),
"unset roots must be None, distinct from the empty list"
);
}
#[test]
fn install_items_absent_parses_to_none() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
install = true
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
assert!(
ns.install_items.is_none(),
"absent install-items must be None"
);
assert!(ns.install, "install = true must still parse");
}
#[test]
fn install_items_empty_list_parses_to_some_empty() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
install-items = []
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
assert_eq!(
ns.install_items.as_deref(),
Some(&[][..]),
"install-items = [] must be Some(empty)"
);
}
#[test]
fn install_items_non_empty_list_parses_correctly() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
install-items = ["skill:review", "agent:dev"]
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let items = ns.install_items.as_deref().expect("must be Some");
assert_eq!(items.len(), 2);
assert_eq!(items[0], "skill:review");
assert_eq!(items[1], "agent:dev");
}
#[test]
fn install_items_three_way_distinction() {
let absent: MindToml = toml::from_str("[[discover.sources]]\nsource = \"x\"\n").unwrap();
let empty: MindToml =
toml::from_str("[[discover.sources]]\nsource = \"x\"\ninstall-items = []\n").unwrap();
let nonempty: MindToml = toml::from_str(
"[[discover.sources]]\nsource = \"x\"\ninstall-items = [\"skill:foo\"]\n",
)
.unwrap();
let ns_absent = &absent.discover.as_ref().unwrap().sources[0];
let ns_empty = &empty.discover.as_ref().unwrap().sources[0];
let ns_nonempty = &nonempty.discover.as_ref().unwrap().sources[0];
assert!(ns_absent.install_items.is_none(), "absent => None");
assert_eq!(
ns_empty.install_items.as_deref(),
Some(&[][..]),
"empty => Some([])"
);
assert_eq!(
ns_nonempty.install_items.as_deref().unwrap().len(),
1,
"non-empty => Some([..])"
);
}
#[test]
fn install_true_with_nonempty_install_items_is_mind_toml_error() {
let ns = NestedSource {
source: "github:owner/repo".into(),
namespace: None,
kind: None,
alias: None,
install: true,
install_items: Some(vec!["skill:review".into()]),
profiles: None,
default_profile: None,
follow_branch: None,
pin_tag: None,
pin_ref: None,
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
let toml_path = Path::new("/super/mind.toml");
let err = ns.validate(toml_path).unwrap_err();
match err {
MindError::MindToml { path, msg } => {
assert_eq!(path, toml_path);
assert!(
msg.contains("mutually exclusive"),
"error must say mutually exclusive: {msg}"
);
assert!(
msg.contains("github:owner/repo"),
"error must name the source: {msg}"
);
}
other => panic!("expected MindError::MindToml, got: {other:?}"),
}
}
#[test]
fn install_true_with_empty_install_items_is_ok() {
let ns = NestedSource {
source: "github:owner/repo".into(),
namespace: None,
kind: None,
alias: None,
install: true,
install_items: Some(vec![]), profiles: None,
default_profile: None,
follow_branch: None,
pin_tag: None,
pin_ref: None,
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
assert!(
ns.validate(Path::new("mind.toml")).is_ok(),
"install = true + install-items = [] must not error (both say 'install nothing')"
);
}
#[test]
fn install_false_with_nonempty_install_items_is_ok() {
let ns = NestedSource {
source: "github:owner/repo".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: Some(vec!["skill:review".into(), "agent:dev".into()]),
profiles: None,
default_profile: None,
follow_branch: None,
pin_tag: None,
pin_ref: None,
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
assert!(
ns.validate(Path::new("mind.toml")).is_ok(),
"install-items alone (install = false) must not error"
);
}
#[test]
fn nested_pin_directive_none_when_no_pin_set() {
let toml = "[[discover.sources]]\nsource = \"github:owner/repo\"\n";
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let pin = ns
.pin_directive(Path::new("mind.toml"))
.expect("no conflict");
assert!(pin.is_none(), "no pin set => None");
}
#[test]
fn nested_pin_directive_follow_branch() {
let toml =
"[[discover.sources]]\nsource = \"github:owner/repo\"\nfollow-branch = \"main\"\n";
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let pin = ns
.pin_directive(Path::new("mind.toml"))
.expect("no conflict");
assert_eq!(pin, Some(Pin::FollowBranch("main".into())));
}
#[test]
fn nested_pin_directive_pin_tag_parses_and_resolves() {
let toml = "[[discover.sources]]\nsource = \"github:owner/repo\"\npin-tag = \"v2.0\"\n";
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let pin = ns
.pin_directive(Path::new("mind.toml"))
.expect("no conflict");
assert_eq!(pin, Some(Pin::Tag("v2.0".into())));
}
#[test]
fn nested_pin_directive_pin_ref_parses_and_resolves() {
let sha = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
let toml =
format!("[[discover.sources]]\nsource = \"github:owner/repo\"\npin-ref = \"{sha}\"\n");
let parsed: MindToml = toml::from_str(&toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let pin = ns
.pin_directive(Path::new("mind.toml"))
.expect("no conflict");
assert_eq!(pin, Some(Pin::Ref(sha.into())));
}
#[test]
fn nested_pin_directive_conflict_follow_branch_and_pin_tag_is_error() {
let toml = "[[discover.sources]]\nsource = \"github:owner/repo\"\nfollow-branch = \"main\"\npin-tag = \"v1\"\n";
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let result = ns.pin_directive(Path::new("/super/mind.toml"));
assert!(result.is_err(), "conflicting pin directives must error");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("conflicting pin"),
"error must mention 'conflicting pin': {err_msg}"
);
}
#[test]
fn nested_pin_directive_conflict_all_three_is_error() {
let toml = "[[discover.sources]]\nsource = \"github:owner/repo\"\nfollow-branch = \"main\"\npin-tag = \"v1\"\npin-ref = \"abc123\"\n";
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let result = ns.pin_directive(Path::new("/super/mind.toml"));
assert!(result.is_err(), "three pin directives must error");
}
#[test]
fn source_meta_pin_rejects_leading_dash_in_follow_branch() {
let meta = SourceMeta {
follow_branch: Some("--upload-pack=touch /tmp/pwned".into()),
..Default::default()
};
let err = meta.pin_directive(Path::new("mind.toml")).unwrap_err();
assert!(
matches!(err, crate::error::MindError::InvalidRef { .. }),
"expected InvalidRef, got: {err}"
);
}
#[test]
fn source_meta_pin_rejects_leading_dash_in_pin_tag() {
let meta = SourceMeta {
pin_tag: Some("-malicious".into()),
..Default::default()
};
let err = meta.pin_directive(Path::new("mind.toml")).unwrap_err();
assert!(
matches!(err, crate::error::MindError::InvalidRef { .. }),
"expected InvalidRef, got: {err}"
);
}
#[test]
fn source_meta_pin_rejects_leading_dash_in_pin_ref() {
let meta = SourceMeta {
pin_ref: Some("--no-tags".into()),
..Default::default()
};
let err = meta.pin_directive(Path::new("mind.toml")).unwrap_err();
assert!(
matches!(err, crate::error::MindError::InvalidRef { .. }),
"expected InvalidRef, got: {err}"
);
}
#[test]
fn source_meta_pin_rejects_empty_follow_branch() {
let meta = SourceMeta {
follow_branch: Some("".into()),
..Default::default()
};
let err = meta.pin_directive(Path::new("mind.toml")).unwrap_err();
assert!(
matches!(err, crate::error::MindError::InvalidRef { .. }),
"expected InvalidRef, got: {err}"
);
}
#[test]
fn source_meta_pin_accepts_valid_values() {
for (field, pin) in [
("follow-branch", "main"),
("pin-tag", "v2.0"),
("pin-ref", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
] {
let meta = match field {
"follow-branch" => SourceMeta {
follow_branch: Some(pin.into()),
..Default::default()
},
"pin-tag" => SourceMeta {
pin_tag: Some(pin.into()),
..Default::default()
},
_ => SourceMeta {
pin_ref: Some(pin.into()),
..Default::default()
},
};
assert!(
meta.pin_directive(Path::new("mind.toml")).is_ok(),
"expected ok for {field}={pin:?}"
);
}
}
#[test]
fn nested_source_pin_rejects_leading_dash_in_follow_branch() {
let ns = NestedSource {
source: "x".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: Some("--upload-pack=touch /tmp/pwned".into()),
pin_tag: None,
pin_ref: None,
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
let err = ns.pin_directive(Path::new("mind.toml")).unwrap_err();
assert!(
matches!(err, crate::error::MindError::InvalidRef { .. }),
"expected InvalidRef, got: {err}"
);
}
#[test]
fn nested_source_pin_rejects_leading_dash_in_pin_tag() {
let ns = NestedSource {
source: "x".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: None,
pin_tag: Some("-evil".into()),
pin_ref: None,
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
let err = ns.pin_directive(Path::new("mind.toml")).unwrap_err();
assert!(
matches!(err, crate::error::MindError::InvalidRef { .. }),
"expected InvalidRef, got: {err}"
);
}
#[test]
fn nested_source_pin_rejects_leading_dash_in_pin_ref() {
let ns = NestedSource {
source: "x".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: None,
pin_tag: None,
pin_ref: Some("--depth=1".into()),
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
let err = ns.pin_directive(Path::new("mind.toml")).unwrap_err();
assert!(
matches!(err, crate::error::MindError::InvalidRef { .. }),
"expected InvalidRef, got: {err}"
);
}
#[test]
fn nested_source_pin_rejects_whitespace_in_ref() {
let ns = NestedSource {
source: "x".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: Some("main branch".into()),
pin_tag: None,
pin_ref: None,
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
let err = ns.pin_directive(Path::new("mind.toml")).unwrap_err();
assert!(
matches!(err, crate::error::MindError::InvalidRef { .. }),
"expected InvalidRef for whitespace in branch name, got: {err}"
);
}
#[test]
fn nested_source_pin_rejects_dotdot_in_ref() {
let ns = NestedSource {
source: "x".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: None,
pin_tag: None,
pin_ref: Some("main..HEAD".into()),
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
let err = ns.pin_directive(Path::new("mind.toml")).unwrap_err();
assert!(
matches!(err, crate::error::MindError::InvalidRef { .. }),
"expected InvalidRef for '..' in ref, got: {err}"
);
}
#[test]
fn nested_source_pin_accepts_valid_values() {
let ns = NestedSource {
source: "x".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: None,
pin_tag: None,
pin_ref: Some("cafebabecafebabecafebabecafebabecafebabe".into()),
roots: None,
add_roots: None,
hooks: vec![],
flat_skills: false,
on_auth_failure: None,
};
assert!(
ns.pin_directive(Path::new("mind.toml")).is_ok(),
"valid SHA must pass pin validation"
);
}
fn write_mind_toml(text: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("mind-dsc66-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("mind.toml"), text).unwrap();
dir
}
#[test]
fn untrusted_source_mind_toml_pin_is_rejected_before_any_git_call() {
for (field, payload) in [
("follow-branch", "--upload-pack=touch /tmp/pwned"),
("pin-tag", "--upload-pack=touch /tmp/pwned"),
("pin-ref", "--upload-pack=touch /tmp/pwned"),
] {
let dir = write_mind_toml(&format!("[source]\n{field} = \"{payload}\"\n"));
let parsed = MindToml::load(&dir)
.expect("load ok")
.expect("mind.toml present");
let err = parsed
.source
.pin_directive(&dir.join("mind.toml"))
.expect_err("malicious pin must be rejected");
assert!(
matches!(err, MindError::InvalidRef { .. }),
"expected InvalidRef for {field}={payload:?}, got: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn untrusted_nested_source_mind_toml_pin_is_rejected_before_any_git_call() {
for (field, payload) in [
("follow-branch", "--upload-pack=touch /tmp/pwned"),
("pin-tag", "-x"),
("pin-ref", "main..HEAD"),
] {
let dir = write_mind_toml(&format!(
"[[discover.sources]]\nsource = \"github:owner/repo\"\n{field} = \"{payload}\"\n"
));
let parsed = MindToml::load(&dir)
.expect("load ok")
.expect("mind.toml present");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let err = ns
.pin_directive(&dir.join("mind.toml"))
.expect_err("malicious nested pin must be rejected");
assert!(
matches!(err, MindError::InvalidRef { .. }),
"expected InvalidRef for nested {field}={payload:?}, got: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn untrusted_mind_toml_with_legitimate_pin_loads_and_resolves() {
let cases = [
(
"follow-branch = \"release/2.0\"",
Pin::FollowBranch("release/2.0".into()),
),
("pin-tag = \"v1.2.3\"", Pin::Tag("v1.2.3".into())),
(
"pin-ref = \"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\"",
Pin::Ref("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".into()),
),
];
for (line, expect) in cases {
let dir = write_mind_toml(&format!("[source]\n{line}\n"));
let parsed = MindToml::load(&dir).unwrap().unwrap();
let pin = parsed
.source
.pin_directive(&dir.join("mind.toml"))
.expect("legitimate pin must resolve");
assert_eq!(pin, Some(expect), "for line {line:?}");
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn nested_pin_directive_pin_ref_parses_back_from_dump_output() {
let sha = "cafebabecafebabecafebabecafebabecafebabe";
let toml_text = format!(
"[source]\ndescription = \"Generated by mind dump.\"\n\
[[discover.sources]]\nsource = \"/path/to/repo\"\npin-ref = \"{sha}\"\ninstall = false\n"
);
let parsed: MindToml = toml::from_str(&toml_text).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let pin = ns
.pin_directive(Path::new("mind.toml"))
.expect("no conflict");
assert_eq!(
pin,
Some(Pin::Ref(sha.into())),
"pin-ref from dump output must round-trip as Pin::Ref"
);
}
#[test]
fn on_auth_failure_skip_parses() {
let toml = r#"
[[discover.sources]]
source = "github:owner/private-repo"
on-auth-failure = { action = "skip" }
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let cfg = ns
.on_auth_failure
.as_ref()
.expect("on-auth-failure must be Some");
assert_eq!(cfg.action, AuthFailureAction::Skip);
assert!(cfg.message.is_none());
}
#[test]
fn on_auth_failure_error_with_message_parses() {
let toml = r#"
[[discover.sources]]
source = "github:owner/private-repo"
on-auth-failure = { action = "error", message = "Configure credentials: https://example.com/auth" }
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let cfg = ns
.on_auth_failure
.as_ref()
.expect("on-auth-failure must be Some");
assert_eq!(cfg.action, AuthFailureAction::Error);
assert_eq!(
cfg.message.as_deref(),
Some("Configure credentials: https://example.com/auth")
);
}
#[test]
fn on_auth_failure_absent_parses_to_none() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
assert!(
ns.on_auth_failure.is_none(),
"absent on-auth-failure must be None"
);
}
#[test]
fn on_auth_failure_unknown_action_is_mind_toml_error() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
on-auth-failure = { action = "warn" }
"#;
let result: std::result::Result<MindToml, _> = toml::from_str(toml);
assert!(
result.is_err(),
"unknown on-auth-failure action must fail at parse time"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("warn") || err_msg.contains("error") || err_msg.contains("expected"),
"error must describe the invalid variant: {err_msg}"
);
}
#[test]
fn on_auth_failure_validate_with_skip_returns_ok() {
let ns = NestedSource {
source: "github:owner/repo".into(),
namespace: None,
kind: None,
alias: None,
install: false,
install_items: None,
profiles: None,
default_profile: None,
follow_branch: None,
pin_tag: None,
pin_ref: None,
roots: None,
add_roots: None,
flat_skills: false,
hooks: vec![],
on_auth_failure: Some(OnAuthFailure {
action: AuthFailureAction::Skip,
message: None,
}),
};
assert!(
ns.validate(Path::new("mind.toml")).is_ok(),
"a valid on-auth-failure must not error from validate()"
);
}
#[test]
fn on_auth_failure_unknown_field_in_inline_table_is_parse_error() {
let toml = r#"
[[discover.sources]]
source = "github:owner/repo"
on-auth-failure = { action = "skip", typo-key = "x" }
"#;
let result: std::result::Result<MindToml, _> = toml::from_str(toml);
assert!(
result.is_err(),
"unknown field in on-auth-failure must be rejected"
);
}
#[test]
fn on_auth_failure_skip_with_message_parses() {
let toml = r#"
[[discover.sources]]
source = "owner/private-repo"
on-auth-failure = { action = "skip", message = "Configure credentials: https://example.com/auth" }
"#;
let parsed: MindToml = toml::from_str(toml).expect("parse");
let ns = &parsed.discover.as_ref().unwrap().sources[0];
let cfg = ns
.on_auth_failure
.as_ref()
.expect("on-auth-failure must be Some");
assert_eq!(cfg.action, AuthFailureAction::Skip);
assert_eq!(
cfg.message.as_deref(),
Some("Configure credentials: https://example.com/auth")
);
}
use std::sync::atomic::{AtomicU32, Ordering};
static MINDFILE_N: AtomicU32 = AtomicU32::new(0);
fn make_tmp_root() -> std::path::PathBuf {
let n = MINDFILE_N.fetch_add(1, Ordering::SeqCst);
let p = std::env::temp_dir().join(format!("mind-mindfile-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn discover_glob_absolute_pattern_is_rejected_at_load() {
let root = make_tmp_root();
std::fs::write(
root.join("mind.toml"),
"[discover]\nagents = { include = [\"/etc/*.md\"] }\n",
)
.unwrap();
let err = MindToml::load(&root).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("absolute"),
"error must mention 'absolute': {msg}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn discover_glob_parent_dir_pattern_is_rejected_at_load() {
let root = make_tmp_root();
std::fs::write(
root.join("mind.toml"),
"[discover]\nskills = { include = [\"../other-repo/skills/*/SKILL.md\"] }\n",
)
.unwrap();
let err = MindToml::load(&root).unwrap_err();
let msg = err.to_string();
assert!(msg.contains(".."), "error must mention '..': {msg}");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn discover_glob_exclude_absolute_pattern_is_rejected_at_load() {
let root = make_tmp_root();
std::fs::write(
root.join("mind.toml"),
"[discover]\nagents = { include = [\"agents/*.md\"], exclude = [\"/tmp/*.md\"] }\n",
)
.unwrap();
let err = MindToml::load(&root).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("absolute"),
"error must mention 'absolute': {msg}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn source_prefix_parses_via_namespace_alias() {
let toml = "[source]\nnamespace = \"jk\"\n";
let parsed: MindToml = toml::from_str(toml).expect("parse");
assert_eq!(
parsed.source.prefix.as_deref(),
Some("jk"),
"namespace alias must populate the prefix field: {parsed:?}"
);
}
#[test]
fn source_prefix_parses_via_legacy_prefix_key() {
let toml = "[source]\nprefix = \"jk\"\n";
let parsed: MindToml = toml::from_str(toml).expect("parse");
assert_eq!(
parsed.source.prefix.as_deref(),
Some("jk"),
"legacy prefix key must still work: {parsed:?}"
);
}
fn prof_dir(body: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("mind-prof-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("mind.toml"), body).unwrap();
dir
}
#[test]
fn profiles_table_parses_and_is_not_authoritative() {
let dir = prof_dir(
"[profiles]\ndefault = [\"basic\"]\nbasic = [\"skill:review\", \"rule:style\"]\n\
full = [\"basic\", \"agent:*\"]\nall = [\"*\"]\n",
);
let mt = MindToml::load(&dir).unwrap().unwrap();
assert!(
!mt.is_authoritative(),
"[profiles] alone must keep convention discovery"
);
assert!(!mt.declares_scan_layout());
let p = mt.profiles.as_ref().expect("profiles parsed");
assert_eq!(p["full"], vec!["basic", "agent:*"]);
assert_eq!(p.len(), 4);
let _ = std::fs::remove_dir_all(&dir);
let dir = prof_dir(
"[profiles]\np = [\"rule:style\"]\n\
[[items]]\nkind = \"rule\"\nname = \"style\"\npath = \"r.md\"\n",
);
assert!(MindToml::load(&dir).unwrap().unwrap().is_authoritative());
let _ = std::fs::remove_dir_all(&dir);
let dir = prof_dir("[source]\ndescription = \"x\"\n");
assert!(MindToml::load(&dir).unwrap().unwrap().profiles.is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn invalid_profiles_table_fails_load_as_mindtoml() {
for (body, needle) in [
("[profiles]\nBad = []\n", "profile name 'Bad'"),
("[profiles]\np = [\"widget:x\"]\n", "unknown kind 'widget'"),
("[profiles]\np = [\"nope\"]\n", "does not declare"),
("[profiles]\np = [\"p\"]\n", "cycle: p -> p"),
] {
let dir = prof_dir(body);
match MindToml::load(&dir) {
Err(MindError::MindToml { msg, .. }) => {
assert!(msg.starts_with("[profiles]: "), "{msg}");
assert!(msg.contains(needle), "{body:?}: {msg}");
}
other => panic!("{body:?}: expected MindToml, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn profiles_in_a_scoped_item_manifest_are_rejected() {
let dir = prof_dir("[profiles]\nbasic = [\"skill:x\"]\n");
match ItemToml::load(&dir) {
Err(MindError::MindToml { msg, .. }) => {
assert!(msg.contains("[profiles]"), "{msg}");
assert!(msg.contains("root mind.toml only"), "{msg}");
}
other => panic!("expected MindToml, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
let dir = prof_dir("[[hooks]]\nrun = \"true\"\n");
assert!(ItemToml::load(&dir).unwrap().is_some());
let _ = std::fs::remove_dir_all(&dir);
}
fn nested(body: &str) -> NestedSource {
let mt: MindToml = toml::from_str(body).expect("parse");
mt.discover.unwrap().sources.into_iter().next().unwrap()
}
#[test]
fn nested_profiles_parse_and_validate() {
let ns = nested(
"[discover]\nsources = [{ source = \"o/r\", profiles = [\"full\"], \
default-profile = false, install-items = [\"skill:x\"] }]\n",
);
assert_eq!(ns.profiles.as_deref(), Some(&["full".to_string()][..]));
assert_eq!(ns.default_profile, Some(false));
ns.validate(Path::new("mind.toml"))
.expect("profiles with install-items is allowed (the union installs)");
let sel = crate::profile::Selection::from_nested(&ns);
assert!(!sel.include_default);
assert_eq!(sel.named, vec!["full"]);
let plain = nested("[discover]\nsources = [{ source = \"o/r\" }]\n");
assert_eq!(plain.profiles, None);
assert_eq!(plain.default_profile, None);
assert!(crate::profile::Selection::from_nested(&plain).include_default);
let both = nested(
"[discover]\nsources = [{ source = \"o/r\", install = true, profiles = [\"full\"] }]\n",
);
match both.validate(Path::new("mind.toml")) {
Err(MindError::MindToml { msg, .. }) => {
assert!(msg.contains("mutually exclusive"), "{msg}");
assert!(msg.contains("o/r"), "{msg}");
}
other => panic!("expected MindToml, got {other:?}"),
}
let bad = nested("[discover]\nsources = [{ source = \"o/r\", profiles = [\"Full\"] }]\n");
match bad.validate(Path::new("mind.toml")) {
Err(MindError::MindToml { msg, .. }) => assert!(msg.contains("'Full'"), "{msg}"),
other => panic!("expected MindToml, got {other:?}"),
}
}
#[test]
fn nested_profiles_edge_cases() {
let p = Path::new("mind.toml");
nested("[discover]\nsources = [{ source = \"o/r\", install = true, profiles = [] }]\n")
.validate(p)
.expect("empty profiles does not conflict with install");
let t = nested("[discover]\nsources = [{ source = \"o/r\", default-profile = true }]\n");
t.validate(p).unwrap();
assert!(crate::profile::Selection::from_nested(&t).include_default);
let f = nested(
"[discover]\nsources = [{ source = \"o/r\", install = true, default-profile = false }]\n",
);
f.validate(p).unwrap();
assert!(!crate::profile::Selection::from_nested(&f).include_default);
for bad in ["skill:x", ""] {
let n = nested(&format!(
"[discover]\nsources = [{{ source = \"o/r\", profiles = [\"{bad}\"] }}]\n"
));
assert!(
matches!(n.validate(p), Err(MindError::MindToml { .. })),
"{bad:?}"
);
}
let n = nested(
"[discover]\nsources = [{ source = \"o/\\u001b[31mr\", profiles = [\"ok\", \"\\u001b[31mBad\"] }]\n",
);
match n.validate(p) {
Err(MindError::MindToml { msg, .. }) => {
assert!(!msg.contains('\u{1b}'), "{msg:?}");
assert!(msg.contains("Bad"), "{msg}");
}
other => panic!("expected MindToml, got {other:?}"),
}
assert!(
toml::from_str::<MindToml>(
"[discover]\nsources = [{ source = \"o/r\", profiles = \"full\" }]\n"
)
.is_err()
);
}
#[test]
fn profiles_table_load_edge_cases() {
let dir = prof_dir("[profiles]\n");
let mt = MindToml::load(&dir).unwrap().unwrap();
assert_eq!(mt.profiles.as_ref().map(|p| p.len()), Some(0));
assert!(!mt.is_authoritative());
let _ = std::fs::remove_dir_all(&dir);
let dir = prof_dir("[profiles]\nnone = []\n");
assert!(MindToml::load(&dir).unwrap().is_some());
let _ = std::fs::remove_dir_all(&dir);
let dir = prof_dir("[profiles]\np = [1]\n");
assert!(MindToml::load(&dir).is_err());
let _ = std::fs::remove_dir_all(&dir);
let dir = prof_dir(&format!("[profiles]\n{} = []\n", "a".repeat(65)));
match MindToml::load(&dir) {
Err(MindError::MindToml { msg, .. }) => assert!(msg.contains("64 bytes"), "{msg}"),
other => panic!("expected MindToml, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
let dir = prof_dir("[profiles]\n\"skill:x\" = []\n");
match MindToml::load(&dir) {
Err(MindError::MindToml { msg, .. }) => assert!(msg.contains("contains ':'"), "{msg}"),
other => panic!("expected MindToml, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
let dir = prof_dir("[profiles]\na = [\"b\"]\nb = [\"a\"]\n");
match MindToml::load(&dir) {
Err(MindError::MindToml { msg, .. }) => {
assert!(msg.contains("cycle: a -> b -> a"), "{msg}")
}
other => panic!("expected MindToml, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn item_manifest_rejects_any_profiles_shape_even_with_hooks() {
for body in [
"[profiles]\n",
"profiles = []\n",
"profiles = \"x\"\n",
"[[hooks]]\nrun = \"true\"\n[profiles]\nb = []\n",
] {
let dir = prof_dir(body);
match ItemToml::load(&dir) {
Err(MindError::MindToml { msg, .. }) => {
assert!(msg.contains("root mind.toml only"), "{body:?}: {msg}")
}
other => panic!("{body:?}: expected MindToml, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
}
}