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 request() -> CallbackRequest {
let entry = lifeloop::lookup_manifest("codex").unwrap();
CallbackRequest {
schema_version: SCHEMA_VERSION.to_string(),
event: LifecycleEventKind::FrameOpening,
event_id: "evt-fixity-cli-1".into(),
adapter_id: "codex".into(),
adapter_version: entry.manifest.adapter_version,
integration_mode: IntegrationMode::NativeHook,
invocation_id: "inv-fixity-cli-1".into(),
harness_session_id: Some("sess-fixity-cli-1".into()),
harness_run_id: Some("run-fixity-cli-1".into()),
harness_task_id: None,
frame_context: Some(FrameContext::top_level("frm-fixity-cli-1")),
capability_snapshot_ref: None,
payload_refs: vec![PayloadRef {
payload_id: "pay-fixity-cli-1".into(),
payload_kind: "fixity.experience".into(),
content_digest: Some("sha256:fixity-cli".into()),
byte_size: Some(124),
}],
sequence: Some(1),
idempotency_key: Some("idem-fixity-cli-1".into()),
metadata: serde_json::Map::new(),
}
}
fn payload() -> PayloadEnvelope {
let body = "Error: TS2345 in API handler. What worked: Number() at call site. Avoid changing the exported function signature.";
PayloadEnvelope {
schema_version: SCHEMA_VERSION.to_string(),
payload_id: "pay-fixity-cli-1".into(),
client_id: "fixity-pilot".into(),
payload_kind: "fixity.experience".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("sha256:fixity-cli".into()),
acceptable_placements: vec![AcceptablePlacement {
placement: PlacementClass::DeveloperEquivalentFrame,
requirement: RequirementLevel::Preferred,
}],
idempotency_key: None,
expires_at_epoch_s: None,
redaction: None,
metadata: serde_json::Map::new(),
}
}
#[test]
fn event_invoke_reaches_fixity_pilot_and_records_payload_observation() {
let dir = tempfile::tempdir().unwrap();
let artifact_path = dir.path().join("fixity-cli-artifact.jsonl");
let dispatch = serde_json::to_vec(&DispatchEnvelope::new(request(), vec![payload()])).unwrap();
let mut args = vec![
"event".to_string(),
"invoke".to_string(),
"--timeout-ms".to_string(),
"30000".to_string(),
"--client-id".to_string(),
"fixity-pilot".to_string(),
"--receipt-id".to_string(),
"rcpt-fixity-cli-1".to_string(),
"--at-epoch-s".to_string(),
"1700000028".to_string(),
];
let (client_cmd, client_args) = fixity_client_command(&artifact_path);
args.push("--client-cmd".into());
args.push(client_cmd);
for arg in client_args {
args.push("--client-arg".into());
args.push(arg);
}
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(&dispatch).unwrap();
let out = child.wait_with_output().unwrap();
assert!(
out.status.success(),
"stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let receipt: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(receipt["receipt_id"], "rcpt-fixity-cli-1");
assert_eq!(receipt["client_id"], "fixity-pilot");
assert_eq!(receipt["status"], "delivered");
assert_eq!(
receipt["payload_receipts"][0]["payload_id"],
"pay-fixity-cli-1"
);
assert_eq!(
receipt["payload_receipts"][0]["payload_kind"],
"fixity.experience"
);
assert_eq!(
receipt["payload_receipts"][0]["content_digest"],
"sha256:fixity-cli"
);
assert_eq!(receipt["payload_receipts"][0]["status"], "delivered");
let artifact_text = std::fs::read_to_string(&artifact_path).unwrap();
let artifact: serde_json::Value = serde_json::from_str(artifact_text.trim()).unwrap();
assert_eq!(artifact["schema"], "lifeloop-fixity-pilot.v0.1");
assert_eq!(artifact["event"], "frame.opening");
assert_eq!(
artifact["payload_observations"][0]["delivery"],
"inline_body"
);
assert!(
artifact["payload_observations"][0]["body_excerpt"]
.as_str()
.unwrap()
.contains("TS2345")
);
}
fn fixity_client_command(artifact_path: &std::path::Path) -> (String, Vec<String>) {
if let Ok(path) = std::env::var("CARGO_BIN_EXE_lifeloop-fixity-pilot") {
return (
path,
vec![
"--artifact-path".into(),
artifact_path.to_string_lossy().into_owned(),
],
);
}
let candidate = lifeloop_bin()
.parent()
.expect("lifeloop bin has parent")
.join("lifeloop-fixity-pilot");
if candidate.exists() {
return (
candidate.to_string_lossy().into_owned(),
vec![
"--artifact-path".into(),
artifact_path.to_string_lossy().into_owned(),
],
);
}
(
"cargo".into(),
vec![
"run".into(),
"--quiet".into(),
"-p".into(),
"fixity-pilot".into(),
"--bin".into(),
"lifeloop-fixity-pilot".into(),
"--".into(),
"--artifact-path".into(),
artifact_path.to_string_lossy().into_owned(),
],
)
}