use lifeloop::{
AcceptablePlacement, AdapterManifest, AdapterRole, CallbackRequest, CallbackResponse,
CapabilityDegradation, DispatchEnvelope, FailureClass, FrameClass, FrameContext,
IntegrationMode, LifecycleEventKind, LifecycleReceipt, ManifestContextPressure,
ManifestLifecycleEventSupport, ManifestPlacementClass, ManifestPlacementSupport,
ManifestReceipts, ManifestRenewal, ManifestRenewalContinuation, ManifestRenewalReset,
ManifestSessionIdentity, NegotiationOutcome, PayloadEnvelope, PayloadReceipt, PayloadRef,
PlacementClass, PlacementOutcome, ReceiptStatus, RequirementLevel, RetryClass, SCHEMA_VERSION,
SupportState, ValidationError, Warning,
};
use serde_json::json;
use std::collections::BTreeMap;
#[test]
fn schema_version_is_pinned() {
assert_eq!(SCHEMA_VERSION, "lifeloop.v0.2");
}
fn roundtrip_enum<T>(cases: &[(T, &str)], all: &[T])
where
T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + PartialEq + Copy,
{
assert_eq!(
cases.len(),
all.len(),
"case table size {} but ::ALL has {} variants — adding a variant requires a roundtrip case",
cases.len(),
all.len()
);
for variant in all {
assert!(
cases.iter().any(|(c, _)| c == variant),
"variant {variant:?} appears in ::ALL but not in roundtrip cases",
);
}
for (variant, wire) in cases {
let serialized = serde_json::to_value(variant).unwrap();
assert_eq!(
serialized,
json!(wire),
"serialize wire mismatch for {wire}"
);
let parsed: T = serde_json::from_value(json!(wire)).unwrap();
assert_eq!(&parsed, variant, "deserialize wire mismatch for {wire}");
}
}
#[test]
fn integration_mode_serializes_and_roundtrips() {
roundtrip_enum(
&[
(IntegrationMode::ManualSkill, "manual_skill"),
(IntegrationMode::LauncherWrapper, "launcher_wrapper"),
(IntegrationMode::NativeHook, "native_hook"),
(IntegrationMode::ReferenceAdapter, "reference_adapter"),
(IntegrationMode::TelemetryOnly, "telemetry_only"),
],
IntegrationMode::ALL,
);
assert_eq!(IntegrationMode::ALL.len(), 5);
}
#[test]
fn support_state_serializes_and_roundtrips() {
roundtrip_enum(
&[
(SupportState::Native, "native"),
(SupportState::Synthesized, "synthesized"),
(SupportState::Manual, "manual"),
(SupportState::Partial, "partial"),
(SupportState::Unavailable, "unavailable"),
],
SupportState::ALL,
);
assert_eq!(SupportState::ALL.len(), 5);
}
#[test]
fn adapter_role_serializes_and_roundtrips() {
roundtrip_enum(
&[
(AdapterRole::PrimaryWorker, "primary_worker"),
(AdapterRole::Worker, "worker"),
(AdapterRole::Supervisor, "supervisor"),
(AdapterRole::Observer, "observer"),
],
AdapterRole::ALL,
);
assert_eq!(AdapterRole::ALL.len(), 4);
}
#[test]
fn lifecycle_event_kind_pins_dotted_names_and_roundtrips() {
roundtrip_enum(
&[
(LifecycleEventKind::SessionStarting, "session.starting"),
(LifecycleEventKind::SessionStarted, "session.started"),
(LifecycleEventKind::FrameOpening, "frame.opening"),
(LifecycleEventKind::FrameOpened, "frame.opened"),
(
LifecycleEventKind::ContextPressureObserved,
"context.pressure_observed",
),
(LifecycleEventKind::ContextCompacted, "context.compacted"),
(LifecycleEventKind::FrameEnding, "frame.ending"),
(LifecycleEventKind::FrameEnded, "frame.ended"),
(LifecycleEventKind::SessionEnding, "session.ending"),
(LifecycleEventKind::SessionEnded, "session.ended"),
(LifecycleEventKind::SupervisorTick, "supervisor.tick"),
(
LifecycleEventKind::CapabilityDegraded,
"capability.degraded",
),
(LifecycleEventKind::ReceiptEmitted, "receipt.emitted"),
(
LifecycleEventKind::ReceiptGapDetected,
"receipt.gap_detected",
),
],
LifecycleEventKind::ALL,
);
assert_eq!(LifecycleEventKind::ALL.len(), 14);
}
#[test]
fn lifecycle_event_kinds_helper_matches_canonical_table() {
assert_eq!(lifeloop::lifecycle_event_kinds(), LifecycleEventKind::ALL);
}
#[test]
fn receipt_status_serializes_and_roundtrips() {
roundtrip_enum(
&[
(ReceiptStatus::Observed, "observed"),
(ReceiptStatus::Delivered, "delivered"),
(ReceiptStatus::Skipped, "skipped"),
(ReceiptStatus::Degraded, "degraded"),
(ReceiptStatus::Failed, "failed"),
],
ReceiptStatus::ALL,
);
assert_eq!(ReceiptStatus::ALL.len(), 5);
}
#[test]
fn failure_class_serializes_and_roundtrips() {
roundtrip_enum(
&[
(FailureClass::AdapterUnavailable, "adapter_unavailable"),
(
FailureClass::CapabilityUnsupported,
"capability_unsupported",
),
(FailureClass::CapabilityDegraded, "capability_degraded"),
(FailureClass::PlacementUnavailable, "placement_unavailable"),
(FailureClass::PayloadTooLarge, "payload_too_large"),
(FailureClass::PayloadRejected, "payload_rejected"),
(FailureClass::IdentityUnavailable, "identity_unavailable"),
(FailureClass::TransportError, "transport_error"),
(FailureClass::Timeout, "timeout"),
(FailureClass::OperatorRequired, "operator_required"),
(FailureClass::StateConflict, "state_conflict"),
(FailureClass::InvalidRequest, "invalid_request"),
(FailureClass::InternalError, "internal_error"),
],
FailureClass::ALL,
);
assert_eq!(FailureClass::ALL.len(), 13);
}
#[test]
fn retry_class_serializes_and_roundtrips() {
roundtrip_enum(
&[
(RetryClass::SafeRetry, "safe_retry"),
(RetryClass::RetryAfterReread, "retry_after_reread"),
(RetryClass::RetryAfterReconfigure, "retry_after_reconfigure"),
(RetryClass::RetryAfterOperator, "retry_after_operator"),
(RetryClass::DoNotRetry, "do_not_retry"),
],
RetryClass::ALL,
);
assert_eq!(RetryClass::ALL.len(), 5);
}
#[test]
fn placement_class_serializes_and_roundtrips() {
roundtrip_enum(
&[
(
PlacementClass::DeveloperEquivalentFrame,
"developer_equivalent_frame",
),
(PlacementClass::PrePromptFrame, "pre_prompt_frame"),
(PlacementClass::SideChannelContext, "side_channel_context"),
(PlacementClass::ReceiptOnly, "receipt_only"),
],
PlacementClass::ALL,
);
assert_eq!(PlacementClass::ALL.len(), 4);
}
#[test]
fn placement_outcome_serializes_and_roundtrips() {
roundtrip_enum(
&[
(PlacementOutcome::Delivered, "delivered"),
(PlacementOutcome::Skipped, "skipped"),
(PlacementOutcome::Degraded, "degraded"),
(PlacementOutcome::Failed, "failed"),
],
PlacementOutcome::ALL,
);
assert_eq!(PlacementOutcome::ALL.len(), 4);
}
#[test]
fn requirement_level_serializes_and_roundtrips() {
roundtrip_enum(
&[
(RequirementLevel::Required, "required"),
(RequirementLevel::Preferred, "preferred"),
(RequirementLevel::Optional, "optional"),
],
RequirementLevel::ALL,
);
assert_eq!(RequirementLevel::ALL.len(), 3);
}
#[test]
fn negotiation_outcome_serializes_and_roundtrips() {
roundtrip_enum(
&[
(NegotiationOutcome::Satisfied, "satisfied"),
(NegotiationOutcome::Degraded, "degraded"),
(NegotiationOutcome::Unsupported, "unsupported"),
(NegotiationOutcome::RequiresOperator, "requires_operator"),
],
NegotiationOutcome::ALL,
);
assert_eq!(NegotiationOutcome::ALL.len(), 4);
}
#[test]
fn frame_class_serializes_and_roundtrips() {
roundtrip_enum(
&[
(FrameClass::TopLevel, "top_level"),
(FrameClass::Subcall, "subcall"),
],
FrameClass::ALL,
);
assert_eq!(FrameClass::ALL.len(), 2);
}
#[test]
fn callback_request_rejects_unknown_top_level_field() {
let mut value = serde_json::to_value(fixture_request()).unwrap();
value
.as_object_mut()
.unwrap()
.insert("bogus_field".into(), serde_json::Value::from(1));
assert!(
serde_json::from_value::<CallbackRequest>(value).is_err(),
"CallbackRequest must reject unknown fields"
);
}
#[test]
fn callback_response_rejects_unknown_top_level_field() {
let resp = CallbackResponse::ok(ReceiptStatus::Delivered);
let mut value = serde_json::to_value(&resp).unwrap();
value
.as_object_mut()
.unwrap()
.insert("bogus_field".into(), serde_json::Value::from("x"));
assert!(
serde_json::from_value::<CallbackResponse>(value).is_err(),
"CallbackResponse must reject unknown fields"
);
}
#[test]
fn payload_envelope_rejects_unknown_top_level_field() {
let mut value = serde_json::to_value(fixture_payload()).unwrap();
value
.as_object_mut()
.unwrap()
.insert("bogus_field".into(), serde_json::Value::from(true));
assert!(
serde_json::from_value::<PayloadEnvelope>(value).is_err(),
"PayloadEnvelope must reject unknown fields"
);
}
#[test]
fn lifecycle_receipt_rejects_unknown_top_level_field() {
let mut value = serde_json::to_value(fixture_receipt()).unwrap();
value
.as_object_mut()
.unwrap()
.insert("bogus_field".into(), serde_json::Value::Null);
assert!(
serde_json::from_value::<LifecycleReceipt>(value).is_err(),
"LifecycleReceipt must reject unknown fields"
);
}
#[test]
fn lifecycle_receipt_rejects_each_missing_required_nullable_field() {
for field in LifecycleReceipt::REQUIRED_NULLABLE_FIELDS {
let mut value = serde_json::to_value(fixture_receipt()).unwrap();
value.as_object_mut().unwrap().remove(*field);
let err = serde_json::from_value::<LifecycleReceipt>(value)
.expect_err(&format!("expected error when `{field}` is omitted"));
let msg = err.to_string();
assert!(
msg.contains(field),
"error must name the missing field `{field}`, got: {msg}"
);
assert!(
msg.contains("required-nullable"),
"error must call out the required-nullable rule, got: {msg}"
);
}
}
#[test]
fn lifecycle_receipt_rejection_lists_all_missing_required_nullable_fields() {
let mut value = serde_json::to_value(fixture_receipt()).unwrap();
{
let obj = value.as_object_mut().unwrap();
obj.remove("sequence");
obj.remove("parent_receipt_id");
obj.remove("retry_class");
}
let err = serde_json::from_value::<LifecycleReceipt>(value)
.expect_err("expected error for multiple missing required-nullable keys");
let msg = err.to_string();
for field in ["sequence", "parent_receipt_id", "retry_class"] {
assert!(
msg.contains(field),
"error must name `{field}` (multi-key form), got: {msg}"
);
}
}
#[test]
fn lifecycle_receipt_accepts_explicit_null_for_required_nullable_fields() {
let mut r = fixture_receipt();
r.sequence = None;
r.parent_receipt_id = None;
r.idempotency_key = None;
r.failure_class = None;
r.retry_class = None;
let value = serde_json::to_value(&r).unwrap();
for field in LifecycleReceipt::REQUIRED_NULLABLE_FIELDS {
assert_eq!(
value[*field],
serde_json::Value::Null,
"fixture must serialize `{field}` as explicit null"
);
}
let roundtrip: LifecycleReceipt =
serde_json::from_value(value).expect("explicit null must deserialize");
assert_eq!(roundtrip, r);
roundtrip
.validate()
.expect("explicit-null receipt must validate");
}
#[test]
fn unknown_enum_wire_name_is_rejected() {
assert!(serde_json::from_value::<IntegrationMode>(json!("not_a_mode")).is_err());
assert!(serde_json::from_value::<LifecycleEventKind>(json!("session.unknown")).is_err());
assert!(serde_json::from_value::<ReceiptStatus>(json!("approved")).is_err());
assert!(serde_json::from_value::<FailureClass>(json!("oops")).is_err());
assert!(serde_json::from_value::<RetryClass>(json!("retry_eventually")).is_err());
assert!(serde_json::from_value::<SupportState>(json!("emulated")).is_err());
assert!(serde_json::from_value::<AdapterRole>(json!("foreman")).is_err());
assert!(serde_json::from_value::<PlacementClass>(json!("inline_assistant")).is_err());
assert!(serde_json::from_value::<PlacementOutcome>(json!("partial")).is_err());
assert!(serde_json::from_value::<RequirementLevel>(json!("nice_to_have")).is_err());
assert!(serde_json::from_value::<NegotiationOutcome>(json!("pending")).is_err());
assert!(serde_json::from_value::<FrameClass>(json!("recursive")).is_err());
}
#[test]
fn failure_class_default_retry_table_matches_spec() {
let cases = [
(
FailureClass::AdapterUnavailable,
RetryClass::RetryAfterReconfigure,
),
(FailureClass::CapabilityUnsupported, RetryClass::DoNotRetry),
(
FailureClass::CapabilityDegraded,
RetryClass::RetryAfterReread,
),
(
FailureClass::PlacementUnavailable,
RetryClass::RetryAfterReconfigure,
),
(FailureClass::PayloadTooLarge, RetryClass::DoNotRetry),
(
FailureClass::PayloadRejected,
RetryClass::RetryAfterReconfigure,
),
(
FailureClass::IdentityUnavailable,
RetryClass::RetryAfterReconfigure,
),
(FailureClass::TransportError, RetryClass::SafeRetry),
(FailureClass::Timeout, RetryClass::SafeRetry),
(
FailureClass::OperatorRequired,
RetryClass::RetryAfterOperator,
),
(FailureClass::StateConflict, RetryClass::RetryAfterReread),
(FailureClass::InvalidRequest, RetryClass::DoNotRetry),
(FailureClass::InternalError, RetryClass::RetryAfterReread),
];
for (failure, expected) in cases {
assert_eq!(failure.default_retry(), expected, "for {failure:?}");
}
}
#[test]
fn frame_context_top_level_pins_shape() {
let fc = FrameContext::top_level("frm-1");
assert_eq!(
serde_json::to_value(&fc).unwrap(),
json!({"frame_id": "frm-1", "frame_class": "top_level"})
);
}
#[test]
fn frame_context_subcall_pins_shape() {
let fc = FrameContext::subcall("frm-2", "frm-1");
assert_eq!(
serde_json::to_value(&fc).unwrap(),
json!({
"frame_id": "frm-2",
"parent_frame_id": "frm-1",
"frame_class": "subcall"
})
);
}
#[test]
fn frame_context_validation_rejects_bad_shapes() {
let mut fc = FrameContext::top_level("frm-1");
fc.parent_frame_id = Some("frm-0".into());
assert!(matches!(
fc.validate().unwrap_err(),
ValidationError::InvalidFrameContext(_)
));
let bad = FrameContext {
frame_id: "frm-3".into(),
parent_frame_id: None,
frame_class: FrameClass::Subcall,
};
assert!(matches!(
bad.validate().unwrap_err(),
ValidationError::InvalidFrameContext(_)
));
let empty = FrameContext::top_level("");
assert!(matches!(
empty.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
}
fn fixture_payload() -> PayloadEnvelope {
PayloadEnvelope {
schema_version: SCHEMA_VERSION.to_string(),
payload_id: "pay-1".into(),
client_id: "ccd".into(),
payload_kind: "instruction_frame".into(),
format: "client-defined".into(),
content_encoding: "utf8".into(),
body: Some("hello".into()),
body_ref: None,
byte_size: 5,
content_digest: None,
acceptable_placements: vec![AcceptablePlacement {
placement: PlacementClass::PrePromptFrame,
requirement: RequirementLevel::Required,
}],
idempotency_key: None,
expires_at_epoch_s: None,
redaction: None,
metadata: serde_json::Map::new(),
}
}
#[test]
fn payload_envelope_pins_shape_with_body() {
let p = fixture_payload();
assert_eq!(
serde_json::to_value(&p).unwrap(),
json!({
"schema_version": "lifeloop.v0.2",
"payload_id": "pay-1",
"client_id": "ccd",
"payload_kind": "instruction_frame",
"format": "client-defined",
"content_encoding": "utf8",
"body": "hello",
"byte_size": 5,
"acceptable_placements": [
{"placement": "pre_prompt_frame", "requirement": "required"}
]
})
);
}
#[test]
fn payload_envelope_validation_rejects_invalid() {
let mut p = fixture_payload();
p.body_ref = Some("ref-1".into());
assert!(matches!(
p.validate().unwrap_err(),
ValidationError::InvalidPayload(_)
));
let mut p = fixture_payload();
p.body = None;
p.body_ref = None;
assert!(matches!(
p.validate().unwrap_err(),
ValidationError::InvalidPayload(_)
));
let mut p = fixture_payload();
p.acceptable_placements.clear();
assert!(matches!(
p.validate().unwrap_err(),
ValidationError::InvalidPayload(_)
));
let mut p = fixture_payload();
p.byte_size = 1;
assert!(matches!(
p.validate().unwrap_err(),
ValidationError::InvalidPayload(_)
));
let mut p = fixture_payload();
p.payload_kind = String::new();
assert!(matches!(
p.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
let mut p = fixture_payload();
p.schema_version = "lifeloop.v0".into();
assert!(matches!(
p.validate().unwrap_err(),
ValidationError::SchemaVersionMismatch { .. }
));
}
#[test]
fn payload_ref_rejects_empty_required_strings() {
let mut payload_ref = PayloadRef {
payload_id: "pay-1".into(),
payload_kind: "instruction_frame".into(),
content_digest: None,
byte_size: Some(5),
};
payload_ref.payload_id.clear();
assert!(matches!(
payload_ref.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
payload_ref.payload_id = "pay-1".into();
payload_ref.payload_kind.clear();
assert!(matches!(
payload_ref.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
}
fn fixture_receipt() -> LifecycleReceipt {
LifecycleReceipt {
schema_version: SCHEMA_VERSION.to_string(),
receipt_id: "lfr-1".into(),
idempotency_key: None,
client_id: "ccd".into(),
adapter_id: "codex".into(),
invocation_id: "inv-1".into(),
event: LifecycleEventKind::FrameOpening,
event_id: "evt-1".into(),
sequence: Some(42),
parent_receipt_id: None,
integration_mode: IntegrationMode::NativeHook,
status: ReceiptStatus::Delivered,
at_epoch_s: 1_778_100_000,
harness_session_id: Some("session-123".into()),
harness_run_id: None,
harness_task_id: None,
payload_receipts: vec![PayloadReceipt {
payload_id: "pay-1".into(),
payload_kind: "instruction_frame".into(),
placement: PlacementClass::PrePromptFrame,
status: PlacementOutcome::Delivered,
byte_size: 5,
content_digest: Some("sha256:pay1".into()),
}],
telemetry_summary: serde_json::Map::new(),
capability_degradations: Vec::new(),
failure_class: None,
retry_class: None,
warnings: Vec::new(),
}
}
#[test]
fn lifecycle_receipt_pins_required_nullable_fields() {
let r = fixture_receipt();
let value = serde_json::to_value(&r).unwrap();
assert_eq!(value["schema_version"], "lifeloop.v0.2");
assert_eq!(value["receipt_id"], "lfr-1");
assert!(value.get("sequence").is_some());
assert_eq!(value["sequence"], 42);
assert!(value.get("parent_receipt_id").is_some());
assert_eq!(value["parent_receipt_id"], serde_json::Value::Null);
assert_eq!(value["idempotency_key"], serde_json::Value::Null);
assert_eq!(value["failure_class"], serde_json::Value::Null);
assert_eq!(value["retry_class"], serde_json::Value::Null);
assert_eq!(value["event"], "frame.opening");
assert_eq!(value["status"], "delivered");
assert_eq!(
value["payload_receipts"][0]["payload_kind"],
"instruction_frame"
);
assert_eq!(
value["payload_receipts"][0]["content_digest"],
"sha256:pay1"
);
assert_eq!(value["payload_receipts"][0]["status"], "delivered");
}
#[test]
fn payload_receipt_pins_optional_digest_and_validation() {
let with_digest = PayloadReceipt {
payload_id: "pay-1".into(),
payload_kind: "instruction_frame".into(),
placement: PlacementClass::PrePromptFrame,
status: PlacementOutcome::Delivered,
byte_size: 5,
content_digest: Some("sha256:pay1".into()),
};
assert_eq!(
serde_json::to_value(&with_digest).unwrap(),
json!({
"payload_id": "pay-1",
"payload_kind": "instruction_frame",
"placement": "pre_prompt_frame",
"status": "delivered",
"byte_size": 5,
"content_digest": "sha256:pay1"
})
);
let without_digest = PayloadReceipt {
content_digest: None,
..with_digest.clone()
};
let value = serde_json::to_value(&without_digest).unwrap();
assert!(
value.get("content_digest").is_none(),
"content_digest must be omitted when absent"
);
let mut invalid = with_digest;
invalid.payload_kind.clear();
assert!(matches!(
invalid.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
}
#[test]
fn lifecycle_receipt_rejects_receipt_emitted_event() {
let mut r = fixture_receipt();
r.event = LifecycleEventKind::ReceiptEmitted;
assert!(matches!(
r.validate().unwrap_err(),
ValidationError::InvalidReceipt(_)
));
}
#[test]
fn lifecycle_receipt_failed_status_requires_failure_class() {
let mut r = fixture_receipt();
r.status = ReceiptStatus::Failed;
r.failure_class = None;
r.retry_class = Some(RetryClass::SafeRetry);
assert!(matches!(
r.validate().unwrap_err(),
ValidationError::InvalidReceipt(_)
));
r.failure_class = Some(FailureClass::TransportError);
assert!(r.validate().is_ok());
}
#[test]
fn lifecycle_receipt_failed_status_requires_retry_class() {
let mut r = fixture_receipt();
r.status = ReceiptStatus::Failed;
r.failure_class = Some(FailureClass::TransportError);
r.retry_class = None;
let err = r.validate().unwrap_err();
assert!(
matches!(err, ValidationError::InvalidReceipt(ref m) if m.contains("retry_class")),
"expected InvalidReceipt mentioning retry_class, got {err:?}"
);
}
#[test]
fn lifecycle_receipt_rejects_empty_required_strings() {
let mut r = fixture_receipt();
r.adapter_id.clear();
assert!(matches!(
r.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
}
#[test]
fn lifecycle_receipt_rejects_failure_class_on_non_failed_status() {
let mut r = fixture_receipt();
r.status = ReceiptStatus::Delivered;
r.failure_class = Some(FailureClass::TransportError);
assert!(matches!(
r.validate().unwrap_err(),
ValidationError::InvalidReceipt(_)
));
}
#[test]
fn lifecycle_receipt_pins_full_literal_shape() {
let r = fixture_receipt();
assert_eq!(
serde_json::to_value(&r).unwrap(),
json!({
"schema_version": "lifeloop.v0.2",
"receipt_id": "lfr-1",
"idempotency_key": serde_json::Value::Null,
"client_id": "ccd",
"adapter_id": "codex",
"invocation_id": "inv-1",
"event": "frame.opening",
"event_id": "evt-1",
"sequence": 42,
"parent_receipt_id": serde_json::Value::Null,
"integration_mode": "native_hook",
"status": "delivered",
"at_epoch_s": 1_778_100_000_u64,
"harness_session_id": "session-123",
"payload_receipts": [
{
"payload_id": "pay-1",
"payload_kind": "instruction_frame",
"placement": "pre_prompt_frame",
"status": "delivered",
"byte_size": 5,
"content_digest": "sha256:pay1"
}
],
"failure_class": serde_json::Value::Null,
"retry_class": serde_json::Value::Null
})
);
}
fn fixture_request() -> CallbackRequest {
CallbackRequest {
schema_version: SCHEMA_VERSION.to_string(),
event: LifecycleEventKind::FrameOpening,
event_id: "evt-1".into(),
adapter_id: "codex".into(),
adapter_version: "0.1.0".into(),
integration_mode: IntegrationMode::NativeHook,
invocation_id: "inv-1".into(),
harness_session_id: Some("session-123".into()),
harness_run_id: None,
harness_task_id: None,
frame_context: Some(FrameContext::top_level("frm-1")),
capability_snapshot_ref: None,
payload_refs: vec![PayloadRef {
payload_id: "pay-1".into(),
payload_kind: "instruction_frame".into(),
content_digest: None,
byte_size: Some(5),
}],
sequence: Some(1),
idempotency_key: Some("idem-1".into()),
metadata: serde_json::Map::new(),
}
}
#[test]
fn dispatch_envelope_pins_shape_with_payloads() {
let envelope = DispatchEnvelope::new(fixture_request(), vec![fixture_payload()]);
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(value["schema_version"], "lifeloop.v0.2");
assert_eq!(value["request"]["adapter_id"], "codex");
assert_eq!(value["request"]["event"], "frame.opening");
assert_eq!(value["payloads"][0]["payload_id"], "pay-1");
assert_eq!(value["payloads"][0]["body"], "hello");
assert!(envelope.validate().is_ok());
}
#[test]
fn dispatch_envelope_omits_empty_payloads_on_the_wire() {
let envelope = DispatchEnvelope::new(fixture_request(), Vec::new());
let value = serde_json::to_value(&envelope).unwrap();
assert!(value.get("payloads").is_none(), "got: {value}");
assert_eq!(value["schema_version"], "lifeloop.v0.2");
assert_eq!(value["request"]["adapter_id"], "codex");
}
#[test]
fn dispatch_envelope_rejects_unknown_top_level_field() {
let envelope = DispatchEnvelope::new(fixture_request(), Vec::new());
let mut value = serde_json::to_value(&envelope).unwrap();
value
.as_object_mut()
.unwrap()
.insert("bogus_field".into(), serde_json::Value::Null);
assert!(
serde_json::from_value::<DispatchEnvelope>(value).is_err(),
"DispatchEnvelope must reject unknown fields",
);
}
#[test]
fn dispatch_envelope_rejects_schema_version_mismatch() {
let mut envelope = DispatchEnvelope::new(fixture_request(), Vec::new());
envelope.schema_version = "lifeloop.v0.0".into();
assert!(matches!(
envelope.validate().unwrap_err(),
ValidationError::SchemaVersionMismatch { .. }
));
}
#[test]
fn dispatch_envelope_round_trips_through_json() {
let envelope = DispatchEnvelope::new(fixture_request(), vec![fixture_payload()]);
let bytes = serde_json::to_vec(&envelope).unwrap();
let parsed: DispatchEnvelope = serde_json::from_slice(&bytes).unwrap();
assert_eq!(parsed, envelope);
parsed.validate().expect("round-tripped envelope validates");
}
#[test]
fn callback_request_pins_shape() {
let req = fixture_request();
assert_eq!(
serde_json::to_value(&req).unwrap(),
json!({
"schema_version": "lifeloop.v0.2",
"event": "frame.opening",
"event_id": "evt-1",
"adapter_id": "codex",
"adapter_version": "0.1.0",
"integration_mode": "native_hook",
"invocation_id": "inv-1",
"harness_session_id": "session-123",
"frame_context": {"frame_id": "frm-1", "frame_class": "top_level"},
"payload_refs": [
{"payload_id": "pay-1", "payload_kind": "instruction_frame", "byte_size": 5}
],
"sequence": 1,
"idempotency_key": "idem-1"
})
);
}
#[test]
fn callback_request_rejects_frame_event_without_frame_context() {
let mut req = fixture_request();
req.frame_context = None;
assert!(matches!(
req.validate().unwrap_err(),
ValidationError::InvalidRequest(_)
));
}
#[test]
fn callback_request_rejects_receipt_emitted_with_idempotency_key() {
let mut req = fixture_request();
req.event = LifecycleEventKind::ReceiptEmitted;
req.frame_context = None;
req.idempotency_key = Some("idem-x".into());
assert!(matches!(
req.validate().unwrap_err(),
ValidationError::InvalidRequest(_)
));
}
#[test]
fn callback_request_rejects_partial_frame_context() {
let mut req = fixture_request();
req.frame_context = Some(FrameContext {
frame_id: String::new(),
parent_frame_id: None,
frame_class: FrameClass::TopLevel,
});
assert!(matches!(
req.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
}
#[test]
fn callback_request_rejects_empty_sentinel_strings() {
let blanks: &[fn(&mut CallbackRequest)] = &[
|r| r.event_id.clear(),
|r| r.adapter_id.clear(),
|r| r.adapter_version.clear(),
|r| r.invocation_id.clear(),
];
for blank in blanks {
let mut req = fixture_request();
blank(&mut req);
assert!(matches!(
req.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
}
}
#[test]
fn callback_request_roundtrips_through_json() {
let req = fixture_request();
let bytes = serde_json::to_vec(&req).unwrap();
let parsed: CallbackRequest = serde_json::from_slice(&bytes).unwrap();
assert_eq!(parsed, req);
}
#[test]
fn callback_response_ok_pins_shape() {
let resp = CallbackResponse::ok(ReceiptStatus::Delivered);
assert_eq!(
serde_json::to_value(&resp).unwrap(),
json!({
"schema_version": "lifeloop.v0.2",
"status": "delivered",
"failure_class": serde_json::Value::Null,
"retry_class": serde_json::Value::Null
})
);
}
#[test]
fn callback_response_ok_accepts_null_failure_and_retry_classes() {
let resp = CallbackResponse::ok(ReceiptStatus::Delivered);
resp.validate().expect("ok response is valid");
}
#[test]
fn callback_response_failed_carries_default_retry() {
let resp = CallbackResponse::failed(FailureClass::Timeout);
assert_eq!(resp.status, ReceiptStatus::Failed);
assert_eq!(resp.failure_class, Some(FailureClass::Timeout));
assert_eq!(resp.retry_class, Some(RetryClass::SafeRetry));
}
#[test]
fn callback_response_failed_status_requires_failure_class() {
let mut resp = CallbackResponse::failed(FailureClass::Timeout);
resp.failure_class = None;
assert!(matches!(
resp.validate().unwrap_err(),
ValidationError::InvalidResponse(_)
));
}
#[test]
fn callback_response_failed_status_requires_retry_class() {
let mut resp = CallbackResponse::failed(FailureClass::Timeout);
resp.retry_class = None;
let err = resp.validate().unwrap_err();
assert!(
matches!(err, ValidationError::InvalidResponse(ref m) if m.contains("retry_class")),
"expected InvalidResponse mentioning retry_class, got {err:?}"
);
}
#[test]
fn callback_response_rejects_failure_class_on_non_failed_status() {
let mut resp = CallbackResponse::ok(ReceiptStatus::Delivered);
resp.failure_class = Some(FailureClass::Timeout);
assert!(matches!(
resp.validate().unwrap_err(),
ValidationError::InvalidResponse(_)
));
}
#[test]
fn callback_response_with_client_payloads_pins_shape() {
let payload = fixture_payload();
let mut resp = CallbackResponse::ok(ReceiptStatus::Delivered);
resp.client_payloads.push(payload);
let value = serde_json::to_value(&resp).unwrap();
assert_eq!(value["schema_version"], "lifeloop.v0.2");
assert_eq!(value["status"], "delivered");
assert_eq!(
value["client_payloads"][0]["schema_version"],
"lifeloop.v0.2"
);
assert_eq!(value["client_payloads"][0]["payload_id"], "pay-1");
assert_eq!(value["client_payloads"][0]["body"], "hello");
assert_eq!(
value["client_payloads"][0]["acceptable_placements"][0]["placement"],
"pre_prompt_frame"
);
assert_eq!(value["failure_class"], serde_json::Value::Null);
assert_eq!(value["retry_class"], serde_json::Value::Null);
}
fn minimal_manifest() -> AdapterManifest {
AdapterManifest {
contract_version: SCHEMA_VERSION.to_string(),
adapter_id: "codex".into(),
adapter_version: "0.1.0".into(),
display_name: "Codex".into(),
role: AdapterRole::PrimaryWorker,
integration_modes: vec![IntegrationMode::NativeHook],
lifecycle_events: BTreeMap::from([(
LifecycleEventKind::SessionStarting,
ManifestLifecycleEventSupport {
support: SupportState::Native,
modes: vec![IntegrationMode::NativeHook],
},
)]),
placement: BTreeMap::from([(
ManifestPlacementClass::PreSession,
ManifestPlacementSupport {
support: SupportState::Native,
max_bytes: Some(8192),
},
)]),
context_pressure: ManifestContextPressure {
support: SupportState::Synthesized,
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(),
}
}
#[test]
fn manifest_placement_class_serializes_and_roundtrips() {
roundtrip_enum(
&[
(ManifestPlacementClass::PreSession, "pre_session"),
(ManifestPlacementClass::PreFrameLeading, "pre_frame_leading"),
(
ManifestPlacementClass::PreFrameTrailing,
"pre_frame_trailing",
),
(ManifestPlacementClass::ToolResult, "tool_result"),
(ManifestPlacementClass::ManualOperator, "manual_operator"),
],
ManifestPlacementClass::ALL,
);
assert_eq!(ManifestPlacementClass::ALL.len(), 5);
}
#[test]
fn adapter_manifest_pins_minimal_shape() {
let manifest = minimal_manifest();
assert_eq!(
serde_json::to_value(&manifest).unwrap(),
json!({
"contract_version": "lifeloop.v0.2",
"adapter_id": "codex",
"adapter_version": "0.1.0",
"display_name": "Codex",
"role": "primary_worker",
"integration_modes": ["native_hook"],
"lifecycle_events": {
"session.starting": {
"support": "native",
"modes": ["native_hook"]
}
},
"placement": {
"pre_session": {
"support": "native",
"max_bytes": 8192
}
},
"context_pressure": {"support": "synthesized"},
"receipts": {
"native": false,
"lifeloop_synthesized": true,
"receipt_ledger": "unavailable"
}
})
);
assert!(manifest.validate().is_ok());
}
#[test]
fn adapter_manifest_rejects_empty_integration_modes() {
let mut manifest = minimal_manifest();
manifest.integration_modes.clear();
assert!(matches!(
manifest.validate().unwrap_err(),
ValidationError::InvalidManifest(_)
));
}
#[test]
fn adapter_manifest_rejects_empty_required_strings() {
for blank in [
|m: &mut AdapterManifest| m.adapter_id.clear(),
|m: &mut AdapterManifest| m.adapter_version.clear(),
|m: &mut AdapterManifest| m.display_name.clear(),
] {
let mut manifest = minimal_manifest();
blank(&mut manifest);
assert!(matches!(
manifest.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
}
}
#[test]
fn adapter_manifest_rejects_unknown_top_level_field() {
let manifest = minimal_manifest();
let mut value = serde_json::to_value(&manifest).unwrap();
value
.as_object_mut()
.unwrap()
.insert("bogus_field".into(), serde_json::Value::Null);
assert!(
serde_json::from_value::<AdapterManifest>(value).is_err(),
"AdapterManifest must reject unknown fields"
);
}
#[test]
fn adapter_manifest_round_trips_through_json() {
let manifest = minimal_manifest();
let json = serde_json::to_string(&manifest).unwrap();
let parsed: AdapterManifest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, manifest);
}
#[test]
fn adapter_manifest_serializes_optional_fields_when_present() {
let mut manifest = minimal_manifest();
manifest.session_identity = Some(ManifestSessionIdentity {
harness_session_id: SupportState::Native,
harness_run_id: SupportState::Synthesized,
harness_task_id: SupportState::Unavailable,
});
manifest.renewal = Some(ManifestRenewal {
reset: ManifestRenewalReset {
native: SupportState::Native,
wrapper_mediated: SupportState::Partial,
manual: SupportState::Manual,
},
continuation: ManifestRenewalContinuation {
observation: SupportState::Synthesized,
payload_delivery: SupportState::Partial,
},
profiles: vec!["ccd-renewal".into()],
evidence: Some("adapter proves renewal delivery without owning client tokens".into()),
});
manifest.failure_modes = vec![FailureClass::TransportError];
let value = serde_json::to_value(&manifest).unwrap();
assert_eq!(
value["session_identity"],
json!({
"harness_session_id": "native",
"harness_run_id": "synthesized",
"harness_task_id": "unavailable"
})
);
assert_eq!(
value["renewal"],
json!({
"reset": {
"native": "native",
"wrapper_mediated": "partial",
"manual": "manual"
},
"continuation": {
"observation": "synthesized",
"payload_delivery": "partial"
},
"profiles": ["ccd-renewal"],
"evidence": "adapter proves renewal delivery without owning client tokens"
})
);
assert_eq!(value["failure_modes"], json!(["transport_error"]));
}
#[test]
fn adapter_manifest_rejects_empty_renewal_evidence() {
let mut manifest = minimal_manifest();
manifest.renewal = Some(ManifestRenewal {
reset: ManifestRenewalReset {
native: SupportState::Unavailable,
wrapper_mediated: SupportState::Unavailable,
manual: SupportState::Unavailable,
},
continuation: ManifestRenewalContinuation {
observation: SupportState::Unavailable,
payload_delivery: SupportState::Unavailable,
},
profiles: Vec::new(),
evidence: Some(String::new()),
});
assert!(matches!(
manifest.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
}
#[test]
fn capability_degradation_pins_shape() {
let deg = CapabilityDegradation {
capability: "context_pressure".into(),
previous_support: SupportState::Native,
current_support: SupportState::Unavailable,
evidence: Some("telemetry file missing".into()),
retry_class: Some(RetryClass::RetryAfterReconfigure),
};
assert_eq!(
serde_json::to_value(°).unwrap(),
json!({
"capability": "context_pressure",
"previous_support": "native",
"current_support": "unavailable",
"evidence": "telemetry file missing",
"retry_class": "retry_after_reconfigure"
})
);
}
#[test]
fn capability_degradation_rejects_empty_capability() {
let deg = CapabilityDegradation {
capability: String::new(),
previous_support: SupportState::Native,
current_support: SupportState::Unavailable,
evidence: None,
retry_class: None,
};
assert!(matches!(
deg.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
}
#[test]
fn warning_pins_shape() {
let w = Warning {
code: "placement_degraded".into(),
message: "fell back to pre_prompt_frame".into(),
capability: Some("developer_equivalent_frame".into()),
};
assert_eq!(
serde_json::to_value(&w).unwrap(),
json!({
"code": "placement_degraded",
"message": "fell back to pre_prompt_frame",
"capability": "developer_equivalent_frame"
})
);
}
#[test]
fn warning_rejects_empty_code() {
let w = Warning {
code: String::new(),
message: "fell back to pre_prompt_frame".into(),
capability: None,
};
assert!(matches!(
w.validate().unwrap_err(),
ValidationError::EmptyField(_)
));
}