From d22c2eef53f95bdeb5a9f61e0d453a4febeb5201 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Mon, 27 Oct 2025 12:49:56 -0700 Subject: [PATCH] prompt_caching test --- codex-rs/core/tests/suite/prompt_caching.rs | 172 ++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 1bf12eb954..2790b35c9f 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -21,6 +21,7 @@ use core_test_support::load_sse_fixture_with_id; use core_test_support::skip_if_no_network; use core_test_support::wait_for_event; use std::collections::HashMap; +use std::process::Command as StdCommand; use tempfile::TempDir; use wiremock::Mock; use wiremock::MockServer; @@ -883,3 +884,174 @@ async fn send_user_turn_with_changes_sends_environment_context() { ]); assert_eq!(body2["input"], expected_input_2); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn environment_context_includes_and_clears_git_info_on_cwd_change() { + skip_if_no_network!(); + use pretty_assertions::assert_eq; + + let server = MockServer::start().await; + + let sse = sse_completed("resp"); + let template = ResponseTemplate::new(200) + .insert_header("content-type", "text/event-stream") + .set_body_raw(sse, "text/event-stream"); + + // Expect two POSTs to /v1/responses + Mock::given(method("POST")) + .and(path("/v1/responses")) + .respond_with(template) + .expect(2) + .mount(&server) + .await; + + let model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + + // Create a git repository for the initial cwd so that git_info is present + let repo_dir = TempDir::new().unwrap(); + let repo_path = repo_dir.path(); + let envs = vec![ + ("GIT_CONFIG_GLOBAL", "/dev/null"), + ("GIT_CONFIG_NOSYSTEM", "1"), + ]; + StdCommand::new("git") + .envs(envs.clone()) + .args(["init"]) + .current_dir(repo_path) + .output() + .expect("git init should succeed"); + StdCommand::new("git") + .envs(envs.clone()) + .args(["config", "user.name", "Test User"]) + .current_dir(repo_path) + .output() + .expect("git config user.name should succeed"); + StdCommand::new("git") + .envs(envs.clone()) + .args(["config", "user.email", "test@example.com"]) + .current_dir(repo_path) + .output() + .expect("git config user.email should succeed"); + std::fs::write(repo_path.join("test.txt"), "content").unwrap(); + StdCommand::new("git") + .envs(envs.clone()) + .args(["add", "."]) + .current_dir(repo_path) + .output() + .expect("git add should succeed"); + let commit_out = StdCommand::new("git") + .envs(envs) + .args(["commit", "-m", "initial"]) + .current_dir(repo_path) + .output() + .expect("git commit should succeed"); + assert!(commit_out.status.success(), "git commit failed"); + + // non-git directory for the override to ensure git_info is cleared + let non_git_dir = TempDir::new().unwrap(); + + let codex_home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&codex_home); + config.cwd = repo_path.to_path_buf(); + config.model_provider = model_provider; + config.user_instructions = Some("be consistent and helpful".to_string()); + + let conversation_manager = + ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key")); + let codex = conversation_manager + .new_conversation(config) + .await + .expect("create new conversation") + .conversation; + + // First turn in the git repo + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "hello 1".into(), + }], + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + // Override cwd to a non-git directory (this should clear git_info) + codex + .submit(Op::OverrideTurnContext { + cwd: Some(non_git_dir.path().to_path_buf()), + approval_policy: None, + sandbox_policy: None, + model: None, + effort: None, + summary: None, + }) + .await + .unwrap(); + + // Second turn after cwd override + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "hello 2".into(), + }], + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + // Verify we issued exactly two requests + let requests = server.received_requests().await.unwrap(); + assert_eq!(requests.len(), 2, "expected two POST requests"); + + let body1 = requests[0].body_json::().unwrap(); + let body2 = requests[1].body_json::().unwrap(); + + // prompt_cache_key should remain constant across cwd change + assert_eq!( + body1["prompt_cache_key"], body2["prompt_cache_key"], + "prompt_cache_key should not change across cwd override" + ); + + // Find the environment context message in the first request + let input1 = body1["input"].as_array().expect("input must be an array"); + let env_text_1 = input1 + .iter() + .filter_map(|it| it.get("content")) + .filter_map(|c| c.as_array()) + .flat_map(|arr| arr.iter()) + .filter_map(|v| v.get("text")) + .filter_map(|v| v.as_str()) + .find(|t| t.contains("")) + .expect("first request should contain environment_context text"); + assert!( + env_text_1.contains(""), + "initial environment_context should include " + ); + assert!( + env_text_1.contains(""), + "git_info should include commit_hash" + ); + assert!( + env_text_1.contains(""), + "git_info should include branch when on a branch" + ); + + // The second request should include an environment_context update that clears git_info + let input2 = body2["input"].as_array().expect("input must be an array"); + let env_text_2 = input2 + .iter() + .filter_map(|it| it.get("content")) + .filter_map(|c| c.as_array()) + .flat_map(|arr| arr.iter()) + .filter_map(|v| v.get("text")) + .filter_map(|v| v.as_str()) + .rfind(|t| t.contains("")) + .expect("second request should contain environment_context text"); + assert!( + env_text_2.contains(""), + "environment_context after cwd override should include a cleared " + ); +}