···9090- [ ] lenient sync1.1
9191 - [ ] *don't* allow non-validating commits that look like sync1.1
9292 - [ ] rachet by PDS host: be lenient if we have never seen a sync1.1-looking commit, always strict after we see one.
9393+- [ ] account status convergeance: if we receive commits from apparently-inactive accounts, should we check upstream status to make sure we're not stale?
9494+9595+very much still todo but i'm getting tired
9396- [ ] multi-relay listener
9497- [ ] special did:web behaviour to keep reusing a stale resolution on failure
9598- [ ] admin view of backfill state etc
9699- [ ] vanity stats for optimizations, like how many in-flight repos were saved from resync due to high-water-mark firehose cursor persistence
9797-- [ ] account status convergeance: if we receive commits from apparently-inactive accounts, should we check upstream status to make sure we're not stale?
98100- [ ] if the upstream is a PDS (check with describeServer?) then make only accept events for DIDs that have it as their PDS
99101- [ ] split the keyspace: put the rbc/cbr indexes on a second keyspace with larger block size, expect hits on main keyspace
100102
+184
src/mst/collections.rs
···7070 collections.sort_unstable();
7171 Ok(collections)
7272}
7373+7474+#[cfg(test)]
7575+mod tests {
7676+ use super::*;
7777+ use std::sync::Arc;
7878+7979+ use bytes::Bytes;
8080+ use jacquard_common::types::string::Did;
8181+ use jacquard_common::types::tid::Tid;
8282+ use jacquard_repo::commit::Commit;
8383+ use jacquard_repo::{BlockStore, MemoryBlockStore, Mst, car::write_car_bytes};
8484+ use repo_stream::DriverBuilder;
8585+8686+ fn nsid(s: &str) -> Nsid<'static> {
8787+ s.parse().unwrap()
8888+ }
8989+9090+ /// Build a `MemCar` from a list of `collection/rkey` MST keys.
9191+ async fn make_mem_car(keys: &[&str]) -> MemCar {
9292+ let storage = Arc::new(MemoryBlockStore::new());
9393+ let mut mst = Mst::new(storage.clone());
9494+ let dummy_cid = storage.put(b"record").await.unwrap();
9595+ for key in keys {
9696+ mst = mst.add(key, dummy_cid).await.unwrap();
9797+ }
9898+ let (mst_root, mut blocks) = mst.collect_blocks().await.unwrap();
9999+ let commit = Commit {
100100+ did: Did::new_owned("did:web:example.com").unwrap(),
101101+ version: 3,
102102+ data: mst_root,
103103+ rev: Tid::now_0(),
104104+ prev: None,
105105+ sig: Bytes::from(vec![0u8; 64]),
106106+ };
107107+ let commit_cid = commit.to_cid().unwrap();
108108+ let commit_cbor = Bytes::from(commit.to_cbor().unwrap());
109109+ blocks.insert(commit_cid, commit_cbor);
110110+ let car_bytes = write_car_bytes(commit_cid, blocks).await.unwrap();
111111+ DriverBuilder::new()
112112+ .load_car(tokio::io::BufReader::new(std::io::Cursor::new(car_bytes)))
113113+ .await
114114+ .unwrap()
115115+ }
116116+117117+ // ---------------------------------------------------------------------------
118118+ // extract_from_slice
119119+ // ---------------------------------------------------------------------------
120120+121121+ #[tokio::test]
122122+ async fn slice_empty_repo() {
123123+ let mut car = make_mem_car(&[]).await;
124124+ let result = extract_from_slice(&mut car).unwrap();
125125+ assert!(result.is_empty());
126126+ }
127127+128128+ #[tokio::test]
129129+ async fn slice_single_collection_single_record() {
130130+ let mut car = make_mem_car(&["app.bsky.feed.post/abc123"]).await;
131131+ let result = extract_from_slice(&mut car).unwrap();
132132+ assert_eq!(result, HashSet::from([nsid("app.bsky.feed.post")]));
133133+ }
134134+135135+ #[tokio::test]
136136+ async fn slice_deduplicates_within_collection() {
137137+ let mut car = make_mem_car(&[
138138+ "app.bsky.feed.post/abc123",
139139+ "app.bsky.feed.post/def456",
140140+ "app.bsky.feed.post/ghi789",
141141+ ])
142142+ .await;
143143+ let result = extract_from_slice(&mut car).unwrap();
144144+ assert_eq!(result, HashSet::from([nsid("app.bsky.feed.post")]));
145145+ }
146146+147147+ #[tokio::test]
148148+ async fn slice_multiple_collections() {
149149+ let mut car = make_mem_car(&[
150150+ "app.bsky.actor.profile/self",
151151+ "app.bsky.feed.post/abc123",
152152+ "app.bsky.graph.follow/abc123",
153153+ ])
154154+ .await;
155155+ let result = extract_from_slice(&mut car).unwrap();
156156+ assert_eq!(
157157+ result,
158158+ HashSet::from([
159159+ nsid("app.bsky.actor.profile"),
160160+ nsid("app.bsky.feed.post"),
161161+ nsid("app.bsky.graph.follow"),
162162+ ])
163163+ );
164164+ }
165165+166166+ // ---------------------------------------------------------------------------
167167+ // extract_from_full
168168+ // ---------------------------------------------------------------------------
169169+170170+ #[tokio::test]
171171+ async fn full_empty_repo() {
172172+ let car = make_mem_car(&[]).await;
173173+ let result = extract_from_full(car).await.unwrap();
174174+ assert!(result.is_empty());
175175+ }
176176+177177+ #[tokio::test]
178178+ async fn full_single_collection_single_record() {
179179+ let car = make_mem_car(&["app.bsky.feed.post/abc123"]).await;
180180+ let result = extract_from_full(car).await.unwrap();
181181+ assert_eq!(result, vec![nsid("app.bsky.feed.post")]);
182182+ }
183183+184184+ #[tokio::test]
185185+ async fn full_deduplicates_and_sorts() {
186186+ let car = make_mem_car(&[
187187+ "app.bsky.actor.profile/self",
188188+ "app.bsky.feed.post/abc123",
189189+ "app.bsky.feed.post/def456",
190190+ "app.bsky.graph.follow/abc123",
191191+ ])
192192+ .await;
193193+ let result = extract_from_full(car).await.unwrap();
194194+ assert_eq!(
195195+ result,
196196+ vec![
197197+ nsid("app.bsky.actor.profile"),
198198+ nsid("app.bsky.feed.post"),
199199+ nsid("app.bsky.graph.follow"),
200200+ ]
201201+ );
202202+ }
203203+204204+ #[tokio::test]
205205+ async fn full_prefix_collection_names() {
206206+ // In the MST, '.' (0x2E) < '/' (0x2F), so `sh.tangled.repo.issue/<rkey>`
207207+ // sorts before `sh.tangled.repo/<rkey>`. The seek strategy must still
208208+ // produce a correctly sorted output that includes all three.
209209+ let car = make_mem_car(&[
210210+ "sh.tangled.repo/self",
211211+ "sh.tangled.repo.issue/abc123",
212212+ "sh.tangled.repo.pull/abc123",
213213+ ])
214214+ .await;
215215+ let result = extract_from_full(car).await.unwrap();
216216+ assert_eq!(
217217+ result,
218218+ vec![
219219+ nsid("sh.tangled.repo"),
220220+ nsid("sh.tangled.repo.issue"),
221221+ nsid("sh.tangled.repo.pull"),
222222+ ]
223223+ );
224224+ }
225225+226226+ #[tokio::test]
227227+ async fn full_many_collections() {
228228+ let car = make_mem_car(&[
229229+ "app.bsky.actor.profile/self",
230230+ "app.bsky.feed.generator/abc123",
231231+ "app.bsky.feed.like/abc123",
232232+ "app.bsky.feed.like/def456",
233233+ "app.bsky.feed.post/abc123",
234234+ "app.bsky.feed.post/def456",
235235+ "app.bsky.feed.post/ghi789",
236236+ "app.bsky.feed.repost/abc123",
237237+ "app.bsky.graph.block/abc123",
238238+ "app.bsky.graph.follow/abc123",
239239+ "app.bsky.graph.follow/def456",
240240+ ])
241241+ .await;
242242+ let result = extract_from_full(car).await.unwrap();
243243+ assert_eq!(
244244+ result,
245245+ vec![
246246+ nsid("app.bsky.actor.profile"),
247247+ nsid("app.bsky.feed.generator"),
248248+ nsid("app.bsky.feed.like"),
249249+ nsid("app.bsky.feed.post"),
250250+ nsid("app.bsky.feed.repost"),
251251+ nsid("app.bsky.graph.block"),
252252+ nsid("app.bsky.graph.follow"),
253253+ ]
254254+ );
255255+ }
256256+}