1use 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
33pub const ENTITY_TYPE_MARKER: &str = ".entity-type";
35
36pub const UNVERSIONED_COMMIT: &str = "unversioned";
39
40pub struct Repository {
42 pub root: PathBuf,
44 pub repository: String,
46 vcs: Option<Box<dyn VcsBackend>>,
50}
51
52impl Repository {
53 pub fn open(path: &Path, vcs: Box<dyn VcsBackend>) -> Result<Self, NapError> {
55 Self::open_optional(path, Some(vcs))
56 }
57
58 pub fn open_optional(path: &Path, vcs: Option<Box<dyn VcsBackend>>) -> Result<Self, NapError> {
62 if !path.join("repository.yaml").exists() && !path.join("repository.yaml").exists() {
64 return Err(NapError::RepositoryNotFound(path.display().to_string()));
65 }
66
67 let repository = path
68 .file_name()
69 .and_then(|n| n.to_str())
70 .unwrap_or("unknown")
71 .to_string();
72
73 debug!(
74 path = %path.display(),
75 repository = %repository,
76 "opened NAP repository"
77 );
78
79 Ok(Self {
80 root: path.to_path_buf(),
81 repository,
82 vcs,
83 })
84 }
85
86 pub fn read_resolve_config(&self) -> ResolveConfig {
88 let repo_yaml_path = self.root.join("repository.yaml");
90 let universe_yaml_path = self.root.join("repository.yaml");
91 let config_path = if repo_yaml_path.exists() {
92 repo_yaml_path
93 } else if universe_yaml_path.exists() {
94 universe_yaml_path
95 } else {
96 debug!("no repository.yaml or repository.yaml found, using default ResolveConfig");
97 return ResolveConfig::default();
98 };
99
100 let yaml_content = match std::fs::read_to_string(&config_path) {
101 Ok(content) => content,
102 Err(e) => {
103 debug!(
104 path = %config_path.display(),
105 error = %e,
106 "failed to read config, using default ResolveConfig"
107 );
108 return ResolveConfig::default();
109 }
110 };
111
112 let parsed: serde_yaml::Value = match serde_yaml::from_str(&yaml_content) {
114 Ok(value) => value,
115 Err(e) => {
116 debug!(
117 path = %config_path.display(),
118 error = %e,
119 "failed to parse config, using default ResolveConfig"
120 );
121 return ResolveConfig::default();
122 }
123 };
124
125 let default_branch = parsed
127 .get("metadata")
128 .and_then(|metadata| metadata.get("nap"))
129 .and_then(|nap| nap.get("default_branch"))
130 .and_then(|branch| branch.as_str())
131 .map(|s| s.to_string());
132
133 if default_branch.is_none() {
135 debug!(
136 path = %config_path.display(),
137 "nap metadata not found, auto-creating with default_branch = 'main'"
138 );
139
140 let mut manifest = match Manifest::from_file(&config_path) {
142 Ok(m) => m,
143 Err(e) => {
144 debug!(
145 path = %config_path.display(),
146 error = %e,
147 "failed to read manifest, using default ResolveConfig"
148 );
149 return ResolveConfig::default();
150 }
151 };
152
153 manifest.metadata.insert(
155 "nap".to_string(),
156 serde_yaml::to_value(serde_json::json!({
157 "default_branch": "main"
158 }))
159 .unwrap(),
160 );
161
162 if let Err(e) = manifest.to_file(&config_path) {
164 debug!(
165 path = %config_path.display(),
166 error = %e,
167 "failed to write nap metadata, using default ResolveConfig"
168 );
169 return ResolveConfig::default();
170 }
171
172 return ResolveConfig {
173 default_branch: Some("main".to_string()),
174 };
175 }
176
177 ResolveConfig { default_branch }
178 }
179
180 pub fn init(path: &Path, repository: &str, vcs: Box<dyn VcsBackend>) -> Result<Self, NapError> {
182 Self::init_optional(path, repository, Some(vcs))
183 }
184
185 pub fn init_optional(
191 path: &Path,
192 repository: &str,
193 vcs: Option<Box<dyn VcsBackend>>,
194 ) -> Result<Self, NapError> {
195 let repo_root = path.to_path_buf();
196 if repo_root.join("repository.yaml").exists() || repo_root.join("repository.yaml").exists()
197 {
198 return Err(NapError::RepositoryAlreadyExists(
199 repo_root.display().to_string(),
200 ));
201 }
202
203 info!(
204 path = %repo_root.display(),
205 repository = %repository,
206 "initializing NAP repository"
207 );
208
209 std::fs::create_dir_all(&repo_root)?;
211
212 let mut repo_manifest = Manifest::new(
214 repository,
215 EntityType::new("world"),
216 repository,
217 &format!("{repository} Repository"),
218 );
219
220 repo_manifest.metadata.insert(
222 "nap".to_string(),
223 serde_yaml::to_value(serde_json::json!({
224 "default_branch": "main"
225 }))
226 .unwrap(),
227 );
228
229 repo_manifest.to_file(&repo_root.join("repository.yaml"))?;
230
231 if let Some(vcs) = vcs.as_ref() {
233 vcs.init(&repo_root)?;
234 vcs.commit(
235 &repo_root,
236 &format!("Initialize {repository} repository"),
237 "nap-init",
238 )?;
239 }
240
241 info!(
242 path = %repo_root.display(),
243 repository = %repository,
244 "NAP repository initialized successfully"
245 );
246
247 Ok(Self {
248 root: repo_root,
249 repository: repository.to_string(),
250 vcs,
251 })
252 }
253
254 pub fn bootstrap_vcs(&self, message: &str, author: &str) -> Result<String, NapError> {
263 let vcs = self.require_vcs("bootstrap the repository into version control")?;
264 vcs.init(&self.root)?;
265 let hash = vcs.commit(&self.root, message, author)?;
266 info!(
267 repository = %self.repository,
268 commit_hash = %hash,
269 "bootstrapped repository into version control"
270 );
271 Ok(hash)
272 }
273
274 fn require_vcs(&self, operation: &str) -> Result<&dyn VcsBackend, NapError> {
275 self.vcs
276 .as_deref()
277 .ok_or_else(|| NapError::BackendNotConfigured {
278 operation: operation.to_string(),
279 })
280 }
281
282 pub fn manifest_path(&self, entity_type: &EntityType, entity_id: &str) -> PathBuf {
284 let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
285 self.root.join(uri.manifest_path())
286 }
287
288 pub fn read_manifest(
290 &self,
291 entity_type: &EntityType,
292 entity_id: &str,
293 ) -> Result<Manifest, NapError> {
294 let path = self.manifest_path(entity_type, entity_id);
295 debug!(
296 path = %path.display(),
297 entity_type = %entity_type,
298 entity_id = %entity_id,
299 "reading manifest"
300 );
301 Manifest::from_file(&path)
302 }
303
304 pub fn read_manifest_at_ref(
306 &self,
307 entity_type: &EntityType,
308 entity_id: &str,
309 reference: &str,
310 ) -> Result<Manifest, NapError> {
311 let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
312 let file_path = uri.manifest_path();
313
314 debug!(
315 file_path = %file_path,
316 reference = %reference,
317 "reading manifest at ref"
318 );
319
320 let content = self.require_vcs("read manifest at ref")?.read_file_at_ref(
321 &self.root,
322 &file_path,
323 Some(reference),
324 )?;
325 Manifest::from_yaml(&content)
326 }
327
328 pub fn write_manifest(&self, manifest: &Manifest) -> Result<PathBuf, NapError> {
330 let uri: NapUri = manifest.id.parse()?;
331 let entity_type = uri.entity_type.clone();
332
333 self.ensure_entity_type_dir(&entity_type)?;
335
336 let path = self.root.join(uri.manifest_path());
337
338 debug!(
339 path = %path.display(),
340 manifest_id = %manifest.id,
341 "writing manifest"
342 );
343
344 manifest.to_file(&path)?;
345 Ok(path)
346 }
347
348 fn ensure_entity_type_dir(&self, entity_type: &EntityType) -> Result<(), NapError> {
350 let dir = self.root.join(entity_type.directory_name());
351 if !dir.exists() {
352 std::fs::create_dir_all(&dir)?;
353 let marker = dir.join(ENTITY_TYPE_MARKER);
355 std::fs::write(&marker, "")?;
356 debug!(
357 entity_type = %entity_type,
358 path = %dir.display(),
359 "created entity type directory with marker"
360 );
361 }
362 Ok(())
363 }
364
365 pub fn create_entity(
367 &self,
368 entity_type: &EntityType,
369 entity_id: &str,
370 name: &str,
371 author: &str,
372 ) -> Result<(Manifest, String), NapError> {
373 self.ensure_entity_type_dir(entity_type)?;
375
376 let mut manifest = Manifest::new(&self.repository, entity_type.clone(), entity_id, name);
377
378 let path = self.manifest_path(entity_type, entity_id);
380 if path.exists() {
381 return Err(NapError::Other(format!(
382 "entity '{entity_id}' of type '{entity_type}' already exists"
383 )));
384 }
385
386 manifest.bump_version();
387
388 crate::schema::validate_manifest(&manifest)
390 .map_err(|errors| NapError::ManifestValidationError(errors.join("; ")))?;
391
392 self.write_manifest(&manifest)?;
394
395 let commit_hash = match self.vcs.as_ref() {
398 Some(vcs) => {
399 let commit_message = format!("Create {entity_type} '{name}' ({entity_id})");
400 vcs.commit(&self.root, &commit_message, author)?
401 }
402 None => UNVERSIONED_COMMIT.to_string(),
403 };
404
405 info!(
406 manifest_id = %manifest.id,
407 commit_hash = %commit_hash,
408 "created entity"
409 );
410
411 Ok((manifest, commit_hash))
412 }
413
414 pub fn commit_manifest(
416 &self,
417 manifest: &mut Manifest,
418 message: &str,
419 author: &str,
420 changes: Vec<Change>,
421 ) -> Result<Commit, NapError> {
422 crate::schema::validate_manifest(manifest)
424 .map_err(|errors| NapError::ManifestValidationError(errors.join("; ")))?;
425
426 manifest.bump_version();
428
429 self.write_manifest(manifest)?;
431
432 let manifest_hash = manifest.content_hash()?.as_str().to_string();
434
435 let (parent, vcs_hash) = match self.vcs.as_ref() {
439 Some(vcs) => (
440 Some(vcs.head_hash(&self.root)?),
441 vcs.commit(&self.root, message, author)?,
442 ),
443 None => (None, UNVERSIONED_COMMIT.to_string()),
444 };
445
446 let nap_commit = Commit::new(parent, author, message, &manifest_hash, changes);
448
449 debug!(
450 manifest_id = %manifest.id,
451 version = manifest.version,
452 nap_commit_id = %nap_commit.id,
453 vcs_hash = %vcs_hash,
454 "manifest committed"
455 );
456
457 Ok(nap_commit)
458 }
459
460 pub fn history(
462 &self,
463 entity_type: &EntityType,
464 entity_id: &str,
465 limit: usize,
466 ) -> Result<Vec<crate::vcs::CommitInfo>, NapError> {
467 let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
468 let file_path = uri.manifest_path();
469 self.require_vcs("view history")?
470 .log(&self.root, Some(&file_path), limit)
471 }
472
473 pub fn list_entities(&self, entity_type: &EntityType) -> Result<Vec<String>, NapError> {
475 let dir = self.root.join(entity_type.directory_name());
476 if !dir.exists() {
477 return Err(NapError::EntityTypeNotFound(entity_type.to_string()));
478 }
479
480 let mut entities = Vec::new();
481 for entry in std::fs::read_dir(&dir)? {
482 let entry = entry?;
483 let path = entry.path();
484 if path.extension().and_then(|e| e.to_str()) == Some("yaml")
485 && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
486 {
487 entities.push(stem.to_string());
488 }
489 }
490 entities.sort();
491 Ok(entities)
492 }
493
494 pub fn list_entity_types(&self) -> Result<Vec<EntityType>, NapError> {
500 let mut types = Vec::new();
501 for entry in std::fs::read_dir(&self.root)? {
502 let entry = entry?;
503 let path = entry.path();
504 if !path.is_dir() {
505 continue;
506 }
507 let dir_name = match path.file_name().and_then(|n| n.to_str()) {
509 Some(name) => name,
510 None => continue,
511 };
512 if dir_name.starts_with('.') || dir_name == "target" {
513 continue;
514 }
515
516 if path.join(ENTITY_TYPE_MARKER).exists() {
518 types.push(EntityType::new(dir_name));
519 continue;
520 }
521
522 let has_yaml = std::fs::read_dir(&path)
524 .ok()
525 .and_then(|entries| {
526 entries
527 .filter_map(|e| e.ok())
528 .find(|e| e.path().extension().and_then(|ext| ext.to_str()) == Some("yaml"))
529 .map(|_| true)
530 })
531 .unwrap_or(false);
532
533 if has_yaml {
534 types.push(EntityType::new(dir_name));
535 }
536 }
537 types.sort_by(|a, b| a.as_str().cmp(b.as_str()));
538 Ok(types)
539 }
540
541 pub fn delete_entity(
543 &self,
544 entity_type: &EntityType,
545 entity_id: &str,
546 author: &str,
547 ) -> Result<String, NapError> {
548 let path = self.manifest_path(entity_type, entity_id);
549 if !path.exists() {
550 return Err(NapError::ManifestNotFound(path.display().to_string()));
551 }
552
553 std::fs::remove_file(&path)?;
554
555 let message = format!("Delete {entity_type} '{entity_id}'");
556 let hash = match self.vcs.as_ref() {
557 Some(vcs) => vcs.commit(&self.root, &message, author)?,
558 None => UNVERSIONED_COMMIT.to_string(),
559 };
560 info!(entity_type = %entity_type, entity_id = %entity_id, "deleted entity");
561 Ok(hash)
562 }
563
564 pub fn create_branch(&self, name: &str) -> Result<(), NapError> {
566 self.require_vcs("create branch")?
567 .create_branch(&self.root, name)
568 }
569
570 pub fn switch_branch(&self, name: &str) -> Result<(), NapError> {
572 self.require_vcs("switch branch")?
573 .switch_branch(&self.root, name)
574 }
575
576 pub fn list_branches(&self) -> Result<Vec<String>, NapError> {
578 self.require_vcs("list branches")?.list_branches(&self.root)
579 }
580
581 pub fn revert_commit(&self, commit_hash: &str, author: &str) -> Result<String, NapError> {
583 let vcs = self.require_vcs("revert commit")?;
584 let new_hash = vcs.revert(&self.root, commit_hash)?;
585
586 info!(
587 commit = %commit_hash,
588 revert = %new_hash,
589 author = %author,
590 "commit reverted"
591 );
592
593 Ok(new_hash)
594 }
595
596 pub fn head_hash(&self) -> Result<String, NapError> {
598 self.require_vcs("read HEAD hash")?.head_hash(&self.root)
599 }
600
601 pub fn resolve_branch_head(&self, branch: &str) -> Result<String, NapError> {
603 self.require_vcs("resolve branch head")?
604 .resolve_branch_head(&self.root, branch)
605 }
606
607 pub fn add_remote(&self, name: &str, url: &str) -> Result<(), NapError> {
611 self.require_vcs("add remote")?
612 .add_remote(&self.root, name, url)
613 }
614
615 pub fn remove_remote(&self, name: &str) -> Result<(), NapError> {
617 self.require_vcs("remove remote")?
618 .remove_remote(&self.root, name)
619 }
620
621 pub fn list_remotes(&self) -> Result<Vec<(String, String)>, NapError> {
623 self.require_vcs("list remotes")?.list_remotes(&self.root)
624 }
625
626 pub fn push(&self, remote: Option<&str>, branch: Option<&str>) -> Result<(), NapError> {
628 self.require_vcs("push")?.push(&self.root, remote, branch)
629 }
630
631 pub fn pull(&self, remote: Option<&str>, branch: Option<&str>) -> Result<(), NapError> {
633 self.require_vcs("pull")?.pull(&self.root, remote, branch)
634 }
635
636 pub fn vcs(&self) -> Option<&dyn VcsBackend> {
640 self.vcs.as_deref()
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 use crate::test_utils::{MockBackend, mock_repo};
648 use tempfile::TempDir;
649
650 #[test]
651 fn test_mock_backend_contract() {
652 crate::test_utils::contract::run_repository_contract(MockBackend::new());
653 }
654
655 #[test]
656 fn test_init_creates_structure() {
657 let tmp = TempDir::new().unwrap();
658 let repo = mock_repo(&tmp);
659
660 assert!(repo.root.join("repository.yaml").exists());
661 }
662
663 #[test]
664 fn test_create_and_read_entity() {
665 let tmp = TempDir::new().unwrap();
666 let repo = mock_repo(&tmp);
667
668 let (manifest, hash) = repo
669 .create_entity(
670 &EntityType::new("character"),
671 "hero",
672 "The Hero",
673 "test-author",
674 )
675 .unwrap();
676
677 assert_eq!(manifest.name, "The Hero");
678 assert_eq!(manifest.entity_type.as_str(), "character");
679 assert_eq!(manifest.version, 1);
680 assert_eq!(repo.head_hash().unwrap(), hash);
681
682 let read_back = repo
684 .read_manifest(&EntityType::new("character"), "hero")
685 .unwrap();
686 assert_eq!(read_back.name, "The Hero");
687 assert_eq!(read_back.version, 1);
688 }
689
690 #[test]
691 fn test_create_entity_auto_creates_type_directory() {
692 let tmp = TempDir::new().unwrap();
693 let repo = mock_repo(&tmp);
694
695 repo.create_entity(&EntityType::new("pokemon"), "pikachu", "Pikachu", "test")
697 .unwrap();
698
699 assert!(repo.root.join("pokemon").exists());
701 assert!(repo.root.join("pokemon").join(".entity-type").exists());
702 assert!(repo.root.join("pokemon/pikachu.yaml").exists());
703 }
704
705 #[test]
706 fn test_list_entity_types() {
707 let tmp = TempDir::new().unwrap();
708 let repo = mock_repo(&tmp);
709
710 repo.create_entity(&EntityType::new("character"), "hero", "Hero", "author")
712 .unwrap();
713 repo.create_entity(&EntityType::new("location"), "village", "Village", "author")
714 .unwrap();
715 repo.create_entity(&EntityType::new("pokemon"), "pikachu", "Pikachu", "author")
716 .unwrap();
717
718 let types = repo.list_entity_types().unwrap();
719 assert!(types.contains(&EntityType::new("character")));
720 assert!(types.contains(&EntityType::new("location")));
721 assert!(types.contains(&EntityType::new("pokemon")));
722 }
723
724 #[test]
725 fn test_list_entities() {
726 let tmp = TempDir::new().unwrap();
727 let repo = mock_repo(&tmp);
728
729 repo.create_entity(&EntityType::new("character"), "alice", "Alice", "author")
730 .unwrap();
731 repo.create_entity(&EntityType::new("character"), "bob", "Bob", "author")
732 .unwrap();
733
734 let chars = repo.list_entities(&EntityType::new("character")).unwrap();
735 assert_eq!(chars, vec!["alice", "bob"]);
736 }
737
738 #[test]
739 fn test_commit_manifest_updates() {
740 let tmp = TempDir::new().unwrap();
741 let repo = mock_repo(&tmp);
742
743 let (mut manifest, create_hash) = repo
744 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
745 .unwrap();
746
747 manifest.set_property("toy_type", serde_yaml::Value::String("elf".to_string()));
749 let changes = vec![Change::set("properties.toy_type", None, "elf".to_string())];
750 let commit = repo
751 .commit_manifest(&mut manifest, "set toy_type to elf", "author", changes)
752 .unwrap();
753
754 assert!(!commit.id.is_empty());
755 assert_eq!(commit.message, "set toy_type to elf");
756 assert_eq!(commit.parent.as_deref(), Some(create_hash.as_str()));
757
758 let read_back = repo
760 .read_manifest(&EntityType::new("character"), "hero")
761 .unwrap();
762 assert!(read_back.version >= 2);
763 }
764
765 #[test]
766 fn test_history() {
767 let tmp = TempDir::new().unwrap();
768 let repo = mock_repo(&tmp);
769
770 let (mut manifest, _) = repo
771 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
772 .unwrap();
773
774 manifest.set_property(
775 "name",
776 serde_yaml::Value::String("Updated Hero".to_string()),
777 );
778 repo.commit_manifest(&mut manifest, "update name", "author", vec![])
779 .unwrap();
780
781 let hist = repo
782 .history(&EntityType::new("character"), "hero", 10)
783 .unwrap();
784 assert!(hist.len() >= 2);
785 }
786
787 #[test]
788 fn test_revert_commit() {
789 let tmp = TempDir::new().unwrap();
790 let repo = mock_repo(&tmp);
791
792 let (mut manifest, _) = repo
794 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
795 .unwrap();
796 assert_eq!(manifest.name, "The Hero");
797
798 manifest.set_property("toy_type", serde_yaml::Value::String("elf".to_string()));
800 let changes = vec![Change::set("properties.toy_type", None, "elf".to_string())];
801 repo.commit_manifest(&mut manifest, "set toy_type to elf", "author", changes)
802 .unwrap();
803 let update_hash = repo.head_hash().unwrap();
804
805 let revert_hash = repo.revert_commit(&update_hash, "author").unwrap();
807 assert!(!revert_hash.is_empty());
808
809 let manifest_path = repo.manifest_path(&EntityType::new("character"), "hero");
811 let manifest_yaml = std::fs::read_to_string(&manifest_path).unwrap();
812 assert!(!manifest_yaml.contains("\nhead:"));
813
814 let hist = repo
816 .history(&EntityType::new("character"), "hero", 10)
817 .unwrap();
818 assert!(hist.iter().any(|c| c.id == revert_hash));
819 }
820
821 fn unversioned_repo(tmp: &TempDir) -> Repository {
824 let repo_path = tmp.path().join("testverse");
825 Repository::init_optional(&repo_path, "testverse", None).unwrap()
826 }
827
828 #[test]
829 fn test_unversioned_init_creates_structure() {
830 let tmp = TempDir::new().unwrap();
831 let repo = unversioned_repo(&tmp);
832
833 assert!(repo.root.join("repository.yaml").exists());
834 assert!(repo.vcs().is_none());
836 }
837
838 #[test]
839 fn test_unversioned_create_returns_sentinel_commit() {
840 let tmp = TempDir::new().unwrap();
841 let repo = unversioned_repo(&tmp);
842
843 let (manifest, hash) = repo
844 .create_entity(
845 &EntityType::new("character"),
846 "hero",
847 "The Hero",
848 "test-author",
849 )
850 .unwrap();
851
852 assert_eq!(manifest.name, "The Hero");
853 assert_eq!(hash, UNVERSIONED_COMMIT);
854 assert!(repo.root.join("character/hero.yaml").exists());
855
856 let read_back = repo
858 .read_manifest(&EntityType::new("character"), "hero")
859 .unwrap();
860 assert_eq!(read_back.name, "The Hero");
861 }
862
863 #[test]
864 fn test_unversioned_commit_manifest_persists() {
865 let tmp = TempDir::new().unwrap();
866 let repo = unversioned_repo(&tmp);
867
868 let (mut manifest, _) = repo
869 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
870 .unwrap();
871
872 manifest.set_property("toy_type", serde_yaml::Value::String("elf".to_string()));
873 let changes = vec![Change::set("properties.toy_type", None, "elf".to_string())];
874 let commit = repo
875 .commit_manifest(&mut manifest, "set toy_type to elf", "author", changes)
876 .unwrap();
877
878 assert!(commit.parent.is_none());
880
881 let read_back = repo
882 .read_manifest(&EntityType::new("character"), "hero")
883 .unwrap();
884 assert_eq!(read_back.properties["toy_type"], "elf");
885 }
886
887 #[test]
888 fn test_unversioned_vcs_operations_error() {
889 let tmp = TempDir::new().unwrap();
890 let repo = unversioned_repo(&tmp);
891
892 repo.create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
893 .unwrap();
894
895 let err = repo
897 .history(&EntityType::new("character"), "hero", 10)
898 .unwrap_err();
899 assert!(matches!(err, NapError::BackendNotConfigured { .. }));
900
901 let err = repo.list_branches().unwrap_err();
902 assert!(matches!(err, NapError::BackendNotConfigured { .. }));
903
904 let err = repo.head_hash().unwrap_err();
905 assert!(matches!(err, NapError::BackendNotConfigured { .. }));
906
907 let err = repo.push(Some("origin"), None).unwrap_err();
908 assert!(matches!(err, NapError::BackendNotConfigured { .. }));
909
910 let msg = err.to_string();
912 assert!(msg.contains("nap backend configure"));
913 }
914
915 #[test]
916 fn test_bootstrap_vcs_attaches_backend_and_commits() {
917 let tmp = TempDir::new().unwrap();
918 let unversioned = unversioned_repo(&tmp);
919
920 unversioned
922 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
923 .unwrap();
924
925 let backend = MockBackend::new();
927 let bootstrapped =
928 Repository::open_optional(&unversioned.root, Some(Box::new(backend))).unwrap();
929
930 let hash = bootstrapped
931 .bootstrap_vcs("Initialize existing NAP repository", "nap")
932 .unwrap();
933
934 assert!(!hash.is_empty());
935 assert!(bootstrapped.head_hash().unwrap() == hash);
936 }
937
938 #[test]
939 fn test_bootstrap_vcs_without_backend_errors() {
940 let tmp = TempDir::new().unwrap();
941 let repo = unversioned_repo(&tmp);
942
943 let err = repo
944 .bootstrap_vcs("Initialize existing NAP repository", "nap")
945 .unwrap_err();
946 assert!(matches!(err, NapError::BackendNotConfigured { .. }));
947 let msg = err.to_string();
948 assert!(msg.contains("nap backend configure"));
949 }
950}
951
952#[cfg(all(test, feature = "lore-integration"))]
956mod lore_integration_tests {
957 use super::*;
958 use crate::vcs_lore::LoreBackend;
959 use std::time::{SystemTime, UNIX_EPOCH};
960 use tempfile::TempDir;
961
962 fn unique_suffix() -> u64 {
963 SystemTime::now()
964 .duration_since(UNIX_EPOCH)
965 .unwrap()
966 .as_nanos() as u64
967 }
968
969 fn setup_lore_repo() -> (TempDir, Repository) {
970 let repository = format!("ri-{}", unique_suffix());
971 let tmp = TempDir::new().unwrap();
972 let repo_path = tmp.path().join(&repository);
973 let repo =
974 Repository::init(&repo_path, &repository, Box::new(LoreBackend::from_env())).unwrap();
975 (tmp, repo)
976 }
977
978 #[test]
979 fn test_lore_init_creates_structure() {
980 let (_tmp, repo) = setup_lore_repo();
981 assert!(repo.root.join(".nap").exists());
982 assert!(repo.root.join("repository.yaml").exists());
983 }
984
985 #[test]
986 fn test_lore_create_and_read_entity() {
987 let (_tmp, repo) = setup_lore_repo();
988
989 let (manifest, _hash) = repo
990 .create_entity(
991 &EntityType::new("character"),
992 "hero",
993 "Test Hero",
994 "integration-test",
995 )
996 .unwrap();
997 assert_eq!(manifest.name, "Test Hero");
998
999 let read_back = repo
1000 .read_manifest(&EntityType::new("character"), "hero")
1001 .unwrap();
1002 assert_eq!(read_back.name, "Test Hero");
1003 }
1004
1005 #[test]
1006 fn test_lore_commit_and_branch() {
1007 let (_tmp, repo) = setup_lore_repo();
1008
1009 let (mut manifest, _) = repo
1010 .create_entity(
1011 &EntityType::new("character"),
1012 "hero",
1013 "Test Hero",
1014 "integration-test",
1015 )
1016 .unwrap();
1017
1018 manifest.set_property("toy_type", serde_yaml::Value::String("plush".to_string()));
1019 let changes = vec![Change::set(
1020 "properties.toy_type",
1021 None,
1022 "plush".to_string(),
1023 )];
1024 repo.commit_manifest(&mut manifest, "add toy_type", "integration-test", changes)
1025 .unwrap();
1026
1027 let read_back = repo
1028 .read_manifest(&EntityType::new("character"), "hero")
1029 .unwrap();
1030 assert_eq!(read_back.version, 2);
1031
1032 repo.create_branch("feature-branch").unwrap();
1033 let branches = repo.list_branches().unwrap();
1034 assert!(branches.contains(&"feature-branch".to_string()));
1035 }
1036
1037 #[test]
1038 fn test_lore_delete_entity() {
1039 let (_tmp, repo) = setup_lore_repo();
1040
1041 repo.create_entity(
1042 &EntityType::new("character"),
1043 "hero",
1044 "Test Hero",
1045 "integration-test",
1046 )
1047 .unwrap();
1048
1049 repo.delete_entity(&EntityType::new("character"), "hero", "integration-test")
1050 .unwrap();
1051
1052 let entities = repo.list_entities(&EntityType::new("character")).unwrap();
1053 assert!(!entities.contains(&"hero".to_string()));
1054 }
1055
1056 #[test]
1057 fn test_lore_history() {
1058 let (_tmp, repo) = setup_lore_repo();
1059
1060 let (mut manifest, _) = repo
1061 .create_entity(
1062 &EntityType::new("character"),
1063 "hero",
1064 "Test Hero",
1065 "integration-test",
1066 )
1067 .unwrap();
1068
1069 manifest.set_property(
1070 "name",
1071 serde_yaml::Value::String("Updated Hero".to_string()),
1072 );
1073 repo.commit_manifest(&mut manifest, "update name", "integration-test", vec![])
1074 .unwrap();
1075
1076 let hist = repo
1077 .history(&EntityType::new("character"), "hero", 10)
1078 .unwrap();
1079 assert!(hist.len() >= 2);
1080 }
1081}