#[cfg(test)]
mod tests {
use super::super::*;
use proptest::prelude::*;
use std::collections::HashSet;
fn arb_module_id() -> impl Strategy<Value = String> {
"[a-z][a-z0-9-]{2,20}".prop_map(|s| s.to_string())
}
fn arb_version() -> impl Strategy<Value = String> {
(0u8..10, 0u8..20, 0u8..100)
.prop_map(|(major, minor, patch)| format!("{}.{}.{}", major, minor, patch))
}
fn arb_module() -> impl Strategy<Value = Module> {
(
arb_module_id(),
"[A-Z][a-zA-Z ]{2,30}",
arb_version(),
"[a-zA-Z0-9 .,!]{0,100}",
)
.prop_map(|(id, name, version, description)| {
Module::new(id, name, version).with_description(description)
})
}
fn arb_module_with_agents() -> impl Strategy<Value = Module> {
(arb_module(), prop::collection::vec("[a-z-]{3,15}", 0..5)).prop_map(
|(mut module, agents)| {
for agent in agents {
module = module.with_agent(agent);
}
module
},
)
}
fn arb_module_with_workflows() -> impl Strategy<Value = Module> {
(arb_module(), prop::collection::vec("[a-z-]{3,15}", 0..5)).prop_map(
|(mut module, workflows)| {
for workflow in workflows {
module = module.with_workflow(workflow);
}
module
},
)
}
fn arb_dependency() -> impl Strategy<Value = ModuleDependency> {
(arb_module_id(), prop::bool::ANY).prop_map(|(id, optional)| {
if optional {
ModuleDependency::optional(id, "*")
} else {
ModuleDependency::new(id, "*")
}
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_module_isolation_namespacing(
module1 in arb_module_with_agents(),
module2 in arb_module_with_agents(),
) {
prop_assume!(module1.id != module2.id);
let mut manager = ModuleManager::new("/tmp/test-isolation");
manager.enable_isolation();
let agents1: Vec<String> = module1.agents.iter()
.map(|a| module1.namespaced_agent(a))
.collect();
let agents2: Vec<String> = module2.agents.iter()
.map(|a| module2.namespaced_agent(a))
.collect();
let set1: HashSet<_> = agents1.iter().collect();
let set2: HashSet<_> = agents2.iter().collect();
let intersection: Vec<_> = set1.intersection(&set2).collect();
prop_assert!(
intersection.is_empty(),
"Namespaced agents should not conflict: {:?}",
intersection
);
}
#[test]
fn prop_module_isolation_workflows(
module1 in arb_module_with_workflows(),
module2 in arb_module_with_workflows(),
) {
prop_assume!(module1.id != module2.id);
let workflows1: Vec<String> = module1.workflows.iter()
.map(|w| module1.namespaced_workflow(w))
.collect();
let workflows2: Vec<String> = module2.workflows.iter()
.map(|w| module2.namespaced_workflow(w))
.collect();
let set1: HashSet<_> = workflows1.iter().collect();
let set2: HashSet<_> = workflows2.iter().collect();
let intersection: Vec<_> = set1.intersection(&set2).collect();
prop_assert!(
intersection.is_empty(),
"Namespaced workflows should not conflict: {:?}",
intersection
);
}
#[test]
fn prop_module_namespace_format(module in arb_module()) {
let agent_name = "test-agent";
let namespaced = module.namespaced_agent(agent_name);
prop_assert!(namespaced.contains(':'), "Namespaced ID should contain ':'");
prop_assert!(
namespaced.starts_with(&module.id),
"Namespaced ID should start with module ID"
);
prop_assert!(
namespaced.ends_with(agent_name),
"Namespaced ID should end with resource name"
);
let parts: Vec<&str> = namespaced.split(':').collect();
prop_assert_eq!(parts.len(), 2);
prop_assert_eq!(parts[0], module.id);
prop_assert_eq!(parts[1], agent_name);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_dependency_resolution_order(
base_module in arb_module(),
dep_module in arb_module(),
) {
prop_assume!(base_module.id != dep_module.id);
let mut manager = ModuleManager::new("/tmp/test-deps");
let dependent = Module::new(
format!("{}-dependent", base_module.id),
"Dependent Module",
"1.0.0",
)
.with_dependency(ModuleDependency::new(&dep_module.id, "*"));
let result = manager.resolve_dependencies(&dependent);
prop_assert!(
result.is_err(),
"Should fail when required dependency is not installed"
);
manager.installed.insert(dep_module.id.clone(), dep_module.clone());
manager.status.insert(dep_module.id.clone(), ModuleStatus::Installed);
let result = manager.resolve_dependencies(&dependent);
prop_assert!(result.is_ok(), "Should succeed when dependency is installed");
let resolved = result.unwrap();
prop_assert!(
resolved.iter().any(|m| m.id == dep_module.id),
"Resolved dependencies should include the required module"
);
}
#[test]
fn prop_optional_dependency_resolution(
base_module in arb_module(),
opt_dep_id in arb_module_id(),
) {
prop_assume!(base_module.id != opt_dep_id);
let manager = ModuleManager::new("/tmp/test-opt-deps");
let module_with_opt = Module::new(
format!("{}-with-opt", base_module.id),
"Module with Optional Dep",
"1.0.0",
)
.with_dependency(ModuleDependency::optional(&opt_dep_id, "*"));
let result = manager.resolve_dependencies(&module_with_opt);
prop_assert!(
result.is_ok(),
"Should succeed even when optional dependency is not installed"
);
}
#[test]
fn prop_version_satisfaction(
major in 0u8..10,
minor in 0u8..20,
patch in 0u8..100,
) {
let version = format!("{}.{}.{}", major, minor, patch);
let module = Module::new("test", "Test", &version);
prop_assert!(module.satisfies_version(&version));
prop_assert!(module.satisfies_version("*"));
let caret_req = format!("^{}", major);
prop_assert!(module.satisfies_version(&caret_req));
let tilde_req = format!("~{}.{}", major, minor);
prop_assert!(module.satisfies_version(&tilde_req));
if major < 9 {
let different_major = format!("^{}", major + 1);
prop_assert!(!module.satisfies_version(&different_major));
}
}
#[test]
fn prop_transitive_dependency_resolution(
module_a in arb_module(),
module_b in arb_module(),
module_c in arb_module(),
) {
prop_assume!(module_a.id != module_b.id);
prop_assume!(module_b.id != module_c.id);
prop_assume!(module_a.id != module_c.id);
let mut manager = ModuleManager::new("/tmp/test-transitive");
let c = module_c.clone();
let b = Module::new(&module_b.id, &module_b.name, &module_b.version)
.with_dependency(ModuleDependency::new(&c.id, "*"));
let a = Module::new(&module_a.id, &module_a.name, &module_a.version)
.with_dependency(ModuleDependency::new(&b.id, "*"));
manager.installed.insert(c.id.clone(), c.clone());
manager.status.insert(c.id.clone(), ModuleStatus::Installed);
manager.installed.insert(b.id.clone(), b.clone());
manager.status.insert(b.id.clone(), ModuleStatus::Installed);
let result = manager.resolve_dependencies(&a);
prop_assert!(result.is_ok(), "Transitive resolution should succeed");
let resolved = result.unwrap();
prop_assert!(
resolved.iter().any(|m| m.id == b.id),
"Should include direct dependency B"
);
prop_assert!(
resolved.iter().any(|m| m.id == c.id),
"Should include transitive dependency C"
);
}
}
#[test]
fn test_circular_dependency_detection() {
let mut manager = ModuleManager::new("/tmp/test-circular");
let module_a = Module::new("module-a", "Module A", "1.0.0")
.with_dependency(ModuleDependency::new("module-b", "*"));
let module_b = Module::new("module-b", "Module B", "1.0.0")
.with_dependency(ModuleDependency::new("module-a", "*"));
manager
.installed
.insert("module-a".to_string(), module_a.clone());
manager
.installed
.insert("module-b".to_string(), module_b.clone());
manager
.status
.insert("module-a".to_string(), ModuleStatus::Installed);
manager
.status
.insert("module-b".to_string(), ModuleStatus::Installed);
let result = manager.resolve_dependencies(&module_a);
assert!(result.is_ok());
}
#[test]
fn test_dx_manifest_parsing() {
let manager = ModuleManager::new("/tmp/test-parse");
let manifest = r#"
# Test Module Manifest
id|test-module
nm|Test Module
v|1.2.3
desc|A test module for testing
author|Test Author
license|MIT
dep.other-module|^1.0.0
agent.0|test-agent
workflow.0|test-workflow
template.0|test-template
resource.0|test-resource
"#;
let module = manager.parse_dx_manifest(manifest).unwrap();
assert_eq!(module.id, "test-module");
assert_eq!(module.name, "Test Module");
assert_eq!(module.version, "1.2.3");
assert_eq!(module.description, "A test module for testing");
assert_eq!(module.author, Some("Test Author".to_string()));
assert_eq!(module.license, Some("MIT".to_string()));
assert_eq!(module.dependencies.len(), 1);
assert_eq!(module.dependencies[0].module_id, "other-module");
assert_eq!(module.agents, vec!["test-agent"]);
assert_eq!(module.workflows, vec!["test-workflow"]);
assert_eq!(module.templates, vec!["test-template"]);
assert_eq!(module.resources, vec!["test-resource"]);
}
#[test]
fn test_manifest_missing_id_fails() {
let manager = ModuleManager::new("/tmp/test-missing-id");
let manifest = r#"
nm|Test Module
v|1.0.0
"#;
let result = manager.parse_dx_manifest(manifest);
assert!(result.is_err());
}
#[test]
fn test_manifest_defaults() {
let manager = ModuleManager::new("/tmp/test-defaults");
let manifest = "id|minimal-module";
let module = manager.parse_dx_manifest(manifest).unwrap();
assert_eq!(module.id, "minimal-module");
assert_eq!(module.name, "minimal-module"); assert_eq!(module.version, "0.0.0"); }
}