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 struct Repository {
38 pub root: PathBuf,
40 pub repository: String,
42 vcs: Box<dyn VcsBackend>,
44}
45
46impl Repository {
47 pub fn open(path: &Path, vcs: Box<dyn VcsBackend>) -> Result<Self, NapError> {
49 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 pub fn read_resolve_config(&self) -> ResolveConfig {
75 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 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 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 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 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 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 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 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 std::fs::create_dir_all(&repo_root)?;
185
186 let mut repo_manifest = Manifest::new(
188 repository,
189 EntityType::new("world"),
190 repository,
191 &format!("{repository} Repository"),
192 );
193
194 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 vcs.init(&repo_root)?;
207
208 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 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 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 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 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 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 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 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 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 self.ensure_entity_type_dir(entity_type)?;
319
320 let mut manifest = Manifest::new(&self.repository, entity_type.clone(), entity_id, name);
321
322 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 crate::schema::validate_manifest(&manifest)
332 .map_err(|errors| NapError::ManifestValidationError(errors.join("; ")))?;
333
334 self.write_manifest(&manifest)?;
336
337 let commit_message = format!("Create {entity_type} '{name}' ({entity_id})");
339 let commit_hash = self.vcs.commit(&self.root, &commit_message, author)?;
340
341 manifest.head = Some(commit_hash.clone());
343 manifest.bump_version();
344 self.write_manifest(&manifest)?;
345
346 info!(
347 manifest_id = %manifest.id,
348 commit_hash = %commit_hash,
349 "created entity"
350 );
351
352 Ok((manifest, commit_hash))
353 }
354
355 pub fn commit_manifest(
357 &self,
358 manifest: &mut Manifest,
359 message: &str,
360 author: &str,
361 changes: Vec<Change>,
362 ) -> Result<Commit, NapError> {
363 crate::schema::validate_manifest(manifest)
365 .map_err(|errors| NapError::ManifestValidationError(errors.join("; ")))?;
366
367 manifest.bump_version();
369
370 self.write_manifest(manifest)?;
372
373 let manifest_hash = manifest.content_hash()?.as_str().to_string();
375
376 let vcs_hash = self.vcs.commit(&self.root, message, author)?;
378
379 let nap_commit = Commit::new(
381 manifest.head.clone(),
382 author,
383 message,
384 &manifest_hash,
385 changes,
386 );
387
388 manifest.head = Some(vcs_hash.clone());
391 self.write_manifest(manifest)?;
392
393 debug!(
394 manifest_id = %manifest.id,
395 version = manifest.version,
396 nap_commit_id = %nap_commit.id,
397 vcs_hash = %vcs_hash,
398 "manifest committed"
399 );
400
401 Ok(nap_commit)
402 }
403
404 pub fn history(
406 &self,
407 entity_type: &EntityType,
408 entity_id: &str,
409 limit: usize,
410 ) -> Result<Vec<crate::vcs::CommitInfo>, NapError> {
411 let uri = NapUri::new(&self.repository, entity_type.clone(), entity_id);
412 let file_path = uri.manifest_path();
413 self.vcs.log(&self.root, Some(&file_path), limit)
414 }
415
416 pub fn list_entities(&self, entity_type: &EntityType) -> Result<Vec<String>, NapError> {
418 let dir = self.root.join(entity_type.directory_name());
419 if !dir.exists() {
420 return Err(NapError::EntityTypeNotFound(entity_type.to_string()));
421 }
422
423 let mut entities = Vec::new();
424 for entry in std::fs::read_dir(&dir)? {
425 let entry = entry?;
426 let path = entry.path();
427 if path.extension().and_then(|e| e.to_str()) == Some("yaml")
428 && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
429 {
430 entities.push(stem.to_string());
431 }
432 }
433 entities.sort();
434 Ok(entities)
435 }
436
437 pub fn list_entity_types(&self) -> Result<Vec<EntityType>, NapError> {
443 let mut types = Vec::new();
444 for entry in std::fs::read_dir(&self.root)? {
445 let entry = entry?;
446 let path = entry.path();
447 if !path.is_dir() {
448 continue;
449 }
450 let dir_name = match path.file_name().and_then(|n| n.to_str()) {
452 Some(name) => name,
453 None => continue,
454 };
455 if dir_name.starts_with('.') || dir_name == "target" {
456 continue;
457 }
458
459 if path.join(ENTITY_TYPE_MARKER).exists() {
461 types.push(EntityType::new(dir_name));
462 continue;
463 }
464
465 let has_yaml = std::fs::read_dir(&path)
467 .ok()
468 .and_then(|entries| {
469 entries
470 .filter_map(|e| e.ok())
471 .find(|e| e.path().extension().and_then(|ext| ext.to_str()) == Some("yaml"))
472 .map(|_| true)
473 })
474 .unwrap_or(false);
475
476 if has_yaml {
477 types.push(EntityType::new(dir_name));
478 }
479 }
480 types.sort_by(|a, b| a.as_str().cmp(b.as_str()));
481 Ok(types)
482 }
483
484 pub fn delete_entity(
486 &self,
487 entity_type: &EntityType,
488 entity_id: &str,
489 author: &str,
490 ) -> Result<String, NapError> {
491 let path = self.manifest_path(entity_type, entity_id);
492 if !path.exists() {
493 return Err(NapError::ManifestNotFound(path.display().to_string()));
494 }
495
496 std::fs::remove_file(&path)?;
497
498 let message = format!("Delete {entity_type} '{entity_id}'");
499 let hash = self.vcs.commit(&self.root, &message, author)?;
500 info!(entity_type = %entity_type, entity_id = %entity_id, "deleted entity");
501 Ok(hash)
502 }
503
504 pub fn create_branch(&self, name: &str) -> Result<(), NapError> {
506 self.vcs.create_branch(&self.root, name)
507 }
508
509 pub fn switch_branch(&self, name: &str) -> Result<(), NapError> {
511 self.vcs.switch_branch(&self.root, name)
512 }
513
514 pub fn create_tag(&self, name: &str) -> Result<(), NapError> {
516 self.vcs.create_tag(&self.root, name)
517 }
518
519 pub fn list_branches(&self) -> Result<Vec<String>, NapError> {
521 self.vcs.list_branches(&self.root)
522 }
523
524 pub fn list_tags(&self) -> Result<Vec<String>, NapError> {
526 self.vcs.list_tags(&self.root)
527 }
528
529 pub fn revert_commit(&self, commit_hash: &str, author: &str) -> Result<String, NapError> {
531 let new_hash = self.vcs.revert(&self.root, commit_hash)?;
532
533 for entity_type in self.list_entity_types()? {
535 if let Ok(ids) = self.list_entities(&entity_type) {
536 for id in &ids {
537 if let Ok(mut manifest) = self.read_manifest(&entity_type, id) {
538 manifest.head = Some(new_hash.clone());
539 self.write_manifest(&manifest)?;
540 }
541 }
542 }
543 }
544
545 info!(
546 commit = %commit_hash,
547 revert = %new_hash,
548 author = %author,
549 "commit reverted"
550 );
551
552 Ok(new_hash)
553 }
554
555 pub fn head_hash(&self) -> Result<String, NapError> {
557 self.vcs.head_hash(&self.root)
558 }
559
560 pub fn resolve_branch_head(&self, branch: &str) -> Result<String, NapError> {
562 self.vcs.resolve_branch_head(&self.root, branch)
563 }
564
565 pub fn add_remote(&self, name: &str, url: &str) -> Result<(), NapError> {
569 self.vcs.add_remote(&self.root, name, url)
570 }
571
572 pub fn remove_remote(&self, name: &str) -> Result<(), NapError> {
574 self.vcs.remove_remote(&self.root, name)
575 }
576
577 pub fn list_remotes(&self) -> Result<Vec<(String, String)>, NapError> {
579 self.vcs.list_remotes(&self.root)
580 }
581
582 pub fn push(&self, remote: Option<&str>, branch: Option<&str>) -> Result<(), NapError> {
584 self.vcs.push(&self.root, remote, branch)
585 }
586
587 pub fn pull(&self, remote: Option<&str>, branch: Option<&str>) -> Result<(), NapError> {
589 self.vcs.pull(&self.root, remote, branch)
590 }
591
592 pub fn vcs(&self) -> &dyn VcsBackend {
594 self.vcs.as_ref()
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use crate::test_utils::{MockBackend, mock_repo};
602 use tempfile::TempDir;
603
604 #[test]
605 fn test_mock_backend_contract() {
606 crate::test_utils::contract::run_repository_contract(MockBackend::new());
607 }
608
609 #[test]
610 fn test_init_creates_structure() {
611 let tmp = TempDir::new().unwrap();
612 let repo = mock_repo(&tmp);
613
614 assert!(repo.root.join("repository.yaml").exists());
615 }
616
617 #[test]
618 fn test_create_and_read_entity() {
619 let tmp = TempDir::new().unwrap();
620 let repo = mock_repo(&tmp);
621
622 let (manifest, _hash) = repo
623 .create_entity(
624 &EntityType::new("character"),
625 "hero",
626 "The Hero",
627 "test-author",
628 )
629 .unwrap();
630
631 assert_eq!(manifest.name, "The Hero");
632 assert_eq!(manifest.entity_type.as_str(), "character");
633
634 let read_back = repo
636 .read_manifest(&EntityType::new("character"), "hero")
637 .unwrap();
638 assert_eq!(read_back.name, "The Hero");
639 }
640
641 #[test]
642 fn test_create_entity_auto_creates_type_directory() {
643 let tmp = TempDir::new().unwrap();
644 let repo = mock_repo(&tmp);
645
646 repo.create_entity(&EntityType::new("pokemon"), "pikachu", "Pikachu", "test")
648 .unwrap();
649
650 assert!(repo.root.join("pokemon").exists());
652 assert!(repo.root.join("pokemon").join(".entity-type").exists());
653 assert!(repo.root.join("pokemon/pikachu.yaml").exists());
654 }
655
656 #[test]
657 fn test_list_entity_types() {
658 let tmp = TempDir::new().unwrap();
659 let repo = mock_repo(&tmp);
660
661 repo.create_entity(&EntityType::new("character"), "hero", "Hero", "author")
663 .unwrap();
664 repo.create_entity(&EntityType::new("location"), "village", "Village", "author")
665 .unwrap();
666 repo.create_entity(&EntityType::new("pokemon"), "pikachu", "Pikachu", "author")
667 .unwrap();
668
669 let types = repo.list_entity_types().unwrap();
670 assert!(types.contains(&EntityType::new("character")));
671 assert!(types.contains(&EntityType::new("location")));
672 assert!(types.contains(&EntityType::new("pokemon")));
673 }
674
675 #[test]
676 fn test_list_entities() {
677 let tmp = TempDir::new().unwrap();
678 let repo = mock_repo(&tmp);
679
680 repo.create_entity(&EntityType::new("character"), "alice", "Alice", "author")
681 .unwrap();
682 repo.create_entity(&EntityType::new("character"), "bob", "Bob", "author")
683 .unwrap();
684
685 let chars = repo.list_entities(&EntityType::new("character")).unwrap();
686 assert_eq!(chars, vec!["alice", "bob"]);
687 }
688
689 #[test]
690 fn test_commit_manifest_updates() {
691 let tmp = TempDir::new().unwrap();
692 let repo = mock_repo(&tmp);
693
694 let (mut manifest, _) = repo
695 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
696 .unwrap();
697
698 manifest.set_property("species", serde_yaml::Value::String("elf".to_string()));
700 let changes = vec![Change::set("properties.species", None, "elf".to_string())];
701 let commit = repo
702 .commit_manifest(&mut manifest, "set species to elf", "author", changes)
703 .unwrap();
704
705 assert!(!commit.id.is_empty());
706 assert_eq!(commit.message, "set species to elf");
707
708 let read_back = repo
710 .read_manifest(&EntityType::new("character"), "hero")
711 .unwrap();
712 assert!(read_back.version >= 2);
713 }
714
715 #[test]
716 fn test_history() {
717 let tmp = TempDir::new().unwrap();
718 let repo = mock_repo(&tmp);
719
720 let (mut manifest, _) = repo
721 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
722 .unwrap();
723
724 manifest.set_property(
725 "name",
726 serde_yaml::Value::String("Updated Hero".to_string()),
727 );
728 repo.commit_manifest(&mut manifest, "update name", "author", vec![])
729 .unwrap();
730
731 let hist = repo
732 .history(&EntityType::new("character"), "hero", 10)
733 .unwrap();
734 assert!(hist.len() >= 2);
735 }
736
737 #[test]
738 fn test_revert_commit() {
739 let tmp = TempDir::new().unwrap();
740 let repo = mock_repo(&tmp);
741
742 let (mut manifest, _) = repo
744 .create_entity(&EntityType::new("character"), "hero", "The Hero", "author")
745 .unwrap();
746 assert_eq!(manifest.name, "The Hero");
747
748 manifest.set_property("species", serde_yaml::Value::String("elf".to_string()));
750 let changes = vec![Change::set("properties.species", None, "elf".to_string())];
751 let _commit = repo
752 .commit_manifest(&mut manifest, "set species to elf", "author", changes)
753 .unwrap();
754
755 let read_back = repo
757 .read_manifest(&EntityType::new("character"), "hero")
758 .unwrap();
759 assert_eq!(
760 read_back.properties.get("species").and_then(|v| v.as_str()),
761 Some("elf")
762 );
763
764 let vcs_hash = read_back
766 .head
767 .as_ref()
768 .expect("head should be set after commit");
769
770 let revert_hash = repo.revert_commit(vcs_hash, "author").unwrap();
772 assert!(!revert_hash.is_empty());
773
774 let after_revert = repo
776 .read_manifest(&EntityType::new("character"), "hero")
777 .unwrap();
778 assert_eq!(after_revert.head.as_deref(), Some(revert_hash.as_str()));
779
780 let hist = repo
782 .history(&EntityType::new("character"), "hero", 10)
783 .unwrap();
784 assert!(hist.iter().any(|c| c.id == revert_hash));
785 }
786}
787
788#[cfg(all(test, feature = "lore-integration"))]
792mod lore_integration_tests {
793 use super::*;
794 use crate::vcs_lore::LoreBackend;
795 use std::time::{SystemTime, UNIX_EPOCH};
796 use tempfile::TempDir;
797
798 fn unique_suffix() -> u64 {
799 SystemTime::now()
800 .duration_since(UNIX_EPOCH)
801 .unwrap()
802 .as_nanos() as u64
803 }
804
805 fn setup_lore_repo() -> (TempDir, Repository) {
806 let repository = format!("ri-{}", unique_suffix());
807 let tmp = TempDir::new().unwrap();
808 let repo_path = tmp.path().join(&repository);
809 let repo =
810 Repository::init(&repo_path, &repository, Box::new(LoreBackend::from_env())).unwrap();
811 (tmp, repo)
812 }
813
814 #[test]
815 fn test_lore_init_creates_structure() {
816 let (_tmp, repo) = setup_lore_repo();
817 assert!(repo.root.join(".nap").exists());
818 assert!(repo.root.join("repository.yaml").exists());
819 }
820
821 #[test]
822 fn test_lore_create_and_read_entity() {
823 let (_tmp, repo) = setup_lore_repo();
824
825 let (manifest, _hash) = repo
826 .create_entity(
827 &EntityType::new("character"),
828 "hero",
829 "Test Hero",
830 "integration-test",
831 )
832 .unwrap();
833 assert_eq!(manifest.name, "Test Hero");
834
835 let read_back = repo
836 .read_manifest(&EntityType::new("character"), "hero")
837 .unwrap();
838 assert_eq!(read_back.name, "Test Hero");
839 }
840
841 #[test]
842 fn test_lore_commit_and_branch() {
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("species", serde_yaml::Value::String("human".to_string()));
855 let changes = vec![Change::set("properties.species", None, "human".to_string())];
856 repo.commit_manifest(&mut manifest, "add species", "integration-test", changes)
857 .unwrap();
858
859 let read_back = repo
860 .read_manifest(&EntityType::new("character"), "hero")
861 .unwrap();
862 assert_eq!(read_back.version, 2);
863
864 repo.create_branch("feature-branch").unwrap();
865 let branches = repo.list_branches().unwrap();
866 assert!(branches.contains(&"feature-branch".to_string()));
867 }
868
869 #[test]
870 fn test_lore_delete_entity() {
871 let (_tmp, repo) = setup_lore_repo();
872
873 repo.create_entity(
874 &EntityType::new("character"),
875 "hero",
876 "Test Hero",
877 "integration-test",
878 )
879 .unwrap();
880
881 repo.delete_entity(&EntityType::new("character"), "hero", "integration-test")
882 .unwrap();
883
884 let entities = repo.list_entities(&EntityType::new("character")).unwrap();
885 assert!(!entities.contains(&"hero".to_string()));
886 }
887
888 #[test]
889 fn test_lore_history() {
890 let (_tmp, repo) = setup_lore_repo();
891
892 let (mut manifest, _) = repo
893 .create_entity(
894 &EntityType::new("character"),
895 "hero",
896 "Test Hero",
897 "integration-test",
898 )
899 .unwrap();
900
901 manifest.set_property(
902 "name",
903 serde_yaml::Value::String("Updated Hero".to_string()),
904 );
905 repo.commit_manifest(&mut manifest, "update name", "integration-test", vec![])
906 .unwrap();
907
908 let hist = repo
909 .history(&EntityType::new("character"), "hero", 10)
910 .unwrap();
911 assert!(hist.len() >= 2);
912 }
913}