feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
139
crates/infrastructure/tests/template_store.rs
Normal file
139
crates/infrastructure/tests/template_store.rs
Normal file
@ -0,0 +1,139 @@
|
||||
//! 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user