This repository has no description
29 kB
1043 lines
1use std::sync::Arc;
2use std::sync::atomic::{AtomicBool, Ordering};
3
4use knot_cob::{ChangePayload, CobHome, CobId, CobStore};
5use knot_cobs::{CollaboratorsChange, MembersChange, RegistryChange, Removal, Rename, RepoRef};
6use knot_git::{RefUpdate, Repo};
7use knot_index::{Coverage, IndexError, Resolved};
8use knot_types::{ClonePath, RefName, RepoName};
9use serde::{Deserialize, Serialize};
10
11mod common;
12use common::{
13 World, acc, at, grant, meta_home, named_registration, own, registration, repo_did, rkey,
14};
15
16#[derive(Serialize, Deserialize)]
17#[serde(tag = "op", content = "data", rename_all = "snake_case")]
18enum BadMembers {
19 Explode(u8),
20}
21impl ChangePayload for BadMembers {
22 const TYPE: &'static str = "sh.tangled.knot.member";
23}
24
25#[test]
26fn a_concurrent_reader_never_sees_a_net_absent_subject() {
27 let world = World::new();
28 let object = world.seed_members();
29 let index = Arc::new(world.index());
30 index.rebuild().unwrap();
31 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false));
32
33 let meta = Repo::open(&world.meta_path).unwrap();
34 let store = CobStore::new(&meta);
35 store
36 .update(
37 &meta_home(),
38 object,
39 &MembersChange::Add(grant("teq", "nel", 10)),
40 &world.signer,
41 at(10),
42 )
43 .unwrap();
44 (0..1000).for_each(|i| {
45 store
46 .update(
47 &meta_home(),
48 object,
49 &MembersChange::Add(grant(&format!("f{i}"), "nel", 11 + i)),
50 &world.signer,
51 at(11 + i),
52 )
53 .unwrap();
54 });
55 store
56 .update(
57 &meta_home(),
58 object,
59 &MembersChange::Remove(Removal {
60 subject: acc("teq"),
61 }),
62 &world.signer,
63 at(20_000),
64 )
65 .unwrap();
66
67 let done = Arc::new(AtomicBool::new(false));
68 let leaked = Arc::new(AtomicBool::new(false));
69 std::thread::scope(|scope| {
70 let reader = Arc::clone(&index);
71 let reader_done = Arc::clone(&done);
72 let reader_leaked = Arc::clone(&leaked);
73 scope.spawn(move || {
74 while !reader_done.load(Ordering::Acquire) {
75 if reader.is_member(&acc("teq")) == Resolved::Ready(true) {
76 reader_leaked.store(true, Ordering::Release);
77 }
78 }
79 });
80 index.refresh_members().unwrap();
81 done.store(true, Ordering::Release);
82 });
83
84 assert!(
85 !leaked.load(Ordering::Acquire),
86 "net-absent subject is never written, so no reader can observe it mid-delta"
87 );
88 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false));
89 assert_eq!(index.is_member(&acc("f0")), Resolved::Ready(true));
90 assert_eq!(index.is_member(&acc("f999")), Resolved::Ready(true));
91}
92
93#[test]
94fn a_concurrent_reader_never_sees_a_collaborator_roster_emptied_mid_refresh() {
95 let world = World::new();
96 let repo = repo_did("squid");
97 let git = world.layout.create(&repo).unwrap();
98 let store = CobStore::new(&git);
99 let object = store
100 .create(
101 &CobHome::from(&repo),
102 &CollaboratorsChange::Add(grant("anchor", "nel", 1)),
103 &world.signer,
104 at(1),
105 )
106 .unwrap()
107 .object;
108 (0..128).for_each(|i| {
109 store
110 .update(
111 &CobHome::from(&repo),
112 object,
113 &CollaboratorsChange::Add(grant(&format!("c{i}"), "nel", 2 + i)),
114 &world.signer,
115 at(2 + i),
116 )
117 .unwrap();
118 });
119
120 let index = Arc::new(world.index());
121 index.rebuild().unwrap();
122 index.ensure_collaborators(&repo).unwrap();
123 assert_eq!(
124 index.is_collaborator(&repo, &acc("anchor")),
125 Resolved::Ready(true)
126 );
127
128 let done = Arc::new(AtomicBool::new(false));
129 let leaked = Arc::new(AtomicBool::new(false));
130 std::thread::scope(|scope| {
131 let reader = Arc::clone(&index);
132 let reader_done = Arc::clone(&done);
133 let reader_leaked = Arc::clone(&leaked);
134 let target = repo.clone();
135 scope.spawn(move || {
136 while !reader_done.load(Ordering::Acquire) {
137 if reader.is_collaborator(&target, &acc("anchor")) != Resolved::Ready(true) {
138 reader_leaked.store(true, Ordering::Release);
139 }
140 }
141 });
142 (0..500).for_each(|_| index.refresh_collaborators(&repo).unwrap());
143 done.store(true, Ordering::Release);
144 });
145
146 assert!(
147 !leaked.load(Ordering::Acquire),
148 "in-place mem::take runs under per-repo lock, so anchor present in both \
149 pre- and post-refresh roster is never observed absent or warming mid-refresh"
150 );
151 assert_eq!(
152 index.is_collaborator(&repo, &acc("anchor")),
153 Resolved::Ready(true)
154 );
155}
156
157fn cob_ref(type_name: &str, object: CobId) -> RefName {
158 RefName::new(format!("refs/cobs/{type_name}/{}", object.oid())).unwrap()
159}
160
161#[test]
162fn a_diverged_collaborators_tip_purges_the_roster_instead_of_serving_it_stale() {
163 let world = World::new();
164 let repo = repo_did("squid");
165 let git = world.layout.create(&repo).unwrap();
166 let store = CobStore::new(&git);
167 let created = store
168 .create(
169 &CobHome::from(&repo),
170 &CollaboratorsChange::Add(grant("lyna", "nel", 1)),
171 &world.signer,
172 at(1),
173 )
174 .unwrap();
175 let tip = store
176 .update(
177 &CobHome::from(&repo),
178 created.object,
179 &CollaboratorsChange::Add(grant("bailey", "nel", 2)),
180 &world.signer,
181 at(2),
182 )
183 .unwrap();
184
185 let index = world.index();
186 index.rebuild().unwrap();
187 index.ensure_collaborators(&repo).unwrap();
188 assert_eq!(
189 index.is_collaborator(&repo, &acc("lyna")),
190 Resolved::Ready(true)
191 );
192
193 git.update_ref(&RefUpdate::Update {
194 name: cob_ref(CollaboratorsChange::TYPE, created.object),
195 old: tip.oid(),
196 new: created.object.oid(),
197 })
198 .unwrap();
199
200 assert!(
201 index.refresh_collaborators(&repo).is_err(),
202 "tip that no longer descends from folded tip is structural error"
203 );
204 assert_eq!(
205 index.is_collaborator(&repo, &acc("lyna")),
206 Resolved::Warming,
207 "diverged COB tip purges roster and fails closed, it does not serve \
208 pre-divergence collaborators"
209 );
210}
211
212#[test]
213fn a_diverged_members_tip_fails_closed_to_warming() {
214 let world = World::new();
215 let object = world.seed_members();
216 let meta = Repo::open(&world.meta_path).unwrap();
217 let store = CobStore::new(&meta);
218 let tip = store
219 .update(
220 &meta_home(),
221 object,
222 &MembersChange::Add(grant("teq", "nel", 3)),
223 &world.signer,
224 at(3),
225 )
226 .unwrap();
227
228 let index = world.index();
229 index.rebuild().unwrap();
230 assert_eq!(index.is_member(&acc("nel")), Resolved::Ready(true));
231
232 meta.update_ref(&RefUpdate::Update {
233 name: cob_ref(MembersChange::TYPE, object),
234 old: tip.oid(),
235 new: object.oid(),
236 })
237 .unwrap();
238
239 assert!(
240 index.refresh_members().is_err(),
241 "tip that no longer descends from folded tip is structural error"
242 );
243 assert_eq!(index.coverage().members, Coverage::Warming);
244 assert_eq!(
245 index.is_member(&acc("nel")),
246 Resolved::Warming,
247 "diverged members COB fails projection closed instead of serving stale members"
248 );
249}
250
251#[test]
252fn an_undecodable_change_fails_closed_with_no_partial_apply() {
253 let world = World::new();
254 let object = world.seed_members();
255 let index = world.index();
256 index.rebuild().unwrap();
257
258 let meta = Repo::open(&world.meta_path).unwrap();
259 let store = CobStore::new(&meta);
260 store
261 .update(
262 &meta_home(),
263 object,
264 &MembersChange::Add(grant("teq", "nel", 3)),
265 &world.signer,
266 at(3),
267 )
268 .unwrap();
269 store
270 .update(
271 &meta_home(),
272 object,
273 &BadMembers::Explode(0),
274 &world.signer,
275 at(4),
276 )
277 .unwrap();
278 store
279 .update(
280 &meta_home(),
281 object,
282 &MembersChange::Remove(Removal {
283 subject: acc("teq"),
284 }),
285 &world.signer,
286 at(5),
287 )
288 .unwrap();
289
290 assert!(matches!(
291 index.refresh_members(),
292 Err(IndexError::Decode { .. })
293 ));
294
295 assert_eq!(index.coverage().members, Coverage::Warming);
296 assert_eq!(
297 index.is_member(&acc("teq")),
298 Resolved::Warming,
299 "no partial apply: teq from pre-error change was never committed"
300 );
301 assert_eq!(
302 index.is_member(&acc("nel")),
303 Resolved::Warming,
304 "structurally broken COB fails whole projection closed"
305 );
306
307 assert!(matches!(
308 index.refresh_members(),
309 Err(IndexError::Decode { .. })
310 ));
311 assert_eq!(index.coverage().members, Coverage::Warming);
312}
313
314#[test]
315fn deregister_purges_collaborators_fail_closed() {
316 let world = World::new();
317 let repo = repo_did("clam");
318
319 let meta = Repo::open(&world.meta_path).unwrap();
320 let store = CobStore::new(&meta);
321 let registry = store
322 .create(
323 &meta_home(),
324 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)),
325 &world.signer,
326 at(1),
327 )
328 .unwrap()
329 .object;
330
331 let git = world.layout.create(&repo).unwrap();
332 let cstore = CobStore::new(&git);
333 cstore
334 .create(
335 &CobHome::from(&repo),
336 &CollaboratorsChange::Add(grant("lyna", "nel", 1)),
337 &world.signer,
338 at(1),
339 )
340 .unwrap();
341
342 let index = world.index();
343 index.rebuild().unwrap();
344 index.warm_collaborators();
345 assert_eq!(
346 index.is_collaborator(&repo, &acc("lyna")),
347 Resolved::Ready(true)
348 );
349
350 store
351 .update(
352 &meta_home(),
353 registry,
354 &RegistryChange::Deregister(RepoRef {
355 owner: own("nel"),
356 rkey: rkey("anemone"),
357 }),
358 &world.signer,
359 at(2),
360 )
361 .unwrap();
362 index.refresh_registry().unwrap();
363
364 assert_eq!(
365 index.resolve_repo(&own("nel"), &rkey("anemone")),
366 Resolved::Ready(None)
367 );
368 assert_eq!(
369 index.is_collaborator(&repo, &acc("lyna")),
370 Resolved::Warming,
371 "deregistered repo's collaborators are purged and fail closed, not served stale"
372 );
373}
374
375#[test]
376fn a_renamed_repo_keeps_both_rkeys_and_its_collaborators() {
377 let world = World::new();
378 let repo = repo_did("squid");
379
380 let meta = Repo::open(&world.meta_path).unwrap();
381 let store = CobStore::new(&meta);
382 let registry = store
383 .create(
384 &meta_home(),
385 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)),
386 &world.signer,
387 at(1),
388 )
389 .unwrap()
390 .object;
391
392 let git = world.layout.create(&repo).unwrap();
393 CobStore::new(&git)
394 .create(
395 &CobHome::from(&repo),
396 &CollaboratorsChange::Add(grant("lyna", "nel", 1)),
397 &world.signer,
398 at(1),
399 )
400 .unwrap();
401
402 let index = world.index();
403 index.rebuild().unwrap();
404 index.warm_collaborators();
405 assert_eq!(index.rkey_of(&repo), Resolved::Ready(Some(rkey("anemone"))));
406
407 store
408 .update(
409 &meta_home(),
410 registry,
411 &RegistryChange::Rename(Rename {
412 owner: own("nel"),
413 rkey: rkey("barnacle"),
414 name: RepoName::new("barnacle").unwrap(),
415 repo: repo.clone(),
416 }),
417 &world.signer,
418 at(2),
419 )
420 .unwrap();
421 index.refresh_registry().unwrap();
422
423 assert_eq!(
424 index.resolve_repo(&own("nel"), &rkey("anemone")),
425 Resolved::Ready(Some(repo.clone())),
426 "prior rkey keeps resolving as alias after rename is delta-applied"
427 );
428 assert_eq!(
429 index.resolve_repo(&own("nel"), &rkey("barnacle")),
430 Resolved::Ready(Some(repo.clone()))
431 );
432 assert_eq!(
433 index.rkey_of(&repo),
434 Resolved::Ready(Some(rkey("barnacle"))),
435 "new rkey is canonical"
436 );
437 assert_eq!(
438 index.is_collaborator(&repo, &acc("lyna")),
439 Resolved::Ready(true),
440 "rename never evacuates repo, so its collaborators survive"
441 );
442
443 store
444 .update(
445 &meta_home(),
446 registry,
447 &RegistryChange::Deregister(RepoRef {
448 owner: own("nel"),
449 rkey: rkey("anemone"),
450 }),
451 &world.signer,
452 at(3),
453 )
454 .unwrap();
455 index.refresh_registry().unwrap();
456 assert_eq!(
457 index.resolve_repo(&own("nel"), &rkey("barnacle")),
458 Resolved::Ready(None),
459 "deregistering through retained alias removes repo and every alias"
460 );
461 assert_eq!(
462 index.is_collaborator(&repo, &acc("lyna")),
463 Resolved::Warming,
464 "deregistered repo's collaborators are evacuated"
465 );
466}
467
468#[test]
469fn a_repo_moved_within_one_delta_is_not_evacuated() {
470 let world = World::new();
471 let repo = repo_did("squid");
472
473 let meta = Repo::open(&world.meta_path).unwrap();
474 let store = CobStore::new(&meta);
475 let registry = store
476 .create(
477 &meta_home(),
478 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)),
479 &world.signer,
480 at(1),
481 )
482 .unwrap()
483 .object;
484
485 let git = world.layout.create(&repo).unwrap();
486 CobStore::new(&git)
487 .create(
488 &CobHome::from(&repo),
489 &CollaboratorsChange::Add(grant("lyna", "nel", 1)),
490 &world.signer,
491 at(1),
492 )
493 .unwrap();
494
495 let index = world.index();
496 index.rebuild().unwrap();
497 index.warm_collaborators();
498 assert_eq!(
499 index.is_collaborator(&repo, &acc("lyna")),
500 Resolved::Ready(true)
501 );
502
503 store
504 .update(
505 &meta_home(),
506 registry,
507 &RegistryChange::Deregister(RepoRef {
508 owner: own("nel"),
509 rkey: rkey("anemone"),
510 }),
511 &world.signer,
512 at(2),
513 )
514 .unwrap();
515 store
516 .update(
517 &meta_home(),
518 registry,
519 &RegistryChange::Register(registration("nel", "barnacle", &repo, 3)),
520 &world.signer,
521 at(3),
522 )
523 .unwrap();
524 index.refresh_registry().unwrap();
525
526 assert_eq!(
527 index.resolve_repo(&own("nel"), &rkey("anemone")),
528 Resolved::Ready(None)
529 );
530 assert_eq!(
531 index.resolve_repo(&own("nel"), &rkey("barnacle")),
532 Resolved::Ready(Some(repo.clone()))
533 );
534 assert_eq!(
535 index.is_collaborator(&repo, &acc("lyna")),
536 Resolved::Ready(true),
537 "deregister and re-register within single delta leaves repo hosted, so its collaborators survive"
538 );
539}
540
541fn path(raw: &str) -> ClonePath {
542 ClonePath::parse(raw).unwrap()
543}
544
545#[test]
546fn a_clone_path_resolves_by_name_when_the_record_key_is_a_tid() {
547 let world = World::new();
548 let repo = repo_did("squid");
549
550 let meta = Repo::open(&world.meta_path).unwrap();
551 let store = CobStore::new(&meta);
552 store
553 .create(
554 &meta_home(),
555 &RegistryChange::Register(named_registration(
556 "nel",
557 "3mizfnpxii522",
558 "substratum.cloud",
559 &repo,
560 1,
561 )),
562 &world.signer,
563 at(1),
564 )
565 .unwrap();
566
567 let index = world.index();
568 index.rebuild().unwrap();
569
570 assert_eq!(
571 index.resolve_clone_path(&own("nel"), &path("substratum.cloud")),
572 Resolved::Ready(Some(repo.clone())),
573 "a PDS-native record key leaves the display name as the only human clone path"
574 );
575 assert_eq!(
576 index.resolve_clone_path(&own("nel"), &path("3mizfnpxii522")),
577 Resolved::Ready(Some(repo.clone())),
578 "the record key still resolves"
579 );
580 assert_eq!(
581 index.resolve_clone_path(&own("nel"), &path("substratum.cloud.git")),
582 Resolved::Ready(Some(repo)),
583 "the conventional .git suffix strips before the name lookup"
584 );
585 assert_eq!(
586 index.resolve_clone_path(&own("nel"), &path("periwinkle")),
587 Resolved::Ready(None)
588 );
589}
590
591#[test]
592fn a_record_key_outranks_another_repos_name() {
593 let world = World::new();
594 let by_name = repo_did("limpet");
595 let by_rkey = repo_did("mussel");
596
597 let meta = Repo::open(&world.meta_path).unwrap();
598 let store = CobStore::new(&meta);
599 let object = store
600 .create(
601 &meta_home(),
602 &RegistryChange::Register(named_registration("nel", "limpet", "mussel", &by_name, 1)),
603 &world.signer,
604 at(1),
605 )
606 .unwrap()
607 .object;
608 store
609 .update(
610 &meta_home(),
611 object,
612 &RegistryChange::Register(named_registration("nel", "mussel", "scallop", &by_rkey, 2)),
613 &world.signer,
614 at(2),
615 )
616 .unwrap();
617
618 let index = world.index();
619 index.rebuild().unwrap();
620
621 assert_eq!(
622 index.resolve_clone_path(&own("nel"), &path("mussel")),
623 Resolved::Ready(Some(by_rkey)),
624 "a record key match wins over another repo holding that string as its name"
625 );
626 assert_eq!(
627 index.resolve_clone_path(&own("nel"), &path("scallop")),
628 Resolved::Ready(Some(repo_did("mussel")))
629 );
630}
631
632#[test]
633fn a_name_two_repos_share_resolves_to_the_older_registration() {
634 let world = World::new();
635 let first = repo_did("whelk");
636 let second = repo_did("conch");
637
638 let meta = Repo::open(&world.meta_path).unwrap();
639 let store = CobStore::new(&meta);
640 let object = store
641 .create(
642 &meta_home(),
643 &RegistryChange::Register(named_registration(
644 "nel",
645 "3lubrptx57d22",
646 "kelp",
647 &first,
648 1,
649 )),
650 &world.signer,
651 at(1),
652 )
653 .unwrap()
654 .object;
655 store
656 .update(
657 &meta_home(),
658 object,
659 &RegistryChange::Register(named_registration(
660 "nel",
661 "3mqydma3re27z",
662 "kelp",
663 &second,
664 2,
665 )),
666 &world.signer,
667 at(2),
668 )
669 .unwrap();
670
671 let index = world.index();
672 index.rebuild().unwrap();
673
674 assert_eq!(
675 index.resolve_clone_path(&own("nel"), &path("kelp")),
676 Resolved::Ready(Some(first.clone())),
677 "a contested name resolves to whichever repo registered first, ordered by \
678 created_at so a cold seed and an incremental replay agree"
679 );
680 assert_eq!(
681 index.resolve_clone_path(&own("nel"), &path("3lubrptx57d22")),
682 Resolved::Ready(Some(first)),
683 "each record key stays unambiguous"
684 );
685 assert_eq!(
686 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")),
687 Resolved::Ready(Some(second)),
688 "the repo that lost the name is still reachable by its record key"
689 );
690}
691
692#[test]
693fn a_repo_registered_later_never_takes_a_contested_name_by_renaming_onto_it() {
694 let world = World::new();
695 let holder = repo_did("whelk");
696 let latecomer = repo_did("conch");
697
698 let meta = Repo::open(&world.meta_path).unwrap();
699 let store = CobStore::new(&meta);
700 let object = store
701 .create(
702 &meta_home(),
703 &RegistryChange::Register(named_registration(
704 "nel",
705 "3lubrptx57d22",
706 "kelp",
707 &holder,
708 1,
709 )),
710 &world.signer,
711 at(1),
712 )
713 .unwrap()
714 .object;
715 store
716 .update(
717 &meta_home(),
718 object,
719 &RegistryChange::Register(named_registration(
720 "nel",
721 "3mqydma3re27z",
722 "uni",
723 &latecomer,
724 2,
725 )),
726 &world.signer,
727 at(2),
728 )
729 .unwrap();
730
731 let index = world.index();
732 index.rebuild().unwrap();
733
734 store
735 .update(
736 &meta_home(),
737 object,
738 &RegistryChange::Rename(Rename {
739 owner: own("nel"),
740 rkey: rkey("3mqydma3re27z"),
741 name: RepoName::new("kelp").unwrap(),
742 repo: latecomer.clone(),
743 }),
744 &world.signer,
745 at(3),
746 )
747 .unwrap();
748 index.refresh_registry().unwrap();
749
750 assert_eq!(
751 index.resolve_clone_path(&own("nel"), &path("kelp")),
752 Resolved::Ready(Some(holder)),
753 "a rename keeps the repo's original created_at, so renaming onto a name \
754 another repo registered earlier cannot take it"
755 );
756 assert_eq!(
757 index.resolve_clone_path(&own("nel"), &path("uni")),
758 Resolved::Ready(None),
759 "the renamed repo's previous name stops resolving"
760 );
761 assert_eq!(
762 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")),
763 Resolved::Ready(Some(latecomer))
764 );
765}
766
767#[test]
768fn deregistering_the_older_registration_moves_a_shared_name_to_the_survivor() {
769 let world = World::new();
770 let first = repo_did("whelk");
771 let survivor = repo_did("conch");
772
773 let meta = Repo::open(&world.meta_path).unwrap();
774 let store = CobStore::new(&meta);
775 let object = store
776 .create(
777 &meta_home(),
778 &RegistryChange::Register(named_registration(
779 "nel",
780 "3lubrptx57d22",
781 "kelp",
782 &first,
783 1,
784 )),
785 &world.signer,
786 at(1),
787 )
788 .unwrap()
789 .object;
790 store
791 .update(
792 &meta_home(),
793 object,
794 &RegistryChange::Register(named_registration(
795 "nel",
796 "3mqydma3re27z",
797 "kelp",
798 &survivor,
799 2,
800 )),
801 &world.signer,
802 at(2),
803 )
804 .unwrap();
805
806 let index = world.index();
807 index.rebuild().unwrap();
808 assert_eq!(
809 index.resolve_clone_path(&own("nel"), &path("kelp")),
810 Resolved::Ready(Some(first)),
811 "the name resolves to the older registration while both exist"
812 );
813
814 store
815 .update(
816 &meta_home(),
817 object,
818 &RegistryChange::Deregister(RepoRef {
819 owner: own("nel"),
820 rkey: rkey("3lubrptx57d22"),
821 }),
822 &world.signer,
823 at(3),
824 )
825 .unwrap();
826 index.refresh_registry().unwrap();
827
828 assert_eq!(
829 index.resolve_clone_path(&own("nel"), &path("kelp")),
830 Resolved::Ready(Some(survivor)),
831 "the name resolves to the remaining repo once the older one is deregistered"
832 );
833}
834
835#[test]
836fn a_rename_moves_name_resolution_off_the_old_name() {
837 let world = World::new();
838 let repo = repo_did("squid");
839
840 let meta = Repo::open(&world.meta_path).unwrap();
841 let store = CobStore::new(&meta);
842 let object = store
843 .create(
844 &meta_home(),
845 &RegistryChange::Register(named_registration(
846 "nel",
847 "3mizfnpxii522",
848 "anemone",
849 &repo,
850 1,
851 )),
852 &world.signer,
853 at(1),
854 )
855 .unwrap()
856 .object;
857
858 let index = world.index();
859 index.rebuild().unwrap();
860 assert_eq!(
861 index.resolve_clone_path(&own("nel"), &path("anemone")),
862 Resolved::Ready(Some(repo.clone()))
863 );
864
865 store
866 .update(
867 &meta_home(),
868 object,
869 &RegistryChange::Rename(Rename {
870 owner: own("nel"),
871 rkey: rkey("3mizfnpxii522"),
872 name: RepoName::new("barnacle").unwrap(),
873 repo: repo.clone(),
874 }),
875 &world.signer,
876 at(2),
877 )
878 .unwrap();
879 index.refresh_registry().unwrap();
880
881 assert_eq!(
882 index.resolve_clone_path(&own("nel"), &path("barnacle")),
883 Resolved::Ready(Some(repo.clone())),
884 "the new name resolves after a rename that keeps the record key"
885 );
886 assert_eq!(
887 index.resolve_clone_path(&own("nel"), &path("anemone")),
888 Resolved::Ready(None),
889 "the superseded name stops resolving"
890 );
891 assert_eq!(
892 index.resolve_clone_path(&own("nel"), &path("3mizfnpxii522")),
893 Resolved::Ready(Some(repo))
894 );
895}
896
897#[test]
898fn a_rename_that_changes_only_the_record_key_keeps_the_name_resolving() {
899 let world = World::new();
900 let repo = repo_did("squid");
901
902 let meta = Repo::open(&world.meta_path).unwrap();
903 let store = CobStore::new(&meta);
904 let object = store
905 .create(
906 &meta_home(),
907 &RegistryChange::Register(named_registration("nel", "3lubrptx57d22", "kelp", &repo, 1)),
908 &world.signer,
909 at(1),
910 )
911 .unwrap()
912 .object;
913
914 let index = world.index();
915 index.rebuild().unwrap();
916
917 store
918 .update(
919 &meta_home(),
920 object,
921 &RegistryChange::Rename(Rename {
922 owner: own("nel"),
923 rkey: rkey("3mqydma3re27z"),
924 name: RepoName::new("kelp").unwrap(),
925 repo: repo.clone(),
926 }),
927 &world.signer,
928 at(2),
929 )
930 .unwrap();
931 index.refresh_registry().unwrap();
932
933 assert_eq!(
934 index.resolve_clone_path(&own("nel"), &path("kelp")),
935 Resolved::Ready(Some(repo.clone())),
936 "the unchanged name survives a rename that swaps the record key"
937 );
938 assert_eq!(
939 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")),
940 Resolved::Ready(Some(repo.clone()))
941 );
942 assert_eq!(
943 index.resolve_clone_path(&own("nel"), &path("3lubrptx57d22")),
944 Resolved::Ready(Some(repo)),
945 "the superseded record key keeps resolving through its retained alias"
946 );
947}
948
949#[test]
950fn seeding_and_replaying_agree_on_name_resolution() {
951 let world = World::new();
952 let first = repo_did("whelk");
953 let second = repo_did("conch");
954
955 let meta = Repo::open(&world.meta_path).unwrap();
956 let store = CobStore::new(&meta);
957 let object = store
958 .create(
959 &meta_home(),
960 &RegistryChange::Register(named_registration(
961 "nel",
962 "3lubrptx57d22",
963 "kelp",
964 &first,
965 1,
966 )),
967 &world.signer,
968 at(1),
969 )
970 .unwrap()
971 .object;
972
973 let replayed = world.index();
974 replayed.rebuild().unwrap();
975
976 store
977 .update(
978 &meta_home(),
979 object,
980 &RegistryChange::Register(named_registration(
981 "nel",
982 "3mqydma3re27z",
983 "uni",
984 &second,
985 2,
986 )),
987 &world.signer,
988 at(2),
989 )
990 .unwrap();
991 replayed.refresh_registry().unwrap();
992
993 let seeded = world.index();
994 seeded.rebuild().unwrap();
995
996 ["kelp", "uni", "3lubrptx57d22", "3mqydma3re27z", "nautilus"]
997 .into_iter()
998 .for_each(|segment| {
999 assert_eq!(
1000 replayed.resolve_clone_path(&own("nel"), &path(segment)),
1001 seeded.resolve_clone_path(&own("nel"), &path(segment)),
1002 "incremental replay and a cold seed disagree on {segment}"
1003 );
1004 });
1005}
1006
1007#[test]
1008fn a_name_differing_from_its_record_key_only_by_case_resolves() {
1009 let world = World::new();
1010 let repo = repo_did("squid");
1011
1012 let meta = Repo::open(&world.meta_path).unwrap();
1013 let store = CobStore::new(&meta);
1014 store
1015 .create(
1016 &meta_home(),
1017 &RegistryChange::Register(named_registration(
1018 "nel",
1019 "runic_lang",
1020 "Runic_lang",
1021 &repo,
1022 1,
1023 )),
1024 &world.signer,
1025 at(1),
1026 )
1027 .unwrap();
1028
1029 let index = world.index();
1030 index.rebuild().unwrap();
1031
1032 assert_eq!(
1033 index.resolve_clone_path(&own("nel"), &path("Runic_lang")),
1034 Resolved::Ready(Some(repo.clone())),
1035 "the appview lowercases the record key but keeps the display name's case, \
1036 so the mixed-case path resolves by name"
1037 );
1038 assert_eq!(
1039 index.resolve_clone_path(&own("nel"), &path("runic_lang")),
1040 Resolved::Ready(Some(repo)),
1041 "the lowercased record key still resolves"
1042 );
1043}