use mem_store::{EventRecord, ObsidianProjector, PgRepo, MemoryNode, VectorKind, Level, RebuildState}; use serde_json::json; use std::fs; #[test] fn m2_gate_vault_byte_identical_rebuild() { let _ = fs::remove_dir_all("test_m2_gate_vault1"); let _ = fs::remove_dir_all("test_m2_gate_vault2"); // Create sample events let events = vec![ EventRecord { project: "test".to_string(), query: "q1".to_string(), run: "run1".to_string(), turn: 1, event_type: "Gate".to_string(), data: json!({}), }, EventRecord { project: "test".to_string(), query: "q1".to_string(), run: "run1".to_string(), turn: 2, event_type: "Evidence".to_string(), data: json!({"parent": "source-001"}), }, EventRecord { project: "test".to_string(), query: "q2".to_string(), run: "run1".to_string(), turn: 1, event_type: "Gate".to_string(), data: json!({}), }, ]; // First rebuild let proj1 = ObsidianProjector::new("log", "test_m2_gate_vault1", false); proj1.project(&events).unwrap(); // Second rebuild (should be identical) let proj2 = ObsidianProjector::new("log", "test_m2_gate_vault2", false); proj2.project(&events).unwrap(); // Compare all files byte-by-byte let files1 = collect_md_files("test_m2_gate_vault1"); let files2 = collect_md_files("test_m2_gate_vault2"); assert_eq!( files1.len(), files2.len(), "Rebuild produced different number of files" ); for file in files1.iter() { let c1 = fs::read_to_string(file).unwrap(); let c2 = fs::read_to_string(file.replace("test_m2_gate_vault1", "test_m2_gate_vault2")).unwrap(); assert_eq!( c1, c2, "File {} is not byte-identical after rebuild", file ); } let _ = fs::remove_dir_all("test_m2_gate_vault1"); let _ = fs::remove_dir_all("test_m2_gate_vault2"); } #[test] fn m2_gate_pg_repo_idempotent_upsert() { // Proof: rebuilding repository produces identical state let mut repo1 = PgRepo::new(); let mut repo2 = PgRepo::new(); let nodes = vec![ MemoryNode { sha256: "abc1".to_string(), level: Level::L0, project: "p1".to_string(), text: "text1".to_string(), tokens: 100, }, MemoryNode { sha256: "abc2".to_string(), level: Level::L1, project: "p1".to_string(), text: "text2".to_string(), tokens: 200, }, ]; // Upsert into repo1 repo1.upsert_many(&nodes).unwrap(); repo1.insert_edges("abc2", &["abc1".to_string()]).unwrap(); // Add vector repo1.upsert_vector("abc1", VectorKind::Text, &[1.0, 0.0, 0.0]).unwrap(); repo1.upsert_vector("abc2", VectorKind::Text, &[1.0, 0.0, 0.0]).unwrap(); // Rebuild: upsert same nodes into repo2 repo2.upsert_many(&nodes).unwrap(); repo2.insert_edges("abc2", &["abc1".to_string()]).unwrap(); repo2.upsert_vector("abc1", VectorKind::Text, &[1.0, 0.0, 0.0]).unwrap(); repo2.upsert_vector("abc2", VectorKind::Text, &[1.0, 0.0, 0.0]).unwrap(); // Verify identical state assert_eq!(repo1.node_count(), repo2.node_count()); assert_eq!(repo1.edge_count(), repo2.edge_count()); let p1_nodes = repo1.all_nodes(); let p2_nodes = repo2.all_nodes(); assert_eq!(p1_nodes.len(), p2_nodes.len()); for (n1, n2) in p1_nodes.iter().zip(p2_nodes.iter()) { assert_eq!(n1.sha256, n2.sha256); assert_eq!(n1.level, n2.level); assert_eq!(n1.project, n2.project); assert_eq!(n1.text, n2.text); } } #[test] fn m2_gate_rebuild_state_consistency() { // Proof: rebuild from events produces consistent state let events = vec![ EventRecord { project: "p".to_string(), query: "q".to_string(), run: "r1".to_string(), turn: 1, event_type: "Gate".to_string(), data: json!({}), }, EventRecord { project: "p".to_string(), query: "q".to_string(), run: "r1".to_string(), turn: 2, event_type: "Evidence".to_string(), data: json!({}), }, ]; // Rebuild state twice let state1 = RebuildState::from_events(&events).unwrap(); let state2 = RebuildState::from_events(&events).unwrap(); // Verify identical assert_eq!(state1.event_count, state2.event_count); assert_eq!(state1.chunks_seen, state2.chunks_seen); assert_eq!(state1.chunks_used, state2.chunks_used); } #[test] fn m2_gate_no_hidden_state() { // Proof: rebuild with no prior state produces same result let events = vec![ EventRecord { project: "fresh".to_string(), query: "newq".to_string(), run: "run1".to_string(), turn: 1, event_type: "Gate".to_string(), data: json!({}), }, ]; // Rebuild 1: fresh repo let mut repo1 = PgRepo::new(); let node1 = MemoryNode { sha256: "new1".to_string(), level: Level::L0, project: "fresh".to_string(), text: "new memory".to_string(), tokens: 50, }; repo1.upsert_node(&node1).unwrap(); // Rebuild 2: same let mut repo2 = PgRepo::new(); let node2 = MemoryNode { sha256: "new1".to_string(), level: Level::L0, project: "fresh".to_string(), text: "new memory".to_string(), tokens: 50, }; repo2.upsert_node(&node2).unwrap(); assert_eq!(repo1.node_count(), repo2.node_count()); } #[test] fn m2_gate_clear_project_is_safe() { // Proof: clearing one project doesn't affect others let mut repo = PgRepo::new(); let n1 = MemoryNode { sha256: "n1".to_string(), level: Level::L1, project: "keep".to_string(), text: "keep".to_string(), tokens: 100, }; let n2 = MemoryNode { sha256: "n2".to_string(), level: Level::L1, project: "delete".to_string(), text: "delete".to_string(), tokens: 100, }; repo.upsert_node(&n1).unwrap(); repo.upsert_node(&n2).unwrap(); assert_eq!(repo.node_count(), 2); // Clear one project repo.clear_project("delete").unwrap(); assert_eq!(repo.node_count(), 1); assert_eq!(repo.all_nodes()[0].project, "keep"); } /// Collect all .md files in directory recursively. fn collect_md_files(dir: &str) -> Vec { let mut files = Vec::new(); if let Ok(entries) = fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_file() && path.extension().map_or(false, |e| e == "md") { files.push(path.to_string_lossy().to_string()); } else if path.is_dir() { let subfiles = collect_md_files(&path.to_string_lossy()); files.extend(subfiles); } } } files.sort(); files }