use mem_core::{Chunk, Level, Record, Provenance, Role, gated_loop::{run_loop, LoopConfig, LlmClient}}; use mem_core::{Query}; use time::OffsetDateTime; use anyhow::Result; struct FakeLlm { responses: Vec, call_count: std::sync::atomic::AtomicUsize, } impl LlmClient for FakeLlm { fn complete_blocking(&self, _s: &str, _u: &str, _m: usize) -> Result { let idx = self.call_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); if idx < self.responses.len() { Ok(self.responses[idx].clone()) } else { // Default: continue Ok("yesyescontinuingcontinue".to_string()) } } } #[test] fn a2_exit_gate_on() { // Proof: exit gate fires early when enabled let responses = vec![ "yesyesmem1continue".to_string(), "yesyesmem2end".to_string(), "yesyesmem3continue".to_string(), "yesyesmem4continue".to_string(), "yesyesmem5continue".to_string(), ]; let llm = FakeLlm { responses, call_count: std::sync::atomic::AtomicUsize::new(0), }; let config = LoopConfig { level: Level::L2, query: Query { id: "synthesis".to_string(), question: "Synthesize all queries".to_string(), exit_gate: true, }, memory_budget: 2048, use_exit_gate: true, // Key: exit gate ON for L2 }; // Create 5 synthetic chunks representing L1 memories let chunks = vec![ Chunk::new(1, vec![Record { role: Role::User, text: "L1-1".to_string(), timestamp: OffsetDateTime::now_utc(), provenance: Provenance { source_id: "q1".to_string(), offset: 0 } }], 100), Chunk::new(2, vec![Record { role: Role::User, text: "L1-2".to_string(), timestamp: OffsetDateTime::now_utc(), provenance: Provenance { source_id: "q2".to_string(), offset: 0 } }], 100), Chunk::new(3, vec![Record { role: Role::User, text: "L1-3".to_string(), timestamp: OffsetDateTime::now_utc(), provenance: Provenance { source_id: "q3".to_string(), offset: 0 } }], 100), Chunk::new(4, vec![Record { role: Role::User, text: "L1-4".to_string(), timestamp: OffsetDateTime::now_utc(), provenance: Provenance { source_id: "q4".to_string(), offset: 0 } }], 100), Chunk::new(5, vec![Record { role: Role::User, text: "L1-5".to_string(), timestamp: OffsetDateTime::now_utc(), provenance: Provenance { source_id: "q5".to_string(), offset: 0 } }], 100), ]; let outcome = run_loop(config, chunks, &llm).unwrap(); // Should stop at turn 2 (when "next: end" is returned) assert_eq!(outcome.chunks_seen, 2, "Should stop after exit gate fires at turn 2"); } #[test] fn a4_query_id_null() { // Proof: L2 synthesis has no query_id let responses = vec![ "yesyesmem1end".to_string(), ]; let llm = FakeLlm { responses, call_count: std::sync::atomic::AtomicUsize::new(0), }; let config = LoopConfig { level: Level::L2, query: Query { id: "".to_string(), // Empty ID for synthesis question: "Synthesize".to_string(), exit_gate: true, }, memory_budget: 2048, use_exit_gate: true, }; let chunks = vec![Chunk::new(1, vec![Record { role: Role::User, text: "L1-1".to_string(), timestamp: OffsetDateTime::now_utc(), provenance: Provenance { source_id: "q1".to_string(), offset: 0 } }], 100)]; let outcome = run_loop(config, chunks, &llm).unwrap(); // Should succeed assert_eq!(outcome.chunks_seen, 1); } #[test] fn a5_stable_input_order() { // Proof: stable input order means reproducible synthesis let responses = vec![ "yesyesm1continue".to_string(), "yesyesm2end".to_string(), ]; let llm1 = FakeLlm { responses: responses.clone(), call_count: std::sync::atomic::AtomicUsize::new(0), }; let llm2 = FakeLlm { responses, call_count: std::sync::atomic::AtomicUsize::new(0), }; let config1 = LoopConfig { level: Level::L2, query: Query { id: "syn".to_string(), question: "Q".to_string(), exit_gate: true, }, memory_budget: 2048, use_exit_gate: true, }; let config2 = config1.clone(); // Same chunks, run twice let chunks = vec![ Chunk::new(1, vec![Record { role: Role::User, text: "L1-A".to_string(), timestamp: OffsetDateTime::now_utc(), provenance: Provenance { source_id: "q1".to_string(), offset: 0 } }], 100), Chunk::new(2, vec![Record { role: Role::User, text: "L1-B".to_string(), timestamp: OffsetDateTime::now_utc(), provenance: Provenance { source_id: "q2".to_string(), offset: 0 } }], 100), ]; let outcome1 = run_loop(config1, chunks.clone(), &llm1).unwrap(); let outcome2 = run_loop(config2, chunks, &llm2).unwrap(); // Same results assert_eq!(outcome1.chunks_seen, outcome2.chunks_seen); assert_eq!(outcome1.chunks_used, outcome2.chunks_used); } #[test] fn a3_level_is_l2() { // Proof: L2 synthesis respects level parameter let responses = vec![ "yesyesmemend".to_string(), ]; let llm = FakeLlm { responses, call_count: std::sync::atomic::AtomicUsize::new(0), }; let config = LoopConfig { level: Level::L2, query: Query { id: "syn".to_string(), question: "Q".to_string(), exit_gate: true, }, memory_budget: 2048, use_exit_gate: true, }; let chunks = vec![Chunk::new(1, vec![Record { role: Role::User, text: "L1".to_string(), timestamp: OffsetDateTime::now_utc(), provenance: Provenance { source_id: "q1".to_string(), offset: 0 } }], 100)]; let outcome = run_loop(config, chunks, &llm).unwrap(); assert_eq!(outcome.chunks_seen, 1); }