f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
265 lines
9.0 KiB
Rust
265 lines
9.0 KiB
Rust
use nomifun_api_types::{PreviewHistoryTargetDto, PreviewSnapshotInfoDto};
|
|
use nomifun_common::PreviewContentType;
|
|
use nomifun_office::SnapshotService;
|
|
|
|
fn make_target(content_type: PreviewContentType, file_path: Option<&str>) -> PreviewHistoryTargetDto {
|
|
PreviewHistoryTargetDto {
|
|
content_type,
|
|
file_path: file_path.map(String::from),
|
|
workspace: None,
|
|
file_name: None,
|
|
title: None,
|
|
language: None,
|
|
conversation_id: None,
|
|
}
|
|
}
|
|
|
|
fn make_target_full(
|
|
content_type: PreviewContentType,
|
|
file_path: Option<&str>,
|
|
workspace: Option<&str>,
|
|
conversation_id: Option<i64>,
|
|
) -> PreviewHistoryTargetDto {
|
|
PreviewHistoryTargetDto {
|
|
content_type,
|
|
file_path: file_path.map(String::from),
|
|
workspace: workspace.map(String::from),
|
|
file_name: None,
|
|
title: None,
|
|
language: None,
|
|
conversation_id,
|
|
}
|
|
}
|
|
|
|
// SH-1: Save snapshot
|
|
#[tokio::test]
|
|
async fn sh1_save_snapshot() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let target = make_target(PreviewContentType::Markdown, Some("/a.md"));
|
|
|
|
let info = svc.save(&target, "# Hello").await.unwrap();
|
|
|
|
assert!(!info.id.is_empty(), "id must not be empty");
|
|
assert!(info.created_at > 0, "createdAt must be current timestamp");
|
|
assert_eq!(info.size, 7, "size must be content byte count");
|
|
assert_eq!(info.content_type, PreviewContentType::Markdown);
|
|
assert!(!info.label.is_empty(), "label must not be empty");
|
|
}
|
|
|
|
// SH-2: List snapshots (ordered by createdAt)
|
|
#[tokio::test]
|
|
async fn sh2_list_snapshots_ordered() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let target = make_target(PreviewContentType::Markdown, Some("/a.md"));
|
|
|
|
let s1 = svc.save(&target, "content-1").await.unwrap();
|
|
let s2 = svc.save(&target, "content-2").await.unwrap();
|
|
let s3 = svc.save(&target, "content-3").await.unwrap();
|
|
|
|
let list = svc.list(&target).await.unwrap();
|
|
assert_eq!(list.len(), 3);
|
|
|
|
assert_eq!(list[0].id, s1.id);
|
|
assert_eq!(list[1].id, s2.id);
|
|
assert_eq!(list[2].id, s3.id);
|
|
assert!(list[0].created_at <= list[1].created_at);
|
|
assert!(list[1].created_at <= list[2].created_at);
|
|
}
|
|
|
|
// SH-3: Get snapshot content
|
|
#[tokio::test]
|
|
async fn sh3_get_snapshot_content() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let target = make_target(PreviewContentType::Markdown, Some("/a.md"));
|
|
|
|
let info = svc.save(&target, "# Hello").await.unwrap();
|
|
let resp = svc.get_content(&target, &info.id).await.unwrap();
|
|
|
|
assert!(resp.is_some());
|
|
let resp = resp.unwrap();
|
|
assert_eq!(resp.content, "# Hello");
|
|
assert_eq!(resp.snapshot.id, info.id);
|
|
assert_eq!(resp.snapshot.size, info.size);
|
|
assert_eq!(resp.snapshot.content_type, PreviewContentType::Markdown);
|
|
}
|
|
|
|
// SH-4: Get nonexistent snapshot returns None
|
|
#[tokio::test]
|
|
async fn sh4_get_nonexistent_snapshot() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let target = make_target(PreviewContentType::Markdown, Some("/a.md"));
|
|
|
|
let resp = svc.get_content(&target, "nonexistent").await.unwrap();
|
|
assert!(resp.is_none());
|
|
}
|
|
|
|
// SH-4b: Get nonexistent snapshot from a target with existing snapshots
|
|
#[tokio::test]
|
|
async fn sh4b_get_nonexistent_snapshot_with_existing() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let target = make_target(PreviewContentType::Markdown, Some("/a.md"));
|
|
|
|
svc.save(&target, "some content").await.unwrap();
|
|
let resp = svc.get_content(&target, "does-not-exist").await.unwrap();
|
|
assert!(resp.is_none());
|
|
}
|
|
|
|
// SH-5: Trim snapshots over limit (50)
|
|
#[tokio::test]
|
|
async fn sh5_trim_over_limit() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let target = make_target(PreviewContentType::Code, Some("/c.rs"));
|
|
|
|
let mut first_ids: Vec<String> = Vec::new();
|
|
for i in 0..51 {
|
|
let info = svc.save(&target, &format!("content-{i}")).await.unwrap();
|
|
if i == 0 {
|
|
first_ids.push(info.id.clone());
|
|
}
|
|
}
|
|
|
|
let list = svc.list(&target).await.unwrap();
|
|
assert_eq!(list.len(), 50, "must trim to 50 snapshots");
|
|
|
|
assert!(
|
|
!list.iter().any(|s| s.id == first_ids[0]),
|
|
"oldest snapshot must be removed"
|
|
);
|
|
}
|
|
|
|
// SH-5b: Verify snapshot file is also deleted
|
|
#[tokio::test]
|
|
async fn sh5b_trim_deletes_files() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let target = make_target(PreviewContentType::Code, Some("/c.rs"));
|
|
|
|
let first = svc.save(&target, "first").await.unwrap();
|
|
for i in 1..51 {
|
|
svc.save(&target, &format!("content-{i}")).await.unwrap();
|
|
}
|
|
|
|
let resp = svc.get_content(&target, &first.id).await.unwrap();
|
|
assert!(resp.is_none(), "trimmed snapshot file must not be readable");
|
|
}
|
|
|
|
// SH-6: Different targets are isolated
|
|
#[tokio::test]
|
|
async fn sh6_different_targets_isolated() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let t1 = make_target(PreviewContentType::Markdown, Some("/a.md"));
|
|
let t2 = make_target(PreviewContentType::Markdown, Some("/b.md"));
|
|
|
|
svc.save(&t1, "content-a").await.unwrap();
|
|
svc.save(&t2, "content-b1").await.unwrap();
|
|
svc.save(&t2, "content-b2").await.unwrap();
|
|
|
|
let list1 = svc.list(&t1).await.unwrap();
|
|
let list2 = svc.list(&t2).await.unwrap();
|
|
assert_eq!(list1.len(), 1);
|
|
assert_eq!(list2.len(), 2);
|
|
}
|
|
|
|
// SH-6b: Different content types are isolated
|
|
#[tokio::test]
|
|
async fn sh6b_different_content_types_isolated() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let t1 = make_target(PreviewContentType::Markdown, Some("/a.md"));
|
|
let t2 = make_target(PreviewContentType::Html, Some("/a.md"));
|
|
|
|
svc.save(&t1, "md content").await.unwrap();
|
|
svc.save(&t2, "html content").await.unwrap();
|
|
|
|
let list1 = svc.list(&t1).await.unwrap();
|
|
let list2 = svc.list(&t2).await.unwrap();
|
|
assert_eq!(list1.len(), 1);
|
|
assert_eq!(list2.len(), 1);
|
|
}
|
|
|
|
// SH-7: Target field combination produces different SHA-1 directories
|
|
#[tokio::test]
|
|
async fn sh7_target_field_combination_different_hash() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
|
|
let t1 = make_target(PreviewContentType::Markdown, Some("/a.md"));
|
|
let t2 = make_target_full(PreviewContentType::Markdown, Some("/a.md"), Some("/ws"), Some(1));
|
|
|
|
svc.save(&t1, "content-1").await.unwrap();
|
|
svc.save(&t2, "content-2").await.unwrap();
|
|
|
|
let list1 = svc.list(&t1).await.unwrap();
|
|
let list2 = svc.list(&t2).await.unwrap();
|
|
assert_eq!(list1.len(), 1);
|
|
assert_eq!(list2.len(), 1);
|
|
|
|
let r1 = svc.get_content(&t1, &list1[0].id).await.unwrap().unwrap();
|
|
let r2 = svc.get_content(&t2, &list2[0].id).await.unwrap().unwrap();
|
|
assert_eq!(r1.content, "content-1");
|
|
assert_eq!(r2.content, "content-2");
|
|
}
|
|
|
|
// SH-7b: Verify SHA-1 directory naming by inspecting filesystem
|
|
#[tokio::test]
|
|
async fn sh7b_sha1_directory_naming() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let target = make_target(PreviewContentType::Markdown, Some("/a.md"));
|
|
|
|
svc.save(&target, "test").await.unwrap();
|
|
|
|
let history_dir = tmp.path().join("preview-history");
|
|
let mut entries = std::fs::read_dir(&history_dir).unwrap();
|
|
let dir_entry = entries.next().unwrap().unwrap();
|
|
let dir_name = dir_entry.file_name().to_string_lossy().to_string();
|
|
|
|
assert_eq!(dir_name.len(), 40, "SHA-1 hex must be 40 characters");
|
|
assert!(
|
|
dir_name.chars().all(|c| c.is_ascii_hexdigit()),
|
|
"directory name must be hex"
|
|
);
|
|
|
|
let index = std::fs::read_to_string(dir_entry.path().join("index.json")).unwrap();
|
|
let snapshots: Vec<PreviewSnapshotInfoDto> = serde_json::from_str(&index).unwrap();
|
|
assert_eq!(snapshots.len(), 1);
|
|
}
|
|
|
|
// Extra: List on empty directory returns empty vec
|
|
#[tokio::test]
|
|
async fn list_empty_returns_empty_vec() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let target = make_target(PreviewContentType::Pdf, Some("/doc.pdf"));
|
|
|
|
let list = svc.list(&target).await.unwrap();
|
|
assert!(list.is_empty());
|
|
}
|
|
|
|
// Extra: Save preserves file_name and file_path from target
|
|
#[tokio::test]
|
|
async fn save_preserves_target_metadata() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let svc = SnapshotService::new(tmp.path());
|
|
let target = PreviewHistoryTargetDto {
|
|
content_type: PreviewContentType::Word,
|
|
file_path: Some("/docs/report.docx".into()),
|
|
workspace: None,
|
|
file_name: Some("report.docx".into()),
|
|
title: None,
|
|
language: None,
|
|
conversation_id: None,
|
|
};
|
|
|
|
let info = svc.save(&target, "word content").await.unwrap();
|
|
assert_eq!(info.file_path.as_deref(), Some("/docs/report.docx"));
|
|
assert_eq!(info.file_name.as_deref(), Some("report.docx"));
|
|
}
|