Files
IdeA/crates/infrastructure/tests/template_store.rs
Blomios 307ae71857 feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
2026-06-06 01:27:01 +02:00

140 lines
4.4 KiB
Rust

//! 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/<id>.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<dyn FileSystem> = 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/<id>.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<u8>;
}
impl ChildRead for LocalFileSystem {
async fn child_read(&self, tmp: &TempDir, rel: &str) -> Vec<u8> {
self.read(&tmp.child(rel)).await.unwrap()
}
}