use std::collections::BTreeMap;
use axum::http::{HeaderMap, HeaderValue};
use boardwalk::runtime::{
Actor, ActorCtx, DynFuture, RequestCtx, Resource, ResourceCtx, ResourceError, TransitionCtx,
TransitionError,
};
use boardwalk::{ResourceSnapshot, ResourceSpec, TransitionInput, TransitionOutcome};
#[test]
fn request_ctx_extracts_traceparent_tracestate_and_x_request_id() {
let mut headers = HeaderMap::new();
headers.insert(
"traceparent",
HeaderValue::from_static("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"),
);
headers.insert(
"tracestate",
HeaderValue::from_static("rojo=00f067aa0ba902b7"),
);
headers.insert("x-request-id", HeaderValue::from_static("req-123"));
let ctx = RequestCtx::from_headers(&headers);
assert_eq!(
ctx.traceparent(),
Some("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")
);
assert_eq!(ctx.tracestate(), Some("rojo=00f067aa0ba902b7"));
assert_eq!(ctx.request_id(), Some("req-123"));
}
#[test]
fn request_ctx_handles_missing_headers() {
let headers = HeaderMap::new();
let ctx = RequestCtx::from_headers(&headers);
assert!(ctx.traceparent().is_none());
assert!(ctx.tracestate().is_none());
assert!(ctx.request_id().is_none());
}
#[test]
fn transition_ctx_allocates_command_id_and_sets_causation() {
let req = RequestCtx::default();
let a = TransitionCtx::new(req.clone(), "hub");
let b = TransitionCtx::new(req, "hub");
let a_id = a.command_id().clone();
let b_id = b.command_id().clone();
assert_ne!(a_id, b_id, "each context mints a fresh command id");
let s = a_id.as_str().to_owned();
assert!(!s.is_empty());
}
#[test]
fn actor_ctx_contains_node_and_resource_identity() {
let mut labels = BTreeMap::new();
labels.insert("owner".into(), "platform".into());
let ctx = ActorCtx::new("hub", "job-1", "job", labels.clone());
assert_eq!(ctx.node(), "hub");
assert_eq!(ctx.resource_id(), "job-1");
assert_eq!(ctx.resource_kind(), "job");
assert_eq!(ctx.labels(), &labels);
}
struct StubActor;
impl Resource for StubActor {
fn spec(&self) -> ResourceSpec {
ResourceSpec {
kind: "job".into(),
name: None,
labels: BTreeMap::new(),
property_schema: None,
streams: vec![],
}
}
fn snapshot<'a>(
&'a self,
_ctx: ResourceCtx,
) -> DynFuture<'a, Result<ResourceSnapshot, ResourceError>> {
Box::pin(
async move { Err::<ResourceSnapshot, _>(ResourceError::Unavailable("stub".into())) },
)
}
}
impl Actor for StubActor {
fn transition<'a>(
&'a mut self,
_ctx: TransitionCtx,
_name: &'a str,
_input: TransitionInput,
) -> DynFuture<'a, Result<TransitionOutcome, TransitionError>> {
Box::pin(
async move { Err::<TransitionOutcome, _>(TransitionError::NotAllowed("stub".into())) },
)
}
}
#[test]
fn transition_ctx_exposes_resource_registration_service_signature() {
let ctx = TransitionCtx::new(RequestCtx::default(), "hub");
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
let result: Result<String, TransitionError> = ctx.register_actor(StubActor).await;
match result {
Err(TransitionError::Internal(_)) => {}
Ok(_) => panic!("test stub should not return Ok yet"),
Err(other) => panic!("expected Internal stub error, got {other:?}"),
}
});
}