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

338 lines
11 KiB
Rust

//! Integration tests for directory browsing and file metadata (task 7.3).
//!
//! These tests exercise the full `FileService` through the `IFileService` trait,
//! including path validation, .gitignore handling, and caching behavior.
use std::fs;
use std::sync::Arc;
use nomifun_api_types::WebSocketMessage;
use nomifun_file::{FileService, IFileService};
use nomifun_realtime::EventBroadcaster;
/// A no-op broadcaster for testing (events are silently discarded).
struct NoopBroadcaster;
impl EventBroadcaster for NoopBroadcaster {
fn broadcast(&self, _event: WebSocketMessage<serde_json::Value>) {}
}
/// Create a `FileService` whose sandbox is rooted at the given temp directory.
fn make_service(root: &std::path::Path) -> FileService {
FileService::new(Arc::new(NoopBroadcaster), vec![root.to_path_buf()])
}
// -----------------------------------------------------------------------
// getFilesByDir
// -----------------------------------------------------------------------
#[tokio::test]
async fn get_files_by_dir_lists_children() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.txt"), "hello").unwrap();
fs::create_dir(dir.path().join("sub")).unwrap();
fs::write(dir.path().join("sub/nested.txt"), "nested").unwrap();
let svc = make_service(dir.path());
let root = dir.path().to_str().unwrap();
let items = svc.get_files_by_dir(root, root).await.unwrap();
// Should have: sub/ and a.txt
assert_eq!(items.len(), 2);
let sub = items.iter().find(|i| i.name == "sub").unwrap();
assert!(sub.is_dir);
assert_eq!(sub.children.len(), 1);
assert_eq!(sub.children[0].name, "nested.txt");
let file = items.iter().find(|i| i.name == "a.txt").unwrap();
assert!(!file.is_dir);
assert!(file.children.is_empty());
}
#[tokio::test]
async fn get_files_by_dir_empty_directory() {
let dir = tempfile::tempdir().unwrap();
let svc = make_service(dir.path());
let root = dir.path().to_str().unwrap();
let items = svc.get_files_by_dir(root, root).await.unwrap();
assert!(items.is_empty());
}
#[tokio::test]
async fn get_files_by_dir_subdirectory() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("src");
fs::create_dir(&sub).unwrap();
fs::write(sub.join("main.rs"), "fn main(){}").unwrap();
fs::write(sub.join("lib.rs"), "pub mod foo;").unwrap();
let svc = make_service(dir.path());
let root = dir.path().to_str().unwrap();
let items = svc.get_files_by_dir(sub.to_str().unwrap(), root).await.unwrap();
assert_eq!(items.len(), 2);
// Relative paths should be relative to root, not to sub
assert!(items[0].relative_path.starts_with("src/"));
}
#[tokio::test]
async fn get_files_by_dir_rejects_path_outside_sandbox() {
let sandbox = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
fs::write(outside.path().join("secret.txt"), "secret").unwrap();
let svc = make_service(sandbox.path());
let result = svc
.get_files_by_dir(outside.path().to_str().unwrap(), outside.path().to_str().unwrap())
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn get_files_by_dir_nonexistent_dir() {
let dir = tempfile::tempdir().unwrap();
let fake = dir.path().join("nonexistent");
let svc = make_service(dir.path());
let result = svc
.get_files_by_dir(fake.to_str().unwrap(), dir.path().to_str().unwrap())
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn get_files_by_dir_directories_sorted_first() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("z_file.txt"), "").unwrap();
fs::create_dir(dir.path().join("a_dir")).unwrap();
fs::write(dir.path().join("a_file.txt"), "").unwrap();
let svc = make_service(dir.path());
let root = dir.path().to_str().unwrap();
let items = svc.get_files_by_dir(root, root).await.unwrap();
// Directory first
assert!(items[0].is_dir);
assert_eq!(items[0].name, "a_dir");
// Then files alphabetically
assert_eq!(items[1].name, "a_file.txt");
assert_eq!(items[2].name, "z_file.txt");
}
// -----------------------------------------------------------------------
// listWorkspaceFiles
// -----------------------------------------------------------------------
#[tokio::test]
async fn list_workspace_files_recursive() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("root.txt"), "").unwrap();
fs::create_dir(dir.path().join("a")).unwrap();
fs::write(dir.path().join("a/nested.txt"), "").unwrap();
fs::create_dir(dir.path().join("a/b")).unwrap();
fs::write(dir.path().join("a/b/deep.txt"), "").unwrap();
let svc = make_service(dir.path());
let files = svc.list_workspace_files(dir.path().to_str().unwrap()).await.unwrap();
assert_eq!(files.len(), 3);
let names: Vec<&str> = files.iter().map(|f| f.name.as_str()).collect();
assert!(names.contains(&"root.txt"));
assert!(names.contains(&"nested.txt"));
assert!(names.contains(&"deep.txt"));
}
#[tokio::test]
async fn list_workspace_files_respects_gitignore() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join(".gitignore"), "*.log\nbuild/\n").unwrap();
fs::write(dir.path().join("app.rs"), "").unwrap();
fs::write(dir.path().join("debug.log"), "").unwrap();
fs::create_dir(dir.path().join("build")).unwrap();
fs::write(dir.path().join("build/output.js"), "").unwrap();
let svc = make_service(dir.path());
let files = svc.list_workspace_files(dir.path().to_str().unwrap()).await.unwrap();
let names: Vec<&str> = files.iter().map(|f| f.name.as_str()).collect();
assert!(names.contains(&"app.rs"));
assert!(names.contains(&".gitignore"));
assert!(!names.contains(&"debug.log"));
assert!(!names.contains(&"output.js"));
}
#[tokio::test]
async fn list_workspace_files_empty_workspace() {
let dir = tempfile::tempdir().unwrap();
let svc = make_service(dir.path());
let files = svc.list_workspace_files(dir.path().to_str().unwrap()).await.unwrap();
assert!(files.is_empty());
}
#[tokio::test]
async fn list_workspace_files_rejects_outside_sandbox() {
let sandbox = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let svc = make_service(sandbox.path());
let result = svc.list_workspace_files(outside.path().to_str().unwrap()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn list_workspace_files_cache_hit() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("file.txt"), "data").unwrap();
let svc = make_service(dir.path());
let root = dir.path().to_str().unwrap();
// First call populates cache
let first = svc.list_workspace_files(root).await.unwrap();
assert_eq!(first.len(), 1);
// Add a file — should NOT appear due to cache
fs::write(dir.path().join("new.txt"), "new").unwrap();
let second = svc.list_workspace_files(root).await.unwrap();
assert_eq!(second.len(), 1); // Still cached
// Invalidate cache
svc.invalidate_cache(&std::fs::canonicalize(dir.path()).unwrap().to_string_lossy());
// Now should see new file
let third = svc.list_workspace_files(root).await.unwrap();
assert_eq!(third.len(), 2);
}
#[tokio::test]
async fn list_workspace_files_relative_paths() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("src/utils")).unwrap();
fs::write(dir.path().join("src/utils/helper.ts"), "").unwrap();
let svc = make_service(dir.path());
let files = svc.list_workspace_files(dir.path().to_str().unwrap()).await.unwrap();
let helper = files.iter().find(|f| f.name == "helper.ts").unwrap();
assert_eq!(helper.relative_path, "src/utils/helper.ts");
}
#[cfg(unix)]
#[tokio::test]
async fn list_workspace_files_skips_directory_symlinks() {
let dir = tempfile::tempdir().unwrap();
let skill_dir = dir.path().join("builtin-skills/auto-inject/nomifun-skills");
fs::create_dir_all(&skill_dir).unwrap();
fs::write(skill_dir.join("SKILL.md"), "---\ndescription: test\n---\nbody").unwrap();
let workspace_skills_dir = dir.path().join("workspace/.claude/skills");
fs::create_dir_all(&workspace_skills_dir).unwrap();
std::os::unix::fs::symlink(&skill_dir, workspace_skills_dir.join("nomifun-skills")).unwrap();
let svc = make_service(dir.path().join("workspace").as_path());
let files = svc
.list_workspace_files(dir.path().join("workspace").to_str().unwrap())
.await
.unwrap();
assert!(
files.iter().all(|file| file.name != "nomifun-skills"),
"directory symlink should not be surfaced as a file: {files:?}"
);
}
// -----------------------------------------------------------------------
// getFileMetadata
// -----------------------------------------------------------------------
#[tokio::test]
async fn get_file_metadata_text_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hello.txt");
fs::write(&file, "hello world").unwrap();
let svc = make_service(dir.path());
let meta = svc.get_file_metadata(file.to_str().unwrap(), None).await.unwrap();
assert_eq!(meta.name, "hello.txt");
assert_eq!(meta.size, 11);
assert_eq!(meta.mime_type, "text/plain");
assert!(!meta.is_directory);
assert!(meta.last_modified > 0);
}
#[tokio::test]
async fn get_file_metadata_image() {
let dir = tempfile::tempdir().unwrap();
let png = dir.path().join("photo.png");
fs::write(&png, [0x89, 0x50, 0x4E, 0x47]).unwrap();
let svc = make_service(dir.path());
let meta = svc.get_file_metadata(png.to_str().unwrap(), None).await.unwrap();
assert_eq!(meta.mime_type, "image/png");
}
#[tokio::test]
async fn get_file_metadata_directory() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("mydir");
fs::create_dir(&sub).unwrap();
let svc = make_service(dir.path());
let meta = svc.get_file_metadata(sub.to_str().unwrap(), None).await.unwrap();
assert!(meta.is_directory);
assert_eq!(meta.mime_type, "inode/directory");
assert_eq!(meta.name, "mydir");
}
#[tokio::test]
async fn get_file_metadata_nonexistent() {
let dir = tempfile::tempdir().unwrap();
let fake = dir.path().join("missing.txt");
let svc = make_service(dir.path());
let result = svc.get_file_metadata(fake.to_str().unwrap(), None).await;
assert!(result.is_err());
}
#[tokio::test]
async fn get_file_metadata_outside_sandbox() {
let sandbox = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let secret = outside.path().join("secret.txt");
fs::write(&secret, "secret").unwrap();
let svc = make_service(sandbox.path());
let result = svc.get_file_metadata(secret.to_str().unwrap(), None).await;
assert!(result.is_err());
}
#[tokio::test]
async fn get_file_metadata_json_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("config.json");
fs::write(&file, r#"{"key":"value"}"#).unwrap();
let svc = make_service(dir.path());
let meta = svc.get_file_metadata(file.to_str().unwrap(), None).await.unwrap();
assert_eq!(meta.mime_type, "application/json");
}