//! L6 integration tests for [`IdeaiContextStore`] against a real temp directory //! and a real [`LocalFileSystem`], exercising the full `.ideai/` persistence path //! (manifest JSON, context `.md` round-trip, tolerant reads, NotFound). use std::path::PathBuf; use std::sync::Arc; use domain::agent::{Agent, AgentManifest, AgentOrigin, ManifestEntry}; use domain::ids::{AgentId, ProfileId}; use domain::markdown::MarkdownDoc; use domain::ports::{AgentContextStore, FileSystem, RemotePath, StoreError}; use domain::project::{Project, ProjectPath}; use domain::remote::RemoteRef; use infrastructure::{IdeaiContextStore, LocalFileSystem}; use uuid::Uuid; /// A unique scratch directory under the OS temp dir, cleaned up on drop. struct TempDir(PathBuf); impl TempDir { fn new() -> Self { let p = std::env::temp_dir().join(format!("idea-l6-ctx-{}", Uuid::new_v4())); std::fs::create_dir_all(&p).unwrap(); Self(p) } fn root(&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() -> IdeaiContextStore { let fs: Arc = Arc::new(LocalFileSystem::new()); IdeaiContextStore::new(fs) } fn project(root: &str) -> Project { Project::new( domain::ids::ProjectId::new_random(), "demo", ProjectPath::new(root).unwrap(), RemoteRef::local(), 1_700_000_000_000, ) .unwrap() } fn aid(n: u128) -> AgentId { AgentId::from_uuid(Uuid::from_u128(n)) } fn pid(n: u128) -> ProfileId { ProfileId::from_uuid(Uuid::from_u128(n)) } fn agent(id: AgentId, name: &str, md: &str, profile: ProfileId) -> Agent { Agent::new(id, name, md, profile, AgentOrigin::Scratch, false).unwrap() } #[tokio::test] async fn missing_manifest_loads_empty() { let tmp = TempDir::new(); let store = store(); let manifest = store.load_manifest(&project(&tmp.root())).await.unwrap(); assert!(manifest.entries.is_empty()); assert_eq!(manifest.version, 1); } #[tokio::test] async fn manifest_save_then_load_roundtrips() { let tmp = TempDir::new(); let store = store(); let p = project(&tmp.root()); let a = agent(aid(1), "Backend", "agents/backend.md", pid(9)); let manifest = AgentManifest::new(1, vec![ManifestEntry::from_agent(&a)]).unwrap(); store.save_manifest(&p, &manifest).await.unwrap(); let back = store.load_manifest(&p).await.unwrap(); assert_eq!(back, manifest); } #[tokio::test] async fn context_write_then_read_roundtrips() { let tmp = TempDir::new(); let store = store(); let p = project(&tmp.root()); // The manifest must know the agent before its context can be addressed. let a = agent(aid(1), "Backend", "agents/backend.md", pid(9)); let manifest = AgentManifest::new(1, vec![ManifestEntry::from_agent(&a)]).unwrap(); store.save_manifest(&p, &manifest).await.unwrap(); let md = MarkdownDoc::new("# Backend\nYou are the backend agent."); store.write_context(&p, &a.id, &md).await.unwrap(); let back = store.read_context(&p, &a.id).await.unwrap(); assert_eq!(back, md); // The `.md` actually landed at `.ideai/agents/backend.md`. let fs = LocalFileSystem::new(); let bytes = fs .read(&tmp.child(".ideai/agents/backend.md")) .await .unwrap(); assert_eq!(String::from_utf8(bytes).unwrap(), md.as_str()); } #[tokio::test] async fn read_context_for_unknown_agent_is_not_found() { let tmp = TempDir::new(); let store = store(); let p = project(&tmp.root()); let err = store.read_context(&p, &aid(404)).await.unwrap_err(); assert!(matches!(err, StoreError::NotFound), "got {err:?}"); } #[tokio::test] async fn manifest_file_is_camelcase_json_under_ideai() { let tmp = TempDir::new(); let store = store(); let p = project(&tmp.root()); let a = agent(aid(1), "Backend", "agents/backend.md", pid(9)); let manifest = AgentManifest::new(1, vec![ManifestEntry::from_agent(&a)]).unwrap(); store.save_manifest(&p, &manifest).await.unwrap(); let fs = LocalFileSystem::new(); let bytes = fs.read(&tmp.child(".ideai/agents.json")).await.unwrap(); let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); let agents = json .get("agents") .and_then(|v| v.as_array()) .expect("top-level `agents` array"); assert_eq!(agents.len(), 1); let entry = &agents[0]; assert_eq!(entry.get("mdPath").and_then(|v| v.as_str()), Some("agents/backend.md")); assert_eq!(entry.get("name").and_then(|v| v.as_str()), Some("Backend")); assert!(entry.get("profileId").is_some(), "camelCase profileId present"); assert!(entry.get("md_path").is_none(), "no snake_case leak"); }