186 lines
5.8 KiB
Rust
186 lines
5.8 KiB
Rust
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<String, Vec<String>> = 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<String, Vec<String>>,
|
||
|
|
visited: &mut HashSet<String>,
|
||
|
|
rec_stack: &mut HashSet<String>,
|
||
|
|
) -> 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)"
|
||
|
|
);
|
||
|
|
}
|