1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
14use std::sync::{Arc, Mutex};
15use std::time::Duration;
16
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20use super::broadcast::BroadcastProvider;
21use super::config::{BroadcastMode, MeshConfig, TrustConfig};
22use super::cooperation::{Cooperation, CooperationStatus};
23use super::identity::{NodeIdentity, PublicIdentity};
24use super::multicast::MulticastProvider;
25use super::protocol::{self, Refusal};
26use super::provider::{Beacon, MeshProvider, Observation, ProviderContext, ProviderStatus};
27use super::registry::{MeshRegistry, MeshSource, NodeRecord, Tallies};
28use super::rendezvous::{RegisterAnswer, RendezvousProvider};
29use super::trust;
30use super::MeshError;
31
32const RECV_TIMEOUT: Duration = Duration::from_millis(500);
34
35const MAX_CANDIDATES: usize = 32;
37
38#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
41pub struct Refusals {
42 pub oversized: u64,
44 pub malformed: u64,
46 pub version: u64,
48 pub bounds: u64,
50 pub stale: u64,
52 pub signature: u64,
54 pub self_heard: u64,
56 #[serde(default)]
59 pub overflow: u64,
60}
61
62#[derive(Default)]
63struct RefusalCounters {
64 oversized: AtomicU64,
65 malformed: AtomicU64,
66 version: AtomicU64,
67 bounds: AtomicU64,
68 stale: AtomicU64,
69 signature: AtomicU64,
70 self_heard: AtomicU64,
71 overflow: Arc<AtomicU64>,
73}
74
75impl RefusalCounters {
76 fn count(&self, refusal: &Refusal) {
77 let counter = match refusal {
78 Refusal::Oversized => &self.oversized,
79 Refusal::Malformed => &self.malformed,
80 Refusal::Version => &self.version,
81 Refusal::Bounds => &self.bounds,
82 Refusal::Stale => &self.stale,
83 Refusal::Signature => &self.signature,
84 };
85 counter.fetch_add(1, Ordering::Relaxed);
86 }
87
88 fn snapshot(&self) -> Refusals {
89 Refusals {
90 oversized: self.oversized.load(Ordering::Relaxed),
91 malformed: self.malformed.load(Ordering::Relaxed),
92 version: self.version.load(Ordering::Relaxed),
93 bounds: self.bounds.load(Ordering::Relaxed),
94 stale: self.stale.load(Ordering::Relaxed),
95 signature: self.signature.load(Ordering::Relaxed),
96 self_heard: self.self_heard.load(Ordering::Relaxed),
97 overflow: self.overflow.load(Ordering::Relaxed),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
104pub struct MeshStatus {
105 pub active: bool,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub reason: Option<String>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub identity: Option<PublicIdentity>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub trust_policy: Option<String>,
116 pub providers: Vec<ProviderStatus>,
118 pub tallies: Tallies,
120 pub refusals: Refusals,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub started_at: Option<String>,
125}
126
127struct Active {
128 beacon: Arc<Beacon>,
129 tx: super::provider::ObservationSender,
130 stop: Arc<AtomicBool>,
131 providers: Vec<Box<dyn MeshProvider>>,
132 trust: TrustConfig,
133 started_at: String,
134}
135
136pub struct MeshRuntime {
139 registry: Arc<MeshRegistry>,
140 refusals: Arc<RefusalCounters>,
141 state: Mutex<Option<Active>>,
142 reason: Mutex<String>,
144 cooperation: Mutex<Option<Arc<Cooperation>>>,
146 cooperation_reason: Mutex<Option<String>>,
148 decided: AtomicBool,
152}
153
154impl Default for MeshRuntime {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160impl MeshRuntime {
161 pub fn new() -> Self {
163 MeshRuntime {
164 registry: Arc::new(MeshRegistry::new()),
165 refusals: Arc::new(RefusalCounters::default()),
166 state: Mutex::new(None),
167 reason: Mutex::new(
168 "not active: no shared server activated the mesh in this process".into(),
169 ),
170 cooperation: Mutex::new(None),
171 cooperation_reason: Mutex::new(None),
172 decided: AtomicBool::new(false),
173 }
174 }
175
176 pub fn decided(&self) -> bool {
197 self.decided.load(Ordering::SeqCst)
198 }
199
200 pub fn attach_cooperation(&self, cooperation: Arc<Cooperation>) {
234 let mut slot = self.cooperation.lock().expect("mesh cooperation");
235 if slot.is_none() {
236 *slot = Some(cooperation);
237 }
238 }
239
240 pub fn decline_cooperation(&self, reason: &str) {
258 *self
259 .cooperation_reason
260 .lock()
261 .expect("mesh cooperation reason") = Some(reason.into());
262 }
263
264 pub fn cooperation(&self) -> Option<Arc<Cooperation>> {
296 self.cooperation
297 .lock()
298 .expect("mesh cooperation")
299 .as_ref()
300 .filter(|c| !c.stopped())
301 .cloned()
302 }
303
304 pub fn cooperation_status(&self) -> CooperationStatus {
325 if let Some(c) = self.cooperation() {
326 return c.status();
327 }
328 let reason = self
329 .cooperation_reason
330 .lock()
331 .expect("mesh cooperation reason")
332 .clone()
333 .unwrap_or_else(|| self.reason.lock().expect("mesh reason").clone());
334 CooperationStatus::inactive(&reason)
335 }
336
337 pub fn registry(&self) -> &Arc<MeshRegistry> {
339 &self.registry
340 }
341
342 pub fn decline(&self, reason: &str) {
345 let state = self.state.lock().expect("mesh state");
346 if state.is_none() {
347 *self.reason.lock().expect("mesh reason") = format!("not active: {reason}");
348 }
349 self.decided.store(true, Ordering::SeqCst);
350 }
351
352 pub fn activate(
356 &self,
357 config: &MeshConfig,
358 identity: NodeIdentity,
359 endpoints: Vec<String>,
360 repos: Vec<String>,
361 version: &str,
362 ) -> Result<(), MeshError> {
363 self.activate_as(config, Arc::new(identity), "", endpoints, repos, version)
364 }
365
366 pub fn activate_as(
406 &self,
407 config: &MeshConfig,
408 identity: Arc<NodeIdentity>,
409 runtime: &str,
410 endpoints: Vec<String>,
411 repos: Vec<String>,
412 version: &str,
413 ) -> Result<(), MeshError> {
414 self.decided.store(true, Ordering::SeqCst);
415 let mut state = self.state.lock().expect("mesh state");
416 if state.is_some() {
417 return Ok(());
418 }
419 if !config.enabled {
420 *self.reason.lock().expect("mesh reason") =
421 "not active: the mesh declaration is disabled".into();
422 return Ok(());
423 }
424 if let Some(why) = closed_to(config, &identity.public.public_key) {
425 *self.reason.lock().expect("mesh reason") = why;
426 return Ok(());
427 }
428 let beacon = Arc::new(
429 Beacon::new(
430 identity,
431 endpoints,
432 vec!["http".into(), "mcp".into(), "ws".into()],
433 repos,
434 version,
435 )
436 .with_runtime(runtime),
437 );
438 let (tx, rx) = super::provider::ObservationSender::counting(
439 super::provider::OBSERVATION_QUEUE,
440 Arc::clone(&self.refusals.overflow),
441 );
442 let stop = Arc::new(AtomicBool::new(false));
443 let ctx = ProviderContext {
444 tx: tx.clone(),
445 stop: Arc::clone(&stop),
446 beacon: Arc::clone(&beacon),
447 };
448
449 let mut providers: Vec<Box<dyn MeshProvider>> = Vec::new();
452 if config.multicast.enabled {
453 providers.push(Box::new(MulticastProvider::new(config.multicast.clone())));
454 }
455 if config.broadcast.mode != BroadcastMode::Disabled {
456 providers.push(Box::new(BroadcastProvider::new(
457 config.broadcast.clone(),
458 !config.multicast.enabled,
459 )));
460 }
461 if !config.rendezvous.endpoints.is_empty() {
462 providers.push(Box::new(RendezvousProvider::new(
463 config.rendezvous.endpoints.clone(),
464 config.rendezvous.interval_seconds,
465 )));
466 }
467 for provider in providers.iter_mut() {
468 if let Err(e) = provider.start(&ctx) {
469 tracing::warn!(provider = provider.id(), error = %e, "mesh provider did not start; the others run on");
470 }
471 }
472
473 {
475 let registry = Arc::clone(&self.registry);
476 let refusals = Arc::clone(&self.refusals);
477 let stop = Arc::clone(&stop);
478 let own = (
479 beacon.identity().public.node_id.clone(),
480 beacon.runtime().to_string(),
481 );
482 let trust = config.trust.clone();
483 let _ = std::thread::Builder::new()
484 .name("majordomus-mesh".into())
485 .spawn(move || {
486 while !stop.load(Ordering::SeqCst) {
487 let observation = match rx.recv_timeout(RECV_TIMEOUT) {
488 Ok(o) => o,
489 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
490 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
491 };
492 ingest(®istry, &refusals, &trust, &own, &observation);
493 }
494 });
495 }
496
497 *state = Some(Active {
498 beacon,
499 tx,
500 stop,
501 providers,
502 trust: config.trust.clone(),
503 started_at: crate::peers::rfc3339(std::time::SystemTime::now()),
504 });
505 Ok(())
506 }
507
508 pub fn attach(&self, mut provider: Box<dyn MeshProvider>) -> Result<(), MeshError> {
511 let mut state = self.state.lock().expect("mesh state");
512 let Some(active) = state.as_mut() else {
513 return Err(MeshError::Provider("the mesh is not active".into()));
514 };
515 let ctx = ProviderContext {
516 tx: active.tx.clone(),
517 stop: Arc::clone(&active.stop),
518 beacon: Arc::clone(&active.beacon),
519 };
520 let outcome = provider.start(&ctx);
521 active.providers.push(provider);
522 outcome
523 }
524
525 pub fn begin_stop(&self) {
540 if let Some(cooperation) = self.cooperation.lock().expect("mesh cooperation").as_ref() {
541 cooperation.begin_stop();
542 }
543 if let Some(active) = self.state.lock().expect("mesh state").as_ref() {
544 active.stop.store(true, Ordering::SeqCst);
545 }
546 }
547
548 pub fn stop(&self) {
551 if let Some(cooperation) = self.cooperation.lock().expect("mesh cooperation").as_ref() {
552 cooperation.stop();
553 }
554 let mut state = self.state.lock().expect("mesh state");
555 if let Some(active) = state.take() {
556 active.stop.store(true, Ordering::SeqCst);
557 *self.reason.lock().expect("mesh reason") = "not active: stopped".into();
558 }
559 }
560
561 pub fn status(&self) -> MeshStatus {
563 let state = self.state.lock().expect("mesh state");
564 let mut status = match state.as_ref() {
565 Some(active) => {
566 let mut providers: Vec<ProviderStatus> =
567 active.providers.iter().map(|p| p.status()).collect();
568 crate::order::canonical(&mut providers);
569 MeshStatus {
570 active: true,
571 reason: None,
572 identity: Some(active.beacon.identity().public.clone()),
573 trust_policy: Some(active.trust.policy.as_str().into()),
574 providers,
575 tallies: Tallies::default(),
576 refusals: Refusals::default(),
577 started_at: Some(active.started_at.clone()),
578 }
579 }
580 None => MeshStatus {
581 active: false,
582 reason: Some(self.reason.lock().expect("mesh reason").clone()),
583 identity: None,
584 trust_policy: None,
585 providers: Vec::new(),
586 tallies: Tallies::default(),
587 refusals: Refusals::default(),
588 started_at: None,
589 },
590 };
591 status.tallies = self.registry.tallies();
592 status.refusals = self.refusals.snapshot();
593 status
594 }
595
596 pub fn nodes(&self) -> Vec<NodeRecord> {
598 self.registry.list()
599 }
600
601 pub fn register(&self, envelope: &serde_json::Value, path: &str) -> RegisterAnswer {
606 let state = self.state.lock().expect("mesh state");
607 let Some(active) = state.as_ref() else {
608 return RegisterAnswer {
609 accepted: false,
610 refusal: Some("the mesh is not active on this node".into()),
611 candidates: Vec::new(),
612 };
613 };
614 let own = (
615 active.beacon.identity().public.node_id.clone(),
616 active.beacon.runtime().to_string(),
617 );
618 let trust = active.trust.clone();
619 let own_envelope = active.beacon.next_envelope();
620 drop(state);
621
622 let bytes = serde_json::to_vec(envelope).unwrap_or_default();
623 let refusal = match ingest(
624 &self.registry,
625 &self.refusals,
626 &trust,
627 &own,
628 &Observation {
629 source: MeshSource::Rendezvous,
630 path: path.into(),
631 bytes,
632 },
633 ) {
634 Ingest::Accepted => None,
635 Ingest::Own => Some("that is this node's own envelope".into()),
636 Ingest::Replay => Some("replayed".into()),
637 Ingest::Refused(r) => Some(r.to_string()),
638 };
639 let mut candidates = Vec::with_capacity(MAX_CANDIDATES + 1);
643 if refusal.is_none() {
644 candidates.extend(serde_json::to_value(&own_envelope).ok());
645 let repos: Vec<String> = envelope
646 .get("repos")
647 .and_then(|r| r.as_array())
648 .map(|r| {
649 r.iter()
650 .filter_map(|v| v.as_str().map(str::to_string))
651 .collect()
652 })
653 .unwrap_or_default();
654 candidates.extend(self.registry.candidates_for(MAX_CANDIDATES, &repos));
655 }
656 let candidates = candidates
657 .into_iter()
658 .filter_map(|v| serde_json::from_value(v).ok())
659 .collect();
660 RegisterAnswer {
661 accepted: refusal.is_none(),
662 refusal,
663 candidates,
664 }
665 }
666}
667
668enum Ingest {
669 Accepted,
670 Own,
671 Replay,
672 Refused(Refusal),
673}
674
675fn ingest(
678 registry: &MeshRegistry,
679 refusals: &RefusalCounters,
680 trust: &TrustConfig,
681 own: &(super::identity::NodeId, String),
682 observation: &Observation,
683) -> Ingest {
684 let envelope = match protocol::parse(&observation.bytes) {
685 Ok(e) => e,
686 Err(refusal) => {
687 refusals.count(&refusal);
688 return Ingest::Refused(refusal);
689 }
690 };
691 let Some(node_id) = envelope.adv.node_id() else {
692 refusals.count(&Refusal::Signature);
694 return Ingest::Refused(Refusal::Signature);
695 };
696 if node_id == own.0 && envelope.adv.rt == own.1 {
699 refusals.self_heard.fetch_add(1, Ordering::Relaxed);
700 return Ingest::Own;
701 }
702 let known_key = registry.known_key(&node_id);
703 let previous = registry.trust_of(&node_id);
704 let verdict = trust::evaluate(
705 &trust.policy,
706 &trust.allow,
707 &envelope.adv.pk,
708 known_key.as_deref(),
709 previous.as_ref(),
710 );
711 let raw = serde_json::to_value(&envelope).unwrap_or(serde_json::Value::Null);
712 if registry.observe(
713 &envelope.adv,
714 node_id,
715 verdict,
716 observation.source,
717 &observation.path,
718 raw,
719 ) {
720 Ingest::Accepted
721 } else {
722 Ingest::Replay
723 }
724}
725
726pub fn closed_to(config: &MeshConfig, public_key: &str) -> Option<String> {
751 if config.trust.policy == trust::TrustPolicy::Tofu
752 || config.trust.allow.iter().any(|k| k == public_key)
753 {
754 return None;
755 }
756 Some(format!(
757 "closed: this machine's key {public_key} is not in trust.allow of the {} declaration, so it neither announces nor dials; to join, list the key there (`majordomus mesh identity` prints it) and commit",
758 config.id
759 ))
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use crate::mesh::config::{MeshConfig, MulticastConfig};
766 use crate::mesh::identity::NodeIdentity;
767 use crate::mesh::protocol::advertise;
768
769 fn identity_in(dir: &tempfile::TempDir, name: &str) -> NodeIdentity {
770 NodeIdentity::load_or_create(&dir.path().join(name)).unwrap()
771 }
772
773 fn listed(identity: &NodeIdentity) -> MeshConfig {
775 let mut config = quiet_config();
776 config.trust.allow = vec![identity.public.public_key.clone()];
777 config
778 }
779
780 fn quiet_config() -> MeshConfig {
781 MeshConfig {
783 schema: "mesh/v1".into(),
784 kind: "mesh-declaration".into(),
785 id: "test".into(),
786 enabled: true,
787 multicast: MulticastConfig {
788 enabled: false,
789 ..MulticastConfig::default()
790 },
791 broadcast: Default::default(),
792 rendezvous: Default::default(),
793 trust: Default::default(),
794 cooperation: Default::default(),
795 }
796 }
797
798 struct SyntheticProvider {
802 datagrams: Vec<Vec<u8>>,
803 }
804
805 impl MeshProvider for SyntheticProvider {
806 fn id(&self) -> &'static str {
807 "synthetic"
808 }
809 fn start(&mut self, ctx: &ProviderContext) -> Result<(), MeshError> {
810 for bytes in self.datagrams.drain(..) {
811 let _ = ctx.tx.send(Observation {
812 source: MeshSource::Synthetic,
813 path: "test".into(),
814 bytes,
815 });
816 }
817 Ok(())
818 }
819 fn status(&self) -> ProviderStatus {
820 ProviderStatus {
821 id: "synthetic".into(),
822 state: crate::mesh::provider::MeshProviderState::Running,
823 detail: None,
824 sent: 0,
825 received: 0,
826 }
827 }
828 }
829
830 fn wait_for<F: Fn() -> bool>(what: &str, check: F) {
831 for _ in 0..100 {
832 if check() {
833 return;
834 }
835 std::thread::sleep(Duration::from_millis(20));
836 }
837 panic!("timed out waiting for {what}");
838 }
839
840 #[test]
841 fn a_synthetic_provider_reaches_the_registry_with_zero_registration() {
842 let dir = tempfile::tempdir().unwrap();
843 let runtime = MeshRuntime::new();
844 runtime
845 .activate(
846 &listed(&identity_in(&dir, "self.json")),
847 identity_in(&dir, "self.json"),
848 vec![],
849 vec![],
850 "0.5.0",
851 )
852 .unwrap();
853 let other = identity_in(&dir, "other.json");
854 let envelope = advertise(
855 &other,
856 1,
857 &["127.0.0.1:1".into()],
858 &["http".into()],
859 &[],
860 "0.5.0",
861 );
862 let bytes = serde_json::to_vec(&envelope).unwrap();
863 runtime
864 .attach(Box::new(SyntheticProvider {
865 datagrams: vec![bytes],
866 }))
867 .unwrap();
868 wait_for("the observation to land", || runtime.nodes().len() == 1);
869 let nodes = runtime.nodes();
870 assert_eq!(nodes[0].node_id, other.public.node_id);
871 assert!(
872 !nodes[0].trust.is_trusted(),
873 "deny_unknown observes, never trusts"
874 );
875 runtime.stop();
876 }
877
878 #[test]
879 fn own_datagrams_are_skipped_not_recorded() {
880 let dir = tempfile::tempdir().unwrap();
881 let runtime = MeshRuntime::new();
882 let own = identity_in(&dir, "self.json");
883 let own_envelope = advertise(&own, 1, &[], &[], &[], "0.5.0");
884 runtime
885 .activate(&listed(&own), own, vec![], vec![], "0.5.0")
886 .unwrap();
887 let bytes = serde_json::to_vec(&own_envelope).unwrap();
888 runtime
889 .attach(Box::new(SyntheticProvider {
890 datagrams: vec![bytes],
891 }))
892 .unwrap();
893 wait_for("the self-heard counter", || {
894 runtime.status().refusals.self_heard == 1
895 });
896 assert!(runtime.nodes().is_empty());
897 runtime.stop();
898 }
899
900 #[test]
901 fn garbage_from_a_provider_is_counted_and_dropped() {
902 let dir = tempfile::tempdir().unwrap();
903 let runtime = MeshRuntime::new();
904 runtime
905 .activate(
906 &listed(&identity_in(&dir, "self.json")),
907 identity_in(&dir, "self.json"),
908 vec![],
909 vec![],
910 "0.5.0",
911 )
912 .unwrap();
913 runtime
914 .attach(Box::new(SyntheticProvider {
915 datagrams: vec![b"garbage".to_vec(), vec![0xff; 2000]],
916 }))
917 .unwrap();
918 wait_for("both refusals", || {
919 let r = runtime.status().refusals;
920 r.malformed == 1 && r.oversized == 1
921 });
922 assert!(runtime.nodes().is_empty());
923 runtime.stop();
924 }
925
926 #[test]
927 fn registration_records_and_answers_with_own_envelope() {
928 let dir = tempfile::tempdir().unwrap();
929 let runtime = MeshRuntime::new();
930 let own = identity_in(&dir, "self.json");
931 let own_node = own.public.node_id.clone();
932 runtime
933 .activate(&listed(&own), own, vec![], vec![], "0.5.0")
934 .unwrap();
935 let caller = identity_in(&dir, "caller.json");
936 let envelope = advertise(&caller, 1, &["127.0.0.1:2".into()], &[], &[], "0.5.0");
937 let answer = runtime.register(&serde_json::to_value(&envelope).unwrap(), "test");
938 assert!(answer.accepted, "{:?}", answer.refusal);
939 assert_eq!(
940 answer.candidates[0].adv.node_id(),
941 Some(own_node),
942 "the answering node's own envelope leads the candidates"
943 );
944 assert_eq!(runtime.nodes().len(), 1);
945 runtime.stop();
946 }
947
948 #[test]
949 fn registration_refuses_garbage_and_an_inactive_mesh_says_so() {
950 let inactive = MeshRuntime::new();
951 let answer = inactive.register(&serde_json::json!({"v": 1}), "test");
952 assert!(!answer.accepted);
953 let dir = tempfile::tempdir().unwrap();
954 let runtime = MeshRuntime::new();
955 runtime
956 .activate(
957 &listed(&identity_in(&dir, "self.json")),
958 identity_in(&dir, "self.json"),
959 vec![],
960 vec![],
961 "0.5.0",
962 )
963 .unwrap();
964 let answer = runtime.register(&serde_json::json!({"v": 1}), "test");
965 assert!(!answer.accepted);
966 assert_eq!(answer.refusal.as_deref(), Some("malformed"));
967 runtime.stop();
968 }
969
970 #[test]
971 fn an_unlisted_machine_keeps_the_mesh_closed_and_starts_no_provider() {
972 let runtime = MeshRuntime::new();
973 let dir = tempfile::tempdir().unwrap();
974 let mut config = quiet_config();
975 config.multicast.enabled = true;
976 config.rendezvous.endpoints = vec!["http://127.0.0.1:9".into()];
977 config.trust.allow = vec!["cd".repeat(32)];
978 let own = identity_in(&dir, "self.json");
979 let key = own.public.public_key.clone();
980 runtime
981 .activate(&config, own, vec![], vec![], "0.5.0")
982 .unwrap();
983 let status = runtime.status();
984 assert!(
985 !status.active,
986 "an unlisted machine announces and dials nothing"
987 );
988 assert!(
989 status.providers.is_empty(),
990 "no provider started: {:?}",
991 status.providers
992 );
993 let reason = status.reason.unwrap();
994 assert!(
995 reason.starts_with("closed: ") && reason.contains(&key),
996 "{reason}"
997 );
998 assert!(runtime.decided(), "closed is a decision, not an absence");
999 let open = MeshRuntime::new();
1001 config.multicast.enabled = false;
1002 config.rendezvous.endpoints.clear();
1003 config.trust.allow.push(key);
1004 open.activate(
1005 &config,
1006 identity_in(&dir, "self.json"),
1007 vec![],
1008 vec![],
1009 "0.5.0",
1010 )
1011 .unwrap();
1012 assert!(open.status().active);
1013 open.stop();
1014 }
1015
1016 #[test]
1017 fn a_disabled_declaration_activates_nothing_and_says_why() {
1018 let runtime = MeshRuntime::new();
1019 let dir = tempfile::tempdir().unwrap();
1020 let mut config = quiet_config();
1021 config.enabled = false;
1022 runtime
1023 .activate(
1024 &config,
1025 identity_in(&dir, "self.json"),
1026 vec![],
1027 vec![],
1028 "0.5.0",
1029 )
1030 .unwrap();
1031 let status = runtime.status();
1032 assert!(!status.active);
1033 assert!(status.reason.unwrap().contains("disabled"));
1034 assert!(
1035 runtime.decided(),
1036 "a server that read a disabled declaration decided: off"
1037 );
1038 }
1039
1040 #[test]
1041 fn a_runtime_is_decided_by_an_activation_and_by_nothing_else_that_reads_it() {
1042 let dir = tempfile::tempdir().unwrap();
1043 let runtime = MeshRuntime::new();
1044 let _ = runtime.status();
1045 let _ = runtime.nodes();
1046 let _ = runtime.cooperation_status();
1047 assert!(!runtime.decided(), "reading a runtime decides nothing");
1048 runtime
1049 .activate(
1050 &listed(&identity_in(&dir, "a.json")),
1051 identity_in(&dir, "a.json"),
1052 vec![],
1053 vec![],
1054 "0.5.0",
1055 )
1056 .unwrap();
1057 assert!(runtime.decided());
1058 runtime.stop();
1059 assert!(runtime.decided(), "a stopped mesh was still decided on");
1060 }
1061
1062 #[test]
1063 fn activation_is_idempotent() {
1064 let dir = tempfile::tempdir().unwrap();
1065 let runtime = MeshRuntime::new();
1066 let config = listed(&identity_in(&dir, "a.json"));
1067 runtime
1068 .activate(
1069 &config,
1070 identity_in(&dir, "a.json"),
1071 vec![],
1072 vec![],
1073 "0.5.0",
1074 )
1075 .unwrap();
1076 runtime
1077 .activate(
1078 &config,
1079 identity_in(&dir, "b.json"),
1080 vec![],
1081 vec![],
1082 "0.5.0",
1083 )
1084 .unwrap();
1085 let status = runtime.status();
1086 assert!(status.active);
1087 runtime.stop();
1088 }
1089}