This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-index / tests / projections.rs
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}