runx_runtime/effects/
mod.rs1mod error;
2mod metadata;
3mod provider_permission;
4mod registry;
5mod types;
6
7pub use error::RuntimeEffectError;
8pub(crate) use metadata::effect_verification_refs;
9pub use metadata::{EFFECT_VERIFICATION_REFS_METADATA, insert_effect_verification_ref};
10pub use provider_permission::{
11 PROVIDER_PERMISSION_EFFECT_FAMILY, PROVIDER_PERMISSION_GRANT_ID_ENV,
12 PROVIDER_PERMISSION_GRANTED_SCOPES_ENV, ProviderPermissionAdmission, ProviderPermissionEffect,
13};
14pub use registry::RuntimeEffectRegistry;
15pub use types::{
16 EffectAdmission, EffectMetadataRefreshRequest, EffectOutputRequest, EffectReceiptRequest,
17 EffectReplay, EffectReplayOutputRequest, EffectReplayReceiptRequest, EffectStepRequest,
18 RuntimeEffect,
19};
20
21#[cfg(test)]
22mod tests {
23 use std::collections::BTreeMap;
24 use std::path::Path;
25
26 use runx_contracts::{AuthorityVerb, JsonObject, Reference};
27 use runx_core::state_machine::AuthorityAdmissionWitness;
28 use runx_parser::GraphStep;
29
30 use super::*;
31 use crate::adapter::{InvocationStatus, SkillOutput};
32
33 struct MockEffect;
34
35 impl RuntimeEffect for MockEffect {
36 fn family(&self) -> &'static str {
37 "deploy"
38 }
39
40 fn admit(
41 &self,
42 request: EffectStepRequest<'_>,
43 ) -> Result<Option<EffectAdmission>, RuntimeEffectError> {
44 let _ = request;
45 Ok(Some(EffectAdmission::new(
46 "deploy",
47 AuthorityVerb::Write,
48 AuthorityAdmissionWitness {
49 verb: AuthorityVerb::Write,
50 parent_term_id: "parent".to_owned(),
51 child_term_id: "child".to_owned(),
52 idempotency_key: Some("deploy-key".to_owned()),
53 capability_ref: None,
54 },
55 (),
56 )))
57 }
58 }
59
60 #[test]
61 fn registry_dispatches_effect_family() {
62 let registry = RuntimeEffectRegistry::with_effect(MockEffect);
63 let step = test_step();
64 let inputs = JsonObject::new();
65 let env = BTreeMap::new();
66 let result = registry.admit(EffectStepRequest {
67 step: &step,
68 inputs: &inputs,
69 env: &env,
70 graph_dir: Path::new("."),
71 });
72 assert!(
73 matches!(
74 &result,
75 Ok(Some(admission))
76 if admission.family() == "deploy" && admission.verb() == AuthorityVerb::Write
77 ),
78 "unexpected admission result: {result:?}"
79 );
80 }
81
82 #[test]
83 fn registry_rejects_missing_effect_family_after_admission() {
84 let registry = RuntimeEffectRegistry::empty();
85 let step = test_step();
86 let admission = EffectAdmission::new(
87 "absent",
88 AuthorityVerb::Write,
89 AuthorityAdmissionWitness {
90 verb: AuthorityVerb::Write,
91 parent_term_id: "parent".to_owned(),
92 child_term_id: "child".to_owned(),
93 idempotency_key: None,
94 capability_ref: None,
95 },
96 (),
97 );
98 let claim = JsonObject::new();
99 let mut output = SkillOutput {
100 status: InvocationStatus::Success,
101 stdout: String::new(),
102 stderr: String::new(),
103 exit_code: Some(0),
104 duration_ms: 0,
105 metadata: JsonObject::new(),
106 };
107
108 let result = registry.prepare_output(EffectOutputRequest {
109 step: &step,
110 admission: &admission,
111 claim: &claim,
112 output: &mut output,
113 });
114
115 assert!(
116 matches!(result, Err(RuntimeEffectError::MissingFamily { ref family }) if family == "absent"),
117 "unexpected missing-family result: {result:?}"
118 );
119 }
120
121 #[test]
122 fn verification_refs_round_trip_through_metadata() {
123 let mut metadata = JsonObject::new();
124 let reference = Reference::runx(runx_contracts::ReferenceType::Verification, "proof:1");
125 let insert = insert_effect_verification_ref(&mut metadata, reference.clone());
126 assert!(insert.is_ok(), "unexpected insert result: {insert:?}");
127 assert_eq!(effect_verification_refs(&metadata), Ok(vec![reference]));
128 }
129
130 fn test_step() -> GraphStep {
131 GraphStep {
132 id: "ship".to_owned(),
133 label: None,
134 skill: None,
135 tool: None,
136 run: None,
137 instructions: None,
138 artifacts: None,
139 runner: None,
140 inputs: JsonObject::new(),
141 context: BTreeMap::new(),
142 context_edges: Vec::new(),
143 context_skills: Vec::new(),
144 scopes: Vec::new(),
145 allowed_tools: None,
146 retry: None,
147 policy: None,
148 fanout_group: None,
149 when: None,
150 mutating: false,
151 idempotency_key: None,
152 mint_authority: None,
153 requested_scope_from: None,
154 }
155 }
156}