1use std::{
2 collections::HashMap,
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 generation: u64,
135}
136
137impl Registry {
138 pub(crate) fn subscribe_module_changes(
141 &self,
142 module_id: &str,
143 ) -> Result<watch::Receiver<()>, RegistryError> {
144 let mut inner = self.lock_inner()?;
145 inner
147 .module_changes
148 .retain(|_, sender| sender.receiver_count() > 0);
149 Ok(inner
150 .module_changes
151 .entry(module_id.to_string())
152 .or_insert_with(|| watch::channel(()).0)
153 .subscribe())
154 }
155
156 pub fn register_with_control_ops(
158 &self,
159 manifest: ModuleManifest,
160 negotiated_ver: u8,
161 connection_id: ConnectionId,
162 control_ops: Vec<String>,
163 ) -> Result<ModuleRegistration, RegistryError> {
164 let module_id = manifest.module_id.clone();
165 if let Err(reason) = module_id_path_hazard(&module_id) {
166 return Err(RegistryError::PathHazardModuleId { module_id, reason });
167 }
168 let mut inner = self.lock_inner()?;
169 if inner.modules.contains_key(&module_id) {
170 return Err(RegistryError::DuplicateModuleId { module_id });
171 }
172
173 let ready = manifest.ready.unwrap_or(true);
174 let registration = ModuleRegistration {
175 manifest,
176 ready,
177 negotiated_ver,
178 state: ChannelState::Active,
179 connection_id,
180 control_ops,
181 };
182
183 inner.modules.insert(module_id, registration.clone());
184 inner.bump_generation();
185 inner.notify_module_changed(®istration.manifest.module_id);
186 Ok(registration)
187 }
188
189 pub fn register_candidate_with_control_ops(
198 &self,
199 manifest: ModuleManifest,
200 negotiated_ver: u8,
201 connection_id: ConnectionId,
202 control_ops: Vec<String>,
203 ) -> Result<ModuleRegistration, RegistryError> {
204 let module_id = manifest.module_id.clone();
205 if let Err(reason) = module_id_path_hazard(&module_id) {
206 return Err(RegistryError::PathHazardModuleId { module_id, reason });
207 }
208 let mut inner = self.lock_inner()?;
209 if inner.candidates.contains_key(&module_id) {
210 return Err(RegistryError::DuplicateModuleId { module_id });
211 }
212 let ready = manifest.ready.unwrap_or(true);
213 let registration = ModuleRegistration {
214 manifest,
215 ready,
216 negotiated_ver,
217 state: ChannelState::Active,
218 connection_id,
219 control_ops,
220 };
221 inner.candidates.insert(module_id, registration.clone());
222 inner.notify_module_changed(®istration.manifest.module_id);
223 Ok(registration)
224 }
225
226 pub fn promote_candidate(
232 &self,
233 module_id: &str,
234 ) -> Result<Option<RegistryCutover>, RegistryError> {
235 let mut inner = self.lock_inner()?;
236 let Some(promoted) = inner.candidates.remove(module_id) else {
237 return Ok(None);
238 };
239 let superseded = inner
240 .modules
241 .insert(module_id.to_string(), promoted.clone());
242 if let Some(superseded) = superseded.clone() {
243 let replaced_connection_id = superseded.connection_id.get();
244 inner
245 .superseded_by
246 .insert(superseded.connection_id, promoted.connection_id);
247 inner.superseded.push(superseded);
248 info!(
249 target: "subc.registry",
250 module_id = %module_id,
251 connection_id = promoted.connection_id.get(),
252 replaced = true,
253 replaced_connection_id,
254 "module registration promoted"
255 );
256 }
257 inner.bump_generation();
258 inner.notify_module_changed(module_id);
259 Ok(Some(RegistryCutover {
260 promoted,
261 superseded,
262 }))
263 }
264
265 pub fn get_module(&self, module_id: &str) -> Result<Option<ModuleRegistration>, RegistryError> {
268 Ok(self.lock_inner()?.modules.get(module_id).cloned())
269 }
270
271 pub fn get_candidate(
273 &self,
274 module_id: &str,
275 ) -> Result<Option<ModuleRegistration>, RegistryError> {
276 Ok(self.lock_inner()?.candidates.get(module_id).cloned())
277 }
278
279 pub fn registration(
281 &self,
282 slot: RegistrationSlot<'_>,
283 ) -> Result<Option<ModuleRegistration>, RegistryError> {
284 let inner = self.lock_inner()?;
285 Ok(match slot {
286 RegistrationSlot::Active(module_id) => inner.modules.get(module_id).cloned(),
287 RegistrationSlot::Candidate(module_id) => inner.candidates.get(module_id).cloned(),
288 RegistrationSlot::Connection(connection_id) => inner
289 .find_by_connection(connection_id)
290 .map(|(_, registration)| registration.clone()),
291 })
292 }
293
294 pub fn active_registration_count(&self) -> Result<usize, RegistryError> {
295 Ok(self.lock_inner()?.modules.len())
296 }
297
298 pub fn list_modules(&self) -> Result<(u64, Vec<ModuleRegistration>), RegistryError> {
299 let inner = self.lock_inner()?;
300 let mut modules = inner.modules.values().cloned().collect::<Vec<_>>();
301 modules.sort_by(|left, right| left.manifest.module_id.cmp(&right.manifest.module_id));
302 Ok((inner.generation, modules))
303 }
304
305 pub fn generation(&self) -> Result<u64, RegistryError> {
306 Ok(self.lock_inner()?.generation)
307 }
308
309 #[cfg(test)]
310 pub(crate) fn set_module_state_for_test(
311 &self,
312 module_id: &str,
313 state: ChannelState,
314 ) -> Result<bool, RegistryError> {
315 let mut inner = self.lock_inner()?;
316 let Some(registration) = inner.modules.get_mut(module_id) else {
317 return Ok(false);
318 };
319 registration.state = state;
320 inner.notify_module_changed(module_id);
321 Ok(true)
322 }
323
324 pub fn get_module_by_connection(
327 &self,
328 connection_id: ConnectionId,
329 ) -> Result<Option<ModuleRegistration>, RegistryError> {
330 Ok(self
331 .lock_inner()?
332 .find_by_connection(connection_id)
333 .map(|(_, registration)| registration.clone()))
334 }
335
336 pub fn replace_catalog_for_connection(
345 &self,
346 connection_id: ConnectionId,
347 provides: Vec<ProviderRole>,
348 capabilities: Option<CapabilityDeclarations>,
349 ready: Option<bool>,
350 ) -> Result<Option<ModuleRegistration>, RegistryError> {
351 let mut inner = self.lock_inner()?;
352 let Some((slot, _)) = inner.find_by_connection(connection_id) else {
353 return Ok(None);
354 };
355 let registration = inner
356 .registration_mut(slot, connection_id)
357 .expect("registration discovered under the same registry lock must still exist");
358 registration.manifest.provides = provides;
359 if let Some(capabilities) = capabilities {
360 registration.manifest.capabilities = Some(capabilities);
361 }
362 if let Some(ready) = ready {
363 registration.ready = ready;
364 registration.manifest.ready = Some(ready);
365 }
366 let updated = registration.clone();
367 if matches!(slot, SlotKind::Active) {
368 inner.bump_generation();
369 }
370 inner.notify_module_changed(&updated.manifest.module_id);
371 Ok(Some(updated))
372 }
373
374 pub fn deregister_connection(
376 &self,
377 connection_id: ConnectionId,
378 ) -> Result<Vec<ModuleRegistration>, RegistryError> {
379 self.deregister_connection_with_reason(
380 connection_id,
381 RegistrationEndReason::ConnectionClosed,
382 )
383 }
384
385 pub(crate) fn deregister_connection_with_reason(
386 &self,
387 connection_id: ConnectionId,
388 reason: RegistrationEndReason,
389 ) -> Result<Vec<ModuleRegistration>, RegistryError> {
390 let mut inner = self.lock_inner()?;
391 let module_ids: Vec<String> = inner
392 .modules
393 .iter()
394 .filter(|(_, registration)| registration.connection_id == connection_id)
395 .map(|(module_id, _)| module_id.clone())
396 .collect();
397
398 let mut closed: Vec<ModuleRegistration> = module_ids
399 .into_iter()
400 .filter_map(|module_id| inner.close_module(&module_id))
401 .collect();
402
403 let candidate_ids: Vec<String> = inner
404 .candidates
405 .iter()
406 .filter(|(_, registration)| registration.connection_id == connection_id)
407 .map(|(module_id, _)| module_id.clone())
408 .collect();
409 for module_id in candidate_ids {
410 if let Some(mut registration) = inner.candidates.remove(&module_id) {
411 registration.state = ChannelState::Closed;
412 closed.push(registration);
413 }
414 }
415
416 let (removed, kept): (Vec<_>, Vec<_>) = std::mem::take(&mut inner.superseded)
417 .into_iter()
418 .partition(|registration| registration.connection_id == connection_id);
419 let superseded_connections: std::collections::HashSet<_> = removed
420 .iter()
421 .map(|registration| registration.connection_id)
422 .collect();
423 inner.superseded = kept;
424 closed.extend(removed.into_iter().map(|mut registration| {
425 registration.state = ChannelState::Closed;
426 registration
427 }));
428 for registration in &closed {
429 inner.notify_module_changed(®istration.manifest.module_id);
430 let replaced = superseded_connections.contains(®istration.connection_id);
431 let end_reason = if replaced {
432 RegistrationEndReason::Replaced
433 } else {
434 reason
435 };
436 let replaced_by_connection_id = if replaced {
437 inner
438 .superseded_by
439 .remove(®istration.connection_id)
440 .map(ConnectionId::get)
441 } else {
442 None
443 };
444 if let Some(replaced_by_connection_id) = replaced_by_connection_id {
445 info!(
446 target: "subc.registry",
447 module_id = %registration.manifest.module_id,
448 connection_id = registration.connection_id.get(),
449 reason = %end_reason.as_str(),
450 replaced_by_connection_id,
451 "module registration ended"
452 );
453 continue;
454 }
455 info!(
456 target: "subc.registry",
457 module_id = %registration.manifest.module_id,
458 connection_id = registration.connection_id.get(),
459 reason = %end_reason.as_str(),
460 "module registration ended"
461 );
462 }
463 Ok(closed)
464 }
465
466 fn lock_inner(&self) -> Result<MutexGuard<'_, RegistryInner>, RegistryError> {
467 self.inner.lock().map_err(|_| RegistryError::Poisoned)
468 }
469}
470
471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
473enum SlotKind {
474 Active,
475 Candidate,
476 Superseded,
477}
478
479impl RegistryInner {
480 fn notify_module_changed(&self, module_id: &str) {
481 if let Some(sender) = self.module_changes.get(module_id) {
482 sender.send_replace(());
483 }
484 }
485
486 fn find_by_connection(
487 &self,
488 connection_id: ConnectionId,
489 ) -> Option<(SlotKind, &ModuleRegistration)> {
490 let owned_by =
491 |registration: &&ModuleRegistration| registration.connection_id == connection_id;
492 self.modules
493 .values()
494 .find(owned_by)
495 .map(|registration| (SlotKind::Active, registration))
496 .or_else(|| {
497 self.candidates
498 .values()
499 .find(owned_by)
500 .map(|registration| (SlotKind::Candidate, registration))
501 })
502 .or_else(|| {
503 self.superseded
504 .iter()
505 .find(owned_by)
506 .map(|registration| (SlotKind::Superseded, registration))
507 })
508 }
509
510 fn registration_mut(
511 &mut self,
512 slot: SlotKind,
513 connection_id: ConnectionId,
514 ) -> Option<&mut ModuleRegistration> {
515 let owned_by =
516 |registration: &&mut ModuleRegistration| registration.connection_id == connection_id;
517 match slot {
518 SlotKind::Active => self.modules.values_mut().find(owned_by),
519 SlotKind::Candidate => self.candidates.values_mut().find(owned_by),
520 SlotKind::Superseded => self.superseded.iter_mut().find(owned_by),
521 }
522 }
523
524 fn close_module(&mut self, module_id: &str) -> Option<ModuleRegistration> {
525 let mut registration = self.modules.remove(module_id)?;
526 registration.state = ChannelState::Closed;
527 self.bump_generation();
528 Some(registration)
529 }
530
531 fn bump_generation(&mut self) {
532 self.generation = self.generation.wrapping_add(1);
533 }
534}
535
536#[derive(Debug, Clone, PartialEq, Eq)]
537pub enum RegistryError {
538 DuplicateModuleId {
539 module_id: String,
540 },
541 PathHazardModuleId {
550 module_id: String,
551 reason: String,
552 },
553 Poisoned,
554}
555
556impl fmt::Display for RegistryError {
557 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558 match self {
559 Self::DuplicateModuleId { module_id } => {
560 write!(f, "module_id '{module_id}' is already registered")
561 }
562 Self::PathHazardModuleId { module_id, reason } => {
563 write!(
564 f,
565 "module_id '{}' is not usable as a path component: {reason}",
566 module_id.escape_debug()
567 )
568 }
569 Self::Poisoned => write!(f, "registry lock was poisoned"),
570 }
571 }
572}
573
574impl Error for RegistryError {}
575
576pub fn module_id_path_hazard(module_id: &str) -> Result<(), String> {
586 if module_id.is_empty() {
587 return Err("empty".to_string());
588 }
589 if module_id.contains('/') || module_id.contains('\\') {
590 return Err("contains a path separator".to_string());
591 }
592 if module_id == "." || module_id == ".." {
593 return Err("is a dot path component".to_string());
594 }
595 if module_id.chars().any(|c| c.is_control()) {
596 return Err("contains a control character".to_string());
597 }
598 if module_id.len() > 255 {
602 return Err("is longer than 255 bytes".to_string());
603 }
604 Ok(())
605}
606
607#[cfg(test)]
608mod change_notification_tests {
609 use super::*;
610
611 fn manifest() -> ModuleManifest {
612 ModuleManifest::builder("watched", "1.0.0")
613 .protocol_ver(1)
614 .build()
615 }
616
617 fn changed(receiver: &mut watch::Receiver<()>) {
618 assert!(
619 receiver.has_changed().unwrap(),
620 "registry write must notify"
621 );
622 receiver.borrow_and_update();
623 }
624
625 #[test]
626 fn every_registration_write_notifies_only_its_module() {
627 let registry = Registry::default();
628 let mut events = registry.subscribe_module_changes("watched").unwrap();
629 let other = registry.subscribe_module_changes("other").unwrap();
630 let active = ConnectionId::new(1);
631 let candidate = ConnectionId::new(2);
632 registry
633 .register_with_control_ops(manifest(), 1, active, vec![])
634 .unwrap();
635 changed(&mut events);
636 registry
637 .set_module_state_for_test("watched", ChannelState::Active)
638 .unwrap();
639 changed(&mut events);
640 registry
641 .replace_catalog_for_connection(active, vec![], None, Some(true))
642 .unwrap();
643 changed(&mut events);
644 registry
645 .register_candidate_with_control_ops(manifest(), 1, candidate, vec![])
646 .unwrap();
647 changed(&mut events);
648 registry
649 .replace_catalog_for_connection(candidate, vec![], None, Some(true))
650 .unwrap();
651 changed(&mut events);
652 registry.promote_candidate("watched").unwrap().unwrap();
653 changed(&mut events);
654 registry
656 .replace_catalog_for_connection(active, vec![], None, Some(false))
657 .unwrap();
658 changed(&mut events);
659 registry.deregister_connection(active).unwrap();
660 changed(&mut events);
661 registry.deregister_connection(candidate).unwrap();
662 changed(&mut events);
663 registry
664 .register_candidate_with_control_ops(manifest(), 1, candidate, vec![])
665 .unwrap();
666 changed(&mut events);
667 registry.deregister_connection(candidate).unwrap();
668 changed(&mut events);
669 assert!(
670 !other.has_changed().unwrap(),
671 "other module must stay parked"
672 );
673 }
674
675 #[test]
676 fn subscription_before_read_keeps_a_change_until_waited_on() {
677 let registry = Registry::default();
678 let mut events = registry.subscribe_module_changes("watched").unwrap();
679 assert!(registry.get_module("watched").unwrap().is_none());
680 registry
681 .register_with_control_ops(manifest(), 1, ConnectionId::new(1), vec![])
682 .unwrap();
683 registry.get_module("watched").unwrap().unwrap();
685 registry
686 .replace_catalog_for_connection(ConnectionId::new(1), vec![], None, Some(true))
687 .unwrap();
688 changed(&mut events);
689 assert!(!events.has_changed().unwrap());
690 }
691}
692
693#[cfg(test)]
694mod path_hazard_tests {
695 use super::*;
696 use crate::ConnectionId;
697 use subc_protocol::manifest::ModuleManifest;
698
699 fn manifest(module_id: &str) -> ModuleManifest {
700 ModuleManifest::builder(module_id, "0.1.0")
701 .protocol_ver(1)
702 .build()
703 }
704
705 #[test]
706 fn path_hazard_ids_are_refused_and_nothing_registers() {
707 let registry = Registry::default();
708 for (bad, reason_fragment) in [
709 ("../escape", "path separator"),
710 ("a/b", "path separator"),
711 ("a\\b", "path separator"),
712 ("..", "dot path component"),
713 (".", "dot path component"),
714 ("", "empty"),
715 ("evil\u{0}id", "control character"),
716 ] {
717 let err = registry
718 .register_with_control_ops(manifest(bad), 1, ConnectionId::new(7), Vec::new())
719 .expect_err("path-hazard id must refuse");
720 assert!(
722 err.to_string().contains(reason_fragment),
723 "id {bad:?}: expected {reason_fragment:?} in {err}"
724 );
725 }
726 assert_eq!(registry.active_registration_count().unwrap(), 0);
729 assert_eq!(registry.generation().unwrap(), 0);
730 }
731
732 #[test]
733 fn module_id_path_component_length_matches_shared_refusal_vectors() {
734 let doc: serde_json::Value = serde_json::from_str(include_str!(
735 "../tests/golden/module_id_path_component_refusals.json"
736 ))
737 .expect("refusal fixture parses");
738
739 for case in doc["vectors"].as_array().expect("vectors array") {
740 let name = case["name"].as_str().expect("name");
741 let module_id = case["module_id"]["unit"]
742 .as_str()
743 .expect("module_id unit")
744 .repeat(
745 case["module_id"]["repeat"]
746 .as_u64()
747 .expect("module_id repeat") as usize,
748 );
749 assert_eq!(
750 module_id.len(),
751 case["utf8_bytes"].as_u64().expect("utf8 bytes") as usize
752 );
753
754 let expected = case["expect_reason"].as_str().map(str::to_owned);
755 assert_eq!(
756 module_id_path_hazard(&module_id).err(),
757 expected,
758 "shared refusal vector {name:?} diverged"
759 );
760 }
761 }
762
763 #[test]
764 fn working_id_shapes_register_including_namespace_colons() {
765 let registry = Registry::default();
766 for (i, good) in ["magic-context", "mcp:everything", "v1.2-module"]
767 .iter()
768 .enumerate()
769 {
770 registry
771 .register_with_control_ops(
772 manifest(good),
773 1,
774 ConnectionId::new(10 + i as u64),
775 Vec::new(),
776 )
777 .unwrap_or_else(|err| panic!("id {good:?} must register: {err}"));
778 }
779 assert_eq!(registry.active_registration_count().unwrap(), 3);
780 }
781}
782
783#[cfg(test)]
784mod swap_slot_tests {
785 use super::*;
786
787 fn manifest(module_id: &str, ready: Option<bool>) -> ModuleManifest {
788 let mut manifest = ModuleManifest::builder(module_id, "0.1.0").build();
789 manifest.ready = ready;
790 manifest
791 }
792
793 const INCUMBENT: ConnectionId = ConnectionId(1);
794 const CANDIDATE: ConnectionId = ConnectionId(2);
795
796 fn registry_with_candidate() -> Registry {
797 let registry = Registry::default();
798 registry
799 .register_with_control_ops(manifest("m", None), 1, INCUMBENT, Vec::new())
800 .unwrap();
801 registry
802 .register_candidate_with_control_ops(
803 manifest("m", Some(false)),
804 1,
805 CANDIDATE,
806 Vec::new(),
807 )
808 .unwrap();
809 registry
810 }
811
812 #[test]
813 fn candidate_is_invisible_to_by_id_lookups_and_listing() {
814 let registry = registry_with_candidate();
815 let generation = registry.generation().unwrap();
816 assert_eq!(
817 registry.get_module("m").unwrap().unwrap().connection_id,
818 INCUMBENT
819 );
820 let (listed_generation, listed) = registry.list_modules().unwrap();
821 assert_eq!(listed.len(), 1);
822 assert_eq!(listed[0].connection_id, INCUMBENT);
823 assert_eq!(listed_generation, generation);
824 assert_eq!(registry.active_registration_count().unwrap(), 1);
825 assert_eq!(
826 registry.get_candidate("m").unwrap().unwrap().connection_id,
827 CANDIDATE
828 );
829 assert_eq!(
830 registry
831 .register_candidate_with_control_ops(
832 manifest("m", None),
833 1,
834 ConnectionId(3),
835 Vec::new()
836 )
837 .unwrap_err(),
838 RegistryError::DuplicateModuleId {
839 module_id: "m".to_string()
840 }
841 );
842 }
843
844 #[test]
847 fn candidate_catalog_update_reaches_the_candidate_registration() {
848 let registry = registry_with_candidate();
849 assert!(!registry.get_candidate("m").unwrap().unwrap().ready);
850
851 let updated = registry
852 .replace_catalog_for_connection(CANDIDATE, Vec::new(), None, Some(true))
853 .unwrap()
854 .expect("the candidate's own connection finds its registration");
855
856 assert_eq!(updated.connection_id, CANDIDATE);
857 assert!(registry.get_candidate("m").unwrap().unwrap().ready);
858 assert_eq!(
859 registry.get_module_by_connection(CANDIDATE).unwrap(),
860 Some(updated)
861 );
862 assert_eq!(
863 registry.get_module("m").unwrap().unwrap().connection_id,
864 INCUMBENT,
865 "a candidate's update must not touch the active registration"
866 );
867 }
868
869 #[test]
870 fn promotion_swaps_slots_and_each_connection_still_deregisters_its_own() {
871 let registry = registry_with_candidate();
872 let before = registry.generation().unwrap();
873 let cutover = registry.promote_candidate("m").unwrap().unwrap();
874 assert_eq!(cutover.promoted.connection_id, CANDIDATE);
875 assert_eq!(cutover.superseded.unwrap().connection_id, INCUMBENT);
876 assert_ne!(registry.generation().unwrap(), before);
877 assert_eq!(registry.promote_candidate("m").unwrap(), None);
878
879 assert_eq!(
880 registry
881 .registration(RegistrationSlot::Active("m"))
882 .unwrap()
883 .unwrap()
884 .connection_id,
885 CANDIDATE
886 );
887 assert!(registry
888 .registration(RegistrationSlot::Candidate("m"))
889 .unwrap()
890 .is_none());
891 assert!(registry
892 .registration(RegistrationSlot::Connection(INCUMBENT))
893 .unwrap()
894 .is_some());
895
896 let closed = registry.deregister_connection(INCUMBENT).unwrap();
897 assert_eq!(closed.len(), 1);
898 assert_eq!(closed[0].connection_id, INCUMBENT);
899 assert_eq!(closed[0].state, ChannelState::Closed);
900 assert!(registry
901 .registration(RegistrationSlot::Connection(INCUMBENT))
902 .unwrap()
903 .is_none());
904 assert_eq!(
905 registry.get_module("m").unwrap().unwrap().connection_id,
906 CANDIDATE
907 );
908 }
909
910 #[test]
911 fn a_dropped_candidate_deregisters_from_the_candidate_slot_only() {
912 let registry = registry_with_candidate();
913 let closed = registry.deregister_connection(CANDIDATE).unwrap();
914 assert_eq!(closed.len(), 1);
915 assert_eq!(closed[0].connection_id, CANDIDATE);
916 assert!(registry.get_candidate("m").unwrap().is_none());
917 assert_eq!(
918 registry.get_module("m").unwrap().unwrap().connection_id,
919 INCUMBENT
920 );
921 }
922}