use std::io::Write;
use std::process::{Command, Stdio};
use lifeloop::{
AcceptablePlacement, CallbackRequest, DispatchEnvelope, FrameContext, IntegrationMode,
LifecycleEventKind, PayloadEnvelope, PayloadRef, PlacementClass, RequirementLevel,
SCHEMA_VERSION,
};
fn lifeloop_bin() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_BIN_EXE_lifeloop"))
}
fn fake_client_bin() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_BIN_EXE_lifeloop-fake-ccd-client"))
}
fn run(args: &[&str], stdin_bytes: &[u8]) -> (i32, String, String) {
let mut child = Command::new(lifeloop_bin())
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn lifeloop");
match child.stdin.as_mut().unwrap().write_all(stdin_bytes) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {}
Err(e) => panic!("pipe stdin: {e}"),
}
let out = child.wait_with_output().expect("wait");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn frame_opening_request_codex() -> CallbackRequest {
let entry = lifeloop::lookup_manifest("codex").unwrap();
CallbackRequest {
schema_version: SCHEMA_VERSION.to_string(),
event: LifecycleEventKind::FrameOpening,
event_id: "evt-cli-1".into(),
adapter_id: "codex".into(),
adapter_version: entry.manifest.adapter_version,
integration_mode: IntegrationMode::NativeHook,
invocation_id: "inv-cli-1".into(),
harness_session_id: Some("session-cli".into()),
harness_run_id: Some("run-cli".into()),
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(11),
}],
sequence: Some(1),
idempotency_key: Some("idem-cli-1".into()),
metadata: serde_json::Map::new(),
}
}
fn session_request_codex(
event: LifecycleEventKind,
suffix: &str,
sequence: u64,
) -> CallbackRequest {
let entry = lifeloop::lookup_manifest("codex").unwrap();
CallbackRequest {
schema_version: SCHEMA_VERSION.to_string(),
event,
event_id: format!("evt-renewal-{suffix}"),
adapter_id: "codex".into(),
adapter_version: entry.manifest.adapter_version,
integration_mode: IntegrationMode::NativeHook,
invocation_id: format!("inv-renewal-{suffix}"),
harness_session_id: Some(format!("session-renewal-{suffix}")),
harness_run_id: Some(format!("run-renewal-{suffix}")),
harness_task_id: None,
frame_context: None,
capability_snapshot_ref: Some(format!("cap-renewal-{suffix}")),
payload_refs: Vec::new(),
sequence: Some(sequence),
idempotency_key: Some(format!("idem-renewal-{suffix}")),
metadata: serde_json::Map::new(),
}
}
fn dispatch_bytes(req: CallbackRequest, payloads: Vec<PayloadEnvelope>) -> Vec<u8> {
serde_json::to_vec(&DispatchEnvelope::new(req, payloads)).expect("serialize envelope")
}
fn instruction_payload(payload_id: &str, body: &str) -> PayloadEnvelope {
PayloadEnvelope {
schema_version: SCHEMA_VERSION.to_string(),
payload_id: payload_id.into(),
client_id: "ccd".into(),
payload_kind: "ccd.instruction_frame".into(),
format: "client-defined".into(),
content_encoding: "utf8".into(),
body: Some(body.into()),
body_ref: None,
byte_size: body.len() as u64,
content_digest: None,
acceptable_placements: vec![AcceptablePlacement {
placement: PlacementClass::PrePromptFrame,
requirement: RequirementLevel::Preferred,
}],
idempotency_key: None,
expires_at_epoch_s: None,
redaction: None,
metadata: serde_json::Map::new(),
}
}
fn renewal_payload(
payload_id: &str,
payload_kind: &str,
body: &str,
placement: PlacementClass,
) -> PayloadEnvelope {
let mut metadata = serde_json::Map::new();
metadata.insert(
"renewal_boundary".into(),
serde_json::json!("reset_continuation"),
);
PayloadEnvelope {
schema_version: SCHEMA_VERSION.to_string(),
payload_id: payload_id.into(),
client_id: "ccd".into(),
payload_kind: payload_kind.into(),
format: "client-defined".into(),
content_encoding: "utf8".into(),
body: Some(body.into()),
body_ref: None,
byte_size: body.len() as u64,
content_digest: Some(format!("sha256:{payload_id}")),
acceptable_placements: vec![AcceptablePlacement {
placement,
requirement: RequirementLevel::Required,
}],
idempotency_key: Some(format!("idem-{payload_id}")),
expires_at_epoch_s: None,
redaction: None,
metadata,
}
}
#[test]
fn event_invoke_happy_path_emits_lifecycle_receipt() {
let req = frame_opening_request_codex();
let bytes = dispatch_bytes(req, Vec::new());
let fake = fake_client_bin();
let (code, stdout, stderr) = run(
&[
"event",
"invoke",
"--client-cmd",
fake.to_str().unwrap(),
"--client-arg",
"ok",
"--timeout-ms",
"5000",
"--client-id",
"test-client",
"--receipt-id",
"rcpt-1",
"--at-epoch-s",
"1700000000",
],
&bytes,
);
assert_eq!(code, 0, "stderr=`{stderr}`");
let v: serde_json::Value = serde_json::from_str(&stdout).expect("stdout is JSON");
assert_eq!(v.get("receipt_id").and_then(|s| s.as_str()), Some("rcpt-1"));
assert_eq!(
v.get("client_id").and_then(|s| s.as_str()),
Some("test-client")
);
assert_eq!(v.get("adapter_id").and_then(|s| s.as_str()), Some("codex"));
let status = v.get("status").and_then(|s| s.as_str()).unwrap();
assert!(
["delivered", "degraded", "skipped", "observed", "failed"].contains(&status),
"unexpected status: {status}"
);
}
#[test]
fn event_invoke_carries_opaque_renewal_payload_receipts() {
let prepare_req = session_request_codex(LifecycleEventKind::SessionEnding, "prepare", 41);
let prepare_payload = renewal_payload(
"pay-renewal-prepare",
"lifeloop.renewal.reset_prepare",
r#"{"phase":"prepare","opaque_client_fact":"client-owned"}"#,
PlacementClass::ReceiptOnly,
);
let (code, stdout, stderr) = run(
&[
"event",
"invoke",
"--in-process",
"--client-id",
"ccd",
"--receipt-id",
"rcpt-renewal-prepare",
"--at-epoch-s",
"1779000000",
],
&dispatch_bytes(prepare_req, vec![prepare_payload]),
);
assert_eq!(code, 0, "stderr=`{stderr}`");
let receipt: serde_json::Value = serde_json::from_str(&stdout).expect("receipt JSON");
assert_eq!(receipt["event"], "session.ending");
assert_eq!(
receipt["payload_receipts"][0]["payload_kind"],
"lifeloop.renewal.reset_prepare"
);
assert_eq!(receipt["payload_receipts"][0]["placement"], "receipt_only");
let continuation_req = session_request_codex(LifecycleEventKind::SessionStarted, "continue", 1);
let continuation_payload = renewal_payload(
"pay-renewal-continuation",
"lifeloop.renewal.continuation",
r#"{"phase":"continued","opaque_client_fact":"client-owned"}"#,
PlacementClass::DeveloperEquivalentFrame,
);
let (code, stdout, stderr) = run(
&[
"event",
"invoke",
"--in-process",
"--client-id",
"ccd",
"--receipt-id",
"rcpt-renewal-continuation",
"--at-epoch-s",
"1779000100",
],
&dispatch_bytes(continuation_req, vec![continuation_payload]),
);
assert_eq!(code, 0, "stderr=`{stderr}`");
let receipt: serde_json::Value = serde_json::from_str(&stdout).expect("receipt JSON");
assert_eq!(receipt["event"], "session.started");
assert_eq!(
receipt["payload_receipts"][0]["payload_kind"],
"lifeloop.renewal.continuation"
);
assert_eq!(
receipt["payload_receipts"][0]["placement"],
"developer_equivalent_frame"
);
assert_eq!(
receipt["payload_receipts"][0]["content_digest"],
"sha256:pay-renewal-continuation"
);
}
#[test]
fn event_invoke_in_process_short_circuits_subprocess() {
let req = frame_opening_request_codex();
let bytes = dispatch_bytes(req, Vec::new());
let (code, stdout, stderr) = run(
&[
"event",
"invoke",
"--in-process",
"--client-id",
"test",
"--receipt-id",
"r1",
"--at-epoch-s",
"0",
],
&bytes,
);
assert_eq!(code, 0, "stderr=`{stderr}`");
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(v.get("receipt_id").and_then(|s| s.as_str()), Some("r1"));
}
#[test]
fn event_invoke_rejects_invalid_request_with_validation_exit() {
let mut req = frame_opening_request_codex();
req.frame_context = None;
let bytes = dispatch_bytes(req, Vec::new());
let (code, _stdout, stderr) = run(&["event", "invoke", "--in-process"], &bytes);
assert_eq!(code, 1, "expected validation exit (1), got {code}");
assert!(stderr.contains("frame_context"));
}
#[test]
fn event_invoke_missing_client_cmd_is_usage_error() {
let req = frame_opening_request_codex();
let bytes = dispatch_bytes(req, Vec::new());
let (code, _stdout, stderr) = run(&["event", "invoke"], &bytes);
assert_eq!(code, 2, "expected usage exit (2), got {code}");
assert!(stderr.contains("--client-cmd"));
}
#[test]
fn event_invoke_payload_bearing_frame_opening_round_trips_body() {
let req = frame_opening_request_codex();
let payload_body = "instruction-frame-body-from-cli";
let payloads = vec![instruction_payload("pay-cli-1", payload_body)];
let bytes = dispatch_bytes(req, payloads);
let fake = fake_client_bin();
let (code, stdout, stderr) = run(
&[
"event",
"invoke",
"--client-cmd",
fake.to_str().unwrap(),
"--client-arg",
"ok",
"--timeout-ms",
"5000",
"--client-id",
"test-client",
"--receipt-id",
"rcpt-pay-1",
"--at-epoch-s",
"1700000001",
],
&bytes,
);
assert_eq!(code, 0, "stderr=`{stderr}`");
let v: serde_json::Value = serde_json::from_str(&stdout).expect("stdout is JSON");
assert_eq!(
v.get("receipt_id").and_then(|s| s.as_str()),
Some("rcpt-pay-1"),
);
let receipts = v
.get("payload_receipts")
.and_then(|r| r.as_array())
.expect("payload_receipts present");
assert!(
!receipts.is_empty(),
"expected at least one payload_receipt, receipt was: {v}",
);
let first = &receipts[0];
assert_eq!(
first.get("payload_id").and_then(|s| s.as_str()),
Some("pay-cli-1"),
);
assert_eq!(
first.get("payload_kind").and_then(|s| s.as_str()),
Some("ccd.instruction_frame"),
);
assert!(
first.get("content_digest").is_none(),
"content_digest should be omitted when source payload has none"
);
let status = first.get("status").and_then(|s| s.as_str()).unwrap();
assert!(
["delivered", "degraded", "skipped", "failed"].contains(&status),
"unexpected payload status: {status}",
);
}
#[test]
fn event_invoke_unknown_flag_is_usage_error() {
let (code, _stdout, stderr) = run(&["event", "invoke", "--bogus"], b"{}");
assert_eq!(code, 2);
assert!(stderr.contains("unknown flag"));
}
#[test]
fn event_invoke_empty_stdin_is_input_error() {
let (code, _stdout, stderr) = run(&["event", "invoke", "--in-process"], b"");
assert_eq!(code, 3, "expected input exit (3), got {code}");
assert!(stderr.contains("stdin is empty"));
}