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");
child
.stdin
.as_mut()
.unwrap()
.write_all(stdin_bytes)
.expect("pipe stdin");
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 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(),
}
}
#[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_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"),
);
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"));
}