143 lines
5.0 KiB
Rust
143 lines
5.0 KiB
Rust
use crank_core::{
|
|
AgentId, CorrelationContext, ExecutionErrorCode, ExecutionFailure, ExecutionOrigin,
|
|
ExecutionStage, OutcomeCertainty, Retryability,
|
|
};
|
|
|
|
#[test]
|
|
fn frozen_execution_taxonomy_is_closed_and_consistent() {
|
|
let cases = [
|
|
(
|
|
ExecutionErrorCode::AuthorizationDenied,
|
|
ExecutionStage::Authorization,
|
|
Retryability::Never,
|
|
OutcomeCertainty::Certain,
|
|
),
|
|
(
|
|
ExecutionErrorCode::InputSchemaInvalid,
|
|
ExecutionStage::InputSchema,
|
|
Retryability::Never,
|
|
OutcomeCertainty::Certain,
|
|
),
|
|
(
|
|
ExecutionErrorCode::ExecutionOverloaded,
|
|
ExecutionStage::Admission,
|
|
Retryability::AfterDelay,
|
|
OutcomeCertainty::Certain,
|
|
),
|
|
(
|
|
ExecutionErrorCode::ConfirmationRequired,
|
|
ExecutionStage::Admission,
|
|
Retryability::RequiresConfirmation,
|
|
OutcomeCertainty::Certain,
|
|
),
|
|
(
|
|
ExecutionErrorCode::IdempotencyOutcomeUnknown,
|
|
ExecutionStage::Admission,
|
|
Retryability::ManualReconcile,
|
|
OutcomeCertainty::OutcomeUnknown,
|
|
),
|
|
(
|
|
ExecutionErrorCode::RuntimeInternal,
|
|
ExecutionStage::Runtime,
|
|
Retryability::Never,
|
|
OutcomeCertainty::Certain,
|
|
),
|
|
];
|
|
|
|
for (code, stage, retryability, certainty) in cases {
|
|
let descriptor = ExecutionFailure::new(code, CorrelationContext::generate());
|
|
assert_eq!(descriptor.stage(), stage);
|
|
assert_eq!(descriptor.retryability(), retryability);
|
|
assert_eq!(descriptor.outcome_certainty(), certainty);
|
|
assert_eq!(descriptor.error_code(), code);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn execution_origin_enforces_agent_scope() {
|
|
let agent_id = AgentId::new("agent_01");
|
|
assert!(ExecutionOrigin::AdminDraft.validate_agent(None).is_ok());
|
|
assert!(
|
|
ExecutionOrigin::AdminDraft
|
|
.validate_agent(Some(&agent_id))
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
ExecutionOrigin::AgentSnapshot
|
|
.validate_agent(Some(&agent_id))
|
|
.is_ok()
|
|
);
|
|
assert!(ExecutionOrigin::AgentSnapshot.validate_agent(None).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn dispatch_uncertainty_is_allowed_only_for_ambiguous_codes() {
|
|
let correlation = CorrelationContext::generate();
|
|
let timeout = ExecutionFailure::new(ExecutionErrorCode::UpstreamTimeout, correlation.clone())
|
|
.with_dispatch_uncertainty();
|
|
assert_eq!(timeout.retryability(), Retryability::ManualReconcile);
|
|
assert_eq!(
|
|
timeout.outcome_certainty(),
|
|
OutcomeCertainty::OutcomeUnknown
|
|
);
|
|
|
|
let validation = ExecutionFailure::new(ExecutionErrorCode::InputSchemaInvalid, correlation)
|
|
.with_dispatch_uncertainty();
|
|
assert_eq!(validation.retryability(), Retryability::Never);
|
|
assert_eq!(validation.outcome_certainty(), OutcomeCertainty::Certain);
|
|
}
|
|
|
|
#[test]
|
|
fn failure_metadata_is_code_specific_and_bounded() {
|
|
let correlation = CorrelationContext::generate();
|
|
assert!(
|
|
ExecutionFailure::new(ExecutionErrorCode::InputSchemaInvalid, correlation.clone())
|
|
.try_with_retry_after_ms(1)
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
ExecutionFailure::new(ExecutionErrorCode::UpstreamRateLimited, correlation.clone())
|
|
.try_with_retry_after_ms(3_600_001)
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
ExecutionFailure::new(ExecutionErrorCode::RuntimeInternal, correlation.clone())
|
|
.try_with_confirmation("token", 1)
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
ExecutionFailure::new(
|
|
ExecutionErrorCode::ConfirmationRequired,
|
|
correlation.clone()
|
|
)
|
|
.try_with_confirmation("x".repeat(4_097), 1)
|
|
.is_err()
|
|
);
|
|
let challenge = ExecutionFailure::new(ExecutionErrorCode::ConfirmationRequired, correlation)
|
|
.try_with_confirmation("secret-canary", 1_000)
|
|
.expect("bounded confirmation");
|
|
assert!(!format!("{challenge:?}").contains("secret-canary"));
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_taxonomy_values_are_rejected_by_serde() {
|
|
assert!(serde_json::from_str::<ExecutionStage>("\"unknown\"").is_err());
|
|
assert!(serde_json::from_str::<ExecutionErrorCode>("\"unknown\"").is_err());
|
|
assert!(serde_json::from_str::<Retryability>("\"automatic\"").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn every_published_code_has_stable_wire_value_stage_and_localized_messages() {
|
|
let mut wire_values = std::collections::BTreeSet::new();
|
|
for code in ExecutionErrorCode::ALL {
|
|
assert!(wire_values.insert(code.as_str()));
|
|
assert_eq!(serde_json::to_value(code).unwrap(), code.as_str());
|
|
assert!(!code.message(crank_core::ExecutionLocale::Ru).is_empty());
|
|
assert!(!code.message(crank_core::ExecutionLocale::En).is_empty());
|
|
assert!(!code.stage().as_str().is_empty());
|
|
assert!(!code.retryability().as_str().is_empty());
|
|
assert!(!code.outcome_certainty().as_str().is_empty());
|
|
}
|
|
assert_eq!(wire_values.len(), ExecutionErrorCode::ALL.len());
|
|
}
|