1use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6use crate::{
7 AdapterRole, FailureClass, IntegrationMode, LifecycleEventKind, SCHEMA_VERSION, SupportState,
8 ValidationError, require_non_empty,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ManifestPlacementClass {
23 PreSession,
25 PreFrameLeading,
27 PreFrameTrailing,
29 ToolResult,
31 ManualOperator,
33}
34
35impl ManifestPlacementClass {
36 pub const ALL: &'static [Self] = &[
37 Self::PreSession,
38 Self::PreFrameLeading,
39 Self::PreFrameTrailing,
40 Self::ToolResult,
41 Self::ManualOperator,
42 ];
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct ManifestLifecycleEventSupport {
49 pub support: SupportState,
50 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub modes: Vec<IntegrationMode>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct ManifestPlacementSupport {
60 pub support: SupportState,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub max_bytes: Option<u64>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct ManifestContextPressure {
71 pub support: SupportState,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub evidence: Option<String>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(deny_unknown_fields)]
79pub struct ManifestReceipts {
80 pub native: bool,
82 pub lifeloop_synthesized: bool,
84 pub receipt_ledger: SupportState,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93pub struct ManifestSessionIdentity {
94 pub harness_session_id: SupportState,
95 pub harness_run_id: SupportState,
96 pub harness_task_id: SupportState,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct ManifestSessionRename {
104 pub support: SupportState,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(deny_unknown_fields)]
113pub struct ManifestRenewal {
114 pub reset: ManifestRenewalReset,
115 pub continuation: ManifestRenewalContinuation,
116 #[serde(default, skip_serializing_if = "Vec::is_empty")]
119 pub profiles: Vec<String>,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub evidence: Option<String>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(deny_unknown_fields)]
130pub struct ManifestRenewalReset {
131 pub native: SupportState,
132 pub wrapper_mediated: SupportState,
133 pub manual: SupportState,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub struct ManifestRenewalContinuation {
143 pub observation: SupportState,
144 pub payload_delivery: SupportState,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(deny_unknown_fields)]
151pub struct ManifestApprovalSurface {
152 pub support: SupportState,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(deny_unknown_fields)]
158pub struct ManifestTelemetrySource {
159 pub source: String,
160 pub support: SupportState,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173#[serde(deny_unknown_fields)]
174pub struct AdapterManifest {
175 pub contract_version: String,
176 pub adapter_id: String,
177 pub adapter_version: String,
178 pub display_name: String,
179 pub role: AdapterRole,
180 pub integration_modes: Vec<IntegrationMode>,
181 pub lifecycle_events: BTreeMap<LifecycleEventKind, ManifestLifecycleEventSupport>,
182 pub placement: BTreeMap<ManifestPlacementClass, ManifestPlacementSupport>,
183 pub context_pressure: ManifestContextPressure,
184 pub receipts: ManifestReceipts,
185
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub session_identity: Option<ManifestSessionIdentity>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub session_rename: Option<ManifestSessionRename>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub renewal: Option<ManifestRenewal>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub approval_surface: Option<ManifestApprovalSurface>,
194 #[serde(default, skip_serializing_if = "Vec::is_empty")]
195 pub failure_modes: Vec<FailureClass>,
196 #[serde(default, skip_serializing_if = "Vec::is_empty")]
197 pub telemetry_sources: Vec<ManifestTelemetrySource>,
198}
199
200impl AdapterManifest {
201 pub fn validate(&self) -> Result<(), ValidationError> {
202 if self.contract_version != SCHEMA_VERSION {
203 return Err(ValidationError::SchemaVersionMismatch {
204 expected: SCHEMA_VERSION.to_string(),
205 found: self.contract_version.clone(),
206 });
207 }
208 require_non_empty(&self.adapter_id, "manifest.adapter_id")?;
209 require_non_empty(&self.adapter_version, "manifest.adapter_version")?;
210 require_non_empty(&self.display_name, "manifest.display_name")?;
211 if self.integration_modes.is_empty() {
212 return Err(ValidationError::InvalidManifest(
213 "manifest.integration_modes must declare at least one integration mode".into(),
214 ));
215 }
216 if let Some(evidence) = &self.context_pressure.evidence {
217 require_non_empty(evidence, "manifest.context_pressure.evidence")?;
218 }
219 for src in &self.telemetry_sources {
220 require_non_empty(&src.source, "manifest.telemetry_sources[].source")?;
221 }
222 if let Some(renewal) = &self.renewal
223 && let Some(evidence) = &renewal.evidence
224 {
225 require_non_empty(evidence, "manifest.renewal.evidence")?;
226 }
227 if let Some(renewal) = &self.renewal {
228 for profile in &renewal.profiles {
229 require_non_empty(profile, "manifest.renewal.profiles[]")?;
230 }
231 }
232 Ok(())
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
242#[serde(rename_all = "snake_case")]
243pub enum ConformanceLevel {
244 V1Conformance,
248 PreConformance,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct RegisteredAdapter {
258 pub manifest: AdapterManifest,
259 pub conformance: ConformanceLevel,
260}
261
262pub fn manifest_registry() -> Vec<RegisteredAdapter> {
266 vec![
267 RegisteredAdapter {
268 manifest: codex_manifest(),
269 conformance: ConformanceLevel::V1Conformance,
270 },
271 RegisteredAdapter {
272 manifest: claude_manifest(),
273 conformance: ConformanceLevel::V1Conformance,
274 },
275 RegisteredAdapter {
276 manifest: hermes_manifest(),
277 conformance: ConformanceLevel::PreConformance,
278 },
279 RegisteredAdapter {
280 manifest: openclaw_manifest(),
281 conformance: ConformanceLevel::PreConformance,
282 },
283 RegisteredAdapter {
284 manifest: gemini_manifest(),
285 conformance: ConformanceLevel::PreConformance,
286 },
287 RegisteredAdapter {
288 manifest: opencode_manifest(),
289 conformance: ConformanceLevel::PreConformance,
290 },
291 ]
292}
293
294pub fn lookup_manifest(adapter_id: &str) -> Option<RegisteredAdapter> {
297 manifest_registry()
298 .into_iter()
299 .find(|entry| entry.manifest.adapter_id == adapter_id)
300}
301
302fn synthesized() -> SupportState {
303 SupportState::Synthesized
304}
305
306fn native() -> SupportState {
307 SupportState::Native
308}
309
310fn unavailable() -> SupportState {
311 SupportState::Unavailable
312}
313
314fn manual() -> SupportState {
315 SupportState::Manual
316}
317
318pub fn codex_manifest() -> AdapterManifest {
323 let lifecycle_events = BTreeMap::from([
324 (
325 LifecycleEventKind::SessionStarting,
326 ManifestLifecycleEventSupport {
327 support: native(),
328 modes: vec![IntegrationMode::NativeHook],
329 },
330 ),
331 (
332 LifecycleEventKind::SessionStarted,
333 ManifestLifecycleEventSupport {
334 support: native(),
335 modes: vec![IntegrationMode::NativeHook],
336 },
337 ),
338 (
339 LifecycleEventKind::FrameOpening,
340 ManifestLifecycleEventSupport {
341 support: native(),
342 modes: vec![IntegrationMode::NativeHook],
343 },
344 ),
345 (
346 LifecycleEventKind::FrameOpened,
347 ManifestLifecycleEventSupport {
348 support: synthesized(),
349 modes: vec![IntegrationMode::NativeHook],
350 },
351 ),
352 (
353 LifecycleEventKind::ContextPressureObserved,
354 ManifestLifecycleEventSupport {
355 support: native(),
356 modes: vec![IntegrationMode::NativeHook],
357 },
358 ),
359 (
360 LifecycleEventKind::ContextCompacted,
361 ManifestLifecycleEventSupport {
362 support: native(),
363 modes: vec![IntegrationMode::NativeHook],
364 },
365 ),
366 (
367 LifecycleEventKind::FrameEnding,
368 ManifestLifecycleEventSupport {
369 support: native(),
370 modes: vec![IntegrationMode::NativeHook],
371 },
372 ),
373 (
374 LifecycleEventKind::FrameEnded,
375 ManifestLifecycleEventSupport {
376 support: native(),
377 modes: vec![IntegrationMode::NativeHook],
378 },
379 ),
380 (
381 LifecycleEventKind::SessionEnding,
382 ManifestLifecycleEventSupport {
383 support: unavailable(),
384 modes: Vec::new(),
385 },
386 ),
387 (
388 LifecycleEventKind::SessionEnded,
389 ManifestLifecycleEventSupport {
390 support: unavailable(),
391 modes: Vec::new(),
392 },
393 ),
394 (
395 LifecycleEventKind::SupervisorTick,
396 ManifestLifecycleEventSupport {
397 support: unavailable(),
398 modes: Vec::new(),
399 },
400 ),
401 (
402 LifecycleEventKind::CapabilityDegraded,
403 ManifestLifecycleEventSupport {
404 support: synthesized(),
405 modes: vec![IntegrationMode::NativeHook],
406 },
407 ),
408 (
409 LifecycleEventKind::ReceiptEmitted,
410 ManifestLifecycleEventSupport {
411 support: synthesized(),
412 modes: vec![IntegrationMode::NativeHook],
413 },
414 ),
415 (
416 LifecycleEventKind::ReceiptGapDetected,
417 ManifestLifecycleEventSupport {
418 support: unavailable(),
419 modes: Vec::new(),
420 },
421 ),
422 ]);
423
424 let placement = BTreeMap::from([
425 (
426 ManifestPlacementClass::PreSession,
427 ManifestPlacementSupport {
428 support: native(),
429 max_bytes: Some(8192),
430 },
431 ),
432 (
433 ManifestPlacementClass::PreFrameLeading,
434 ManifestPlacementSupport {
435 support: native(),
436 max_bytes: Some(8192),
437 },
438 ),
439 (
440 ManifestPlacementClass::PreFrameTrailing,
441 ManifestPlacementSupport {
442 support: unavailable(),
443 max_bytes: None,
444 },
445 ),
446 (
447 ManifestPlacementClass::ToolResult,
448 ManifestPlacementSupport {
449 support: unavailable(),
450 max_bytes: None,
451 },
452 ),
453 (
454 ManifestPlacementClass::ManualOperator,
455 ManifestPlacementSupport {
456 support: manual(),
457 max_bytes: None,
458 },
459 ),
460 ]);
461
462 AdapterManifest {
463 contract_version: SCHEMA_VERSION.to_string(),
464 adapter_id: "codex".into(),
465 adapter_version: "0.1.0".into(),
466 display_name: "Codex".into(),
467 role: AdapterRole::PrimaryWorker,
468 integration_modes: vec![IntegrationMode::NativeHook, IntegrationMode::ManualSkill],
469 lifecycle_events,
470 placement,
471 context_pressure: ManifestContextPressure {
472 support: native(),
473 evidence: Some(
474 "Codex CLI 0.129 exposes PreCompact before context pressure handling and PostCompact after context compacts"
475 .into(),
476 ),
477 },
478 receipts: ManifestReceipts {
479 native: false,
480 lifeloop_synthesized: true,
481 receipt_ledger: unavailable(),
482 },
483 session_identity: Some(ManifestSessionIdentity {
484 harness_session_id: native(),
485 harness_run_id: synthesized(),
486 harness_task_id: unavailable(),
487 }),
488 session_rename: None,
489 renewal: Some(ManifestRenewal {
490 reset: ManifestRenewalReset {
491 native: unavailable(),
492 wrapper_mediated: synthesized(),
493 manual: manual(),
494 },
495 continuation: ManifestRenewalContinuation {
496 observation: native(),
497 payload_delivery: synthesized(),
498 },
499 profiles: Vec::new(),
500 evidence: Some(
501 "Codex SessionStart/Stop lifecycle hooks plus the subprocess client callback prove continuation-boundary observation and continuation payload delivery"
502 .into(),
503 ),
504 }),
505 approval_surface: None,
506 failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
507 telemetry_sources: Vec::new(),
508 }
509}
510
511pub fn claude_manifest() -> AdapterManifest {
513 let lifecycle_events = BTreeMap::from([
514 (
515 LifecycleEventKind::SessionStarting,
516 ManifestLifecycleEventSupport {
517 support: native(),
518 modes: vec![IntegrationMode::NativeHook],
519 },
520 ),
521 (
522 LifecycleEventKind::SessionStarted,
523 ManifestLifecycleEventSupport {
524 support: native(),
525 modes: vec![IntegrationMode::NativeHook],
526 },
527 ),
528 (
529 LifecycleEventKind::FrameOpening,
530 ManifestLifecycleEventSupport {
531 support: native(),
532 modes: vec![IntegrationMode::NativeHook],
533 },
534 ),
535 (
536 LifecycleEventKind::FrameOpened,
537 ManifestLifecycleEventSupport {
538 support: native(),
539 modes: vec![IntegrationMode::NativeHook],
540 },
541 ),
542 (
543 LifecycleEventKind::ContextPressureObserved,
544 ManifestLifecycleEventSupport {
545 support: native(),
546 modes: vec![IntegrationMode::NativeHook],
547 },
548 ),
549 (
550 LifecycleEventKind::ContextCompacted,
551 ManifestLifecycleEventSupport {
552 support: unavailable(),
553 modes: Vec::new(),
554 },
555 ),
556 (
557 LifecycleEventKind::FrameEnding,
558 ManifestLifecycleEventSupport {
559 support: native(),
560 modes: vec![IntegrationMode::NativeHook],
561 },
562 ),
563 (
564 LifecycleEventKind::FrameEnded,
565 ManifestLifecycleEventSupport {
566 support: native(),
567 modes: vec![IntegrationMode::NativeHook],
568 },
569 ),
570 (
571 LifecycleEventKind::SessionEnding,
572 ManifestLifecycleEventSupport {
573 support: native(),
574 modes: vec![IntegrationMode::NativeHook],
575 },
576 ),
577 (
578 LifecycleEventKind::SessionEnded,
579 ManifestLifecycleEventSupport {
580 support: native(),
581 modes: vec![IntegrationMode::NativeHook],
582 },
583 ),
584 (
585 LifecycleEventKind::SupervisorTick,
586 ManifestLifecycleEventSupport {
587 support: unavailable(),
588 modes: Vec::new(),
589 },
590 ),
591 (
592 LifecycleEventKind::CapabilityDegraded,
593 ManifestLifecycleEventSupport {
594 support: synthesized(),
595 modes: vec![IntegrationMode::NativeHook],
596 },
597 ),
598 (
599 LifecycleEventKind::ReceiptEmitted,
600 ManifestLifecycleEventSupport {
601 support: synthesized(),
602 modes: vec![IntegrationMode::NativeHook],
603 },
604 ),
605 (
606 LifecycleEventKind::ReceiptGapDetected,
607 ManifestLifecycleEventSupport {
608 support: unavailable(),
609 modes: Vec::new(),
610 },
611 ),
612 ]);
613
614 let placement = BTreeMap::from([
615 (
616 ManifestPlacementClass::PreSession,
617 ManifestPlacementSupport {
618 support: native(),
619 max_bytes: Some(16_384),
620 },
621 ),
622 (
623 ManifestPlacementClass::PreFrameLeading,
624 ManifestPlacementSupport {
625 support: native(),
626 max_bytes: Some(16_384),
627 },
628 ),
629 (
630 ManifestPlacementClass::PreFrameTrailing,
631 ManifestPlacementSupport {
632 support: unavailable(),
633 max_bytes: None,
634 },
635 ),
636 (
637 ManifestPlacementClass::ToolResult,
638 ManifestPlacementSupport {
639 support: unavailable(),
640 max_bytes: None,
641 },
642 ),
643 (
644 ManifestPlacementClass::ManualOperator,
645 ManifestPlacementSupport {
646 support: manual(),
647 max_bytes: None,
648 },
649 ),
650 ]);
651
652 AdapterManifest {
653 contract_version: SCHEMA_VERSION.to_string(),
654 adapter_id: "claude".into(),
655 adapter_version: "0.1.0".into(),
656 display_name: "Claude".into(),
657 role: AdapterRole::PrimaryWorker,
658 integration_modes: vec![IntegrationMode::NativeHook],
659 lifecycle_events,
660 placement,
661 context_pressure: ManifestContextPressure {
662 support: native(),
663 evidence: Some(
664 "Claude emits PreCompact and SessionEnd events that map directly to context.pressure_observed"
665 .into(),
666 ),
667 },
668 receipts: ManifestReceipts {
669 native: false,
670 lifeloop_synthesized: true,
671 receipt_ledger: unavailable(),
672 },
673 session_identity: Some(ManifestSessionIdentity {
674 harness_session_id: native(),
675 harness_run_id: synthesized(),
676 harness_task_id: unavailable(),
677 }),
678 session_rename: None,
679 renewal: None,
680 approval_surface: None,
681 failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
682 telemetry_sources: Vec::new(),
683 }
684}
685
686pub fn hermes_manifest() -> AdapterManifest {
690 pre_conformance_reference_adapter_manifest("hermes", "Hermes")
691}
692
693pub fn openclaw_manifest() -> AdapterManifest {
695 pre_conformance_reference_adapter_manifest("openclaw", "OpenClaw")
696}
697
698pub fn gemini_manifest() -> AdapterManifest {
700 pre_conformance_telemetry_only_manifest("gemini", "Gemini")
701}
702
703pub fn opencode_manifest() -> AdapterManifest {
705 pre_conformance_telemetry_only_manifest("opencode", "OpenCode")
706}
707
708fn pre_conformance_reference_adapter_manifest(
709 adapter_id: &str,
710 display_name: &str,
711) -> AdapterManifest {
712 let lifecycle_events = BTreeMap::from([
713 (
714 LifecycleEventKind::SessionStarting,
715 ManifestLifecycleEventSupport {
716 support: SupportState::Partial,
717 modes: vec![IntegrationMode::ReferenceAdapter],
718 },
719 ),
720 (
721 LifecycleEventKind::SessionStarted,
722 ManifestLifecycleEventSupport {
723 support: SupportState::Partial,
724 modes: vec![IntegrationMode::ReferenceAdapter],
725 },
726 ),
727 (
728 LifecycleEventKind::FrameOpening,
729 ManifestLifecycleEventSupport {
730 support: SupportState::Partial,
731 modes: vec![IntegrationMode::ReferenceAdapter],
732 },
733 ),
734 (
735 LifecycleEventKind::FrameEnded,
736 ManifestLifecycleEventSupport {
737 support: SupportState::Partial,
738 modes: vec![IntegrationMode::ReferenceAdapter],
739 },
740 ),
741 (
742 LifecycleEventKind::SessionEnded,
743 ManifestLifecycleEventSupport {
744 support: SupportState::Partial,
745 modes: vec![IntegrationMode::ReferenceAdapter],
746 },
747 ),
748 ]);
749
750 let placement = BTreeMap::from([
751 (
752 ManifestPlacementClass::PreSession,
753 ManifestPlacementSupport {
754 support: SupportState::Partial,
755 max_bytes: None,
756 },
757 ),
758 (
759 ManifestPlacementClass::PreFrameLeading,
760 ManifestPlacementSupport {
761 support: SupportState::Partial,
762 max_bytes: None,
763 },
764 ),
765 (
766 ManifestPlacementClass::ManualOperator,
767 ManifestPlacementSupport {
768 support: SupportState::Manual,
769 max_bytes: None,
770 },
771 ),
772 ]);
773
774 AdapterManifest {
775 contract_version: SCHEMA_VERSION.to_string(),
776 adapter_id: adapter_id.to_string(),
777 adapter_version: "0.0.1-pre".into(),
778 display_name: display_name.to_string(),
779 role: AdapterRole::Worker,
780 integration_modes: vec![IntegrationMode::ReferenceAdapter],
781 lifecycle_events,
782 placement,
783 context_pressure: ManifestContextPressure {
784 support: SupportState::Partial,
785 evidence: None,
786 },
787 receipts: ManifestReceipts {
788 native: false,
789 lifeloop_synthesized: true,
790 receipt_ledger: SupportState::Unavailable,
791 },
792 session_identity: None,
793 session_rename: None,
794 renewal: None,
795 approval_surface: None,
796 failure_modes: Vec::new(),
797 telemetry_sources: Vec::new(),
798 }
799}
800
801fn pre_conformance_telemetry_only_manifest(
802 adapter_id: &str,
803 display_name: &str,
804) -> AdapterManifest {
805 let lifecycle_events = BTreeMap::from([
806 (
807 LifecycleEventKind::SessionStarting,
808 ManifestLifecycleEventSupport {
809 support: SupportState::Partial,
810 modes: vec![IntegrationMode::TelemetryOnly],
811 },
812 ),
813 (
814 LifecycleEventKind::ContextPressureObserved,
815 ManifestLifecycleEventSupport {
816 support: SupportState::Partial,
817 modes: vec![IntegrationMode::TelemetryOnly],
818 },
819 ),
820 (
821 LifecycleEventKind::SessionEnded,
822 ManifestLifecycleEventSupport {
823 support: SupportState::Partial,
824 modes: vec![IntegrationMode::TelemetryOnly],
825 },
826 ),
827 ]);
828
829 let placement = BTreeMap::from([(
830 ManifestPlacementClass::ManualOperator,
831 ManifestPlacementSupport {
832 support: SupportState::Manual,
833 max_bytes: None,
834 },
835 )]);
836
837 AdapterManifest {
838 contract_version: SCHEMA_VERSION.to_string(),
839 adapter_id: adapter_id.to_string(),
840 adapter_version: "0.0.1-pre".into(),
841 display_name: display_name.to_string(),
842 role: AdapterRole::Observer,
843 integration_modes: vec![IntegrationMode::TelemetryOnly],
844 lifecycle_events,
845 placement,
846 context_pressure: ManifestContextPressure {
847 support: SupportState::Partial,
848 evidence: None,
849 },
850 receipts: ManifestReceipts {
851 native: false,
852 lifeloop_synthesized: true,
853 receipt_ledger: SupportState::Unavailable,
854 },
855 session_identity: None,
856 session_rename: None,
857 renewal: None,
858 approval_surface: None,
859 failure_modes: Vec::new(),
860 telemetry_sources: Vec::new(),
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867
868 fn valid_manifest() -> AdapterManifest {
869 manifest_registry()
870 .into_iter()
871 .next()
872 .expect("registry has at least one adapter")
873 .manifest
874 }
875
876 #[test]
877 fn empty_context_pressure_evidence_is_rejected() {
878 let mut manifest = valid_manifest();
879 manifest.validate().expect("baseline manifest validates");
880 manifest.context_pressure.evidence = Some(String::new());
881 assert!(matches!(
882 manifest.validate(),
883 Err(ValidationError::EmptyField(field)) if field == "manifest.context_pressure.evidence"
884 ));
885 }
886}