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