runx_runtime/execution/harness/
assertions.rs1use runx_contracts::{ClosureDisposition, Receipt, ReceiptSchema};
2use runx_receipts::{
3 ReceiptProofContextProvider, canonical_receipt_body_digest, canonical_receipt_digest,
4 verify_receipt_proof,
5};
6
7use crate::execution::harness::fixtures::{HarnessExpectedStatus, ReceiptExpectation};
8use crate::execution::harness::runner::{HarnessReplayError, HarnessReplayOutput};
9use crate::receipts::{RuntimeReceiptProofContextProvider, RuntimeReceiptSignaturePolicy};
10
11#[derive(Clone, Debug, PartialEq)]
12pub struct HarnessReplayReceipt {
13 pub receipt_id: String,
14 pub harness_id: String,
15 pub state: String,
16 pub disposition: ClosureDisposition,
17 pub reason_code: String,
18 pub act_ids: Vec<String>,
19 pub decision_ids: Vec<String>,
20 pub child_receipt_refs: Vec<String>,
21 pub verification_refs: Vec<String>,
22}
23
24pub(super) fn assert_expectations(
25 output: &HarnessReplayOutput,
26 signature_policy: RuntimeReceiptSignaturePolicy<'_>,
27) -> Result<(), HarnessReplayError> {
28 if let Some(expected_status) = &output.fixture.expect.status {
29 assert_equal(
30 "expect.status",
31 status_name(expected_status),
32 status_name(&output.status),
33 )?;
34 }
35 if let Some(expected_receipt) = &output.fixture.expect.receipt {
36 assert_receipt(expected_receipt, &output.receipt, signature_policy)?;
37 }
38 if !output.fixture.expect.steps.is_empty() {
39 let actual = output
40 .step_receipts
41 .iter()
42 .map(receipt_step_name)
43 .collect::<Vec<_>>();
44 assert_equal(
45 "expect.steps",
46 output.fixture.expect.steps.join(","),
47 actual.join(","),
48 )?;
49 }
50 Ok(())
51}
52
53pub(super) fn status_from_disposition(disposition: &ClosureDisposition) -> HarnessExpectedStatus {
54 match disposition {
55 ClosureDisposition::Closed => HarnessExpectedStatus::Sealed,
56 ClosureDisposition::Deferred => HarnessExpectedStatus::NeedsAgent,
57 ClosureDisposition::Blocked => HarnessExpectedStatus::PolicyDenied,
58 ClosureDisposition::TimedOut
59 | ClosureDisposition::Declined
60 | ClosureDisposition::Failed
61 | ClosureDisposition::Killed
62 | ClosureDisposition::Superseded => HarnessExpectedStatus::Failure,
63 }
64}
65
66#[allow(clippy::print_stderr)]
70fn assert_receipt(
71 expected: &ReceiptExpectation,
72 actual: &Receipt,
73 signature_policy: RuntimeReceiptSignaturePolicy<'_>,
74) -> Result<(), HarnessReplayError> {
75 assert_receipt_proof(actual, signature_policy)?;
76 if crate::services::process_env_value("RUNX_REGEN_FIXTURES").is_some() {
77 let summary = summarize_receipt(actual);
78 let body_digest = canonical_receipt_body_digest(actual).map_err(receipt_digest_error)?;
79 let receipt_digest = canonical_receipt_digest(actual).map_err(receipt_digest_error)?;
80 eprintln!(
81 "REGEN id={} receipt_id={} body_digest={} receipt_digest={} child_refs=[{}]",
82 actual.subject.reference.uri,
83 actual.id,
84 body_digest,
85 receipt_digest,
86 summary.child_receipt_refs.join(",")
87 );
88 return Ok(());
89 }
90 assert_equal(
91 "expect.receipt.schema",
92 schema_name(&expected.schema),
93 schema_name(&actual.schema),
94 )?;
95 if let Some(expected_id) = &expected.receipt_id {
96 assert_equal("expect.receipt.receipt_id", expected_id, &actual.id)?;
97 }
98 let summary = summarize_receipt(actual);
99 assert_receipt_identity(expected, &summary)?;
100 assert_receipt_lists(expected, &summary)?;
101 assert_receipt_digests(expected, actual)
102}
103
104fn assert_receipt_identity(
105 expected: &ReceiptExpectation,
106 summary: &HarnessReplayReceipt,
107) -> Result<(), HarnessReplayError> {
108 if let Some(expected_harness_id) = &expected.harness_id {
109 assert_equal(
110 "expect.receipt.harness_id",
111 expected_harness_id,
112 &summary.harness_id,
113 )?;
114 }
115 if let Some(expected_state) = &expected.state {
116 assert_equal(
117 "expect.receipt.state",
118 expected_state.as_str(),
119 summary.state.as_str(),
120 )?;
121 }
122 if let Some(expected_disposition) = &expected.disposition {
123 assert_equal(
124 "expect.receipt.disposition",
125 disposition_name(expected_disposition),
126 disposition_name(&summary.disposition),
127 )?;
128 }
129 if let Some(expected_reason_code) = &expected.reason_code {
130 assert_equal(
131 "expect.receipt.reason_code",
132 expected_reason_code,
133 &summary.reason_code,
134 )?;
135 }
136 Ok(())
137}
138
139fn assert_receipt_lists(
140 expected: &ReceiptExpectation,
141 summary: &HarnessReplayReceipt,
142) -> Result<(), HarnessReplayError> {
143 assert_optional_list(
144 "expect.receipt.act_ids",
145 &expected.act_ids,
146 &summary.act_ids,
147 )?;
148 assert_optional_list(
149 "expect.receipt.decision_ids",
150 &expected.decision_ids,
151 &summary.decision_ids,
152 )?;
153 assert_optional_list(
154 "expect.receipt.child_receipt_refs",
155 &expected.child_receipt_refs,
156 &summary.child_receipt_refs,
157 )?;
158 assert_optional_list(
159 "expect.receipt.verification_refs",
160 &expected.verification_refs,
161 &summary.verification_refs,
162 )
163}
164
165fn assert_receipt_digests(
166 expected: &ReceiptExpectation,
167 actual: &Receipt,
168) -> Result<(), HarnessReplayError> {
169 if let Some(expected_body_digest) = &expected.body_digest {
170 let body_digest = canonical_receipt_body_digest(actual).map_err(receipt_digest_error)?;
171 assert_equal(
172 "expect.receipt.body_digest",
173 expected_body_digest,
174 body_digest,
175 )?;
176 }
177 if let Some(expected_digest) = &expected.receipt_digest {
178 let receipt_digest = canonical_receipt_digest(actual).map_err(receipt_digest_error)?;
179 assert_equal(
180 "expect.receipt.receipt_digest",
181 expected_digest,
182 receipt_digest,
183 )?;
184 }
185 Ok(())
186}
187
188fn assert_receipt_proof(
189 receipt: &Receipt,
190 signature_policy: RuntimeReceiptSignaturePolicy<'_>,
191) -> Result<(), HarnessReplayError> {
192 let proof_contexts = RuntimeReceiptProofContextProvider::new(signature_policy);
193 let context = proof_contexts.proof_context(receipt);
194 let verification = verify_receipt_proof(receipt, &context);
195 if verification.valid {
196 Ok(())
197 } else {
198 Err(HarnessReplayError::ReceiptProofInvalid {
199 receipt_id: receipt.id.to_string(),
200 findings: format!("{:?}", verification.findings),
201 })
202 }
203}
204
205fn receipt_digest_error(error: runx_receipts::ReceiptError) -> HarnessReplayError {
206 HarnessReplayError::ReceiptDigest {
207 message: error.to_string(),
208 }
209}
210
211fn summarize_receipt(receipt: &Receipt) -> HarnessReplayReceipt {
212 let state = if matches!(receipt.seal.disposition, ClosureDisposition::Deferred) {
213 "deferred".to_owned()
214 } else {
215 "sealed".to_owned()
216 };
217 HarnessReplayReceipt {
218 receipt_id: receipt.id.to_string(),
219 harness_id: receipt.subject.reference.uri.clone().into_string(),
220 state,
221 disposition: receipt.seal.disposition.clone(),
222 reason_code: receipt.seal.reason_code.to_string(),
223 act_ids: receipt.acts.iter().map(|act| act.id.to_string()).collect(),
224 decision_ids: receipt
225 .decisions
226 .iter()
227 .map(|decision| decision.decision_id.as_str().to_owned())
228 .collect(),
229 child_receipt_refs: receipt
230 .lineage
231 .as_ref()
232 .map(|lineage| {
233 lineage
234 .children
235 .iter()
236 .map(|reference| reference.uri.clone().into_string())
237 .collect()
238 })
239 .unwrap_or_default(),
240 verification_refs: receipt
241 .acts
242 .iter()
243 .flat_map(|act| act.criterion_bindings.iter())
244 .flat_map(|binding| binding.verification_refs.iter())
245 .map(|reference| reference.uri.clone().into_string())
246 .collect(),
247 }
248}
249
250fn receipt_step_name(receipt: &Receipt) -> String {
251 receipt.acts.first().map_or_else(
252 || receipt.subject.reference.uri.clone().into_string(),
253 |act| act.id.trim_start_matches("act_").to_owned(),
254 )
255}
256
257fn assert_optional_list(
258 field: &'static str,
259 expected: &[String],
260 actual: &[String],
261) -> Result<(), HarnessReplayError> {
262 if expected.is_empty() {
263 return Ok(());
264 }
265 assert_equal(field, expected.join(","), actual.join(","))
266}
267
268fn assert_equal(
269 field: &'static str,
270 expected: impl AsRef<str>,
271 actual: impl AsRef<str>,
272) -> Result<(), HarnessReplayError> {
273 let expected = expected.as_ref();
274 let actual = actual.as_ref();
275 if expected == actual {
276 return Ok(());
277 }
278 Err(HarnessReplayError::Mismatch {
279 field,
280 expected: expected.to_owned(),
281 actual: actual.to_owned(),
282 })
283}
284
285fn schema_name(schema: &ReceiptSchema) -> &'static str {
286 match schema {
287 ReceiptSchema::V1 => "runx.receipt.v1",
288 }
289}
290
291fn disposition_name(disposition: &ClosureDisposition) -> &'static str {
292 match disposition {
293 ClosureDisposition::Closed => "closed",
294 ClosureDisposition::Deferred => "deferred",
295 ClosureDisposition::Superseded => "superseded",
296 ClosureDisposition::Declined => "declined",
297 ClosureDisposition::Blocked => "blocked",
298 ClosureDisposition::Failed => "failed",
299 ClosureDisposition::Killed => "killed",
300 ClosureDisposition::TimedOut => "timed_out",
301 }
302}
303
304fn status_name(status: &HarnessExpectedStatus) -> &'static str {
305 match status {
306 HarnessExpectedStatus::Sealed => "sealed",
307 HarnessExpectedStatus::Failure => "failure",
308 HarnessExpectedStatus::NeedsAgent => "needs_agent",
309 HarnessExpectedStatus::PolicyDenied => "policy_denied",
310 HarnessExpectedStatus::Escalated => "escalated",
311 }
312}