use std::collections::{HashMap, HashSet}; use std::fs; use std::process::Command; #[test] fn a1_all_members_build() { let output = Command::new("cargo") .args(&["build", "--workspace"]) .output() .expect("Failed to run cargo build"); assert!( output.status.success(), "cargo build --workspace failed:\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } #[test] fn a2_mem_core_has_no_sibling_deps() { let cargo_toml_path = "crates/mem-core/Cargo.toml"; let content = fs::read_to_string(cargo_toml_path) .expect("Failed to read mem-core Cargo.toml"); let table: toml::Table = toml::from_str(&content) .expect("Failed to parse Cargo.toml"); // Check dependencies section if let Some(deps) = table.get("dependencies") { if let Some(deps_table) = deps.as_table() { for key in deps_table.keys() { assert!( !key.starts_with("mem-"), "mem-core should not depend on {}, found dependency in Cargo.toml", key ); } } } // Check dev-dependencies section if let Some(dev_deps) = table.get("dev-dependencies") { if let Some(dev_deps_table) = dev_deps.as_table() { for key in dev_deps_table.keys() { assert!( !key.starts_with("mem-"), "mem-core should not have dev-dependency on {}", key ); } } } } #[test] fn a3_dependency_direction() { // Define the allowed edges (dependency direction) let allowed_edges: HashSet<(String, String)> = vec![ ("mem-cli".to_string(), "mem-ingest".to_string()), ("mem-cli".to_string(), "mem-store".to_string()), ("mem-cli".to_string(), "mem-llm".to_string()), ("mem-cli".to_string(), "mem-chunk".to_string()), ("mem-cli".to_string(), "mem-core".to_string()), ("mem-store".to_string(), "mem-core".to_string()), ("mem-ingest".to_string(), "mem-chunk".to_string()), ("mem-ingest".to_string(), "mem-core".to_string()), ("mem-chunk".to_string(), "mem-core".to_string()), ("mem-llm".to_string(), "mem-core".to_string()), ] .into_iter() .collect(); let crates = vec!["mem-core", "mem-chunk", "mem-llm", "mem-ingest", "mem-store", "mem-cli"]; let mut edges: HashSet<(String, String)> = HashSet::new(); // Parse each crate's Cargo.toml for crate_name in &crates { let cargo_toml_path = format!("crates/{}/Cargo.toml", crate_name); let content = fs::read_to_string(&cargo_toml_path) .unwrap_or_else(|_| panic!("Failed to read {}", cargo_toml_path)); let table: toml::Table = toml::from_str(&content) .unwrap_or_else(|_| panic!("Failed to parse {}", cargo_toml_path)); // Check dependencies if let Some(deps) = table.get("dependencies") { if let Some(deps_table) = deps.as_table() { for key in deps_table.keys() { if key.starts_with("mem-") { edges.insert((crate_name.to_string(), key.clone())); } } } } } // Check that all edges are allowed for (from, to) in &edges { assert!( allowed_edges.contains(&(from.clone(), to.clone())), "Invalid edge: {} -> {} not in allowed dependency graph", from, to ); } // Check for cycles using DFS let mut graph: HashMap> = HashMap::new(); for crate_name in &crates { graph.insert(crate_name.to_string(), Vec::new()); } for (from, to) in &edges { graph.entry(from.clone()).or_insert_with(Vec::new).push(to.clone()); } // DFS to detect cycles fn has_cycle( node: &str, graph: &HashMap>, visited: &mut HashSet, rec_stack: &mut HashSet, ) -> bool { visited.insert(node.to_string()); rec_stack.insert(node.to_string()); if let Some(neighbors) = graph.get(node) { for neighbor in neighbors { if !visited.contains(neighbor) { if has_cycle(neighbor, graph, visited, rec_stack) { return true; } } else if rec_stack.contains(neighbor) { return true; } } } rec_stack.remove(node); false } let mut visited = HashSet::new(); let mut rec_stack = HashSet::new(); for crate_name in &crates { if !visited.contains(*crate_name) { assert!( !has_cycle(crate_name, &graph, &mut visited, &mut rec_stack), "Cycle detected in dependency graph" ); } } } #[test] fn a4_log_and_tasks_are_tracked() { let gitignore_path = ".gitignore"; let content = fs::read_to_string(gitignore_path) .expect("Failed to read .gitignore"); // Check that log/ is NOT ignored let log_lines: Vec<&str> = content.lines() .filter(|line| line.trim() == "log/") .collect(); for line in log_lines { assert!( line.starts_with("#"), ".gitignore should not ignore log/ (the authoritative JSONL event log)" ); } // Check that tasks/ is NOT ignored let has_tasks_ignored = content.lines() .filter(|line| { let trimmed = line.trim(); (trimmed == "tasks/" || trimmed == "memory-tasks/") && !line.starts_with("#") }) .count() > 0; assert!( !has_tasks_ignored, ".gitignore should not ignore tasks/ (the task board)" ); }