use lifeloop::telemetry::{
self, EnvAlias, PressureObservation, TelemetryError, claude, codex, gemini, opencode,
resolve_env_string_with,
};
use std::sync::Mutex;
static ENV_SINK_LOCK: Mutex<()> = Mutex::new(());
fn fixture(name: &str) -> Vec<u8> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("telemetry")
.join(name);
std::fs::read(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()))
}
#[test]
fn claude_parse_session_log_extracts_latest_metrics() {
let bytes = fixture("claude_session.jsonl");
let obs = claude::parse_session_log(&bytes, 1_700_000_000, Some(200_000))
.expect("parse ok")
.expect("snapshot present");
assert_eq!(obs.adapter_id, "claude");
assert_eq!(obs.observed_at_epoch_s, 1_700_000_000);
assert_eq!(obs.model_name.as_deref(), Some("claude-test"));
assert_eq!(obs.total_tokens, Some(240));
assert_eq!(obs.context_window_tokens, Some(200_000));
assert!(obs.context_used_pct.is_some());
assert_eq!(obs.usage.input_tokens, 300);
assert_eq!(obs.usage.output_tokens, 130);
assert_eq!(obs.usage.cache_creation_input_tokens, 20);
assert_eq!(obs.usage.cache_read_input_tokens, 70);
}
#[test]
fn claude_parse_returns_none_for_empty_log() {
let obs = claude::parse_session_log(b"", 0, None).expect("ok");
assert!(obs.is_none());
}
#[test]
fn codex_parse_session_log_uses_latest_token_count() {
let bytes = fixture("codex_session.jsonl");
let obs = codex::parse_session_log(&bytes, 1_700_000_100)
.expect("parse ok")
.expect("snapshot present");
assert_eq!(obs.adapter_id, "codex");
assert_eq!(obs.total_tokens, Some(500));
assert_eq!(obs.context_window_tokens, Some(4096));
assert_eq!(obs.usage.blended_total_tokens, Some(2000));
assert_eq!(obs.model_name.as_deref(), Some("codex-test"));
}
#[test]
fn codex_parse_returns_none_with_no_token_count_events() {
let obs = codex::parse_session_log(b"{\"type\":\"other\"}\n", 0).expect("ok");
assert!(obs.is_none());
}
#[test]
fn gemini_parse_telemetry_log_aggregates_session() {
let bytes = fixture("gemini_telemetry.log");
let obs = gemini::parse_telemetry_log(&bytes, 1_700_000_200)
.expect("parse ok")
.expect("snapshot present");
assert_eq!(obs.adapter_id, "gemini");
assert_eq!(obs.total_tokens, Some(480));
assert_eq!(obs.context_window_tokens, Some(8192));
assert_eq!(obs.compaction_signal, Some(true));
assert_eq!(obs.model_name.as_deref(), Some("gemini-test"));
assert_eq!(obs.usage.input_tokens, 700);
assert_eq!(obs.usage.output_tokens, 220);
assert_eq!(obs.usage.cache_read_input_tokens, 130);
}
#[test]
fn opencode_config_yields_context_window_and_model_name() {
let bytes = fixture("opencode_config.json");
assert_eq!(
opencode::read_context_window_from_config(&bytes),
Some(200_000)
);
assert_eq!(
opencode::read_model_name_from_config(&bytes).as_deref(),
Some("anthropic/test-model")
);
}
#[test]
fn opencode_build_observation_from_metrics() {
let metrics = opencode::SessionMetrics {
prompt_tokens: 1500,
completion_tokens: 400,
summary_message_id: Some("summary-1".into()),
};
let obs = opencode::build_observation(metrics, 1_700_000_300, Some(200_000), None);
assert_eq!(obs.adapter_id, "opencode");
assert_eq!(obs.total_tokens, Some(1500));
assert_eq!(obs.context_window_tokens, Some(200_000));
assert_eq!(obs.compaction_signal, Some(true));
assert_eq!(obs.usage.input_tokens, 1500);
assert_eq!(obs.usage.output_tokens, 400);
}
#[test]
fn opencode_no_summary_means_no_compaction_signal() {
let metrics = opencode::SessionMetrics {
prompt_tokens: 100,
completion_tokens: 0,
summary_message_id: None,
};
let obs = opencode::build_observation(metrics, 0, None, None);
assert!(obs.compaction_signal.is_none());
}
#[test]
fn host_adapter_alias_resolves() {
let _g = ENV_SINK_LOCK.lock().unwrap();
telemetry::env_warning_sink().reset_for_tests();
let read = |_name: &str| -> Option<String> { None };
let resolved = resolve_env_string_with(telemetry::host::HOST_ADAPTER_ALIASES, &read);
assert!(resolved.is_none());
}
#[test]
fn lifeloop_alias_wins_over_ccd_compat() {
let _g = ENV_SINK_LOCK.lock().unwrap();
telemetry::env_warning_sink().reset_for_tests();
let aliases = &[EnvAlias {
lifeloop: "LIFELOOP_TELEMETRY_INTEG_X",
ccd_compat: "CCD_TELEMETRY_INTEG_X",
}];
let read = |name: &str| -> Option<String> {
match name {
"LIFELOOP_TELEMETRY_INTEG_X" => Some("from-lifeloop".into()),
"CCD_TELEMETRY_INTEG_X" => Some("from-ccd".into()),
_ => None,
}
};
let resolved = resolve_env_string_with(aliases, &read);
assert_eq!(resolved.as_deref(), Some("from-lifeloop"));
let warnings = telemetry::env_warning_sink().drain();
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].lifeloop_key, "LIFELOOP_TELEMETRY_INTEG_X");
assert_eq!(warnings[0].ccd_compat_key, "CCD_TELEMETRY_INTEG_X");
}
#[test]
fn ccd_compat_only_does_not_warn() {
let _g = ENV_SINK_LOCK.lock().unwrap();
telemetry::env_warning_sink().reset_for_tests();
let aliases = &[EnvAlias {
lifeloop: "LIFELOOP_TELEMETRY_INTEG_Y",
ccd_compat: "CCD_TELEMETRY_INTEG_Y",
}];
let read = |name: &str| -> Option<String> {
match name {
"CCD_TELEMETRY_INTEG_Y" => Some("legacy".into()),
_ => None,
}
};
let resolved = resolve_env_string_with(aliases, &read);
assert_eq!(resolved.as_deref(), Some("legacy"));
assert!(telemetry::env_warning_sink().drain().is_empty());
}
#[test]
fn telemetry_error_maps_to_named_failure_classes() {
assert_eq!(
TelemetryError::Unavailable("missing".into()).failure_class(),
"telemetry_unavailable"
);
assert_eq!(
TelemetryError::HookProtocol("bad shape".into()).failure_class(),
"hook_protocol_error"
);
assert_eq!(
TelemetryError::Internal("bug".into()).failure_class(),
"internal_error"
);
}
#[test]
fn pressure_observation_round_trips_via_serde() {
let obs = PressureObservation {
adapter_id: "claude".into(),
adapter_version: Some("0.1.0".into()),
observed_at_epoch_s: 42,
model_name: Some("claude-test".into()),
total_tokens: Some(100),
context_window_tokens: Some(1000),
context_used_pct: Some(10),
compaction_signal: Some(true),
usage: telemetry::TokenUsage {
input_tokens: 50,
output_tokens: 30,
cache_creation_input_tokens: 10,
cache_read_input_tokens: 10,
blended_total_tokens: None,
},
};
let json = serde_json::to_string(&obs).unwrap();
let back: PressureObservation = serde_json::from_str(&json).unwrap();
assert_eq!(obs, back);
}