use super::{ServerError, StreamFailure};
use aion::{EngineError, QueryError, engine_seam::EngineSeamError};
use aion_core::WorkflowId;
use aion_proto::WireErrorCode;
fn assert_send_sync<T: Send + Sync>() {}
#[test]
fn server_error_is_send_sync() {
assert_send_sync::<ServerError>();
}
#[test]
fn lagged_stream_maps_to_wire_lagged() {
let error = ServerError::Stream {
failure: StreamFailure::Lagged,
};
assert_eq!(error.to_wire_error().code, WireErrorCode::Lagged);
}
#[test]
fn missing_event_outcome_invariant_maps_to_typed_backend() {
let error = ServerError::EngineCall {
source: EngineError::ProcessExitOutcomeMissingAfterEvent { process_id: 23 },
};
let wire = error.to_wire_error();
assert_eq!(wire.code, WireErrorCode::Backend);
assert_eq!(
wire.error_type.as_deref(),
Some("ProcessExitOutcomeMissingAfterEvent")
);
assert_eq!(
error.trace_fields().error_type,
"ProcessExitOutcomeMissingAfterEvent"
);
}
#[test]
fn activity_delivery_poison_maps_to_typed_backend() {
let error = ServerError::EngineCall {
source: EngineError::ActivityDeliveryPoisoned { process_id: 17 },
};
let wire = error.to_wire_error();
assert_eq!(wire.code, WireErrorCode::Backend);
assert_eq!(wire.error_type.as_deref(), Some("ActivityDeliveryPoisoned"));
assert_eq!(error.trace_fields().error_type, "ActivityDeliveryPoisoned");
}
#[test]
fn not_owner_store_error_maps_to_wire_not_owner() {
let error = ServerError::StoreBackend {
source: aion_store::StoreError::NotOwner { shard: 3 },
};
let wire = error.to_wire_error();
assert_eq!(wire.code, WireErrorCode::NotOwner);
assert_eq!(wire.error_type.as_deref(), Some("NotOwner"));
}
#[test]
fn run_not_in_history_maps_to_typed_backend_on_both_halves() {
let run_id = aion_core::RunId::new(uuid::Uuid::from_u128(94));
let error = ServerError::EngineCall {
source: EngineError::RunNotInHistory {
workflow_id: workflow_id(),
run_id: run_id.clone(),
},
};
let wire = error.to_wire_error();
assert_eq!(
wire.code,
WireErrorCode::Backend,
"an engine-internal disagreement must not be classed as the caller's fault"
);
assert_eq!(wire.error_type.as_deref(), Some("RunNotInHistory"));
assert_eq!(error.trace_fields().error_type, "RunNotInHistory");
assert!(
wire.message.contains(&run_id.to_string()),
"the wire message dropped the run the refusal is about: {}",
wire.message
);
}
fn workflow_id() -> WorkflowId {
WorkflowId::new(uuid::Uuid::from_u128(7))
}
fn query_wire(query: QueryError) -> aion_proto::WireError {
ServerError::EngineCall {
source: EngineError::Query(query),
}
.to_wire_error()
}
#[test]
fn every_query_error_arm_maps_to_its_pinned_wire_code() {
let arms: Vec<(QueryError, WireErrorCode, Option<&str>)> = vec![
(
QueryError::UnknownQuery(String::from("state")),
WireErrorCode::UnknownQuery,
None,
),
(QueryError::Timeout, WireErrorCode::QueryTimeout, None),
(
QueryError::NotRunning(workflow_id()),
WireErrorCode::NotRunning,
Some("QueryNotRunning"),
),
(
QueryError::Unknown(workflow_id()),
WireErrorCode::NotFound,
Some("QueryUnknownWorkflow"),
),
(
QueryError::ReplyDropped,
WireErrorCode::NotRunning,
Some("QueryReplyDropped"),
),
(
QueryError::HandlerFailed {
message: String::from("handler raised"),
},
WireErrorCode::QueryFailed,
Some("QueryFailed"),
),
(
QueryError::InvalidArguments {
reason: String::from("arguments payload is not a well-formed JSON document"),
},
WireErrorCode::InvalidInput,
Some("QueryInvalidArguments"),
),
(
QueryError::Engine(EngineSeamError::Delivery {
reason: String::from("mailbox closed"),
}),
WireErrorCode::Backend,
Some("QueryEngine"),
),
];
let variant_count = arms
.iter()
.map(|(query, _, _)| match query {
QueryError::UnknownQuery(_) => 0,
QueryError::Timeout => 1,
QueryError::NotRunning(_) => 2,
QueryError::Unknown(_) => 3,
QueryError::ReplyDropped => 4,
QueryError::HandlerFailed { .. } => 5,
QueryError::InvalidArguments { .. } => 6,
QueryError::Engine(_) => 7,
})
.collect::<std::collections::BTreeSet<usize>>()
.len();
assert_eq!(
arms.len(),
variant_count,
"every QueryError variant must appear exactly once in the pin list",
);
assert_eq!(variant_count, 8, "pin list must cover all 8 variants");
for (query, expected_code, expected_type) in arms {
let wire = query_wire(query.clone());
assert_eq!(
wire.code, expected_code,
"{query:?} must map to {expected_code:?}",
);
assert_eq!(
wire.error_type.as_deref(),
expected_type,
"{query:?} must carry error_type {expected_type:?}",
);
}
}
#[test]
fn engine_task_epoch_closed_maps_to_typed_backend_on_both_halves() {
let error = ServerError::EngineCall {
source: EngineError::EngineTaskEpochClosed {
workflow_id: String::from("orders"),
run_id: String::from("019000ff-0000-7000-8000-000000000001"),
},
};
let wire = error.to_wire_error();
assert_eq!(
wire.code,
WireErrorCode::Backend,
"an epoch-closed refusal must not claim the run stopped running"
);
assert_eq!(wire.error_type.as_deref(), Some("EngineTaskEpochClosed"));
assert_eq!(error.trace_fields().error_type, "EngineTaskEpochClosed");
assert!(
wire.message.contains("orders"),
"the wire message dropped the workflow the refusal is about: {}",
wire.message
);
}
#[test]
fn durability_epoch_closed_maps_to_the_same_name_on_both_halves() {
use aion::durability::DurabilityError;
let error = ServerError::EngineCall {
source: EngineError::Durability(DurabilityError::EngineTaskEpochClosed {
reason: String::from(
"continue_as_new refused for run 019000ff-0000-7000-8000-000000000002",
),
}),
};
let wire = error.to_wire_error();
assert_eq!(
wire.code,
WireErrorCode::Backend,
"a durability-level epoch refusal leaves the run Running, so it must not be classified \
as NotRunning"
);
assert_eq!(
wire.error_type.as_deref(),
Some("EngineTaskEpochClosed"),
"the wire half must carry the SAME discriminator as the engine-level variant — a caller \
branching on the condition cannot branch on two names for it"
);
assert_eq!(
error.trace_fields().error_type,
"EngineTaskEpochClosed",
"the trace half must carry it too, and must not fall back to the generic `Durability` \
label — an operator searching for one engine's shutdown would miss this seam entirely"
);
let other = ServerError::EngineCall {
source: EngineError::Durability(DurabilityError::HistoryShape {
reason: String::from("history shape"),
}),
};
assert_eq!(
other.trace_fields().error_type,
"Durability",
"control: an ordinary durability fault keeps the generic label, so the epoch's own label \
above is attributable to the epoch and not to the family"
);
assert!(
wire.message
.contains("019000ff-0000-7000-8000-000000000002"),
"the wire message dropped the run the refusal is about: {}",
wire.message
);
}
#[test]
fn handler_failed_trace_fields_use_query_failed_type() {
let error = ServerError::EngineCall {
source: EngineError::Query(QueryError::HandlerFailed {
message: String::from("handler raised"),
}),
};
assert_eq!(error.trace_fields().error_type, "QueryFailed");
}