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