Skip to main content

macp_modes/
mode_registry.rs

1use std::collections::HashMap;
2use std::sync::{Arc, RwLock};
3use tokio::sync::broadcast;
4
5use crate::mode::decision::DecisionMode;
6use crate::mode::handoff::HandoffMode;
7use crate::mode::multi_round::MultiRoundMode;
8use crate::mode::passthrough::PassthroughMode;
9use crate::mode::proposal::ProposalMode;
10use crate::mode::quorum::QuorumMode;
11use crate::mode::task::TaskMode;
12use crate::mode::{
13    extension_mode_descriptors, standard_mode_descriptors, Mode, STANDARD_MODE_NAMES,
14};
15use macp_pb::pb::ModeDescriptor;
16
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct ModeConformanceCatalog {
19    pub fixture_set_name: String,
20    pub golden_transcript_paths: Vec<String>,
21    pub conformance_fixture_paths: Vec<String>,
22}
23
24pub trait ModeFactory: Send + Sync {
25    fn create(&self) -> Box<dyn Mode>;
26}
27
28pub trait ModeDescriptorProvider: Send + Sync {
29    fn descriptor(&self) -> Option<ModeDescriptor>;
30}
31
32pub trait ModeSchemaProvider: Send + Sync {
33    fn schema_uris(&self) -> HashMap<String, String>;
34}
35
36pub trait ModeConformanceProvider: Send + Sync {
37    fn fixture_set_name(&self) -> &'static str;
38    fn golden_transcript_paths(&self) -> Vec<&'static str>;
39    fn conformance_fixture_paths(&self) -> Vec<&'static str>;
40}
41
42pub struct StaticModeFactory {
43    constructor: fn() -> Box<dyn Mode>,
44}
45
46impl StaticModeFactory {
47    pub fn new(constructor: fn() -> Box<dyn Mode>) -> Self {
48        Self { constructor }
49    }
50}
51
52impl ModeFactory for StaticModeFactory {
53    fn create(&self) -> Box<dyn Mode> {
54        (self.constructor)()
55    }
56}
57
58pub struct ClosureModeFactory {
59    constructor: Arc<dyn Fn() -> Box<dyn Mode> + Send + Sync>,
60}
61
62impl ClosureModeFactory {
63    pub fn new(constructor: Arc<dyn Fn() -> Box<dyn Mode> + Send + Sync>) -> Self {
64        Self { constructor }
65    }
66}
67
68impl ModeFactory for ClosureModeFactory {
69    fn create(&self) -> Box<dyn Mode> {
70        (self.constructor)()
71    }
72}
73
74#[derive(Clone)]
75pub struct StaticModeDescriptorProvider {
76    descriptor: Option<ModeDescriptor>,
77}
78
79impl StaticModeDescriptorProvider {
80    pub fn new(descriptor: Option<ModeDescriptor>) -> Self {
81        Self { descriptor }
82    }
83}
84
85impl ModeDescriptorProvider for StaticModeDescriptorProvider {
86    fn descriptor(&self) -> Option<ModeDescriptor> {
87        self.descriptor.clone()
88    }
89}
90
91#[derive(Clone, Default)]
92pub struct StaticModeSchemaProvider {
93    schema_uris: HashMap<String, String>,
94}
95
96impl StaticModeSchemaProvider {
97    pub fn new(schema_uris: HashMap<String, String>) -> Self {
98        Self { schema_uris }
99    }
100}
101
102impl ModeSchemaProvider for StaticModeSchemaProvider {
103    fn schema_uris(&self) -> HashMap<String, String> {
104        self.schema_uris.clone()
105    }
106}
107
108#[derive(Clone, Default)]
109pub struct StaticModeConformanceProvider {
110    fixture_set_name: &'static str,
111    golden_transcript_paths: Vec<&'static str>,
112    conformance_fixture_paths: Vec<&'static str>,
113}
114
115impl StaticModeConformanceProvider {
116    pub fn new(
117        fixture_set_name: &'static str,
118        golden_transcript_paths: Vec<&'static str>,
119        conformance_fixture_paths: Vec<&'static str>,
120    ) -> Self {
121        Self {
122            fixture_set_name,
123            golden_transcript_paths,
124            conformance_fixture_paths,
125        }
126    }
127}
128
129impl ModeConformanceProvider for StaticModeConformanceProvider {
130    fn fixture_set_name(&self) -> &'static str {
131        self.fixture_set_name
132    }
133
134    fn golden_transcript_paths(&self) -> Vec<&'static str> {
135        self.golden_transcript_paths.clone()
136    }
137
138    fn conformance_fixture_paths(&self) -> Vec<&'static str> {
139        self.conformance_fixture_paths.clone()
140    }
141}
142
143pub struct ModeRegistration {
144    pub mode_name: String,
145    pub factory: Arc<dyn ModeFactory>,
146    pub descriptor_provider: Arc<dyn ModeDescriptorProvider>,
147    pub schema_provider: Arc<dyn ModeSchemaProvider>,
148    pub conformance_provider: Arc<dyn ModeConformanceProvider>,
149    pub standards_track: bool,
150    pub builtin: bool,
151    pub strict_session_start: bool,
152}
153
154impl ModeRegistration {
155    pub fn descriptor(&self) -> Option<ModeDescriptor> {
156        self.descriptor_provider.descriptor().map(|mut descriptor| {
157            descriptor.mode = self.mode_name.clone();
158            descriptor.schema_uris = self.schema_provider.schema_uris();
159            descriptor
160        })
161    }
162
163    pub fn conformance_catalog(&self) -> ModeConformanceCatalog {
164        ModeConformanceCatalog {
165            fixture_set_name: self.conformance_provider.fixture_set_name().to_string(),
166            golden_transcript_paths: self
167                .conformance_provider
168                .golden_transcript_paths()
169                .into_iter()
170                .map(str::to_string)
171                .collect(),
172            conformance_fixture_paths: self
173                .conformance_provider
174                .conformance_fixture_paths()
175                .into_iter()
176                .map(str::to_string)
177                .collect(),
178        }
179    }
180}
181
182pub struct ModeRegistry {
183    entries: RwLock<HashMap<String, ModeRegistration>>,
184    change_tx: broadcast::Sender<()>,
185}
186
187impl ModeRegistry {
188    /// Build the default registry with 5 standards-track modes and 1 built-in extension.
189    pub fn build_default(evaluator: Arc<dyn macp_core::policy::PolicyEvaluator>) -> Self {
190        let std_descriptors = standard_mode_descriptors();
191        let ext_descriptors = extension_mode_descriptors();
192        let mut descriptor_map: HashMap<String, ModeDescriptor> = std_descriptors
193            .into_iter()
194            .chain(ext_descriptors)
195            .map(|d| (d.mode.clone(), d))
196            .collect();
197
198        let mut entries = HashMap::new();
199
200        Self::insert_builtin(
201            &mut entries,
202            "macp.mode.decision.v1",
203            Arc::new(ClosureModeFactory::new(Arc::new({
204                let e = evaluator.clone();
205                move || Box::new(DecisionMode::new(e.clone())) as Box<dyn Mode>
206            }))),
207            descriptor_map.remove("macp.mode.decision.v1"),
208            true,
209            true,
210            "decision",
211            vec!["tests/conformance/decision_happy_path.json"],
212            vec![
213                "tests/conformance/decision_happy_path.json",
214                "tests/conformance/decision_reject_paths.json",
215            ],
216        );
217        Self::insert_builtin(
218            &mut entries,
219            "macp.mode.proposal.v1",
220            Arc::new(ClosureModeFactory::new(Arc::new({
221                let e = evaluator.clone();
222                move || Box::new(ProposalMode::new(e.clone())) as Box<dyn Mode>
223            }))),
224            descriptor_map.remove("macp.mode.proposal.v1"),
225            true,
226            true,
227            "proposal",
228            vec!["tests/conformance/proposal_happy_path.json"],
229            vec![
230                "tests/conformance/proposal_happy_path.json",
231                "tests/conformance/proposal_reject_paths.json",
232            ],
233        );
234        Self::insert_builtin(
235            &mut entries,
236            "macp.mode.task.v1",
237            Arc::new(ClosureModeFactory::new(Arc::new({
238                let e = evaluator.clone();
239                move || Box::new(TaskMode::new(e.clone())) as Box<dyn Mode>
240            }))),
241            descriptor_map.remove("macp.mode.task.v1"),
242            true,
243            true,
244            "task",
245            vec!["tests/conformance/task_happy_path.json"],
246            vec![
247                "tests/conformance/task_happy_path.json",
248                "tests/conformance/task_reject_paths.json",
249            ],
250        );
251        Self::insert_builtin(
252            &mut entries,
253            "macp.mode.handoff.v1",
254            Arc::new(ClosureModeFactory::new(Arc::new({
255                let e = evaluator.clone();
256                move || Box::new(HandoffMode::new(e.clone())) as Box<dyn Mode>
257            }))),
258            descriptor_map.remove("macp.mode.handoff.v1"),
259            true,
260            true,
261            "handoff",
262            vec!["tests/conformance/handoff_happy_path.json"],
263            vec![
264                "tests/conformance/handoff_happy_path.json",
265                "tests/conformance/handoff_reject_paths.json",
266            ],
267        );
268        Self::insert_builtin(
269            &mut entries,
270            "macp.mode.quorum.v1",
271            Arc::new(ClosureModeFactory::new(Arc::new({
272                let e = evaluator.clone();
273                move || Box::new(QuorumMode::new(e.clone())) as Box<dyn Mode>
274            }))),
275            descriptor_map.remove("macp.mode.quorum.v1"),
276            true,
277            true,
278            "quorum",
279            vec!["tests/conformance/quorum_happy_path.json"],
280            vec![
281                "tests/conformance/quorum_happy_path.json",
282                "tests/conformance/quorum_reject_paths.json",
283            ],
284        );
285        Self::insert_builtin(
286            &mut entries,
287            "ext.multi_round.v1",
288            Arc::new(StaticModeFactory::new(|| {
289                Box::new(MultiRoundMode) as Box<dyn Mode>
290            })),
291            descriptor_map.remove("ext.multi_round.v1"),
292            false,
293            true,
294            "multi_round",
295            vec!["tests/conformance/multi_round_happy_path.json"],
296            vec![
297                "tests/conformance/multi_round_happy_path.json",
298                "tests/conformance/multi_round_reject_paths.json",
299            ],
300        );
301
302        let (change_tx, _) = broadcast::channel(16);
303        Self {
304            entries: RwLock::new(entries),
305            change_tx,
306        }
307    }
308
309    #[allow(clippy::too_many_arguments)]
310    fn insert_builtin(
311        entries: &mut HashMap<String, ModeRegistration>,
312        name: &str,
313        factory: Arc<dyn ModeFactory>,
314        descriptor: Option<ModeDescriptor>,
315        standards_track: bool,
316        strict_session_start: bool,
317        fixture_set_name: &'static str,
318        golden_transcripts: Vec<&'static str>,
319        conformance_fixtures: Vec<&'static str>,
320    ) {
321        let schema_uris = descriptor
322            .as_ref()
323            .map(|d| d.schema_uris.clone())
324            .unwrap_or_default();
325        let descriptor_provider = Arc::new(StaticModeDescriptorProvider::new(descriptor));
326        let schema_provider = Arc::new(StaticModeSchemaProvider::new(schema_uris));
327        let conformance_provider = Arc::new(StaticModeConformanceProvider::new(
328            fixture_set_name,
329            golden_transcripts,
330            conformance_fixtures,
331        ));
332        entries.insert(
333            name.to_string(),
334            ModeRegistration {
335                mode_name: name.to_string(),
336                factory,
337                descriptor_provider,
338                schema_provider,
339                conformance_provider,
340                standards_track,
341                builtin: true,
342                strict_session_start,
343            },
344        );
345    }
346
347    fn ordered_standard_names(entries: &HashMap<String, ModeRegistration>) -> Vec<String> {
348        let mut names: Vec<String> = STANDARD_MODE_NAMES
349            .iter()
350            .filter(|name| {
351                entries
352                    .get(**name)
353                    .map(|entry| entry.standards_track)
354                    .unwrap_or(false)
355            })
356            .map(|name| (*name).to_string())
357            .collect();
358
359        let mut promoted: Vec<String> = entries
360            .iter()
361            .filter(|(name, entry)| {
362                entry.standards_track && !STANDARD_MODE_NAMES.contains(&name.as_str())
363            })
364            .map(|(name, _)| name.clone())
365            .collect();
366        promoted.sort();
367        names.extend(promoted);
368        names
369    }
370
371    pub fn get_mode(&self, name: &str) -> Option<ModeRef<'_>> {
372        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
373        if guard.contains_key(name) {
374            Some(ModeRef {
375                registry: self,
376                name: name.to_string(),
377            })
378        } else {
379            None
380        }
381    }
382
383    pub fn standard_mode_names(&self) -> Vec<String> {
384        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
385        Self::ordered_standard_names(&guard)
386    }
387
388    pub fn standard_mode_descriptors(&self) -> Vec<ModeDescriptor> {
389        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
390        Self::ordered_standard_names(&guard)
391            .into_iter()
392            .filter_map(|name| guard.get(&name).and_then(ModeRegistration::descriptor))
393            .collect()
394    }
395
396    pub fn extension_mode_names(&self) -> Vec<String> {
397        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
398        let mut names: Vec<String> = guard
399            .iter()
400            .filter(|(_, e)| !e.standards_track)
401            .map(|(name, _)| name.clone())
402            .collect();
403        names.sort();
404        names
405    }
406
407    pub fn extension_mode_descriptors(&self) -> Vec<ModeDescriptor> {
408        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
409        let mut descriptors: Vec<ModeDescriptor> = guard
410            .iter()
411            .filter(|(_, e)| !e.standards_track)
412            .filter_map(|(_, e)| e.descriptor())
413            .collect();
414        descriptors.sort_by(|a, b| a.mode.cmp(&b.mode));
415        descriptors
416    }
417
418    pub fn all_mode_names(&self) -> Vec<String> {
419        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
420        let mut names: Vec<String> = guard.keys().cloned().collect();
421        names.sort();
422        names
423    }
424
425    pub fn all_mode_descriptors(&self) -> Vec<ModeDescriptor> {
426        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
427        let mut descriptors: Vec<ModeDescriptor> = guard
428            .values()
429            .filter_map(ModeRegistration::descriptor)
430            .collect();
431        descriptors.sort_by(|a, b| a.mode.cmp(&b.mode));
432        descriptors
433    }
434
435    pub fn all_mode_conformance(&self) -> Vec<(String, ModeConformanceCatalog)> {
436        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
437        let mut conformance: Vec<(String, ModeConformanceCatalog)> = guard
438            .iter()
439            .map(|(name, entry)| (name.clone(), entry.conformance_catalog()))
440            .collect();
441        conformance.sort_by(|a, b| a.0.cmp(&b.0));
442        conformance
443    }
444
445    pub fn is_standard_mode(&self, name: &str) -> bool {
446        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
447        guard.get(name).map(|e| e.standards_track).unwrap_or(false)
448    }
449
450    pub fn requires_strict_session_start(&self, name: &str) -> bool {
451        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
452        guard
453            .get(name)
454            .map(|entry| entry.strict_session_start)
455            .unwrap_or(false)
456    }
457
458    /// Returns the mode_version from the mode's descriptor, if available.
459    pub fn get_mode_version(&self, name: &str) -> Option<String> {
460        let guard = self.entries.read().unwrap_or_else(|e| e.into_inner());
461        guard
462            .get(name)
463            .and_then(|entry| entry.descriptor())
464            .map(|d| d.mode_version)
465            .filter(|v| !v.is_empty())
466    }
467
468    fn validate_extension_descriptor(descriptor: &ModeDescriptor) -> Result<(), String> {
469        if descriptor.mode.trim().is_empty() {
470            return Err("mode name must not be empty".into());
471        }
472        if descriptor.mode.starts_with("macp.mode.") {
473            return Err("cannot register extension with reserved macp.mode.* namespace".into());
474        }
475        if descriptor.message_types.is_empty() {
476            return Err("extension descriptor must declare at least one message type".into());
477        }
478        if descriptor.mode_version.trim().is_empty() {
479            return Err("extension descriptor must bind mode_version".into());
480        }
481        for terminal in &descriptor.terminal_message_types {
482            if !descriptor
483                .message_types
484                .iter()
485                .any(|message_type| message_type == terminal)
486            {
487                return Err(format!(
488                    "terminal message type '{}' must also appear in message_types",
489                    terminal
490                ));
491            }
492        }
493        Ok(())
494    }
495
496    /// Register a new descriptor-driven extension mode dynamically.
497    ///
498    /// Dynamically registered extensions currently use `PassthroughMode`, which
499    /// validates only the descriptor-declared message types and commitment
500    /// authority. This keeps runtime behavior explicit until a richer external
501    /// plugin mechanism is introduced.
502    pub fn register_extension(&self, descriptor: ModeDescriptor) -> Result<(), String> {
503        Self::validate_extension_descriptor(&descriptor)?;
504        let name = descriptor.mode.clone();
505        let schema_uris = descriptor.schema_uris.clone();
506        let allowed_types: Vec<String> = descriptor
507            .message_types
508            .iter()
509            .filter(|t| *t != "SessionStart")
510            .cloned()
511            .collect();
512        let allowed_types = Arc::new(allowed_types);
513        let factory: Arc<dyn ModeFactory> = Arc::new(ClosureModeFactory::new(Arc::new({
514            let allowed_types = Arc::clone(&allowed_types);
515            move || {
516                Box::new(PassthroughMode {
517                    allowed_message_types: (*allowed_types).clone(),
518                }) as Box<dyn Mode>
519            }
520        })));
521        let descriptor_provider = Arc::new(StaticModeDescriptorProvider::new(Some(descriptor)));
522        let schema_provider = Arc::new(StaticModeSchemaProvider::new(schema_uris));
523        let conformance_provider = Arc::new(StaticModeConformanceProvider::default());
524
525        let mut guard = self.entries.write().unwrap_or_else(|e| e.into_inner());
526        if guard.contains_key(&name) {
527            return Err(format!("mode '{}' is already registered", name));
528        }
529        guard.insert(
530            name.clone(),
531            ModeRegistration {
532                mode_name: name,
533                factory,
534                descriptor_provider,
535                schema_provider,
536                conformance_provider,
537                standards_track: false,
538                builtin: false,
539                strict_session_start: false,
540            },
541        );
542        drop(guard);
543        let _ = self.change_tx.send(());
544        Ok(())
545    }
546
547    /// Unregister a dynamically registered extension mode.
548    pub fn unregister_extension(&self, mode: &str) -> Result<(), String> {
549        let mut guard = self.entries.write().unwrap_or_else(|e| e.into_inner());
550        match guard.get(mode) {
551            None => return Err(format!("mode '{}' not found", mode)),
552            Some(entry) if entry.builtin => {
553                return Err(format!("cannot unregister built-in mode '{}'", mode))
554            }
555            Some(entry) if entry.standards_track => {
556                return Err(format!("cannot unregister standards-track mode '{}'", mode))
557            }
558            _ => {}
559        }
560        guard.remove(mode);
561        drop(guard);
562        let _ = self.change_tx.send(());
563        Ok(())
564    }
565
566    /// Promote an extension mode to standards-track.
567    /// Optionally re-keys the entry with a new identifier.
568    pub fn promote_mode(&self, mode: &str, new_name: Option<&str>) -> Result<String, String> {
569        let mut guard = self.entries.write().unwrap_or_else(|e| e.into_inner());
570        let entry = guard
571            .get(mode)
572            .ok_or_else(|| format!("mode '{}' not found", mode))?;
573        if entry.standards_track {
574            return Err(format!("mode '{}' is already standards-track", mode));
575        }
576
577        let final_name = new_name.unwrap_or(mode).to_string();
578        if final_name != mode && guard.contains_key(&final_name) {
579            return Err(format!(
580                "cannot promote: target name '{}' already exists",
581                final_name
582            ));
583        }
584
585        let mut registration = guard
586            .remove(mode)
587            .ok_or_else(|| format!("mode '{}' not found", mode))?;
588        registration.standards_track = true;
589        registration.strict_session_start = true;
590        registration.mode_name = final_name.clone();
591        guard.insert(final_name.clone(), registration);
592        drop(guard);
593        let _ = self.change_tx.send(());
594        Ok(final_name)
595    }
596
597    /// Subscribe to mode registry change notifications.
598    pub fn subscribe_changes(&self) -> broadcast::Receiver<()> {
599        self.change_tx.subscribe()
600    }
601}
602
603/// A handle that allows calling mode methods without keeping the registry read
604/// lock held across callback execution.
605pub struct ModeRef<'a> {
606    registry: &'a ModeRegistry,
607    name: String,
608}
609
610impl<'a> ModeRef<'a> {
611    fn factory(&self) -> Result<Arc<dyn ModeFactory>, macp_core::error::MacpError> {
612        let guard = self
613            .registry
614            .entries
615            .read()
616            .unwrap_or_else(|e| e.into_inner());
617        guard
618            .get(&self.name)
619            .map(|entry| Arc::clone(&entry.factory))
620            .ok_or(macp_core::error::MacpError::UnknownMode)
621    }
622
623    pub fn on_session_start(
624        &self,
625        session: &macp_core::session::Session,
626        env: &macp_pb::pb::Envelope,
627    ) -> Result<crate::mode::ModeResponse, macp_core::error::MacpError> {
628        let mode = self.factory()?.create();
629        mode.on_session_start(session, env)
630    }
631
632    pub fn on_message(
633        &self,
634        session: &macp_core::session::Session,
635        env: &macp_pb::pb::Envelope,
636    ) -> Result<crate::mode::ModeResponse, macp_core::error::MacpError> {
637        let mode = self.factory()?.create();
638        mode.on_message(session, env)
639    }
640
641    pub fn authorize_sender(
642        &self,
643        session: &macp_core::session::Session,
644        env: &macp_pb::pb::Envelope,
645    ) -> Result<(), macp_core::error::MacpError> {
646        let mode = self.factory()?.create();
647        mode.authorize_sender(session, env)
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654    use crate::mode::EXTENSION_MODE_NAMES;
655
656    #[test]
657    fn build_default_contains_all_standard_modes() {
658        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
659        for name in STANDARD_MODE_NAMES {
660            assert!(registry.get_mode(name).is_some(), "missing mode: {name}");
661            assert!(registry.is_standard_mode(name));
662            assert!(registry.requires_strict_session_start(name));
663        }
664    }
665
666    #[test]
667    fn build_default_contains_multi_round_as_extension() {
668        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
669        assert!(registry.get_mode("ext.multi_round.v1").is_some());
670        assert!(!registry.is_standard_mode("ext.multi_round.v1"));
671        assert!(registry.requires_strict_session_start("ext.multi_round.v1"));
672    }
673
674    #[test]
675    fn standard_mode_names_returns_five() {
676        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
677        assert_eq!(registry.standard_mode_names().len(), 5);
678    }
679
680    #[test]
681    fn standard_mode_descriptors_returns_five() {
682        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
683        assert_eq!(registry.standard_mode_descriptors().len(), 5);
684    }
685
686    #[test]
687    fn all_mode_names_returns_six() {
688        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
689        assert_eq!(registry.all_mode_names().len(), 6);
690    }
691
692    #[test]
693    fn extension_mode_names_returns_one() {
694        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
695        let ext = registry.extension_mode_names();
696        assert_eq!(ext.len(), 1);
697        assert!(ext.contains(&"ext.multi_round.v1".to_string()));
698    }
699
700    #[test]
701    fn extension_mode_descriptors_returns_one() {
702        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
703        let descs = registry.extension_mode_descriptors();
704        assert_eq!(descs.len(), 1);
705        assert_eq!(descs[0].mode, "ext.multi_round.v1");
706    }
707
708    #[test]
709    fn unknown_mode_returns_none() {
710        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
711        assert!(registry.get_mode("nonexistent").is_none());
712        assert!(!registry.is_standard_mode("nonexistent"));
713        assert!(!registry.requires_strict_session_start("nonexistent"));
714    }
715
716    #[test]
717    fn register_extension_mode() {
718        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
719        let descriptor = ModeDescriptor {
720            mode: "ext.custom.v1".into(),
721            mode_version: "1.0.0".into(),
722            title: "Custom Mode".into(),
723            description: "Test custom mode".into(),
724            message_types: vec![
725                "SessionStart".into(),
726                "CustomMsg".into(),
727                "Commitment".into(),
728            ],
729            terminal_message_types: vec!["Commitment".into()],
730            ..Default::default()
731        };
732        registry.register_extension(descriptor).unwrap();
733        assert!(registry.get_mode("ext.custom.v1").is_some());
734        assert!(!registry.is_standard_mode("ext.custom.v1"));
735        assert_eq!(registry.all_mode_names().len(), 7);
736        assert_eq!(registry.extension_mode_names().len(), 2);
737    }
738
739    #[test]
740    fn register_rejects_macp_namespace() {
741        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
742        let descriptor = ModeDescriptor {
743            mode: "macp.mode.evil.v1".into(),
744            mode_version: "1.0.0".into(),
745            message_types: vec!["SessionStart".into(), "Commitment".into()],
746            ..Default::default()
747        };
748        assert!(registry.register_extension(descriptor).is_err());
749    }
750
751    #[test]
752    fn register_rejects_empty_message_types() {
753        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
754        let descriptor = ModeDescriptor {
755            mode: "ext.invalid.v1".into(),
756            mode_version: "1.0.0".into(),
757            ..Default::default()
758        };
759        assert!(registry.register_extension(descriptor).is_err());
760    }
761
762    #[test]
763    fn register_rejects_terminal_not_in_message_types() {
764        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
765        let descriptor = ModeDescriptor {
766            mode: "ext.invalid.v1".into(),
767            mode_version: "1.0.0".into(),
768            message_types: vec!["SessionStart".into(), "Custom".into()],
769            terminal_message_types: vec!["Commitment".into()],
770            ..Default::default()
771        };
772        assert!(registry.register_extension(descriptor).is_err());
773    }
774
775    #[test]
776    fn register_rejects_duplicate() {
777        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
778        let descriptor = ModeDescriptor {
779            mode: "ext.multi_round.v1".into(),
780            mode_version: "1.0.0".into(),
781            message_types: vec!["SessionStart".into(), "Commitment".into()],
782            ..Default::default()
783        };
784        assert!(registry.register_extension(descriptor).is_err());
785    }
786
787    #[test]
788    fn unregister_extension_mode() {
789        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
790        let descriptor = ModeDescriptor {
791            mode: "ext.temp.v1".into(),
792            mode_version: "1.0.0".into(),
793            message_types: vec!["SessionStart".into(), "Commitment".into()],
794            ..Default::default()
795        };
796        registry.register_extension(descriptor).unwrap();
797        assert_eq!(registry.all_mode_names().len(), 7);
798        registry.unregister_extension("ext.temp.v1").unwrap();
799        assert_eq!(registry.all_mode_names().len(), 6);
800        assert!(registry.get_mode("ext.temp.v1").is_none());
801    }
802
803    #[test]
804    fn cannot_unregister_builtin() {
805        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
806        assert!(registry.unregister_extension("ext.multi_round.v1").is_err());
807        assert!(registry
808            .unregister_extension("macp.mode.decision.v1")
809            .is_err());
810    }
811
812    #[test]
813    fn promote_extension_to_standard() {
814        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
815        let descriptor = ModeDescriptor {
816            mode: "ext.new.v1".into(),
817            mode_version: "1.0.0".into(),
818            message_types: vec!["SessionStart".into(), "Commitment".into()],
819            terminal_message_types: vec!["Commitment".into()],
820            ..Default::default()
821        };
822        registry.register_extension(descriptor).unwrap();
823        assert!(!registry.is_standard_mode("ext.new.v1"));
824        assert!(!registry.requires_strict_session_start("ext.new.v1"));
825
826        let final_name = registry
827            .promote_mode("ext.new.v1", Some("macp.mode.new.v1"))
828            .unwrap();
829        assert_eq!(final_name, "macp.mode.new.v1");
830        assert!(registry.is_standard_mode("macp.mode.new.v1"));
831        assert!(registry.requires_strict_session_start("macp.mode.new.v1"));
832        assert!(registry.get_mode("ext.new.v1").is_none());
833        assert!(registry.get_mode("macp.mode.new.v1").is_some());
834    }
835
836    #[test]
837    fn promote_without_rename() {
838        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
839        let descriptor = ModeDescriptor {
840            mode: "ext.keep.v1".into(),
841            mode_version: "1.0.0".into(),
842            message_types: vec!["SessionStart".into(), "Commitment".into()],
843            terminal_message_types: vec!["Commitment".into()],
844            ..Default::default()
845        };
846        registry.register_extension(descriptor).unwrap();
847        let final_name = registry.promote_mode("ext.keep.v1", None).unwrap();
848        assert_eq!(final_name, "ext.keep.v1");
849        assert!(registry.is_standard_mode("ext.keep.v1"));
850        assert!(registry.requires_strict_session_start("ext.keep.v1"));
851    }
852
853    #[test]
854    fn promoted_mode_appears_in_standard_mode_names_and_descriptors() {
855        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
856        let descriptor = ModeDescriptor {
857            mode: "ext.promoted.v1".into(),
858            mode_version: "1.0.0".into(),
859            title: "Promoted".into(),
860            message_types: vec!["SessionStart".into(), "Commitment".into()],
861            terminal_message_types: vec!["Commitment".into()],
862            ..Default::default()
863        };
864        registry.register_extension(descriptor).unwrap();
865        registry
866            .promote_mode("ext.promoted.v1", Some("macp.mode.promoted.v1"))
867            .unwrap();
868
869        let standard_names = registry.standard_mode_names();
870        assert!(standard_names.contains(&"macp.mode.promoted.v1".to_string()));
871
872        let standard_modes: Vec<String> = registry
873            .standard_mode_descriptors()
874            .into_iter()
875            .map(|d| d.mode)
876            .collect();
877        assert!(standard_modes.contains(&"macp.mode.promoted.v1".to_string()));
878    }
879
880    #[test]
881    fn promote_already_standard_fails() {
882        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
883        assert!(registry
884            .promote_mode("macp.mode.decision.v1", None)
885            .is_err());
886    }
887
888    #[test]
889    fn extension_names_constant_matches() {
890        for name in EXTENSION_MODE_NAMES {
891            let registry =
892                ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
893            assert!(
894                registry.get_mode(name).is_some(),
895                "EXTENSION_MODE_NAMES entry missing from registry: {name}"
896            );
897        }
898    }
899
900    #[test]
901    fn conformance_catalog_exposes_builtin_fixture_sets() {
902        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
903        let catalog = registry.all_mode_conformance();
904        let decision = catalog
905            .into_iter()
906            .find(|(name, _)| name == "macp.mode.decision.v1")
907            .expect("decision catalog should exist");
908        assert_eq!(decision.1.fixture_set_name, "decision");
909        assert!(decision
910            .1
911            .conformance_fixture_paths
912            .iter()
913            .any(|path| path.ends_with("decision_happy_path.json")));
914    }
915}