1use std::sync::OnceLock;
4
5use schemars::JsonSchema;
6use semver::Version;
7use serde::{Deserialize, Serialize};
8
9use super::command::Namespace;
10
11const RESERVED_ROOT_NAMESPACES: &[&str] = &[
12 "apps",
13 "cli",
14 "completion",
15 "config",
16 "doctor",
17 "help",
18 "history",
19 "inspect",
20 "install",
21 "memory",
22 "plugins",
23 "repl",
24 "self",
25 "status",
26 "version",
27];
28const OFFICIAL_STATUS_VALUES: &[&str] = &["declared", "supported", "deprecated", "unsupported"];
29
30#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
31pub struct ProductRegistryDocument {
32 pub schema_version: String,
33 pub owner: String,
34 pub policy: String,
35 pub entries: Vec<ProductRegistryEntry>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
39pub struct ProductRegistryEntry {
40 pub namespace: String,
41 pub display_name: String,
42 #[serde(default)]
43 pub aliases: Vec<String>,
44 pub runtime_binary: String,
45 pub control_binary: String,
46 pub runtime_package: String,
47 pub control_package: String,
48 pub repository: String,
49 pub status: String,
50 pub language: String,
51 #[serde(default)]
52 pub version: Option<String>,
53 pub help_summary: String,
54 #[serde(default)]
55 pub capabilities: Vec<String>,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
59#[serde(rename_all = "snake_case")]
60pub enum ProductEntrypointKind {
61 Binary,
62 PythonModule,
63 PythonConsoleScript,
64 PluginProcess,
65 EmbeddedRust,
66}
67
68#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
69pub struct ProductEntrypoint {
70 pub kind: ProductEntrypointKind,
71 pub command: String,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub module: Option<String>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub function: Option<String>,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
79pub struct ProductHelpMetadata {
80 pub summary: String,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
84pub struct ProductCompatibilityWindow {
85 pub min_cli_version: String,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub max_cli_version_exclusive: Option<String>,
88}
89
90impl ProductCompatibilityWindow {
91 pub fn new(
93 min_cli_version: impl Into<String>,
94 max_cli_version_exclusive: Option<String>,
95 ) -> Result<Self, String> {
96 let window = Self { min_cli_version: min_cli_version.into(), max_cli_version_exclusive };
97 validate_compatibility_window(&window)?;
98 Ok(window)
99 }
100}
101
102#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
103pub struct ProductMountDescriptor {
104 pub namespace: Namespace,
105 pub display_name: String,
106 #[serde(default)]
107 pub aliases: Vec<Namespace>,
108 pub entrypoint: ProductEntrypoint,
109 pub control_entrypoint: ProductEntrypoint,
110 pub help: ProductHelpMetadata,
111 #[serde(default)]
112 pub capabilities: Vec<String>,
113 #[serde(default)]
114 pub version: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub compatibility: Option<ProductCompatibilityWindow>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct ProductMountDescriptorBuilder {
121 namespace: Namespace,
122 display_name: Option<String>,
123 aliases: Vec<Namespace>,
124 entrypoint: Option<ProductEntrypoint>,
125 control_entrypoint: Option<ProductEntrypoint>,
126 help_summary: Option<String>,
127 capabilities: Vec<String>,
128 version: Option<String>,
129 compatibility: Option<ProductCompatibilityWindow>,
130}
131
132impl ProductMountDescriptor {
133 #[must_use]
134 pub fn builder(namespace: Namespace) -> ProductMountDescriptorBuilder {
135 ProductMountDescriptorBuilder {
136 namespace,
137 display_name: None,
138 aliases: Vec::new(),
139 entrypoint: None,
140 control_entrypoint: None,
141 help_summary: None,
142 capabilities: Vec::new(),
143 version: None,
144 compatibility: None,
145 }
146 }
147}
148
149impl ProductMountDescriptorBuilder {
150 #[must_use]
151 pub fn display_name(mut self, value: impl Into<String>) -> Self {
152 self.display_name = Some(value.into());
153 self
154 }
155
156 #[must_use]
157 pub fn alias(mut self, value: Namespace) -> Self {
158 self.aliases.push(value);
159 self
160 }
161
162 #[must_use]
163 pub fn aliases(mut self, values: Vec<Namespace>) -> Self {
164 self.aliases.extend(values);
165 self
166 }
167
168 #[must_use]
169 pub fn entrypoint(mut self, kind: ProductEntrypointKind, command: impl Into<String>) -> Self {
170 self.entrypoint =
171 Some(ProductEntrypoint { kind, command: command.into(), module: None, function: None });
172 self
173 }
174
175 #[must_use]
176 pub fn entrypoint_value(mut self, entrypoint: ProductEntrypoint) -> Self {
177 self.entrypoint = Some(entrypoint);
178 self
179 }
180
181 #[must_use]
182 pub fn control_entrypoint(
183 mut self,
184 kind: ProductEntrypointKind,
185 command: impl Into<String>,
186 ) -> Self {
187 self.control_entrypoint =
188 Some(ProductEntrypoint { kind, command: command.into(), module: None, function: None });
189 self
190 }
191
192 #[must_use]
193 pub fn control_entrypoint_value(mut self, entrypoint: ProductEntrypoint) -> Self {
194 self.control_entrypoint = Some(entrypoint);
195 self
196 }
197
198 #[must_use]
199 pub fn help_summary(mut self, value: impl Into<String>) -> Self {
200 self.help_summary = Some(value.into());
201 self
202 }
203
204 #[must_use]
205 pub fn capability(mut self, value: impl Into<String>) -> Self {
206 self.capabilities.push(value.into());
207 self
208 }
209
210 #[must_use]
211 pub fn capabilities(mut self, values: Vec<String>) -> Self {
212 self.capabilities.extend(values);
213 self
214 }
215
216 #[must_use]
217 pub fn version(mut self, value: impl Into<String>) -> Self {
218 self.version = Some(value.into());
219 self
220 }
221
222 #[must_use]
223 pub fn compatibility(mut self, value: ProductCompatibilityWindow) -> Self {
224 self.compatibility = Some(value);
225 self
226 }
227
228 pub fn build(self) -> Result<ProductMountDescriptor, String> {
229 let descriptor = ProductMountDescriptor {
230 namespace: self.namespace,
231 display_name: self
232 .display_name
233 .ok_or_else(|| "product mount display_name is required".to_string())?,
234 aliases: self.aliases,
235 entrypoint: self
236 .entrypoint
237 .ok_or_else(|| "product mount entrypoint is required".to_string())?,
238 control_entrypoint: self
239 .control_entrypoint
240 .ok_or_else(|| "product mount control_entrypoint is required".to_string())?,
241 help: ProductHelpMetadata {
242 summary: self
243 .help_summary
244 .ok_or_else(|| "product mount help summary is required".to_string())?,
245 },
246 capabilities: self.capabilities,
247 version: self.version,
248 compatibility: self.compatibility,
249 };
250 validate_product_mount_descriptor(&descriptor)?;
251 Ok(descriptor)
252 }
253}
254
255pub fn validate_product_mount_descriptor(
256 descriptor: &ProductMountDescriptor,
257) -> Result<(), String> {
258 if descriptor.display_name.trim().is_empty() {
259 return Err("product mount display_name cannot be empty".to_string());
260 }
261 if descriptor.help.summary.trim().is_empty() {
262 return Err("product mount help summary cannot be empty".to_string());
263 }
264 validate_entrypoint("entrypoint", &descriptor.entrypoint)?;
265 validate_entrypoint("control entrypoint", &descriptor.control_entrypoint)?;
266
267 let mut alias_set = std::collections::BTreeSet::new();
268 for alias in &descriptor.aliases {
269 if alias.as_str() == descriptor.namespace.as_str() {
270 return Err(format!(
271 "product mount `{}` cannot repeat itself as an alias",
272 descriptor.namespace.as_str()
273 ));
274 }
275 if RESERVED_ROOT_NAMESPACES.contains(&alias.as_str()) {
276 return Err(format!(
277 "product mount alias `{}` collides with reserved runtime root",
278 alias.as_str()
279 ));
280 }
281 if !alias_set.insert(alias.as_str().to_string()) {
282 return Err(format!(
283 "product mount `{}` declares duplicate alias `{}`",
284 descriptor.namespace.as_str(),
285 alias.as_str()
286 ));
287 }
288 }
289
290 let mut capability_set = std::collections::BTreeSet::new();
291 for capability in &descriptor.capabilities {
292 let normalized = capability.trim().to_ascii_lowercase().replace(' ', "_");
293 if normalized.is_empty() {
294 return Err(format!(
295 "product mount `{}` has an empty capability entry",
296 descriptor.namespace.as_str()
297 ));
298 }
299 if !capability_set.insert(normalized.clone()) {
300 return Err(format!(
301 "product mount `{}` declares duplicate capability `{}`",
302 descriptor.namespace.as_str(),
303 normalized
304 ));
305 }
306 }
307
308 if let Some(compatibility) = &descriptor.compatibility {
309 validate_compatibility_window(compatibility)?;
310 }
311
312 Ok(())
313}
314
315fn validate_entrypoint(label: &str, entrypoint: &ProductEntrypoint) -> Result<(), String> {
316 match entrypoint.kind {
317 ProductEntrypointKind::PythonModule => {
318 let module = entrypoint.module.as_deref().unwrap_or(entrypoint.command.as_str()).trim();
319 if module.is_empty() {
320 return Err(format!("product mount {label} python module cannot be empty"));
321 }
322 if let Some(function) = &entrypoint.function {
323 if function.trim().is_empty() {
324 return Err(format!("product mount {label} python function cannot be empty"));
325 }
326 }
327 }
328 _ => {
329 if entrypoint.command.trim().is_empty() {
330 return Err(format!("product mount {label} command cannot be empty"));
331 }
332 if entrypoint.module.as_deref().is_some_and(|value| !value.trim().is_empty()) {
333 return Err(format!(
334 "product mount {label} only supports `module` for python_module entrypoints"
335 ));
336 }
337 if entrypoint.function.as_deref().is_some_and(|value| !value.trim().is_empty()) {
338 return Err(format!(
339 "product mount {label} only supports `function` for python_module entrypoints"
340 ));
341 }
342 }
343 }
344
345 Ok(())
346}
347
348fn validate_compatibility_window(window: &ProductCompatibilityWindow) -> Result<(), String> {
349 let min = Version::parse(&window.min_cli_version)
350 .map_err(|error| format!("min_cli_version is not valid semver: {error}"))?;
351 if let Some(max) = &window.max_cli_version_exclusive {
352 let max_version = Version::parse(max)
353 .map_err(|error| format!("max_cli_version_exclusive is not valid semver: {error}"))?;
354 if max_version <= min {
355 return Err(
356 "max_cli_version_exclusive must be greater than min_cli_version".to_string()
357 );
358 }
359 }
360 Ok(())
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub struct KnownBijuxTool {
366 pub namespace: &'static str,
368 pub runtime_binary_name: &'static str,
370 pub control_binary_name: &'static str,
372 pub runtime_package_name: &'static str,
374 pub control_package_name: &'static str,
376 pub repository_name: &'static str,
378 pub status: &'static str,
380 pub display_name: &'static str,
382 pub aliases: &'static [&'static str],
384 pub language: &'static str,
386 pub version: Option<&'static str>,
388 pub help_summary: &'static str,
390 pub capabilities: &'static [&'static str],
392}
393
394impl KnownBijuxTool {
395 #[must_use]
397 pub fn runtime_binary(&self) -> String {
398 self.runtime_binary_name.to_string()
399 }
400
401 #[must_use]
403 pub fn control_binary(&self) -> String {
404 self.control_binary_name.to_string()
405 }
406
407 #[must_use]
409 pub fn runtime_package(&self) -> String {
410 self.runtime_package_name.to_string()
411 }
412
413 #[must_use]
415 pub fn control_package(&self) -> String {
416 self.control_package_name.to_string()
417 }
418
419 #[must_use]
421 pub fn repository(&self) -> String {
422 self.repository_name.to_string()
423 }
424
425 #[must_use]
427 pub fn descriptor(&self) -> ProductMountDescriptor {
428 ProductMountDescriptor {
429 namespace: Namespace(self.namespace.to_string()),
430 display_name: self.display_name.to_string(),
431 aliases: self.aliases.iter().map(|alias| Namespace((*alias).to_string())).collect(),
432 entrypoint: ProductEntrypoint {
433 kind: ProductEntrypointKind::Binary,
434 command: self.runtime_binary(),
435 module: None,
436 function: None,
437 },
438 control_entrypoint: ProductEntrypoint {
439 kind: ProductEntrypointKind::Binary,
440 command: self.control_binary(),
441 module: None,
442 function: None,
443 },
444 help: ProductHelpMetadata { summary: self.help_summary.to_string() },
445 capabilities: self.capabilities.iter().map(|value| (*value).to_string()).collect(),
446 version: self.version.map(ToOwned::to_owned),
447 compatibility: None,
448 }
449 }
450}
451
452fn leak(raw: String) -> &'static str {
453 Box::leak(raw.into_boxed_str())
454}
455
456fn leak_vec(raw: Vec<String>) -> &'static [&'static str] {
457 Box::leak(raw.into_iter().map(leak).collect::<Vec<&'static str>>().into_boxed_slice())
458}
459
460fn validate_registry_document(document: &ProductRegistryDocument) -> Result<(), String> {
461 if document.schema_version.trim() != "v1" {
462 return Err(format!(
463 "official product registry schema_version must be `v1`, got `{}`",
464 document.schema_version
465 ));
466 }
467 if document.owner.trim().is_empty() {
468 return Err("official product registry owner cannot be empty".to_string());
469 }
470 if document.policy.trim().is_empty() {
471 return Err("official product registry policy cannot be empty".to_string());
472 }
473
474 let mut seen_namespaces = std::collections::BTreeSet::new();
475 let mut seen_aliases = std::collections::BTreeSet::new();
476
477 for entry in &document.entries {
478 let namespace = Namespace::new(&entry.namespace).map_err(|error| {
479 format!("invalid official namespace `{}`: {error}", entry.namespace)
480 })?;
481 if RESERVED_ROOT_NAMESPACES.contains(&namespace.as_str()) {
482 return Err(format!(
483 "official namespace `{}` collides with reserved runtime root",
484 namespace.as_str()
485 ));
486 }
487 if !seen_namespaces.insert(namespace.as_str().to_string()) {
488 return Err(format!("duplicate official namespace `{}`", namespace.as_str()));
489 }
490 if entry.display_name.trim().is_empty() {
491 return Err(format!(
492 "official namespace `{}` is missing display_name",
493 namespace.as_str()
494 ));
495 }
496 if entry.runtime_binary.trim().is_empty()
497 || entry.control_binary.trim().is_empty()
498 || entry.runtime_package.trim().is_empty()
499 || entry.control_package.trim().is_empty()
500 || entry.repository.trim().is_empty()
501 || entry.language.trim().is_empty()
502 || entry.help_summary.trim().is_empty()
503 {
504 return Err(format!(
505 "official namespace `{}` has one or more empty required fields",
506 namespace.as_str()
507 ));
508 }
509 validate_registry_status(namespace.as_str(), &entry.status)?;
510
511 let mut capability_set = std::collections::BTreeSet::new();
512 for capability in &entry.capabilities {
513 let normalized = capability.trim().to_ascii_lowercase().replace(' ', "_");
514 if normalized.is_empty() {
515 return Err(format!(
516 "official namespace `{}` has an empty capability entry",
517 namespace.as_str()
518 ));
519 }
520 if !capability_set.insert(normalized.clone()) {
521 return Err(format!(
522 "official namespace `{}` declares duplicate capability `{}`",
523 namespace.as_str(),
524 normalized
525 ));
526 }
527 }
528
529 let mut local_aliases = std::collections::BTreeSet::new();
530 for alias in &entry.aliases {
531 let normalized = Namespace::new(alias)
532 .map_err(|error| format!("invalid alias `{alias}`: {error}"))?;
533 if normalized.as_str() == namespace.as_str() {
534 return Err(format!(
535 "official namespace `{}` cannot repeat itself as an alias",
536 namespace.as_str()
537 ));
538 }
539 if RESERVED_ROOT_NAMESPACES.contains(&normalized.as_str()) {
540 return Err(format!(
541 "official alias `{}` for `{}` collides with reserved runtime root",
542 normalized.as_str(),
543 namespace.as_str()
544 ));
545 }
546 if seen_namespaces.contains(normalized.as_str()) {
547 return Err(format!(
548 "official alias `{}` for `{}` collides with another namespace",
549 normalized.as_str(),
550 namespace.as_str()
551 ));
552 }
553 if !local_aliases.insert(normalized.as_str().to_string()) {
554 return Err(format!(
555 "official namespace `{}` declares duplicate alias `{}`",
556 namespace.as_str(),
557 normalized.as_str()
558 ));
559 }
560 if !seen_aliases.insert(normalized.as_str().to_string()) {
561 return Err(format!(
562 "official alias `{}` is declared by multiple products",
563 normalized.as_str()
564 ));
565 }
566 }
567 }
568
569 Ok(())
570}
571
572fn validate_registry_status(namespace: &str, status: &str) -> Result<(), String> {
573 if status.trim().is_empty() {
574 return Err(format!("official namespace `{namespace}` has empty status"));
575 }
576 let normalized = status.trim().to_ascii_lowercase();
577 if !OFFICIAL_STATUS_VALUES.contains(&normalized.as_str()) {
578 return Err(format!(
579 "official namespace `{namespace}` declares unsupported status `{status}`"
580 ));
581 }
582 Ok(())
583}
584
585fn load_known_bijux_tools() -> Vec<KnownBijuxTool> {
586 let raw = include_str!(concat!(
587 env!("CARGO_MANIFEST_DIR"),
588 "/contracts/official_product_namespace_registry.json"
589 ));
590 let document: ProductRegistryDocument =
591 serde_json::from_str(raw).expect("official product registry must stay valid JSON");
592 validate_registry_document(&document)
593 .expect("official product registry must satisfy namespace, alias, and field contracts");
594
595 document
596 .entries
597 .into_iter()
598 .map(|entry| KnownBijuxTool {
599 namespace: leak(entry.namespace),
600 runtime_binary_name: leak(entry.runtime_binary),
601 control_binary_name: leak(entry.control_binary),
602 runtime_package_name: leak(entry.runtime_package),
603 control_package_name: leak(entry.control_package),
604 repository_name: leak(entry.repository),
605 status: leak(entry.status),
606 display_name: leak(entry.display_name),
607 aliases: leak_vec(entry.aliases),
608 language: leak(entry.language),
609 version: entry.version.map(leak),
610 help_summary: leak(entry.help_summary),
611 capabilities: leak_vec(entry.capabilities),
612 })
613 .collect()
614}
615
616fn known_bijux_tools_storage() -> &'static Vec<KnownBijuxTool> {
617 static STORAGE: OnceLock<Vec<KnownBijuxTool>> = OnceLock::new();
618 STORAGE.get_or_init(load_known_bijux_tools)
619}
620
621#[must_use]
623pub fn known_bijux_tools() -> &'static [KnownBijuxTool] {
624 known_bijux_tools_storage().as_slice()
625}
626
627fn load_known_bijux_tool_namespaces() -> Vec<&'static str> {
628 known_bijux_tools().iter().map(|tool| tool.namespace).collect()
629}
630
631#[must_use]
633pub fn known_bijux_tool_namespaces() -> &'static [&'static str] {
634 static STORAGE: OnceLock<Vec<&'static str>> = OnceLock::new();
635 STORAGE.get_or_init(load_known_bijux_tool_namespaces).as_slice()
636}
637
638#[must_use]
640pub fn official_product_namespaces() -> &'static [&'static str] {
641 known_bijux_tool_namespaces()
642}
643
644#[must_use]
646pub fn known_bijux_tool(namespace: &str) -> Option<&'static KnownBijuxTool> {
647 known_bijux_tools().iter().find(|tool| tool.namespace == namespace)
648}
649
650#[must_use]
652pub fn known_bijux_tool_by_query(query: &str) -> Option<&'static KnownBijuxTool> {
653 let normalized = Namespace::normalize(query);
654 known_bijux_tools().iter().find(|tool| {
655 tool.namespace == normalized
656 || tool.aliases.iter().any(|alias| *alias == normalized.as_str())
657 })
658}
659
660#[must_use]
662pub fn canonical_bijux_tool_namespace(query: &str) -> Option<&'static str> {
663 known_bijux_tool_by_query(query).map(|tool| tool.namespace)
664}
665
666#[must_use]
668pub fn official_status_allows_runtime_dispatch(status: &str) -> bool {
669 !status.trim().eq_ignore_ascii_case("unsupported")
670}
671
672pub type ProductMountMetadata = ProductMountDescriptor;
674
675#[cfg(test)]
676mod tests {
677 use super::{
678 known_bijux_tool_by_query, official_product_namespaces,
679 official_status_allows_runtime_dispatch, validate_product_mount_descriptor,
680 validate_registry_document, Namespace, ProductEntrypointKind, ProductMountDescriptor,
681 ProductRegistryDocument, ProductRegistryEntry,
682 };
683
684 #[test]
685 fn descriptor_builder_builds_valid_mount() {
686 let descriptor = ProductMountDescriptor::builder(Namespace::new("workflow").expect("ns"))
687 .display_name("Workflow")
688 .entrypoint(ProductEntrypointKind::PythonModule, "workflow_app")
689 .control_entrypoint(ProductEntrypointKind::PythonModule, "workflow_app")
690 .help_summary("Workflow runtime")
691 .capability("json_output")
692 .build()
693 .expect("descriptor");
694
695 assert_eq!(descriptor.namespace.as_str(), "workflow");
696 assert_eq!(descriptor.entrypoint.command, "workflow_app");
697 }
698
699 #[test]
700 fn descriptor_validation_rejects_duplicate_aliases() {
701 let descriptor = ProductMountDescriptor {
702 namespace: Namespace::new("workflow").expect("ns"),
703 display_name: "Workflow".to_string(),
704 aliases: vec![
705 Namespace::new("wf").expect("alias"),
706 Namespace::new("wf").expect("alias"),
707 ],
708 entrypoint: super::ProductEntrypoint {
709 kind: ProductEntrypointKind::Binary,
710 command: "workflow".to_string(),
711 module: None,
712 function: None,
713 },
714 control_entrypoint: super::ProductEntrypoint {
715 kind: ProductEntrypointKind::Binary,
716 command: "workflow".to_string(),
717 module: None,
718 function: None,
719 },
720 help: super::ProductHelpMetadata { summary: "Workflow runtime".to_string() },
721 capabilities: vec!["json_output".to_string()],
722 version: None,
723 compatibility: None,
724 };
725
726 let error = validate_product_mount_descriptor(&descriptor).expect_err("must reject");
727 assert!(error.contains("duplicate alias"));
728 }
729
730 fn sample_registry_entry(namespace: &str, status: &str) -> ProductRegistryEntry {
731 ProductRegistryEntry {
732 namespace: namespace.to_string(),
733 display_name: format!("{namespace} display"),
734 aliases: Vec::new(),
735 runtime_binary: format!("bijux-{namespace}"),
736 control_binary: format!("bijux-dev-{namespace}"),
737 runtime_package: format!("bijux-{namespace}"),
738 control_package: format!("bijux-dev-{namespace}"),
739 repository: format!("bijux-{namespace}"),
740 status: status.to_string(),
741 language: "rust".to_string(),
742 version: None,
743 help_summary: format!("{namespace} summary"),
744 capabilities: vec!["json_output".to_string()],
745 }
746 }
747
748 fn sample_registry(entries: Vec<ProductRegistryEntry>) -> ProductRegistryDocument {
749 ProductRegistryDocument {
750 schema_version: "v1".to_string(),
751 owner: "bijux-cli".to_string(),
752 policy: "policy".to_string(),
753 entries,
754 }
755 }
756
757 #[test]
758 fn registry_validation_rejects_duplicate_namespaces() {
759 let document = sample_registry(vec![
760 sample_registry_entry("dag", "declared"),
761 sample_registry_entry("dag", "declared"),
762 ]);
763 let error = validate_registry_document(&document).expect_err("duplicate namespace");
764 assert!(error.contains("duplicate official namespace"));
765 }
766
767 #[test]
768 fn registry_validation_rejects_unsupported_status_values() {
769 let document = sample_registry(vec![sample_registry_entry("dag", "preview")]);
770 let error = validate_registry_document(&document).expect_err("unsupported status");
771 assert!(error.contains("unsupported status"));
772 }
773
774 #[test]
775 fn runtime_dispatch_policy_blocks_unsupported_statuses() {
776 assert!(official_status_allows_runtime_dispatch("declared"));
777 assert!(official_status_allows_runtime_dispatch("supported"));
778 assert!(official_status_allows_runtime_dispatch("deprecated"));
779 assert!(!official_status_allows_runtime_dispatch("unsupported"));
780 }
781
782 #[test]
783 fn official_namespace_registry_exposes_known_namespaces() {
784 assert!(official_product_namespaces().contains(&"dag"));
785 let dag = known_bijux_tool_by_query("dag").expect("dag tool metadata");
786 assert_eq!(dag.namespace, "dag");
787 }
788}