Skip to main content

nap_core/
repository.rs

1//! Repository — filesystem layout and manifest CRUD.
2//!
3//! A NAP repository is a self-contained directory of entities.
4//! Repository structure:
5//!
6//! ```text
7//! starwars/               ← repository root
8//! ├── repository.yaml     ← repository metadata (name, description, nap config)
9//! ├── character/          ← entity type (has .entity-type marker)
10//! │   ├── .entity-type    ← marker file
11//! │   ├── lukeskywalker.yaml
12//! │   └── darthvader.yaml
13//! ├── location/
14//! │   ├── .entity-type
15//! │   └── tatooine.yaml
16//! └── scene/
17//!     ├── .entity-type
18//!     └── cantina-scene.yaml
19//! ```
20
21use std::path::{Path, PathBuf};
22
23use tracing::{debug, info};
24
25use crate::commit::{Change, Commit};
26use crate::error::NapError;
27use crate::manifest::Manifest;
28use crate::resolver::ResolveConfig;
29use crate::types::EntityType;
30use crate::uri::NapUri;
31use crate::vcs::VcsBackend;
32
33/// Marker filename for entity type directories.
34pub const ENTITY_TYPE_MARKER: &str = ".entity-type";
35
36/// A NAP repository.
37pub struct Repository {
38    /// Filesystem path to the repository root.
39    pub root: PathBuf,
40    /// The repository name (derived from directory name).
41    pub repository: String,
42    /// The VCS backend (Lore).
43    vcs: Box<dyn VcsBackend>,
44}
45
46impl Repository {
47    /// Open an existing NAP repository at the given path.
48    pub fn open(path: &Path, vcs: Box<dyn VcsBackend>) -> Result<Self, NapError> {
49        // Check for repository.yaml or repository.yaml to identify valid repository
50        if !path.join("repository.yaml").exists() && !path.join("repository.yaml").exists() {
51            return Err(NapError::RepositoryNotFound(path.display().to_string()));
52        }
53
54        let repository = path
55            .file_name()
56            .and_then(|n| n.to_str())
57            .unwrap_or("unknown")
58            .to_string();
59
60        debug!(
61            path = %path.display(),
62            repository = %repository,
63            "opened NAP repository"
64        );
65
66        Ok(Self {
67            root: path.to_path_buf(),
68            repository,
69            vcs,
70        })
71    }
72
73    /// Read the resolve configuration from repository.yaml (or repository.yaml) metadata.
74    pub fn read_resolve_config(&self) -> ResolveConfig {
75        // Prefer repository.yaml, fall back to repository.yaml
76        let repo_yaml_path = self.root.join("repository.yaml");
77        let universe_yaml_path = self.root.join("repository.yaml");
78        let config_path = if repo_yaml_path.exists() {
79            repo_yaml_path
80        } else if universe_yaml_path.exists() {
81            universe_yaml_path
82        } else {
83            debug!("no repository.yaml or repository.yaml found, using default ResolveConfig");
84            return ResolveConfig::default();
85        };
86
87        let yaml_content = match std::fs::read_to_string(&config_path) {
88            Ok(content) => content,
89            Err(e) => {
90                debug!(
91                    path = %config_path.display(),
92                    error = %e,
93                    "failed to read config, using default ResolveConfig"
94                );
95                return ResolveConfig::default();
96            }
97        };
98
99        // Parse YAML and extract nap metadata
100        let parsed: serde_yaml::Value = match serde_yaml::from_str(&yaml_content) {
101            Ok(value) => value,
102            Err(e) => {
103                debug!(
104                    path = %config_path.display(),
105                    error = %e,
106                    "failed to parse config, using default ResolveConfig"
107                );
108                return ResolveConfig::default();
109            }
110        };
111
112        // Extract default_branch from nap metadata
113        let default_branch = parsed
114            .get("metadata")
115            .and_then(|metadata| metadata.get("nap"))
116            .and_then(|nap| nap.get("default_branch"))
117            .and_then(|branch| branch.as_str())
118            .map(|s| s.to_string());
119
120        // Auto-create nap metadata if missing
121        if default_branch.is_none() {
122            debug!(
123                path = %config_path.display(),
124                "nap metadata not found, auto-creating with default_branch = 'main'"
125            );
126
127            // Read the existing manifest
128            let mut manifest = match Manifest::from_file(&config_path) {
129                Ok(m) => m,
130                Err(e) => {
131                    debug!(
132                        path = %config_path.display(),
133                        error = %e,
134                        "failed to read manifest, using default ResolveConfig"
135                    );
136                    return ResolveConfig::default();
137                }
138            };
139
140            // Add nap metadata
141            manifest.metadata.insert(
142                "nap".to_string(),
143                serde_yaml::to_value(serde_json::json!({
144                    "default_branch": "main"
145                }))
146                .unwrap(),
147            );
148
149            // Write back to file
150            if let Err(e) = manifest.to_file(&config_path) {
151                debug!(
152                    path = %config_path.display(),
153                    error = %e,
154                    "failed to write nap metadata, using default ResolveConfig"
155                );
156                return ResolveConfig::default();
157            }
158
159            return ResolveConfig {
160                default_branch: Some("main".to_string()),
161            };
162        }
163
164        ResolveConfig { default_branch }
165    }
166
167    /// Initialize a new NAP repository.
168    pub fn init(path: &Path, repository: &str, vcs: Box<dyn VcsBackend>) -> Result<Self, NapError> {
169        let repo_root = path.to_path_buf();
170        if repo_root.join("repository.yaml").exists() || repo_root.join("repository.yaml").exists()
171        {
172            return Err(NapError::RepositoryAlreadyExists(
173                repo_root.display().to_string(),
174            ));
175        }
176
177        info!(
178            path = %repo_root.display(),
179            repository = %repository,
180            "initializing NAP repository"
181        );
182
183        // Create directory structure
184        std::fs::create_dir_all(&repo_root)?;
185
186        // Create repository.yaml with [nap] metadata
187        let mut repo_manifest = Manifest::new(
188            repository,
189            EntityType::new("world"),
190            repository,
191            &format!("{repository} Repository"),
192        );
193
194        // Add nap configuration to metadata
195        repo_manifest.metadata.insert(
196            "nap".to_string(),
197            serde_yaml::to_value(serde_json::json!({
198                "default_branch": "main"
199            }))
200            .unwrap(),
201        );
202
203        repo_manifest.to_file(&repo_root.join("repository.yaml"))?;
204
205        // Initialize VCS
206        vcs.init(&repo_root)?;
207
208        // Initial commit
209        vcs.commit(
210            &repo_root,
211            &format!("Initialize {repository} repository"),
212            "nap-init",
213        )?;
214
215        info!(
216            path = %repo_root.display(),
217            repository = %repository,
218            "NAP repository initialized successfully"
219        );
220
221        Ok(Self {
222            root: repo_root,
223            repository: repository.to_string(),
224            vcs,
225        })
226    }
227
228    /// Get the full filesystem path to an entity's manifest file.
229    pub fn manifest_path(&self, entity_type: &EntityType, entity_id: &str) -> PathBuf {
230        let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
231        self.root.join(uri.manifest_path())
232    }
233
234    /// Read a manifest from the repository.
235    pub fn read_manifest(
236        &self,
237        entity_type: &EntityType,
238        entity_id: &str,
239    ) -> Result<Manifest, NapError> {
240        let path = self.manifest_path(entity_type, entity_id);
241        debug!(
242            path = %path.display(),
243            entity_type = %entity_type,
244            entity_id = %entity_id,
245            "reading manifest"
246        );
247        Manifest::from_file(&path)
248    }
249
250    /// Read a manifest at a specific VCS ref (commit, branch, tag).
251    pub fn read_manifest_at_ref(
252        &self,
253        entity_type: &EntityType,
254        entity_id: &str,
255        reference: &str,
256    ) -> Result<Manifest, NapError> {
257        let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
258        let file_path = uri.manifest_path();
259
260        debug!(
261            file_path = %file_path,
262            reference = %reference,
263            "reading manifest at ref"
264        );
265
266        let content = self
267            .vcs
268            .read_file_at_ref(&self.root, &file_path, Some(reference))?;
269        Manifest::from_yaml(&content)
270    }
271
272    /// Write a manifest to the repository (does NOT commit).
273    pub fn write_manifest(&self, manifest: &Manifest) -> Result<PathBuf, NapError> {
274        let uri: NapUri = manifest.id.parse()?;
275        let entity_type = uri.entity_type.clone();
276
277        // Ensure the entity type directory and marker exist
278        self.ensure_entity_type_dir(&entity_type)?;
279
280        let path = self.root.join(uri.manifest_path());
281
282        debug!(
283            path = %path.display(),
284            manifest_id = %manifest.id,
285            "writing manifest"
286        );
287
288        manifest.to_file(&path)?;
289        Ok(path)
290    }
291
292    /// Ensure an entity type directory exists with its marker file.
293    fn ensure_entity_type_dir(&self, entity_type: &EntityType) -> Result<(), NapError> {
294        let dir = self.root.join(entity_type.directory_name());
295        if !dir.exists() {
296            std::fs::create_dir_all(&dir)?;
297            // Create .entity-type marker file
298            let marker = dir.join(ENTITY_TYPE_MARKER);
299            std::fs::write(&marker, "")?;
300            debug!(
301                entity_type = %entity_type,
302                path = %dir.display(),
303                "created entity type directory with marker"
304            );
305        }
306        Ok(())
307    }
308
309    /// Create a new entity manifest and commit it.
310    pub fn create_entity(
311        &self,
312        entity_type: &EntityType,
313        entity_id: &str,
314        name: &str,
315        author: &str,
316    ) -> Result<(Manifest, String), NapError> {
317        // Ensure entity type directory exists
318        self.ensure_entity_type_dir(entity_type)?;
319
320        let mut manifest = Manifest::new(&self.repository, entity_type.clone(), entity_id, name);
321
322        // Check if entity already exists (idempotency guard)
323        let path = self.manifest_path(entity_type, entity_id);
324        if path.exists() {
325            return Err(NapError::Other(format!(
326                "entity '{entity_id}' of type '{entity_type}' already exists"
327            )));
328        }
329
330        manifest.bump_version();
331
332        // Validate against schema before writing
333        crate::schema::validate_manifest(&manifest)
334            .map_err(|errors| NapError::ManifestValidationError(errors.join("; ")))?;
335
336        // Write the manifest
337        self.write_manifest(&manifest)?;
338
339        // Commit via VCS
340        let commit_message = format!("Create {entity_type} '{name}' ({entity_id})");
341        let commit_hash = self.vcs.commit(&self.root, &commit_message, author)?;
342
343        info!(
344            manifest_id = %manifest.id,
345            commit_hash = %commit_hash,
346            "created entity"
347        );
348
349        Ok((manifest, commit_hash))
350    }
351
352    /// Update an existing manifest and commit the changes.
353    pub fn commit_manifest(
354        &self,
355        manifest: &mut Manifest,
356        message: &str,
357        author: &str,
358        changes: Vec<Change>,
359    ) -> Result<Commit, NapError> {
360        // Validate against schema before writing
361        crate::schema::validate_manifest(manifest)
362            .map_err(|errors| NapError::ManifestValidationError(errors.join("; ")))?;
363
364        // Bump version
365        manifest.bump_version();
366
367        // Write updated manifest.
368        self.write_manifest(manifest)?;
369
370        // Compute manifest hash
371        let manifest_hash = manifest.content_hash()?.as_str().to_string();
372        let parent = Some(self.vcs.head_hash(&self.root)?);
373
374        // VCS commit — produces the new HEAD hash
375        let vcs_hash = self.vcs.commit(&self.root, message, author)?;
376
377        // Create NAP commit metadata with the previous VCS HEAD as parent.
378        let nap_commit = Commit::new(parent, author, message, &manifest_hash, changes);
379
380        debug!(
381            manifest_id = %manifest.id,
382            version = manifest.version,
383            nap_commit_id = %nap_commit.id,
384            vcs_hash = %vcs_hash,
385            "manifest committed"
386        );
387
388        Ok(nap_commit)
389    }
390
391    /// Get the commit history for a specific entity.
392    pub fn history(
393        &self,
394        entity_type: &EntityType,
395        entity_id: &str,
396        limit: usize,
397    ) -> Result<Vec<crate::vcs::CommitInfo>, NapError> {
398        let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
399        let file_path = uri.manifest_path();
400        self.vcs.log(&self.root, Some(&file_path), limit)
401    }
402
403    /// List all entity IDs of a given type in the repository.
404    pub fn list_entities(&self, entity_type: &EntityType) -> Result<Vec<String>, NapError> {
405        let dir = self.root.join(entity_type.directory_name());
406        if !dir.exists() {
407            return Err(NapError::EntityTypeNotFound(entity_type.to_string()));
408        }
409
410        let mut entities = Vec::new();
411        for entry in std::fs::read_dir(&dir)? {
412            let entry = entry?;
413            let path = entry.path();
414            if path.extension().and_then(|e| e.to_str()) == Some("yaml")
415                && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
416            {
417                entities.push(stem.to_string());
418            }
419        }
420        entities.sort();
421        Ok(entities)
422    }
423
424    /// Discover all entity types in this repository.
425    ///
426    /// Scans the repository root for directories containing a `.entity-type`
427    /// marker file OR directories containing at least one `.yaml` file
428    /// (implicit discovery for backward compatibility).
429    pub fn list_entity_types(&self) -> Result<Vec<EntityType>, NapError> {
430        let mut types = Vec::new();
431        for entry in std::fs::read_dir(&self.root)? {
432            let entry = entry?;
433            let path = entry.path();
434            if !path.is_dir() {
435                continue;
436            }
437            // Skip hidden directories (.nap, etc.)
438            let dir_name = match path.file_name().and_then(|n| n.to_str()) {
439                Some(name) => name,
440                None => continue,
441            };
442            if dir_name.starts_with('.') || dir_name == "target" {
443                continue;
444            }
445
446            // Check for .entity-type marker file (explicit)
447            if path.join(ENTITY_TYPE_MARKER).exists() {
448                types.push(EntityType::new(dir_name));
449                continue;
450            }
451
452            // Implicit: directory contains at least one .yaml file
453            let has_yaml = std::fs::read_dir(&path)
454                .ok()
455                .and_then(|entries| {
456                    entries
457                        .filter_map(|e| e.ok())
458                        .find(|e| e.path().extension().and_then(|ext| ext.to_str()) == Some("yaml"))
459                        .map(|_| true)
460                })
461                .unwrap_or(false);
462
463            if has_yaml {
464                types.push(EntityType::new(dir_name));
465            }
466        }
467        types.sort_by(|a, b| a.as_str().cmp(b.as_str()));
468        Ok(types)
469    }
470
471    /// Delete an entity manifest and commit the deletion.
472    pub fn delete_entity(
473        &self,
474        entity_type: &EntityType,
475        entity_id: &str,
476        author: &str,
477    ) -> Result<String, NapError> {
478        let path = self.manifest_path(entity_type, entity_id);
479        if !path.exists() {
480            return Err(NapError::ManifestNotFound(path.display().to_string()));
481        }
482
483        std::fs::remove_file(&path)?;
484
485        let message = format!("Delete {entity_type} '{entity_id}'");
486        let hash = self.vcs.commit(&self.root, &message, author)?;
487        info!(entity_type = %entity_type, entity_id = %entity_id, "deleted entity");
488        Ok(hash)
489    }
490
491    /// Create a branch in the underlying VCS.
492    pub fn create_branch(&self, name: &str) -> Result<(), NapError> {
493        self.vcs.create_branch(&self.root, name)
494    }
495
496    /// Switch to a branch.
497    pub fn switch_branch(&self, name: &str) -> Result<(), NapError> {
498        self.vcs.switch_branch(&self.root, name)
499    }
500
501    /// List branches.
502    pub fn list_branches(&self) -> Result<Vec<String>, NapError> {
503        self.vcs.list_branches(&self.root)
504    }
505
506    /// Revert a commit by creating a new VCS commit that undoes the specified one.
507    pub fn revert_commit(&self, commit_hash: &str, author: &str) -> Result<String, NapError> {
508        let new_hash = self.vcs.revert(&self.root, commit_hash)?;
509
510        info!(
511            commit = %commit_hash,
512            revert = %new_hash,
513            author = %author,
514            "commit reverted"
515        );
516
517        Ok(new_hash)
518    }
519
520    /// Get current HEAD hash.
521    pub fn head_hash(&self) -> Result<String, NapError> {
522        self.vcs.head_hash(&self.root)
523    }
524
525    /// Resolve the most recent commit hash on a given branch.
526    pub fn resolve_branch_head(&self, branch: &str) -> Result<String, NapError> {
527        self.vcs.resolve_branch_head(&self.root, branch)
528    }
529
530    // ── Remote operations ─────────────────────────────────────────
531
532    /// Add a remote to the repository.
533    pub fn add_remote(&self, name: &str, url: &str) -> Result<(), NapError> {
534        self.vcs.add_remote(&self.root, name, url)
535    }
536
537    /// Remove a remote from the repository.
538    pub fn remove_remote(&self, name: &str) -> Result<(), NapError> {
539        self.vcs.remove_remote(&self.root, name)
540    }
541
542    /// List remotes as `(name, url)` pairs.
543    pub fn list_remotes(&self) -> Result<Vec<(String, String)>, NapError> {
544        self.vcs.list_remotes(&self.root)
545    }
546
547    /// Push the current branch to a remote.
548    pub fn push(&self, remote: Option<&str>, branch: Option<&str>) -> Result<(), NapError> {
549        self.vcs.push(&self.root, remote, branch)
550    }
551
552    /// Pull the current branch from a remote.
553    pub fn pull(&self, remote: Option<&str>, branch: Option<&str>) -> Result<(), NapError> {
554        self.vcs.pull(&self.root, remote, branch)
555    }
556
557    /// Access the VCS backend (for the resolver to read files at specific refs).
558    pub fn vcs(&self) -> &dyn VcsBackend {
559        self.vcs.as_ref()
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use crate::test_utils::{MockBackend, mock_repo};
567    use tempfile::TempDir;
568
569    #[test]
570    fn test_mock_backend_contract() {
571        crate::test_utils::contract::run_repository_contract(MockBackend::new());
572    }
573
574    #[test]
575    fn test_init_creates_structure() {
576        let tmp = TempDir::new().unwrap();
577        let repo = mock_repo(&tmp);
578
579        assert!(repo.root.join("repository.yaml").exists());
580    }
581
582    #[test]
583    fn test_create_and_read_entity() {
584        let tmp = TempDir::new().unwrap();
585        let repo = mock_repo(&tmp);
586
587        let (manifest, hash) = repo
588            .create_entity(
589                &EntityType::new("character"),
590                "hero",
591                "The Hero",
592                "test-author",
593            )
594            .unwrap();
595
596        assert_eq!(manifest.name, "The Hero");
597        assert_eq!(manifest.entity_type.as_str(), "character");
598        assert_eq!(manifest.version, 1);
599        assert_eq!(repo.head_hash().unwrap(), hash);
600
601        // Read it back
602        let read_back = repo
603            .read_manifest(&EntityType::new("character"), "hero")
604            .unwrap();
605        assert_eq!(read_back.name, "The Hero");
606        assert_eq!(read_back.version, 1);
607    }
608
609    #[test]
610    fn test_create_entity_auto_creates_type_directory() {
611        let tmp = TempDir::new().unwrap();
612        let repo = mock_repo(&tmp);
613
614        // Create entity of a custom type
615        repo.create_entity(&EntityType::new("pokemon"), "pikachu", "Pikachu", "test")
616            .unwrap();
617
618        // Verify the type directory and marker exist
619        assert!(repo.root.join("pokemon").exists());
620        assert!(repo.root.join("pokemon").join(".entity-type").exists());
621        assert!(repo.root.join("pokemon/pikachu.yaml").exists());
622    }
623
624    #[test]
625    fn test_list_entity_types() {
626        let tmp = TempDir::new().unwrap();
627        let repo = mock_repo(&tmp);
628
629        // Create entities of different types
630        repo.create_entity(&EntityType::new("character"), "hero", "Hero", "author")
631            .unwrap();
632        repo.create_entity(&EntityType::new("location"), "village", "Village", "author")
633            .unwrap();
634        repo.create_entity(&EntityType::new("pokemon"), "pikachu", "Pikachu", "author")
635            .unwrap();
636
637        let types = repo.list_entity_types().unwrap();
638        assert!(types.contains(&EntityType::new("character")));
639        assert!(types.contains(&EntityType::new("location")));
640        assert!(types.contains(&EntityType::new("pokemon")));
641    }
642
643    #[test]
644    fn test_list_entities() {
645        let tmp = TempDir::new().unwrap();
646        let repo = mock_repo(&tmp);
647
648        repo.create_entity(&EntityType::new("character"), "alice", "Alice", "author")
649            .unwrap();
650        repo.create_entity(&EntityType::new("character"), "bob", "Bob", "author")
651            .unwrap();
652
653        let chars = repo.list_entities(&EntityType::new("character")).unwrap();
654        assert_eq!(chars, vec!["alice", "bob"]);
655    }
656
657    #[test]
658    fn test_commit_manifest_updates() {
659        let tmp = TempDir::new().unwrap();
660        let repo = mock_repo(&tmp);
661
662        let (mut manifest, create_hash) = repo
663            .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
664            .unwrap();
665
666        // Modify and commit
667        manifest.set_property("species", serde_yaml::Value::String("elf".to_string()));
668        let changes = vec![Change::set("properties.species", None, "elf".to_string())];
669        let commit = repo
670            .commit_manifest(&mut manifest, "set species to elf", "author", changes)
671            .unwrap();
672
673        assert!(!commit.id.is_empty());
674        assert_eq!(commit.message, "set species to elf");
675        assert_eq!(commit.parent.as_deref(), Some(create_hash.as_str()));
676
677        // Verify version incremented
678        let read_back = repo
679            .read_manifest(&EntityType::new("character"), "hero")
680            .unwrap();
681        assert!(read_back.version >= 2);
682    }
683
684    #[test]
685    fn test_history() {
686        let tmp = TempDir::new().unwrap();
687        let repo = mock_repo(&tmp);
688
689        let (mut manifest, _) = repo
690            .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
691            .unwrap();
692
693        manifest.set_property(
694            "name",
695            serde_yaml::Value::String("Updated Hero".to_string()),
696        );
697        repo.commit_manifest(&mut manifest, "update name", "author", vec![])
698            .unwrap();
699
700        let hist = repo
701            .history(&EntityType::new("character"), "hero", 10)
702            .unwrap();
703        assert!(hist.len() >= 2);
704    }
705
706    #[test]
707    fn test_revert_commit() {
708        let tmp = TempDir::new().unwrap();
709        let repo = mock_repo(&tmp);
710
711        // Create entity and note its name
712        let (mut manifest, _) = repo
713            .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
714            .unwrap();
715        assert_eq!(manifest.name, "The Hero");
716
717        // Modify and commit
718        manifest.set_property("species", serde_yaml::Value::String("elf".to_string()));
719        let changes = vec![Change::set("properties.species", None, "elf".to_string())];
720        repo.commit_manifest(&mut manifest, "set species to elf", "author", changes)
721            .unwrap();
722        let update_hash = repo.head_hash().unwrap();
723
724        // Revert the VCS commit.
725        let revert_hash = repo.revert_commit(&update_hash, "author").unwrap();
726        assert!(!revert_hash.is_empty());
727
728        // Verify the manifest remains normal YAML without a cached head field.
729        let manifest_path = repo.manifest_path(&EntityType::new("character"), "hero");
730        let manifest_yaml = std::fs::read_to_string(&manifest_path).unwrap();
731        assert!(!manifest_yaml.contains("\nhead:"));
732
733        // Verify the revert appears in history
734        let hist = repo
735            .history(&EntityType::new("character"), "hero", 10)
736            .unwrap();
737        assert!(hist.iter().any(|c| c.id == revert_hash));
738    }
739}
740
741// ── Integration tests: Repository + LoreBackend ─────────────────────
742// These require a running Lore server. Run with:
743//   cargo test --features lore-integration
744#[cfg(all(test, feature = "lore-integration"))]
745mod lore_integration_tests {
746    use super::*;
747    use crate::vcs_lore::LoreBackend;
748    use std::time::{SystemTime, UNIX_EPOCH};
749    use tempfile::TempDir;
750
751    fn unique_suffix() -> u64 {
752        SystemTime::now()
753            .duration_since(UNIX_EPOCH)
754            .unwrap()
755            .as_nanos() as u64
756    }
757
758    fn setup_lore_repo() -> (TempDir, Repository) {
759        let repository = format!("ri-{}", unique_suffix());
760        let tmp = TempDir::new().unwrap();
761        let repo_path = tmp.path().join(&repository);
762        let repo =
763            Repository::init(&repo_path, &repository, Box::new(LoreBackend::from_env())).unwrap();
764        (tmp, repo)
765    }
766
767    #[test]
768    fn test_lore_init_creates_structure() {
769        let (_tmp, repo) = setup_lore_repo();
770        assert!(repo.root.join(".nap").exists());
771        assert!(repo.root.join("repository.yaml").exists());
772    }
773
774    #[test]
775    fn test_lore_create_and_read_entity() {
776        let (_tmp, repo) = setup_lore_repo();
777
778        let (manifest, _hash) = repo
779            .create_entity(
780                &EntityType::new("character"),
781                "hero",
782                "Test Hero",
783                "integration-test",
784            )
785            .unwrap();
786        assert_eq!(manifest.name, "Test Hero");
787
788        let read_back = repo
789            .read_manifest(&EntityType::new("character"), "hero")
790            .unwrap();
791        assert_eq!(read_back.name, "Test Hero");
792    }
793
794    #[test]
795    fn test_lore_commit_and_branch() {
796        let (_tmp, repo) = setup_lore_repo();
797
798        let (mut manifest, _) = repo
799            .create_entity(
800                &EntityType::new("character"),
801                "hero",
802                "Test Hero",
803                "integration-test",
804            )
805            .unwrap();
806
807        manifest.set_property("species", serde_yaml::Value::String("human".to_string()));
808        let changes = vec![Change::set("properties.species", None, "human".to_string())];
809        repo.commit_manifest(&mut manifest, "add species", "integration-test", changes)
810            .unwrap();
811
812        let read_back = repo
813            .read_manifest(&EntityType::new("character"), "hero")
814            .unwrap();
815        assert_eq!(read_back.version, 2);
816
817        repo.create_branch("feature-branch").unwrap();
818        let branches = repo.list_branches().unwrap();
819        assert!(branches.contains(&"feature-branch".to_string()));
820    }
821
822    #[test]
823    fn test_lore_delete_entity() {
824        let (_tmp, repo) = setup_lore_repo();
825
826        repo.create_entity(
827            &EntityType::new("character"),
828            "hero",
829            "Test Hero",
830            "integration-test",
831        )
832        .unwrap();
833
834        repo.delete_entity(&EntityType::new("character"), "hero", "integration-test")
835            .unwrap();
836
837        let entities = repo.list_entities(&EntityType::new("character")).unwrap();
838        assert!(!entities.contains(&"hero".to_string()));
839    }
840
841    #[test]
842    fn test_lore_history() {
843        let (_tmp, repo) = setup_lore_repo();
844
845        let (mut manifest, _) = repo
846            .create_entity(
847                &EntityType::new("character"),
848                "hero",
849                "Test Hero",
850                "integration-test",
851            )
852            .unwrap();
853
854        manifest.set_property(
855            "name",
856            serde_yaml::Value::String("Updated Hero".to_string()),
857        );
858        repo.commit_manifest(&mut manifest, "update name", "integration-test", vec![])
859            .unwrap();
860
861        let hist = repo
862            .history(&EntityType::new("character"), "hero", 10)
863            .unwrap();
864        assert!(hist.len() >= 2);
865    }
866}