use std::collections::{BTreeMap, HashSet};
use lifeloop::router::{
BuiltinAdapterRegistry, LifeloopFailureMapper, ReceiptError, RouteError, TransportError,
classes_for_negotiation_outcome, failure_class_for_receipt_error,
failure_class_for_route_error, failure_class_for_transport, retry_class_for, route,
validate_receipt_eligible,
};
use lifeloop::{
AdapterManifest, AdapterRole, CallbackRequest, ConformanceLevel, FailureClass, FrameClass,
FrameContext, IntegrationMode, LifecycleEventKind, ManifestContextPressure,
ManifestPlacementClass, ManifestPlacementSupport, ManifestReceipts, NegotiationOutcome,
RetryClass, SCHEMA_VERSION, SupportState,
router::{AdapterRegistry, AdapterResolution, FailureMapper},
};
const FAKE_ADAPTER_ID: &str = "fake-adapter";
const FAKE_ADAPTER_VERSION: &str = "0.0.1";
fn fake_manifest() -> AdapterManifest {
AdapterManifest {
contract_version: SCHEMA_VERSION.to_string(),
adapter_id: FAKE_ADAPTER_ID.into(),
adapter_version: FAKE_ADAPTER_VERSION.into(),
display_name: "Fake".into(),
role: AdapterRole::PrimaryWorker,
integration_modes: vec![IntegrationMode::NativeHook],
lifecycle_events: BTreeMap::new(),
placement: BTreeMap::from([(
ManifestPlacementClass::PreSession,
ManifestPlacementSupport {
support: SupportState::Native,
max_bytes: None,
},
)]),
context_pressure: ManifestContextPressure {
support: SupportState::Unavailable,
evidence: None,
},
receipts: ManifestReceipts {
native: false,
lifeloop_synthesized: true,
receipt_ledger: SupportState::Unavailable,
},
session_identity: None,
session_rename: None,
renewal: None,
approval_surface: None,
failure_modes: Vec::new(),
telemetry_sources: Vec::new(),
known_degradations: Vec::new(),
}
}
struct FakeRegistry {
manifest: AdapterManifest,
}
impl FakeRegistry {
fn new() -> Self {
Self {
manifest: fake_manifest(),
}
}
}
impl AdapterRegistry for FakeRegistry {
fn resolve(&self, adapter_id: &str, adapter_version: &str) -> AdapterResolution {
if adapter_id != self.manifest.adapter_id {
return AdapterResolution::UnknownId;
}
if adapter_version != self.manifest.adapter_version {
return AdapterResolution::VersionMismatch {
registered_version: self.manifest.adapter_version.clone(),
};
}
AdapterResolution::Found(lifeloop::RegisteredAdapter {
manifest: self.manifest.clone(),
conformance: ConformanceLevel::PreConformance,
})
}
}
fn valid_request() -> CallbackRequest {
CallbackRequest {
schema_version: SCHEMA_VERSION.to_string(),
event: LifecycleEventKind::SessionStarted,
event_id: "evt-1".into(),
adapter_id: FAKE_ADAPTER_ID.into(),
adapter_version: FAKE_ADAPTER_VERSION.into(),
integration_mode: IntegrationMode::NativeHook,
invocation_id: "inv-1".into(),
harness_session_id: Some("sess-1".into()),
harness_run_id: None,
harness_task_id: None,
frame_context: None,
capability_snapshot_ref: None,
payload_refs: Vec::new(),
sequence: None,
idempotency_key: None,
metadata: serde_json::Map::new(),
}
}
fn sample_for(target: FailureClass) -> FailureClass {
let mapper = LifeloopFailureMapper::new();
match target {
FailureClass::AdapterUnavailable => {
mapper
.map_route_error(&RouteError::AdapterIdNotFound {
adapter_id: "ghost".into(),
})
.0
}
FailureClass::CapabilityUnsupported => {
classes_for_negotiation_outcome(NegotiationOutcome::Unsupported, None)
.expect("unsupported pair")
.0
}
FailureClass::CapabilityDegraded => {
classes_for_negotiation_outcome(
NegotiationOutcome::Unsupported,
Some(FailureClass::CapabilityDegraded),
)
.expect("override")
.0
}
FailureClass::PlacementUnavailable => {
classes_for_negotiation_outcome(
NegotiationOutcome::Unsupported,
Some(FailureClass::PlacementUnavailable),
)
.expect("override")
.0
}
FailureClass::PayloadTooLarge => {
classes_for_negotiation_outcome(
NegotiationOutcome::Unsupported,
Some(FailureClass::PayloadTooLarge),
)
.expect("override")
.0
}
FailureClass::PayloadRejected => {
classes_for_negotiation_outcome(
NegotiationOutcome::Unsupported,
Some(FailureClass::PayloadRejected),
)
.expect("override")
.0
}
FailureClass::IdentityUnavailable => {
classes_for_negotiation_outcome(
NegotiationOutcome::Unsupported,
Some(FailureClass::IdentityUnavailable),
)
.expect("override")
.0
}
FailureClass::TransportError => {
mapper
.map_transport_error(&TransportError::Io("connection refused".into()))
.0
}
FailureClass::Timeout => mapper.map_transport_error(&TransportError::Timeout).0,
FailureClass::OperatorRequired => {
classes_for_negotiation_outcome(NegotiationOutcome::RequiresOperator, None)
.expect("operator-required pair")
.0
}
FailureClass::StateConflict => {
mapper
.map_receipt_error(&ReceiptError::Conflict {
idempotency_key: "k".into(),
})
.0
}
FailureClass::InvalidRequest => {
mapper
.map_route_error(&RouteError::InvalidFrameContext {
detail: "missing".into(),
})
.0
}
FailureClass::InternalError => {
mapper
.map_transport_error(&TransportError::Internal("panic".into()))
.0
}
}
}
#[test]
fn every_failure_class_variant_is_covered_by_mapper() {
let mut seen = HashSet::new();
for &fc in FailureClass::ALL {
let observed = sample_for(fc);
assert_eq!(observed, fc, "sample_for({fc:?}) returned {observed:?}");
seen.insert(fc);
}
assert_eq!(
seen.len(),
FailureClass::ALL.len(),
"FailureClass coverage incomplete: {seen:?}"
);
}
#[test]
fn every_retry_class_variant_is_the_default_for_some_failure_class() {
let mut seen: HashSet<RetryClass> = HashSet::new();
for &fc in FailureClass::ALL {
seen.insert(retry_class_for(fc));
}
for &rc in RetryClass::ALL {
assert!(
seen.contains(&rc),
"RetryClass::{rc:?} is not produced by any FailureClass::default_retry()"
);
}
}
#[test]
fn operator_required_uses_operator_required_failure_and_retry_after_operator() {
let pair = classes_for_negotiation_outcome(NegotiationOutcome::RequiresOperator, None);
assert_eq!(
pair,
Some((
FailureClass::OperatorRequired,
RetryClass::RetryAfterOperator
))
);
let pair_with_override = classes_for_negotiation_outcome(
NegotiationOutcome::RequiresOperator,
Some(FailureClass::PayloadTooLarge),
);
assert_eq!(
pair_with_override,
Some((
FailureClass::OperatorRequired,
RetryClass::RetryAfterOperator
)),
"RequiresOperator must always pair OperatorRequired + RetryAfterOperator"
);
}
#[test]
fn validate_receipt_eligible_rejects_receipt_emitted_plan() {
let reg = FakeRegistry::new();
let mut req = valid_request();
req.event = LifecycleEventKind::ReceiptEmitted;
req.frame_context = None;
req.idempotency_key = None; let plan = route(&req, ®).expect("plan synthesizes");
let err = validate_receipt_eligible(&plan).expect_err("must reject");
assert!(
matches!(err, RouteError::InvalidEventEnvelope { .. }),
"expected InvalidEventEnvelope, got {err:?}"
);
let mapper = LifeloopFailureMapper::new();
let (fc, rc) = mapper.map_route_error(&err);
assert_eq!(fc, FailureClass::InvalidRequest);
assert_eq!(rc, RetryClass::DoNotRetry);
}
#[test]
fn validate_receipt_eligible_accepts_non_receipt_emitted_events() {
let reg = FakeRegistry::new();
let req = valid_request();
let plan = route(&req, ®).expect("plan");
validate_receipt_eligible(&plan).expect("non-notification event passes");
}
#[test]
fn empty_sentinel_strings_are_rejected_and_classify_as_invalid_request() {
let reg = FakeRegistry::new();
type Mutator = Box<dyn Fn(&mut CallbackRequest)>;
let cases: Vec<(Mutator, &'static str)> = vec![
(Box::new(|r| r.event_id = String::new()), "request.event_id"),
(
Box::new(|r| r.adapter_id = String::new()),
"request.adapter_id",
),
(
Box::new(|r| r.adapter_version = String::new()),
"request.adapter_version",
),
(
Box::new(|r| r.invocation_id = String::new()),
"request.invocation_id",
),
(
Box::new(|r| r.harness_session_id = Some(String::new())),
"request.harness_session_id",
),
(
Box::new(|r| r.harness_run_id = Some(String::new())),
"request.harness_run_id",
),
(
Box::new(|r| r.harness_task_id = Some(String::new())),
"request.harness_task_id",
),
(
Box::new(|r| r.capability_snapshot_ref = Some(String::new())),
"request.capability_snapshot_ref",
),
(
Box::new(|r| r.idempotency_key = Some(String::new())),
"request.idempotency_key",
),
];
let mapper = LifeloopFailureMapper::new();
for (mutate, expected_field) in cases {
let mut req = valid_request();
mutate(&mut req);
let err = route(&req, ®).unwrap_err();
match &err {
RouteError::EmptySentinel { field } => assert_eq!(*field, expected_field),
other => panic!("expected EmptySentinel for {expected_field}, got {other:?}"),
}
assert_eq!(mapper.map_route_error(&err).0, FailureClass::InvalidRequest);
}
}
#[test]
fn partial_frame_context_is_rejected_and_classifies_as_invalid_request() {
let reg = FakeRegistry::new();
let mapper = LifeloopFailureMapper::new();
let mut req = valid_request();
req.event = LifecycleEventKind::FrameOpening;
req.frame_context = Some(FrameContext {
frame_id: "f-1".into(),
parent_frame_id: Some("p-1".into()),
frame_class: FrameClass::TopLevel,
});
let err = route(&req, ®).unwrap_err();
assert!(matches!(err, RouteError::InvalidFrameContext { .. }));
assert_eq!(mapper.map_route_error(&err).0, FailureClass::InvalidRequest);
let mut req = valid_request();
req.event = LifecycleEventKind::FrameOpening;
req.frame_context = Some(FrameContext {
frame_id: "f-1".into(),
parent_frame_id: None,
frame_class: FrameClass::Subcall,
});
let err = route(&req, ®).unwrap_err();
assert!(matches!(err, RouteError::InvalidFrameContext { .. }));
assert_eq!(mapper.map_route_error(&err).0, FailureClass::InvalidRequest);
let mut req = valid_request();
req.event = LifecycleEventKind::FrameOpening;
req.frame_context = Some(FrameContext {
frame_id: String::new(),
parent_frame_id: None,
frame_class: FrameClass::TopLevel,
});
let err = route(&req, ®).unwrap_err();
assert!(matches!(err, RouteError::InvalidFrameContext { .. }));
assert_eq!(mapper.map_route_error(&err).0, FailureClass::InvalidRequest);
let mut req = valid_request();
req.event = LifecycleEventKind::FrameOpening;
req.frame_context = Some(FrameContext {
frame_id: "f-1".into(),
parent_frame_id: Some(String::new()),
frame_class: FrameClass::Subcall,
});
let err = route(&req, ®).unwrap_err();
assert!(matches!(err, RouteError::InvalidFrameContext { .. }));
assert_eq!(mapper.map_route_error(&err).0, FailureClass::InvalidRequest);
let mut req = valid_request();
req.event = LifecycleEventKind::FrameEnded;
req.frame_context = None;
let err = route(&req, ®).unwrap_err();
assert!(matches!(err, RouteError::InvalidFrameContext { .. }));
}
#[test]
fn unknown_event_wire_name_rejected_at_deserialize_boundary() {
let raw = serde_json::json!({
"schema_version": SCHEMA_VERSION,
"event": "session.bogus",
"event_id": "e",
"adapter_id": FAKE_ADAPTER_ID,
"adapter_version": FAKE_ADAPTER_VERSION,
"integration_mode": "native_hook",
"invocation_id": "i",
});
let res: Result<CallbackRequest, _> = serde_json::from_value(raw);
assert!(res.is_err(), "deserialize must reject unknown event name");
}
#[test]
fn unknown_receipt_status_rejected_at_deserialize_boundary() {
let raw = serde_json::json!({
"schema_version": SCHEMA_VERSION,
"status": "vibes",
"client_payloads": [],
});
let res: Result<lifeloop::CallbackResponse, _> = serde_json::from_value(raw);
assert!(
res.is_err(),
"deserialize must reject unknown receipt status"
);
}
#[test]
fn invalid_payload_ref_classifies_as_invalid_request() {
let reg = FakeRegistry::new();
let mapper = LifeloopFailureMapper::new();
let mut req = valid_request();
req.payload_refs = vec![lifeloop::PayloadRef {
payload_id: String::new(),
payload_kind: "k".into(),
content_digest: None,
byte_size: None,
}];
let err = route(&req, ®).unwrap_err();
assert!(matches!(err, RouteError::InvalidPayloadRef { .. }));
assert_eq!(mapper.map_route_error(&err).0, FailureClass::InvalidRequest);
}
#[test]
fn adapter_version_mismatch_against_builtin_classifies_as_adapter_unavailable() {
let req = CallbackRequest {
adapter_id: "codex".into(),
adapter_version: "9.9.9".into(),
..valid_request()
};
let err = route(&req, &BuiltinAdapterRegistry).unwrap_err();
assert!(matches!(err, RouteError::AdapterVersionMismatch { .. }));
let mapper = LifeloopFailureMapper::new();
assert_eq!(
mapper.map_route_error(&err).0,
FailureClass::AdapterUnavailable
);
}
fn codex_raw_to_shared(raw: &str) -> FailureClass {
if raw.contains("ETIMEDOUT") || raw.contains("deadline_exceeded") {
FailureClass::Timeout
} else if raw.contains("ECONNREFUSED") || raw.contains("EPIPE") || raw.contains("broken_pipe") {
FailureClass::TransportError
} else if raw.contains("payload_too_large") || raw.contains("413 Payload Too Large") {
FailureClass::PayloadTooLarge
} else if raw.contains("invalid_request") || raw.contains("400 Bad Request") {
FailureClass::InvalidRequest
} else {
FailureClass::InternalError
}
}
fn claude_raw_to_shared(raw: &str) -> FailureClass {
if raw.contains("rate_limit_error") || raw.contains("overloaded_error") {
FailureClass::TransportError
} else if raw.contains("invalid_request_error") {
FailureClass::InvalidRequest
} else if raw.contains("not_found_error") {
FailureClass::AdapterUnavailable
} else if raw.contains("api_timeout") {
FailureClass::Timeout
} else if raw.contains("permission_error") {
FailureClass::OperatorRequired
} else {
FailureClass::InternalError
}
}
#[test]
fn codex_adapter_fixture_maps_raw_strings_to_shared_classes() {
assert_eq!(codex_raw_to_shared("ETIMEDOUT"), FailureClass::Timeout);
assert_eq!(
codex_raw_to_shared("ECONNREFUSED upstream"),
FailureClass::TransportError
);
assert_eq!(
codex_raw_to_shared("413 Payload Too Large"),
FailureClass::PayloadTooLarge
);
assert_eq!(
codex_raw_to_shared("400 Bad Request: invalid_request"),
FailureClass::InvalidRequest
);
assert_eq!(
codex_raw_to_shared("kernel panic"),
FailureClass::InternalError
);
assert_eq!(
retry_class_for(codex_raw_to_shared("ETIMEDOUT")),
RetryClass::SafeRetry
);
}
#[test]
fn claude_adapter_fixture_maps_raw_strings_to_shared_classes() {
assert_eq!(
claude_raw_to_shared("rate_limit_error"),
FailureClass::TransportError
);
assert_eq!(
claude_raw_to_shared("overloaded_error"),
FailureClass::TransportError
);
assert_eq!(
claude_raw_to_shared("invalid_request_error: missing field"),
FailureClass::InvalidRequest
);
assert_eq!(
claude_raw_to_shared("not_found_error: model unknown"),
FailureClass::AdapterUnavailable
);
assert_eq!(claude_raw_to_shared("api_timeout"), FailureClass::Timeout);
assert_eq!(
claude_raw_to_shared("permission_error: operator must approve"),
FailureClass::OperatorRequired
);
assert_eq!(
retry_class_for(claude_raw_to_shared("permission_error")),
RetryClass::RetryAfterOperator
);
}
#[test]
fn from_impls_match_free_helpers() {
let r = RouteError::SchemaVersionMismatch {
expected: "a".into(),
found: "b".into(),
};
let via_helper = failure_class_for_route_error(&r);
let via_from: FailureClass = (&r).into();
assert_eq!(via_helper, via_from);
let rc = ReceiptError::Conflict {
idempotency_key: "k".into(),
};
let via_helper = failure_class_for_receipt_error(&rc);
let via_from: FailureClass = (&rc).into();
assert_eq!(via_helper, via_from);
let t = TransportError::Timeout;
let via_helper = failure_class_for_transport(&t);
let via_from: FailureClass = (&t).into();
assert_eq!(via_helper, via_from);
}
#[test]
fn transport_error_display_strings_include_variant_context() {
assert_eq!(
TransportError::Io("EPIPE".into()).to_string(),
"transport io error: EPIPE"
);
assert_eq!(TransportError::Timeout.to_string(), "transport timeout");
assert_eq!(
TransportError::Internal("panic".into()).to_string(),
"transport internal error: panic"
);
}