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
50 kB 1612 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::{ 8 Coverage, HostedCoverage, IndexError, KeptAccounts, KeyBudget, KeyRecord, KeyReprieve, 9 KeyReprieved, KeyTtl, OfferedKey, Resolved, SweepFloor, 10}; 11use knot_types::{ClonePath, RefName, RepoName}; 12use serde::{Deserialize, Serialize}; 13 14mod common; 15use common::{ 16 World, acc, at, grant, meta_home, named_registration, own, registration, repo_did, rkey, 17}; 18 19#[derive(Serialize, Deserialize)] 20#[serde(tag = "op", content = "data", rename_all = "snake_case")] 21enum BadMembers { 22 Explode(u8), 23} 24impl ChangePayload for BadMembers { 25 const TYPE: &'static str = "sh.tangled.knot.member"; 26} 27 28#[test] 29fn a_concurrent_reader_never_sees_a_net_absent_subject() { 30 let world = World::new(); 31 let object = world.seed_members(); 32 let index = Arc::new(world.index()); 33 index.rebuild().unwrap(); 34 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false)); 35 36 let meta = Repo::open(&world.meta_path).unwrap(); 37 let store = CobStore::new(&meta); 38 store 39 .update( 40 &meta_home(), 41 object, 42 &MembersChange::Add(grant("teq", "nel", 10)), 43 &world.signer, 44 at(10), 45 ) 46 .unwrap(); 47 (0..1000).for_each(|i| { 48 store 49 .update( 50 &meta_home(), 51 object, 52 &MembersChange::Add(grant(&format!("f{i}"), "nel", 11 + i)), 53 &world.signer, 54 at(11 + i), 55 ) 56 .unwrap(); 57 }); 58 store 59 .update( 60 &meta_home(), 61 object, 62 &MembersChange::Remove(Removal { 63 subject: acc("teq"), 64 }), 65 &world.signer, 66 at(20_000), 67 ) 68 .unwrap(); 69 70 let done = Arc::new(AtomicBool::new(false)); 71 let leaked = Arc::new(AtomicBool::new(false)); 72 std::thread::scope(|scope| { 73 let reader = Arc::clone(&index); 74 let reader_done = Arc::clone(&done); 75 let reader_leaked = Arc::clone(&leaked); 76 scope.spawn(move || { 77 while !reader_done.load(Ordering::Acquire) { 78 if reader.is_member(&acc("teq")) == Resolved::Ready(true) { 79 reader_leaked.store(true, Ordering::Release); 80 } 81 } 82 }); 83 index.refresh_members().unwrap(); 84 done.store(true, Ordering::Release); 85 }); 86 87 assert!( 88 !leaked.load(Ordering::Acquire), 89 "net-absent subject is never written, so no reader can observe it mid-delta" 90 ); 91 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false)); 92 assert_eq!(index.is_member(&acc("f0")), Resolved::Ready(true)); 93 assert_eq!(index.is_member(&acc("f999")), Resolved::Ready(true)); 94} 95 96#[test] 97fn a_concurrent_reader_never_sees_a_collaborator_roster_emptied_mid_refresh() { 98 let world = World::new(); 99 let repo = repo_did("squid"); 100 let git = world.layout.create(&repo).unwrap(); 101 let store = CobStore::new(&git); 102 let object = store 103 .create( 104 &CobHome::from(&repo), 105 &CollaboratorsChange::Add(grant("anchor", "nel", 1)), 106 &world.signer, 107 at(1), 108 ) 109 .unwrap() 110 .object; 111 (0..128).for_each(|i| { 112 store 113 .update( 114 &CobHome::from(&repo), 115 object, 116 &CollaboratorsChange::Add(grant(&format!("c{i}"), "nel", 2 + i)), 117 &world.signer, 118 at(2 + i), 119 ) 120 .unwrap(); 121 }); 122 123 let index = Arc::new(world.index()); 124 index.rebuild().unwrap(); 125 index.ensure_collaborators(&repo).unwrap(); 126 assert_eq!( 127 index.is_collaborator(&repo, &acc("anchor")), 128 Resolved::Ready(true) 129 ); 130 131 let done = Arc::new(AtomicBool::new(false)); 132 let leaked = Arc::new(AtomicBool::new(false)); 133 std::thread::scope(|scope| { 134 let reader = Arc::clone(&index); 135 let reader_done = Arc::clone(&done); 136 let reader_leaked = Arc::clone(&leaked); 137 let target = repo.clone(); 138 scope.spawn(move || { 139 while !reader_done.load(Ordering::Acquire) { 140 if reader.is_collaborator(&target, &acc("anchor")) != Resolved::Ready(true) { 141 reader_leaked.store(true, Ordering::Release); 142 } 143 } 144 }); 145 (0..500).for_each(|_| index.refresh_collaborators(&repo).unwrap()); 146 done.store(true, Ordering::Release); 147 }); 148 149 assert!( 150 !leaked.load(Ordering::Acquire), 151 "in-place mem::take runs under per-repo lock, so anchor present in both \ 152 pre- and post-refresh roster is never observed absent or warming mid-refresh" 153 ); 154 assert_eq!( 155 index.is_collaborator(&repo, &acc("anchor")), 156 Resolved::Ready(true) 157 ); 158} 159 160fn cob_ref(type_name: &str, object: CobId) -> RefName { 161 RefName::new(format!("refs/cobs/{type_name}/{}", object.oid())).unwrap() 162} 163 164#[test] 165fn a_diverged_collaborators_tip_purges_the_roster_instead_of_serving_it_stale() { 166 let world = World::new(); 167 let repo = repo_did("squid"); 168 let git = world.layout.create(&repo).unwrap(); 169 let store = CobStore::new(&git); 170 let created = store 171 .create( 172 &CobHome::from(&repo), 173 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 174 &world.signer, 175 at(1), 176 ) 177 .unwrap(); 178 let tip = store 179 .update( 180 &CobHome::from(&repo), 181 created.object, 182 &CollaboratorsChange::Add(grant("bailey", "nel", 2)), 183 &world.signer, 184 at(2), 185 ) 186 .unwrap(); 187 188 let index = world.index(); 189 index.rebuild().unwrap(); 190 index.ensure_collaborators(&repo).unwrap(); 191 assert_eq!( 192 index.is_collaborator(&repo, &acc("lyna")), 193 Resolved::Ready(true) 194 ); 195 196 git.update_ref(&RefUpdate::Update { 197 name: cob_ref(CollaboratorsChange::TYPE, created.object), 198 old: tip.oid(), 199 new: created.object.oid(), 200 }) 201 .unwrap(); 202 203 assert!( 204 index.refresh_collaborators(&repo).is_err(), 205 "tip that no longer descends from folded tip is structural error" 206 ); 207 assert_eq!( 208 index.is_collaborator(&repo, &acc("lyna")), 209 Resolved::Warming, 210 "diverged COB tip purges roster and fails closed, it does not serve \ 211 pre-divergence collaborators" 212 ); 213} 214 215#[test] 216fn a_diverged_members_tip_fails_closed_to_warming() { 217 let world = World::new(); 218 let object = world.seed_members(); 219 let meta = Repo::open(&world.meta_path).unwrap(); 220 let store = CobStore::new(&meta); 221 let tip = store 222 .update( 223 &meta_home(), 224 object, 225 &MembersChange::Add(grant("teq", "nel", 3)), 226 &world.signer, 227 at(3), 228 ) 229 .unwrap(); 230 231 let index = world.index(); 232 index.rebuild().unwrap(); 233 assert_eq!(index.is_member(&acc("nel")), Resolved::Ready(true)); 234 235 meta.update_ref(&RefUpdate::Update { 236 name: cob_ref(MembersChange::TYPE, object), 237 old: tip.oid(), 238 new: object.oid(), 239 }) 240 .unwrap(); 241 242 assert!( 243 index.refresh_members().is_err(), 244 "tip that no longer descends from folded tip is structural error" 245 ); 246 assert_eq!(index.coverage().members, Coverage::Warming); 247 assert_eq!( 248 index.is_member(&acc("nel")), 249 Resolved::Warming, 250 "diverged members COB fails projection closed instead of serving stale members" 251 ); 252} 253 254#[test] 255fn an_undecodable_change_fails_closed_with_no_partial_apply() { 256 let world = World::new(); 257 let object = world.seed_members(); 258 let index = world.index(); 259 index.rebuild().unwrap(); 260 261 let meta = Repo::open(&world.meta_path).unwrap(); 262 let store = CobStore::new(&meta); 263 store 264 .update( 265 &meta_home(), 266 object, 267 &MembersChange::Add(grant("teq", "nel", 3)), 268 &world.signer, 269 at(3), 270 ) 271 .unwrap(); 272 store 273 .update( 274 &meta_home(), 275 object, 276 &BadMembers::Explode(0), 277 &world.signer, 278 at(4), 279 ) 280 .unwrap(); 281 store 282 .update( 283 &meta_home(), 284 object, 285 &MembersChange::Remove(Removal { 286 subject: acc("teq"), 287 }), 288 &world.signer, 289 at(5), 290 ) 291 .unwrap(); 292 293 assert!(matches!( 294 index.refresh_members(), 295 Err(IndexError::Decode { .. }) 296 )); 297 298 assert_eq!(index.coverage().members, Coverage::Warming); 299 assert_eq!( 300 index.is_member(&acc("teq")), 301 Resolved::Warming, 302 "no partial apply: teq from pre-error change was never committed" 303 ); 304 assert_eq!( 305 index.is_member(&acc("nel")), 306 Resolved::Warming, 307 "structurally broken COB fails whole projection closed" 308 ); 309 310 assert!(matches!( 311 index.refresh_members(), 312 Err(IndexError::Decode { .. }) 313 )); 314 assert_eq!(index.coverage().members, Coverage::Warming); 315} 316 317#[test] 318fn deregister_purges_collaborators_fail_closed() { 319 let world = World::new(); 320 let repo = repo_did("clam"); 321 322 let meta = Repo::open(&world.meta_path).unwrap(); 323 let store = CobStore::new(&meta); 324 let registry = store 325 .create( 326 &meta_home(), 327 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)), 328 &world.signer, 329 at(1), 330 ) 331 .unwrap() 332 .object; 333 334 let git = world.layout.create(&repo).unwrap(); 335 let cstore = CobStore::new(&git); 336 cstore 337 .create( 338 &CobHome::from(&repo), 339 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 340 &world.signer, 341 at(1), 342 ) 343 .unwrap(); 344 345 let index = world.index(); 346 index.rebuild().unwrap(); 347 index.warm_collaborators(); 348 assert_eq!( 349 index.is_collaborator(&repo, &acc("lyna")), 350 Resolved::Ready(true) 351 ); 352 353 store 354 .update( 355 &meta_home(), 356 registry, 357 &RegistryChange::Deregister(RepoRef { 358 owner: own("nel"), 359 rkey: rkey("anemone"), 360 }), 361 &world.signer, 362 at(2), 363 ) 364 .unwrap(); 365 index.refresh_registry().unwrap(); 366 367 assert_eq!( 368 index.resolve_repo(&own("nel"), &rkey("anemone")), 369 Resolved::Ready(None) 370 ); 371 assert_eq!( 372 index.is_collaborator(&repo, &acc("lyna")), 373 Resolved::Warming, 374 "deregistered repo's collaborators are purged and fail closed, not served stale" 375 ); 376} 377 378#[test] 379fn a_renamed_repo_keeps_both_rkeys_and_its_collaborators() { 380 let world = World::new(); 381 let repo = repo_did("squid"); 382 383 let meta = Repo::open(&world.meta_path).unwrap(); 384 let store = CobStore::new(&meta); 385 let registry = store 386 .create( 387 &meta_home(), 388 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)), 389 &world.signer, 390 at(1), 391 ) 392 .unwrap() 393 .object; 394 395 let git = world.layout.create(&repo).unwrap(); 396 CobStore::new(&git) 397 .create( 398 &CobHome::from(&repo), 399 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 400 &world.signer, 401 at(1), 402 ) 403 .unwrap(); 404 405 let index = world.index(); 406 index.rebuild().unwrap(); 407 index.warm_collaborators(); 408 assert_eq!(index.rkey_of(&repo), Resolved::Ready(Some(rkey("anemone")))); 409 410 store 411 .update( 412 &meta_home(), 413 registry, 414 &RegistryChange::Rename(Rename { 415 owner: own("nel"), 416 rkey: rkey("barnacle"), 417 name: RepoName::new("barnacle").unwrap(), 418 repo: repo.clone(), 419 }), 420 &world.signer, 421 at(2), 422 ) 423 .unwrap(); 424 index.refresh_registry().unwrap(); 425 426 assert_eq!( 427 index.resolve_repo(&own("nel"), &rkey("anemone")), 428 Resolved::Ready(Some(repo.clone())), 429 "prior rkey keeps resolving as alias after rename is delta-applied" 430 ); 431 assert_eq!( 432 index.resolve_repo(&own("nel"), &rkey("barnacle")), 433 Resolved::Ready(Some(repo.clone())) 434 ); 435 assert_eq!( 436 index.rkey_of(&repo), 437 Resolved::Ready(Some(rkey("barnacle"))), 438 "new rkey is canonical" 439 ); 440 assert_eq!( 441 index.is_collaborator(&repo, &acc("lyna")), 442 Resolved::Ready(true), 443 "rename never evacuates repo, so its collaborators survive" 444 ); 445 446 store 447 .update( 448 &meta_home(), 449 registry, 450 &RegistryChange::Deregister(RepoRef { 451 owner: own("nel"), 452 rkey: rkey("anemone"), 453 }), 454 &world.signer, 455 at(3), 456 ) 457 .unwrap(); 458 index.refresh_registry().unwrap(); 459 assert_eq!( 460 index.resolve_repo(&own("nel"), &rkey("barnacle")), 461 Resolved::Ready(None), 462 "deregistering through retained alias removes repo and every alias" 463 ); 464 assert_eq!( 465 index.is_collaborator(&repo, &acc("lyna")), 466 Resolved::Warming, 467 "deregistered repo's collaborators are evacuated" 468 ); 469} 470 471#[test] 472fn a_repo_moved_within_one_delta_is_not_evacuated() { 473 let world = World::new(); 474 let repo = repo_did("squid"); 475 476 let meta = Repo::open(&world.meta_path).unwrap(); 477 let store = CobStore::new(&meta); 478 let registry = store 479 .create( 480 &meta_home(), 481 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)), 482 &world.signer, 483 at(1), 484 ) 485 .unwrap() 486 .object; 487 488 let git = world.layout.create(&repo).unwrap(); 489 CobStore::new(&git) 490 .create( 491 &CobHome::from(&repo), 492 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 493 &world.signer, 494 at(1), 495 ) 496 .unwrap(); 497 498 let index = world.index(); 499 index.rebuild().unwrap(); 500 index.warm_collaborators(); 501 assert_eq!( 502 index.is_collaborator(&repo, &acc("lyna")), 503 Resolved::Ready(true) 504 ); 505 506 store 507 .update( 508 &meta_home(), 509 registry, 510 &RegistryChange::Deregister(RepoRef { 511 owner: own("nel"), 512 rkey: rkey("anemone"), 513 }), 514 &world.signer, 515 at(2), 516 ) 517 .unwrap(); 518 store 519 .update( 520 &meta_home(), 521 registry, 522 &RegistryChange::Register(registration("nel", "barnacle", &repo, 3)), 523 &world.signer, 524 at(3), 525 ) 526 .unwrap(); 527 index.refresh_registry().unwrap(); 528 529 assert_eq!( 530 index.resolve_repo(&own("nel"), &rkey("anemone")), 531 Resolved::Ready(None) 532 ); 533 assert_eq!( 534 index.resolve_repo(&own("nel"), &rkey("barnacle")), 535 Resolved::Ready(Some(repo.clone())) 536 ); 537 assert_eq!( 538 index.is_collaborator(&repo, &acc("lyna")), 539 Resolved::Ready(true), 540 "deregister and re-register within single delta leaves repo hosted, so its collaborators survive" 541 ); 542} 543 544#[test] 545fn recording_and_retaining_track_who_still_publishes_each_key() { 546 let world = World::new(); 547 let index = world.index(); 548 let key = |i: u32| OfferedKey::from_bytes(i.to_le_bytes().to_vec()); 549 550 index 551 .keys() 552 .record(&acc("nel"), vec![key(0), key(1)], hour(0)); 553 index 554 .keys() 555 .record(&acc("nel"), vec![key(1), key(2)], hour(0)); 556 assert_eq!( 557 index.owner_of_key(&key(0), at(0)), 558 Resolved::Ready(None), 559 "a key the account stopped publishing stops resolving to it" 560 ); 561 assert_eq!( 562 index.owner_of_key(&key(1), at(0)), 563 Resolved::Ready(Some(acc("nel"))), 564 "a key present in both sets survives the replacement" 565 ); 566 567 index 568 .keys() 569 .record(&acc("cuttle"), vec![key(1), key(3)], hour(0)); 570 assert_eq!( 571 index 572 .keys() 573 .publisher_among(&[acc("cuttle")], &key(1), at(0)), 574 Some(acc("cuttle")), 575 "a deploy key two accounts publish must answer for the candidate that may push here, \ 576 whichever of them the knot happened to read first" 577 ); 578 assert_eq!( 579 index.keys().publisher_among(&[acc("teq")], &key(1), at(0)), 580 None, 581 "the key mustn't answer for an account that doesn't publish it" 582 ); 583 584 index.keys().retain(&KeptAccounts::new(vec![acc("nel")])); 585 assert_eq!( 586 index.owner_of_key(&key(3), at(0)), 587 Resolved::Ready(None), 588 "an account outside the grant set has its keys released" 589 ); 590 assert_eq!( 591 index.owner_of_key(&key(1), at(0)), 592 Resolved::Ready(Some(acc("nel"))), 593 "one account leaving the grant set mustn't take a shared deploy key away from the \ 594 account that still publishes it" 595 ); 596 597 index.keys().record(&acc("nel"), vec![key(2)], hour(0)); 598 assert_eq!( 599 index.owner_of_key(&key(1), at(0)), 600 Resolved::Ready(None), 601 "with its last publisher no longer offering it, the key stops answering for anybody" 602 ); 603} 604 605#[test] 606fn a_full_budget_reports_unheld_readings_and_then_refuses_to_grow_at_all() { 607 let world = World::new(); 608 let index = world.index_within(KeyBudget::from_bytes(560)); 609 let fat = |seed: u8| OfferedKey::from_bytes(vec![seed; 48]); 610 611 assert_eq!( 612 index.keys().record(&acc("nel"), vec![fat(1)], hour(0)), 613 KeyRecord::Stored 614 ); 615 assert!( 616 !index.keys().any_unheld(), 617 "a set that kept every reading it took can vouch for its misses" 618 ); 619 620 assert_eq!( 621 index.keys().record(&acc("cuttle"), vec![fat(2)], hour(0)), 622 KeyRecord::Unheld, 623 "a budget too small for another account's keys still has room to record that the \ 624 knot read the account" 625 ); 626 assert!( 627 index.keys().any_unheld(), 628 "a reading the budget couldn't fit means an unknown key may still belong to a \ 629 candidate, so the handshake must defer to the push check instead of refusing" 630 ); 631 assert!( 632 !index.keys().is_fresh(&acc("cuttle"), at(0)), 633 "the refused account stays stale, so the fill keeps reporting the set incomplete and \ 634 the knot keeps checking its pushes against its PDS" 635 ); 636 assert_eq!( 637 index.owner_of_key(&fat(2), at(0)), 638 Resolved::Ready(None), 639 "a key the budget refused mustn't answer for anybody" 640 ); 641 642 assert_eq!( 643 index.keys().record(&acc("teq"), vec![fat(3)], hour(0)), 644 KeyRecord::Saturated, 645 "a knot whose grant set publishes more key bytes than it budgeted for must stop growing" 646 ); 647 assert_eq!( 648 index.owner_of_key(&fat(1), at(0)), 649 Resolved::Ready(Some(acc("nel"))), 650 "refusing the accounts that didn't fit mustn't release the accounts already on file" 651 ); 652 653 index.keys().retain(&KeptAccounts::new(vec![acc("cuttle")])); 654 assert!( 655 index.keys().any_unheld(), 656 "releasing a different account mustn't clear the report while the unheld reading stays" 657 ); 658 assert_eq!( 659 index.keys().record(&acc("cuttle"), vec![fat(2)], hour(0)), 660 KeyRecord::Stored, 661 "the budget frees up with the accounts that leave the grant set" 662 ); 663 assert!( 664 !index.keys().any_unheld(), 665 "with every reading back on file the handshake can refuse unknown keys outright" 666 ); 667 668 assert_eq!( 669 index.keys().record(&acc("teq"), vec![fat(3)], hour(0)), 670 KeyRecord::Unheld 671 ); 672 assert_eq!( 673 index 674 .keys() 675 .reprieve(&acc("teq"), at(21_601), grace(), hour(21_601)), 676 KeyReprieved::Exhausted 677 ); 678 assert!( 679 !index.keys().any_unheld(), 680 "an account the knot gave up rereading is recorded with an empty key set, so its \ 681 unheld reading mustn't keep the handshake open" 682 ); 683} 684 685#[test] 686fn a_pass_renews_at_the_lease_halfway_and_rereads_once_per_miss_outside_the_floor() { 687 let (_world, index) = folded(); 688 index 689 .keys() 690 .record(&acc("nel"), vec![OfferedKey::from_bytes(vec![7])], hour(0)); 691 692 assert!( 693 matches!(index.keys().work(at(0), sweep()), Resolved::Ready(work) 694 if work.due.is_empty() && work.suspected.is_empty()), 695 "with every account's keys read inside the ttl the fill won't fetch anything" 696 ); 697 698 index.keys().note_miss(); 699 let Resolved::Ready(work) = index.keys().work(at(100), sweep()) else { 700 panic!("the registry is folded, so the knot knows who may push"); 701 }; 702 assert_eq!( 703 work.suspected.len(), 704 1, 705 "a key the accounts on file don't publish is the only evidence the knot gets that an \ 706 account published a new key, so the next pass must reread rather than wait out the ttl" 707 ); 708 assert!( 709 work.due.is_empty(), 710 "a reread a stranger asked for must stay separate from the reads coverage waits on, or \ 711 the knot spends a whole pass on it without renewing anything" 712 ); 713 assert!( 714 matches!(index.keys().work(at(100), sweep()), Resolved::Ready(work) if work.suspected.is_empty()), 715 "one miss is worth one reread, so a stranger offering keys can't set the fill's pace" 716 ); 717 718 index.keys().note_miss(); 719 assert!( 720 matches!(index.keys().work(at(159), sweep()), Resolved::Ready(work) if work.suspected.is_empty()), 721 "a stranger can offer an unsigned key for free, so a knot that swept a moment ago \ 722 mustn't spend another read at every PDS in the grant set" 723 ); 724 assert!( 725 matches!(index.keys().work(at(160), sweep()), Resolved::Ready(work) if work.suspected.len() == 1), 726 "the miss the floor delayed is honored once the floor has passed, so a key published \ 727 between passes is still picked up" 728 ); 729 730 assert!( 731 matches!(index.keys().work(at(1_799), sweep()), Resolved::Ready(work) if work.due.is_empty()), 732 "an account inside the first half of its lease is left alone" 733 ); 734 let Resolved::Ready(work) = index.keys().work(at(1_800), sweep()) else { 735 panic!("the registry is folded, so the knot knows who may push"); 736 }; 737 assert_eq!( 738 work.due.len(), 739 1, 740 "the fill renews at the halfway mark, so a pass no longer than the lease it is working \ 741 to finishes before anything expires" 742 ); 743 assert!( 744 work.complete, 745 "the lease the account still has answers for it, so a knot busy renewing keeps \ 746 refusing keys the accounts on file don't publish instead of opening the handshake \ 747 to every stranger" 748 ); 749} 750 751#[test] 752fn an_unfolded_repo_keeps_the_pass_partial_and_an_unopenable_repo_never_grants() { 753 let world = World::new(); 754 let folded = repo_did("squid"); 755 let never_created = repo_did("limpet"); 756 world.layout.create(&folded).unwrap(); 757 let registry = world.seed_registry(&folded); 758 world.register_owned(&never_created, "limpet", "cuttle", registry); 759 let index = world.index(); 760 index.refresh_registry().unwrap(); 761 index.refresh_collaborators(&folded).unwrap(); 762 763 let Resolved::Ready(work) = index.keys().work(at(0), sweep()) else { 764 panic!("one repo the knot hasn't folded mustn't stop it filling the keys it can enumerate"); 765 }; 766 assert_eq!( 767 work.hosted, 768 HostedCoverage::Partial { unread: 1 }, 769 "the pass must know it read less than the whole roll, or it evicts the accounts it \ 770 couldn't enumerate" 771 ); 772 assert!( 773 !work.due.is_empty(), 774 "the owner of the repo that did fold still needs its keys read" 775 ); 776 assert!( 777 !work.complete, 778 "a partial roll mustn't let the fill call the set complete, or the knot refuses a \ 779 pusher whose repo it never managed to read" 780 ); 781 782 assert_eq!( 783 index.warm_collaborators(), 784 1, 785 "the repo registered without ever being created is what the knot can't open" 786 ); 787 let Resolved::Ready(work) = index.keys().work(at(0), sweep()) else { 788 panic!("the registry is folded, so the knot knows who may push"); 789 }; 790 assert_eq!( 791 work.hosted, 792 HostedCoverage::Whole, 793 "a repo the knot can't open won't serve a push, so counting it as unread would leave \ 794 the set warming and the handshake open to every stranger for good" 795 ); 796 assert_eq!( 797 work.pushers.as_slice(), 798 [acc("nel")], 799 "only the repo the knot can open grants anybody" 800 ); 801 802 index 803 .keys() 804 .record(&acc("nel"), vec![OfferedKey::from_bytes(vec![7])], hour(0)); 805 assert!( 806 pushers_complete(&index, at(0)), 807 "with the one readable repo's owner on file the set is complete, so the knot can go \ 808 back to refusing keys the accounts on file don't publish" 809 ); 810} 811 812#[test] 813fn an_acl_write_puts_the_key_set_back_to_warming_even_mid_pass() { 814 let (world, index) = folded(); 815 index 816 .keys() 817 .record(&acc("nel"), vec![OfferedKey::from_bytes(vec![7])], hour(0)); 818 let Resolved::Ready(work) = index.keys().work(at(0), sweep()) else { 819 panic!("the registry is folded, so the knot knows who may push"); 820 }; 821 assert!(work.complete, "the one pusher's keys are on file"); 822 index.keys().mark_ready(work.generation); 823 assert_eq!(index.keys().coverage(), Coverage::Ready); 824 825 let roll = world.seed_members(); 826 index.refresh_members().unwrap(); 827 assert_eq!( 828 index.keys().coverage(), 829 Coverage::Warming, 830 "the knot mustn't check a grant it has never read keys for against a set it calls \ 831 complete, or a new collaborator is refused until the next fill pass" 832 ); 833 834 let Resolved::Ready(rereading) = index.keys().work(at(0), sweep()) else { 835 panic!("the registry is folded, so the knot knows who may push"); 836 }; 837 world.add_member(roll, "teq", 5); 838 index.refresh_members().unwrap(); 839 index.keys().mark_ready(rereading.generation); 840 assert_eq!( 841 index.keys().coverage(), 842 Coverage::Warming, 843 "a pass claims the grant set it read, so a grant written while it was reading keys \ 844 mustn't count as covered, or whoever it grants is refused at the handshake" 845 ); 846} 847 848#[test] 849fn an_account_the_budget_cant_fit_is_checked_against_its_pds_without_delaying_coverage() { 850 let world = World::new(); 851 let folded = repo_did("squid"); 852 let second = repo_did("limpet"); 853 world.layout.create(&folded).unwrap(); 854 world.layout.create(&second).unwrap(); 855 let registry = world.seed_registry(&folded); 856 world.register_owned(&second, "limpet", "cuttle", registry); 857 let index = world.index_within(KeyBudget::from_bytes(560)); 858 index.refresh_registry().unwrap(); 859 index.warm_collaborators(); 860 let fat = |seed: u8| OfferedKey::from_bytes(vec![seed; 48]); 861 862 assert_eq!( 863 index.keys().record(&acc("nel"), vec![fat(1)], hour(0)), 864 KeyRecord::Stored 865 ); 866 assert_eq!( 867 index.keys().record(&acc("cuttle"), vec![fat(2)], hour(0)), 868 KeyRecord::Unheld 869 ); 870 assert!( 871 pushers_complete(&index, at(0)), 872 "a full budget mustn't leave the set warming for good, or the knot spends the rest of \ 873 its life accepting every key offered to it" 874 ); 875 let Resolved::Ready(later) = index.keys().work(at(3_601), sweep()) else { 876 panic!("the registry is folded, so the knot knows who may push"); 877 }; 878 assert!( 879 later.due.as_slice().contains(&acc("cuttle")), 880 "the knot tries the account again once the lease runs out, so a budget that frees up \ 881 starts keeping its keys. Every pass until then costs one read rather than a reread \ 882 of the whole grant set" 883 ); 884} 885 886#[test] 887fn a_key_read_outside_its_lease_stops_answering_for_its_publisher() { 888 let world = World::new(); 889 let index = world.index(); 890 let key = OfferedKey::from_bytes(vec![7]); 891 index.keys().record(&acc("nel"), vec![key.clone()], hour(0)); 892 893 assert_eq!( 894 index.owner_of_key(&key, at(3_599)), 895 Resolved::Ready(Some(acc("nel"))), 896 "inside the lease the set answers for the account that published the key" 897 ); 898 assert_eq!( 899 index.keys().publisher_among(&[acc("nel")], &key, at(3_599)), 900 Some(acc("nel")) 901 ); 902 assert_eq!( 903 index.owner_of_key(&key, at(3_601)), 904 Resolved::Ready(None), 905 "past the lease the knot no longer knows the account publishes the key, so a key \ 906 revoked while the fill was behind mustn't keep clearing the handshake" 907 ); 908 assert_eq!( 909 index.keys().publisher_among(&[acc("nel")], &key, at(3_601)), 910 None, 911 "a push check is bound by the same lease, or a key revoked while the fill was behind \ 912 keeps authorizing pushes" 913 ); 914} 915 916#[test] 917fn a_warming_member_roll_still_yields_the_accounts_that_may_push() { 918 let (_world, index) = folded(); 919 920 let Resolved::Ready(work) = index.keys().work(at(0), sweep()) else { 921 panic!("the registry is folded, so the knot knows who may push"); 922 }; 923 assert_eq!(work.pushers.len(), 1, "the one hosted repo has one owner"); 924 assert_eq!( 925 work.due.len(), 926 1, 927 "the owner of the one hosted repo may push and hasn't had its keys read yet" 928 ); 929 assert!( 930 work.members.is_warming(), 931 "an unfolded member roll mustn't stop the knot reading the keys it checks pushes against" 932 ); 933} 934 935#[test] 936fn a_reprieve_coasts_on_the_last_read_until_its_budget_runs_out() { 937 let (_world, index) = folded(); 938 let key = OfferedKey::from_bytes(vec![7]); 939 index.keys().record(&acc("nel"), vec![key.clone()], hour(0)); 940 941 assert_eq!( 942 index 943 .keys() 944 .reprieve(&acc("nel"), at(10), grace(), hour(10)), 945 KeyReprieved::Extended 946 ); 947 assert!( 948 index.keys().is_fresh(&acc("nel"), at(3_599)), 949 "a reread the knot attempted early and couldn't finish mustn't cut the lease it already \ 950 has down to one retry, or a flaky PDS multiplies what the knot reads from it" 951 ); 952 953 assert_eq!( 954 index 955 .keys() 956 .reprieve(&acc("nel"), at(4_000), grace(), hour(4_000)), 957 KeyReprieved::Extended 958 ); 959 assert!( 960 pushers_complete(&index, at(4_000)), 961 "the reprieve keeps the keys the knot last read, so one unreachable PDS mustn't reopen \ 962 the knot to every offered key" 963 ); 964 assert_eq!( 965 index.owner_of_key(&key, at(4_000)), 966 Resolved::Ready(Some(acc("nel"))) 967 ); 968 assert!( 969 !index.keys().is_fresh(&acc("nel"), at(4_301)), 970 "a reprieve is one retry's worth, so the knot tries the account again shortly" 971 ); 972 973 assert_eq!( 974 index 975 .keys() 976 .reprieve(&acc("nel"), at(21_500), grace(), hour(21_500)), 977 KeyReprieved::Extended 978 ); 979 assert!( 980 !index.keys().is_fresh(&acc("nel"), at(21_600)), 981 "the last reprieve before the budget runs out mustn't stretch past it" 982 ); 983 984 assert_eq!( 985 index 986 .keys() 987 .reprieve(&acc("nel"), at(21_601), grace(), hour(21_601)), 988 KeyReprieved::Exhausted, 989 "past the reprieve budget the knot stops coasting on keys it hasn't reread" 990 ); 991 assert_eq!( 992 index.owner_of_key(&key, at(21_601)), 993 Resolved::Ready(None), 994 "a key revoked while its PDS was unreachable mustn't keep authorizing pushes forever" 995 ); 996} 997 998#[test] 999fn an_account_the_knot_never_reads_stops_delaying_the_whole_set() { 1000 let (_world, index) = folded(); 1001 let offered = OfferedKey::from_bytes(vec![7]); 1002 1003 assert_eq!( 1004 index.keys().reprieve(&acc("nel"), at(0), grace(), hour(0)), 1005 KeyReprieved::Pending, 1006 "an account the knot has never managed to read is still unread after a reprieve" 1007 ); 1008 assert!( 1009 !pushers_complete(&index, at(0)), 1010 "an unread account mustn't count toward a complete set, or the knot refuses the keys \ 1011 it has yet to fetch" 1012 ); 1013 assert!( 1014 matches!(index.keys().work(at(299), sweep()), Resolved::Ready(work) if work.due.is_empty()), 1015 "a PDS that just refused the knot is left alone until the retry comes round" 1016 ); 1017 assert!( 1018 matches!(index.keys().work(at(400), sweep()), Resolved::Ready(work) if work.due.len() == 1), 1019 "the knot retries an unread account every reprieve interval rather than every pass" 1020 ); 1021 1022 assert_eq!( 1023 index 1024 .keys() 1025 .reprieve(&acc("nel"), at(21_601), grace(), hour(21_601)), 1026 KeyReprieved::Exhausted, 1027 "the reprieve budget runs from the first failure, so a PDS that never answers stops \ 1028 being a pending read" 1029 ); 1030 assert!( 1031 pushers_complete(&index, at(21_601)), 1032 "one dead account mustn't keep the set warming forever, or the knot spends its whole \ 1033 life accepting every key offered to it" 1034 ); 1035 assert_eq!( 1036 index 1037 .keys() 1038 .publisher_among(&[acc("nel")], &offered, at(21_601)), 1039 None, 1040 "giving up on an account records what the knot knows, an empty key set" 1041 ); 1042} 1043 1044#[test] 1045fn concurrent_reads_of_one_account_never_leave_a_key_answering_for_a_set_it_left() { 1046 let world = World::new(); 1047 let index = Arc::new(world.index()); 1048 let hour = KeyTtl::from_secs(3_600).lease_from(at(0)); 1049 let key = |i: u8| OfferedKey::from_bytes(vec![i]); 1050 1051 (0..2_000u32).for_each(|round| { 1052 let first = key((round % 7) as u8); 1053 let second = key(7 + (round % 7) as u8); 1054 let start = std::sync::Barrier::new(6); 1055 std::thread::scope(|scope| { 1056 (0..6).for_each(|slot| { 1057 let index = Arc::clone(&index); 1058 let keys = match slot % 2 { 1059 0 => vec![first.clone()], 1060 _ => vec![second.clone()], 1061 }; 1062 let start = &start; 1063 scope.spawn(move || { 1064 start.wait(); 1065 index.keys().record(&acc("nel"), keys, hour) 1066 }); 1067 }); 1068 }); 1069 1070 [first, second].iter().for_each(|key| { 1071 let reverse = index.owner_of_key(key, at(0)); 1072 let forward = index.keys().publisher_among(&[acc("nel")], key, at(0)); 1073 assert_eq!( 1074 reverse, 1075 Resolved::Ready(forward), 1076 "a key the account no longer publishes must stop answering for it, or two reads \ 1077 arriving at once leave a revoked key authorizing pushes for good" 1078 ); 1079 }); 1080 }); 1081} 1082 1083fn folded() -> (World, knot_index::Index) { 1084 let world = World::new(); 1085 let repo = repo_did("squid"); 1086 world.layout.create(&repo).unwrap(); 1087 world.seed_registry(&repo); 1088 let index = world.index(); 1089 index.refresh_registry().unwrap(); 1090 index.warm_collaborators(); 1091 (world, index) 1092} 1093 1094fn grace() -> KeyReprieve { 1095 KeyReprieve::from_secs(300, 21_600) 1096} 1097 1098fn hour(from: i64) -> knot_index::KeyLease { 1099 KeyTtl::from_secs(3_600).lease_from(at(from)) 1100} 1101 1102fn sweep() -> SweepFloor { 1103 SweepFloor::from_secs(60) 1104} 1105 1106fn pushers_complete(index: &knot_index::Index, now: knot_types::UnixSeconds) -> bool { 1107 matches!(index.keys().work(now, sweep()), Resolved::Ready(work) if work.complete) 1108} 1109 1110fn path(raw: &str) -> ClonePath { 1111 ClonePath::parse(raw).unwrap() 1112} 1113 1114#[test] 1115fn a_clone_path_resolves_by_name_when_the_record_key_is_a_tid() { 1116 let world = World::new(); 1117 let repo = repo_did("squid"); 1118 1119 let meta = Repo::open(&world.meta_path).unwrap(); 1120 let store = CobStore::new(&meta); 1121 store 1122 .create( 1123 &meta_home(), 1124 &RegistryChange::Register(named_registration( 1125 "nel", 1126 "3mizfnpxii522", 1127 "substratum.cloud", 1128 &repo, 1129 1, 1130 )), 1131 &world.signer, 1132 at(1), 1133 ) 1134 .unwrap(); 1135 1136 let index = world.index(); 1137 index.rebuild().unwrap(); 1138 1139 assert_eq!( 1140 index.resolve_clone_path(&own("nel"), &path("substratum.cloud")), 1141 Resolved::Ready(Some(repo.clone())), 1142 "a PDS-native record key leaves the display name as the only human clone path" 1143 ); 1144 assert_eq!( 1145 index.resolve_clone_path(&own("nel"), &path("3mizfnpxii522")), 1146 Resolved::Ready(Some(repo.clone())), 1147 "the record key still resolves" 1148 ); 1149 assert_eq!( 1150 index.resolve_clone_path(&own("nel"), &path("substratum.cloud.git")), 1151 Resolved::Ready(Some(repo)), 1152 "the conventional .git suffix strips before the name lookup" 1153 ); 1154 assert_eq!( 1155 index.resolve_clone_path(&own("nel"), &path("periwinkle")), 1156 Resolved::Ready(None) 1157 ); 1158} 1159 1160#[test] 1161fn a_record_key_outranks_another_repos_name() { 1162 let world = World::new(); 1163 let by_name = repo_did("limpet"); 1164 let by_rkey = repo_did("mussel"); 1165 1166 let meta = Repo::open(&world.meta_path).unwrap(); 1167 let store = CobStore::new(&meta); 1168 let object = store 1169 .create( 1170 &meta_home(), 1171 &RegistryChange::Register(named_registration("nel", "limpet", "mussel", &by_name, 1)), 1172 &world.signer, 1173 at(1), 1174 ) 1175 .unwrap() 1176 .object; 1177 store 1178 .update( 1179 &meta_home(), 1180 object, 1181 &RegistryChange::Register(named_registration("nel", "mussel", "scallop", &by_rkey, 2)), 1182 &world.signer, 1183 at(2), 1184 ) 1185 .unwrap(); 1186 1187 let index = world.index(); 1188 index.rebuild().unwrap(); 1189 1190 assert_eq!( 1191 index.resolve_clone_path(&own("nel"), &path("mussel")), 1192 Resolved::Ready(Some(by_rkey)), 1193 "a record key match wins over another repo holding that string as its name" 1194 ); 1195 assert_eq!( 1196 index.resolve_clone_path(&own("nel"), &path("scallop")), 1197 Resolved::Ready(Some(repo_did("mussel"))) 1198 ); 1199} 1200 1201#[test] 1202fn a_name_two_repos_share_resolves_to_the_older_registration() { 1203 let world = World::new(); 1204 let first = repo_did("whelk"); 1205 let second = repo_did("conch"); 1206 1207 let meta = Repo::open(&world.meta_path).unwrap(); 1208 let store = CobStore::new(&meta); 1209 let object = store 1210 .create( 1211 &meta_home(), 1212 &RegistryChange::Register(named_registration( 1213 "nel", 1214 "3lubrptx57d22", 1215 "kelp", 1216 &first, 1217 1, 1218 )), 1219 &world.signer, 1220 at(1), 1221 ) 1222 .unwrap() 1223 .object; 1224 store 1225 .update( 1226 &meta_home(), 1227 object, 1228 &RegistryChange::Register(named_registration( 1229 "nel", 1230 "3mqydma3re27z", 1231 "kelp", 1232 &second, 1233 2, 1234 )), 1235 &world.signer, 1236 at(2), 1237 ) 1238 .unwrap(); 1239 1240 let index = world.index(); 1241 index.rebuild().unwrap(); 1242 1243 assert_eq!( 1244 index.resolve_clone_path(&own("nel"), &path("kelp")), 1245 Resolved::Ready(Some(first.clone())), 1246 "a contested name resolves to whichever repo registered first, ordered by \ 1247 created_at so a cold seed and an incremental replay agree" 1248 ); 1249 assert_eq!( 1250 index.resolve_clone_path(&own("nel"), &path("3lubrptx57d22")), 1251 Resolved::Ready(Some(first)), 1252 "each record key stays unambiguous" 1253 ); 1254 assert_eq!( 1255 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")), 1256 Resolved::Ready(Some(second)), 1257 "the repo that lost the name is still reachable by its record key" 1258 ); 1259} 1260 1261#[test] 1262fn a_repo_registered_later_never_takes_a_contested_name_by_renaming_onto_it() { 1263 let world = World::new(); 1264 let holder = repo_did("whelk"); 1265 let latecomer = repo_did("conch"); 1266 1267 let meta = Repo::open(&world.meta_path).unwrap(); 1268 let store = CobStore::new(&meta); 1269 let object = store 1270 .create( 1271 &meta_home(), 1272 &RegistryChange::Register(named_registration( 1273 "nel", 1274 "3lubrptx57d22", 1275 "kelp", 1276 &holder, 1277 1, 1278 )), 1279 &world.signer, 1280 at(1), 1281 ) 1282 .unwrap() 1283 .object; 1284 store 1285 .update( 1286 &meta_home(), 1287 object, 1288 &RegistryChange::Register(named_registration( 1289 "nel", 1290 "3mqydma3re27z", 1291 "uni", 1292 &latecomer, 1293 2, 1294 )), 1295 &world.signer, 1296 at(2), 1297 ) 1298 .unwrap(); 1299 1300 let index = world.index(); 1301 index.rebuild().unwrap(); 1302 1303 store 1304 .update( 1305 &meta_home(), 1306 object, 1307 &RegistryChange::Rename(Rename { 1308 owner: own("nel"), 1309 rkey: rkey("3mqydma3re27z"), 1310 name: RepoName::new("kelp").unwrap(), 1311 repo: latecomer.clone(), 1312 }), 1313 &world.signer, 1314 at(3), 1315 ) 1316 .unwrap(); 1317 index.refresh_registry().unwrap(); 1318 1319 assert_eq!( 1320 index.resolve_clone_path(&own("nel"), &path("kelp")), 1321 Resolved::Ready(Some(holder)), 1322 "a rename keeps the repo's original created_at, so renaming onto a name \ 1323 another repo registered earlier cannot take it" 1324 ); 1325 assert_eq!( 1326 index.resolve_clone_path(&own("nel"), &path("uni")), 1327 Resolved::Ready(None), 1328 "the renamed repo's previous name stops resolving" 1329 ); 1330 assert_eq!( 1331 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")), 1332 Resolved::Ready(Some(latecomer)) 1333 ); 1334} 1335 1336#[test] 1337fn deregistering_the_older_registration_moves_a_shared_name_to_the_survivor() { 1338 let world = World::new(); 1339 let first = repo_did("whelk"); 1340 let survivor = repo_did("conch"); 1341 1342 let meta = Repo::open(&world.meta_path).unwrap(); 1343 let store = CobStore::new(&meta); 1344 let object = store 1345 .create( 1346 &meta_home(), 1347 &RegistryChange::Register(named_registration( 1348 "nel", 1349 "3lubrptx57d22", 1350 "kelp", 1351 &first, 1352 1, 1353 )), 1354 &world.signer, 1355 at(1), 1356 ) 1357 .unwrap() 1358 .object; 1359 store 1360 .update( 1361 &meta_home(), 1362 object, 1363 &RegistryChange::Register(named_registration( 1364 "nel", 1365 "3mqydma3re27z", 1366 "kelp", 1367 &survivor, 1368 2, 1369 )), 1370 &world.signer, 1371 at(2), 1372 ) 1373 .unwrap(); 1374 1375 let index = world.index(); 1376 index.rebuild().unwrap(); 1377 assert_eq!( 1378 index.resolve_clone_path(&own("nel"), &path("kelp")), 1379 Resolved::Ready(Some(first)), 1380 "the name resolves to the older registration while both exist" 1381 ); 1382 1383 store 1384 .update( 1385 &meta_home(), 1386 object, 1387 &RegistryChange::Deregister(RepoRef { 1388 owner: own("nel"), 1389 rkey: rkey("3lubrptx57d22"), 1390 }), 1391 &world.signer, 1392 at(3), 1393 ) 1394 .unwrap(); 1395 index.refresh_registry().unwrap(); 1396 1397 assert_eq!( 1398 index.resolve_clone_path(&own("nel"), &path("kelp")), 1399 Resolved::Ready(Some(survivor)), 1400 "the name resolves to the remaining repo once the older one is deregistered" 1401 ); 1402} 1403 1404#[test] 1405fn a_rename_moves_name_resolution_off_the_old_name() { 1406 let world = World::new(); 1407 let repo = repo_did("squid"); 1408 1409 let meta = Repo::open(&world.meta_path).unwrap(); 1410 let store = CobStore::new(&meta); 1411 let object = store 1412 .create( 1413 &meta_home(), 1414 &RegistryChange::Register(named_registration( 1415 "nel", 1416 "3mizfnpxii522", 1417 "anemone", 1418 &repo, 1419 1, 1420 )), 1421 &world.signer, 1422 at(1), 1423 ) 1424 .unwrap() 1425 .object; 1426 1427 let index = world.index(); 1428 index.rebuild().unwrap(); 1429 assert_eq!( 1430 index.resolve_clone_path(&own("nel"), &path("anemone")), 1431 Resolved::Ready(Some(repo.clone())) 1432 ); 1433 1434 store 1435 .update( 1436 &meta_home(), 1437 object, 1438 &RegistryChange::Rename(Rename { 1439 owner: own("nel"), 1440 rkey: rkey("3mizfnpxii522"), 1441 name: RepoName::new("barnacle").unwrap(), 1442 repo: repo.clone(), 1443 }), 1444 &world.signer, 1445 at(2), 1446 ) 1447 .unwrap(); 1448 index.refresh_registry().unwrap(); 1449 1450 assert_eq!( 1451 index.resolve_clone_path(&own("nel"), &path("barnacle")), 1452 Resolved::Ready(Some(repo.clone())), 1453 "the new name resolves after a rename that keeps the record key" 1454 ); 1455 assert_eq!( 1456 index.resolve_clone_path(&own("nel"), &path("anemone")), 1457 Resolved::Ready(None), 1458 "the superseded name stops resolving" 1459 ); 1460 assert_eq!( 1461 index.resolve_clone_path(&own("nel"), &path("3mizfnpxii522")), 1462 Resolved::Ready(Some(repo)) 1463 ); 1464} 1465 1466#[test] 1467fn a_rename_that_changes_only_the_record_key_keeps_the_name_resolving() { 1468 let world = World::new(); 1469 let repo = repo_did("squid"); 1470 1471 let meta = Repo::open(&world.meta_path).unwrap(); 1472 let store = CobStore::new(&meta); 1473 let object = store 1474 .create( 1475 &meta_home(), 1476 &RegistryChange::Register(named_registration("nel", "3lubrptx57d22", "kelp", &repo, 1)), 1477 &world.signer, 1478 at(1), 1479 ) 1480 .unwrap() 1481 .object; 1482 1483 let index = world.index(); 1484 index.rebuild().unwrap(); 1485 1486 store 1487 .update( 1488 &meta_home(), 1489 object, 1490 &RegistryChange::Rename(Rename { 1491 owner: own("nel"), 1492 rkey: rkey("3mqydma3re27z"), 1493 name: RepoName::new("kelp").unwrap(), 1494 repo: repo.clone(), 1495 }), 1496 &world.signer, 1497 at(2), 1498 ) 1499 .unwrap(); 1500 index.refresh_registry().unwrap(); 1501 1502 assert_eq!( 1503 index.resolve_clone_path(&own("nel"), &path("kelp")), 1504 Resolved::Ready(Some(repo.clone())), 1505 "the unchanged name survives a rename that swaps the record key" 1506 ); 1507 assert_eq!( 1508 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")), 1509 Resolved::Ready(Some(repo.clone())) 1510 ); 1511 assert_eq!( 1512 index.resolve_clone_path(&own("nel"), &path("3lubrptx57d22")), 1513 Resolved::Ready(Some(repo)), 1514 "the superseded record key keeps resolving through its retained alias" 1515 ); 1516} 1517 1518#[test] 1519fn seeding_and_replaying_agree_on_name_resolution() { 1520 let world = World::new(); 1521 let first = repo_did("whelk"); 1522 let second = repo_did("conch"); 1523 1524 let meta = Repo::open(&world.meta_path).unwrap(); 1525 let store = CobStore::new(&meta); 1526 let object = store 1527 .create( 1528 &meta_home(), 1529 &RegistryChange::Register(named_registration( 1530 "nel", 1531 "3lubrptx57d22", 1532 "kelp", 1533 &first, 1534 1, 1535 )), 1536 &world.signer, 1537 at(1), 1538 ) 1539 .unwrap() 1540 .object; 1541 1542 let replayed = world.index(); 1543 replayed.rebuild().unwrap(); 1544 1545 store 1546 .update( 1547 &meta_home(), 1548 object, 1549 &RegistryChange::Register(named_registration( 1550 "nel", 1551 "3mqydma3re27z", 1552 "uni", 1553 &second, 1554 2, 1555 )), 1556 &world.signer, 1557 at(2), 1558 ) 1559 .unwrap(); 1560 replayed.refresh_registry().unwrap(); 1561 1562 let seeded = world.index(); 1563 seeded.rebuild().unwrap(); 1564 1565 ["kelp", "uni", "3lubrptx57d22", "3mqydma3re27z", "nautilus"] 1566 .into_iter() 1567 .for_each(|segment| { 1568 assert_eq!( 1569 replayed.resolve_clone_path(&own("nel"), &path(segment)), 1570 seeded.resolve_clone_path(&own("nel"), &path(segment)), 1571 "incremental replay and a cold seed disagree on {segment}" 1572 ); 1573 }); 1574} 1575 1576#[test] 1577fn a_name_differing_from_its_record_key_only_by_case_resolves() { 1578 let world = World::new(); 1579 let repo = repo_did("squid"); 1580 1581 let meta = Repo::open(&world.meta_path).unwrap(); 1582 let store = CobStore::new(&meta); 1583 store 1584 .create( 1585 &meta_home(), 1586 &RegistryChange::Register(named_registration( 1587 "nel", 1588 "runic_lang", 1589 "Runic_lang", 1590 &repo, 1591 1, 1592 )), 1593 &world.signer, 1594 at(1), 1595 ) 1596 .unwrap(); 1597 1598 let index = world.index(); 1599 index.rebuild().unwrap(); 1600 1601 assert_eq!( 1602 index.resolve_clone_path(&own("nel"), &path("Runic_lang")), 1603 Resolved::Ready(Some(repo.clone())), 1604 "the appview lowercases the record key but keeps the display name's case, \ 1605 so the mixed-case path resolves by name" 1606 ); 1607 assert_eq!( 1608 index.resolve_clone_path(&own("nel"), &path("runic_lang")), 1609 Resolved::Ready(Some(repo)), 1610 "the lowercased record key still resolves" 1611 ); 1612}