From ad78dce1d97ab919226ad0a3ae52a730f5f45e32 Mon Sep 17 00:00:00 2001 From: Vishnu Chopra Date: Sat, 9 Aug 2025 23:22:55 +0100 Subject: [PATCH] Add tests --- codex-rs/Cargo.lock | 1 + codex-rs/codex-telemetry/Cargo.toml | 7 +- .../codex-telemetry/tests/otel_file_export.rs | 120 ++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 codex-rs/codex-telemetry/tests/otel_file_export.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index cb3240fc9d..af465c1924 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -977,6 +977,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", + "tempfile", "thiserror 1.0.69", "tokio", "tonic", diff --git a/codex-rs/codex-telemetry/Cargo.toml b/codex-rs/codex-telemetry/Cargo.toml index 412e98c807..d4579e2738 100644 --- a/codex-rs/codex-telemetry/Cargo.toml +++ b/codex-rs/codex-telemetry/Cargo.toml @@ -45,4 +45,9 @@ chrono = { version = "0.4", default-features = false, features = ["std", "clock" rand = { version = "0.8", optional = true } # Always-available deps (small) -thiserror = "1" \ No newline at end of file +thiserror = "1" + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +tracing-subscriber = { version = "0.3", features = ["registry"] } +tempfile = "3" \ No newline at end of file diff --git a/codex-rs/codex-telemetry/tests/otel_file_export.rs b/codex-rs/codex-telemetry/tests/otel_file_export.rs new file mode 100644 index 0000000000..b086582323 --- /dev/null +++ b/codex-rs/codex-telemetry/tests/otel_file_export.rs @@ -0,0 +1,120 @@ +#![cfg(feature = "otel")] + +use std::fs; +use std::path::PathBuf; + +use codex_telemetry as telemetry; +use tempfile::TempDir; +use tokio; +use tracing_subscriber::prelude::*; + +fn latest_trace_file(dir: &PathBuf) -> Option { + let traces_dir = dir.join("traces"); + let mut entries: Vec<_> = fs::read_dir(&traces_dir) + .ok()? + .filter_map(|e| e.ok()) + .collect(); + if entries.is_empty() { + return None; + } + entries.sort_by_key(|e| e.metadata().and_then(|m| m.modified()).ok()); + entries.last().map(|e| e.path()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn file_exporter_writes_span_json() { + // Arrange + let tmp = TempDir::new().expect("temp dir"); + let codex_home = tmp.path().to_path_buf(); + + let (guard, tracer) = telemetry::build_layer(&telemetry::Settings { + enabled: true, + exporter: telemetry::Exporter::OtlpFile { + path: PathBuf::new(), + rotate_mb: Some(100), + }, + service_name: "codex-test".to_string(), + service_version: "0.0.0".to_string(), + codex_home: Some(codex_home.clone()), + }) + .expect("build otel layer"); + + let otel_layer = tracing_opentelemetry::OpenTelemetryLayer::new(tracer); + let subscriber = tracing_subscriber::registry().with(otel_layer); + + // Act: create and drop a span within a scoped subscriber. + tracing::subscriber::with_default(subscriber, || { + let span = tracing::info_span!("test.span", test_id = %"123"); + let _entered = span.entered(); + }); + + // Drop guard to flush provider and batch processor. + drop(guard); + + // Assert: a traces file exists and contains a JSON line with the span name. + let file = latest_trace_file(&codex_home).expect("traces file should exist"); + let contents = fs::read_to_string(&file).expect("read traces file"); + assert!(!contents.is_empty(), "traces file should not be empty"); + + // Parse first line and check basic OTLP shape and span name. + let first_line = contents.lines().next().expect("at least one line"); + let v: serde_json::Value = serde_json::from_str(first_line).expect("valid json line"); + + let span_name = v["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["name"] + .as_str() + .unwrap_or(""); + assert_eq!(span_name, "test.span"); + + // Ensure required fields exist + assert!(v["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["traceId"].is_string()); + assert!(v["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["spanId"].is_string()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn session_span_flushes_on_shutdown() { + // Arrange + let tmp = TempDir::new().expect("temp dir"); + let codex_home = tmp.path().to_path_buf(); + + let (guard, tracer) = telemetry::build_layer(&telemetry::Settings { + enabled: true, + exporter: telemetry::Exporter::OtlpFile { + path: PathBuf::new(), + rotate_mb: Some(100), + }, + service_name: "codex-test".to_string(), + service_version: "0.0.0".to_string(), + codex_home: Some(codex_home.clone()), + }) + .expect("build otel layer"); + + let otel_layer = tracing_opentelemetry::OpenTelemetryLayer::new(tracer); + let subscriber = tracing_subscriber::registry().with(otel_layer); + + // Act: create a session span via helper in a scoped subscriber, then drop guard to flush + tracing::subscriber::with_default(subscriber, || { + let span = telemetry::make_session_span("s1", "model-x", "provider-y"); + drop(span); + }); + + // Drop guard to flush provider and batch processor. + drop(guard); + + // Assert: traces contain a span named "codex.session" + let file = latest_trace_file(&codex_home).expect("traces file should exist"); + let contents = fs::read_to_string(&file).expect("read traces file"); + let mut found = false; + for line in contents.lines() { + let v: serde_json::Value = match serde_json::from_str(line) { + Ok(v) => v, + Err(_) => continue, + }; + if let Some(name) = v["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["name"].as_str() { + if name == "codex.session" { + found = true; + break; + } + } + } + assert!(found, "expected a codex.session span to be exported"); +}