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

367 lines
12 KiB
Rust

//! Integration tests for EditTool / WriteTool file-state cache integration
//! (TC-5.4 and TC-5.4-W series).
//!
//! Black-box tests: exercise Edit/Write tools through their public API with
//! a real filesystem and shared FileStateCache, validating "must Read first"
//! guard, staleness detection, and post-write cache updates.
use std::path::Path;
use std::sync::{Arc, RwLock};
use serde_json::json;
use nomi_config::file_cache::FileCacheConfig;
use nomi_tools::Tool;
use nomi_tools::edit::EditTool;
use nomi_tools::file_cache::{FileStateCache, file_mtime_ms};
use nomi_tools::read::ReadTool;
use nomi_tools::write::WriteTool;
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)))
}
/// Populate cache by actually reading the file through ReadTool.
async fn read_file(tool: &ReadTool, path: &Path) {
let input = json!({ "file_path": path.to_str().unwrap() });
let r = tool.execute(input).await;
assert!(!r.is_error, "read failed: {}", r.content);
}
const UNCHANGED_MARKER: &str = "File unchanged since last read";
// ==========================================================================
// TC-5.4: EditTool guard and staleness detection
// ==========================================================================
/// TC-5.4-01: Normal Read → Edit succeeds.
#[tokio::test]
async fn tc_5_4_01_read_then_edit() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("normal.txt");
std::fs::write(&file, "hello world").unwrap();
let cache = make_cache();
let read_tool = ReadTool::new(Some(cache.clone()), None);
let edit_tool = EditTool::new(Some(cache));
read_file(&read_tool, &file).await;
let input = json!({
"file_path": file.to_str().unwrap(),
"old_string": "hello",
"new_string": "goodbye"
});
let result = edit_tool.execute(input).await;
assert!(
!result.is_error,
"Edit after Read should succeed: {}",
result.content
);
assert_eq!(std::fs::read_to_string(&file).unwrap(), "goodbye world");
}
/// TC-5.4-02: Edit without prior Read returns "must Read first" error.
#[tokio::test]
async fn tc_5_4_02_edit_without_read() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("no_read.txt");
std::fs::write(&file, "content").unwrap();
let cache = make_cache();
let edit_tool = EditTool::new(Some(cache));
let input = json!({
"file_path": file.to_str().unwrap(),
"old_string": "content",
"new_string": "new"
});
let result = edit_tool.execute(input).await;
assert!(result.is_error, "Edit without Read should fail");
assert!(
result.content.contains("must Read"),
"Error should mention 'must Read': {}",
result.content
);
// File must be unchanged.
assert_eq!(std::fs::read_to_string(&file).unwrap(), "content");
}
/// TC-5.4-03: External modification after Read triggers staleness error.
#[tokio::test]
async fn tc_5_4_03_external_modification_detected() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("stale.txt");
std::fs::write(&file, "original content").unwrap();
let cache = make_cache();
let read_tool = ReadTool::new(Some(cache.clone()), None);
let edit_tool = EditTool::new(Some(cache));
read_file(&read_tool, &file).await;
// External modification.
std::thread::sleep(std::time::Duration::from_millis(50));
std::fs::write(&file, "externally changed").unwrap();
let input = json!({
"file_path": file.to_str().unwrap(),
"old_string": "original content",
"new_string": "new"
});
let result = edit_tool.execute(input).await;
assert!(
result.is_error,
"Edit of externally modified file should fail"
);
assert!(
result.content.contains("modified externally"),
"Error should mention external modification: {}",
result.content
);
}
/// TC-5.4-04: Edit → Edit succeeds because first Edit updates the cache.
#[tokio::test]
async fn tc_5_4_04_edit_then_edit() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("double.txt");
std::fs::write(&file, "aaa bbb ccc").unwrap();
let cache = make_cache();
let read_tool = ReadTool::new(Some(cache.clone()), None);
let edit_tool = EditTool::new(Some(cache));
read_file(&read_tool, &file).await;
// First edit.
let input1 = json!({
"file_path": file.to_str().unwrap(),
"old_string": "aaa",
"new_string": "AAA"
});
let r1 = edit_tool.execute(input1).await;
assert!(!r1.is_error, "First edit failed: {}", r1.content);
// Second edit — should work because first edit updated cache mtime.
let input2 = json!({
"file_path": file.to_str().unwrap(),
"old_string": "bbb",
"new_string": "BBB"
});
let r2 = edit_tool.execute(input2).await;
assert!(!r2.is_error, "Second edit failed: {}", r2.content);
assert_eq!(std::fs::read_to_string(&file).unwrap(), "AAA BBB ccc");
}
/// TC-5.4-05: With cache disabled (None), Edit works without prior Read.
#[tokio::test]
async fn tc_5_4_05_no_cache_edit_bypasses_guard() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("nocache.txt");
std::fs::write(&file, "hello").unwrap();
let edit_tool = EditTool::new(None);
let input = json!({
"file_path": file.to_str().unwrap(),
"old_string": "hello",
"new_string": "bye"
});
let result = edit_tool.execute(input).await;
assert!(
!result.is_error,
"Edit without cache should succeed: {}",
result.content
);
assert_eq!(std::fs::read_to_string(&file).unwrap(), "bye");
}
/// TC-5.4-06: replace_all updates cache mtime correctly.
#[tokio::test]
async fn tc_5_4_06_replace_all_updates_cache() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("replaceall.txt");
std::fs::write(&file, "x-x-x-x").unwrap();
let cache = make_cache();
let read_tool = ReadTool::new(Some(cache.clone()), None);
let edit_tool = EditTool::new(Some(cache.clone()));
read_file(&read_tool, &file).await;
let input = json!({
"file_path": file.to_str().unwrap(),
"old_string": "x",
"new_string": "y",
"replace_all": true
});
let result = edit_tool.execute(input).await;
assert!(!result.is_error, "replace_all failed: {}", result.content);
assert_eq!(std::fs::read_to_string(&file).unwrap(), "y-y-y-y");
// Verify cache mtime matches disk.
let disk_mtime = file_mtime_ms(&file).unwrap();
let mut c = cache.write().unwrap();
let cached = c.get(&file).expect("file should be in cache");
assert_eq!(cached.mtime_ms, disk_mtime);
}
// ==========================================================================
// TC-5.4-W: WriteTool cache update
// ==========================================================================
/// TC-5.4-W01: Write then Read returns "unchanged" (Write populates cache).
#[tokio::test]
async fn tc_5_4_w01_write_then_read_dedup() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("write_read.txt");
let cache = make_cache();
let write_tool = WriteTool::new(Some(cache.clone()));
let read_tool = ReadTool::new(Some(cache), None);
// Write creates file and populates cache.
let write_input = json!({
"file_path": file.to_str().unwrap(),
"content": "written content"
});
let wr = write_tool.execute(write_input).await;
assert!(!wr.is_error, "write failed: {}", wr.content);
// Read immediately after: should return "unchanged" because Write
// already cached the content with the correct mtime.
let read_input = json!({ "file_path": file.to_str().unwrap() });
let rr = read_tool.execute(read_input).await;
assert!(!rr.is_error);
assert!(
rr.content.contains(UNCHANGED_MARKER),
"Read after Write should return unchanged stub, got: {}",
rr.content
);
}
/// TC-5.4-W02: Write then Edit succeeds (Write populates cache for Edit guard).
#[tokio::test]
async fn tc_5_4_w02_write_then_edit() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("write_edit.txt");
let cache = make_cache();
let write_tool = WriteTool::new(Some(cache.clone()));
let edit_tool = EditTool::new(Some(cache));
let write_input = json!({
"file_path": file.to_str().unwrap(),
"content": "hello world"
});
let wr = write_tool.execute(write_input).await;
assert!(!wr.is_error, "write failed: {}", wr.content);
let edit_input = json!({
"file_path": file.to_str().unwrap(),
"old_string": "hello",
"new_string": "goodbye"
});
let er = edit_tool.execute(edit_input).await;
assert!(
!er.is_error,
"Edit after Write should succeed: {}",
er.content
);
assert_eq!(std::fs::read_to_string(&file).unwrap(), "goodbye world");
}
/// TC-5.4-W03: Write → Write → Read returns fresh content (mtime updated).
#[tokio::test]
async fn tc_5_4_w03_write_overwrite_then_read() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("overwrite.txt");
let cache = make_cache();
let write_tool = WriteTool::new(Some(cache.clone()));
let read_tool = ReadTool::new(Some(cache), None);
// First write.
let w1 = json!({
"file_path": file.to_str().unwrap(),
"content": "version 1"
});
write_tool.execute(w1).await;
// Brief delay to change mtime.
std::thread::sleep(std::time::Duration::from_millis(50));
// Second write (overwrite).
let w2 = json!({
"file_path": file.to_str().unwrap(),
"content": "version 2"
});
write_tool.execute(w2).await;
// Read: cache was updated by second Write, so should see "unchanged"
// (cache content matches disk content with matching mtime).
let read_input = json!({ "file_path": file.to_str().unwrap() });
let rr = read_tool.execute(read_input).await;
assert!(!rr.is_error);
// The cache was updated by the second Write with the new content,
// so Read should hit the dedup path.
assert!(
rr.content.contains(UNCHANGED_MARKER),
"Read after second Write should dedup, got: {}",
rr.content
);
// Verify disk has version 2.
assert_eq!(std::fs::read_to_string(&file).unwrap(), "version 2");
}
// ==========================================================================
// Supplementary: Cross-tool interaction tests
// ==========================================================================
/// Read → Edit → Read should dedup (Edit updated the cache).
#[tokio::test]
async fn read_edit_read_dedup() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("cross.txt");
std::fs::write(&file, "alpha beta").unwrap();
let cache = make_cache();
let read_tool = ReadTool::new(Some(cache.clone()), None);
let edit_tool = EditTool::new(Some(cache));
// Read.
read_file(&read_tool, &file).await;
// Edit.
let edit_input = json!({
"file_path": file.to_str().unwrap(),
"old_string": "alpha",
"new_string": "ALPHA"
});
let er = edit_tool.execute(edit_input).await;
assert!(!er.is_error);
// Read again: Edit updated the cache, so Read should see "unchanged".
let read_input = json!({ "file_path": file.to_str().unwrap() });
let rr = read_tool.execute(read_input).await;
assert!(!rr.is_error);
assert!(
rr.content.contains(UNCHANGED_MARKER),
"Read after Edit should dedup, got: {}",
rr.content
);
}