1use std::{
2 collections::{HashMap, HashSet},
3 error::Error,
4 fmt,
5 sync::{Mutex, MutexGuard},
6};
7
8use subc_protocol::manifest::{CapabilityDeclarations, ModuleManifest, ProviderRole};
9use tokio::sync::watch;
10use tracing::info;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct ConnectionId(u64);
15
16impl ConnectionId {
17 #[cfg(test)]
20 pub const LOCAL: Self = Self(0);
21
22 pub const fn new(raw: u64) -> Self {
23 Self(raw)
24 }
25
26 pub fn get(self) -> u64 {
27 self.0
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ChannelState {
34 Active,
35 Closed,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub(crate) enum RegistrationEndReason {
41 ConnectionClosed,
42 ExplicitGoodbye,
43 Replaced,
44 SupervisorStop,
45 SupervisorRestart,
46 RegistrationFailed,
47}
48
49impl RegistrationEndReason {
50 fn as_str(self) -> &'static str {
51 match self {
52 Self::ConnectionClosed => "connection_closed",
53 Self::ExplicitGoodbye => "explicit_goodbye",
54 Self::Replaced => "replaced",
55 Self::SupervisorStop => "supervisor_stop",
56 Self::SupervisorRestart => "supervisor_restart",
57 Self::RegistrationFailed => "registration_failed",
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq)]
64pub struct ModuleRegistration {
65 pub manifest: ModuleManifest,
66 pub ready: bool,
67 pub negotiated_ver: u8,
68 pub state: ChannelState,
69 pub connection_id: ConnectionId,
70 pub control_ops: Vec<String>,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum RegistrationSlot<'a> {
83 Active(&'a str),
86 Candidate(&'a str),
88 Connection(ConnectionId),
90}
91
92#[derive(Debug, Clone, PartialEq)]
94pub struct RegistryCutover {
95 pub promoted: ModuleRegistration,
97 pub superseded: Option<ModuleRegistration>,
100}
101
102#[derive(Debug, Default)]
117pub struct Registry {
118 inner: Mutex<RegistryInner>,
119}
120
121#[derive(Debug, Default)]
122struct RegistryInner {
123 module_changes: HashMap<String, watch::Sender<()>>,
126 modules: HashMap<String, ModuleRegistration>,
127 candidates: HashMap<String, ModuleRegistration>,
129 superseded: Vec<ModuleRegistration>,
132 superseded_by: HashMap<ConnectionId, ConnectionId>,
134 unpublished: HashSet<ConnectionId>,
148 generation: u64,
149}
150
151impl Registry {
152 pub(crate) fn subscribe_module_changes(
155 &self,
156 module_id: &str,
157 ) -> Result<watch::Receiver<()>, RegistryError> {
158 let mut inner = self.lock_inner()?;
159 inner
161 .module_changes
162 .retain(|_, sender| sender.receiver_count() > 0);
163 Ok(inner
164 .module_changes
165 .entry(module_id.to_string())
166 .or_insert_with(|| watch::channel(()).0)
167 .subscribe())
168 }
169
170 pub fn register_with_control_ops(
177 &self,
178 manifest: ModuleManifest,
179 negotiated_ver: u8,
180 connection_id: ConnectionId,
181 control_ops: Vec<String>,
182 ) -> Result<ModuleRegistration, RegistryError> {
183 self.register_active(manifest, negotiated_ver, connection_id, control_ops, true)
184 }
185
186 pub fn register_unpublished_with_control_ops(
195 &self,
196 manifest: ModuleManifest,
197 negotiated_ver: u8,
198 connection_id: ConnectionId,
199 control_ops: Vec<String>,
200 ) -> Result<ModuleRegistration, RegistryError> {
201 self.register_active(manifest, negotiated_ver, connection_id, control_ops, false)
202 }
203
204 fn register_active(
205 &self,
206 manifest: ModuleManifest,
207 negotiated_ver: u8,
208 connection_id: ConnectionId,
209 control_ops: Vec<String>,
210 publish: bool,
211 ) -> Result<ModuleRegistration, RegistryError> {
212 let module_id = manifest.module_id.clone();
213 if let Err(reason) = module_id_path_hazard(&module_id) {
214 return Err(RegistryError::PathHazardModuleId { module_id, reason });
215 }
216 let mut inner = self.lock_inner()?;
217 if inner.modules.contains_key(&module_id) {
218 return Err(RegistryError::DuplicateModuleId { module_id });
219 }
220
221 let ready = manifest.ready.unwrap_or(true);
222 let registration = ModuleRegistration {
223 manifest,
224 ready,
225 negotiated_ver,
226 state: ChannelState::Active,
227 connection_id,
228 control_ops,
229 };
230
231 inner.modules.insert(module_id, registration.clone());
232 if publish {
233 inner.bump_generation();
234 } else {
235 inner.unpublished.insert(connection_id);
236 }
237 inner.notify_module_changed(®istration.manifest.module_id);
238 Ok(registration)
239 }
240
241 pub fn register_candidate_with_control_ops(
255 &self,
256 manifest: ModuleManifest,
257 negotiated_ver: u8,
258 connection_id: ConnectionId,
259 control_ops: Vec<String>,
260 ) -> Result<ModuleRegistration, RegistryError> {
261 self.register_candidate(manifest, negotiated_ver, connection_id, control_ops, true)
262 }
263
264 pub fn register_unpublished_candidate_with_control_ops(
271 &self,
272 manifest: ModuleManifest,
273 negotiated_ver: u8,
274 connection_id: ConnectionId,
275 control_ops: Vec<String>,
276 ) -> Result<ModuleRegistration, RegistryError> {
277 self.register_candidate(manifest, negotiated_ver, connection_id, control_ops, false)
278 }
279
280 fn register_candidate(
281 &self,
282 manifest: ModuleManifest,
283 negotiated_ver: u8,
284 connection_id: ConnectionId,
285 control_ops: Vec<String>,
286 publish: bool,
287 ) -> Result<ModuleRegistration, RegistryError> {
288 let module_id = manifest.module_id.clone();
289 if let Err(reason) = module_id_path_hazard(&module_id) {
290 return Err(RegistryError::PathHazardModuleId { module_id, reason });
291 }
292 let mut inner = self.lock_inner()?;
293 if inner.candidates.contains_key(&module_id) {
294 return Err(RegistryError::DuplicateModuleId { module_id });
295 }
296 let ready = manifest.ready.unwrap_or(true);
297 let registration = ModuleRegistration {
298 manifest,
299 ready,
300 negotiated_ver,
301 state: ChannelState::Active,
302 connection_id,
303 control_ops,
304 };
305 inner.candidates.insert(module_id, registration.clone());
306 if !publish {
307 inner.unpublished.insert(connection_id);
308 }
309 inner.notify_module_changed(®istration.manifest.module_id);
310 Ok(registration)
311 }
312
313 pub fn publish_registration(&self, connection_id: ConnectionId) -> Result<bool, RegistryError> {
325 let mut inner = self.lock_inner()?;
326 if !inner.unpublished.remove(&connection_id) {
327 return Ok(false);
328 }
329 let Some((slot, registration)) = inner.find_by_connection(connection_id) else {
330 return Ok(false);
331 };
332 let module_id = registration.manifest.module_id.clone();
333 if matches!(slot, SlotKind::Active) {
334 inner.bump_generation();
335 }
336 inner.notify_module_changed(&module_id);
337 Ok(true)
338 }
339
340 pub fn promote_candidate(
346 &self,
347 module_id: &str,
348 ) -> Result<Option<RegistryCutover>, RegistryError> {
349 let mut inner = self.lock_inner()?;
350 let Some(promoted) = inner.candidates.remove(module_id) else {
351 return Ok(None);
352 };
353 let superseded = inner
354 .modules
355 .insert(module_id.to_string(), promoted.clone());
356 if let Some(superseded) = superseded.clone() {
357 let replaced_connection_id = superseded.connection_id.get();
358 inner
359 .superseded_by
360 .insert(superseded.connection_id, promoted.connection_id);
361 inner.superseded.push(superseded);
362 info!(
363 target: "subc.registry",
364 module_id = %module_id,
365 connection_id = promoted.connection_id.get(),
366 replaced = true,
367 replaced_connection_id,
368 "module registration promoted"
369 );
370 }
371 inner.bump_generation();
372 inner.notify_module_changed(module_id);
373 Ok(Some(RegistryCutover {
374 promoted,
375 superseded,
376 }))
377 }
378
379 pub fn get_module(&self, module_id: &str) -> Result<Option<ModuleRegistration>, RegistryError> {
382 Ok(self.lock_inner()?.modules.get(module_id).cloned())
383 }
384
385 pub fn get_candidate(
387 &self,
388 module_id: &str,
389 ) -> Result<Option<ModuleRegistration>, RegistryError> {
390 Ok(self.lock_inner()?.candidates.get(module_id).cloned())
391 }
392
393 pub fn registration(
395 &self,
396 slot: RegistrationSlot<'_>,
397 ) -> Result<Option<ModuleRegistration>, RegistryError> {
398 let inner = self.lock_inner()?;
399 Ok(match slot {
400 RegistrationSlot::Active(module_id) => inner.modules.get(module_id).cloned(),
401 RegistrationSlot::Candidate(module_id) => inner.candidates.get(module_id).cloned(),
402 RegistrationSlot::Connection(connection_id) => inner
403 .find_by_connection(connection_id)
404 .map(|(_, registration)| registration.clone()),
405 })
406 }
407
408 pub fn active_registration_count(&self) -> Result<usize, RegistryError> {
409 Ok(self.lock_inner()?.modules.len())
410 }
411
412 pub fn list_modules(&self) -> Result<(u64, Vec<ModuleRegistration>), RegistryError> {
416 let inner = self.lock_inner()?;
417 let mut modules = inner
418 .modules
419 .values()
420 .filter(|registration| !inner.unpublished.contains(®istration.connection_id))
421 .cloned()
422 .collect::<Vec<_>>();
423 modules.sort_by(|left, right| left.manifest.module_id.cmp(&right.manifest.module_id));
424 Ok((inner.generation, modules))
425 }
426
427 pub fn list_registered_modules(&self) -> Result<Vec<ModuleRegistration>, RegistryError> {
433 let inner = self.lock_inner()?;
434 let mut modules = inner.modules.values().cloned().collect::<Vec<_>>();
435 modules.sort_by(|left, right| left.manifest.module_id.cmp(&right.manifest.module_id));
436 Ok(modules)
437 }
438
439 pub fn published_registration(
442 &self,
443 slot: RegistrationSlot<'_>,
444 ) -> Result<Option<ModuleRegistration>, RegistryError> {
445 let inner = self.lock_inner()?;
446 let registration = match slot {
447 RegistrationSlot::Active(module_id) => inner.modules.get(module_id),
448 RegistrationSlot::Candidate(module_id) => inner.candidates.get(module_id),
449 RegistrationSlot::Connection(connection_id) => inner
450 .find_by_connection(connection_id)
451 .map(|(_, registration)| registration),
452 };
453 Ok(registration
454 .filter(|registration| !inner.unpublished.contains(®istration.connection_id))
455 .cloned())
456 }
457
458 pub fn generation(&self) -> Result<u64, RegistryError> {
459 Ok(self.lock_inner()?.generation)
460 }
461
462 #[cfg(test)]
463 pub(crate) fn set_module_state_for_test(
464 &self,
465 module_id: &str,
466 state: ChannelState,
467 ) -> Result<bool, RegistryError> {
468 let mut inner = self.lock_inner()?;
469 let Some(registration) = inner.modules.get_mut(module_id) else {
470 return Ok(false);
471 };
472 registration.state = state;
473 inner.notify_module_changed(module_id);
474 Ok(true)
475 }
476
477 pub fn get_module_by_connection(
480 &self,
481 connection_id: ConnectionId,
482 ) -> Result<Option<ModuleRegistration>, RegistryError> {
483 Ok(self
484 .lock_inner()?
485 .find_by_connection(connection_id)
486 .map(|(_, registration)| registration.clone()))
487 }
488
489 pub fn replace_catalog_for_connection(
498 &self,
499 connection_id: ConnectionId,
500 provides: Vec<ProviderRole>,
501 capabilities: Option<CapabilityDeclarations>,
502 ready: Option<bool>,
503 ) -> Result<Option<ModuleRegistration>, RegistryError> {
504 let mut inner = self.lock_inner()?;
505 let Some((slot, _)) = inner.find_by_connection(connection_id) else {
506 return Ok(None);
507 };
508 let registration = inner
509 .registration_mut(slot, connection_id)
510 .expect("registration discovered under the same registry lock must still exist");
511 registration.manifest.provides = provides;
512 if let Some(capabilities) = capabilities {
513 registration.manifest.capabilities = Some(capabilities);
514 }
515 if let Some(ready) = ready {
516 registration.ready = ready;
517 registration.manifest.ready = Some(ready);
518 }
519 let updated = registration.clone();
520 if matches!(slot, SlotKind::Active) && !inner.unpublished.contains(&connection_id) {
521 inner.bump_generation();
522 }
523 inner.notify_module_changed(&updated.manifest.module_id);
524 Ok(Some(updated))
525 }
526
527 pub fn deregister_connection(
529 &self,
530 connection_id: ConnectionId,
531 ) -> Result<Vec<ModuleRegistration>, RegistryError> {
532 self.deregister_connection_with_reason(
533 connection_id,
534 RegistrationEndReason::ConnectionClosed,
535 )
536 }
537
538 pub(crate) fn deregister_connection_with_reason(
539 &self,
540 connection_id: ConnectionId,
541 reason: RegistrationEndReason,
542 ) -> Result<Vec<ModuleRegistration>, RegistryError> {
543 let mut inner = self.lock_inner()?;
544 let was_published = !inner.unpublished.remove(&connection_id);
547 let module_ids: Vec<String> = inner
548 .modules
549 .iter()
550 .filter(|(_, registration)| registration.connection_id == connection_id)
551 .map(|(module_id, _)| module_id.clone())
552 .collect();
553
554 let mut closed: Vec<ModuleRegistration> = module_ids
555 .into_iter()
556 .filter_map(|module_id| inner.close_module(&module_id, was_published))
557 .collect();
558
559 let candidate_ids: Vec<String> = inner
560 .candidates
561 .iter()
562 .filter(|(_, registration)| registration.connection_id == connection_id)
563 .map(|(module_id, _)| module_id.clone())
564 .collect();
565 for module_id in candidate_ids {
566 if let Some(mut registration) = inner.candidates.remove(&module_id) {
567 registration.state = ChannelState::Closed;
568 closed.push(registration);
569 }
570 }
571
572 let (removed, kept): (Vec<_>, Vec<_>) = std::mem::take(&mut inner.superseded)
573 .into_iter()
574 .partition(|registration| registration.connection_id == connection_id);
575 let superseded_connections: std::collections::HashSet<_> = removed
576 .iter()
577 .map(|registration| registration.connection_id)
578 .collect();
579 inner.superseded = kept;
580 closed.extend(removed.into_iter().map(|mut registration| {
581 registration.state = ChannelState::Closed;
582 registration
583 }));
584 for registration in &closed {
585 inner.notify_module_changed(®istration.manifest.module_id);
586 let replaced = superseded_connections.contains(®istration.connection_id);
587 let end_reason = if replaced {
588 RegistrationEndReason::Replaced
589 } else {
590 reason
591 };
592 let replaced_by_connection_id = if replaced {
593 inner
594 .superseded_by
595 .remove(®istration.connection_id)
596 .map(ConnectionId::get)
597 } else {
598 None
599 };
600 if let Some(replaced_by_connection_id) = replaced_by_connection_id {
601 info!(
602 target: "subc.registry",
603 module_id = %registration.manifest.module_id,
604 connection_id = registration.connection_id.get(),
605 reason = %end_reason.as_str(),
606 replaced_by_connection_id,
607 "module registration ended"
608 );
609 continue;
610 }
611 info!(
612 target: "subc.registry",
613 module_id = %registration.manifest.module_id,
614 connection_id = registration.connection_id.get(),
615 reason = %end_reason.as_str(),
616 "module registration ended"
617 );
618 }
619 Ok(closed)
620 }
621
622 fn lock_inner(&self) -> Result<MutexGuard<'_, RegistryInner>, RegistryError> {
623 self.inner.lock().map_err(|_| RegistryError::Poisoned)
624 }
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq)]
629enum SlotKind {
630 Active,
631 Candidate,
632 Superseded,
633}
634
635impl RegistryInner {
636 fn notify_module_changed(&self, module_id: &str) {
637 if let Some(sender) = self.module_changes.get(module_id) {
638 sender.send_replace(());
639 }
640 }
641
642 fn find_by_connection(
643 &self,
644 connection_id: ConnectionId,
645 ) -> Option<(SlotKind, &ModuleRegistration)> {
646 let owned_by =
647 |registration: &&ModuleRegistration| registration.connection_id == connection_id;
648 self.modules
649 .values()
650 .find(owned_by)
651 .map(|registration| (SlotKind::Active, registration))
652 .or_else(|| {
653 self.candidates
654 .values()
655 .find(owned_by)
656 .map(|registration| (SlotKind::Candidate, registration))
657 })
658 .or_else(|| {
659 self.superseded
660 .iter()
661 .find(owned_by)
662 .map(|registration| (SlotKind::Superseded, registration))
663 })
664 }
665
666 fn registration_mut(
667 &mut self,
668 slot: SlotKind,
669 connection_id: ConnectionId,
670 ) -> Option<&mut ModuleRegistration> {
671 let owned_by =
672 |registration: &&mut ModuleRegistration| registration.connection_id == connection_id;
673 match slot {
674 SlotKind::Active => self.modules.values_mut().find(owned_by),
675 SlotKind::Candidate => self.candidates.values_mut().find(owned_by),
676 SlotKind::Superseded => self.superseded.iter_mut().find(owned_by),
677 }
678 }
679
680 fn close_module(&mut self, module_id: &str, listed: bool) -> Option<ModuleRegistration> {
681 let mut registration = self.modules.remove(module_id)?;
682 registration.state = ChannelState::Closed;
683 if listed {
684 self.bump_generation();
685 }
686 Some(registration)
687 }
688
689 fn bump_generation(&mut self) {
690 self.generation = self.generation.wrapping_add(1);
691 }
692}
693
694#[derive(Debug, Clone, PartialEq, Eq)]
695pub enum RegistryError {
696 DuplicateModuleId {
697 module_id: String,
698 },
699 PathHazardModuleId {
708 module_id: String,
709 reason: String,
710 },
711 Poisoned,
712}
713
714impl fmt::Display for RegistryError {
715 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
716 match self {
717 Self::DuplicateModuleId { module_id } => {
718 write!(f, "module_id '{module_id}' is already registered")
719 }
720 Self::PathHazardModuleId { module_id, reason } => {
721 write!(
722 f,
723 "module_id '{}' is not usable as a path component: {reason}",
724 module_id.escape_debug()
725 )
726 }
727 Self::Poisoned => write!(f, "registry lock was poisoned"),
728 }
729 }
730}
731
732impl Error for RegistryError {}
733
734pub fn module_id_path_hazard(module_id: &str) -> Result<(), String> {
744 if module_id.is_empty() {
745 return Err("empty".to_string());
746 }
747 if module_id.contains('/') || module_id.contains('\\') {
748 return Err("contains a path separator".to_string());
749 }
750 if module_id == "." || module_id == ".." {
751 return Err("is a dot path component".to_string());
752 }
753 if module_id.chars().any(|c| c.is_control()) {
754 return Err("contains a control character".to_string());
755 }
756 if module_id.len() > 255 {
760 return Err("is longer than 255 bytes".to_string());
761 }
762 Ok(())
763}
764
765#[cfg(test)]
766mod change_notification_tests {
767 use super::*;
768
769 fn manifest() -> ModuleManifest {
770 ModuleManifest::builder("watched", "1.0.0")
771 .protocol_ver(1)
772 .build()
773 }
774
775 fn changed(receiver: &mut watch::Receiver<()>) {
776 assert!(
777 receiver.has_changed().unwrap(),
778 "registry write must notify"
779 );
780 receiver.borrow_and_update();
781 }
782
783 #[test]
784 fn every_registration_write_notifies_only_its_module() {
785 let registry = Registry::default();
786 let mut events = registry.subscribe_module_changes("watched").unwrap();
787 let other = registry.subscribe_module_changes("other").unwrap();
788 let active = ConnectionId::new(1);
789 let candidate = ConnectionId::new(2);
790 registry
791 .register_with_control_ops(manifest(), 1, active, vec![])
792 .unwrap();
793 changed(&mut events);
794 registry
795 .set_module_state_for_test("watched", ChannelState::Active)
796 .unwrap();
797 changed(&mut events);
798 registry
799 .replace_catalog_for_connection(active, vec![], None, Some(true))
800 .unwrap();
801 changed(&mut events);
802 registry
803 .register_candidate_with_control_ops(manifest(), 1, candidate, vec![])
804 .unwrap();
805 changed(&mut events);
806 registry
807 .replace_catalog_for_connection(candidate, vec![], None, Some(true))
808 .unwrap();
809 changed(&mut events);
810 registry.promote_candidate("watched").unwrap().unwrap();
811 changed(&mut events);
812 registry
814 .replace_catalog_for_connection(active, vec![], None, Some(false))
815 .unwrap();
816 changed(&mut events);
817 registry.deregister_connection(active).unwrap();
818 changed(&mut events);
819 registry.deregister_connection(candidate).unwrap();
820 changed(&mut events);
821 registry
822 .register_candidate_with_control_ops(manifest(), 1, candidate, vec![])
823 .unwrap();
824 changed(&mut events);
825 registry.deregister_connection(candidate).unwrap();
826 changed(&mut events);
827 assert!(
828 !other.has_changed().unwrap(),
829 "other module must stay parked"
830 );
831 }
832
833 #[test]
834 fn subscription_before_read_keeps_a_change_until_waited_on() {
835 let registry = Registry::default();
836 let mut events = registry.subscribe_module_changes("watched").unwrap();
837 assert!(registry.get_module("watched").unwrap().is_none());
838 registry
839 .register_with_control_ops(manifest(), 1, ConnectionId::new(1), vec![])
840 .unwrap();
841 registry.get_module("watched").unwrap().unwrap();
843 registry
844 .replace_catalog_for_connection(ConnectionId::new(1), vec![], None, Some(true))
845 .unwrap();
846 changed(&mut events);
847 assert!(!events.has_changed().unwrap());
848 }
849}
850
851#[cfg(test)]
852mod publication_tests {
853 use super::*;
854
855 fn manifest() -> ModuleManifest {
856 ModuleManifest::builder("m", "1.0.0")
857 .protocol_ver(1)
858 .build()
859 }
860
861 #[test]
865 fn unpublished_registration_is_unlisted_until_published() {
866 let registry = Registry::default();
867 let connection = ConnectionId::new(1);
868 registry
869 .register_unpublished_with_control_ops(manifest(), 1, connection, vec![])
870 .unwrap();
871 assert_eq!(registry.list_modules().unwrap(), (0, Vec::new()));
872 assert!(registry.get_module("m").unwrap().is_some());
873 assert_eq!(registry.list_registered_modules().unwrap().len(), 1);
874 assert!(registry
875 .published_registration(RegistrationSlot::Active("m"))
876 .unwrap()
877 .is_none());
878 assert!(matches!(
879 registry.register_with_control_ops(manifest(), 1, ConnectionId::new(2), vec![]),
880 Err(RegistryError::DuplicateModuleId { .. })
881 ));
882 registry
884 .replace_catalog_for_connection(connection, vec![], None, Some(true))
885 .unwrap();
886 assert_eq!(registry.generation().unwrap(), 0);
887
888 assert!(registry.publish_registration(connection).unwrap());
889 let (generation, listed) = registry.list_modules().unwrap();
890 assert_eq!(generation, 1);
891 assert_eq!(listed.len(), 1);
892 assert!(!registry.publish_registration(connection).unwrap());
893 assert_eq!(registry.generation().unwrap(), 1);
894 }
895
896 #[test]
899 fn deregistering_an_unpublished_registration_leaves_the_generation() {
900 let registry = Registry::default();
901 let connection = ConnectionId::new(1);
902 registry
903 .register_unpublished_with_control_ops(manifest(), 1, connection, vec![])
904 .unwrap();
905 assert_eq!(registry.deregister_connection(connection).unwrap().len(), 1);
906 assert_eq!(registry.list_modules().unwrap(), (0, Vec::new()));
907 assert!(!registry.publish_registration(connection).unwrap());
908 assert_eq!(registry.generation().unwrap(), 0);
909 }
910
911 #[test]
916 fn unpublished_candidate_is_hidden_from_published_lookups() {
917 let registry = Registry::default();
918 let candidate = ConnectionId::new(2);
919 registry
920 .register_unpublished_candidate_with_control_ops(manifest(), 1, candidate, vec![])
921 .unwrap();
922 assert!(registry
923 .registration(RegistrationSlot::Candidate("m"))
924 .unwrap()
925 .is_some());
926 assert!(registry
927 .published_registration(RegistrationSlot::Candidate("m"))
928 .unwrap()
929 .is_none());
930 assert!(registry
931 .published_registration(RegistrationSlot::Connection(candidate))
932 .unwrap()
933 .is_none());
934 assert!(registry.publish_registration(candidate).unwrap());
935 assert!(registry
936 .published_registration(RegistrationSlot::Candidate("m"))
937 .unwrap()
938 .is_some());
939 assert_eq!(registry.generation().unwrap(), 0);
940 }
941}
942
943#[cfg(test)]
944mod path_hazard_tests {
945 use super::*;
946 use crate::ConnectionId;
947 use subc_protocol::manifest::ModuleManifest;
948
949 fn manifest(module_id: &str) -> ModuleManifest {
950 ModuleManifest::builder(module_id, "0.1.0")
951 .protocol_ver(1)
952 .build()
953 }
954
955 #[test]
956 fn path_hazard_ids_are_refused_and_nothing_registers() {
957 let registry = Registry::default();
958 for (bad, reason_fragment) in [
959 ("../escape", "path separator"),
960 ("a/b", "path separator"),
961 ("a\\b", "path separator"),
962 ("..", "dot path component"),
963 (".", "dot path component"),
964 ("", "empty"),
965 ("evil\u{0}id", "control character"),
966 ] {
967 let err = registry
968 .register_with_control_ops(manifest(bad), 1, ConnectionId::new(7), Vec::new())
969 .expect_err("path-hazard id must refuse");
970 assert!(
972 err.to_string().contains(reason_fragment),
973 "id {bad:?}: expected {reason_fragment:?} in {err}"
974 );
975 }
976 assert_eq!(registry.active_registration_count().unwrap(), 0);
979 assert_eq!(registry.generation().unwrap(), 0);
980 }
981
982 #[test]
983 fn module_id_path_component_length_matches_shared_refusal_vectors() {
984 let doc: serde_json::Value = serde_json::from_str(include_str!(
985 "../tests/golden/module_id_path_component_refusals.json"
986 ))
987 .expect("refusal fixture parses");
988
989 for case in doc["vectors"].as_array().expect("vectors array") {
990 let name = case["name"].as_str().expect("name");
991 let module_id = case["module_id"]["unit"]
992 .as_str()
993 .expect("module_id unit")
994 .repeat(
995 case["module_id"]["repeat"]
996 .as_u64()
997 .expect("module_id repeat") as usize,
998 );
999 assert_eq!(
1000 module_id.len(),
1001 case["utf8_bytes"].as_u64().expect("utf8 bytes") as usize
1002 );
1003
1004 let expected = case["expect_reason"].as_str().map(str::to_owned);
1005 assert_eq!(
1006 module_id_path_hazard(&module_id).err(),
1007 expected,
1008 "shared refusal vector {name:?} diverged"
1009 );
1010 }
1011 }
1012
1013 #[test]
1014 fn working_id_shapes_register_including_namespace_colons() {
1015 let registry = Registry::default();
1016 for (i, good) in ["magic-context", "mcp:everything", "v1.2-module"]
1017 .iter()
1018 .enumerate()
1019 {
1020 registry
1021 .register_with_control_ops(
1022 manifest(good),
1023 1,
1024 ConnectionId::new(10 + i as u64),
1025 Vec::new(),
1026 )
1027 .unwrap_or_else(|err| panic!("id {good:?} must register: {err}"));
1028 }
1029 assert_eq!(registry.active_registration_count().unwrap(), 3);
1030 }
1031}
1032
1033#[cfg(test)]
1034mod swap_slot_tests {
1035 use super::*;
1036
1037 fn manifest(module_id: &str, ready: Option<bool>) -> ModuleManifest {
1038 let mut manifest = ModuleManifest::builder(module_id, "0.1.0").build();
1039 manifest.ready = ready;
1040 manifest
1041 }
1042
1043 const INCUMBENT: ConnectionId = ConnectionId(1);
1044 const CANDIDATE: ConnectionId = ConnectionId(2);
1045
1046 fn registry_with_candidate() -> Registry {
1047 let registry = Registry::default();
1048 registry
1049 .register_with_control_ops(manifest("m", None), 1, INCUMBENT, Vec::new())
1050 .unwrap();
1051 registry
1052 .register_candidate_with_control_ops(
1053 manifest("m", Some(false)),
1054 1,
1055 CANDIDATE,
1056 Vec::new(),
1057 )
1058 .unwrap();
1059 registry
1060 }
1061
1062 #[test]
1063 fn candidate_is_invisible_to_by_id_lookups_and_listing() {
1064 let registry = registry_with_candidate();
1065 let generation = registry.generation().unwrap();
1066 assert_eq!(
1067 registry.get_module("m").unwrap().unwrap().connection_id,
1068 INCUMBENT
1069 );
1070 let (listed_generation, listed) = registry.list_modules().unwrap();
1071 assert_eq!(listed.len(), 1);
1072 assert_eq!(listed[0].connection_id, INCUMBENT);
1073 assert_eq!(listed_generation, generation);
1074 assert_eq!(registry.active_registration_count().unwrap(), 1);
1075 assert_eq!(
1076 registry.get_candidate("m").unwrap().unwrap().connection_id,
1077 CANDIDATE
1078 );
1079 assert_eq!(
1080 registry
1081 .register_candidate_with_control_ops(
1082 manifest("m", None),
1083 1,
1084 ConnectionId(3),
1085 Vec::new()
1086 )
1087 .unwrap_err(),
1088 RegistryError::DuplicateModuleId {
1089 module_id: "m".to_string()
1090 }
1091 );
1092 }
1093
1094 #[test]
1097 fn candidate_catalog_update_reaches_the_candidate_registration() {
1098 let registry = registry_with_candidate();
1099 assert!(!registry.get_candidate("m").unwrap().unwrap().ready);
1100
1101 let updated = registry
1102 .replace_catalog_for_connection(CANDIDATE, Vec::new(), None, Some(true))
1103 .unwrap()
1104 .expect("the candidate's own connection finds its registration");
1105
1106 assert_eq!(updated.connection_id, CANDIDATE);
1107 assert!(registry.get_candidate("m").unwrap().unwrap().ready);
1108 assert_eq!(
1109 registry.get_module_by_connection(CANDIDATE).unwrap(),
1110 Some(updated)
1111 );
1112 assert_eq!(
1113 registry.get_module("m").unwrap().unwrap().connection_id,
1114 INCUMBENT,
1115 "a candidate's update must not touch the active registration"
1116 );
1117 }
1118
1119 #[test]
1120 fn promotion_swaps_slots_and_each_connection_still_deregisters_its_own() {
1121 let registry = registry_with_candidate();
1122 let before = registry.generation().unwrap();
1123 let cutover = registry.promote_candidate("m").unwrap().unwrap();
1124 assert_eq!(cutover.promoted.connection_id, CANDIDATE);
1125 assert_eq!(cutover.superseded.unwrap().connection_id, INCUMBENT);
1126 assert_ne!(registry.generation().unwrap(), before);
1127 assert_eq!(registry.promote_candidate("m").unwrap(), None);
1128
1129 assert_eq!(
1130 registry
1131 .registration(RegistrationSlot::Active("m"))
1132 .unwrap()
1133 .unwrap()
1134 .connection_id,
1135 CANDIDATE
1136 );
1137 assert!(registry
1138 .registration(RegistrationSlot::Candidate("m"))
1139 .unwrap()
1140 .is_none());
1141 assert!(registry
1142 .registration(RegistrationSlot::Connection(INCUMBENT))
1143 .unwrap()
1144 .is_some());
1145
1146 let closed = registry.deregister_connection(INCUMBENT).unwrap();
1147 assert_eq!(closed.len(), 1);
1148 assert_eq!(closed[0].connection_id, INCUMBENT);
1149 assert_eq!(closed[0].state, ChannelState::Closed);
1150 assert!(registry
1151 .registration(RegistrationSlot::Connection(INCUMBENT))
1152 .unwrap()
1153 .is_none());
1154 assert_eq!(
1155 registry.get_module("m").unwrap().unwrap().connection_id,
1156 CANDIDATE
1157 );
1158 }
1159
1160 #[test]
1161 fn a_dropped_candidate_deregisters_from_the_candidate_slot_only() {
1162 let registry = registry_with_candidate();
1163 let closed = registry.deregister_connection(CANDIDATE).unwrap();
1164 assert_eq!(closed.len(), 1);
1165 assert_eq!(closed[0].connection_id, CANDIDATE);
1166 assert!(registry.get_candidate("m").unwrap().is_none());
1167 assert_eq!(
1168 registry.get_module("m").unwrap().unwrap().connection_id,
1169 INCUMBENT
1170 );
1171 }
1172}