diff --git a/codex-rs/memory/Cargo.toml b/codex-rs/memory/Cargo.toml index be0026239b4..196f888628c 100644 --- a/codex-rs/memory/Cargo.toml +++ b/codex-rs/memory/Cargo.toml @@ -18,3 +18,6 @@ sqlite = ["dep:rusqlite"] version = "0.31" optional = true features = ["bundled"] + +[dev-dependencies] +tempfile = "3" diff --git a/codex-rs/memory/tests/recall.rs b/codex-rs/memory/tests/recall.rs index 27a5b49e5e8..08e851f5f78 100644 --- a/codex-rs/memory/tests/recall.rs +++ b/codex-rs/memory/tests/recall.rs @@ -1,4 +1,42 @@ +use codex_memory::factory::Backend; +use codex_memory::factory::open_repo_store; +use codex_memory::recall::RecallContext; +use codex_memory::recall::recall; + +fn backends() -> Vec { + #[cfg(feature = "sqlite")] + { + vec![Backend::Jsonl, Backend::Sqlite] + } + #[cfg(not(feature = "sqlite"))] + { + vec![Backend::Jsonl] + } +} + +fn sample_ctx() -> RecallContext { + RecallContext { + repo_root: None, + dir: None, + current_file: None, + crate_name: None, + language: None, + command: None, + now_rfc3339: "2024-01-01T00:00:00Z".to_string(), + item_cap: 10, + token_cap: 1000, + } +} + #[test] -fn placeholder() { - // placeholder test +fn recall_unimplemented_panics() { + for be in backends() { + let repo = tempfile::tempdir().unwrap(); + let store = open_repo_store(repo.path(), Some(be)).unwrap(); + let ctx = sample_ctx(); + let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + recall(store.as_ref(), "", &ctx) + })); + assert!(res.is_err()); + } } diff --git a/codex-rs/memory/tests/redact.rs b/codex-rs/memory/tests/redact.rs index fe9b036debf..bbb3776756f 100644 --- a/codex-rs/memory/tests/redact.rs +++ b/codex-rs/memory/tests/redact.rs @@ -1,3 +1,25 @@ + +use codex_memory::factory::Backend; +use codex_memory::redact::redact_candidate; + +fn backends() -> Vec { + #[cfg(feature = "sqlite")] + { + vec![Backend::Jsonl, Backend::Sqlite] + } + #[cfg(not(feature = "sqlite"))] + { + vec![Backend::Jsonl] + } +} + +#[test] +fn redact_unimplemented_panics() { + for _be in backends() { + let res = std::panic::catch_unwind(|| redact_candidate("secret")); + assert!(res.is_err()); + } + use codex_memory::redact::redact_candidate; #[test] @@ -34,4 +56,5 @@ fn no_detection() { assert!(!result.blocked); assert!(result.issues.is_empty()); assert_eq!(result.masked, input); + } diff --git a/codex-rs/memory/tests/store.rs b/codex-rs/memory/tests/store.rs new file mode 100644 index 00000000000..334268868b4 --- /dev/null +++ b/codex-rs/memory/tests/store.rs @@ -0,0 +1,94 @@ +use codex_memory::factory::Backend; +use codex_memory::factory::open_repo_store; +use codex_memory::types::*; + +fn backends() -> Vec { + #[cfg(feature = "sqlite")] + { + vec![Backend::Jsonl, Backend::Sqlite] + } + #[cfg(not(feature = "sqlite"))] + { + vec![Backend::Jsonl] + } +} + +fn sample_item(id: &str, scope: Scope, status: Status) -> MemoryItem { + MemoryItem { + id: id.to_string(), + created_at: "2024-01-01T00:00:00Z".to_string(), + updated_at: "2024-01-01T00:00:00Z".to_string(), + schema_version: 1, + source: "test".to_string(), + scope, + status, + kind: Kind::Note, + content: format!("content-{id}"), + tags: vec!["tag".to_string()], + relevance_hints: RelevanceHints { + files: vec![], + crates: vec![], + languages: vec![], + commands: vec![], + }, + counters: Counters { + seen_count: 0, + used_count: 0, + last_used_at: None, + }, + expiry: None, + } +} + +#[test] +fn store_crud_import_export_stats() { + for be in backends() { + let repo = tempfile::tempdir().unwrap(); + let store = open_repo_store(repo.path(), Some(be)).unwrap(); + + // create + let item_a = sample_item("a", Scope::Global, Status::Active); + store.add(item_a.clone()).unwrap(); + assert_eq!(store.get("a").unwrap().unwrap().content, item_a.content); + + // update + let mut updated = item_a.clone(); + updated.content = "updated".to_string(); + store.update(&updated).unwrap(); + assert_eq!(store.get("a").unwrap().unwrap().content, "updated"); + + // list + assert_eq!(store.list(None, None).unwrap().len(), 1); + + // archive + store.archive("a", true).unwrap(); + assert_eq!(store.get("a").unwrap().unwrap().status, Status::Archived); + + // add second item for stats + let item_b = sample_item("b", Scope::Repo, Status::Active); + store.add(item_b.clone()).unwrap(); + + // stats + let stats = store.stats().unwrap(); + assert_eq!(stats["total"], 2); + assert_eq!(stats["active"], 1); + assert_eq!(stats["archived"], 1); + assert_eq!(stats["by_scope"]["global"], 1); + assert_eq!(stats["by_scope"]["repo"], 1); + + // export + let mut buf = Vec::new(); + store.export(&mut buf).unwrap(); + + // import into fresh store + let repo2 = tempfile::tempdir().unwrap(); + let store2 = open_repo_store(repo2.path(), Some(be)).unwrap(); + store2.import(&mut buf.as_slice()).unwrap(); + assert_eq!(store2.list(None, None).unwrap().len(), 2); + assert_eq!(store2.get("a").unwrap().unwrap().content, "updated"); + + // delete + store2.delete("a").unwrap(); + assert!(store2.get("a").unwrap().is_none()); + } +}