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 standard_mode_names_returns_five() {
721        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
722        assert_eq!(registry.standard_mode_names().len(), 5);
723    }
724
725    #[test]
726    fn standard_mode_descriptors_returns_five() {
727        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
728        assert_eq!(registry.standard_mode_descriptors().len(), 5);
729    }
730
731    #[test]
732    fn all_mode_names_returns_six() {
733        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
734        assert_eq!(registry.all_mode_names().len(), 6);
735    }
736
737    #[test]
738    fn extension_mode_names_returns_one() {
739        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
740        let ext = registry.extension_mode_names();
741        assert_eq!(ext.len(), 1);
742        assert!(ext.contains(&"ext.multi_round.v1".to_string()));
743    }
744
745    #[test]
746    fn extension_mode_descriptors_returns_one() {
747        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
748        let descs = registry.extension_mode_descriptors();
749        assert_eq!(descs.len(), 1);
750        assert_eq!(descs[0].mode, "ext.multi_round.v1");
751    }
752
753    #[test]
754    fn unknown_mode_returns_none() {
755        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
756        assert!(registry.get_mode("nonexistent").is_none());
757        assert!(!registry.is_standard_mode("nonexistent"));
758        assert!(!registry.requires_strict_session_start("nonexistent"));
759    }
760
761    #[test]
762    fn register_extension_mode() {
763        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
764        let descriptor = ModeDescriptor {
765            mode: "ext.custom.v1".into(),
766            mode_version: "1.0.0".into(),
767            title: "Custom Mode".into(),
768            description: "Test custom mode".into(),
769            message_types: vec![
770                "SessionStart".into(),
771                "CustomMsg".into(),
772                "Commitment".into(),
773            ],
774            terminal_message_types: vec!["Commitment".into()],
775            ..Default::default()
776        };
777        registry.register_extension(descriptor).unwrap();
778        assert!(registry.get_mode("ext.custom.v1").is_some());
779        assert!(!registry.is_standard_mode("ext.custom.v1"));
780        assert_eq!(registry.all_mode_names().len(), 7);
781        assert_eq!(registry.extension_mode_names().len(), 2);
782    }
783
784    #[test]
785    fn register_rejects_macp_namespace() {
786        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
787        let descriptor = ModeDescriptor {
788            mode: "macp.mode.evil.v1".into(),
789            mode_version: "1.0.0".into(),
790            message_types: vec!["SessionStart".into(), "Commitment".into()],
791            ..Default::default()
792        };
793        assert!(registry.register_extension(descriptor).is_err());
794    }
795
796    #[test]
797    fn register_rejects_empty_message_types() {
798        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
799        let descriptor = ModeDescriptor {
800            mode: "ext.invalid.v1".into(),
801            mode_version: "1.0.0".into(),
802            ..Default::default()
803        };
804        assert!(registry.register_extension(descriptor).is_err());
805    }
806
807    #[test]
808    fn register_rejects_terminal_not_in_message_types() {
809        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
810        let descriptor = ModeDescriptor {
811            mode: "ext.invalid.v1".into(),
812            mode_version: "1.0.0".into(),
813            message_types: vec!["SessionStart".into(), "Custom".into()],
814            terminal_message_types: vec!["Commitment".into()],
815            ..Default::default()
816        };
817        assert!(registry.register_extension(descriptor).is_err());
818    }
819
820    #[test]
821    fn register_rejects_duplicate() {
822        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
823        // Descriptor is otherwise valid so the failure exercised is the duplicate
824        // check, not descriptor validation.
825        let descriptor = ModeDescriptor {
826            mode: "ext.multi_round.v1".into(),
827            mode_version: "1.0.0".into(),
828            message_types: vec!["SessionStart".into(), "Commitment".into()],
829            terminal_message_types: vec!["Commitment".into()],
830            ..Default::default()
831        };
832        assert!(registry.register_extension(descriptor).is_err());
833    }
834
835    #[test]
836    fn register_rejects_empty_terminal_types() {
837        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
838        let descriptor = ModeDescriptor {
839            mode: "ext.noterm.v1".into(),
840            mode_version: "1.0.0".into(),
841            message_types: vec!["SessionStart".into(), "Commitment".into()],
842            terminal_message_types: vec![],
843            ..Default::default()
844        };
845        let err = registry.register_extension(descriptor).unwrap_err();
846        assert!(
847            err.contains("terminal message type"),
848            "unexpected error: {err}"
849        );
850    }
851
852    #[test]
853    fn register_rejects_non_commitment_terminal() {
854        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
855        // Passthrough-backed modes resolve only on Commitment; a different declared
856        // terminal would be advertised but non-functional.
857        let descriptor = ModeDescriptor {
858            mode: "ext.badterm.v1".into(),
859            mode_version: "1.0.0".into(),
860            message_types: vec!["SessionStart".into(), "Finalize".into()],
861            terminal_message_types: vec!["Finalize".into()],
862            ..Default::default()
863        };
864        let err = registry.register_extension(descriptor).unwrap_err();
865        assert!(err.contains("Commitment"), "unexpected error: {err}");
866    }
867
868    #[test]
869    fn unregister_extension_mode() {
870        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
871        let descriptor = ModeDescriptor {
872            mode: "ext.temp.v1".into(),
873            mode_version: "1.0.0".into(),
874            message_types: vec!["SessionStart".into(), "Commitment".into()],
875            terminal_message_types: vec!["Commitment".into()],
876            ..Default::default()
877        };
878        registry.register_extension(descriptor).unwrap();
879        assert_eq!(registry.all_mode_names().len(), 7);
880        registry.unregister_extension("ext.temp.v1").unwrap();
881        assert_eq!(registry.all_mode_names().len(), 6);
882        assert!(registry.get_mode("ext.temp.v1").is_none());
883    }
884
885    #[test]
886    fn cannot_unregister_builtin() {
887        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
888        assert!(registry.unregister_extension("ext.multi_round.v1").is_err());
889        assert!(registry
890            .unregister_extension("macp.mode.decision.v1")
891            .is_err());
892    }
893
894    #[test]
895    fn promote_rejects_reserved_namespace_target() {
896        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
897        let descriptor = ModeDescriptor {
898            mode: "ext.new.v1".into(),
899            mode_version: "1.0.0".into(),
900            message_types: vec!["SessionStart".into(), "Commitment".into()],
901            terminal_message_types: vec!["Commitment".into()],
902            ..Default::default()
903        };
904        registry.register_extension(descriptor).unwrap();
905
906        // Promotion must not be a side door into the reserved RFC namespace.
907        let err = registry
908            .promote_mode("ext.new.v1", Some("macp.mode.new.v1"))
909            .unwrap_err();
910        assert!(err.contains("reserved"), "unexpected error: {err}");
911
912        // The failed promotion must not have mutated the entry.
913        assert!(registry.get_mode("ext.new.v1").is_some());
914        assert!(!registry.is_standard_mode("ext.new.v1"));
915        assert!(registry.get_mode("macp.mode.new.v1").is_none());
916    }
917
918    #[test]
919    fn promote_extension_to_standard_with_ext_rename() {
920        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
921        let descriptor = ModeDescriptor {
922            mode: "ext.new.v1".into(),
923            mode_version: "1.0.0".into(),
924            message_types: vec!["SessionStart".into(), "Commitment".into()],
925            terminal_message_types: vec!["Commitment".into()],
926            ..Default::default()
927        };
928        registry.register_extension(descriptor).unwrap();
929        assert!(!registry.is_standard_mode("ext.new.v1"));
930        assert!(!registry.requires_strict_session_start("ext.new.v1"));
931
932        // Rename within the extension namespace is allowed.
933        let final_name = registry
934            .promote_mode("ext.new.v1", Some("ext.new.v2"))
935            .unwrap();
936        assert_eq!(final_name, "ext.new.v2");
937        assert!(registry.is_standard_mode("ext.new.v2"));
938        assert!(registry.requires_strict_session_start("ext.new.v2"));
939        assert!(registry.get_mode("ext.new.v1").is_none());
940        assert!(registry.get_mode("ext.new.v2").is_some());
941    }
942
943    #[test]
944    fn promote_without_rename() {
945        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
946        let descriptor = ModeDescriptor {
947            mode: "ext.keep.v1".into(),
948            mode_version: "1.0.0".into(),
949            message_types: vec!["SessionStart".into(), "Commitment".into()],
950            terminal_message_types: vec!["Commitment".into()],
951            ..Default::default()
952        };
953        registry.register_extension(descriptor).unwrap();
954        let final_name = registry.promote_mode("ext.keep.v1", None).unwrap();
955        assert_eq!(final_name, "ext.keep.v1");
956        assert!(registry.is_standard_mode("ext.keep.v1"));
957        assert!(registry.requires_strict_session_start("ext.keep.v1"));
958    }
959
960    #[test]
961    fn promoted_mode_appears_in_standard_mode_names_and_descriptors() {
962        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
963        let descriptor = ModeDescriptor {
964            mode: "ext.promoted.v1".into(),
965            mode_version: "1.0.0".into(),
966            title: "Promoted".into(),
967            message_types: vec!["SessionStart".into(), "Commitment".into()],
968            terminal_message_types: vec!["Commitment".into()],
969            ..Default::default()
970        };
971        registry.register_extension(descriptor).unwrap();
972        registry.promote_mode("ext.promoted.v1", None).unwrap();
973
974        let standard_names = registry.standard_mode_names();
975        assert!(standard_names.contains(&"ext.promoted.v1".to_string()));
976
977        let standard_modes: Vec<String> = registry
978            .standard_mode_descriptors()
979            .into_iter()
980            .map(|d| d.mode)
981            .collect();
982        assert!(standard_modes.contains(&"ext.promoted.v1".to_string()));
983    }
984
985    #[test]
986    fn promote_already_standard_fails() {
987        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
988        assert!(registry
989            .promote_mode("macp.mode.decision.v1", None)
990            .is_err());
991    }
992
993    #[test]
994    fn extension_names_constant_matches() {
995        for name in EXTENSION_MODE_NAMES {
996            let registry =
997                ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
998            assert!(
999                registry.get_mode(name).is_some(),
1000                "EXTENSION_MODE_NAMES entry missing from registry: {name}"
1001            );
1002        }
1003    }
1004
1005    #[test]
1006    fn conformance_catalog_exposes_builtin_fixture_sets() {
1007        let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
1008        let catalog = registry.all_mode_conformance();
1009        let decision = catalog
1010            .into_iter()
1011            .find(|(name, _)| name == "macp.mode.decision.v1")
1012            .expect("decision catalog should exist");
1013        assert_eq!(decision.1.fixture_set_name, "decision");
1014        assert!(decision
1015            .1
1016            .conformance_fixture_paths
1017            .iter()
1018            .any(|path| path.ends_with("decision_happy_path.json")));
1019    }
1020}