Codex Handbook
rollout-trace/src/thread_tests.rs 216 lines
use std::cell::Cell;use std::fs;use std::path::Path;use std::path::PathBuf;use codex_protocol::AgentPath;use codex_protocol::ThreadId;use codex_protocol::protocol::AgentStatus;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::SandboxPolicy;use codex_protocol::protocol::SessionSource;use codex_protocol::protocol::SubAgentSource;use tempfile::TempDir;use super::*;use crate::AgentResultTracePayload;use crate::CompactionCheckpointTracePayload;use crate::ExecutionStatus;use crate::RawTraceEventPayload;use crate::RolloutStatus;use crate::replay_bundle;#[test]fn create_in_root_writes_replayable_lifecycle_events() -> anyhow::Result<()> {    let temp = TempDir::new()?;    let thread_id = ThreadId::new();    let thread_trace = ThreadTraceContext::start_root_in_root_for_test(        temp.path(),        ThreadStartedTraceMetadata {            thread_id: thread_id.to_string(),            agent_path: "/root".to_string(),            task_name: None,            nickname: None,            agent_role: None,            session_source: SessionSource::Exec,            cwd: PathBuf::from("/workspace"),            rollout_path: Some(PathBuf::from("/tmp/rollout.jsonl")),            model: "gpt-test".to_string(),            provider_name: "test-provider".to_string(),            approval_policy: "never".to_string(),            sandbox_policy: format!("{:?}", SandboxPolicy::DangerFullAccess),        },    )?;    thread_trace.record_ended(RolloutStatus::Completed);    let bundle_dir = single_bundle_dir(temp.path())?;    let replayed = replay_bundle(&bundle_dir)?;    assert_eq!(replayed.status, RolloutStatus::Completed);    assert_eq!(replayed.root_thread_id, thread_id.to_string());    assert_eq!(replayed.threads[&thread_id.to_string()].agent_path, "/root");    assert_eq!(replayed.raw_payloads.len(), 1);    Ok(())}#[test]fn spawned_thread_start_appends_to_root_bundle() -> anyhow::Result<()> {    let temp = TempDir::new()?;    let root_thread_id = ThreadId::new();    let child_thread_id = ThreadId::new();    let root_trace = ThreadTraceContext::start_root_in_root_for_test(        temp.path(),        minimal_metadata(root_thread_id),    )?;    let child_trace = root_trace.start_child_thread_trace_or_disabled(ThreadStartedTraceMetadata {        thread_id: child_thread_id.to_string(),        agent_path: "/root/repo_file_counter".to_string(),        task_name: Some("repo_file_counter".to_string()),        nickname: Some("Kepler".to_string()),        agent_role: Some("worker".to_string()),        session_source: SessionSource::SubAgent(SubAgentSource::ThreadSpawn {            parent_thread_id: root_thread_id,            depth: 1,            agent_path: Some(                AgentPath::try_from("/root/repo_file_counter").map_err(anyhow::Error::msg)?,            ),            agent_nickname: Some("Kepler".to_string()),            agent_role: Some("worker".to_string()),        }),        cwd: PathBuf::from("/workspace"),        rollout_path: Some(PathBuf::from("/tmp/child-rollout.jsonl")),        model: "gpt-test".to_string(),        provider_name: "test-provider".to_string(),        approval_policy: "never".to_string(),        sandbox_policy: format!("{:?}", SandboxPolicy::DangerFullAccess),    });    child_trace.record_ended(RolloutStatus::Completed);    let bundle_dir = single_bundle_dir(temp.path())?;    let replayed = replay_bundle(&bundle_dir)?;    assert_eq!(fs::read_dir(temp.path())?.count(), 1);    assert_eq!(replayed.threads.len(), 2);    assert_eq!(        replayed.threads[&child_thread_id.to_string()].agent_path,        "/root/repo_file_counter"    );    assert_eq!(replayed.status, RolloutStatus::Running);    assert_eq!(        replayed.threads[&child_thread_id.to_string()]            .execution            .status,        ExecutionStatus::Completed    );    assert_eq!(replayed.raw_payloads.len(), 2);    Ok(())}#[test]fn disabled_thread_context_accepts_trace_calls_without_writing() -> anyhow::Result<()> {    let temp = TempDir::new()?;    let thread_trace = ThreadTraceContext::disabled();    thread_trace.record_ended(RolloutStatus::Completed);    thread_trace.record_protocol_event(&EventMsg::ShutdownComplete);    thread_trace.record_codex_turn_event("turn-1", &EventMsg::ShutdownComplete);    thread_trace.record_tool_call_event("turn-1", &EventMsg::ShutdownComplete);    thread_trace.record_agent_result_interaction(        "turn-1",        ThreadId::new(),        &AgentResultTracePayload {            child_agent_path: "/root/child",            message: "done",            status: &AgentStatus::Completed(Some("done".to_string())),        },    );    let inference_trace =        thread_trace.inference_trace_context("turn-1", "gpt-test", "test-provider");    let inference_attempt = inference_trace.start_attempt();    inference_attempt.record_started(&serde_json::json!({ "kind": "inference" }));    let token_usage: Option<codex_protocol::protocol::TokenUsage> = None;    inference_attempt.record_completed("response-1", Some("req-1"), &token_usage, &[]);    inference_attempt.record_failed("inference failed", /*upstream_request_id*/ None, &[]);    let compaction_trace = thread_trace.compaction_trace_context(        "turn-1",        "compaction-1",        "gpt-test",        "test-provider",    );    let compaction_attempt =        compaction_trace.start_attempt(&serde_json::json!({ "kind": "compaction" }));    compaction_attempt.record_completed(&[]);    compaction_attempt.record_failed("compaction failed");    compaction_trace.record_installed(&CompactionCheckpointTracePayload {        input_history: &[],        replacement_history: &[],    });    let built_dispatch_invocation = Cell::new(false);    let dispatch_trace = thread_trace.start_tool_dispatch_trace(|| {        built_dispatch_invocation.set(true);        None    });    assert!(!built_dispatch_invocation.get());    assert!(!dispatch_trace.is_enabled());    assert_eq!(fs::read_dir(temp.path())?.count(), 0);    Ok(())}#[test]fn protocol_wrapper_records_selected_events_as_raw_payloads() -> anyhow::Result<()> {    let temp = TempDir::new()?;    let thread_id = ThreadId::new();    let thread_trace =        ThreadTraceContext::start_root_in_root_for_test(temp.path(), minimal_metadata(thread_id))?;    thread_trace.record_protocol_event(&EventMsg::ShutdownComplete);    let event_log = fs::read_to_string(single_bundle_dir(temp.path())?.join("trace.jsonl"))?;    let protocol_event_seen = event_log.lines().any(|line| {        let event: crate::RawTraceEvent = serde_json::from_str(line).expect("raw trace event");        matches!(            event.payload,            RawTraceEventPayload::ProtocolEventObserved {                event_type,                ..            } if event_type == "shutdown_complete"        )    });    assert!(protocol_event_seen);    Ok(())}fn minimal_metadata(thread_id: ThreadId) -> ThreadStartedTraceMetadata {    ThreadStartedTraceMetadata {        thread_id: thread_id.to_string(),        agent_path: "/root".to_string(),        task_name: None,        nickname: None,        agent_role: None,        session_source: SessionSource::Exec,        cwd: PathBuf::from("/workspace"),        rollout_path: None,        model: "gpt-test".to_string(),        provider_name: "test-provider".to_string(),        approval_policy: "never".to_string(),        sandbox_policy: "danger-full-access".to_string(),    }}fn single_bundle_dir(root: &Path) -> anyhow::Result<PathBuf> {    let mut entries = fs::read_dir(root)?        .map(|entry| entry.map(|entry| entry.path()))        .collect::<Result<Vec<_>, _>>()?;    entries.sort();    assert_eq!(entries.len(), 1);    Ok(entries.remove(0))}