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        // A mode with no terminal message type can never resolve — sessions under it
482        // could only expire or be cancelled. Reject at registration rather than
483        // letting every session dead-end.
484        if descriptor.terminal_message_types.is_empty() {
485            return Err(
486                "extension descriptor must declare at least one terminal message type".into(),
487            );
488        }
489        for terminal in &descriptor.terminal_message_types {
490            // Dynamically registered modes are passthrough-backed, and passthrough
491            // resolves only on `Commitment`. Any other declared terminal would be
492            // advertised to clients but non-functional.
493            if terminal != "Commitment" {
494                return Err(format!(
495                    "dynamically registered modes resolve only on 'Commitment'; \
496                     terminal message type '{}' would never terminate a session",
497                    terminal
498                ));
499            }
500            if !descriptor
501                .message_types
502                .iter()
503                .any(|message_type| message_type == terminal)
504            {
505                return Err(format!(
506                    "terminal message type '{}' must also appear in message_types",
507                    terminal
508                ));
509            }
510        }
511        Ok(())
512    }
513
514    /// Register a new descriptor-driven extension mode dynamically.
515    ///
516    /// Dynamically registered extensions currently use `PassthroughMode`, which
517    /// validates only the descriptor-declared message types and commitment
518    /// authority. This keeps runtime behavior explicit until a richer external
519    /// plugin mechanism is introduced.
520    pub fn register_extension(&self, descriptor: ModeDescriptor) -> Result<(), String> {
521        Self::validate_extension_descriptor(&descriptor)?;
522        let name = descriptor.mode.clone();
523        let schema_uris = descriptor.schema_uris.clone();
524        let allowed_types: Vec<String> = descriptor
525            .message_types
526            .iter()
527            .filter(|t| *t != "SessionStart")
528            .cloned()
529            .collect();
530        let allowed_types = Arc::new(allowed_types);
531        let factory: Arc<dyn ModeFactory> = Arc::new(ClosureModeFactory::new(Arc::new({
532            let allowed_types = Arc::clone(&allowed_types);
533            move || {
534                Box::new(PassthroughMode {
535                    allowed_message_types: (*allowed_types).clone(),
536                }) as Box<dyn Mode>
537            }
538        })));
539        let descriptor_provider = Arc::new(StaticModeDescriptorProvider::new(Some(descriptor)));
540        let schema_provider = Arc::new(StaticModeSchemaProvider::new(schema_uris));
541        let conformance_provider = Arc::new(StaticModeConformanceProvider::default());
542
543        let mut guard = self.entries.write().unwrap_or_else(|e| e.into_inner());
544        if guard.contains_key(&name) {
545            return Err(format!("mode '{}' is already registered", name));
546        }
547        guard.insert(
548            name.clone(),
549            ModeRegistration {
550                mode_name: name,
551                factory,
552                descriptor_provider,
553                schema_provider,
554                conformance_provider,
555                standards_track: false,
556                builtin: false,
557                strict_session_start: false,
558            },
559        );
560        drop(guard);
561        let _ = self.change_tx.send(());
562        Ok(())
563    }
564
565    /// Unregister a dynamically registered extension mode.
566    pub fn unregister_extension(&self, mode: &str) -> Result<(), String> {
567        let mut guard = self.entries.write().unwrap_or_else(|e| e.into_inner());
568        match guard.get(mode) {
569            None => return Err(format!("mode '{}' not found", mode)),
570            Some(entry) if entry.builtin => {
571                return Err(format!("cannot unregister built-in mode '{}'", mode))
572            }
573            Some(entry) if entry.standards_track => {
574                return Err(format!("cannot unregister standards-track mode '{}'", mode))
575            }
576            _ => {}
577        }
578        guard.remove(mode);
579        drop(guard);
580        let _ = self.change_tx.send(());
581        Ok(())
582    }
583
584    /// Promote an extension mode to standards-track.
585    /// Optionally re-keys the entry with a new identifier.
586    pub fn promote_mode(&self, mode: &str, new_name: Option<&str>) -> Result<String, String> {
587        let mut guard = self.entries.write().unwrap_or_else(|e| e.into_inner());
588        let entry = guard
589            .get(mode)
590            .ok_or_else(|| format!("mode '{}' not found", mode))?;
591        if entry.standards_track {
592            return Err(format!("mode '{}' is already standards-track", mode));
593        }
594
595        let final_name = new_name.unwrap_or(mode).to_string();
596        // The macp.mode.* namespace is reserved for RFC-standardized modes
597        // (RFC-MACP-0002 §12). Promotion grants standards-track status on this
598        // runtime, not RFC namespace membership — without this guard, a
599        // passthrough-backed extension could masquerade as a standard mode via
600        // register(ext.x) + promote(-> macp.mode.x).
601        if final_name != mode && final_name.starts_with("macp.mode.") {
602            return Err(format!(
603                "cannot promote '{}' into the reserved macp.mode.* namespace",
604                mode
605            ));
606        }
607        if final_name.trim().is_empty() {
608            return Err("promotion target name must not be empty".into());
609        }
610        if final_name != mode && guard.contains_key(&final_name) {
611            return Err(format!(
612                "cannot promote: target name '{}' already exists",
613                final_name
614            ));
615        }
616
617        let mut registration = guard
618            .remove(mode)
619            .ok_or_else(|| format!("mode '{}' not found", mode))?;
620        registration.standards_track = true;
621        registration.strict_session_start = true;
622        registration.mode_name = final_name.clone();
623        guard.insert(final_name.clone(), registration);
624        drop(guard);
625        let _ = self.change_tx.send(());
626        Ok(final_name)
627    }
628
629    /// Subscribe to mode registry change notifications.
630    pub fn subscribe_changes(&self) -> broadcast::Receiver<()> {
631        self.change_tx.subscribe()
632    }
633}
634
635/// A handle that allows calling mode methods without keeping the registry read
636/// lock held across callback execution.
637pub struct ModeRef<'a> {
638    registry: &'a ModeRegistry,
639    name: String,
640}
641
642impl<'a> ModeRef<'a> {
643    fn factory(&self) -> Result<Arc<dyn ModeFactory>, macp_core::error::MacpError> {
644        let guard = self
645            .registry
646            .entries
647            .read()
648            .unwrap_or_else(|e| e.into_inner());
649        guard
650            .get(&self.name)
651            .map(|entry| Arc::clone(&entry.factory))
652            .ok_or(macp_core::error::MacpError::UnknownMode)
653    }
654
655    pub fn on_session_start(
656        &self,
657        session: &macp_core::session::Session,
658        env: &macp_pb::pb::Envelope,
659    ) -> Result<crate::mode::ModeResponse, macp_core::error::MacpError> {
660        let mode = self.factory()?.create();
661        mode.on_session_start(session, env)
662    }
663
664    pub fn on_message(
665        &self,
666        session: &macp_core::session::Session,
667        env: &macp_pb::pb::Envelope,
668    ) -> Result<crate::mode::ModeResponse, macp_core::error::MacpError> {
669        let mode = self.factory()?.create();
670        mode.on_message(session, env)
671    }
672
673    /// Kernel entry point: `on_message` with the runtime's acceptance-clock
674    /// context. The runtime and replay call this so modes that need a
675    /// trustworthy time source receive the log-recorded clock.
676    pub fn on_message_at(
677        &self,
678        session: &macp_core::session::Session,
679        env: &macp_pb::pb::Envelope,
680        ctx: &macp_core::mode::MessageContext,
681    ) -> Result<crate::mode::ModeResponse, macp_core::error::MacpError> {
682        let mode = self.factory()?.create();
683        mode.on_message_at(session, env, ctx)
684    }
685
686    pub fn authorize_sender(
687        &self,
688        session: &macp_core::session::Session,
689        env: &macp_pb::pb::Envelope,
690    ) -> Result<(), macp_core::error::MacpError> {
691        let mode = self.factory()?.create();
692        mode.authorize_sender(session, env)
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699    use crate::mode::EXTENSION_MODE_NAMES;
700
701    #[test]
702    fn build_default_contains_all_standard_modes() {
703        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
704        for name in STANDARD_MODE_NAMES {
705            assert!(registry.get_mode(name).is_some(), "missing mode: {name}");
706            assert!(registry.is_standard_mode(name));
707            assert!(registry.requires_strict_session_start(name));
708        }
709    }
710
711    #[test]
712    fn build_default_contains_multi_round_as_extension() {
713        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
714        assert!(registry.get_mode("ext.multi_round.v1").is_some());
715        assert!(!registry.is_standard_mode("ext.multi_round.v1"));
716        assert!(registry.requires_strict_session_start("ext.multi_round.v1"));
717    }
718
719    #[test]
720    fn default_registry_shape_is_five_standard_plus_one_extension() {
721        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
722        assert_eq!(registry.standard_mode_names().len(), 5);
723        assert_eq!(registry.standard_mode_descriptors().len(), 5);
724        assert_eq!(registry.all_mode_names().len(), 6);
725        assert_eq!(
726            registry.extension_mode_names(),
727            vec!["ext.multi_round.v1".to_string()]
728        );
729        let descs = registry.extension_mode_descriptors();
730        assert_eq!(descs.len(), 1);
731        assert_eq!(descs[0].mode, "ext.multi_round.v1");
732    }
733
734    #[test]
735    fn unknown_mode_returns_none() {
736        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
737        assert!(registry.get_mode("nonexistent").is_none());
738        assert!(!registry.is_standard_mode("nonexistent"));
739        assert!(!registry.requires_strict_session_start("nonexistent"));
740    }
741
742    #[test]
743    fn register_extension_mode() {
744        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
745        let descriptor = ModeDescriptor {
746            mode: "ext.custom.v1".into(),
747            mode_version: "1.0.0".into(),
748            title: "Custom Mode".into(),
749            description: "Test custom mode".into(),
750            message_types: vec![
751                "SessionStart".into(),
752                "CustomMsg".into(),
753                "Commitment".into(),
754            ],
755            terminal_message_types: vec!["Commitment".into()],
756            ..Default::default()
757        };
758        registry.register_extension(descriptor).unwrap();
759        assert!(registry.get_mode("ext.custom.v1").is_some());
760        assert!(!registry.is_standard_mode("ext.custom.v1"));
761        assert_eq!(registry.all_mode_names().len(), 7);
762        assert_eq!(registry.extension_mode_names().len(), 2);
763    }
764
765    #[test]
766    fn register_rejects_macp_namespace() {
767        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
768        let descriptor = ModeDescriptor {
769            mode: "macp.mode.evil.v1".into(),
770            mode_version: "1.0.0".into(),
771            message_types: vec!["SessionStart".into(), "Commitment".into()],
772            ..Default::default()
773        };
774        assert!(registry.register_extension(descriptor).is_err());
775    }
776
777    #[test]
778    fn register_rejects_empty_message_types() {
779        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
780        let descriptor = ModeDescriptor {
781            mode: "ext.invalid.v1".into(),
782            mode_version: "1.0.0".into(),
783            ..Default::default()
784        };
785        assert!(registry.register_extension(descriptor).is_err());
786    }
787
788    #[test]
789    fn register_rejects_terminal_not_in_message_types() {
790        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
791        let descriptor = ModeDescriptor {
792            mode: "ext.invalid.v1".into(),
793            mode_version: "1.0.0".into(),
794            message_types: vec!["SessionStart".into(), "Custom".into()],
795            terminal_message_types: vec!["Commitment".into()],
796            ..Default::default()
797        };
798        assert!(registry.register_extension(descriptor).is_err());
799    }
800
801    #[test]
802    fn register_rejects_duplicate() {
803        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
804        // Descriptor is otherwise valid so the failure exercised is the duplicate
805        // check, not descriptor validation.
806        let descriptor = ModeDescriptor {
807            mode: "ext.multi_round.v1".into(),
808            mode_version: "1.0.0".into(),
809            message_types: vec!["SessionStart".into(), "Commitment".into()],
810            terminal_message_types: vec!["Commitment".into()],
811            ..Default::default()
812        };
813        assert!(registry.register_extension(descriptor).is_err());
814    }
815
816    #[test]
817    fn register_rejects_empty_terminal_types() {
818        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
819        let descriptor = ModeDescriptor {
820            mode: "ext.noterm.v1".into(),
821            mode_version: "1.0.0".into(),
822            message_types: vec!["SessionStart".into(), "Commitment".into()],
823            terminal_message_types: vec![],
824            ..Default::default()
825        };
826        let err = registry.register_extension(descriptor).unwrap_err();
827        assert!(
828            err.contains("terminal message type"),
829            "unexpected error: {err}"
830        );
831    }
832
833    #[test]
834    fn register_rejects_non_commitment_terminal() {
835        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
836        // Passthrough-backed modes resolve only on Commitment; a different declared
837        // terminal would be advertised but non-functional.
838        let descriptor = ModeDescriptor {
839            mode: "ext.badterm.v1".into(),
840            mode_version: "1.0.0".into(),
841            message_types: vec!["SessionStart".into(), "Finalize".into()],
842            terminal_message_types: vec!["Finalize".into()],
843            ..Default::default()
844        };
845        let err = registry.register_extension(descriptor).unwrap_err();
846        assert!(err.contains("Commitment"), "unexpected error: {err}");
847    }
848
849    #[test]
850    fn unregister_extension_mode() {
851        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
852        let descriptor = ModeDescriptor {
853            mode: "ext.temp.v1".into(),
854            mode_version: "1.0.0".into(),
855            message_types: vec!["SessionStart".into(), "Commitment".into()],
856            terminal_message_types: vec!["Commitment".into()],
857            ..Default::default()
858        };
859        registry.register_extension(descriptor).unwrap();
860        assert_eq!(registry.all_mode_names().len(), 7);
861        registry.unregister_extension("ext.temp.v1").unwrap();
862        assert_eq!(registry.all_mode_names().len(), 6);
863        assert!(registry.get_mode("ext.temp.v1").is_none());
864    }
865
866    #[test]
867    fn cannot_unregister_builtin() {
868        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
869        assert!(registry.unregister_extension("ext.multi_round.v1").is_err());
870        assert!(registry
871            .unregister_extension("macp.mode.decision.v1")
872            .is_err());
873    }
874
875    #[test]
876    fn promote_rejects_reserved_namespace_target() {
877        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
878        let descriptor = ModeDescriptor {
879            mode: "ext.new.v1".into(),
880            mode_version: "1.0.0".into(),
881            message_types: vec!["SessionStart".into(), "Commitment".into()],
882            terminal_message_types: vec!["Commitment".into()],
883            ..Default::default()
884        };
885        registry.register_extension(descriptor).unwrap();
886
887        // Promotion must not be a side door into the reserved RFC namespace.
888        let err = registry
889            .promote_mode("ext.new.v1", Some("macp.mode.new.v1"))
890            .unwrap_err();
891        assert!(err.contains("reserved"), "unexpected error: {err}");
892
893        // The failed promotion must not have mutated the entry.
894        assert!(registry.get_mode("ext.new.v1").is_some());
895        assert!(!registry.is_standard_mode("ext.new.v1"));
896        assert!(registry.get_mode("macp.mode.new.v1").is_none());
897    }
898
899    #[test]
900    fn promote_extension_to_standard_with_ext_rename() {
901        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
902        let descriptor = ModeDescriptor {
903            mode: "ext.new.v1".into(),
904            mode_version: "1.0.0".into(),
905            message_types: vec!["SessionStart".into(), "Commitment".into()],
906            terminal_message_types: vec!["Commitment".into()],
907            ..Default::default()
908        };
909        registry.register_extension(descriptor).unwrap();
910        assert!(!registry.is_standard_mode("ext.new.v1"));
911        assert!(!registry.requires_strict_session_start("ext.new.v1"));
912
913        // Rename within the extension namespace is allowed.
914        let final_name = registry
915            .promote_mode("ext.new.v1", Some("ext.new.v2"))
916            .unwrap();
917        assert_eq!(final_name, "ext.new.v2");
918        assert!(registry.is_standard_mode("ext.new.v2"));
919        assert!(registry.requires_strict_session_start("ext.new.v2"));
920        assert!(registry.get_mode("ext.new.v1").is_none());
921        assert!(registry.get_mode("ext.new.v2").is_some());
922    }
923
924    #[test]
925    fn promote_without_rename() {
926        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
927        let descriptor = ModeDescriptor {
928            mode: "ext.keep.v1".into(),
929            mode_version: "1.0.0".into(),
930            message_types: vec!["SessionStart".into(), "Commitment".into()],
931            terminal_message_types: vec!["Commitment".into()],
932            ..Default::default()
933        };
934        registry.register_extension(descriptor).unwrap();
935        let final_name = registry.promote_mode("ext.keep.v1", None).unwrap();
936        assert_eq!(final_name, "ext.keep.v1");
937        assert!(registry.is_standard_mode("ext.keep.v1"));
938        assert!(registry.requires_strict_session_start("ext.keep.v1"));
939    }
940
941    #[test]
942    fn promoted_mode_appears_in_standard_mode_names_and_descriptors() {
943        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
944        let descriptor = ModeDescriptor {
945            mode: "ext.promoted.v1".into(),
946            mode_version: "1.0.0".into(),
947            title: "Promoted".into(),
948            message_types: vec!["SessionStart".into(), "Commitment".into()],
949            terminal_message_types: vec!["Commitment".into()],
950            ..Default::default()
951        };
952        registry.register_extension(descriptor).unwrap();
953        registry.promote_mode("ext.promoted.v1", None).unwrap();
954
955        let standard_names = registry.standard_mode_names();
956        assert!(standard_names.contains(&"ext.promoted.v1".to_string()));
957
958        let standard_modes: Vec<String> = registry
959            .standard_mode_descriptors()
960            .into_iter()
961            .map(|d| d.mode)
962            .collect();
963        assert!(standard_modes.contains(&"ext.promoted.v1".to_string()));
964    }
965
966    #[test]
967    fn promote_already_standard_fails() {
968        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
969        assert!(registry
970            .promote_mode("macp.mode.decision.v1", None)
971            .is_err());
972    }
973
974    #[test]
975    fn extension_names_constant_matches() {
976        for name in EXTENSION_MODE_NAMES {
977            let registry =
978                ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
979            assert!(
980                registry.get_mode(name).is_some(),
981                "EXTENSION_MODE_NAMES entry missing from registry: {name}"
982            );
983        }
984    }
985
986    #[test]
987    fn conformance_catalog_exposes_builtin_fixture_sets() {
988        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
989        let catalog = registry.all_mode_conformance();
990        let decision = catalog
991            .into_iter()
992            .find(|(name, _)| name == "macp.mode.decision.v1")
993            .expect("decision catalog should exist");
994        assert_eq!(decision.1.fixture_set_name, "decision");
995        assert!(decision
996            .1
997            .conformance_fixture_paths
998            .iter()
999            .any(|path| path.ends_with("decision_happy_path.json")));
1000    }
1001}