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 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 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 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 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 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 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 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 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 pub fn subscribe_changes(&self) -> broadcast::Receiver<()> {
631 self.change_tx.subscribe()
632 }
633}
634
635pub 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 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 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 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 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 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 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}