From a10b23dbc1c4bd737591e4032c70738e6f270ad8 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 10 Jun 2026 18:44:32 -0600 Subject: [PATCH] test: added more unit tests for the memory system --- src/config/memory.rs | 77 +++++++++++++++++++++++++++++++ src/config/request_context.rs | 15 ++++++ src/function/memory.rs | 87 +++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) diff --git a/src/config/memory.rs b/src/config/memory.rs index d12e064..abed514 100644 --- a/src/config/memory.rs +++ b/src/config/memory.rs @@ -427,4 +427,81 @@ mod tests { let _ = fs::remove_dir_all(&root); } + + #[test] + fn parse_frontmatter_extracts_yaml() { + let raw = "---\nname: foo\ndescription: a thing\ntype: user\n---\nBody text\n"; + + let (fm, body) = parse_frontmatter(raw).unwrap(); + + assert_eq!(fm.name, "foo"); + assert_eq!(fm.description.as_deref(), Some("a thing")); + assert_eq!(fm.kind.as_deref(), Some("user")); + assert_eq!(body, "Body text\n"); + } + + #[test] + fn parse_frontmatter_handles_missing_block() { + let raw = "# Just markdown, no frontmatter\nbody"; + + let (fm, body) = parse_frontmatter(raw).unwrap(); + + assert_eq!(fm.name, ""); + assert!(fm.kind.is_none()); + assert_eq!(body, raw); + } + + #[test] + fn parse_frontmatter_handles_unterminated_block() { + let raw = "---\nname: oops\nno closing delimiter\n# rest of doc"; + + let (fm, body) = parse_frontmatter(raw).unwrap(); + + assert_eq!(fm.name, ""); + assert_eq!(body, raw); + } + + #[test] + fn memory_file_save_and_load_roundtrip() { + let root = temp_root("roundtrip"); + let path = root.join("test.md"); + let file = MemoryFile { + path: path.clone(), + frontmatter: MemoryFrontmatter { + name: "test".into(), + description: Some("a test".into()), + kind: Some("user".into()), + }, + body: "Hello world\nmore text".into(), + }; + file.save().unwrap(); + let loaded = MemoryFile::load(&path).unwrap(); + assert_eq!(loaded.frontmatter.name, "test"); + assert_eq!(loaded.frontmatter.description.as_deref(), Some("a test")); + assert_eq!(loaded.frontmatter.kind.as_deref(), Some("user")); + assert_eq!(loaded.body, "Hello world\nmore text"); + + let raw = fs::read_to_string(&path).unwrap(); + assert!(raw.contains("type: user"), "kind must serialize as 'type:'"); + + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn discover_walks_up_from_nested_dir() { + let root = temp_root("walk_up"); + let workspace = root.join("ws"); + let mem_dir = workspace + .join(WORKSPACE_MEMORY_DIR_NAME) + .join(MEMORY_DIR_NAME); + fs::create_dir_all(&mem_dir).unwrap(); + fs::write(mem_dir.join(MEMORY_INDEX_FILE_NAME), "idx").unwrap(); + let nested = workspace.join("src").join("deep").join("path"); + fs::create_dir_all(&nested).unwrap(); + + let found = discover_workspace_memory(&nested); + assert!(matches!(found, Some(WorkspaceMemory::Structured { .. }))); + + let _ = fs::remove_dir_all(&root); + } } diff --git a/src/config/request_context.rs b/src/config/request_context.rs index 3f8c7f0..5b79103 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -3277,6 +3277,21 @@ mod tests { ); } + #[test] + fn should_register_memory_tools_false_when_function_calling_off() { + let mut ctx = create_test_ctx(); + + ctx.update_app_config(|app| { + app.memory = Some(true); + app.function_calling_support = false; + }); + + assert!( + !ctx.should_register_memory_tools(), + "memory tools must require function_calling_support even when memory itself would otherwise be enabled" + ); + } + #[test] fn use_role_obj_sets_role() { let mut ctx = create_test_ctx(); diff --git a/src/function/memory.rs b/src/function/memory.rs index 211eabd..b3e12ea 100644 --- a/src/function/memory.rs +++ b/src/function/memory.rs @@ -339,6 +339,93 @@ mod tests { assert!(extract_wikilinks("nothing here").is_empty()); } + #[test] + fn workspace_write_dir_returns_structured_dir_directly() { + let root = temp_root("ws_structured"); + let workspace = root.join("ws"); + let structured = workspace.join(".coyote").join("memory"); + fs::create_dir_all(&structured).unwrap(); + fs::write(structured.join("MEMORY.md"), "idx").unwrap(); + + let store = MemoryStore { + global_dir: root.join("g"), + workspace: discover_workspace_memory(&workspace), + }; + + let dir = workspace_write_dir(&store).unwrap(); + assert_eq!(dir, structured); + + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn workspace_write_dir_promotes_lite_to_structured_subdir() { + let root = temp_root("ws_lite_promote"); + let workspace = root.join("ws"); + fs::create_dir_all(&workspace).unwrap(); + fs::write(workspace.join("COYOTE.md"), "lite").unwrap(); + + let store = MemoryStore { + global_dir: root.join("g"), + workspace: discover_workspace_memory(&workspace), + }; + + let dir = workspace_write_dir(&store).unwrap(); + assert_eq!(dir, workspace.join(".coyote").join("memory")); + + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn workspace_write_dir_errors_when_no_workspace() { + let root = temp_root("ws_none"); + let bare = root.join("nowhere"); + fs::create_dir_all(&bare).unwrap(); + + let store = MemoryStore { + global_dir: root.join("g"), + workspace: discover_workspace_memory(&bare), + }; + + let err = workspace_write_dir(&store).unwrap_err(); + assert!(err.to_string().contains("no workspace memory discoverable")); + + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn find_file_returns_matching_file() { + let root = temp_root("find_file"); + let workspace = root.join("ws"); + let structured = workspace.join(".coyote").join("memory"); + fs::create_dir_all(&structured).unwrap(); + fs::write(structured.join("MEMORY.md"), "idx").unwrap(); + fs::write( + structured.join("target.md"), + "---\nname: target\n---\nfound me\n", + ) + .unwrap(); + fs::write( + structured.join("other.md"), + "---\nname: other\n---\nignored\n", + ) + .unwrap(); + + let store = MemoryStore { + global_dir: root.join("g"), + workspace: discover_workspace_memory(&workspace), + }; + + let hit = find_file(&store, "target").unwrap(); + assert!(hit.is_some()); + assert_eq!(hit.unwrap().body.trim(), "found me"); + + let miss = find_file(&store, "nope").unwrap(); + assert!(miss.is_none()); + + let _ = fs::remove_dir_all(&root); + } + #[test] fn lint_flags_orphans_broken_links_and_oversized() { let root = temp_root("lint");