This repository has no description
30 kB
1073 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, OfferedKey, 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
541#[test]
542fn key_cache_evicts_least_recently_used() {
543 const CAP: u32 = 16_384;
544 let world = World::new();
545 let index = world.index();
546 let key = |i: u32| OfferedKey::from_bytes(i.to_le_bytes().to_vec());
547
548 (0..CAP).for_each(|i| index.cache_key(key(i), &acc("nel")));
549 assert_eq!(
550 index.owner_of_key(&key(0)),
551 Resolved::Ready(Some(acc("nel")))
552 );
553 index.cache_key(key(CAP), &acc("nel"));
554
555 assert_eq!(
556 index.owner_of_key(&key(1)),
557 Resolved::Ready(None),
558 "least-recently-used key is evicted"
559 );
560 assert_eq!(
561 index.owner_of_key(&key(0)),
562 Resolved::Ready(Some(acc("nel"))),
563 "recently-used key survives despite being inserted first"
564 );
565 assert_eq!(
566 index.owner_of_key(&key(CAP)),
567 Resolved::Ready(Some(acc("nel")))
568 );
569}
570
571fn path(raw: &str) -> ClonePath {
572 ClonePath::parse(raw).unwrap()
573}
574
575#[test]
576fn a_clone_path_resolves_by_name_when_the_record_key_is_a_tid() {
577 let world = World::new();
578 let repo = repo_did("squid");
579
580 let meta = Repo::open(&world.meta_path).unwrap();
581 let store = CobStore::new(&meta);
582 store
583 .create(
584 &meta_home(),
585 &RegistryChange::Register(named_registration(
586 "nel",
587 "3mizfnpxii522",
588 "substratum.cloud",
589 &repo,
590 1,
591 )),
592 &world.signer,
593 at(1),
594 )
595 .unwrap();
596
597 let index = world.index();
598 index.rebuild().unwrap();
599
600 assert_eq!(
601 index.resolve_clone_path(&own("nel"), &path("substratum.cloud")),
602 Resolved::Ready(Some(repo.clone())),
603 "a PDS-native record key leaves the display name as the only human clone path"
604 );
605 assert_eq!(
606 index.resolve_clone_path(&own("nel"), &path("3mizfnpxii522")),
607 Resolved::Ready(Some(repo.clone())),
608 "the record key still resolves"
609 );
610 assert_eq!(
611 index.resolve_clone_path(&own("nel"), &path("substratum.cloud.git")),
612 Resolved::Ready(Some(repo)),
613 "the conventional .git suffix strips before the name lookup"
614 );
615 assert_eq!(
616 index.resolve_clone_path(&own("nel"), &path("periwinkle")),
617 Resolved::Ready(None)
618 );
619}
620
621#[test]
622fn a_record_key_outranks_another_repos_name() {
623 let world = World::new();
624 let by_name = repo_did("limpet");
625 let by_rkey = repo_did("mussel");
626
627 let meta = Repo::open(&world.meta_path).unwrap();
628 let store = CobStore::new(&meta);
629 let object = store
630 .create(
631 &meta_home(),
632 &RegistryChange::Register(named_registration("nel", "limpet", "mussel", &by_name, 1)),
633 &world.signer,
634 at(1),
635 )
636 .unwrap()
637 .object;
638 store
639 .update(
640 &meta_home(),
641 object,
642 &RegistryChange::Register(named_registration("nel", "mussel", "scallop", &by_rkey, 2)),
643 &world.signer,
644 at(2),
645 )
646 .unwrap();
647
648 let index = world.index();
649 index.rebuild().unwrap();
650
651 assert_eq!(
652 index.resolve_clone_path(&own("nel"), &path("mussel")),
653 Resolved::Ready(Some(by_rkey)),
654 "a record key match wins over another repo holding that string as its name"
655 );
656 assert_eq!(
657 index.resolve_clone_path(&own("nel"), &path("scallop")),
658 Resolved::Ready(Some(repo_did("mussel")))
659 );
660}
661
662#[test]
663fn a_name_two_repos_share_resolves_to_the_older_registration() {
664 let world = World::new();
665 let first = repo_did("whelk");
666 let second = repo_did("conch");
667
668 let meta = Repo::open(&world.meta_path).unwrap();
669 let store = CobStore::new(&meta);
670 let object = store
671 .create(
672 &meta_home(),
673 &RegistryChange::Register(named_registration(
674 "nel",
675 "3lubrptx57d22",
676 "kelp",
677 &first,
678 1,
679 )),
680 &world.signer,
681 at(1),
682 )
683 .unwrap()
684 .object;
685 store
686 .update(
687 &meta_home(),
688 object,
689 &RegistryChange::Register(named_registration(
690 "nel",
691 "3mqydma3re27z",
692 "kelp",
693 &second,
694 2,
695 )),
696 &world.signer,
697 at(2),
698 )
699 .unwrap();
700
701 let index = world.index();
702 index.rebuild().unwrap();
703
704 assert_eq!(
705 index.resolve_clone_path(&own("nel"), &path("kelp")),
706 Resolved::Ready(Some(first.clone())),
707 "a contested name resolves to whichever repo registered first, ordered by \
708 created_at so a cold seed and an incremental replay agree"
709 );
710 assert_eq!(
711 index.resolve_clone_path(&own("nel"), &path("3lubrptx57d22")),
712 Resolved::Ready(Some(first)),
713 "each record key stays unambiguous"
714 );
715 assert_eq!(
716 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")),
717 Resolved::Ready(Some(second)),
718 "the repo that lost the name is still reachable by its record key"
719 );
720}
721
722#[test]
723fn a_repo_registered_later_never_takes_a_contested_name_by_renaming_onto_it() {
724 let world = World::new();
725 let holder = repo_did("whelk");
726 let latecomer = repo_did("conch");
727
728 let meta = Repo::open(&world.meta_path).unwrap();
729 let store = CobStore::new(&meta);
730 let object = store
731 .create(
732 &meta_home(),
733 &RegistryChange::Register(named_registration(
734 "nel",
735 "3lubrptx57d22",
736 "kelp",
737 &holder,
738 1,
739 )),
740 &world.signer,
741 at(1),
742 )
743 .unwrap()
744 .object;
745 store
746 .update(
747 &meta_home(),
748 object,
749 &RegistryChange::Register(named_registration(
750 "nel",
751 "3mqydma3re27z",
752 "uni",
753 &latecomer,
754 2,
755 )),
756 &world.signer,
757 at(2),
758 )
759 .unwrap();
760
761 let index = world.index();
762 index.rebuild().unwrap();
763
764 store
765 .update(
766 &meta_home(),
767 object,
768 &RegistryChange::Rename(Rename {
769 owner: own("nel"),
770 rkey: rkey("3mqydma3re27z"),
771 name: RepoName::new("kelp").unwrap(),
772 repo: latecomer.clone(),
773 }),
774 &world.signer,
775 at(3),
776 )
777 .unwrap();
778 index.refresh_registry().unwrap();
779
780 assert_eq!(
781 index.resolve_clone_path(&own("nel"), &path("kelp")),
782 Resolved::Ready(Some(holder)),
783 "a rename keeps the repo's original created_at, so renaming onto a name \
784 another repo registered earlier cannot take it"
785 );
786 assert_eq!(
787 index.resolve_clone_path(&own("nel"), &path("uni")),
788 Resolved::Ready(None),
789 "the renamed repo's previous name stops resolving"
790 );
791 assert_eq!(
792 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")),
793 Resolved::Ready(Some(latecomer))
794 );
795}
796
797#[test]
798fn deregistering_the_older_registration_moves_a_shared_name_to_the_survivor() {
799 let world = World::new();
800 let first = repo_did("whelk");
801 let survivor = repo_did("conch");
802
803 let meta = Repo::open(&world.meta_path).unwrap();
804 let store = CobStore::new(&meta);
805 let object = store
806 .create(
807 &meta_home(),
808 &RegistryChange::Register(named_registration(
809 "nel",
810 "3lubrptx57d22",
811 "kelp",
812 &first,
813 1,
814 )),
815 &world.signer,
816 at(1),
817 )
818 .unwrap()
819 .object;
820 store
821 .update(
822 &meta_home(),
823 object,
824 &RegistryChange::Register(named_registration(
825 "nel",
826 "3mqydma3re27z",
827 "kelp",
828 &survivor,
829 2,
830 )),
831 &world.signer,
832 at(2),
833 )
834 .unwrap();
835
836 let index = world.index();
837 index.rebuild().unwrap();
838 assert_eq!(
839 index.resolve_clone_path(&own("nel"), &path("kelp")),
840 Resolved::Ready(Some(first)),
841 "the name resolves to the older registration while both exist"
842 );
843
844 store
845 .update(
846 &meta_home(),
847 object,
848 &RegistryChange::Deregister(RepoRef {
849 owner: own("nel"),
850 rkey: rkey("3lubrptx57d22"),
851 }),
852 &world.signer,
853 at(3),
854 )
855 .unwrap();
856 index.refresh_registry().unwrap();
857
858 assert_eq!(
859 index.resolve_clone_path(&own("nel"), &path("kelp")),
860 Resolved::Ready(Some(survivor)),
861 "the name resolves to the remaining repo once the older one is deregistered"
862 );
863}
864
865#[test]
866fn a_rename_moves_name_resolution_off_the_old_name() {
867 let world = World::new();
868 let repo = repo_did("squid");
869
870 let meta = Repo::open(&world.meta_path).unwrap();
871 let store = CobStore::new(&meta);
872 let object = store
873 .create(
874 &meta_home(),
875 &RegistryChange::Register(named_registration(
876 "nel",
877 "3mizfnpxii522",
878 "anemone",
879 &repo,
880 1,
881 )),
882 &world.signer,
883 at(1),
884 )
885 .unwrap()
886 .object;
887
888 let index = world.index();
889 index.rebuild().unwrap();
890 assert_eq!(
891 index.resolve_clone_path(&own("nel"), &path("anemone")),
892 Resolved::Ready(Some(repo.clone()))
893 );
894
895 store
896 .update(
897 &meta_home(),
898 object,
899 &RegistryChange::Rename(Rename {
900 owner: own("nel"),
901 rkey: rkey("3mizfnpxii522"),
902 name: RepoName::new("barnacle").unwrap(),
903 repo: repo.clone(),
904 }),
905 &world.signer,
906 at(2),
907 )
908 .unwrap();
909 index.refresh_registry().unwrap();
910
911 assert_eq!(
912 index.resolve_clone_path(&own("nel"), &path("barnacle")),
913 Resolved::Ready(Some(repo.clone())),
914 "the new name resolves after a rename that keeps the record key"
915 );
916 assert_eq!(
917 index.resolve_clone_path(&own("nel"), &path("anemone")),
918 Resolved::Ready(None),
919 "the superseded name stops resolving"
920 );
921 assert_eq!(
922 index.resolve_clone_path(&own("nel"), &path("3mizfnpxii522")),
923 Resolved::Ready(Some(repo))
924 );
925}
926
927#[test]
928fn a_rename_that_changes_only_the_record_key_keeps_the_name_resolving() {
929 let world = World::new();
930 let repo = repo_did("squid");
931
932 let meta = Repo::open(&world.meta_path).unwrap();
933 let store = CobStore::new(&meta);
934 let object = store
935 .create(
936 &meta_home(),
937 &RegistryChange::Register(named_registration("nel", "3lubrptx57d22", "kelp", &repo, 1)),
938 &world.signer,
939 at(1),
940 )
941 .unwrap()
942 .object;
943
944 let index = world.index();
945 index.rebuild().unwrap();
946
947 store
948 .update(
949 &meta_home(),
950 object,
951 &RegistryChange::Rename(Rename {
952 owner: own("nel"),
953 rkey: rkey("3mqydma3re27z"),
954 name: RepoName::new("kelp").unwrap(),
955 repo: repo.clone(),
956 }),
957 &world.signer,
958 at(2),
959 )
960 .unwrap();
961 index.refresh_registry().unwrap();
962
963 assert_eq!(
964 index.resolve_clone_path(&own("nel"), &path("kelp")),
965 Resolved::Ready(Some(repo.clone())),
966 "the unchanged name survives a rename that swaps the record key"
967 );
968 assert_eq!(
969 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")),
970 Resolved::Ready(Some(repo.clone()))
971 );
972 assert_eq!(
973 index.resolve_clone_path(&own("nel"), &path("3lubrptx57d22")),
974 Resolved::Ready(Some(repo)),
975 "the superseded record key keeps resolving through its retained alias"
976 );
977}
978
979#[test]
980fn seeding_and_replaying_agree_on_name_resolution() {
981 let world = World::new();
982 let first = repo_did("whelk");
983 let second = repo_did("conch");
984
985 let meta = Repo::open(&world.meta_path).unwrap();
986 let store = CobStore::new(&meta);
987 let object = store
988 .create(
989 &meta_home(),
990 &RegistryChange::Register(named_registration(
991 "nel",
992 "3lubrptx57d22",
993 "kelp",
994 &first,
995 1,
996 )),
997 &world.signer,
998 at(1),
999 )
1000 .unwrap()
1001 .object;
1002
1003 let replayed = world.index();
1004 replayed.rebuild().unwrap();
1005
1006 store
1007 .update(
1008 &meta_home(),
1009 object,
1010 &RegistryChange::Register(named_registration(
1011 "nel",
1012 "3mqydma3re27z",
1013 "uni",
1014 &second,
1015 2,
1016 )),
1017 &world.signer,
1018 at(2),
1019 )
1020 .unwrap();
1021 replayed.refresh_registry().unwrap();
1022
1023 let seeded = world.index();
1024 seeded.rebuild().unwrap();
1025
1026 ["kelp", "uni", "3lubrptx57d22", "3mqydma3re27z", "nautilus"]
1027 .into_iter()
1028 .for_each(|segment| {
1029 assert_eq!(
1030 replayed.resolve_clone_path(&own("nel"), &path(segment)),
1031 seeded.resolve_clone_path(&own("nel"), &path(segment)),
1032 "incremental replay and a cold seed disagree on {segment}"
1033 );
1034 });
1035}
1036
1037#[test]
1038fn a_name_differing_from_its_record_key_only_by_case_resolves() {
1039 let world = World::new();
1040 let repo = repo_did("squid");
1041
1042 let meta = Repo::open(&world.meta_path).unwrap();
1043 let store = CobStore::new(&meta);
1044 store
1045 .create(
1046 &meta_home(),
1047 &RegistryChange::Register(named_registration(
1048 "nel",
1049 "runic_lang",
1050 "Runic_lang",
1051 &repo,
1052 1,
1053 )),
1054 &world.signer,
1055 at(1),
1056 )
1057 .unwrap();
1058
1059 let index = world.index();
1060 index.rebuild().unwrap();
1061
1062 assert_eq!(
1063 index.resolve_clone_path(&own("nel"), &path("Runic_lang")),
1064 Resolved::Ready(Some(repo.clone())),
1065 "the appview lowercases the record key but keeps the display name's case, \
1066 so the mixed-case path resolves by name"
1067 );
1068 assert_eq!(
1069 index.resolve_clone_path(&own("nel"), &path("runic_lang")),
1070 Resolved::Ready(Some(repo)),
1071 "the lowercased record key still resolves"
1072 );
1073}