Files
codex/codex-rs/core/src/turn_metadata_tests.rs
T
mchen-oai ccec84b148 Add turn start timestamp to turn metadata (#19473)
## Why
- Without change: MCP tool calls receive
`_meta["x-codex-turn-metadata"]` with `session_id` and `turn_id`.
- Issue: MCP servers may want the turn start timestamp to measure
internal latency relative to turn start.

## What Changed
- With change: turn metadata now includes `turn_started_at_unix_ms`,
which is propagated to MCP tool calls in
`_meta["x-codex-turn-metadata"]`.

## Verification
- `codex-rs/core/src/mcp_tool_call_tests.rs`
- `codex-rs/core/src/turn_metadata_tests.rs`
- `codex-rs/core/src/turn_timing_tests.rs`
- `codex-rs/core/tests/responses_headers.rs`
- `codex-rs/core/tests/suite/search_tool.rs`
2026-04-28 16:36:59 +00:00

215 lines
7.0 KiB
Rust

use super::*;
use crate::sandbox_tags::sandbox_tag;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use serde_json::Value;
use std::collections::HashMap;
use tempfile::TempDir;
use tokio::process::Command;
#[tokio::test]
async fn build_turn_metadata_header_includes_has_changes_for_clean_repo() {
let temp_dir = TempDir::new().expect("temp dir");
let repo_path = temp_dir.path().join("repo").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");
let header = build_turn_metadata_header(&repo_path, Some("none"))
.await
.expect("header");
let parsed: Value = serde_json::from_str(&header).expect("valid json");
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)
);
}
#[test]
fn turn_metadata_state_uses_platform_sandbox_tag() {
let temp_dir = TempDir::new().expect("temp dir");
let cwd = temp_dir.path().abs();
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let permission_profile = PermissionProfile::read_only();
let state = TurnMetadataState::new(
"session-a".to_string(),
&SessionSource::Exec,
"turn-a".to_string(),
cwd,
&permission_profile,
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
);
let header = state.current_header_value().expect("header");
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_source = json.get("thread_source").and_then(Value::as_str);
let expected_sandbox = sandbox_tag(&sandbox_policy, WindowsSandboxLevel::Disabled);
assert_eq!(sandbox_name, Some(expected_sandbox));
assert_eq!(session_id, Some("session-a"));
assert_eq!(thread_source, Some("user"));
assert!(json.get("session_source").is_none());
}
#[test]
fn turn_metadata_state_classifies_subagent_thread_source() {
let temp_dir = TempDir::new().expect("temp dir");
let cwd = temp_dir.path().abs();
let permission_profile = PermissionProfile::read_only();
let session_source = SessionSource::SubAgent(SubAgentSource::Review);
let state = TurnMetadataState::new(
"session-a".to_string(),
&session_source,
"turn-a".to_string(),
cwd,
&permission_profile,
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
);
let header = state.current_header_value().expect("header");
let json: Value = serde_json::from_str(&header).expect("json");
assert_eq!(json["thread_source"].as_str(), Some("subagent"));
assert!(json.get("session_source").is_none());
}
#[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(),
&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 = state.current_header_value().expect("header");
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_ignores_client_turn_started_at_unix_ms_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(),
&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(),
)]));
let header = state.current_header_value().expect("header");
let json: Value = serde_json::from_str(&header).expect("json");
assert!(json.get("turn_started_at_unix_ms").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 state = TurnMetadataState::new(
"session-a".to_string(),
&SessionSource::Exec,
"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()),
("session_id".to_string(), "client-supplied".to_string()),
("thread_source".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 = state.current_header_value().expect("header");
let json: Value = serde_json::from_str(&header).expect("json");
assert_eq!(json["fiber_run_id"].as_str(), Some("fiber-123"));
assert_eq!(json["session_id"].as_str(), Some("session-a"));
assert_eq!(json["thread_source"].as_str(), Some("user"));
assert_eq!(json["turn_id"].as_str(), Some("turn-a"));
assert_eq!(
json["turn_started_at_unix_ms"].as_i64(),
Some(1_700_000_000_123)
);
}