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 only_decision_accepts_an_empty_roster_at_the_mode_layer() {
736 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
751 let env = macp_pb::pb::Envelope {
752 message_type: "SessionStart".into(),
753 sender: "agent://initiator".into(),
754 ..Default::default()
755 };
756
757 let mut names: Vec<&str> = STANDARD_MODE_NAMES.to_vec();
758 names.push("ext.multi_round.v1");
759 assert_eq!(
760 names.len(),
761 6,
762 "the strict-mode set is five standard plus multi_round"
763 );
764
765 for name in names {
766 let session = macp_core::session::Session::builder("s1", name, "agent://initiator")
767 .participants(vec![])
768 .build();
769 let mode = registry.get_mode(name).expect("mode is registered");
770 let result = mode.on_session_start(&session, &env);
771 if name == "macp.mode.decision.v1" {
772 assert!(
773 matches!(result, Ok(crate::mode::ModeResponse::PersistState(_))),
774 "Decision is the one carve-out and must accept an empty roster, got: {result:?}"
775 );
776 } else {
777 assert_eq!(
778 result.unwrap_err().to_string(),
779 "InvalidPayload",
780 "{name} must reject an empty roster in its own on_session_start"
781 );
782 }
783 }
784 }
785
786 #[test]
787 fn unknown_mode_returns_none() {
788 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
789 assert!(registry.get_mode("nonexistent").is_none());
790 assert!(!registry.is_standard_mode("nonexistent"));
791 assert!(!registry.requires_strict_session_start("nonexistent"));
792 }
793
794 #[test]
795 fn register_extension_mode() {
796 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
797 let descriptor = ModeDescriptor {
798 mode: "ext.custom.v1".into(),
799 mode_version: "1.0.0".into(),
800 title: "Custom Mode".into(),
801 description: "Test custom mode".into(),
802 message_types: vec![
803 "SessionStart".into(),
804 "CustomMsg".into(),
805 "Commitment".into(),
806 ],
807 terminal_message_types: vec!["Commitment".into()],
808 ..Default::default()
809 };
810 registry.register_extension(descriptor).unwrap();
811 assert!(registry.get_mode("ext.custom.v1").is_some());
812 assert!(!registry.is_standard_mode("ext.custom.v1"));
813 assert_eq!(registry.all_mode_names().len(), 7);
814 assert_eq!(registry.extension_mode_names().len(), 2);
815 }
816
817 #[test]
818 fn register_rejects_macp_namespace() {
819 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
820 let descriptor = ModeDescriptor {
821 mode: "macp.mode.evil.v1".into(),
822 mode_version: "1.0.0".into(),
823 message_types: vec!["SessionStart".into(), "Commitment".into()],
824 ..Default::default()
825 };
826 assert!(registry.register_extension(descriptor).is_err());
827 }
828
829 #[test]
830 fn register_rejects_empty_message_types() {
831 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
832 let descriptor = ModeDescriptor {
833 mode: "ext.invalid.v1".into(),
834 mode_version: "1.0.0".into(),
835 ..Default::default()
836 };
837 assert!(registry.register_extension(descriptor).is_err());
838 }
839
840 #[test]
841 fn register_rejects_terminal_not_in_message_types() {
842 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
843 let descriptor = ModeDescriptor {
844 mode: "ext.invalid.v1".into(),
845 mode_version: "1.0.0".into(),
846 message_types: vec!["SessionStart".into(), "Custom".into()],
847 terminal_message_types: vec!["Commitment".into()],
848 ..Default::default()
849 };
850 assert!(registry.register_extension(descriptor).is_err());
851 }
852
853 #[test]
854 fn register_rejects_duplicate() {
855 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
856 let descriptor = ModeDescriptor {
859 mode: "ext.multi_round.v1".into(),
860 mode_version: "1.0.0".into(),
861 message_types: vec!["SessionStart".into(), "Commitment".into()],
862 terminal_message_types: vec!["Commitment".into()],
863 ..Default::default()
864 };
865 assert!(registry.register_extension(descriptor).is_err());
866 }
867
868 #[test]
869 fn register_rejects_empty_terminal_types() {
870 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
871 let descriptor = ModeDescriptor {
872 mode: "ext.noterm.v1".into(),
873 mode_version: "1.0.0".into(),
874 message_types: vec!["SessionStart".into(), "Commitment".into()],
875 terminal_message_types: vec![],
876 ..Default::default()
877 };
878 let err = registry.register_extension(descriptor).unwrap_err();
879 assert!(
880 err.contains("terminal message type"),
881 "unexpected error: {err}"
882 );
883 }
884
885 #[test]
886 fn register_rejects_non_commitment_terminal() {
887 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
888 let descriptor = ModeDescriptor {
891 mode: "ext.badterm.v1".into(),
892 mode_version: "1.0.0".into(),
893 message_types: vec!["SessionStart".into(), "Finalize".into()],
894 terminal_message_types: vec!["Finalize".into()],
895 ..Default::default()
896 };
897 let err = registry.register_extension(descriptor).unwrap_err();
898 assert!(err.contains("Commitment"), "unexpected error: {err}");
899 }
900
901 #[test]
902 fn unregister_extension_mode() {
903 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
904 let descriptor = ModeDescriptor {
905 mode: "ext.temp.v1".into(),
906 mode_version: "1.0.0".into(),
907 message_types: vec!["SessionStart".into(), "Commitment".into()],
908 terminal_message_types: vec!["Commitment".into()],
909 ..Default::default()
910 };
911 registry.register_extension(descriptor).unwrap();
912 assert_eq!(registry.all_mode_names().len(), 7);
913 registry.unregister_extension("ext.temp.v1").unwrap();
914 assert_eq!(registry.all_mode_names().len(), 6);
915 assert!(registry.get_mode("ext.temp.v1").is_none());
916 }
917
918 #[test]
919 fn cannot_unregister_builtin() {
920 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
921 assert!(registry.unregister_extension("ext.multi_round.v1").is_err());
922 assert!(registry
923 .unregister_extension("macp.mode.decision.v1")
924 .is_err());
925 }
926
927 #[test]
928 fn promote_rejects_reserved_namespace_target() {
929 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
930 let descriptor = ModeDescriptor {
931 mode: "ext.new.v1".into(),
932 mode_version: "1.0.0".into(),
933 message_types: vec!["SessionStart".into(), "Commitment".into()],
934 terminal_message_types: vec!["Commitment".into()],
935 ..Default::default()
936 };
937 registry.register_extension(descriptor).unwrap();
938
939 let err = registry
941 .promote_mode("ext.new.v1", Some("macp.mode.new.v1"))
942 .unwrap_err();
943 assert!(err.contains("reserved"), "unexpected error: {err}");
944
945 assert!(registry.get_mode("ext.new.v1").is_some());
947 assert!(!registry.is_standard_mode("ext.new.v1"));
948 assert!(registry.get_mode("macp.mode.new.v1").is_none());
949 }
950
951 #[test]
952 fn promote_extension_to_standard_with_ext_rename() {
953 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
954 let descriptor = ModeDescriptor {
955 mode: "ext.new.v1".into(),
956 mode_version: "1.0.0".into(),
957 message_types: vec!["SessionStart".into(), "Commitment".into()],
958 terminal_message_types: vec!["Commitment".into()],
959 ..Default::default()
960 };
961 registry.register_extension(descriptor).unwrap();
962 assert!(!registry.is_standard_mode("ext.new.v1"));
963 assert!(!registry.requires_strict_session_start("ext.new.v1"));
964
965 let final_name = registry
967 .promote_mode("ext.new.v1", Some("ext.new.v2"))
968 .unwrap();
969 assert_eq!(final_name, "ext.new.v2");
970 assert!(registry.is_standard_mode("ext.new.v2"));
971 assert!(registry.requires_strict_session_start("ext.new.v2"));
972 assert!(registry.get_mode("ext.new.v1").is_none());
973 assert!(registry.get_mode("ext.new.v2").is_some());
974 }
975
976 #[test]
977 fn promote_without_rename() {
978 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
979 let descriptor = ModeDescriptor {
980 mode: "ext.keep.v1".into(),
981 mode_version: "1.0.0".into(),
982 message_types: vec!["SessionStart".into(), "Commitment".into()],
983 terminal_message_types: vec!["Commitment".into()],
984 ..Default::default()
985 };
986 registry.register_extension(descriptor).unwrap();
987 let final_name = registry.promote_mode("ext.keep.v1", None).unwrap();
988 assert_eq!(final_name, "ext.keep.v1");
989 assert!(registry.is_standard_mode("ext.keep.v1"));
990 assert!(registry.requires_strict_session_start("ext.keep.v1"));
991 }
992
993 #[test]
994 fn promoted_mode_appears_in_standard_mode_names_and_descriptors() {
995 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
996 let descriptor = ModeDescriptor {
997 mode: "ext.promoted.v1".into(),
998 mode_version: "1.0.0".into(),
999 title: "Promoted".into(),
1000 message_types: vec!["SessionStart".into(), "Commitment".into()],
1001 terminal_message_types: vec!["Commitment".into()],
1002 ..Default::default()
1003 };
1004 registry.register_extension(descriptor).unwrap();
1005 registry.promote_mode("ext.promoted.v1", None).unwrap();
1006
1007 let standard_names = registry.standard_mode_names();
1008 assert!(standard_names.contains(&"ext.promoted.v1".to_string()));
1009
1010 let standard_modes: Vec<String> = registry
1011 .standard_mode_descriptors()
1012 .into_iter()
1013 .map(|d| d.mode)
1014 .collect();
1015 assert!(standard_modes.contains(&"ext.promoted.v1".to_string()));
1016 }
1017
1018 #[test]
1019 fn promote_already_standard_fails() {
1020 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
1021 assert!(registry
1022 .promote_mode("macp.mode.decision.v1", None)
1023 .is_err());
1024 }
1025
1026 #[test]
1027 fn extension_names_constant_matches() {
1028 for name in EXTENSION_MODE_NAMES {
1029 let registry =
1030 ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
1031 assert!(
1032 registry.get_mode(name).is_some(),
1033 "EXTENSION_MODE_NAMES entry missing from registry: {name}"
1034 );
1035 }
1036 }
1037
1038 #[test]
1039 fn conformance_catalog_exposes_builtin_fixture_sets() {
1040 let registry = ModeRegistry::build_default(Arc::new(macp_policy::DefaultPolicyEvaluator));
1041 let catalog = registry.all_mode_conformance();
1042 let decision = catalog
1043 .into_iter()
1044 .find(|(name, _)| name == "macp.mode.decision.v1")
1045 .expect("decision catalog should exist");
1046 assert_eq!(decision.1.fixture_set_name, "decision");
1047 assert!(decision
1048 .1
1049 .conformance_fixture_paths
1050 .iter()
1051 .any(|path| path.ends_with("decision_happy_path.json")));
1052 }
1053}