Files
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

255 lines
8.1 KiB
Rust

//! Integration tests for ReadTool dedup and cache integration (TC-5.3 series).
//!
//! Black-box tests: exercise ReadTool through its public API with a real
//! filesystem, validating dedup detection and cache update behavior.
use std::sync::{Arc, RwLock};
use serde_json::json;
use nomi_config::file_cache::FileCacheConfig;
use nomi_tools::Tool;
use nomi_tools::file_cache::FileStateCache;
use nomi_tools::read::ReadTool;
fn make_cache() -> Arc<RwLock<FileStateCache>> {
let config = FileCacheConfig {
max_entries: 100,
max_size_bytes: 25 * 1024 * 1024,
enabled: true,
};
Arc::new(RwLock::new(FileStateCache::new(&config)))
}
const UNCHANGED_MARKER: &str = "File unchanged since last read";
/// TC-5.3-01: First read returns full content with line numbers.
#[tokio::test]
async fn tc_5_3_01_first_read_returns_full_content() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hello.rs");
std::fs::write(&file, "fn main() {\n println!(\"hello\");\n}\n").unwrap();
let cache = make_cache();
let tool = ReadTool::new(Some(cache), None);
let input = json!({ "file_path": file.to_str().unwrap() });
let result = tool.execute(input).await;
assert!(!result.is_error);
assert!(result.content.contains("1\tfn main()"));
assert!(result.content.contains("2\t println!"));
assert!(result.content.contains("3\t}"));
assert!(
!result.content.contains(UNCHANGED_MARKER),
"First read must not return the unchanged stub"
);
}
/// TC-5.3-02: Second read of the same unchanged file returns the dedup stub.
#[tokio::test]
async fn tc_5_3_02_dedup_on_unchanged_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("stable.txt");
std::fs::write(&file, "line one\nline two\n").unwrap();
let cache = make_cache();
let tool = ReadTool::new(Some(cache), None);
let input = json!({ "file_path": file.to_str().unwrap() });
// First read: full content.
let r1 = tool.execute(input.clone()).await;
assert!(!r1.is_error);
assert!(r1.content.contains("line one"));
// Second read: unchanged stub.
let r2 = tool.execute(input).await;
assert!(!r2.is_error);
assert!(
r2.content.contains(UNCHANGED_MARKER),
"Second read of unchanged file should return the dedup stub"
);
}
/// TC-5.3-03: After external modification, re-read returns new content.
#[tokio::test]
async fn tc_5_3_03_modified_file_returns_new_content() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("evolving.txt");
std::fs::write(&file, "version 1\n").unwrap();
let cache = make_cache();
let tool = ReadTool::new(Some(cache), None);
let input = json!({ "file_path": file.to_str().unwrap() });
let r1 = tool.execute(input.clone()).await;
assert!(r1.content.contains("version 1"));
// External modification — sleep to ensure mtime changes.
std::thread::sleep(std::time::Duration::from_millis(50));
std::fs::write(&file, "version 2\n").unwrap();
let r2 = tool.execute(input).await;
assert!(!r2.is_error);
assert!(
r2.content.contains("version 2"),
"After modification, read should return new content"
);
assert!(
!r2.content.contains(UNCHANGED_MARKER),
"Modified file must not return unchanged stub"
);
}
/// TC-5.3-04: Different offset/limit parameters are not deduped.
#[tokio::test]
async fn tc_5_3_04_different_range_no_dedup() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("multiline.txt");
let content: String = (1..=30).map(|i| format!("line {}\n", i)).collect();
std::fs::write(&file, &content).unwrap();
let cache = make_cache();
let tool = ReadTool::new(Some(cache), None);
let path_str = file.to_str().unwrap();
// Read lines 0..10.
let input1 = json!({ "file_path": path_str, "offset": 0, "limit": 10 });
let r1 = tool.execute(input1).await;
assert!(!r1.is_error);
assert!(r1.content.contains("line 1"));
// Read lines 10..20 — different range, should return full content.
let input2 = json!({ "file_path": path_str, "offset": 10, "limit": 10 });
let r2 = tool.execute(input2).await;
assert!(!r2.is_error);
assert!(
r2.content.contains("line 11"),
"Different offset/limit should return full content"
);
assert!(
!r2.content.contains(UNCHANGED_MARKER),
"Different range must not trigger dedup"
);
}
/// TC-5.3-05: With cache disabled (None), reads always return full content.
#[tokio::test]
async fn tc_5_3_05_cache_disabled_no_dedup() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("nocache.txt");
std::fs::write(&file, "always full\n").unwrap();
let tool = ReadTool::new(None, None);
let input = json!({ "file_path": file.to_str().unwrap() });
let r1 = tool.execute(input.clone()).await;
assert!(r1.content.contains("always full"));
let r2 = tool.execute(input).await;
assert!(
r2.content.contains("always full"),
"Without cache, second read should still return full content"
);
assert!(!r2.content.contains(UNCHANGED_MARKER));
}
/// TC-5.3-06: Reading a non-existent file returns an error and does not cache.
#[tokio::test]
async fn tc_5_3_06_nonexistent_file_error_no_cache() {
let cache = make_cache();
let tool = ReadTool::new(Some(cache.clone()), None);
let input = json!({ "file_path": "/tmp/does_not_exist_tc_5_3_06.txt" });
let result = tool.execute(input).await;
assert!(result.is_error);
assert!(result.content.contains("Failed to read file"));
// Cache should remain empty.
let c = cache.read().unwrap();
assert!(c.is_empty(), "Failed reads must not populate the cache");
}
/// TC-5.3-07: Empty file can be deduped on second read.
#[tokio::test]
async fn tc_5_3_07_empty_file_dedup() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("empty.txt");
std::fs::File::create(&file).unwrap();
let cache = make_cache();
let tool = ReadTool::new(Some(cache), None);
let input = json!({ "file_path": file.to_str().unwrap() });
let r1 = tool.execute(input.clone()).await;
assert!(!r1.is_error);
let r2 = tool.execute(input).await;
assert!(!r2.is_error);
assert!(
r2.content.contains(UNCHANGED_MARKER),
"Empty file should be deduped on second read"
);
}
/// Supplementary: Same range read twice returns dedup stub.
#[tokio::test]
async fn same_range_dedup() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("range.txt");
let content: String = (1..=20).map(|i| format!("line {}\n", i)).collect();
std::fs::write(&file, &content).unwrap();
let cache = make_cache();
let tool = ReadTool::new(Some(cache), None);
let input = json!({ "file_path": file.to_str().unwrap(), "offset": 5, "limit": 5 });
let r1 = tool.execute(input.clone()).await;
assert!(!r1.is_error);
assert!(r1.content.contains("line 6"));
let r2 = tool.execute(input).await;
assert!(!r2.is_error);
assert!(
r2.content.contains(UNCHANGED_MARKER),
"Same range on unchanged file should trigger dedup"
);
}
/// Supplementary: Cache entry is updated after modification + re-read.
#[tokio::test]
async fn cache_updated_after_modification() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("update.txt");
std::fs::write(&file, "v1\n").unwrap();
let cache = make_cache();
let tool = ReadTool::new(Some(cache.clone()), None);
let input = json!({ "file_path": file.to_str().unwrap() });
// First read: caches v1.
tool.execute(input.clone()).await;
// Modify.
std::thread::sleep(std::time::Duration::from_millis(50));
std::fs::write(&file, "v2\n").unwrap();
// Second read: returns v2 and updates cache.
let r2 = tool.execute(input.clone()).await;
assert!(r2.content.contains("v2"));
// Third read: should dedup on v2.
let r3 = tool.execute(input).await;
assert!(
r3.content.contains(UNCHANGED_MARKER),
"After re-read of modified file, cache should be updated and third read deduped"
);
}