use std::path::{Path, PathBuf};
use super::*;
use crate::workspace::ProjectTrust;
#[test]
fn skips_invalid_user_files_and_blocks_their_ids() {
struct Case {
name: &'static str,
files: &'static [(&'static str, &'static str)],
skipped: &'static str,
field: &'static str,
message: &'static str,
blocked_id: Option<&'static str>,
}
let cases = [
Case {
name: "unknown tool shadows a built-in",
files: &[(
"reviewer.md",
"---\ndescription: bad\ntools: [teleport]\n---\n",
)],
skipped: "reviewer.md",
field: "tools",
message: "unknown tool 'teleport'",
blocked_id: Some("reviewer"),
},
Case {
name: "same-tier duplicates",
files: &[
("one.md", "---\nid: duplicate\ndescription: one\n---\n"),
("two.md", "---\nid: duplicate\ndescription: two\n---\n"),
],
skipped: "two.md",
field: "id",
message: "duplicate agent ID",
blocked_id: Some("duplicate"),
},
Case {
name: "reserved internal id",
files: &[("session-title.md", "---\ndescription: shadow\n---\n")],
skipped: "session-title.md",
field: "id",
message: "reserved for an internal agent",
blocked_id: None,
},
];
for case in cases {
let root = tempfile::tempdir().unwrap();
let agents = root.path().join(".rho/agents");
std::fs::create_dir_all(&agents).unwrap();
for (file, contents) in case.files {
std::fs::write(agents.join(file), contents).unwrap();
}
std::fs::write(agents.join("ok.md"), "---\ndescription: ok\n---\n").unwrap();
let catalog = AgentCatalog::discover_with_home(root.path(), Some(root.path()))
.unwrap_or_else(|error| panic!("{}: discovery failed: {error}", case.name));
let [skipped] = catalog.skipped() else {
panic!(
"{}: expected one skipped file, got {:?}",
case.name,
catalog.skipped()
);
};
assert_eq!(skipped.path, agents.join(case.skipped), "{}", case.name);
assert_eq!(skipped.field.as_deref(), Some(case.field), "{}", case.name);
assert!(
skipped.message.contains(case.message),
"{}: {}",
case.name,
skipped.message
);
assert!(
catalog.find("ok").is_ok(),
"{}: valid file still loads",
case.name
);
if let Some(id) = case.blocked_id {
assert_eq!(
catalog.find(id).unwrap_err().path,
skipped.path,
"{}",
case.name
);
}
}
}
#[test]
fn semantic_fingerprint_ignores_formatting_and_source() {
let a = parse_definition(
Path::new("a.md"),
"worker",
"---\ndescription: work\ntools: [read_file, write]\n---\nship it\n",
)
.unwrap();
let b = parse_definition(
Path::new("elsewhere.md"),
"worker",
"---\nid: worker\ndescription: work\ntools:\n - write\n - read_file\n---\n\nship it\n",
)
.unwrap();
assert_eq!(a.fingerprint(), b.fingerprint());
}
#[test]
fn edit_capability_format_names_match_edit() {
let canonical = parse_definition(
Path::new("a.md"),
"worker",
"---\ndescription: work\ntools: [edit]\n---\n",
)
.unwrap();
for name in ["apply_patch", "hashline", "str_replace"] {
let format_named = parse_definition(
Path::new("b.md"),
"worker",
&format!("---\ndescription: work\ntools: [{name}]\n---\n"),
)
.unwrap();
assert_eq!(
canonical.fingerprint(),
format_named.fingerprint(),
"format name {name}"
);
assert_eq!(
ToolCapability::parse(name.into()).as_str(),
"edit",
"format name {name}"
);
}
}
#[test]
fn current_fingerprint_uses_v2_marker_and_differs_from_legacy_v1() {
let definition = parse_definition(
Path::new("default.md"),
"default",
"---\ndescription: demo\ntools: all\n---\n",
)
.unwrap();
let current = definition.fingerprint().to_string();
let legacy = definition
.legacy_v1_fingerprint()
.expect("default rho definition encodes legacy v1")
.to_string();
assert_ne!(current, legacy);
assert!(definition.accepts_stored_fingerprint(¤t));
assert!(definition.accepts_stored_fingerprint(&legacy));
assert!(!definition.accepts_stored_fingerprint("deadbeef"));
}
#[test]
fn golden_legacy_v1_fingerprints_for_builtin_rho_agents() {
let root = tempfile::tempdir().unwrap();
let catalog = AgentCatalog::discover_with_home(root.path(), None).unwrap();
let expected = [
(
"default",
"ffc3f694800c9e3d284e457e63b2a61ad97f361f84ce3493314cc9c69892826d",
),
(
"explorer",
"b393602c7cb63eca98273b1b43640767f31c88a42abfbfe04dc387acfd68f3fc",
),
(
"reviewer",
"b6dbdf4028def08031a039f757116526e833c45b3c73318246b519d50246c469",
),
(
"worker",
"3974c0730d03a37c063e0a9cb43496f17aab9bb152c97397b52d9425c1329079",
),
];
for (id, expected_legacy) in expected {
let definition = &catalog.find(id).unwrap().definition;
assert!(
matches!(definition.runtime, AgentRuntimeSpec::Rho { .. }),
"{id} must remain default Rho for legacy resume"
);
let legacy = definition
.legacy_v1_fingerprint()
.unwrap_or_else(|| panic!("{id} should expose legacy v1"))
.to_string();
assert_eq!(legacy, expected_legacy, "legacy v1 drift for {id}");
assert_ne!(
definition.fingerprint().to_string(),
legacy,
"{id} current fingerprint must be v2"
);
assert!(definition.accepts_stored_fingerprint(&legacy));
}
}
#[test]
fn real_definition_change_still_rejects_resume() {
let original = parse_definition(
Path::new("worker.md"),
"worker",
"---\ndescription: work\ntools: [read_file]\n---\nship it\n",
)
.unwrap();
let changed = parse_definition(
Path::new("worker.md"),
"worker",
"---\ndescription: work\ntools: [read_file, write]\n---\nship it\n",
)
.unwrap();
let stored_v2 = original.fingerprint().to_string();
let stored_v1 = original.legacy_v1_fingerprint().unwrap().to_string();
assert!(!changed.accepts_stored_fingerprint(&stored_v2));
assert!(!changed.accepts_stored_fingerprint(&stored_v1));
}
#[test]
fn claude_definitions_have_no_legacy_v1_fingerprint() {
let definition = parse_definition(
Path::new("claude.md"),
"claude",
"---\ndescription: demo\nruntime: claude-cli\ntools: [Read]\n---\n",
)
.unwrap();
assert!(definition.legacy_v1_fingerprint().is_none());
assert!(!definition.accepts_stored_fingerprint("anything"));
assert!(definition.accepts_stored_fingerprint(&definition.fingerprint().to_string()));
}
#[test]
fn internal_agents_are_visible_but_not_selectable() {
let root = tempfile::tempdir().unwrap();
let catalog = AgentCatalog::discover_with_home(root.path(), None).unwrap();
for id in [
SESSION_TITLE_AGENT_ID,
GOAL_JUDGE_AGENT_ID,
ADVISOR_AGENT_ID,
] {
assert!(catalog.find(id).is_err());
}
assert!(catalog
.iter()
.all(|entry| entry.metadata.origin != AgentOrigin::Internal));
let internal_count = internal_definitions().len();
let origins = catalog
.iter_with_internal()
.map(|entry| entry.metadata.origin)
.collect::<Vec<_>>();
assert!(origins[..internal_count]
.iter()
.all(|origin| *origin == AgentOrigin::Internal));
assert!(origins[internal_count..]
.iter()
.all(|origin| *origin != AgentOrigin::Internal));
}
#[test]
fn model_required_internal_agents_require_their_own_model() {
let requires = internal_definitions()
.iter()
.map(|definition| {
(
definition.id.as_str(),
internal_agent_requires_model(definition.id.as_str()),
)
})
.collect::<Vec<_>>();
assert_eq!(
requires,
vec![
(SESSION_TITLE_AGENT_ID, false),
(GOAL_JUDGE_AGENT_ID, false),
(ADVISOR_AGENT_ID, true),
(PERMISSION_CLASSIFIER_AGENT_ID, true),
(COMPACTION_AGENT_ID, false),
]
);
}
#[test]
fn project_definitions_require_explicit_trust() {
let project = tempfile::tempdir().unwrap();
let agents = project.path().join(".agents/agents");
std::fs::create_dir_all(&agents).unwrap();
std::fs::write(
agents.join("project.md"),
"---\ndescription: project agent\n---\n",
)
.unwrap();
let untrusted =
AgentCatalog::discover_with_home_and_trust(project.path(), None, ProjectTrust::Untrusted)
.unwrap();
assert!(untrusted.find("project").is_err());
let trusted =
AgentCatalog::discover_with_home_and_trust(project.path(), None, ProjectTrust::Trusted)
.unwrap();
assert_eq!(
trusted.find("project").unwrap().metadata.origin,
AgentOrigin::Project
);
}
#[test]
fn workflow_entry_loads_local_agents_directory() {
let root = tempfile::tempdir().unwrap();
let workflow_dir = root.path().join("review");
let agents = workflow_dir.join("agents");
std::fs::create_dir_all(&agents).unwrap();
let entry = workflow_dir.join("workflow.star");
std::fs::write(&entry, "def build(inputs):\n pass\n").unwrap();
std::fs::write(
agents.join("boundary-reviewer.md"),
"---\ndescription: workflow-local specialist\ntools: [read_file, grep, glob, list_dir]\n---\nReview boundaries only.\n",
)
.unwrap();
let without =
AgentCatalog::discover_with_home_and_trust(root.path(), None, ProjectTrust::Untrusted)
.unwrap();
assert!(without.find("boundary-reviewer").is_err());
let with = AgentCatalog::discover_for_workflow_entry(
root.path(),
&entry,
None,
ProjectTrust::Untrusted,
)
.unwrap();
let entry = with.find("boundary-reviewer").unwrap();
assert_eq!(entry.metadata.origin, AgentOrigin::Workflow);
assert_eq!(entry.definition.description, "workflow-local specialist");
}
#[test]
fn workflow_local_agents_root_is_sibling_agents_dir() {
assert_eq!(
workflow_local_agents_root(Path::new(".rho/workflows/review/workflow.star")),
PathBuf::from(".rho/workflows/review/agents")
);
assert_eq!(
workflow_local_agents_root(Path::new("workflow.star")),
PathBuf::from("agents")
);
}