This repository has no description
0

Configure Feed

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

knot2/index: test key set's leases, budget, sweep, work planning

Lewis: May this revision serve well! <did:plc:3fwecdnvtcscjnrx2p4n7alz>

author did:plc:3fwecdnvtcscjnrx2p4n7a… date (Aug 3, 2026, 11:38 PM +0300) commit e2d846f7 parent 060d6733 change-id zlplsrws
+580 -3
+10 -2
knot2/crates/knot-index/tests/common/mod.rs
··· 5 5 use knot_cob::{CobHome, CobId, CobStore}; 6 6 use knot_cobs::{CollaboratorsChange, Grant, MembersChange, Registration, RegistryChange, Removal}; 7 7 use knot_git::{Layout, Repo}; 8 - use knot_index::Index; 8 + use knot_index::{Index, KeyBudget}; 9 9 use knot_runtime::{K256Signer, SeededEntropy}; 10 10 use knot_types::{AccountDid, KnotId, OwnerDid, RepoDid, RepoName, RepoRkey, UnixSeconds}; 11 11 use tempfile::TempDir; ··· 98 98 Index::new(&self.meta_path, self.layout.clone()) 99 99 } 100 100 101 + pub fn index_within(&self, budget: KeyBudget) -> Index { 102 + Index::with_key_budget(&self.meta_path, self.layout.clone(), budget) 103 + } 104 + 101 105 pub fn seed_members(&self) -> CobId { 102 106 let meta = Repo::open(&self.meta_path).unwrap(); 103 107 let store = CobStore::new(&meta); ··· 150 154 } 151 155 152 156 pub fn register_extra(&self, repo: &RepoDid, key: &str, registry: CobId) { 157 + self.register_owned(repo, key, "nel", registry); 158 + } 159 + 160 + pub fn register_owned(&self, repo: &RepoDid, key: &str, owner: &str, registry: CobId) { 153 161 let meta = Repo::open(&self.meta_path).unwrap(); 154 162 let store = CobStore::new(&meta); 155 163 store 156 164 .update( 157 165 &meta_home(), 158 166 registry, 159 - &RegistryChange::Register(registration("nel", key, repo, 2)), 167 + &RegistryChange::Register(registration(owner, key, repo, 2)), 160 168 &self.signer, 161 169 at(2), 162 170 )
+570 -1
knot2/crates/knot-index/tests/projections.rs
··· 4 4 use knot_cob::{ChangePayload, CobHome, CobId, CobStore}; 5 5 use knot_cobs::{CollaboratorsChange, MembersChange, RegistryChange, Removal, Rename, RepoRef}; 6 6 use knot_git::{RefUpdate, Repo}; 7 - use knot_index::{Coverage, IndexError, Resolved}; 7 + use knot_index::{ 8 + Coverage, HostedCoverage, IndexError, KeptAccounts, KeyBudget, KeyRecord, KeyReprieve, 9 + KeyReprieved, KeyTtl, OfferedKey, Resolved, SweepFloor, 10 + }; 8 11 use knot_types::{ClonePath, RefName, RepoName}; 9 12 use serde::{Deserialize, Serialize}; 10 13 ··· 536 539 Resolved::Ready(true), 537 540 "deregister and re-register within single delta leaves repo hosted, so its collaborators survive" 538 541 ); 542 + } 543 + 544 + #[test] 545 + fn 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] 606 + fn 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] 686 + fn 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] 752 + fn 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] 813 + fn 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] 849 + fn 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] 887 + fn 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] 917 + fn 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] 936 + fn 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] 999 + fn 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] 1045 + fn 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 + 1083 + fn 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 + 1094 + fn grace() -> KeyReprieve { 1095 + KeyReprieve::from_secs(300, 21_600) 1096 + } 1097 + 1098 + fn hour(from: i64) -> knot_index::KeyLease { 1099 + KeyTtl::from_secs(3_600).lease_from(at(from)) 1100 + } 1101 + 1102 + fn sweep() -> SweepFloor { 1103 + SweepFloor::from_secs(60) 1104 + } 1105 + 1106 + fn pushers_complete(index: &knot_index::Index, now: knot_types::UnixSeconds) -> bool { 1107 + matches!(index.keys().work(now, sweep()), Resolved::Ready(work) if work.complete) 539 1108 } 540 1109 541 1110 fn path(raw: &str) -> ClonePath {