use std::io::Write;
use std::process::{Command, Stdio};
use lifeloop::{
CallbackRequest, CallbackResponse, FrameContext, IntegrationMode, LifecycleEventKind,
PayloadRef, ReceiptStatus, SCHEMA_VERSION,
};
fn lifeloop_bin() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_BIN_EXE_lifeloop"))
}
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");
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 codex_request() -> CallbackRequest {
let entry = lifeloop::lookup_manifest("codex").unwrap();
CallbackRequest {
schema_version: SCHEMA_VERSION.to_string(),
event: LifecycleEventKind::SessionStarted,
event_id: "evt-r-1".into(),
adapter_id: "codex".into(),
adapter_version: entry.manifest.adapter_version,
integration_mode: IntegrationMode::NativeHook,
invocation_id: "inv-r-1".into(),
harness_session_id: Some("sess-r".into()),
harness_run_id: Some("run-r".into()),
harness_task_id: None,
frame_context: None,
capability_snapshot_ref: None,
payload_refs: Vec::new(),
sequence: Some(1),
idempotency_key: Some("idem-r-1".into()),
metadata: serde_json::Map::new(),
}
}
#[test]
fn receipt_emit_synthesizes_a_valid_receipt() {
let request = codex_request();
let response = CallbackResponse::ok(ReceiptStatus::Delivered);
let envelope = serde_json::json!({
"request": request,
"response": response,
"context": {
"client_id": "ccd",
"receipt_id": "rcpt-cli-r-1",
"at_epoch_s": 1700000000_u64,
"harness_run_id": "run-r",
}
});
let bytes = serde_json::to_vec(&envelope).unwrap();
let (code, stdout, stderr) = run(&["receipt", "emit"], &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("rcpt-cli-r-1")
);
assert_eq!(v.get("client_id").and_then(|s| s.as_str()), Some("ccd"));
assert_eq!(v.get("status").and_then(|s| s.as_str()), Some("delivered"));
}
#[test]
fn receipt_emit_invalid_request_is_validation_error() {
let mut request = codex_request();
request.event = LifecycleEventKind::FrameOpening;
request.frame_context = None;
let response = CallbackResponse::ok(ReceiptStatus::Delivered);
let envelope = serde_json::json!({
"request": request,
"response": response,
"context": {
"client_id": "ccd",
"receipt_id": "rcpt-1",
"at_epoch_s": 0_u64,
}
});
let bytes = serde_json::to_vec(&envelope).unwrap();
let (code, _stdout, stderr) = run(&["receipt", "emit"], &bytes);
assert_eq!(code, 1, "expected validation exit (1), got {code}");
assert!(stderr.contains("frame_context") || stderr.contains("CallbackRequest"));
}
#[test]
fn receipt_show_validates_and_pretty_prints() {
let request = codex_request();
let response = CallbackResponse::ok(ReceiptStatus::Delivered);
let envelope = serde_json::json!({
"request": request,
"response": response,
"context": {
"client_id": "ccd",
"receipt_id": "rcpt-show-1",
"at_epoch_s": 1700000000_u64,
"harness_run_id": "run-r",
}
});
let bytes = serde_json::to_vec(&envelope).unwrap();
let (code, emitted, _) = run(&["receipt", "emit"], &bytes);
assert_eq!(code, 0);
let (code, stdout, stderr) = run(&["receipt", "show"], emitted.as_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("rcpt-show-1")
);
}
#[test]
fn receipt_show_rejects_invalid_receipt() {
let bytes = b"{\"schema_version\":\"lifeloop.v0.1\"}";
let (code, _stdout, stderr) = run(&["receipt", "show"], bytes);
assert_eq!(code, 3, "expected input exit (3), got {code}");
assert!(stderr.contains("invalid JSON") || stderr.contains("missing"));
}
#[test]
fn receipt_unknown_subcommand_is_usage_error() {
let (code, _stdout, stderr) = run(&["receipt", "weird"], b"{}");
assert_eq!(code, 2);
assert!(stderr.contains("unknown subcommand"));
}
#[allow(dead_code)]
fn _unused_imports_keepalive() -> (FrameContext, PayloadRef) {
(
FrameContext::top_level("x"),
PayloadRef {
payload_id: "p".into(),
payload_kind: "k".into(),
content_digest: None,
byte_size: None,
},
)
}