f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
298 lines
9.3 KiB
Rust
298 lines
9.3 KiB
Rust
// Integration tests for the memory store.
|
|
//
|
|
// These tests target functional requirements from test-plan.md TC-3 and TC-4,
|
|
// treating the public API as a black box.
|
|
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
use nomi_memory::store;
|
|
use nomi_memory::types::{MemoryEntry, MemoryFrontmatter, MemoryType};
|
|
|
|
// ===========================================================================
|
|
// TC-3: Memory file read/write
|
|
// ===========================================================================
|
|
|
|
// -- TC-3.1: Write then read full memory ------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_1_write_then_read_full_memory() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let entry = MemoryEntry::build(
|
|
"test memory",
|
|
"a test description",
|
|
MemoryType::User,
|
|
"Body content here",
|
|
);
|
|
|
|
let path = store::write_memory(tmp.path(), &entry).unwrap();
|
|
let read_back = store::read_memory(&path).unwrap();
|
|
|
|
assert_eq!(read_back.frontmatter.name, entry.frontmatter.name);
|
|
assert_eq!(
|
|
read_back.frontmatter.description,
|
|
entry.frontmatter.description
|
|
);
|
|
assert_eq!(
|
|
read_back.frontmatter.memory_type,
|
|
entry.frontmatter.memory_type
|
|
);
|
|
assert_eq!(read_back.content, entry.content);
|
|
}
|
|
|
|
// -- TC-3.2: Read file with frontmatter -------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_2_read_with_frontmatter() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let path = tmp.path().join("test.md");
|
|
fs::write(
|
|
&path,
|
|
"---\nname: test memory\ndescription: a test\ntype: feedback\n---\nBody content here",
|
|
)
|
|
.unwrap();
|
|
|
|
let entry = store::read_memory(&path).unwrap();
|
|
assert_eq!(entry.frontmatter.name.as_deref(), Some("test memory"));
|
|
assert_eq!(entry.frontmatter.description.as_deref(), Some("a test"));
|
|
assert_eq!(entry.frontmatter.memory_type, Some(MemoryType::Feedback));
|
|
assert_eq!(entry.content, "Body content here");
|
|
}
|
|
|
|
// -- TC-3.3: Read file without frontmatter ----------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_3_read_without_frontmatter() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let path = tmp.path().join("plain.md");
|
|
fs::write(&path, "Just plain text").unwrap();
|
|
|
|
let entry = store::read_memory(&path).unwrap();
|
|
assert_eq!(entry.frontmatter.name, None);
|
|
assert_eq!(entry.frontmatter.description, None);
|
|
assert_eq!(entry.frontmatter.memory_type, None);
|
|
assert_eq!(entry.content, "Just plain text");
|
|
}
|
|
|
|
// -- TC-3.4: Read empty file ------------------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_4_read_empty_file() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let path = tmp.path().join("empty.md");
|
|
fs::write(&path, "").unwrap();
|
|
|
|
let entry = store::read_memory(&path).unwrap();
|
|
assert_eq!(entry.frontmatter, MemoryFrontmatter::default());
|
|
assert_eq!(entry.content, "");
|
|
}
|
|
|
|
// -- TC-3.5: Read incomplete frontmatter ------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_5_read_incomplete_frontmatter() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let path = tmp.path().join("incomplete.md");
|
|
fs::write(&path, "---\nname: orphan\nno closing delimiter").unwrap();
|
|
|
|
// Should not panic, should degrade gracefully
|
|
let entry = store::read_memory(&path).unwrap();
|
|
// Entire content treated as body since frontmatter is incomplete
|
|
assert_eq!(entry.frontmatter, MemoryFrontmatter::default());
|
|
assert!(entry.content.contains("orphan"));
|
|
}
|
|
|
|
// -- TC-3.6: Delete existing memory file ------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_6_delete_existing_file() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let entry = MemoryEntry::build("to delete", "desc", MemoryType::Feedback, "content");
|
|
let path = store::write_memory(tmp.path(), &entry).unwrap();
|
|
assert!(path.exists());
|
|
|
|
store::delete_memory(&path).unwrap();
|
|
assert!(!path.exists());
|
|
}
|
|
|
|
// -- TC-3.7: Delete non-existent file returns error -------------------------
|
|
|
|
#[test]
|
|
fn tc_3_7_delete_nonexistent_file() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let path = tmp.path().join("nonexistent.md");
|
|
|
|
let result = store::delete_memory(&path);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
// -- TC-3.8: Written filename format ----------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_8_filename_format() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let entry = MemoryEntry::build("My Role", "desc", MemoryType::User, "content");
|
|
|
|
let path = store::write_memory(tmp.path(), &entry).unwrap();
|
|
let filename = path.file_name().unwrap().to_str().unwrap();
|
|
|
|
// Should be lowercase, safe characters
|
|
assert_eq!(filename, "user_my_role.md");
|
|
assert!(
|
|
filename
|
|
.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// TC-4: Directory scanning
|
|
// ===========================================================================
|
|
|
|
// -- TC-4.1: Scan directory with multiple memory files ----------------------
|
|
|
|
#[test]
|
|
fn tc_4_1_scan_multiple_files() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
|
|
// Create 3 memory files + MEMORY.md
|
|
fs::write(
|
|
dir.join("user_role.md"),
|
|
"---\ntype: user\ndescription: role\n---\nBody",
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
dir.join("feedback_testing.md"),
|
|
"---\ntype: feedback\n---\nBody",
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
dir.join("project_status.md"),
|
|
"---\ntype: project\n---\nBody",
|
|
)
|
|
.unwrap();
|
|
fs::write(dir.join("MEMORY.md"), "# Index\n- [role](user_role.md)").unwrap();
|
|
|
|
let headers = store::scan_memory_files(dir).unwrap();
|
|
|
|
// MEMORY.md should be excluded
|
|
assert_eq!(headers.len(), 3);
|
|
let filenames: Vec<&str> = headers.iter().map(|h| h.filename.as_str()).collect();
|
|
assert!(!filenames.contains(&"MEMORY.md"));
|
|
}
|
|
|
|
// -- TC-4.2: Scan empty directory -------------------------------------------
|
|
|
|
#[test]
|
|
fn tc_4_2_scan_empty_dir() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let headers = store::scan_memory_files(tmp.path()).unwrap();
|
|
assert!(headers.is_empty());
|
|
}
|
|
|
|
// -- TC-4.3: Scan non-existent directory ------------------------------------
|
|
|
|
#[test]
|
|
fn tc_4_3_scan_nonexistent_dir() {
|
|
let headers = store::scan_memory_files(Path::new("/nonexistent/dir")).unwrap();
|
|
assert!(headers.is_empty());
|
|
}
|
|
|
|
// -- TC-4.4: Sort by modification time (newest first) -----------------------
|
|
|
|
#[test]
|
|
fn tc_4_4_sort_by_mtime() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
|
|
// Write files with small delays to ensure different mtimes
|
|
fs::write(dir.join("old.md"), "---\nname: old\n---\nOld").unwrap();
|
|
thread::sleep(Duration::from_millis(50));
|
|
fs::write(dir.join("mid.md"), "---\nname: mid\n---\nMid").unwrap();
|
|
thread::sleep(Duration::from_millis(50));
|
|
fs::write(dir.join("new.md"), "---\nname: new\n---\nNew").unwrap();
|
|
|
|
let headers = store::scan_memory_files(dir).unwrap();
|
|
assert_eq!(headers.len(), 3);
|
|
|
|
// Newest first
|
|
assert_eq!(headers[0].filename, "new.md");
|
|
assert_eq!(headers[2].filename, "old.md");
|
|
}
|
|
|
|
// -- TC-4.5: File count cap at 200 -----------------------------------------
|
|
|
|
#[test]
|
|
fn tc_4_5_file_count_cap() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
|
|
// Create 210 files
|
|
for i in 0..210 {
|
|
fs::write(
|
|
dir.join(format!("mem_{i:03}.md")),
|
|
format!("---\nname: mem{i}\n---\nBody {i}"),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let headers = store::scan_memory_files(dir).unwrap();
|
|
assert_eq!(headers.len(), 200);
|
|
}
|
|
|
|
// -- TC-4.6: Non-.md files are ignored --------------------------------------
|
|
|
|
#[test]
|
|
fn tc_4_6_non_md_ignored() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
|
|
fs::write(dir.join("memory.md"), "---\nname: valid\n---\nBody").unwrap();
|
|
fs::write(dir.join("notes.txt"), "text file").unwrap();
|
|
fs::write(dir.join("data.json"), "{}").unwrap();
|
|
fs::write(dir.join("script.py"), "pass").unwrap();
|
|
|
|
let headers = store::scan_memory_files(dir).unwrap();
|
|
assert_eq!(headers.len(), 1);
|
|
assert_eq!(headers[0].filename, "memory.md");
|
|
}
|
|
|
|
// -- TC-4.7: Format memory manifest -----------------------------------------
|
|
|
|
#[test]
|
|
fn tc_4_7_format_manifest() {
|
|
use chrono::{TimeZone, Utc};
|
|
|
|
let headers = vec![
|
|
nomi_memory::types::MemoryHeader {
|
|
filename: "user_role.md".into(),
|
|
file_path: "/mem/user_role.md".into(),
|
|
mtime: Utc.with_ymd_and_hms(2026, 4, 10, 12, 0, 0).unwrap(),
|
|
description: Some("User role info".into()),
|
|
memory_type: Some(MemoryType::User),
|
|
},
|
|
nomi_memory::types::MemoryHeader {
|
|
filename: "notes.md".into(),
|
|
file_path: "/mem/notes.md".into(),
|
|
mtime: Utc.with_ymd_and_hms(2026, 4, 9, 8, 0, 0).unwrap(),
|
|
description: None,
|
|
memory_type: None,
|
|
},
|
|
];
|
|
|
|
let manifest = store::format_memory_manifest(&headers);
|
|
let lines: Vec<&str> = manifest.lines().collect();
|
|
|
|
assert_eq!(lines.len(), 2);
|
|
// First: has type and description
|
|
assert!(lines[0].contains("[user]"));
|
|
assert!(lines[0].contains("user_role.md"));
|
|
assert!(lines[0].contains("User role info"));
|
|
// Second: no type, no description
|
|
assert!(!lines[1].contains("["));
|
|
assert!(lines[1].contains("notes.md"));
|
|
}
|