use super::*;use crate::responses_metadata::CodexResponsesRequestKind;use crate::responses_metadata::CompactionTurnMetadata;use crate::responses_metadata::INSTALLATION_ID_KEY;use crate::responses_metadata::WINDOW_ID_KEY;use crate::sandbox_tags::permission_profile_sandbox_tag;use codex_analytics::CompactionImplementation;use codex_analytics::CompactionPhase;use codex_analytics::CompactionReason;use codex_analytics::CompactionTrigger;use codex_protocol::models::PermissionProfile;use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;use codex_protocol::protocol::SessionSource;use codex_protocol::protocol::SubAgentSource;use codex_utils_absolute_path::AbsolutePathBuf;use core_test_support::PathBufExt;use core_test_support::PathExt;use pretty_assertions::assert_eq;use serde_json::Value;use std::collections::HashMap;use std::time::Duration;use tempfile::TempDir;use tokio::process::Command;fn test_mcp_turn_metadata_context() -> McpTurnMetadataContext<'static> { McpTurnMetadataContext { model: "gpt-5.4", reasoning_effort: Some(ReasoningEffortConfig::High), }}fn test_responses_metadata_json( state: &TurnMetadataState, window_id: &str, request_kind: CodexResponsesRequestKind,) -> String { state .to_responses_metadata( "installation-a".to_string(), window_id.to_string(), request_kind, ) .turn_metadata_json() .expect("turn metadata json")}fn test_turn_responses_metadata_json(state: &TurnMetadataState, window_id: &str) -> String { test_responses_metadata_json(state, window_id, CodexResponsesRequestKind::Turn)}fn test_compaction_responses_metadata_json( state: &TurnMetadataState, window_id: &str, compaction: CompactionTurnMetadata,) -> String { test_responses_metadata_json( state, window_id, CodexResponsesRequestKind::Compaction(compaction), )}fn test_turn_metadata_header(state: &TurnMetadataState) -> String { state .responses_metadata_template() .turn_metadata_json() .expect("header")}async fn create_clean_git_repo(repo_name: &str) -> (TempDir, AbsolutePathBuf) { let temp_dir = TempDir::new().expect("temp dir"); let repo_path = temp_dir.path().join(repo_name).abs(); std::fs::create_dir_all(&repo_path).expect("create repo"); Command::new("git") .args(["init"]) .current_dir(&repo_path) .output() .await .expect("git init"); Command::new("git") .args(["config", "user.name", "Test User"]) .current_dir(&repo_path) .output() .await .expect("git config user.name"); Command::new("git") .args(["config", "user.email", "test@example.com"]) .current_dir(&repo_path) .output() .await .expect("git config user.email"); std::fs::write(repo_path.join("README.md"), "hello").expect("write file"); Command::new("git") .args(["add", "."]) .current_dir(&repo_path) .output() .await .expect("git add"); Command::new("git") .args(["commit", "-m", "initial"]) .current_dir(&repo_path) .output() .await .expect("git commit"); (temp_dir, repo_path)}#[tokio::test]async fn detached_memory_responses_metadata_omits_turn_identity() { let (_temp_dir, repo_path) = create_clean_git_repo("repo-東京").await; let header = detached_memory_responses_metadata( String::new(), String::new(), String::new(), String::new(), &SessionSource::Unknown, &repo_path, Some("none"), ) .await .turn_metadata_json() .expect("header"); assert!(header.is_ascii()); assert!(!header.contains("東京")); let parsed: Value = serde_json::from_str(&header).expect("valid json"); assert_eq!(parsed["request_kind"].as_str(), Some("memory")); assert!(parsed.get("session_id").is_none()); assert!(parsed.get("thread_id").is_none()); assert!(parsed.get("forked_from_thread_id").is_none()); assert!(parsed.get("turn_id").is_none()); assert!(parsed.get(WINDOW_ID_KEY).is_none()); let expected_repo_path = repo_path.to_string_lossy().into_owned(); let actual_repo_path = parsed .get("workspaces") .and_then(Value::as_object) .and_then(|workspaces| workspaces.keys().next()) .expect("workspace path"); assert_eq!(actual_repo_path, &expected_repo_path); let workspace = parsed .get("workspaces") .and_then(Value::as_object) .and_then(|workspaces| workspaces.values().next()) .cloned() .expect("workspace"); assert_eq!( workspace.get("has_changes").and_then(Value::as_bool), Some(false) );}#[tokio::test]async fn detached_memory_responses_metadata_omits_empty_workspace_metadata() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let header = detached_memory_responses_metadata( String::new(), String::new(), String::new(), String::new(), &SessionSource::Unknown, &cwd, /*sandbox*/ None, ) .await .turn_metadata_json() .expect("detached memory should emit its request kind"); let parsed: Value = serde_json::from_str(&header).expect("valid json"); assert_eq!(parsed, serde_json::json!({"request_kind": "memory"}));}#[test]fn turn_metadata_state_uses_platform_sandbox_tag() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, "turn-a".to_string(), cwd, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); let header = test_turn_metadata_header(&state); let json: Value = serde_json::from_str(&header).expect("json"); let sandbox_name = json.get("sandbox").and_then(Value::as_str); let session_id = json.get("session_id").and_then(Value::as_str); let thread_id = json.get("thread_id").and_then(Value::as_str); assert!(json.get("request_kind").is_none()); let expected_sandbox = permission_profile_sandbox_tag( &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); assert_eq!(sandbox_name, Some(expected_sandbox)); assert_eq!(session_id, Some("session-a")); assert_eq!(thread_id, Some("thread-a")); assert!(json.get("forked_from_thread_id").is_none()); assert!(json.get("parent_thread_id").is_none()); assert!(json.get("subagent_kind").is_none()); assert!(json.get("session_source").is_none());}#[test]fn turn_metadata_state_includes_root_fork_lineage() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let source_thread_id = ThreadId::from_string("11111111-1111-4111-8111-111111111111").expect("thread id"); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), Some(source_thread_id), /*parent_thread_id*/ None, &SessionSource::Exec, "turn-a".to_string(), cwd, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); let header = test_turn_metadata_header(&state); let json: Value = serde_json::from_str(&header).expect("json"); assert_eq!( json["forked_from_thread_id"].as_str(), Some("11111111-1111-4111-8111-111111111111") ); assert!(json.get("parent_thread_id").is_none()); assert!(json.get("subagent_kind").is_none());}#[test]fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let parent_thread_id = ThreadId::from_string("22222222-2222-4222-8222-222222222222").expect("thread id"); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), /*forked_from_thread_id*/ None, Some(parent_thread_id), &SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, }), "turn-a".to_string(), cwd, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); let header = test_turn_metadata_header(&state); let json: Value = serde_json::from_str(&header).expect("json"); assert!(json.get("forked_from_thread_id").is_none()); assert_eq!( json["parent_thread_id"].as_str(), Some("22222222-2222-4222-8222-222222222222") ); assert_eq!(json["subagent_kind"].as_str(), Some("thread_spawn"));}#[test]fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let parent_thread_id = ThreadId::from_string("33333333-3333-4333-8333-333333333333").expect("thread id"); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), Some(parent_thread_id), Some(parent_thread_id), &SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, }), "turn-a".to_string(), cwd, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); let header = test_turn_metadata_header(&state); let json: Value = serde_json::from_str(&header).expect("json"); assert_eq!( json["forked_from_thread_id"].as_str(), Some("33333333-3333-4333-8333-333333333333") ); assert_eq!( json["parent_thread_id"].as_str(), Some("33333333-3333-4333-8333-333333333333") ); assert_eq!(json["subagent_kind"].as_str(), Some("thread_spawn"));}#[test]fn turn_metadata_state_includes_known_parent_for_non_thread_spawn_subagents_without_fork() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let parent_thread_id = ThreadId::from_string("44444444-4444-4444-8444-444444444444").expect("thread id"); let sources = [ (SubAgentSource::Review, "review"), (SubAgentSource::Other("guardian".to_string()), "guardian"), ( SubAgentSource::Other("agent_job:job-1".to_string()), "agent_job:job-1", ), ]; for (subagent_source, subagent_kind) in sources { let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), /*forked_from_thread_id*/ None, Some(parent_thread_id), &SessionSource::SubAgent(subagent_source), "turn-a".to_string(), cwd.clone(), &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); let header = test_turn_metadata_header(&state); let json: Value = serde_json::from_str(&header).expect("json"); assert!(json.get("forked_from_thread_id").is_none()); assert_eq!( json["parent_thread_id"].as_str(), Some("44444444-4444-4444-8444-444444444444") ); assert_eq!(json["subagent_kind"].as_str(), Some(subagent_kind)); }}#[test]fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, "turn-a".to_string(), cwd, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); state.set_turn_started_at_unix_ms(/*turn_started_at_unix_ms*/ 1_700_000_000_123); let header = test_turn_metadata_header(&state); let json: Value = serde_json::from_str(&header).expect("json"); assert_eq!( json["turn_started_at_unix_ms"].as_i64(), Some(1_700_000_000_123) );}#[test]fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, "turn-a".to_string(), cwd, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); let header = test_turn_metadata_header(&state); let header_json: Value = serde_json::from_str(&header).expect("json"); assert!(header_json.get("model").is_none()); assert!(header_json.get("reasoning_effort").is_none()); let meta = state .current_meta_value_for_mcp_request(test_mcp_turn_metadata_context()) .expect("turn metadata should be present"); assert!(meta.get("request_kind").is_none()); assert_eq!(meta["model"].as_str(), Some("gpt-5.4")); assert_eq!(meta["reasoning_effort"].as_str(), Some("high")); let meta_without_reasoning_effort = state .current_meta_value_for_mcp_request(McpTurnMetadataContext { model: "gpt-5.4", reasoning_effort: None, }) .expect("turn metadata should be present"); assert_eq!( meta_without_reasoning_effort["model"].as_str(), Some("gpt-5.4") ); assert!( meta_without_reasoning_effort .get("reasoning_effort") .is_none() );}#[test]fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_request_meta() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, "turn-a".to_string(), cwd, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); let header = test_turn_metadata_header(&state); let header_json: Value = serde_json::from_str(&header).expect("json"); assert!( header_json .get(USER_INPUT_REQUESTED_DURING_TURN_KEY) .is_none() ); let meta = state .current_meta_value_for_mcp_request(test_mcp_turn_metadata_context()) .expect("turn metadata should be present"); assert!(meta.get(USER_INPUT_REQUESTED_DURING_TURN_KEY).is_none()); state.mark_user_input_requested_during_turn(); let header = test_turn_metadata_header(&state); let header_json: Value = serde_json::from_str(&header).expect("json"); assert!( header_json .get(USER_INPUT_REQUESTED_DURING_TURN_KEY) .is_none() ); let meta = state .current_meta_value_for_mcp_request(test_mcp_turn_metadata_context()) .expect("turn metadata should be present"); assert_eq!( meta.get(USER_INPUT_REQUESTED_DURING_TURN_KEY) .and_then(Value::as_bool), Some(true) );}#[test]fn turn_metadata_state_ignores_client_reserved_metadata_before_start() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, "turn-a".to_string(), cwd, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); state.set_responsesapi_client_metadata(HashMap::from([ ( "turn_started_at_unix_ms".to_string(), "client-supplied".to_string(), ), ( "forked_from_thread_id".to_string(), "client-supplied".to_string(), ), ( "parent_thread_id".to_string(), "client-supplied".to_string(), ), ("subagent_kind".to_string(), "client-supplied".to_string()), ])); let header = test_turn_metadata_header(&state); let json: Value = serde_json::from_str(&header).expect("json"); assert!(json.get("turn_started_at_unix_ms").is_none()); assert!(json.get("forked_from_thread_id").is_none()); assert!(json.get("parent_thread_id").is_none()); assert!(json.get("subagent_kind").is_none());}#[test]fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let source_thread_id = ThreadId::from_string("44444444-4444-4444-8444-444444444444").expect("thread id"); let parent_thread_id = ThreadId::from_string("55555555-5555-4555-8555-555555555555").expect("thread id"); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), Some(source_thread_id), Some(parent_thread_id), &SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, }), "turn-a".to_string(), cwd, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); state.set_responsesapi_client_metadata(HashMap::from([ ("fiber_run_id".to_string(), "fiber-123".to_string()), ("origin".to_string(), "東京".to_string()), ("workspace_kind".to_string(), "projectless".to_string()), ("model".to_string(), "client-supplied".to_string()), ( "reasoning_effort".to_string(), "client-supplied".to_string(), ), ("session_id".to_string(), "client-supplied".to_string()), ("thread_id".to_string(), "client-supplied".to_string()), ("installation_id".to_string(), "client-supplied".to_string()), ( "x-codex-installation-id".to_string(), "client-supplied".to_string(), ), ( "x-codex-parent-thread-id".to_string(), "client-supplied".to_string(), ), ( "x-openai-subagent".to_string(), "client-supplied".to_string(), ), ( "forked_from_thread_id".to_string(), "client-supplied".to_string(), ), ( "parent_thread_id".to_string(), "client-supplied".to_string(), ), ("subagent_kind".to_string(), "client-supplied".to_string()), ("turn_id".to_string(), "client-supplied".to_string()), (WINDOW_ID_KEY.to_string(), "client-supplied".to_string()), ("thread_source".to_string(), "client-supplied".to_string()), ("request_kind".to_string(), "client-supplied".to_string()), ( "turn_started_at_unix_ms".to_string(), "client-supplied".to_string(), ), ])); state.set_turn_started_at_unix_ms(/*turn_started_at_unix_ms*/ 1_700_000_000_123); let header = test_turn_metadata_header(&state); assert!(header.is_ascii()); assert!(!header.contains("東京")); let json: Value = serde_json::from_str(&header).expect("json"); assert_eq!(json["fiber_run_id"].as_str(), Some("fiber-123")); assert_eq!(json["origin"].as_str(), Some("東京")); assert_eq!(json["workspace_kind"].as_str(), Some("projectless")); assert_eq!(json["model"].as_str(), Some("client-supplied")); assert_eq!(json["reasoning_effort"].as_str(), Some("client-supplied")); assert_eq!(json["session_id"].as_str(), Some("session-a")); assert_eq!(json["thread_id"].as_str(), Some("thread-a")); assert!(json.get(INSTALLATION_ID_KEY).is_none()); assert!(json.get("x-codex-installation-id").is_none()); assert!(json.get("x-codex-parent-thread-id").is_none()); assert!(json.get("x-openai-subagent").is_none()); assert_eq!( json["forked_from_thread_id"].as_str(), Some("44444444-4444-4444-8444-444444444444") ); assert_eq!( json["parent_thread_id"].as_str(), Some("55555555-5555-4555-8555-555555555555") ); assert_eq!(json["subagent_kind"].as_str(), Some("thread_spawn")); assert_eq!(json["thread_source"].as_str(), Some("client-supplied")); assert_eq!(json["turn_id"].as_str(), Some("turn-a")); assert!(json.get("request_kind").is_none()); assert!(json.get(WINDOW_ID_KEY).is_none()); assert_eq!( json["turn_started_at_unix_ms"].as_i64(), Some(1_700_000_000_123) ); let model_request_header = test_turn_responses_metadata_json(&state, "thread-a:1"); let model_request_json: Value = serde_json::from_str(&model_request_header).expect("model request json"); assert_eq!(model_request_json["request_kind"].as_str(), Some("turn")); assert_eq!( model_request_json[INSTALLATION_ID_KEY].as_str(), Some("installation-a") ); assert_eq!( model_request_json[WINDOW_ID_KEY].as_str(), Some("thread-a:1") ); let meta = state .current_meta_value_for_mcp_request(test_mcp_turn_metadata_context()) .expect("turn metadata should be present"); assert_eq!(meta["model"].as_str(), Some("gpt-5.4")); assert_eq!(meta["reasoning_effort"].as_str(), Some("high")); assert!(meta.get(WINDOW_ID_KEY).is_none()); assert_eq!(state.workspace_kind().as_deref(), Some("projectless"));}#[test]fn turn_metadata_state_overlays_compaction_only_on_compaction_requests() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let permission_profile = PermissionProfile::read_only(); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, "turn-a".to_string(), cwd, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); state.set_responsesapi_client_metadata(HashMap::from([( "compaction".to_string(), "client-supplied".to_string(), )])); let compact_header = test_compaction_responses_metadata_json( &state, "thread-a:2", CompactionTurnMetadata::new( CompactionTrigger::Auto, CompactionReason::ContextLimit, CompactionImplementation::ResponsesCompactionV2, CompactionPhase::MidTurn, ), ); let compact_json: Value = serde_json::from_str(&compact_header).expect("json"); assert_eq!(compact_json["request_kind"].as_str(), Some("compaction")); assert_eq!(compact_json["turn_id"].as_str(), Some("turn-a")); assert_eq!(compact_json[WINDOW_ID_KEY].as_str(), Some("thread-a:2")); assert_eq!( compact_json["compaction"], serde_json::json!({ "trigger": "auto", "reason": "context_limit", "implementation": "responses_compaction_v2", "phase": "mid_turn", "strategy": "memento", }) ); let regular_header = test_turn_responses_metadata_json(&state, "thread-a:3"); let regular_json: Value = serde_json::from_str(®ular_header).expect("json"); assert_eq!(regular_json["request_kind"].as_str(), Some("turn")); assert_eq!(regular_json[WINDOW_ID_KEY].as_str(), Some("thread-a:3")); assert!(regular_json.get("compaction").is_none());}#[tokio::test]async fn turn_metadata_state_preserves_lineage_after_git_enrichment() { let (_temp_dir, repo_path) = create_clean_git_repo("repo").await; let permission_profile = PermissionProfile::read_only(); let parent_thread_id = ThreadId::from_string("66666666-6666-4666-8666-666666666666").expect("thread id"); let state = TurnMetadataState::new( "session-a".to_string(), "thread-a".to_string(), Some(parent_thread_id), Some(parent_thread_id), &SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, }), "turn-a".to_string(), repo_path, &permission_profile, WindowsSandboxLevel::Disabled, /*enforce_managed_network*/ false, ); state.spawn_git_enrichment_task(); let json = tokio::time::timeout(Duration::from_secs(2), async { loop { let header = test_turn_metadata_header(&state); let json: Value = serde_json::from_str(&header).expect("json"); if json .get("workspaces") .and_then(Value::as_object) .is_some_and(|workspaces| !workspaces.is_empty()) { return json; } tokio::time::sleep(Duration::from_millis(10)).await; } }) .await .expect("git enrichment should complete"); assert_eq!( json["forked_from_thread_id"].as_str(), Some("66666666-6666-4666-8666-666666666666") ); assert_eq!( json["parent_thread_id"].as_str(), Some("66666666-6666-4666-8666-666666666666") ); assert_eq!(json["subagent_kind"].as_str(), Some("thread_spawn"));}