use runx_contracts::{ClosureDisposition, Receipt, ReceiptSchema};
use runx_receipts::{
ReceiptProofContextProvider, canonical_receipt_body_digest, canonical_receipt_digest,
verify_receipt_proof,
};
use crate::execution::harness::fixtures::{HarnessExpectedStatus, ReceiptExpectation};
use crate::execution::harness::runner::{HarnessReplayError, HarnessReplayOutput};
use crate::receipts::{RuntimeReceiptProofContextProvider, RuntimeReceiptSignaturePolicy};
#[derive(Clone, Debug, PartialEq)]
pub struct HarnessReplayReceipt {
pub receipt_id: String,
pub harness_id: String,
pub state: String,
pub disposition: ClosureDisposition,
pub reason_code: String,
pub act_ids: Vec<String>,
pub decision_ids: Vec<String>,
pub child_receipt_refs: Vec<String>,
pub verification_refs: Vec<String>,
}
pub(super) fn assert_expectations(
output: &HarnessReplayOutput,
signature_policy: RuntimeReceiptSignaturePolicy<'_>,
) -> Result<(), HarnessReplayError> {
if let Some(expected_status) = &output.fixture.expect.status {
assert_equal(
"expect.status",
status_name(expected_status),
status_name(&output.status),
)?;
}
if let Some(expected_receipt) = &output.fixture.expect.receipt {
assert_receipt(expected_receipt, &output.receipt, signature_policy)?;
}
if !output.fixture.expect.steps.is_empty() {
let actual = output
.step_receipts
.iter()
.map(receipt_step_name)
.collect::<Vec<_>>();
assert_equal(
"expect.steps",
output.fixture.expect.steps.join(","),
actual.join(","),
)?;
}
Ok(())
}
pub(super) fn status_from_disposition(disposition: &ClosureDisposition) -> HarnessExpectedStatus {
match disposition {
ClosureDisposition::Closed => HarnessExpectedStatus::Sealed,
ClosureDisposition::Deferred => HarnessExpectedStatus::NeedsAgent,
ClosureDisposition::Blocked => HarnessExpectedStatus::PolicyDenied,
ClosureDisposition::TimedOut
| ClosureDisposition::Declined
| ClosureDisposition::Failed
| ClosureDisposition::Killed
| ClosureDisposition::Superseded => HarnessExpectedStatus::Failure,
}
}
#[allow(clippy::print_stderr)]
fn assert_receipt(
expected: &ReceiptExpectation,
actual: &Receipt,
signature_policy: RuntimeReceiptSignaturePolicy<'_>,
) -> Result<(), HarnessReplayError> {
assert_receipt_proof(actual, signature_policy)?;
if crate::services::process_env_value("RUNX_REGEN_FIXTURES").is_some() {
let summary = summarize_receipt(actual);
let body_digest = canonical_receipt_body_digest(actual).map_err(receipt_digest_error)?;
let receipt_digest = canonical_receipt_digest(actual).map_err(receipt_digest_error)?;
eprintln!(
"REGEN id={} receipt_id={} body_digest={} receipt_digest={} child_refs=[{}]",
actual.subject.reference.uri,
actual.id,
body_digest,
receipt_digest,
summary.child_receipt_refs.join(",")
);
return Ok(());
}
assert_equal(
"expect.receipt.schema",
schema_name(&expected.schema),
schema_name(&actual.schema),
)?;
if let Some(expected_id) = &expected.receipt_id {
assert_equal("expect.receipt.receipt_id", expected_id, &actual.id)?;
}
let summary = summarize_receipt(actual);
assert_receipt_identity(expected, &summary)?;
assert_receipt_lists(expected, &summary)?;
assert_receipt_digests(expected, actual)
}
fn assert_receipt_identity(
expected: &ReceiptExpectation,
summary: &HarnessReplayReceipt,
) -> Result<(), HarnessReplayError> {
if let Some(expected_harness_id) = &expected.harness_id {
assert_equal(
"expect.receipt.harness_id",
expected_harness_id,
&summary.harness_id,
)?;
}
if let Some(expected_state) = &expected.state {
assert_equal(
"expect.receipt.state",
expected_state.as_str(),
summary.state.as_str(),
)?;
}
if let Some(expected_disposition) = &expected.disposition {
assert_equal(
"expect.receipt.disposition",
disposition_name(expected_disposition),
disposition_name(&summary.disposition),
)?;
}
if let Some(expected_reason_code) = &expected.reason_code {
assert_equal(
"expect.receipt.reason_code",
expected_reason_code,
&summary.reason_code,
)?;
}
Ok(())
}
fn assert_receipt_lists(
expected: &ReceiptExpectation,
summary: &HarnessReplayReceipt,
) -> Result<(), HarnessReplayError> {
assert_optional_list(
"expect.receipt.act_ids",
&expected.act_ids,
&summary.act_ids,
)?;
assert_optional_list(
"expect.receipt.decision_ids",
&expected.decision_ids,
&summary.decision_ids,
)?;
assert_optional_list(
"expect.receipt.child_receipt_refs",
&expected.child_receipt_refs,
&summary.child_receipt_refs,
)?;
assert_optional_list(
"expect.receipt.verification_refs",
&expected.verification_refs,
&summary.verification_refs,
)
}
fn assert_receipt_digests(
expected: &ReceiptExpectation,
actual: &Receipt,
) -> Result<(), HarnessReplayError> {
if let Some(expected_body_digest) = &expected.body_digest {
let body_digest = canonical_receipt_body_digest(actual).map_err(receipt_digest_error)?;
assert_equal(
"expect.receipt.body_digest",
expected_body_digest,
body_digest,
)?;
}
if let Some(expected_digest) = &expected.receipt_digest {
let receipt_digest = canonical_receipt_digest(actual).map_err(receipt_digest_error)?;
assert_equal(
"expect.receipt.receipt_digest",
expected_digest,
receipt_digest,
)?;
}
Ok(())
}
fn assert_receipt_proof(
receipt: &Receipt,
signature_policy: RuntimeReceiptSignaturePolicy<'_>,
) -> Result<(), HarnessReplayError> {
let proof_contexts = RuntimeReceiptProofContextProvider::new(signature_policy);
let context = proof_contexts.proof_context(receipt);
let verification = verify_receipt_proof(receipt, &context);
if verification.valid {
Ok(())
} else {
Err(HarnessReplayError::ReceiptProofInvalid {
receipt_id: receipt.id.to_string(),
findings: format!("{:?}", verification.findings),
})
}
}
fn receipt_digest_error(error: runx_receipts::ReceiptError) -> HarnessReplayError {
HarnessReplayError::ReceiptDigest {
message: error.to_string(),
}
}
fn summarize_receipt(receipt: &Receipt) -> HarnessReplayReceipt {
let state = if matches!(receipt.seal.disposition, ClosureDisposition::Deferred) {
"deferred".to_owned()
} else {
"sealed".to_owned()
};
HarnessReplayReceipt {
receipt_id: receipt.id.to_string(),
harness_id: receipt.subject.reference.uri.clone().into_string(),
state,
disposition: receipt.seal.disposition.clone(),
reason_code: receipt.seal.reason_code.to_string(),
act_ids: receipt.acts.iter().map(|act| act.id.to_string()).collect(),
decision_ids: receipt
.decisions
.iter()
.map(|decision| decision.decision_id.as_str().to_owned())
.collect(),
child_receipt_refs: receipt
.lineage
.as_ref()
.map(|lineage| {
lineage
.children
.iter()
.map(|reference| reference.uri.clone().into_string())
.collect()
})
.unwrap_or_default(),
verification_refs: receipt
.acts
.iter()
.flat_map(|act| act.criterion_bindings.iter())
.flat_map(|binding| binding.verification_refs.iter())
.map(|reference| reference.uri.clone().into_string())
.collect(),
}
}
fn receipt_step_name(receipt: &Receipt) -> String {
receipt.acts.first().map_or_else(
|| receipt.subject.reference.uri.clone().into_string(),
|act| act.id.trim_start_matches("act_").to_owned(),
)
}
fn assert_optional_list(
field: &'static str,
expected: &[String],
actual: &[String],
) -> Result<(), HarnessReplayError> {
if expected.is_empty() {
return Ok(());
}
assert_equal(field, expected.join(","), actual.join(","))
}
fn assert_equal(
field: &'static str,
expected: impl AsRef<str>,
actual: impl AsRef<str>,
) -> Result<(), HarnessReplayError> {
let expected = expected.as_ref();
let actual = actual.as_ref();
if expected == actual {
return Ok(());
}
Err(HarnessReplayError::Mismatch {
field,
expected: expected.to_owned(),
actual: actual.to_owned(),
})
}
fn schema_name(schema: &ReceiptSchema) -> &'static str {
match schema {
ReceiptSchema::V1 => "runx.receipt.v1",
}
}
fn disposition_name(disposition: &ClosureDisposition) -> &'static str {
match disposition {
ClosureDisposition::Closed => "closed",
ClosureDisposition::Deferred => "deferred",
ClosureDisposition::Superseded => "superseded",
ClosureDisposition::Declined => "declined",
ClosureDisposition::Blocked => "blocked",
ClosureDisposition::Failed => "failed",
ClosureDisposition::Killed => "killed",
ClosureDisposition::TimedOut => "timed_out",
}
}
fn status_name(status: &HarnessExpectedStatus) -> &'static str {
match status {
HarnessExpectedStatus::Sealed => "sealed",
HarnessExpectedStatus::Failure => "failure",
HarnessExpectedStatus::NeedsAgent => "needs_agent",
HarnessExpectedStatus::PolicyDenied => "policy_denied",
HarnessExpectedStatus::Escalated => "escalated",
}
}