1use std::collections::HashMap;
60use std::sync::atomic::{AtomicU64, Ordering};
61use std::sync::Arc;
62
63use alkgit::validate_repo_id;
64#[cfg(any(feature = "sqlite", feature = "postgres"))]
65use alkgit::{AccessAction, Visibility};
66use alkgit::{GitRegistry, GitRegistryStore, RegistryError, RepoRecord};
67use async_trait::async_trait;
68#[cfg(any(feature = "sqlite", feature = "postgres"))]
69use std::collections::BTreeMap;
70
71pub const HEAD_SEED_TARGET: &str = "refs/heads/main";
76
77#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct RegistryRow {
87 pub record: RepoRecord,
89 pub seq: i64,
91}
92
93pub trait RegistryRead: Send + Sync {
102 fn resolve(&self, repo_id: &str) -> Option<RepoRecord>;
104}
105
106#[async_trait]
121pub trait RegistryWrite: Send + Sync {
122 async fn put(&self, record: &RepoRecord) -> Result<i64, RegistryError>;
126
127 async fn update(&self, record: &RepoRecord) -> Result<i64, RegistryError>;
132
133 async fn remove(&self, repo_id: &str) -> Result<i64, RegistryError>;
136
137 async fn watermark(&self) -> Result<i64, RegistryError>;
141
142 async fn rows_since(&self, seq: i64) -> Result<Vec<RegistryRow>, RegistryError>;
146}
147
148pub(crate) fn validate_record(record: &RepoRecord) -> Result<(), RegistryError> {
155 validate_repo_id(&record.repo_id).map_err(|_| RegistryError::Invalid)?;
156 if record.grants.keys().any(String::is_empty) {
157 return Err(RegistryError::Invalid);
158 }
159 Ok(())
160}
161
162#[cfg(any(feature = "sqlite", feature = "postgres"))]
168pub(crate) fn row_to_record(
169 repo_id: &str,
170 visibility_col: &str,
171 grants_col: &str,
172 seq: i64,
173) -> Result<RegistryRow, RegistryError> {
174 let visibility = match visibility_col {
175 "public" => Visibility::Public,
176 "private" => Visibility::Private,
177 _ => return Err(RegistryError::Invalid),
178 };
179 let grants: BTreeMap<String, Vec<AccessAction>> =
180 serde_json::from_str(grants_col).map_err(|_| RegistryError::Invalid)?;
181 Ok(RegistryRow {
182 record: RepoRecord {
183 repo_id: repo_id.to_owned(),
184 visibility,
185 grants,
186 },
187 seq,
188 })
189}
190
191#[cfg(any(feature = "sqlite", feature = "postgres"))]
195pub(crate) fn visibility_str(visibility: Visibility) -> &'static str {
196 match visibility {
197 Visibility::Public => "public",
198 Visibility::Private => "private",
199 }
200}
201
202pub struct InMemoryRegistry {
213 rows: parking_lot::RwLock<HashMap<String, RegistryRow>>,
214 seq: AtomicU64,
215}
216
217impl Default for InMemoryRegistry {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223impl InMemoryRegistry {
224 pub fn new() -> Self {
226 InMemoryRegistry {
227 rows: parking_lot::RwLock::new(HashMap::new()),
228 seq: AtomicU64::new(0),
229 }
230 }
231
232 fn next_seq(&self) -> i64 {
233 self.seq.fetch_add(1, Ordering::Relaxed) as i64 + 1
234 }
235
236 pub(crate) async fn record_mutation(&self, repo_id: &str) -> Result<i64, RegistryError> {
246 if !self.rows.read().contains_key(repo_id) {
247 return Err(RegistryError::NotFound);
248 }
249 Ok(self.next_seq())
250 }
251}
252
253#[async_trait]
254impl RegistryWrite for InMemoryRegistry {
255 async fn put(&self, record: &RepoRecord) -> Result<i64, RegistryError> {
256 validate_record(record)?;
257 let mut rows = self.rows.write();
258 if rows.contains_key(&record.repo_id) {
259 return Err(RegistryError::AlreadyExists);
260 }
261 let seq = self.next_seq();
262 rows.insert(
263 record.repo_id.clone(),
264 RegistryRow {
265 record: record.clone(),
266 seq,
267 },
268 );
269 Ok(seq)
270 }
271
272 async fn update(&self, record: &RepoRecord) -> Result<i64, RegistryError> {
273 validate_record(record)?;
274 let mut rows = self.rows.write();
275 if !rows.contains_key(&record.repo_id) {
276 return Err(RegistryError::NotFound);
277 }
278 let seq = self.next_seq();
279 rows.insert(
280 record.repo_id.clone(),
281 RegistryRow {
282 record: record.clone(),
283 seq,
284 },
285 );
286 Ok(seq)
287 }
288
289 async fn remove(&self, repo_id: &str) -> Result<i64, RegistryError> {
290 let mut rows = self.rows.write();
291 if rows.remove(repo_id).is_none() {
292 return Err(RegistryError::NotFound);
293 }
294 Ok(self.next_seq())
295 }
296
297 async fn watermark(&self) -> Result<i64, RegistryError> {
298 Ok(self.seq.load(Ordering::Relaxed) as i64)
299 }
300
301 async fn rows_since(&self, seq: i64) -> Result<Vec<RegistryRow>, RegistryError> {
302 let mut rows: Vec<RegistryRow> = self
303 .rows
304 .read()
305 .values()
306 .filter(|row| row.seq > seq)
307 .cloned()
308 .collect();
309 rows.sort_by_key(|row| row.seq);
310 Ok(rows)
311 }
312}
313
314impl RegistryRead for InMemoryRegistry {
315 fn resolve(&self, repo_id: &str) -> Option<RepoRecord> {
316 self.rows.read().get(repo_id).map(|row| row.record.clone())
317 }
318}
319
320pub struct CachedRegistryProvider {
353 snapshot: parking_lot::RwLock<Arc<HashMap<String, RepoRecord>>>,
354 refreshes: AtomicU64,
355}
356
357impl Default for CachedRegistryProvider {
358 fn default() -> Self {
359 Self::new()
360 }
361}
362
363impl CachedRegistryProvider {
364 pub fn new() -> Self {
366 CachedRegistryProvider {
367 snapshot: parking_lot::RwLock::new(Arc::new(HashMap::new())),
368 refreshes: AtomicU64::new(0),
369 }
370 }
371
372 pub fn install(&self, rows: Vec<RegistryRow>) {
375 let map: HashMap<String, RepoRecord> = rows
376 .into_iter()
377 .map(|row| (row.record.repo_id.clone(), row.record))
378 .collect();
379 *self.snapshot.write() = Arc::new(map);
380 self.refreshes.fetch_add(1, Ordering::Relaxed);
381 }
382
383 pub fn install_one(&self, row: RegistryRow) {
386 let mut guard = self.snapshot.write();
387 let mut map = (**guard).clone();
388 map.insert(row.record.repo_id.clone(), row.record);
389 *guard = Arc::new(map);
390 self.refreshes.fetch_add(1, Ordering::Relaxed);
391 }
392
393 pub fn install_removal(&self, repo_id: &str) {
395 let mut guard = self.snapshot.write();
396 let mut map = (**guard).clone();
397 map.remove(repo_id);
398 *guard = Arc::new(map);
399 self.refreshes.fetch_add(1, Ordering::Relaxed);
400 }
401
402 pub async fn refresh_from(&self, registry: &dyn RegistryWrite) -> Result<(), RegistryError> {
407 let rows = registry.rows_since(0).await?;
408 self.install(rows);
409 Ok(())
410 }
411
412 pub fn snapshot(&self) -> Arc<HashMap<String, RepoRecord>> {
415 self.snapshot.read().clone()
416 }
417
418 pub fn refresh_count(&self) -> u64 {
422 self.refreshes.load(Ordering::Relaxed)
423 }
424}
425
426impl RegistryRead for CachedRegistryProvider {
427 fn resolve(&self, repo_id: &str) -> Option<RepoRecord> {
428 self.snapshot.read().get(repo_id).cloned()
429 }
430}
431
432pub struct RegistryBackend {
450 write: Arc<dyn RegistryWrite>,
451 read: Arc<dyn RegistryRead>,
452 write_through: Option<Arc<CachedRegistryProvider>>,
453}
454
455impl RegistryBackend {
456 pub fn new(write: Arc<dyn RegistryWrite>, read: Arc<dyn RegistryRead>) -> Self {
459 RegistryBackend {
460 write,
461 read,
462 write_through: None,
463 }
464 }
465
466 pub fn with_cached_provider(
470 write: Arc<dyn RegistryWrite>,
471 provider: Arc<CachedRegistryProvider>,
472 ) -> Self {
473 RegistryBackend {
474 write: write.clone(),
475 read: provider.clone(),
476 write_through: Some(provider),
477 }
478 }
479
480 pub fn write(&self) -> &Arc<dyn RegistryWrite> {
483 &self.write
484 }
485
486 pub fn read(&self) -> &Arc<dyn RegistryRead> {
488 &self.read
489 }
490
491 pub fn provider(&self) -> Option<&Arc<CachedRegistryProvider>> {
494 self.write_through.as_ref()
495 }
496}
497
498#[async_trait]
499impl GitRegistry for RegistryBackend {
500 async fn resolve(&self, repo_id: &str) -> Result<RepoRecord, RegistryError> {
501 self.read.resolve(repo_id).ok_or(RegistryError::NotFound)
502 }
503}
504
505#[async_trait]
506impl GitRegistryStore for RegistryBackend {
507 async fn put_repo(&self, record: &RepoRecord) -> Result<(), RegistryError> {
508 let seq = self.write.put(record).await?;
509 if let Some(provider) = &self.write_through {
510 provider.install_one(RegistryRow {
511 record: record.clone(),
512 seq,
513 });
514 }
515 Ok(())
516 }
517
518 async fn update_repo(&self, record: &RepoRecord) -> Result<(), RegistryError> {
519 let seq = self.write.update(record).await?;
520 if let Some(provider) = &self.write_through {
521 provider.install_one(RegistryRow {
522 record: record.clone(),
523 seq,
524 });
525 }
526 Ok(())
527 }
528
529 async fn remove_repo(&self, repo_id: &str) -> Result<(), RegistryError> {
530 self.write.remove(repo_id).await?;
531 if let Some(provider) = &self.write_through {
532 provider.install_removal(repo_id);
533 }
534 Ok(())
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 use crate::testutil::{mem_registry_pair, private_record, record};
543
544 #[tokio::test]
545 async fn mem_adapter_is_complete_put_resolve_update_remove() {
546 let registry = InMemoryRegistry::new();
547
548 let put_seq = registry.put(&record("demo")).await.unwrap();
549 assert_eq!(put_seq, 1);
550 assert_eq!(registry.resolve("demo"), Some(record("demo")));
551
552 let mut updated = record("demo");
553 updated.visibility = alkgit::Visibility::Private;
554 let update_seq = registry.update(&updated).await.unwrap();
555 assert_eq!(update_seq, 2);
556 assert_eq!(registry.resolve("demo"), Some(updated));
557
558 let remove_seq = registry.remove("demo").await.unwrap();
559 assert_eq!(remove_seq, 3);
560 assert_eq!(registry.resolve("demo"), None);
561 }
562
563 #[tokio::test]
564 async fn mem_adapter_error_map_is_pinned() {
565 let registry = InMemoryRegistry::new();
566 assert_eq!(
567 registry.update(&record("ghost")).await.unwrap_err(),
568 RegistryError::NotFound
569 );
570 assert_eq!(
571 registry.remove("ghost").await.unwrap_err(),
572 RegistryError::NotFound
573 );
574 assert_eq!(
575 registry.put(&record("demo")).await.unwrap(),
576 1,
577 "first mutation carries seq 1"
578 );
579 assert_eq!(
580 registry.put(&record("demo")).await.unwrap_err(),
581 RegistryError::AlreadyExists
582 );
583
584 let mut bad_id = record("Owner/name");
585 bad_id.repo_id = "Owner/name".to_string();
586 assert_eq!(
587 registry.put(&bad_id).await.unwrap_err(),
588 RegistryError::Invalid
589 );
590 let mut empty_repo_id = record("");
591 empty_repo_id.repo_id = String::new();
592 assert_eq!(
593 registry.put(&empty_repo_id).await.unwrap_err(),
594 RegistryError::Invalid
595 );
596 }
597
598 #[tokio::test]
599 async fn mem_adapter_failed_mutations_consume_no_seq() {
600 let registry = InMemoryRegistry::new();
601 assert!(registry.put(&record("a")).await.is_ok());
602 assert!(registry.put(&record("a")).await.is_err());
603 assert_eq!(registry.watermark().await.unwrap(), 1);
604 let seq = registry.put(&record("b")).await.unwrap();
605 assert_eq!(seq, 2, "the failed duplicate put consumed nothing");
606 }
607
608 #[tokio::test]
609 async fn mem_adapter_watermark_is_monotone_across_removal() {
610 let registry = InMemoryRegistry::new();
611 assert_eq!(registry.watermark().await.unwrap(), 0, "fresh table");
612 registry.put(&record("a")).await.unwrap();
613 registry.put(&record("b")).await.unwrap();
614 assert_eq!(registry.watermark().await.unwrap(), 2);
615 registry.remove("a").await.unwrap();
616 assert_eq!(
617 registry.watermark().await.unwrap(),
618 3,
619 "a committed removal advances the watermark though it leaves no row"
620 );
621 registry.remove("never-was").await.unwrap_err();
622 assert_eq!(
623 registry.watermark().await.unwrap(),
624 3,
625 "a failed removal consumes no seq"
626 );
627 }
628
629 #[tokio::test]
630 async fn mem_adapter_rows_since_covers_the_reconcile_recipe() {
631 let registry = InMemoryRegistry::new();
632 let seq_a = registry.put(&record("a")).await.unwrap();
633 let seq_b = registry.put(&private_record("b")).await.unwrap();
634 assert_eq!(seq_b, seq_a + 1);
635
636 let all = registry.rows_since(0).await.unwrap();
637 assert_eq!(
638 all.iter().map(|row| row.seq).collect::<Vec<_>>(),
639 vec![seq_a, seq_b],
640 "rows_since(0) is the whole table, in sequence order"
641 );
642
643 registry.update(&record("a")).await.unwrap();
644 registry.remove("b").await.unwrap();
645
646 let catchup = registry.rows_since(seq_b).await.unwrap();
647 assert_eq!(
648 catchup
649 .iter()
650 .map(|row| (row.record.repo_id.as_str(), row.seq))
651 .collect::<Vec<_>>(),
652 vec![("a", seq_a + 2)],
653 "the committed removal leaves no row for the catch-up read"
654 );
655 }
656
657 #[tokio::test]
658 async fn mem_adapter_rows_since_is_seq_ordered_and_exclusive() {
659 let registry = InMemoryRegistry::new();
660 let first = registry.put(&record("a")).await.unwrap();
661 registry.put(&record("b")).await.unwrap();
662 registry.put(&record("c")).await.unwrap();
663
664 let rows = registry.rows_since(first).await.unwrap();
665 assert_eq!(
666 rows.iter()
667 .map(|row| row.record.repo_id.as_str())
668 .collect::<Vec<_>>(),
669 vec!["b", "c"],
670 "rows after `seq` only, in sequence order"
671 );
672 assert!(registry.rows_since(first).await.unwrap()[0].seq > first);
673 }
674
675 #[tokio::test]
676 async fn resolve_hits_are_cloned_not_aliased() {
677 let registry = InMemoryRegistry::new();
678 registry.put(&record("demo")).await.unwrap();
679 let resolved = registry.resolve("demo").unwrap();
680 let via_provider = {
681 let provider = CachedRegistryProvider::new();
682 provider.install(registry.rows_since(0).await.unwrap());
683 provider.resolve("demo").unwrap()
684 };
685 assert_eq!(resolved, record("demo"));
686 assert_eq!(via_provider, record("demo"));
687 }
688
689 #[tokio::test]
690 async fn provider_snapshot_swap_serves_the_whole_table() {
691 let registry = InMemoryRegistry::new();
692 let provider = CachedRegistryProvider::new();
693
694 assert_eq!(provider.resolve("demo"), None, "fresh provider misses");
695 assert_eq!(provider.refresh_count(), 0, "no installs at construction");
696
697 registry.put(&record("demo")).await.unwrap();
698 provider.install(registry.rows_since(0).await.unwrap());
699
700 assert_eq!(provider.resolve("demo"), Some(record("demo")));
701 assert_eq!(provider.snapshot().len(), 1, "whole-table install");
702 }
703
704 #[tokio::test]
705 async fn provider_hot_path_never_touches_the_durable_side() {
706 let registry = InMemoryRegistry::new();
707 registry.put(&record("demo")).await.unwrap();
708 let provider = CachedRegistryProvider::new();
709 provider.install(registry.rows_since(0).await.unwrap());
710
711 let before = registry.watermark().await.unwrap();
712 for _ in 0..1_000 {
713 assert_eq!(provider.resolve("demo"), Some(record("demo")));
714 }
715 assert_eq!(
716 registry.watermark().await.unwrap(),
717 before,
718 "resolve is served entirely from the installed snapshot — \
719 zero durable-side reads on the hot path"
720 );
721 }
722
723 #[tokio::test]
724 async fn provider_write_through_sees_its_own_writes_immediately() {
725 let registry = Arc::new(InMemoryRegistry::new());
726 let provider = Arc::new(CachedRegistryProvider::new());
727 let backend = RegistryBackend::with_cached_provider(registry.clone(), provider.clone());
728 let write = backend.write().clone();
729 drop(backend);
730
731 let seq = write.put(&record("demo")).await.unwrap();
732 provider.install_one(RegistryRow {
733 record: record("demo"),
734 seq,
735 });
736 assert_eq!(
737 provider.resolve("demo"),
738 Some(record("demo")),
739 "write-through install surfaces the mutation with no refresh"
740 );
741 assert_eq!(provider.refresh_count(), 1, "one install, no table read");
742
743 let seq = write.put(&record("other")).await.unwrap();
744 provider.install_one(RegistryRow {
745 record: record("other"),
746 seq,
747 });
748 assert_eq!(provider.resolve("demo"), Some(record("demo")));
749 assert_eq!(provider.resolve("other"), Some(record("other")));
750
751 write.remove("demo").await.unwrap();
752 provider.install_removal("demo");
753 assert_eq!(provider.resolve("demo"), None);
754 assert_eq!(
755 provider.resolve("other"),
756 Some(record("other")),
757 "removal is an install, not a re-read"
758 );
759 }
760
761 struct FlakyCatchUp {
762 registry: InMemoryRegistry,
763 fail_first_rows_since: std::sync::atomic::AtomicBool,
764 }
765
766 #[async_trait]
767 impl RegistryWrite for FlakyCatchUp {
768 async fn put(&self, record: &RepoRecord) -> Result<i64, RegistryError> {
769 self.registry.put(record).await
770 }
771 async fn update(&self, record: &RepoRecord) -> Result<i64, RegistryError> {
772 self.registry.update(record).await
773 }
774 async fn remove(&self, repo_id: &str) -> Result<i64, RegistryError> {
775 self.registry.remove(repo_id).await
776 }
777 async fn watermark(&self) -> Result<i64, RegistryError> {
778 self.registry.watermark().await
779 }
780 async fn rows_since(&self, seq: i64) -> Result<Vec<RegistryRow>, RegistryError> {
781 if self.fail_first_rows_since.swap(false, Ordering::Relaxed) {
782 return Err(RegistryError::Io("refresh transport failed".into()));
783 }
784 self.registry.rows_since(seq).await
785 }
786 }
787
788 #[tokio::test]
789 async fn provider_refresh_failure_leaves_the_previous_snapshot_serving() {
790 let flaky = FlakyCatchUp {
791 registry: InMemoryRegistry::new(),
792 fail_first_rows_since: std::sync::atomic::AtomicBool::new(false),
793 };
794 let provider = CachedRegistryProvider::new();
795 let seq = flaky.put(&record("good")).await.unwrap();
796 provider.install_one(RegistryRow {
797 record: record("good"),
798 seq,
799 });
800
801 flaky.fail_first_rows_since.store(true, Ordering::Relaxed);
802 assert!(provider.refresh_from(&flaky).await.is_err());
803 assert_eq!(
804 provider.resolve("good"),
805 Some(record("good")),
806 "the failed refresh left the previous snapshot serving"
807 );
808
809 assert!(provider.refresh_from(&flaky).await.is_ok());
810 assert_eq!(provider.resolve("good"), Some(record("good")));
811
812 flaky.put(&record("late")).await.unwrap();
813 assert!(provider.refresh_from(&flaky).await.is_ok());
814 assert_eq!(
815 provider.resolve("late"),
816 Some(record("late")),
817 "the next tick's refresh retries and succeeds"
818 );
819 }
820
821 #[tokio::test]
822 async fn provider_one_refresh_surfaces_a_burst_and_is_idempotent() {
823 let registry = InMemoryRegistry::new();
824 let provider = CachedRegistryProvider::new();
825
826 for id in ["a", "b", "c"] {
827 registry.put(&record(id)).await.unwrap();
828 }
829 provider.refresh_from(®istry).await.unwrap();
830 assert_eq!(provider.refresh_count(), 1);
831 for id in ["a", "b", "c"] {
832 assert_eq!(
833 provider.resolve(id),
834 Some(record(id)),
835 "one refresh carries the whole burst — coalescing is \
836 correct by the snapshot swap"
837 );
838 }
839 assert_eq!(provider.snapshot().len(), 3);
840
841 registry.remove("b").await.unwrap();
842 provider.refresh_from(®istry).await.unwrap();
843 provider.refresh_from(®istry).await.unwrap();
844 assert_eq!(provider.resolve("a"), Some(record("a")));
845 assert_eq!(provider.resolve("b"), None);
846 assert_eq!(provider.resolve("c"), Some(record("c")));
847 assert_eq!(
848 provider.refresh_count(),
849 3,
850 "reconcile ticks re-install whole-table state — \
851 idempotent by construction"
852 );
853 }
854
855 #[tokio::test]
856 async fn mem_backend_binds_end_to_end_put_resolve_remove() {
857 let (_, store) = mem_registry_pair();
858
859 store.put_repo(&record("demo")).await.unwrap();
860 assert_eq!(
861 store.resolve("demo").await.unwrap(),
862 record("demo"),
863 "mem-backed end-to-end: put is hot-visible through the \
864 write-through provider"
865 );
866
867 let mut granted = record("demo");
868 granted
869 .grants
870 .insert("u-7731".to_string(), vec![alkgit::AccessAction::Read]);
871 store.update_repo(&granted).await.unwrap();
872 assert_eq!(store.resolve("demo").await.unwrap(), granted);
873
874 store.remove_repo("demo").await.unwrap();
875 assert_eq!(
876 store.resolve("demo").await.unwrap_err(),
877 RegistryError::NotFound,
878 "a resolve miss after removal maps to the collapse posture \
879 — NotFound, the definitive unresolved id"
880 );
881 }
882
883 #[tokio::test]
884 async fn mem_backend_error_map_matches_the_core_traits() {
885 let (_, store) = mem_registry_pair();
886 assert_eq!(
887 store.update_repo(&record("ghost")).await.unwrap_err(),
888 RegistryError::NotFound
889 );
890 assert_eq!(
891 store.remove_repo("ghost").await.unwrap_err(),
892 RegistryError::NotFound
893 );
894 store.put_repo(&record("demo")).await.unwrap();
895 assert_eq!(
896 store.put_repo(&record("demo")).await.unwrap_err(),
897 RegistryError::AlreadyExists
898 );
899
900 let mut malformed = record("Bad/id");
901 malformed.repo_id = "Bad/id".to_string();
902 assert_eq!(
903 store.put_repo(&malformed).await.unwrap_err(),
904 RegistryError::Invalid
905 );
906 assert_eq!(
907 store.resolve("demo").await.unwrap(),
908 record("demo"),
909 "the rejected mutation left the hot path untouched"
910 );
911 }
912
913 #[tokio::test]
914 async fn mem_backend_store_methods_flow_through_the_pair() {
915 let registry = Arc::new(InMemoryRegistry::new());
916 let provider = Arc::new(CachedRegistryProvider::new());
917 let store = RegistryBackend::with_cached_provider(
918 registry.clone() as Arc<dyn RegistryWrite>,
919 provider.clone(),
920 );
921
922 store.put_repo(&record("demo")).await.unwrap();
923 assert_eq!(registry.resolve("demo"), Some(record("demo")));
924 assert_eq!(registry.watermark().await.unwrap(), 1);
925 assert_eq!(
926 provider.resolve("demo"),
927 Some(record("demo")),
928 "the provider was written through by the store, not by a \
929 durable-side refresh"
930 );
931 assert_eq!(provider.refresh_count(), 1);
932
933 store.remove_repo("demo").await.unwrap();
934 assert_eq!(registry.resolve("demo"), None);
935 assert_eq!(registry.watermark().await.unwrap(), 2);
936 assert_eq!(provider.resolve("demo"), None);
937 }
938
939 #[tokio::test]
940 async fn backend_binds_over_an_external_read_side_without_write_through() {
941 let registry = Arc::new(InMemoryRegistry::new());
942 registry.put(&record("demo")).await.unwrap();
943 let store = RegistryBackend::new(registry.clone(), registry.clone());
944
945 assert_eq!(store.resolve("demo").await.unwrap(), record("demo"));
946 assert!(
947 store.provider().is_none(),
948 "no write-through target on the plain binding"
949 );
950 }
951
952 #[tokio::test]
953 async fn all_public_shapes_are_dyn_compatible() {
954 let (_, store) = mem_registry_pair();
955 let registry: Arc<dyn GitRegistry> = store.clone();
956 let store: Arc<dyn GitRegistryStore> = store;
957
958 let resolved = registry.resolve("nope").await;
959 assert_eq!(resolved.unwrap_err(), RegistryError::NotFound);
960 store.put_repo(&record("demo")).await.unwrap();
961 assert_eq!(registry.resolve("demo").await.unwrap(), record("demo"));
962 }
963
964 #[tokio::test]
965 async fn mem_registry_pair_exposes_the_write_side_for_the_reactor() {
966 let (write, store) = mem_registry_pair();
967 let seq = write.put(&record("demo")).await.unwrap();
968 assert_eq!(seq, 1);
969 assert_eq!(write.watermark().await.unwrap(), 1);
970 assert_eq!(
971 store.resolve("demo").await.unwrap_err(),
972 RegistryError::NotFound,
973 "a direct pair write rides the wake-driven refresh cadence — \
974 never write-through"
975 );
976 }
977
978 #[test]
979 fn repo_record_validation_rejects_the_pinned_shapes() {
980 let mut empty_key = record("demo");
981 empty_key
982 .grants
983 .insert(String::new(), vec![alkgit::AccessAction::Read]);
984 assert_eq!(validate_record(&empty_key), Err(RegistryError::Invalid));
985
986 let mut long_id = record("x");
987 long_id.repo_id = "a".repeat(101);
988 assert_eq!(validate_record(&long_id), Err(RegistryError::Invalid));
989
990 assert_eq!(validate_record(&record("owner/name")), Ok(()));
991 }
992}