f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
290 lines
9.0 KiB
Rust
290 lines
9.0 KiB
Rust
//! Integration tests for file watching (task 7.8).
|
|
//!
|
|
//! Tests exercise `IFileWatchService` through `FileWatchService`, verifying
|
|
//! that filesystem changes produce the expected broadcast events.
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
|
|
use nomifun_api_types::WebSocketMessage;
|
|
use nomifun_file::{FileWatchService, IFileWatchService};
|
|
use nomifun_realtime::EventBroadcaster;
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test helpers
|
|
// -----------------------------------------------------------------------
|
|
|
|
/// A broadcaster that records every event for later assertion.
|
|
struct RecordingBroadcaster {
|
|
events: Mutex<Vec<WebSocketMessage<serde_json::Value>>>,
|
|
}
|
|
|
|
impl RecordingBroadcaster {
|
|
fn new() -> Self {
|
|
Self {
|
|
events: Mutex::new(Vec::new()),
|
|
}
|
|
}
|
|
|
|
/// Drain all recorded events.
|
|
fn take_events(&self) -> Vec<WebSocketMessage<serde_json::Value>> {
|
|
let mut guard = self.events.lock().unwrap();
|
|
std::mem::take(&mut *guard)
|
|
}
|
|
}
|
|
|
|
impl EventBroadcaster for RecordingBroadcaster {
|
|
fn broadcast(&self, event: WebSocketMessage<serde_json::Value>) {
|
|
self.events.lock().unwrap().push(event);
|
|
}
|
|
}
|
|
|
|
fn make_service() -> (Arc<dyn IFileWatchService>, Arc<RecordingBroadcaster>) {
|
|
let recorder = Arc::new(RecordingBroadcaster::new());
|
|
let svc = FileWatchService::new(recorder.clone()).unwrap();
|
|
(Arc::new(svc), recorder)
|
|
}
|
|
|
|
/// Wait a bit for the OS file-system event to propagate and the watcher
|
|
/// callback to fire. File-system notifications are inherently asynchronous.
|
|
async fn settle() {
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Single-file watching
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn start_watch_and_detect_change() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("watched.txt");
|
|
std::fs::write(&file, "initial").unwrap();
|
|
|
|
let (svc, recorder) = make_service();
|
|
svc.start_watch(file.to_str().unwrap()).await.unwrap();
|
|
|
|
// Modify the file.
|
|
settle().await;
|
|
std::fs::write(&file, "updated").unwrap();
|
|
settle().await;
|
|
|
|
let events = recorder.take_events();
|
|
assert!(
|
|
events.iter().any(|e| e.name == "fileWatch.fileChanged"),
|
|
"expected fileWatch.fileChanged event, got: {events:?}"
|
|
);
|
|
|
|
let ev = events.iter().find(|e| e.name == "fileWatch.fileChanged").unwrap();
|
|
assert!(ev.data["file_path"].as_str().is_some());
|
|
assert!(ev.data["event_type"].as_str().is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stop_watch_stops_events() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("stop_me.txt");
|
|
std::fs::write(&file, "v1").unwrap();
|
|
|
|
let (svc, recorder) = make_service();
|
|
let path_str = file.to_str().unwrap();
|
|
svc.start_watch(path_str).await.unwrap();
|
|
settle().await;
|
|
|
|
svc.stop_watch(path_str).await.unwrap();
|
|
// Drain any events from the watch setup.
|
|
recorder.take_events();
|
|
|
|
// Modify after stop — should NOT produce events.
|
|
std::fs::write(&file, "v2").unwrap();
|
|
settle().await;
|
|
|
|
let events = recorder.take_events();
|
|
assert!(events.is_empty(), "expected no events after stop, got: {events:?}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stop_all_watches_clears_file_watches() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file_a = dir.path().join("a.txt");
|
|
let file_b = dir.path().join("b.txt");
|
|
std::fs::write(&file_a, "a").unwrap();
|
|
std::fs::write(&file_b, "b").unwrap();
|
|
|
|
let (svc, recorder) = make_service();
|
|
svc.start_watch(file_a.to_str().unwrap()).await.unwrap();
|
|
svc.start_watch(file_b.to_str().unwrap()).await.unwrap();
|
|
settle().await;
|
|
|
|
svc.stop_all_watches().await.unwrap();
|
|
recorder.take_events();
|
|
|
|
std::fs::write(&file_a, "a2").unwrap();
|
|
std::fs::write(&file_b, "b2").unwrap();
|
|
settle().await;
|
|
|
|
let events = recorder.take_events();
|
|
assert!(events.is_empty(), "expected no events after stop_all, got: {events:?}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn idempotent_start_watch() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("idem.txt");
|
|
std::fs::write(&file, "x").unwrap();
|
|
|
|
let (svc, _recorder) = make_service();
|
|
let path_str = file.to_str().unwrap();
|
|
svc.start_watch(path_str).await.unwrap();
|
|
// Second start should be a no-op, not an error.
|
|
svc.start_watch(path_str).await.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn watch_nonexistent_file_returns_error() {
|
|
let (svc, _recorder) = make_service();
|
|
let result = svc.start_watch("/tmp/nonexistent_12345.txt").await;
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Workspace Office file watching
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn office_watch_detects_docx() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let (svc, recorder) = make_service();
|
|
svc.start_office_watch(dir.path().to_str().unwrap()).await.unwrap();
|
|
settle().await;
|
|
|
|
// Create a .docx file.
|
|
std::fs::write(dir.path().join("report.docx"), "fake docx").unwrap();
|
|
settle().await;
|
|
|
|
let events = recorder.take_events();
|
|
assert!(
|
|
events.iter().any(|e| e.name == "workspaceOfficeWatch.fileAdded"),
|
|
"expected workspaceOfficeWatch.fileAdded event, got: {events:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn office_watch_detects_xlsx() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let (svc, recorder) = make_service();
|
|
svc.start_office_watch(dir.path().to_str().unwrap()).await.unwrap();
|
|
settle().await;
|
|
|
|
std::fs::write(dir.path().join("data.xlsx"), "fake xlsx").unwrap();
|
|
settle().await;
|
|
|
|
let events = recorder.take_events();
|
|
assert!(
|
|
events.iter().any(|e| e.name == "workspaceOfficeWatch.fileAdded"),
|
|
"expected fileAdded for .xlsx, got: {events:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn office_watch_detects_pptx() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let (svc, recorder) = make_service();
|
|
svc.start_office_watch(dir.path().to_str().unwrap()).await.unwrap();
|
|
settle().await;
|
|
|
|
std::fs::write(dir.path().join("slides.pptx"), "fake pptx").unwrap();
|
|
settle().await;
|
|
|
|
let events = recorder.take_events();
|
|
assert!(
|
|
events.iter().any(|e| e.name == "workspaceOfficeWatch.fileAdded"),
|
|
"expected fileAdded for .pptx, got: {events:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn office_watch_ignores_non_office_files() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let (svc, recorder) = make_service();
|
|
svc.start_office_watch(dir.path().to_str().unwrap()).await.unwrap();
|
|
settle().await;
|
|
|
|
// Drain any setup events.
|
|
recorder.take_events();
|
|
|
|
// Create a non-Office file — should NOT trigger.
|
|
std::fs::write(dir.path().join("notes.txt"), "hello").unwrap();
|
|
settle().await;
|
|
|
|
let events = recorder.take_events();
|
|
let office_events: Vec<_> = events
|
|
.iter()
|
|
.filter(|e| e.name == "workspaceOfficeWatch.fileAdded")
|
|
.collect();
|
|
assert!(
|
|
office_events.is_empty(),
|
|
"expected no office events for .txt, got: {office_events:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stop_office_watch_stops_events() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let (svc, recorder) = make_service();
|
|
let ws = dir.path().to_str().unwrap();
|
|
svc.start_office_watch(ws).await.unwrap();
|
|
settle().await;
|
|
|
|
svc.stop_office_watch(ws).await.unwrap();
|
|
recorder.take_events();
|
|
|
|
std::fs::write(dir.path().join("after_stop.docx"), "data").unwrap();
|
|
settle().await;
|
|
|
|
let events = recorder.take_events();
|
|
let office_events: Vec<_> = events
|
|
.iter()
|
|
.filter(|e| e.name == "workspaceOfficeWatch.fileAdded")
|
|
.collect();
|
|
assert!(
|
|
office_events.is_empty(),
|
|
"expected no events after stop, got: {office_events:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn idempotent_office_watch() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let (svc, _recorder) = make_service();
|
|
let ws = dir.path().to_str().unwrap();
|
|
svc.start_office_watch(ws).await.unwrap();
|
|
// Second call should be a no-op.
|
|
svc.start_office_watch(ws).await.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn office_watch_event_has_correct_fields() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let (svc, recorder) = make_service();
|
|
svc.start_office_watch(dir.path().to_str().unwrap()).await.unwrap();
|
|
settle().await;
|
|
|
|
std::fs::write(dir.path().join("check.docx"), "content").unwrap();
|
|
settle().await;
|
|
|
|
let events = recorder.take_events();
|
|
let ev = events.iter().find(|e| e.name == "workspaceOfficeWatch.fileAdded");
|
|
assert!(ev.is_some(), "expected fileAdded event, got: {events:?}");
|
|
|
|
let data = &ev.unwrap().data;
|
|
assert!(
|
|
data["file_path"].as_str().is_some_and(|p| p.ends_with("check.docx")),
|
|
"file_path should end with check.docx: {data:?}"
|
|
);
|
|
assert!(
|
|
data["workspace"].as_str().is_some(),
|
|
"workspace should be present: {data:?}"
|
|
);
|
|
}
|