diff --git a/codex-rs/core/tests/env_snapshot.rs b/codex-rs/core/tests/env_snapshot.rs index 92b284b085..81c4607ab6 100644 --- a/codex-rs/core/tests/env_snapshot.rs +++ b/codex-rs/core/tests/env_snapshot.rs @@ -1,3 +1,5 @@ +#![allow(clippy::expect_used, clippy::unwrap_used)] + use codex_core::Codex; use codex_core::CodexSpawnOk; use codex_core::ModelProviderInfo; @@ -19,8 +21,6 @@ use wiremock::matchers::path; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_turn_includes_environment_snapshot() { - #![allow(clippy::unwrap_used)] - if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { println!( "Skipping test because it cannot execute when network is disabled in a Codex sandbox." @@ -101,3 +101,89 @@ async fn first_turn_includes_environment_snapshot() { let second_text = first_input["content"][1]["text"].as_str().unwrap(); assert_eq!(second_text, "hello"); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn snapshot_is_not_injected_on_second_turn() { + if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + // Prepare cwd with a couple of files (and a hidden one). + let cwd = TempDir::new().unwrap(); + std::fs::write(cwd.path().join("first.txt"), b"x").unwrap(); + std::fs::write(cwd.path().join("second.txt"), b"x").unwrap(); + std::fs::write(cwd.path().join(".dot"), b"x").unwrap(); + + // Mock server that accepts two requests and completes both. + let server = MockServer::start().await; + let sse = load_sse_fixture_with_id("tests/fixtures/completed_template.json", "resp1"); + let responder = ResponseTemplate::new(200) + .insert_header("content-type", "text/event-stream") + .set_body_raw(sse, "text/event-stream"); + Mock::given(method("POST")) + .and(path("/v1/responses")) + .respond_with(responder) + .expect(2) + .mount(&server) + .await; + + let model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + + let codex_home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&codex_home); + config.model_provider = model_provider; + config.cwd = cwd.path().to_path_buf(); + + let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); + let CodexSpawnOk { codex, .. } = Codex::spawn( + config, + Some(CodexAuth::from_api_key("Test API Key".to_string())), + ctrl_c.clone(), + ) + .await + .unwrap(); + + // First user message. + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "first".into(), + }], + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + // Second user message. + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "second".into(), + }], + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + // Verify the second request's last user message does not include the environment snapshot. + let requests = server.received_requests().await.unwrap(); + assert!( + requests.len() >= 2, + "expected two requests to the mock server" + ); + let second_req = &requests[1]; + let body = second_req.body_json::().unwrap(); + + let input = body["input"].as_array().expect("input array"); + let last = input.last().expect("at least one input item"); + assert_eq!(last["role"], "user"); + let last_text = last["content"][0]["text"].as_str().unwrap(); + // Should be exactly the submitted text, without the snapshot header prefix. + assert_eq!(last_text, "second"); +}