mirror of
https://github.com/openai/codex.git
synced 2026-04-26 15:45:02 +00:00
make codex better at git (#10145)
adds basic git context to the session prefix so the model can anchor git actions and be a bit more version-aware. structured it in a multiroot-friendly shape even though we only have one root today
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
@@ -23,6 +24,7 @@ use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use futures::StreamExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::matchers::header;
|
||||
|
||||
@@ -98,7 +100,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
|
||||
session_source,
|
||||
TransportManager::new(),
|
||||
)
|
||||
.new_session();
|
||||
.new_session(None);
|
||||
|
||||
let mut prompt = Prompt::default();
|
||||
prompt.input = vec![ResponseItem::Message {
|
||||
@@ -197,7 +199,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
|
||||
session_source,
|
||||
TransportManager::new(),
|
||||
)
|
||||
.new_session();
|
||||
.new_session(None);
|
||||
|
||||
let mut prompt = Prompt::default();
|
||||
prompt.input = vec![ResponseItem::Message {
|
||||
@@ -354,7 +356,7 @@ async fn responses_respects_model_info_overrides_from_config() {
|
||||
session_source,
|
||||
TransportManager::new(),
|
||||
)
|
||||
.new_session();
|
||||
.new_session(None);
|
||||
|
||||
let mut prompt = Prompt::default();
|
||||
prompt.input = vec![ResponseItem::Message {
|
||||
@@ -393,3 +395,196 @@ async fn responses_respects_model_info_overrides_from_config() {
|
||||
Some("detailed")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn responses_stream_includes_turn_metadata_header_for_git_workspace_e2e() {
|
||||
core_test_support::skip_if_no_network!();
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_body = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let provider = ModelProviderInfo {
|
||||
name: "mock".into(),
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
env_key: None,
|
||||
env_key_instructions: None,
|
||||
experimental_bearer_token: None,
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
request_max_retries: Some(0),
|
||||
stream_max_retries: Some(0),
|
||||
stream_idle_timeout_ms: Some(5_000),
|
||||
requires_openai_auth: false,
|
||||
supports_websockets: false,
|
||||
};
|
||||
|
||||
let codex_home = TempDir::new().expect("failed to create TempDir");
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
config.model_provider_id = provider.name.clone();
|
||||
config.model_provider = provider.clone();
|
||||
let effort = config.model_reasoning_effort;
|
||||
let summary = config.model_reasoning_summary;
|
||||
let model = ModelsManager::get_model_offline(config.model.as_deref());
|
||||
config.model = Some(model.clone());
|
||||
let config = Arc::new(config);
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let auth_mode = AuthMode::Chatgpt;
|
||||
let session_source =
|
||||
SessionSource::SubAgent(SubAgentSource::Other("turn-metadata-e2e".to_string()));
|
||||
let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config);
|
||||
let otel_manager = OtelManager::new(
|
||||
conversation_id,
|
||||
model.as_str(),
|
||||
model_info.slug.as_str(),
|
||||
None,
|
||||
Some("test@test.com".to_string()),
|
||||
Some(auth_mode),
|
||||
false,
|
||||
"test".to_string(),
|
||||
session_source.clone(),
|
||||
);
|
||||
|
||||
let client = ModelClient::new(
|
||||
Arc::clone(&config),
|
||||
None,
|
||||
model_info,
|
||||
otel_manager,
|
||||
provider,
|
||||
effort,
|
||||
summary,
|
||||
conversation_id,
|
||||
session_source,
|
||||
TransportManager::new(),
|
||||
);
|
||||
|
||||
let workspace = TempDir::new().expect("workspace tempdir");
|
||||
let cwd = workspace.path();
|
||||
|
||||
let mut prompt = Prompt::default();
|
||||
prompt.input = vec![ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".into(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "hello".into(),
|
||||
}],
|
||||
end_turn: None,
|
||||
}];
|
||||
|
||||
let first_request = responses::mount_sse_once(&server, response_body.clone()).await;
|
||||
let mut first_session = client.new_session(Some(cwd.to_path_buf()));
|
||||
let mut first_stream = first_session
|
||||
.stream(&prompt)
|
||||
.await
|
||||
.expect("stream first turn");
|
||||
while let Some(event) = first_stream.next().await {
|
||||
if matches!(event, Ok(ResponseEvent::Completed { .. })) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
first_request
|
||||
.single_request()
|
||||
.header("x-codex-turn-metadata"),
|
||||
None
|
||||
);
|
||||
|
||||
let git_config_global = cwd.join("empty-git-config");
|
||||
std::fs::write(&git_config_global, "").expect("write empty git config");
|
||||
let run_git = |args: &[&str]| {
|
||||
let output = Command::new("git")
|
||||
.env("GIT_CONFIG_GLOBAL", &git_config_global)
|
||||
.env("GIT_CONFIG_NOSYSTEM", "1")
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.expect("git command should run");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"git {:?} failed: stdout={} stderr={}",
|
||||
args,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
output
|
||||
};
|
||||
|
||||
run_git(&["init"]);
|
||||
run_git(&["config", "user.name", "Test User"]);
|
||||
run_git(&["config", "user.email", "test@example.com"]);
|
||||
std::fs::write(cwd.join("README.md"), "hello").expect("write README");
|
||||
run_git(&["add", "."]);
|
||||
run_git(&["commit", "-m", "initial commit"]);
|
||||
run_git(&[
|
||||
"remote",
|
||||
"add",
|
||||
"origin",
|
||||
"https://github.com/openai/codex.git",
|
||||
]);
|
||||
|
||||
let expected_head = String::from_utf8(run_git(&["rev-parse", "HEAD"]).stdout)
|
||||
.expect("git rev-parse output should be valid UTF-8")
|
||||
.trim()
|
||||
.to_string();
|
||||
let expected_origin = String::from_utf8(run_git(&["remote", "get-url", "origin"]).stdout)
|
||||
.expect("git remote get-url output should be valid UTF-8")
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let repo_root = std::fs::canonicalize(cwd)
|
||||
.unwrap_or_else(|_| cwd.to_path_buf())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
loop {
|
||||
let request_recorder = responses::mount_sse_once(&server, response_body.clone()).await;
|
||||
let mut session = client.new_session(Some(cwd.to_path_buf()));
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
let mut stream = session.stream(&prompt).await.expect("stream post-git turn");
|
||||
while let Some(event) = stream.next().await {
|
||||
if matches!(event, Ok(ResponseEvent::Completed { .. })) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let maybe_header = request_recorder
|
||||
.single_request()
|
||||
.header("x-codex-turn-metadata");
|
||||
if let Some(header_value) = maybe_header {
|
||||
let parsed: serde_json::Value = serde_json::from_str(&header_value)
|
||||
.expect("x-codex-turn-metadata should be valid JSON");
|
||||
let workspace = parsed
|
||||
.get("workspaces")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|workspaces| workspaces.get(&repo_root))
|
||||
.expect("metadata should include cwd repo root workspace entry");
|
||||
|
||||
assert_eq!(
|
||||
workspace
|
||||
.get("latest_git_commit_hash")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some(expected_head.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
workspace
|
||||
.get("associated_remote_urls")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|remotes| remotes.get("origin"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some(expected_origin.as_str())
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
||||
}
|
||||
|
||||
panic!("x-codex-turn-metadata was never observed within 5s after git setup");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user