1use crate::{Capabilities, Error, Result};
4use opcda_bridge_proto::bridge as proto;
5use serde::Serialize;
6use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
10#[serde(rename_all = "snake_case")]
11pub enum CompatibilityFeature {
12 Core,
13 Namespace,
14 IndexedSearch,
15}
16
17impl CompatibilityFeature {
18 fn from_proto(value: i32) -> Result<Self> {
19 match proto::ProtocolFeatureKind::try_from(value).map_err(|_| {
20 Error::Protocol(format!("gateway returned unknown protocol feature {value}"))
21 })? {
22 proto::ProtocolFeatureKind::Core => Ok(Self::Core),
23 proto::ProtocolFeatureKind::Namespace => Ok(Self::Namespace),
24 proto::ProtocolFeatureKind::IndexedSearch => Ok(Self::IndexedSearch),
25 proto::ProtocolFeatureKind::Unspecified => Err(Error::Protocol(
26 "gateway returned an unspecified protocol feature".into(),
27 )),
28 }
29 }
30}
31
32impl fmt::Display for CompatibilityFeature {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.write_str(match self {
35 Self::Core => "core",
36 Self::Namespace => "namespace",
37 Self::IndexedSearch => "indexed-search",
38 })
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
44pub struct ProtocolVersionRange {
45 pub min: u32,
46 pub max: u32,
47}
48
49impl ProtocolVersionRange {
50 pub const fn new(min: u32, max: u32) -> Option<Self> {
52 if min > max {
53 None
54 } else {
55 Some(Self { min, max })
56 }
57 }
58
59 pub const fn exact(version: u32) -> Self {
61 Self {
62 min: version,
63 max: version,
64 }
65 }
66
67 pub const fn overlaps(self, other: Self) -> bool {
69 self.min <= other.max && other.min <= self.max
70 }
71
72 const fn negotiated_version(self, other: Self) -> Option<u32> {
73 if self.overlaps(other) {
74 Some(if self.min > other.min {
75 self.min
76 } else {
77 other.min
78 })
79 } else {
80 None
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
87pub struct ProtocolFeatureSupport {
88 pub feature: CompatibilityFeature,
89 pub versions: ProtocolVersionRange,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
94pub struct GatewayInfo {
95 pub application_version: String,
96 pub compatibility_schema_version: u32,
97 pub features: Vec<ProtocolFeatureSupport>,
98}
99
100impl TryFrom<proto::GetGatewayInfoResponse> for GatewayInfo {
101 type Error = Error;
102
103 fn try_from(value: proto::GetGatewayInfoResponse) -> Result<Self> {
104 let features = value
105 .features
106 .into_iter()
107 .map(|feature| {
108 let feature_kind = CompatibilityFeature::from_proto(feature.kind)?;
109 let versions = ProtocolVersionRange::new(feature.min_version, feature.max_version)
110 .ok_or_else(|| {
111 Error::Protocol(format!(
112 "gateway returned reversed {feature_kind} protocol version range"
113 ))
114 })?;
115 Ok(ProtocolFeatureSupport {
116 feature: feature_kind,
117 versions,
118 })
119 })
120 .collect::<Result<Vec<_>>>()?;
121 Ok(Self {
122 application_version: value.application_version,
123 compatibility_schema_version: value.compatibility_schema_version,
124 features,
125 })
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
131#[serde(rename_all = "snake_case")]
132pub enum CompatibilitySource {
133 GatewayInfo,
134 LegacyCapabilities,
135 Unknown,
136}
137
138impl fmt::Display for CompatibilitySource {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 f.write_str(match self {
141 Self::GatewayInfo => "gateway-info",
142 Self::LegacyCapabilities => "legacy-capabilities",
143 Self::Unknown => "unknown",
144 })
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
150pub struct ProtocolProfile {
151 pub application_version: Option<String>,
152 pub source: CompatibilitySource,
153 pub features: Vec<ProtocolFeatureSupport>,
154}
155
156impl ProtocolProfile {
157 pub fn from_gateway_info(info: &GatewayInfo) -> Self {
159 Self {
160 application_version: Some(info.application_version.clone()),
161 source: CompatibilitySource::GatewayInfo,
162 features: info.features.clone(),
163 }
164 }
165
166 fn feature(&self, feature: CompatibilityFeature) -> Option<ProtocolVersionRange> {
167 self.features
168 .iter()
169 .find(|support| support.feature == feature)
170 .map(|support| support.versions)
171 }
172}
173
174pub fn current_client_profile(application_version: impl Into<String>) -> ProtocolProfile {
176 let application_version = application_version.into();
177 let release_line =
178 opcda_bridge_proto::compatibility::release_line_for(env!("CARGO_PKG_VERSION"))
179 .expect("reusable client package version must be in the compatibility catalog");
180 ProtocolProfile {
181 application_version: Some(application_version),
182 source: CompatibilitySource::GatewayInfo,
183 features: vec![
184 ProtocolFeatureSupport {
185 feature: CompatibilityFeature::Core,
186 versions: ProtocolVersionRange::exact(release_line.core_protocol),
187 },
188 ProtocolFeatureSupport {
189 feature: CompatibilityFeature::Namespace,
190 versions: ProtocolVersionRange::exact(release_line.namespace_protocol),
191 },
192 ProtocolFeatureSupport {
193 feature: CompatibilityFeature::IndexedSearch,
194 versions: ProtocolVersionRange::exact(release_line.indexed_search_protocol),
195 },
196 ],
197 }
198}
199
200pub fn legacy_gateway_profile(capabilities: &Capabilities) -> ProtocolProfile {
202 let mut features = Vec::new();
203 let release_line =
204 opcda_bridge_proto::compatibility::release_line_for(&capabilities.application_version);
205 if let Some(namespace_version) = parse_namespace_protocol(&capabilities.protocol_version) {
206 features.push(ProtocolFeatureSupport {
207 feature: CompatibilityFeature::Core,
208 versions: ProtocolVersionRange::exact(release_line.map_or(
209 opcda_bridge_proto::compatibility::CORE_PROTOCOL_VERSION,
210 |line| line.core_protocol,
211 )),
212 });
213 features.push(ProtocolFeatureSupport {
214 feature: CompatibilityFeature::Namespace,
215 versions: ProtocolVersionRange::exact(namespace_version),
216 });
217 }
218 if capabilities.supports_indexed_search
219 && let Some(index_protocol) =
220 parse_index_protocol(&capabilities.indexed_search_protocol_version)
221 {
222 features.push(ProtocolFeatureSupport {
223 feature: CompatibilityFeature::IndexedSearch,
224 versions: ProtocolVersionRange::exact(index_protocol),
225 });
226 }
227 ProtocolProfile {
228 application_version: Some(capabilities.application_version.clone()),
229 source: CompatibilitySource::LegacyCapabilities,
230 features,
231 }
232}
233
234fn catalog_line(version: &str) -> Option<&'static str> {
235 opcda_bridge_proto::compatibility::release_line_for(version).map(|line| line.name)
236}
237
238fn evidence_status(value: &str) -> Option<CompatibilityEvidence> {
239 match value {
240 "contract-boundary-tested" => Some(CompatibilityEvidence::ContractBoundaryTested),
241 "exact-pair-tested" => Some(CompatibilityEvidence::ExactPairTested),
242 "unverified" => Some(CompatibilityEvidence::Unverified),
243 _ => None,
244 }
245}
246
247fn catalog_evidence(
248 client_version: Option<&str>,
249 gateway_version: Option<&str>,
250) -> CompatibilityEvidence {
251 let (Some(client_version), Some(gateway_version)) = (client_version, gateway_version) else {
252 return CompatibilityEvidence::Unverified;
253 };
254 let (Some(client_line), Some(gateway_line)) =
255 (catalog_line(client_version), catalog_line(gateway_version))
256 else {
257 return CompatibilityEvidence::Unverified;
258 };
259
260 for &(catalog_client_line, catalog_gateway_line, status, exact_client, exact_gateway) in
261 opcda_bridge_proto::compatibility::EVIDENCE
262 {
263 if catalog_client_line == client_line
264 && catalog_gateway_line == gateway_line
265 && !exact_client.is_empty()
266 && exact_client == client_version
267 && exact_gateway == gateway_version
268 && let Some(status) = evidence_status(status)
269 {
270 return status;
271 }
272 }
273 for &(catalog_client_line, catalog_gateway_line, status, exact_client, exact_gateway) in
274 opcda_bridge_proto::compatibility::EVIDENCE
275 {
276 if catalog_client_line == client_line
277 && catalog_gateway_line == gateway_line
278 && exact_client.is_empty()
279 && exact_gateway.is_empty()
280 && let Some(status) = evidence_status(status)
281 {
282 return status;
283 }
284 }
285 CompatibilityEvidence::Unverified
286}
287
288fn parse_namespace_protocol(value: &str) -> Option<u32> {
289 match value.trim() {
290 "1" | "1.0" => Some(1),
291 "2" | "2.0" => Some(2),
292 "0.3" | "0.3.0" => Some(2),
293 _ => None,
294 }
295}
296
297fn parse_index_protocol(value: &str) -> Option<u32> {
298 match value.trim() {
299 "1" | "1.0" => Some(1),
300 _ => None,
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
306pub struct FeatureCompatibility {
307 pub feature: CompatibilityFeature,
308 pub status: FeatureCompatibilityStatus,
309 pub client_versions: ProtocolVersionRange,
310 pub gateway_versions: Option<ProtocolVersionRange>,
311 pub negotiated_version: Option<u32>,
312 pub reason: String,
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
317#[serde(rename_all = "snake_case")]
318pub enum FeatureCompatibilityStatus {
319 Compatible,
320 Unsupported,
321 Incompatible,
322 Unknown,
323}
324
325impl fmt::Display for FeatureCompatibilityStatus {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 f.write_str(match self {
328 Self::Compatible => "compatible",
329 Self::Unsupported => "unsupported",
330 Self::Incompatible => "incompatible",
331 Self::Unknown => "unknown",
332 })
333 }
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
338#[serde(rename_all = "snake_case")]
339pub enum CompatibilityStatus {
340 Full,
341 Partial,
342 Incompatible,
343 Unknown,
344}
345
346impl fmt::Display for CompatibilityStatus {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 f.write_str(match self {
349 Self::Full => "full",
350 Self::Partial => "partial",
351 Self::Incompatible => "incompatible",
352 Self::Unknown => "unknown",
353 })
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
359#[serde(rename_all = "snake_case")]
360pub enum CompatibilityEvidence {
361 ContractBoundaryTested,
362 ExactPairTested,
363 Unverified,
364}
365
366impl fmt::Display for CompatibilityEvidence {
367 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368 f.write_str(match self {
369 Self::ContractBoundaryTested => "contract-boundary-tested",
370 Self::ExactPairTested => "exact-pair-tested",
371 Self::Unverified => "unverified",
372 })
373 }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
378pub struct CompatibilityReport {
379 pub client_version: String,
380 pub library_version: String,
381 pub gateway_version: Option<String>,
382 pub source: CompatibilitySource,
383 pub status: CompatibilityStatus,
384 pub evidence: CompatibilityEvidence,
385 pub features: Vec<FeatureCompatibility>,
386}
387
388impl CompatibilityReport {
389 pub fn feature(&self, feature: CompatibilityFeature) -> Option<&FeatureCompatibility> {
391 self.features
392 .iter()
393 .find(|result| result.feature == feature)
394 }
395
396 pub fn satisfies(&self, required: &[CompatibilityFeature]) -> bool {
398 required.iter().all(|feature| {
399 self.feature(*feature)
400 .is_some_and(|result| result.status == FeatureCompatibilityStatus::Compatible)
401 })
402 }
403}
404
405pub fn evaluate_compatibility(
407 client: &ProtocolProfile,
408 gateway: &ProtocolProfile,
409) -> CompatibilityReport {
410 let mut features = Vec::new();
411 for client_support in &client.features {
412 let gateway_versions = gateway.feature(client_support.feature);
413 let (status, negotiated_version, reason) = match gateway_versions {
414 Some(gateway_versions) if client_support.versions.overlaps(gateway_versions) => (
415 FeatureCompatibilityStatus::Compatible,
416 client_support.versions.negotiated_version(gateway_versions),
417 format!(
418 "{feature} protocol ranges overlap",
419 feature = client_support.feature
420 ),
421 ),
422 Some(gateway_versions) => (
423 FeatureCompatibilityStatus::Incompatible,
424 None,
425 format!(
426 "{feature} protocol ranges do not overlap: client {}-{}, gateway {}-{}",
427 client_support.versions.min,
428 client_support.versions.max,
429 gateway_versions.min,
430 gateway_versions.max,
431 feature = client_support.feature
432 ),
433 ),
434 None if client_support.feature == CompatibilityFeature::IndexedSearch => (
435 FeatureCompatibilityStatus::Unsupported,
436 None,
437 "gateway does not advertise indexed search".into(),
438 ),
439 None => (
440 FeatureCompatibilityStatus::Unknown,
441 None,
442 format!(
443 "gateway did not advertise the {feature} protocol",
444 feature = client_support.feature
445 ),
446 ),
447 };
448 features.push(FeatureCompatibility {
449 feature: client_support.feature,
450 status,
451 client_versions: client_support.versions,
452 gateway_versions,
453 negotiated_version,
454 reason,
455 });
456 }
457
458 let core_status = features
459 .iter()
460 .find(|result| result.feature == CompatibilityFeature::Core)
461 .map(|result| result.status);
462 let status = match core_status {
463 Some(FeatureCompatibilityStatus::Compatible) => {
464 if features
465 .iter()
466 .all(|result| result.status == FeatureCompatibilityStatus::Compatible)
467 {
468 CompatibilityStatus::Full
469 } else {
470 CompatibilityStatus::Partial
471 }
472 }
473 Some(FeatureCompatibilityStatus::Incompatible)
474 | Some(FeatureCompatibilityStatus::Unsupported) => CompatibilityStatus::Incompatible,
475 Some(FeatureCompatibilityStatus::Unknown) | None => CompatibilityStatus::Unknown,
476 };
477
478 CompatibilityReport {
479 client_version: client
480 .application_version
481 .clone()
482 .unwrap_or_else(|| "unknown".into()),
483 library_version: env!("CARGO_PKG_VERSION").into(),
484 gateway_version: gateway.application_version.clone(),
485 source: gateway.source,
486 status,
487 evidence: catalog_evidence(
488 client.application_version.as_deref(),
489 gateway.application_version.as_deref(),
490 ),
491 features,
492 }
493}
494
495pub fn unknown_compatibility_report(client_version: impl Into<String>) -> CompatibilityReport {
497 CompatibilityReport {
498 client_version: client_version.into(),
499 library_version: env!("CARGO_PKG_VERSION").into(),
500 gateway_version: None,
501 source: CompatibilitySource::Unknown,
502 status: CompatibilityStatus::Unknown,
503 evidence: CompatibilityEvidence::Unverified,
504 features: Vec::new(),
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 fn capabilities(protocol: &str, indexed: bool) -> Capabilities {
513 Capabilities {
514 application_version: "0.4.3".into(),
515 protocol_version: protocol.into(),
516 max_page_size: 200,
517 supports_browse_sessions: true,
518 supports_search: true,
519 organization: crate::NamespaceOrganization::Hierarchical,
520 source: crate::BrowseSource::Da2,
521 supports_indexed_search: indexed,
522 indexed_search_protocol_version: if indexed { "1" } else { "" }.into(),
523 max_indexed_search_results: 50,
524 search_index_state: crate::SearchIndexState::Ready,
525 }
526 }
527
528 #[test]
529 fn ranges_validate_and_negotiate() {
530 assert!(ProtocolVersionRange::new(2, 1).is_none());
531 let left = ProtocolVersionRange::new(1, 3).unwrap();
532 let right = ProtocolVersionRange::new(3, 4).unwrap();
533 assert!(left.overlaps(right));
534 assert_eq!(left.negotiated_version(right), Some(3));
535 assert!(!ProtocolVersionRange::exact(1).overlaps(ProtocolVersionRange::exact(2)));
536 }
537
538 #[test]
539 fn feature_display_and_status_display_are_stable() {
540 assert_eq!(
541 CompatibilityFeature::IndexedSearch.to_string(),
542 "indexed-search"
543 );
544 assert_eq!(
545 FeatureCompatibilityStatus::Unsupported.to_string(),
546 "unsupported"
547 );
548 assert_eq!(CompatibilityStatus::Partial.to_string(), "partial");
549 assert_eq!(
550 CompatibilityEvidence::ContractBoundaryTested.to_string(),
551 "contract-boundary-tested"
552 );
553 }
554
555 #[test]
556 fn current_profile_uses_generated_contract_versions() {
557 let profile = current_client_profile("0.4.3");
558 assert_eq!(
559 profile.feature(CompatibilityFeature::Core),
560 Some(ProtocolVersionRange::exact(1))
561 );
562 assert_eq!(
563 profile.feature(CompatibilityFeature::Namespace),
564 Some(ProtocolVersionRange::exact(2))
565 );
566 assert_eq!(
567 profile.feature(CompatibilityFeature::IndexedSearch),
568 Some(ProtocolVersionRange::exact(1))
569 );
570 assert_eq!(profile.application_version.as_deref(), Some("0.4.3"));
571
572 let relabeled = current_client_profile("application-build");
573 assert_eq!(
574 relabeled.feature(CompatibilityFeature::IndexedSearch),
575 Some(ProtocolVersionRange::exact(1))
576 );
577 }
578
579 #[test]
580 fn legacy_profiles_map_supported_and_unknown_protocol_strings() {
581 let profile = legacy_gateway_profile(&capabilities("2", true));
582 assert_eq!(profile.source, CompatibilitySource::LegacyCapabilities);
583 assert_eq!(
584 profile.feature(CompatibilityFeature::Namespace),
585 Some(ProtocolVersionRange::exact(2))
586 );
587 assert!(
588 profile
589 .feature(CompatibilityFeature::IndexedSearch)
590 .is_some()
591 );
592
593 let old = legacy_gateway_profile(&capabilities("1.0", false));
594 assert_eq!(
595 old.feature(CompatibilityFeature::Namespace),
596 Some(ProtocolVersionRange::exact(1))
597 );
598 assert!(old.feature(CompatibilityFeature::IndexedSearch).is_none());
599
600 let shorthand = legacy_gateway_profile(&capabilities("0.3", false));
601 assert_eq!(
602 shorthand.feature(CompatibilityFeature::Namespace),
603 Some(ProtocolVersionRange::exact(2))
604 );
605
606 let unknown = legacy_gateway_profile(&capabilities("future", false));
607 assert!(unknown.features.is_empty());
608
609 let mut unknown_version = capabilities("2", true);
610 unknown_version.application_version = "future".into();
611 let unknown_version_profile = legacy_gateway_profile(&unknown_version);
612 assert_eq!(
613 unknown_version_profile.feature(CompatibilityFeature::Core),
614 Some(ProtocolVersionRange::exact(1))
615 );
616 assert_eq!(
617 unknown_version_profile.feature(CompatibilityFeature::IndexedSearch),
618 Some(ProtocolVersionRange::exact(1))
619 );
620 let mut invalid_index = capabilities("2", true);
621 invalid_index.indexed_search_protocol_version = "future".into();
622 assert!(
623 legacy_gateway_profile(&invalid_index)
624 .feature(CompatibilityFeature::IndexedSearch)
625 .is_none()
626 );
627 }
628
629 #[test]
630 fn evaluates_full_partial_and_incompatible_profiles() {
631 let client = current_client_profile("0.4.3");
632 let full = evaluate_compatibility(
633 &client,
634 &ProtocolProfile {
635 application_version: Some("0.4.3".into()),
636 source: CompatibilitySource::GatewayInfo,
637 features: client.features.clone(),
638 },
639 );
640 assert_eq!(full.status, CompatibilityStatus::Full);
641 assert_eq!(full.evidence, CompatibilityEvidence::ExactPairTested);
642 assert_eq!(full.library_version, env!("CARGO_PKG_VERSION"));
643 assert!(full.satisfies(&[CompatibilityFeature::Core]));
644
645 let partial = evaluate_compatibility(
646 &client,
647 &ProtocolProfile {
648 application_version: Some("0.3.2".into()),
649 source: CompatibilitySource::LegacyCapabilities,
650 features: vec![
651 ProtocolFeatureSupport {
652 feature: CompatibilityFeature::Core,
653 versions: ProtocolVersionRange::exact(1),
654 },
655 ProtocolFeatureSupport {
656 feature: CompatibilityFeature::Namespace,
657 versions: ProtocolVersionRange::exact(2),
658 },
659 ],
660 },
661 );
662 assert_eq!(partial.status, CompatibilityStatus::Partial);
663 assert_eq!(
664 partial.evidence,
665 CompatibilityEvidence::ContractBoundaryTested
666 );
667 assert!(!partial.satisfies(&[CompatibilityFeature::IndexedSearch]));
668
669 let incompatible = evaluate_compatibility(
670 &client,
671 &ProtocolProfile {
672 application_version: Some("future".into()),
673 source: CompatibilitySource::GatewayInfo,
674 features: vec![ProtocolFeatureSupport {
675 feature: CompatibilityFeature::Core,
676 versions: ProtocolVersionRange::exact(2),
677 }],
678 },
679 );
680 assert_eq!(incompatible.status, CompatibilityStatus::Incompatible);
681 assert_eq!(
682 incompatible
683 .feature(CompatibilityFeature::Core)
684 .unwrap()
685 .status,
686 FeatureCompatibilityStatus::Incompatible
687 );
688
689 let unknown = evaluate_compatibility(
690 &client,
691 &ProtocolProfile {
692 application_version: Some("0.4.3".into()),
693 source: CompatibilitySource::GatewayInfo,
694 features: Vec::new(),
695 },
696 );
697 assert_eq!(unknown.status, CompatibilityStatus::Unknown);
698 assert!(!unknown.satisfies(&[CompatibilityFeature::Core]));
699 }
700
701 #[test]
702 fn evaluates_unknown_and_converts_gateway_info() {
703 let unknown = unknown_compatibility_report("0.4.3");
704 assert_eq!(unknown.status, CompatibilityStatus::Unknown);
705 assert!(!unknown.satisfies(&[CompatibilityFeature::Core]));
706
707 let info = GatewayInfo::try_from(proto::GetGatewayInfoResponse {
708 application_version: "0.4.3".into(),
709 compatibility_schema_version: 1,
710 features: vec![proto::ProtocolFeature {
711 kind: proto::ProtocolFeatureKind::Core as i32,
712 min_version: 1,
713 max_version: 2,
714 }],
715 })
716 .unwrap();
717 assert_eq!(info.features[0].versions.max, 2);
718 }
719
720 #[test]
721 fn rejects_invalid_gateway_feature_data() {
722 let error = GatewayInfo::try_from(proto::GetGatewayInfoResponse {
723 features: vec![proto::ProtocolFeature {
724 kind: 99,
725 min_version: 1,
726 max_version: 1,
727 }],
728 ..Default::default()
729 })
730 .unwrap_err();
731 assert!(error.to_string().contains("unknown protocol feature"));
732
733 let error = GatewayInfo::try_from(proto::GetGatewayInfoResponse {
734 features: vec![proto::ProtocolFeature {
735 kind: proto::ProtocolFeatureKind::Core as i32,
736 min_version: 2,
737 max_version: 1,
738 }],
739 ..Default::default()
740 })
741 .unwrap_err();
742 assert!(error.to_string().contains("reversed core"));
743
744 let error = GatewayInfo::try_from(proto::GetGatewayInfoResponse {
745 features: vec![proto::ProtocolFeature {
746 kind: proto::ProtocolFeatureKind::Unspecified as i32,
747 min_version: 1,
748 max_version: 1,
749 }],
750 ..Default::default()
751 })
752 .unwrap_err();
753 assert!(error.to_string().contains("unspecified protocol feature"));
754 }
755
756 #[test]
757 fn catalog_evidence_handles_exact_boundary_and_unknown_versions() {
758 assert_eq!(catalog_line("0.3.1"), Some("legacy"));
759 assert_eq!(catalog_line("0.3.2"), Some("paged"));
760 assert_eq!(catalog_line("0.4.0"), Some("indexed"));
761 assert_eq!(catalog_line("1.0.0"), None);
762 assert_eq!(
763 catalog_evidence(Some("0.4.0"), Some("0.4.3")),
764 CompatibilityEvidence::ExactPairTested
765 );
766 assert_eq!(
767 catalog_evidence(Some("0.4.3"), Some("0.3.2")),
768 CompatibilityEvidence::ContractBoundaryTested
769 );
770 assert_eq!(
771 catalog_evidence(Some("0.3.2"), Some("0.4.3")),
772 CompatibilityEvidence::ContractBoundaryTested
773 );
774 assert_eq!(
775 catalog_evidence(Some("0.4.3"), Some("0.4.2")),
776 CompatibilityEvidence::Unverified
777 );
778 assert_eq!(
779 catalog_evidence(Some("future"), Some("0.4.3")),
780 CompatibilityEvidence::Unverified
781 );
782 assert_eq!(
783 catalog_evidence(None, Some("0.4.3")),
784 CompatibilityEvidence::Unverified
785 );
786 assert!(catalog_line("0.4").is_none());
787 assert!(catalog_line("0.x.3").is_none());
788 assert_eq!(evidence_status("invalid"), None);
789 }
790}