//! L7 integration tests for [`FsTemplateStore`] against a real temp directory and //! a real [`LocalFileSystem`]: md + `index.json` round-trip, version persistence, //! upsert, delete, tolerant reads, and the on-disk layout (`templates/md/.md`). use std::path::PathBuf; use std::sync::Arc; use domain::ids::{ProfileId, TemplateId}; use domain::markdown::MarkdownDoc; use domain::ports::{FileSystem, RemotePath, StoreError, TemplateStore}; use domain::template::AgentTemplate; use infrastructure::{FsTemplateStore, LocalFileSystem}; use uuid::Uuid; struct TempDir(PathBuf); impl TempDir { fn new() -> Self { let p = std::env::temp_dir().join(format!("idea-l7-tpl-{}", Uuid::new_v4())); std::fs::create_dir_all(&p).unwrap(); Self(p) } fn app_data_dir(&self) -> String { self.0.to_string_lossy().into_owned() } fn child(&self, rel: &str) -> RemotePath { RemotePath::new(self.0.join(rel).to_string_lossy().into_owned()) } } impl Drop for TempDir { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } fn store(tmp: &TempDir) -> FsTemplateStore { let fs: Arc = Arc::new(LocalFileSystem::new()); FsTemplateStore::new(fs, tmp.app_data_dir()) } fn tid(n: u128) -> TemplateId { TemplateId::from_uuid(Uuid::from_u128(n)) } fn pid(n: u128) -> ProfileId { ProfileId::from_uuid(Uuid::from_u128(n)) } fn template(id: TemplateId, name: &str, content: &str) -> AgentTemplate { AgentTemplate::new(id, name, MarkdownDoc::new(content), pid(1)).unwrap() } #[tokio::test] async fn missing_index_lists_empty() { let tmp = TempDir::new(); assert!(store(&tmp).list().await.unwrap().is_empty()); } #[tokio::test] async fn save_then_get_and_list_roundtrip() { let tmp = TempDir::new(); let store = store(&tmp); let t = template(tid(1), "Backend", "# Backend template"); store.save(&t).await.unwrap(); assert_eq!(store.get(tid(1)).await.unwrap(), t); assert_eq!(store.list().await.unwrap(), vec![t.clone()]); // The Markdown actually landed at templates/md/.md. let fs = LocalFileSystem::new(); let bytes = fs .child_read(&tmp, &format!("templates/md/{}.md", tid(1))) .await; assert_eq!(String::from_utf8(bytes).unwrap(), t.content_md.as_str()); } #[tokio::test] async fn save_upserts_and_persists_bumped_version() { let tmp = TempDir::new(); let store = store(&tmp); let t0 = template(tid(1), "Backend", "v1"); store.save(&t0).await.unwrap(); let t1 = t0.with_updated_content(MarkdownDoc::new("v2")); store.save(&t1).await.unwrap(); let back = store.get(tid(1)).await.unwrap(); assert_eq!(back.version.get(), 2, "bumped version persisted"); assert_eq!(back.content_md.as_str(), "v2"); assert_eq!(store.list().await.unwrap().len(), 1, "upsert, not append"); } #[tokio::test] async fn get_unknown_is_not_found() { let tmp = TempDir::new(); assert!(matches!( store(&tmp).get(tid(404)).await.unwrap_err(), StoreError::NotFound )); } #[tokio::test] async fn delete_removes_from_index() { let tmp = TempDir::new(); let store = store(&tmp); store.save(&template(tid(1), "T", "x")).await.unwrap(); store.delete(tid(1)).await.unwrap(); assert!(store.list().await.unwrap().is_empty()); assert!(matches!( store.delete(tid(1)).await.unwrap_err(), StoreError::NotFound )); } #[tokio::test] async fn index_is_camelcase_with_content_hash() { let tmp = TempDir::new(); let store = store(&tmp); store.save(&template(tid(1), "Backend", "hello")).await.unwrap(); let fs = LocalFileSystem::new(); let bytes = fs.read(&tmp.child("templates/index.json")).await.unwrap(); let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); let entry = &json.get("templates").unwrap().as_array().unwrap()[0]; assert_eq!(entry.get("name").and_then(|v| v.as_str()), Some("Backend")); assert!(entry.get("contentHash").is_some(), "camelCase contentHash present"); assert!(entry.get("defaultProfileId").is_some()); assert!(entry.get("content_hash").is_none(), "no snake_case leak"); } /// Tiny read helper so the md-path assertion stays readable. trait ChildRead { async fn child_read(&self, tmp: &TempDir, rel: &str) -> Vec; } impl ChildRead for LocalFileSystem { async fn child_read(&self, tmp: &TempDir, rel: &str) -> Vec { self.read(&tmp.child(rel)).await.unwrap() } }