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 for terminal in &descriptor.terminal_message_types {
482 if !descriptor
483 .message_types
484 .iter()
485 .any(|message_type| message_type == terminal)
486 {
487 return Err(format!(
488 "terminal message type '{}' must also appear in message_types",
489 terminal
490 ));
491 }
492 }
493 Ok(())
494 }
495
496 pub fn register_extension(&self, descriptor: ModeDescriptor) -> Result<(), String> {
503 Self::validate_extension_descriptor(&descriptor)?;
504 let name = descriptor.mode.clone();
505 let schema_uris = descriptor.schema_uris.clone();
506 let allowed_types: Vec<String> = descriptor
507 .message_types
508 .iter()
509 .filter(|t| *t != "SessionStart")
510 .cloned()
511 .collect();
512 let allowed_types = Arc::new(allowed_types);
513 let factory: Arc<dyn ModeFactory> = Arc::new(ClosureModeFactory::new(Arc::new({
514 let allowed_types = Arc::clone(&allowed_types);
515 move || {
516 Box::new(PassthroughMode {
517 allowed_message_types: (*allowed_types).clone(),
518 }) as Box<dyn Mode>
519 }
520 })));
521 let descriptor_provider = Arc::new(StaticModeDescriptorProvider::new(Some(descriptor)));
522 let schema_provider = Arc::new(StaticModeSchemaProvider::new(schema_uris));
523 let conformance_provider = Arc::new(StaticModeConformanceProvider::default());
524
525 let mut guard = self.entries.write().unwrap_or_else(|e| e.into_inner());
526 if guard.contains_key(&name) {
527 return Err(format!("mode '{}' is already registered", name));
528 }
529 guard.insert(
530 name.clone(),
531 ModeRegistration {
532 mode_name: name,
533 factory,
534 descriptor_provider,
535 schema_provider,
536 conformance_provider,
537 standards_track: false,
538 builtin: false,
539 strict_session_start: false,
540 },
541 );
542 drop(guard);
543 let _ = self.change_tx.send(());
544 Ok(())
545 }
546
547 pub fn unregister_extension(&self, mode: &str) -> Result<(), String> {
549 let mut guard = self.entries.write().unwrap_or_else(|e| e.into_inner());
550 match guard.get(mode) {
551 None => return Err(format!("mode '{}' not found", mode)),
552 Some(entry) if entry.builtin => {
553 return Err(format!("cannot unregister built-in mode '{}'", mode))
554 }
555 Some(entry) if entry.standards_track => {
556 return Err(format!("cannot unregister standards-track mode '{}'", mode))
557 }
558 _ => {}
559 }
560 guard.remove(mode);
561 drop(guard);
562 let _ = self.change_tx.send(());
563 Ok(())
564 }
565
566 pub fn promote_mode(&self, mode: &str, new_name: Option<&str>) -> Result<String, String> {
569 let mut guard = self.entries.write().unwrap_or_else(|e| e.into_inner());
570 let entry = guard
571 .get(mode)
572 .ok_or_else(|| format!("mode '{}' not found", mode))?;
573 if entry.standards_track {
574 return Err(format!("mode '{}' is already standards-track", mode));
575 }
576
577 let final_name = new_name.unwrap_or(mode).to_string();
578 if final_name != mode && guard.contains_key(&final_name) {
579 return Err(format!(
580 "cannot promote: target name '{}' already exists",
581 final_name
582 ));
583 }
584
585 let mut registration = guard
586 .remove(mode)
587 .ok_or_else(|| format!("mode '{}' not found", mode))?;
588 registration.standards_track = true;
589 registration.strict_session_start = true;
590 registration.mode_name = final_name.clone();
591 guard.insert(final_name.clone(), registration);
592 drop(guard);
593 let _ = self.change_tx.send(());
594 Ok(final_name)
595 }
596
597 pub fn subscribe_changes(&self) -> broadcast::Receiver<()> {
599 self.change_tx.subscribe()
600 }
601}
602
603pub struct ModeRef<'a> {
606 registry: &'a ModeRegistry,
607 name: String,
608}
609
610impl<'a> ModeRef<'a> {
611 fn factory(&self) -> Result<Arc<dyn ModeFactory>, macp_core::error::MacpError> {
612 let guard = self
613 .registry
614 .entries
615 .read()
616 .unwrap_or_else(|e| e.into_inner());
617 guard
618 .get(&self.name)
619 .map(|entry| Arc::clone(&entry.factory))
620 .ok_or(macp_core::error::MacpError::UnknownMode)
621 }
622
623 pub fn on_session_start(
624 &self,
625 session: &macp_core::session::Session,
626 env: &macp_pb::pb::Envelope,
627 ) -> Result<crate::mode::ModeResponse, macp_core::error::MacpError> {
628 let mode = self.factory()?.create();
629 mode.on_session_start(session, env)
630 }
631
632 pub fn on_message(
633 &self,
634 session: &macp_core::session::Session,
635 env: &macp_pb::pb::Envelope,
636 ) -> Result<crate::mode::ModeResponse, macp_core::error::MacpError> {
637 let mode = self.factory()?.create();
638 mode.on_message(session, env)
639 }
640
641 pub fn authorize_sender(
642 &self,
643 session: &macp_core::session::Session,
644 env: &macp_pb::pb::Envelope,
645 ) -> Result<(), macp_core::error::MacpError> {
646 let mode = self.factory()?.create();
647 mode.authorize_sender(session, env)
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use crate::mode::EXTENSION_MODE_NAMES;
655
656 #[test]
657 fn build_default_contains_all_standard_modes() {
658 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
659 for name in STANDARD_MODE_NAMES {
660 assert!(registry.get_mode(name).is_some(), "missing mode: {name}");
661 assert!(registry.is_standard_mode(name));
662 assert!(registry.requires_strict_session_start(name));
663 }
664 }
665
666 #[test]
667 fn build_default_contains_multi_round_as_extension() {
668 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
669 assert!(registry.get_mode("ext.multi_round.v1").is_some());
670 assert!(!registry.is_standard_mode("ext.multi_round.v1"));
671 assert!(registry.requires_strict_session_start("ext.multi_round.v1"));
672 }
673
674 #[test]
675 fn standard_mode_names_returns_five() {
676 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
677 assert_eq!(registry.standard_mode_names().len(), 5);
678 }
679
680 #[test]
681 fn standard_mode_descriptors_returns_five() {
682 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
683 assert_eq!(registry.standard_mode_descriptors().len(), 5);
684 }
685
686 #[test]
687 fn all_mode_names_returns_six() {
688 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
689 assert_eq!(registry.all_mode_names().len(), 6);
690 }
691
692 #[test]
693 fn extension_mode_names_returns_one() {
694 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
695 let ext = registry.extension_mode_names();
696 assert_eq!(ext.len(), 1);
697 assert!(ext.contains(&"ext.multi_round.v1".to_string()));
698 }
699
700 #[test]
701 fn extension_mode_descriptors_returns_one() {
702 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
703 let descs = registry.extension_mode_descriptors();
704 assert_eq!(descs.len(), 1);
705 assert_eq!(descs[0].mode, "ext.multi_round.v1");
706 }
707
708 #[test]
709 fn unknown_mode_returns_none() {
710 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
711 assert!(registry.get_mode("nonexistent").is_none());
712 assert!(!registry.is_standard_mode("nonexistent"));
713 assert!(!registry.requires_strict_session_start("nonexistent"));
714 }
715
716 #[test]
717 fn register_extension_mode() {
718 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
719 let descriptor = ModeDescriptor {
720 mode: "ext.custom.v1".into(),
721 mode_version: "1.0.0".into(),
722 title: "Custom Mode".into(),
723 description: "Test custom mode".into(),
724 message_types: vec![
725 "SessionStart".into(),
726 "CustomMsg".into(),
727 "Commitment".into(),
728 ],
729 terminal_message_types: vec!["Commitment".into()],
730 ..Default::default()
731 };
732 registry.register_extension(descriptor).unwrap();
733 assert!(registry.get_mode("ext.custom.v1").is_some());
734 assert!(!registry.is_standard_mode("ext.custom.v1"));
735 assert_eq!(registry.all_mode_names().len(), 7);
736 assert_eq!(registry.extension_mode_names().len(), 2);
737 }
738
739 #[test]
740 fn register_rejects_macp_namespace() {
741 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
742 let descriptor = ModeDescriptor {
743 mode: "macp.mode.evil.v1".into(),
744 mode_version: "1.0.0".into(),
745 message_types: vec!["SessionStart".into(), "Commitment".into()],
746 ..Default::default()
747 };
748 assert!(registry.register_extension(descriptor).is_err());
749 }
750
751 #[test]
752 fn register_rejects_empty_message_types() {
753 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
754 let descriptor = ModeDescriptor {
755 mode: "ext.invalid.v1".into(),
756 mode_version: "1.0.0".into(),
757 ..Default::default()
758 };
759 assert!(registry.register_extension(descriptor).is_err());
760 }
761
762 #[test]
763 fn register_rejects_terminal_not_in_message_types() {
764 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
765 let descriptor = ModeDescriptor {
766 mode: "ext.invalid.v1".into(),
767 mode_version: "1.0.0".into(),
768 message_types: vec!["SessionStart".into(), "Custom".into()],
769 terminal_message_types: vec!["Commitment".into()],
770 ..Default::default()
771 };
772 assert!(registry.register_extension(descriptor).is_err());
773 }
774
775 #[test]
776 fn register_rejects_duplicate() {
777 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
778 let descriptor = ModeDescriptor {
779 mode: "ext.multi_round.v1".into(),
780 mode_version: "1.0.0".into(),
781 message_types: vec!["SessionStart".into(), "Commitment".into()],
782 ..Default::default()
783 };
784 assert!(registry.register_extension(descriptor).is_err());
785 }
786
787 #[test]
788 fn unregister_extension_mode() {
789 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
790 let descriptor = ModeDescriptor {
791 mode: "ext.temp.v1".into(),
792 mode_version: "1.0.0".into(),
793 message_types: vec!["SessionStart".into(), "Commitment".into()],
794 ..Default::default()
795 };
796 registry.register_extension(descriptor).unwrap();
797 assert_eq!(registry.all_mode_names().len(), 7);
798 registry.unregister_extension("ext.temp.v1").unwrap();
799 assert_eq!(registry.all_mode_names().len(), 6);
800 assert!(registry.get_mode("ext.temp.v1").is_none());
801 }
802
803 #[test]
804 fn cannot_unregister_builtin() {
805 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
806 assert!(registry.unregister_extension("ext.multi_round.v1").is_err());
807 assert!(registry
808 .unregister_extension("macp.mode.decision.v1")
809 .is_err());
810 }
811
812 #[test]
813 fn promote_extension_to_standard() {
814 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
815 let descriptor = ModeDescriptor {
816 mode: "ext.new.v1".into(),
817 mode_version: "1.0.0".into(),
818 message_types: vec!["SessionStart".into(), "Commitment".into()],
819 terminal_message_types: vec!["Commitment".into()],
820 ..Default::default()
821 };
822 registry.register_extension(descriptor).unwrap();
823 assert!(!registry.is_standard_mode("ext.new.v1"));
824 assert!(!registry.requires_strict_session_start("ext.new.v1"));
825
826 let final_name = registry
827 .promote_mode("ext.new.v1", Some("macp.mode.new.v1"))
828 .unwrap();
829 assert_eq!(final_name, "macp.mode.new.v1");
830 assert!(registry.is_standard_mode("macp.mode.new.v1"));
831 assert!(registry.requires_strict_session_start("macp.mode.new.v1"));
832 assert!(registry.get_mode("ext.new.v1").is_none());
833 assert!(registry.get_mode("macp.mode.new.v1").is_some());
834 }
835
836 #[test]
837 fn promote_without_rename() {
838 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
839 let descriptor = ModeDescriptor {
840 mode: "ext.keep.v1".into(),
841 mode_version: "1.0.0".into(),
842 message_types: vec!["SessionStart".into(), "Commitment".into()],
843 terminal_message_types: vec!["Commitment".into()],
844 ..Default::default()
845 };
846 registry.register_extension(descriptor).unwrap();
847 let final_name = registry.promote_mode("ext.keep.v1", None).unwrap();
848 assert_eq!(final_name, "ext.keep.v1");
849 assert!(registry.is_standard_mode("ext.keep.v1"));
850 assert!(registry.requires_strict_session_start("ext.keep.v1"));
851 }
852
853 #[test]
854 fn promoted_mode_appears_in_standard_mode_names_and_descriptors() {
855 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
856 let descriptor = ModeDescriptor {
857 mode: "ext.promoted.v1".into(),
858 mode_version: "1.0.0".into(),
859 title: "Promoted".into(),
860 message_types: vec!["SessionStart".into(), "Commitment".into()],
861 terminal_message_types: vec!["Commitment".into()],
862 ..Default::default()
863 };
864 registry.register_extension(descriptor).unwrap();
865 registry
866 .promote_mode("ext.promoted.v1", Some("macp.mode.promoted.v1"))
867 .unwrap();
868
869 let standard_names = registry.standard_mode_names();
870 assert!(standard_names.contains(&"macp.mode.promoted.v1".to_string()));
871
872 let standard_modes: Vec<String> = registry
873 .standard_mode_descriptors()
874 .into_iter()
875 .map(|d| d.mode)
876 .collect();
877 assert!(standard_modes.contains(&"macp.mode.promoted.v1".to_string()));
878 }
879
880 #[test]
881 fn promote_already_standard_fails() {
882 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
883 assert!(registry
884 .promote_mode("macp.mode.decision.v1", None)
885 .is_err());
886 }
887
888 #[test]
889 fn extension_names_constant_matches() {
890 for name in EXTENSION_MODE_NAMES {
891 let registry =
892 ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
893 assert!(
894 registry.get_mode(name).is_some(),
895 "EXTENSION_MODE_NAMES entry missing from registry: {name}"
896 );
897 }
898 }
899
900 #[test]
901 fn conformance_catalog_exposes_builtin_fixture_sets() {
902 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
903 let catalog = registry.all_mode_conformance();
904 let decision = catalog
905 .into_iter()
906 .find(|(name, _)| name == "macp.mode.decision.v1")
907 .expect("decision catalog should exist");
908 assert_eq!(decision.1.fixture_set_name, "decision");
909 assert!(decision
910 .1
911 .conformance_fixture_paths
912 .iter()
913 .any(|path| path.ends_with("decision_happy_path.json")));
914 }
915}