mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Merge branch 'main' into nicholasclark/2026-03-23-server-spans
This commit is contained in:
@@ -185,6 +185,11 @@ impl Respond for CodexAppsJsonRpcResponder {
|
||||
{
|
||||
"name": "calendar_create_event",
|
||||
"description": "Create a calendar event.",
|
||||
"annotations": {
|
||||
"readOnlyHint": false,
|
||||
"destructiveHint": false,
|
||||
"openWorldHint": false
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -209,6 +214,9 @@ impl Respond for CodexAppsJsonRpcResponder {
|
||||
{
|
||||
"name": "calendar_list_events",
|
||||
"description": "List calendar events.",
|
||||
"annotations": {
|
||||
"readOnlyHint": true
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -241,6 +249,9 @@ impl Respond for CodexAppsJsonRpcResponder {
|
||||
tools.push(json!({
|
||||
"name": format!("calendar_timezone_option_{index}"),
|
||||
"description": format!("Read timezone option {index}."),
|
||||
"annotations": {
|
||||
"readOnlyHint": true
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use regex_lite::Regex;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod apps_test_server;
|
||||
@@ -104,6 +105,36 @@ pub fn test_absolute_path(unix_path: &str) -> AbsolutePathBuf {
|
||||
test_absolute_path_with_windows(unix_path, /*windows_path*/ None)
|
||||
}
|
||||
|
||||
pub trait PathExt {
|
||||
fn abs(&self) -> AbsolutePathBuf;
|
||||
}
|
||||
|
||||
impl PathExt for Path {
|
||||
fn abs(&self) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::try_from(self.to_path_buf()).expect("path should already be absolute")
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PathBufExt {
|
||||
fn abs(&self) -> AbsolutePathBuf;
|
||||
}
|
||||
|
||||
impl PathBufExt for PathBuf {
|
||||
fn abs(&self) -> AbsolutePathBuf {
|
||||
self.as_path().abs()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TempDirExt {
|
||||
fn abs(&self) -> AbsolutePathBuf;
|
||||
}
|
||||
|
||||
impl TempDirExt for TempDir {
|
||||
fn abs(&self) -> AbsolutePathBuf {
|
||||
self.path().abs()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_tmp_path() -> AbsolutePathBuf {
|
||||
test_absolute_path_with_windows("/tmp", Some(r"C:\Users\codex\AppData\Local\Temp"))
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ use serde_json::Value;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::MockServer;
|
||||
|
||||
use crate::PathBufExt;
|
||||
use crate::PathExt;
|
||||
use crate::RemoteEnvConfig;
|
||||
use crate::TempDirExt;
|
||||
use crate::get_remote_test_env;
|
||||
use crate::load_default_config_for_test;
|
||||
use crate::responses::WebSocketTestServer;
|
||||
@@ -297,8 +300,7 @@ fn docker_command_capture_stdout<const N: usize>(args: [&str; N]) -> Result<Stri
|
||||
}
|
||||
|
||||
fn absolute_path(path: &Path) -> Result<AbsolutePathBuf> {
|
||||
AbsolutePathBuf::try_from(path.to_path_buf())
|
||||
.map_err(|err| anyhow!("invalid absolute path {}: {err}", path.display()))
|
||||
Ok(path.abs())
|
||||
}
|
||||
|
||||
/// A collection of different ways the model can output an apply_patch call
|
||||
@@ -393,7 +395,7 @@ impl TestCodexBuilder {
|
||||
let cwd = test_env.cwd.to_path_buf();
|
||||
self.config_mutators.push(Box::new(move |config| {
|
||||
config.experimental_exec_server_url = experimental_exec_server_url;
|
||||
config.cwd = cwd;
|
||||
config.cwd = cwd.abs();
|
||||
}));
|
||||
|
||||
let mut test = self.build(server).await?;
|
||||
@@ -556,7 +558,7 @@ impl TestCodexBuilder {
|
||||
};
|
||||
let cwd = Arc::new(TempDir::new()?);
|
||||
let mut config = load_default_config_for_test(home).await;
|
||||
config.cwd = cwd.path().to_path_buf();
|
||||
config.cwd = cwd.abs();
|
||||
config.model_provider = model_provider;
|
||||
for hook in self.pre_build_hooks.drain(..) {
|
||||
hook(home.path());
|
||||
@@ -616,12 +618,7 @@ fn ensure_test_model_catalog(config: &mut Config) -> Result<()> {
|
||||
.unwrap_or_else(|| panic!("missing bundled model gpt-5.1-codex"));
|
||||
model.slug = TEST_MODEL_WITH_EXPERIMENTAL_TOOLS.to_string();
|
||||
model.display_name = TEST_MODEL_WITH_EXPERIMENTAL_TOOLS.to_string();
|
||||
model.experimental_supported_tools = vec![
|
||||
"test_sync_tool".to_string(),
|
||||
"read_file".to_string(),
|
||||
"grep_files".to_string(),
|
||||
"list_dir".to_string(),
|
||||
];
|
||||
model.experimental_supported_tools = vec!["test_sync_tool".to_string()];
|
||||
config.model_catalog = Some(ModelsResponse {
|
||||
models: vec![model],
|
||||
});
|
||||
@@ -721,8 +718,9 @@ impl TestCodex {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: self.config.cwd.clone(),
|
||||
cwd: self.config.cwd.to_path_buf(),
|
||||
approval_policy,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -310,6 +310,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model,
|
||||
effort: None,
|
||||
@@ -919,6 +920,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<(
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model,
|
||||
effort: None,
|
||||
@@ -1000,6 +1002,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() ->
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model,
|
||||
effort: None,
|
||||
@@ -1151,6 +1154,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff(
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model,
|
||||
effort: None,
|
||||
@@ -1215,6 +1219,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change(
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model,
|
||||
effort: None,
|
||||
@@ -1287,6 +1292,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model,
|
||||
effort: None,
|
||||
@@ -1359,6 +1365,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model,
|
||||
effort: None,
|
||||
|
||||
@@ -558,6 +558,7 @@ async fn submit_turn(
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -294,13 +294,14 @@ async fn returns_fresh_tokens_as_is() -> Result<()> {
|
||||
.await;
|
||||
|
||||
let ctx = RefreshTokenTestContext::new(&server)?;
|
||||
let initial_last_refresh = Utc::now() - Duration::days(1);
|
||||
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
|
||||
let stale_refresh = Utc::now() - Duration::days(9);
|
||||
let fresh_access_token = access_token_with_expiration(Utc::now() + Duration::hours(1));
|
||||
let initial_tokens = build_tokens(&fresh_access_token, INITIAL_REFRESH_TOKEN);
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
last_refresh: Some(stale_refresh),
|
||||
};
|
||||
ctx.write_auth(&initial_auth)?;
|
||||
|
||||
@@ -325,7 +326,7 @@ async fn returns_fresh_tokens_as_is() -> Result<()> {
|
||||
|
||||
#[serial_test::serial(auth_refresh)]
|
||||
#[tokio::test]
|
||||
async fn refreshes_token_when_last_refresh_is_stale() -> Result<()> {
|
||||
async fn refreshes_token_when_access_token_is_expired() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = MockServer::start().await;
|
||||
@@ -340,13 +341,14 @@ async fn refreshes_token_when_last_refresh_is_stale() -> Result<()> {
|
||||
.await;
|
||||
|
||||
let ctx = RefreshTokenTestContext::new(&server)?;
|
||||
let stale_refresh = Utc::now() - Duration::days(9);
|
||||
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
|
||||
let fresh_refresh = Utc::now() - Duration::days(1);
|
||||
let expired_access_token = access_token_with_expiration(Utc::now() - Duration::hours(1));
|
||||
let initial_tokens = build_tokens(&expired_access_token, INITIAL_REFRESH_TOKEN);
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(stale_refresh),
|
||||
last_refresh: Some(fresh_refresh),
|
||||
};
|
||||
ctx.write_auth(&initial_auth)?;
|
||||
|
||||
@@ -373,7 +375,7 @@ async fn refreshes_token_when_last_refresh_is_stale() -> Result<()> {
|
||||
.as_ref()
|
||||
.context("last_refresh should be recorded")?;
|
||||
assert!(
|
||||
*refreshed_at >= stale_refresh,
|
||||
*refreshed_at >= fresh_refresh,
|
||||
"last_refresh should advance"
|
||||
);
|
||||
|
||||
@@ -543,6 +545,153 @@ async fn refresh_token_returns_permanent_error_for_expired_refresh_token() -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[serial_test::serial(auth_refresh)]
|
||||
#[tokio::test]
|
||||
async fn refresh_token_does_not_retry_after_permanent_failure() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/oauth/token"))
|
||||
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
|
||||
"error": {
|
||||
"code": "refresh_token_reused"
|
||||
}
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let ctx = RefreshTokenTestContext::new(&server)?;
|
||||
let initial_last_refresh = Utc::now() - Duration::days(1);
|
||||
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
ctx.write_auth(&initial_auth)?;
|
||||
|
||||
let first_err = ctx
|
||||
.auth_manager
|
||||
.refresh_token()
|
||||
.await
|
||||
.err()
|
||||
.context("first refresh should fail")?;
|
||||
assert_eq!(
|
||||
first_err.failed_reason(),
|
||||
Some(RefreshTokenFailedReason::Exhausted)
|
||||
);
|
||||
|
||||
let second_err = ctx
|
||||
.auth_manager
|
||||
.refresh_token()
|
||||
.await
|
||||
.err()
|
||||
.context("second refresh should fail without retrying")?;
|
||||
assert_eq!(
|
||||
second_err.failed_reason(),
|
||||
Some(RefreshTokenFailedReason::Exhausted)
|
||||
);
|
||||
|
||||
let stored = ctx.load_auth()?;
|
||||
assert_eq!(stored, initial_auth);
|
||||
let cached_auth = ctx
|
||||
.auth_manager
|
||||
.auth()
|
||||
.await
|
||||
.context("auth should remain cached")?;
|
||||
let cached = cached_auth
|
||||
.get_token_data()
|
||||
.context("token data should remain cached")?;
|
||||
assert_eq!(cached, initial_tokens);
|
||||
|
||||
server.verify().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[serial_test::serial(auth_refresh)]
|
||||
#[tokio::test]
|
||||
async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/oauth/token"))
|
||||
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
|
||||
"error": {
|
||||
"code": "refresh_token_reused"
|
||||
}
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let ctx = RefreshTokenTestContext::new(&server)?;
|
||||
let initial_last_refresh = Utc::now() - Duration::days(1);
|
||||
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
ctx.write_auth(&initial_auth)?;
|
||||
|
||||
let first_err = ctx
|
||||
.auth_manager
|
||||
.refresh_token()
|
||||
.await
|
||||
.err()
|
||||
.context("first refresh should fail")?;
|
||||
assert_eq!(
|
||||
first_err.failed_reason(),
|
||||
Some(RefreshTokenFailedReason::Exhausted)
|
||||
);
|
||||
|
||||
let fresh_refresh = Utc::now() - Duration::hours(1);
|
||||
let disk_tokens = build_tokens("disk-access-token", "disk-refresh-token");
|
||||
let disk_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
tokens: Some(disk_tokens.clone()),
|
||||
last_refresh: Some(fresh_refresh),
|
||||
};
|
||||
save_auth(
|
||||
ctx.codex_home.path(),
|
||||
&disk_auth,
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
ctx.auth_manager
|
||||
.refresh_token()
|
||||
.await
|
||||
.context("refresh should reload changed auth without retrying")?;
|
||||
|
||||
let stored = ctx.load_auth()?;
|
||||
assert_eq!(stored, disk_auth);
|
||||
|
||||
let cached_auth = ctx
|
||||
.auth_manager
|
||||
.auth_cached()
|
||||
.context("auth should be cached")?;
|
||||
let cached = cached_auth
|
||||
.get_token_data()
|
||||
.context("token data should reload from disk")?;
|
||||
assert_eq!(cached, disk_tokens);
|
||||
|
||||
let requests = server.received_requests().await.unwrap_or_default();
|
||||
assert_eq!(
|
||||
requests.len(),
|
||||
1,
|
||||
"expected only the initial refresh request"
|
||||
);
|
||||
|
||||
server.verify().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[serial_test::serial(auth_refresh)]
|
||||
#[tokio::test]
|
||||
async fn refresh_token_returns_transient_error_on_server_failure() -> Result<()> {
|
||||
@@ -867,7 +1016,7 @@ impl Drop for EnvGuard {
|
||||
}
|
||||
}
|
||||
|
||||
fn minimal_jwt() -> String {
|
||||
fn jwt_with_payload(payload: serde_json::Value) -> String {
|
||||
#[derive(Serialize)]
|
||||
struct Header {
|
||||
alg: &'static str,
|
||||
@@ -878,7 +1027,6 @@ fn minimal_jwt() -> String {
|
||||
alg: "none",
|
||||
typ: "JWT",
|
||||
};
|
||||
let payload = json!({ "sub": "user-123" });
|
||||
|
||||
fn b64(data: &[u8]) -> String {
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
|
||||
@@ -898,6 +1046,14 @@ fn minimal_jwt() -> String {
|
||||
format!("{header_b64}.{payload_b64}.{signature_b64}")
|
||||
}
|
||||
|
||||
fn minimal_jwt() -> String {
|
||||
jwt_with_payload(json!({ "sub": "user-123" }))
|
||||
}
|
||||
|
||||
fn access_token_with_expiration(expires_at: chrono::DateTime<Utc>) -> String {
|
||||
jwt_with_payload(json!({ "sub": "user-123", "exp": expires_at.timestamp() }))
|
||||
}
|
||||
|
||||
fn build_tokens(access_token: &str, refresh_token: &str) -> TokenData {
|
||||
let id_token = IdTokenInfo {
|
||||
raw_jwt: minimal_jwt(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use assert_cmd::Command as AssertCommand;
|
||||
use codex_core::auth::CODEX_API_KEY_ENV_VAR;
|
||||
use codex_git_utils::collect_git_info;
|
||||
use codex_protocol::protocol::GitInfo;
|
||||
use codex_utils_cargo_bin::find_resource;
|
||||
use core_test_support::fs_wait;
|
||||
@@ -596,7 +597,7 @@ async fn integration_git_info_unit_test() {
|
||||
.unwrap();
|
||||
|
||||
// 3. Test git info collection directly
|
||||
let git_info = codex_core::git_info::collect_git_info(&git_repo).await;
|
||||
let git_info = collect_git_info(&git_repo).await;
|
||||
|
||||
// 4. Verify git info is present and contains expected data
|
||||
assert!(git_info.is_some(), "Git info should be collected");
|
||||
@@ -608,7 +609,7 @@ async fn integration_git_info_unit_test() {
|
||||
git_info.commit_hash.is_some(),
|
||||
"Git info should contain commit_hash"
|
||||
);
|
||||
let commit_hash = git_info.commit_hash.as_ref().unwrap();
|
||||
let commit_hash = &git_info.commit_hash.as_ref().unwrap().0;
|
||||
assert_eq!(commit_hash.len(), 40, "Commit hash should be 40 characters");
|
||||
assert!(
|
||||
commit_hash.chars().all(|c| c.is_ascii_hexdigit()),
|
||||
|
||||
@@ -42,6 +42,7 @@ use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::responses::ev_completed;
|
||||
@@ -1105,7 +1106,7 @@ async fn skills_append_to_developer_message() {
|
||||
.with_home(codex_home.clone())
|
||||
.with_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
.with_config(move |config| {
|
||||
config.cwd = codex_home_path;
|
||||
config.cwd = codex_home_path.abs();
|
||||
});
|
||||
let codex = builder
|
||||
.build(&server)
|
||||
@@ -1308,8 +1309,9 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
|
||||
model: session_configured.model.clone(),
|
||||
effort: Some(ReasoningEffort::Low),
|
||||
@@ -1425,8 +1427,9 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default()
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
|
||||
model: session_configured.model,
|
||||
effort: None,
|
||||
|
||||
@@ -2319,6 +2319,7 @@ text(
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: None,
|
||||
|
||||
@@ -176,8 +176,9 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: test.config.cwd.clone(),
|
||||
cwd: test.config.cwd.to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: test.config.permissions.sandbox_policy.get().clone(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: None,
|
||||
@@ -291,8 +292,9 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: test.config.cwd.clone(),
|
||||
cwd: test.config.cwd.to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: test.config.permissions.sandbox_policy.get().clone(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: None,
|
||||
|
||||
@@ -1658,6 +1658,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
|
||||
final_output_json_schema: None,
|
||||
cwd: resumed.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: resumed.session_configured.model.clone(),
|
||||
effort: None,
|
||||
@@ -1748,6 +1749,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: previous_model.to_string(),
|
||||
effort: None,
|
||||
@@ -1772,6 +1774,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: next_model.to_string(),
|
||||
effort: None,
|
||||
@@ -1882,6 +1885,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
|
||||
final_output_json_schema: None,
|
||||
cwd: initial.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: previous_model.to_string(),
|
||||
effort: None,
|
||||
@@ -1930,6 +1934,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
|
||||
final_output_json_schema: None,
|
||||
cwd: resumed.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: next_model.to_string(),
|
||||
effort: None,
|
||||
@@ -3132,6 +3137,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: previous_model.to_string(),
|
||||
effort: None,
|
||||
@@ -3156,6 +3162,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: next_model.to_string(),
|
||||
effort: None,
|
||||
|
||||
@@ -16,6 +16,9 @@ use codex_core::ThreadManager;
|
||||
use codex_core::compact::SUMMARIZATION_PROMPT;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
@@ -307,10 +310,10 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<()
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 1. Arrange mocked SSE responses for the initial flow plus the second compact.
|
||||
// 1. Arrange mocked SSE responses as a single ordered stream so assertions
|
||||
// observe the real request sequence instead of per-mock duplicate captures.
|
||||
let server = MockServer::start().await;
|
||||
let mut request_log = mount_initial_flow(&server).await;
|
||||
request_log.extend(mount_second_compact_flow(&server).await);
|
||||
let request_log = mount_second_compact_sequence(&server).await;
|
||||
|
||||
// 2. Drive the conversation through compact -> resume -> fork -> compact -> resume.
|
||||
let (_home, config, manager, base) = start_test_conversation(&server, None).await;
|
||||
@@ -346,7 +349,12 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<()
|
||||
let resumed_again = resume_conversation(&manager, &config, forked_path).await;
|
||||
user_turn(&resumed_again, AFTER_SECOND_RESUME).await;
|
||||
|
||||
let mut requests = gather_request_bodies(&request_log);
|
||||
let mut requests = request_log
|
||||
.requests()
|
||||
.into_iter()
|
||||
.map(|request| request.body_json())
|
||||
.collect::<Vec<_>>();
|
||||
requests.iter_mut().for_each(normalize_line_endings);
|
||||
normalize_compact_prompts(&mut requests);
|
||||
let input_after_compact = json!(requests[requests.len() - 2]["input"]);
|
||||
let input_after_resume = json!(requests[requests.len() - 1]["input"]);
|
||||
@@ -379,21 +387,39 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<()
|
||||
);
|
||||
let seeded_user_prefix = &first_request_user_texts[..first_turn_user_index];
|
||||
let summary_after_second_compact =
|
||||
extract_summary_user_text(&requests[requests.len() - 3], SUMMARY_TEXT);
|
||||
let mut expected_after_second_compact_user_texts =
|
||||
vec!["AFTER_FORK".to_string(), summary_after_second_compact];
|
||||
extract_summary_user_text(&requests[requests.len() - 2], SUMMARY_TEXT);
|
||||
let mut expected_after_second_compact_user_texts = vec![
|
||||
"hello world".to_string(),
|
||||
"AFTER_COMPACT".to_string(),
|
||||
"AFTER_RESUME".to_string(),
|
||||
"AFTER_FORK".to_string(),
|
||||
summary_after_second_compact.clone(),
|
||||
];
|
||||
expected_after_second_compact_user_texts.extend_from_slice(seeded_user_prefix);
|
||||
expected_after_second_compact_user_texts.push("AFTER_COMPACT_2".to_string());
|
||||
let mut expected_fork_local_user_texts =
|
||||
vec!["AFTER_FORK".to_string(), summary_after_second_compact];
|
||||
expected_fork_local_user_texts.extend_from_slice(seeded_user_prefix);
|
||||
expected_fork_local_user_texts.push("AFTER_COMPACT_2".to_string());
|
||||
let final_user_texts = json_message_input_texts(&requests[requests.len() - 1], "user");
|
||||
let (final_last, final_prefix) = final_user_texts
|
||||
.split_last()
|
||||
.unwrap_or_else(|| panic!("after-second-resume request missing user messages"));
|
||||
assert_eq!(final_last, AFTER_SECOND_RESUME);
|
||||
assert!(
|
||||
final_prefix.starts_with(&expected_after_second_compact_user_texts),
|
||||
"after-second-resume user texts should preserve post-compact user history prefix"
|
||||
);
|
||||
let final_seeded_suffix = &final_prefix[expected_after_second_compact_user_texts.len()..];
|
||||
let matched_prefix_len = if let Some(start) = final_prefix
|
||||
.windows(expected_after_second_compact_user_texts.len())
|
||||
.position(|window| window == expected_after_second_compact_user_texts)
|
||||
{
|
||||
start + expected_after_second_compact_user_texts.len()
|
||||
} else if let Some(start) = final_prefix
|
||||
.windows(expected_fork_local_user_texts.len())
|
||||
.position(|window| window == expected_fork_local_user_texts)
|
||||
{
|
||||
start + expected_fork_local_user_texts.len()
|
||||
} else {
|
||||
panic!("after-second-resume user texts should preserve post-compact user history prefix");
|
||||
};
|
||||
let final_seeded_suffix = &final_prefix[matched_prefix_len..];
|
||||
if seeded_user_prefix.is_empty() {
|
||||
assert!(
|
||||
final_seeded_suffix.is_empty(),
|
||||
@@ -502,6 +528,141 @@ async fn snapshot_rollback_past_compaction_replays_append_only_history() -> Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
/// Scenario: rolling back a turn that introduced persistent pre-turn context
|
||||
/// diffs should trim those context updates so the next request includes them
|
||||
/// only once.
|
||||
async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> {
|
||||
if network_disabled() {
|
||||
println!("Skipping test because network is disabled in this sandbox");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
const MODEL: &str = "gpt-5.1-codex";
|
||||
const TURN_ONE_USER: &str = "turn 1 user";
|
||||
const TURN_TWO_USER: &str = "turn 2 user";
|
||||
const FOLLOWUP_USER: &str = "follow-up user";
|
||||
const ROLLED_BACK_DEV_INSTRUCTIONS: &str = "ROLLED_BACK_DEV_INSTRUCTIONS";
|
||||
const PRETURN_CONTEXT_DIFF_CWD: &str = "PRETURN_CONTEXT_DIFF_CWD";
|
||||
|
||||
let server = MockServer::start().await;
|
||||
let request_log = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_assistant_message("m1", "turn 1 assistant"),
|
||||
ev_completed("r1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("m2", "turn 2 assistant"),
|
||||
ev_completed("r2"),
|
||||
]),
|
||||
sse(vec![ev_completed("r3")]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_home, config, _manager, conversation) =
|
||||
start_test_conversation(&server, Some(MODEL)).await;
|
||||
|
||||
user_turn(&conversation, TURN_ONE_USER).await;
|
||||
|
||||
let override_cwd = config.cwd.join(PRETURN_CONTEXT_DIFF_CWD)?;
|
||||
std::fs::create_dir_all(&override_cwd)?;
|
||||
conversation
|
||||
.submit(Op::OverrideTurnContext {
|
||||
cwd: Some(override_cwd.to_path_buf()),
|
||||
approval_policy: None,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: None,
|
||||
windows_sandbox_level: None,
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: Some(CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model: MODEL.to_string(),
|
||||
reasoning_effort: None,
|
||||
developer_instructions: Some(ROLLED_BACK_DEV_INSTRUCTIONS.to_string()),
|
||||
},
|
||||
}),
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
user_turn(&conversation, TURN_TWO_USER).await;
|
||||
|
||||
conversation
|
||||
.submit(Op::ThreadRollback { num_turns: 1 })
|
||||
.await?;
|
||||
let rollback_event = wait_for_event(&conversation, |ev| {
|
||||
matches!(ev, EventMsg::ThreadRolledBack(_))
|
||||
})
|
||||
.await;
|
||||
let EventMsg::ThreadRolledBack(rollback_event) = rollback_event else {
|
||||
panic!("expected thread rolled back event");
|
||||
};
|
||||
assert_eq!(rollback_event.num_turns, 1);
|
||||
|
||||
user_turn(&conversation, FOLLOWUP_USER).await;
|
||||
|
||||
let requests = request_log.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
|
||||
let before_rollback_developer_count = requests[1]
|
||||
.message_input_texts("developer")
|
||||
.iter()
|
||||
.filter(|text| text.contains(ROLLED_BACK_DEV_INSTRUCTIONS))
|
||||
.count();
|
||||
assert_eq!(before_rollback_developer_count, 1);
|
||||
assert_eq!(
|
||||
requests[1]
|
||||
.message_input_texts("user")
|
||||
.iter()
|
||||
.filter(|text| text.contains(PRETURN_CONTEXT_DIFF_CWD))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
|
||||
let after_rollback_developer_count = requests[2]
|
||||
.message_input_texts("developer")
|
||||
.iter()
|
||||
.filter(|text| text.contains(ROLLED_BACK_DEV_INSTRUCTIONS))
|
||||
.count();
|
||||
assert_eq!(after_rollback_developer_count, 1);
|
||||
|
||||
let after_rollback_user_texts = requests[2].message_input_texts("user");
|
||||
assert_eq!(
|
||||
after_rollback_user_texts
|
||||
.iter()
|
||||
.filter(|text| text.contains(PRETURN_CONTEXT_DIFF_CWD))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
after_rollback_user_texts.last().map(String::as_str),
|
||||
Some(FOLLOWUP_USER)
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"rollback_followup_turn_trims_context_updates",
|
||||
context_snapshot::format_labeled_requests_snapshot(
|
||||
"rollback trims pre-turn override context updates before the follow-up request",
|
||||
&[
|
||||
("rolled-back turn request", &requests[1]),
|
||||
("follow-up request after rollback", &requests[2]),
|
||||
],
|
||||
&ContextSnapshotOptions::default()
|
||||
.strip_capability_instructions()
|
||||
.render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 96 }),
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_line_endings(value: &mut Value) {
|
||||
match value {
|
||||
Value::String(text) => {
|
||||
@@ -594,33 +755,29 @@ async fn mount_initial_flow(server: &MockServer) -> Vec<ResponseMock> {
|
||||
vec![first, compact, after_compact, after_resume, after_fork]
|
||||
}
|
||||
|
||||
async fn mount_second_compact_flow(server: &MockServer) -> Vec<ResponseMock> {
|
||||
async fn mount_second_compact_sequence(server: &MockServer) -> ResponseMock {
|
||||
let sse1 = sse(vec![
|
||||
ev_assistant_message("m1", FIRST_REPLY),
|
||||
ev_completed("r1"),
|
||||
]);
|
||||
let sse2 = sse(vec![
|
||||
ev_assistant_message("m2", SUMMARY_TEXT),
|
||||
ev_completed("r2"),
|
||||
]);
|
||||
let sse3 = sse(vec![
|
||||
ev_assistant_message("m3", "AFTER_COMPACT_REPLY"),
|
||||
ev_completed("r3"),
|
||||
]);
|
||||
let sse4 = sse(vec![ev_completed("r4")]);
|
||||
let sse5 = sse(vec![ev_completed("r5")]);
|
||||
let sse6 = sse(vec![
|
||||
ev_assistant_message("m4", SUMMARY_TEXT),
|
||||
ev_completed("r6"),
|
||||
]);
|
||||
let sse7 = sse(vec![ev_completed("r7")]);
|
||||
let sse8 = sse(vec![ev_completed("r8")]);
|
||||
|
||||
// Keep this matcher broad enough to survive prompt-shape differences across
|
||||
// platforms/config (history may include either marker text or compact prompt
|
||||
// fragments), but explicitly exclude the final resume turn so these two
|
||||
// one-shot mocks cannot race for the same request.
|
||||
let match_second_compact = |req: &wiremock::Request| {
|
||||
let body = std::str::from_utf8(&req.body).unwrap_or("");
|
||||
(body.contains("AFTER_FORK")
|
||||
|| body_contains_text(body, SUMMARIZATION_PROMPT)
|
||||
|| body.contains(&json_fragment(FIRST_REPLY)))
|
||||
&& !body.contains(&format!("\"text\":\"{AFTER_SECOND_RESUME}\""))
|
||||
};
|
||||
let second_compact = mount_sse_once_match(server, match_second_compact, sse6).await;
|
||||
|
||||
let match_after_second_resume = |req: &wiremock::Request| {
|
||||
let body = std::str::from_utf8(&req.body).unwrap_or("");
|
||||
body.contains(&format!("\"text\":\"{AFTER_SECOND_RESUME}\""))
|
||||
};
|
||||
let after_second_resume = mount_sse_once_match(server, match_after_second_resume, sse7).await;
|
||||
|
||||
vec![second_compact, after_second_resume]
|
||||
mount_sse_sequence(server, vec![sse1, sse2, sse3, sse4, sse5, sse6, sse7, sse8]).await
|
||||
}
|
||||
|
||||
async fn start_test_conversation(
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::string::ToString;
|
||||
use codex_core::exec::ExecCapturePolicy;
|
||||
use codex_core::exec::ExecParams;
|
||||
use codex_core::exec::ExecToolCallOutput;
|
||||
use codex_core::exec::SandboxType;
|
||||
use codex_core::exec::process_exec_tool_call;
|
||||
use codex_core::sandboxing::SandboxPermissions;
|
||||
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
@@ -14,12 +13,12 @@ use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::get_platform_sandbox;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use codex_core::error::Result;
|
||||
|
||||
use codex_core::get_platform_sandbox;
|
||||
|
||||
fn skip_test() -> bool {
|
||||
if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) {
|
||||
eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test.");
|
||||
|
||||
@@ -51,6 +51,7 @@ async fn submit_user_turn(
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -131,6 +132,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use codex_core::ForkSnapshot;
|
||||
use codex_core::NewThread;
|
||||
use codex_core::parse_turn_item;
|
||||
use codex_protocol::items::TurnItem;
|
||||
@@ -110,7 +111,13 @@ async fn fork_thread_twice_drops_to_first_message() {
|
||||
thread: codex_fork1,
|
||||
..
|
||||
} = thread_manager
|
||||
.fork_thread(1, config_for_fork.clone(), base_path.clone(), false, None)
|
||||
.fork_thread(
|
||||
ForkSnapshot::TruncateBeforeNthUserMessage(1),
|
||||
config_for_fork.clone(),
|
||||
base_path.clone(),
|
||||
/*persist_extended_history*/ false,
|
||||
/*parent_trace*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("fork 1");
|
||||
|
||||
@@ -118,9 +125,8 @@ async fn fork_thread_twice_drops_to_first_message() {
|
||||
|
||||
// GetHistory on fork1 flushed; the file is ready.
|
||||
let fork1_items = read_items(&fork1_path);
|
||||
assert!(fork1_items.len() > expected_after_first.len());
|
||||
pretty_assertions::assert_eq!(
|
||||
serde_json::to_value(&fork1_items[..expected_after_first.len()]).unwrap(),
|
||||
serde_json::to_value(&fork1_items).unwrap(),
|
||||
serde_json::to_value(&expected_after_first).unwrap()
|
||||
);
|
||||
|
||||
@@ -129,7 +135,13 @@ async fn fork_thread_twice_drops_to_first_message() {
|
||||
thread: codex_fork2,
|
||||
..
|
||||
} = thread_manager
|
||||
.fork_thread(0, config_for_fork.clone(), fork1_path.clone(), false, None)
|
||||
.fork_thread(
|
||||
ForkSnapshot::TruncateBeforeNthUserMessage(0),
|
||||
config_for_fork.clone(),
|
||||
fork1_path.clone(),
|
||||
/*persist_extended_history*/ false,
|
||||
/*parent_trace*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("fork 2");
|
||||
|
||||
@@ -143,9 +155,8 @@ async fn fork_thread_twice_drops_to_first_message() {
|
||||
.unwrap_or(0);
|
||||
let expected_after_second: Vec<RolloutItem> = fork1_items[..cut_last_on_fork1].to_vec();
|
||||
let fork2_items = read_items(&fork2_path);
|
||||
assert!(fork2_items.len() > expected_after_second.len());
|
||||
pretty_assertions::assert_eq!(
|
||||
serde_json::to_value(&fork2_items[..expected_after_second.len()]).unwrap(),
|
||||
serde_json::to_value(&fork2_items).unwrap(),
|
||||
serde_json::to_value(&expected_after_second).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
#![cfg(not(target_os = "windows"))]
|
||||
|
||||
use anyhow::Result;
|
||||
use core_test_support::responses::mount_function_call_agent_response;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
const MODEL_WITH_TOOL: &str = "test-gpt-5.1-codex";
|
||||
|
||||
fn ripgrep_available() -> bool {
|
||||
StdCommand::new("rg")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
macro_rules! skip_if_ripgrep_missing {
|
||||
($ret:expr $(,)?) => {{
|
||||
if !ripgrep_available() {
|
||||
eprintln!("rg not available in PATH; skipping test");
|
||||
return $ret;
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn grep_files_tool_collects_matches() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_ripgrep_missing!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = build_test_codex(&server).await?;
|
||||
|
||||
let search_dir = test.cwd.path().join("src");
|
||||
std::fs::create_dir_all(&search_dir)?;
|
||||
let alpha = search_dir.join("alpha.rs");
|
||||
let beta = search_dir.join("beta.rs");
|
||||
let gamma = search_dir.join("gamma.txt");
|
||||
std::fs::write(&alpha, "alpha needle\n")?;
|
||||
std::fs::write(&beta, "beta needle\n")?;
|
||||
std::fs::write(&gamma, "needle in text but excluded\n")?;
|
||||
|
||||
let call_id = "grep-files-collect";
|
||||
let arguments = serde_json::json!({
|
||||
"pattern": "needle",
|
||||
"path": search_dir.to_string_lossy(),
|
||||
"include": "*.rs",
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let mocks =
|
||||
mount_function_call_agent_response(&server, call_id, &arguments, "grep_files").await;
|
||||
test.submit_turn("please find uses of needle").await?;
|
||||
|
||||
let req = mocks.completion.single_request();
|
||||
let (content_opt, success_opt) = req
|
||||
.function_call_output_content_and_success(call_id)
|
||||
.expect("tool output present");
|
||||
let content = content_opt.expect("content present");
|
||||
let success = success_opt.unwrap_or(true);
|
||||
assert!(
|
||||
success,
|
||||
"expected success for matches, got content={content}"
|
||||
);
|
||||
|
||||
let entries = collect_file_names(&content);
|
||||
assert_eq!(entries.len(), 2, "content: {content}");
|
||||
assert!(
|
||||
entries.contains("alpha.rs"),
|
||||
"missing alpha.rs in {entries:?}"
|
||||
);
|
||||
assert!(
|
||||
entries.contains("beta.rs"),
|
||||
"missing beta.rs in {entries:?}"
|
||||
);
|
||||
assert!(
|
||||
!entries.contains("gamma.txt"),
|
||||
"txt file should be filtered out: {entries:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn grep_files_tool_reports_empty_results() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_ripgrep_missing!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = build_test_codex(&server).await?;
|
||||
|
||||
let search_dir = test.cwd.path().join("logs");
|
||||
std::fs::create_dir_all(&search_dir)?;
|
||||
std::fs::write(search_dir.join("output.txt"), "no hits here")?;
|
||||
|
||||
let call_id = "grep-files-empty";
|
||||
let arguments = serde_json::json!({
|
||||
"pattern": "needle",
|
||||
"path": search_dir.to_string_lossy(),
|
||||
"limit": 5,
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let mocks =
|
||||
mount_function_call_agent_response(&server, call_id, &arguments, "grep_files").await;
|
||||
test.submit_turn("search again").await?;
|
||||
|
||||
let req = mocks.completion.single_request();
|
||||
let (content_opt, success_opt) = req
|
||||
.function_call_output_content_and_success(call_id)
|
||||
.expect("tool output present");
|
||||
let content = content_opt.expect("content present");
|
||||
if let Some(success) = success_opt {
|
||||
assert!(!success, "expected success=false content={content}");
|
||||
}
|
||||
assert_eq!(content, "No matches found.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
async fn build_test_codex(server: &wiremock::MockServer) -> Result<TestCodex> {
|
||||
let mut builder = test_codex().with_model(MODEL_WITH_TOOL);
|
||||
builder.build(server).await
|
||||
}
|
||||
|
||||
fn collect_file_names(content: &str) -> HashSet<String> {
|
||||
content
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
if line.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
Path::new(line)
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -23,7 +23,14 @@ async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
|
||||
.features
|
||||
.enable(Feature::ChildAgentsMd)
|
||||
.expect("test config should allow feature update");
|
||||
std::fs::write(config.cwd.join("AGENTS.md"), "be nice").expect("write AGENTS.md");
|
||||
std::fs::write(
|
||||
config
|
||||
.cwd
|
||||
.join("AGENTS.md")
|
||||
.expect("absolute AGENTS.md path"),
|
||||
"be nice",
|
||||
)
|
||||
.expect("write AGENTS.md");
|
||||
});
|
||||
let test = builder.build(&server).await.expect("build test codex");
|
||||
|
||||
|
||||
@@ -174,6 +174,69 @@ if payload.get("prompt") == {blocked_prompt_json}:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_pre_tool_use_hook(
|
||||
home: &Path,
|
||||
matcher: Option<&str>,
|
||||
mode: &str,
|
||||
reason: &str,
|
||||
) -> Result<()> {
|
||||
let script_path = home.join("pre_tool_use_hook.py");
|
||||
let log_path = home.join("pre_tool_use_hook_log.jsonl");
|
||||
let mode_json = serde_json::to_string(mode).context("serialize pre tool use mode")?;
|
||||
let reason_json = serde_json::to_string(reason).context("serialize pre tool use reason")?;
|
||||
let script = format!(
|
||||
r#"import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
log_path = Path(r"{log_path}")
|
||||
mode = {mode_json}
|
||||
reason = {reason_json}
|
||||
|
||||
payload = json.load(sys.stdin)
|
||||
|
||||
with log_path.open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload) + "\n")
|
||||
|
||||
if mode == "json_deny":
|
||||
print(json.dumps({{
|
||||
"hookSpecificOutput": {{
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": reason
|
||||
}}
|
||||
}}))
|
||||
elif mode == "exit_2":
|
||||
sys.stderr.write(reason + "\n")
|
||||
raise SystemExit(2)
|
||||
"#,
|
||||
log_path = log_path.display(),
|
||||
mode_json = mode_json,
|
||||
reason_json = reason_json,
|
||||
);
|
||||
|
||||
let mut group = serde_json::json!({
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": format!("python3 {}", script_path.display()),
|
||||
"statusMessage": "running pre tool use hook",
|
||||
}]
|
||||
});
|
||||
if let Some(matcher) = matcher {
|
||||
group["matcher"] = Value::String(matcher.to_string());
|
||||
}
|
||||
|
||||
let hooks = serde_json::json!({
|
||||
"hooks": {
|
||||
"PreToolUse": [group]
|
||||
}
|
||||
});
|
||||
|
||||
fs::write(&script_path, script).context("write pre tool use hook script")?;
|
||||
fs::write(home.join("hooks.json"), hooks.to_string()).context("write hooks.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_session_start_hook_recording_transcript(home: &Path) -> Result<()> {
|
||||
let script_path = home.join("session_start_hook.py");
|
||||
let log_path = home.join("session_start_hook_log.jsonl");
|
||||
@@ -253,6 +316,15 @@ fn read_stop_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_pre_tool_use_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
|
||||
fs::read_to_string(home.join("pre_tool_use_hook_log.jsonl"))
|
||||
.context("read pre tool use hook log")?
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| serde_json::from_str(line).context("parse pre tool use hook log line"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_session_start_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
|
||||
fs::read_to_string(home.join("session_start_hook_log.jsonl"))
|
||||
.context("read session start hook log")?
|
||||
@@ -849,3 +921,357 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu
|
||||
server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pre_tool_use_blocks_shell_command_before_execution() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "pretooluse-shell-command";
|
||||
let marker = std::env::temp_dir().join("pretooluse-shell-command-marker");
|
||||
let command = format!("printf blocked > {}", marker.display());
|
||||
let args = serde_json::json!({ "command": command });
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
core_test_support::responses::ev_function_call(
|
||||
call_id,
|
||||
"shell_command",
|
||||
&serde_json::to_string(&args)?,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "hook blocked it"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) =
|
||||
write_pre_tool_use_hook(home, Some("^Bash$"), "json_deny", "blocked by pre hook")
|
||||
{
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
if marker.exists() {
|
||||
fs::remove_file(&marker).context("remove leftover pre tool use marker")?;
|
||||
}
|
||||
|
||||
test.submit_turn_with_policy(
|
||||
"run the blocked shell command",
|
||||
codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let output_item = requests[1].function_call_output(call_id);
|
||||
let output = output_item
|
||||
.get("output")
|
||||
.and_then(Value::as_str)
|
||||
.expect("shell command output string");
|
||||
assert!(
|
||||
output.contains("Bash command blocked by hook: blocked by pre hook"),
|
||||
"blocked tool output should surface the hook reason",
|
||||
);
|
||||
assert!(
|
||||
output.contains(&format!("Command: {command}")),
|
||||
"blocked tool output should surface the blocked command",
|
||||
);
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"blocked command should not create marker file"
|
||||
);
|
||||
|
||||
let hook_inputs = read_pre_tool_use_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["hook_event_name"], "PreToolUse");
|
||||
assert_eq!(hook_inputs[0]["tool_name"], "Bash");
|
||||
assert_eq!(hook_inputs[0]["tool_use_id"], call_id);
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], command);
|
||||
let transcript_path = hook_inputs[0]["transcript_path"]
|
||||
.as_str()
|
||||
.expect("pre tool use hook transcript_path");
|
||||
assert!(
|
||||
!transcript_path.is_empty(),
|
||||
"pre tool use hook should receive a non-empty transcript_path",
|
||||
);
|
||||
assert!(
|
||||
Path::new(transcript_path).exists(),
|
||||
"pre tool use hook transcript_path should be materialized on disk",
|
||||
);
|
||||
assert!(
|
||||
hook_inputs[0]["turn_id"]
|
||||
.as_str()
|
||||
.is_some_and(|turn_id| !turn_id.is_empty())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pre_tool_use_blocks_local_shell_before_execution() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "pretooluse-local-shell";
|
||||
let marker = std::env::temp_dir().join("pretooluse-local-shell-marker");
|
||||
let command = vec![
|
||||
"/bin/sh".to_string(),
|
||||
"-c".to_string(),
|
||||
format!("printf blocked > {}", marker.display()),
|
||||
];
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
core_test_support::responses::ev_local_shell_call(
|
||||
call_id,
|
||||
"completed",
|
||||
command.iter().map(String::as_str).collect(),
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "local shell blocked"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) =
|
||||
write_pre_tool_use_hook(home, Some("^Bash$"), "json_deny", "blocked local shell")
|
||||
{
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
if marker.exists() {
|
||||
fs::remove_file(&marker).context("remove leftover local shell marker")?;
|
||||
}
|
||||
|
||||
test.submit_turn("run the blocked local shell command")
|
||||
.await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let output_item = requests[1].function_call_output(call_id);
|
||||
let output = output_item
|
||||
.get("output")
|
||||
.and_then(Value::as_str)
|
||||
.expect("local shell output string");
|
||||
assert!(
|
||||
output.contains("Bash command blocked by hook: blocked local shell"),
|
||||
"blocked local shell output should surface the hook reason",
|
||||
);
|
||||
assert!(
|
||||
output.contains(&format!(
|
||||
"Command: {}",
|
||||
codex_shell_command::parse_command::shlex_join(&command)
|
||||
)),
|
||||
"blocked local shell output should surface the blocked command",
|
||||
);
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"blocked local shell command should not execute"
|
||||
);
|
||||
|
||||
let hook_inputs = read_pre_tool_use_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(
|
||||
hook_inputs[0]["tool_input"]["command"],
|
||||
codex_shell_command::parse_command::shlex_join(&command),
|
||||
);
|
||||
assert!(
|
||||
hook_inputs[0]["turn_id"]
|
||||
.as_str()
|
||||
.is_some_and(|turn_id| !turn_id.is_empty())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pre_tool_use_blocks_exec_command_before_execution() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "pretooluse-exec-command";
|
||||
let marker = std::env::temp_dir().join("pretooluse-exec-command-marker");
|
||||
let command = format!("printf blocked > {}", marker.display());
|
||||
let args = serde_json::json!({ "cmd": command });
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
core_test_support::responses::ev_function_call(
|
||||
call_id,
|
||||
"exec_command",
|
||||
&serde_json::to_string(&args)?,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "exec command blocked"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) =
|
||||
write_pre_tool_use_hook(home, Some("^Bash$"), "exit_2", "blocked exec command")
|
||||
{
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
if marker.exists() {
|
||||
fs::remove_file(&marker).context("remove leftover exec marker")?;
|
||||
}
|
||||
|
||||
test.submit_turn("run the blocked exec command").await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let output_item = requests[1].function_call_output(call_id);
|
||||
let output = output_item
|
||||
.get("output")
|
||||
.and_then(Value::as_str)
|
||||
.expect("exec command output string");
|
||||
assert!(
|
||||
output.contains("Bash command blocked by hook: blocked exec command"),
|
||||
"blocked exec command output should surface the hook reason",
|
||||
);
|
||||
assert!(
|
||||
output.contains(&format!("Command: {command}")),
|
||||
"blocked exec command output should surface the blocked command",
|
||||
);
|
||||
assert!(!marker.exists(), "blocked exec command should not execute");
|
||||
|
||||
let hook_inputs = read_pre_tool_use_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["tool_use_id"], call_id);
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], command);
|
||||
assert!(
|
||||
hook_inputs[0]["turn_id"]
|
||||
.as_str()
|
||||
.is_some_and(|turn_id| !turn_id.is_empty())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pre_tool_use_does_not_fire_for_non_shell_tools() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let call_id = "pretooluse-update-plan";
|
||||
let args = serde_json::json!({
|
||||
"plan": [{
|
||||
"step": "watch the tide",
|
||||
"status": "pending",
|
||||
}]
|
||||
});
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
core_test_support::responses::ev_function_call(
|
||||
call_id,
|
||||
"update_plan",
|
||||
&serde_json::to_string(&args)?,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "plan updated"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) = write_pre_tool_use_hook(home, None, "json_deny", "should not fire")
|
||||
{
|
||||
panic!("failed to write pre tool use hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("update the plan").await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let output_item = requests[1].function_call_output(call_id);
|
||||
let output = output_item
|
||||
.get("output")
|
||||
.and_then(Value::as_str)
|
||||
.expect("update plan output string");
|
||||
assert!(
|
||||
!output.contains("should not fire"),
|
||||
"non-shell tool output should not be blocked by PreToolUse",
|
||||
);
|
||||
|
||||
let hook_log_path = test.codex_home_path().join("pre_tool_use_hook_log.jsonl");
|
||||
assert!(
|
||||
!hook_log_path.exists(),
|
||||
"non-shell tools should not trigger pre tool use hooks",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -205,6 +206,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -522,6 +522,7 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result<
|
||||
final_output_json_schema: None,
|
||||
cwd: std::env::current_dir()?,
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
model: session_configured.model.clone(),
|
||||
effort: None,
|
||||
@@ -598,6 +599,7 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: std::env::current_dir()?,
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
model: session_configured.model.clone(),
|
||||
effort: None,
|
||||
@@ -706,6 +708,7 @@ async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done
|
||||
final_output_json_schema: None,
|
||||
cwd: std::env::current_dir()?,
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
model: session_configured.model.clone(),
|
||||
effort: None,
|
||||
@@ -892,6 +895,7 @@ async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_p
|
||||
final_output_json_schema: None,
|
||||
cwd: std::env::current_dir()?,
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
model: session_configured.model.clone(),
|
||||
effort: None,
|
||||
@@ -1005,6 +1009,7 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: std::env::current_dir()?,
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
model: session_configured.model.clone(),
|
||||
effort: None,
|
||||
|
||||
@@ -80,6 +80,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> {
|
||||
final_output_json_schema: Some(serde_json::from_str(SCHEMA)?),
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model,
|
||||
effort: None,
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
#![cfg(not(target_os = "windows"))]
|
||||
|
||||
use core_test_support::responses::mount_function_call_agent_response;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore = "disabled until we enable list_dir tool"]
|
||||
async fn list_dir_tool_returns_entries() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = test_codex().build(&server).await?;
|
||||
|
||||
let dir_path = test.cwd.path().join("sample_dir");
|
||||
std::fs::create_dir(&dir_path)?;
|
||||
std::fs::write(dir_path.join("alpha.txt"), "first file")?;
|
||||
std::fs::create_dir(dir_path.join("nested"))?;
|
||||
let dir_path = dir_path.to_string_lossy().to_string();
|
||||
|
||||
let call_id = "list-dir-call";
|
||||
let arguments = json!({
|
||||
"dir_path": dir_path,
|
||||
"offset": 1,
|
||||
"limit": 2,
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let mocks = mount_function_call_agent_response(&server, call_id, &arguments, "list_dir").await;
|
||||
test.submit_turn("list directory contents").await?;
|
||||
let req = mocks.completion.single_request();
|
||||
let (content_opt, _) = req
|
||||
.function_call_output_content_and_success(call_id)
|
||||
.expect("function_call_output present");
|
||||
let output = content_opt.expect("output content present in tool output");
|
||||
assert_eq!(output, "E1: [file] alpha.txt\nE2: [dir] nested");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore = "disabled until we enable list_dir tool"]
|
||||
async fn list_dir_tool_depth_one_omits_children() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = test_codex().build(&server).await?;
|
||||
|
||||
let dir_path = test.cwd.path().join("depth_one");
|
||||
std::fs::create_dir(&dir_path)?;
|
||||
std::fs::write(dir_path.join("alpha.txt"), "alpha")?;
|
||||
std::fs::create_dir(dir_path.join("nested"))?;
|
||||
std::fs::write(dir_path.join("nested").join("beta.txt"), "beta")?;
|
||||
let dir_path = dir_path.to_string_lossy().to_string();
|
||||
|
||||
let call_id = "list-dir-depth1";
|
||||
let arguments = json!({
|
||||
"dir_path": dir_path,
|
||||
"offset": 1,
|
||||
"limit": 10,
|
||||
"depth": 1,
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let mocks = mount_function_call_agent_response(&server, call_id, &arguments, "list_dir").await;
|
||||
test.submit_turn("list directory contents depth one")
|
||||
.await?;
|
||||
let req = mocks.completion.single_request();
|
||||
let (content_opt, _) = req
|
||||
.function_call_output_content_and_success(call_id)
|
||||
.expect("function_call_output present");
|
||||
let output = content_opt.expect("output content present in tool output");
|
||||
assert_eq!(output, "E1: [file] alpha.txt\nE2: [dir] nested");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore = "disabled until we enable list_dir tool"]
|
||||
async fn list_dir_tool_depth_two_includes_children_only() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = test_codex().build(&server).await?;
|
||||
|
||||
let dir_path = test.cwd.path().join("depth_two");
|
||||
std::fs::create_dir(&dir_path)?;
|
||||
std::fs::write(dir_path.join("alpha.txt"), "alpha")?;
|
||||
let nested = dir_path.join("nested");
|
||||
std::fs::create_dir(&nested)?;
|
||||
std::fs::write(nested.join("beta.txt"), "beta")?;
|
||||
let deeper = nested.join("grand");
|
||||
std::fs::create_dir(&deeper)?;
|
||||
std::fs::write(deeper.join("gamma.txt"), "gamma")?;
|
||||
let dir_path_string = dir_path.to_string_lossy().to_string();
|
||||
|
||||
let call_id = "list-dir-depth2";
|
||||
let arguments = json!({
|
||||
"dir_path": dir_path_string,
|
||||
"offset": 1,
|
||||
"limit": 10,
|
||||
"depth": 2,
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let mocks = mount_function_call_agent_response(&server, call_id, &arguments, "list_dir").await;
|
||||
test.submit_turn("list directory contents depth two")
|
||||
.await?;
|
||||
let req = mocks.completion.single_request();
|
||||
let (content_opt, _) = req
|
||||
.function_call_output_content_and_success(call_id)
|
||||
.expect("function_call_output present");
|
||||
let output = content_opt.expect("output content present in tool output");
|
||||
assert_eq!(
|
||||
output,
|
||||
"E1: [file] alpha.txt\nE2: [dir] nested\nE3: [file] nested/beta.txt\nE4: [dir] nested/grand"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore = "disabled until we enable list_dir tool"]
|
||||
async fn list_dir_tool_depth_three_includes_grandchildren() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = test_codex().build(&server).await?;
|
||||
|
||||
let dir_path = test.cwd.path().join("depth_three");
|
||||
std::fs::create_dir(&dir_path)?;
|
||||
std::fs::write(dir_path.join("alpha.txt"), "alpha")?;
|
||||
let nested = dir_path.join("nested");
|
||||
std::fs::create_dir(&nested)?;
|
||||
std::fs::write(nested.join("beta.txt"), "beta")?;
|
||||
let deeper = nested.join("grand");
|
||||
std::fs::create_dir(&deeper)?;
|
||||
std::fs::write(deeper.join("gamma.txt"), "gamma")?;
|
||||
let dir_path_string = dir_path.to_string_lossy().to_string();
|
||||
|
||||
let call_id = "list-dir-depth3";
|
||||
let arguments = json!({
|
||||
"dir_path": dir_path_string,
|
||||
"offset": 1,
|
||||
"limit": 10,
|
||||
"depth": 3,
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let mocks = mount_function_call_agent_response(&server, call_id, &arguments, "list_dir").await;
|
||||
test.submit_turn("list directory contents depth three")
|
||||
.await?;
|
||||
let req = mocks.completion.single_request();
|
||||
let (content_opt, _) = req
|
||||
.function_call_output_content_and_success(call_id)
|
||||
.expect("function_call_output present");
|
||||
let output = content_opt.expect("output content present in tool output");
|
||||
assert_eq!(
|
||||
output,
|
||||
"E1: [file] alpha.txt\nE2: [dir] nested\nE3: [file] nested/beta.txt\nE4: [dir] nested/grand\nE5: [file] nested/grand/gamma.txt"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -61,6 +61,7 @@ async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str)
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -75,7 +75,6 @@ mod deprecation_notice;
|
||||
mod exec;
|
||||
mod exec_policy;
|
||||
mod fork_thread;
|
||||
mod grep_files;
|
||||
mod hierarchical_agents;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
mod hooks;
|
||||
@@ -83,7 +82,6 @@ mod image_rollout;
|
||||
mod items;
|
||||
mod js_repl;
|
||||
mod json_result;
|
||||
mod list_dir;
|
||||
mod live_cli;
|
||||
mod live_reload;
|
||||
mod memories;
|
||||
@@ -101,7 +99,6 @@ mod personality_migration;
|
||||
mod plugins;
|
||||
mod prompt_caching;
|
||||
mod quota_exceeded;
|
||||
mod read_file;
|
||||
mod realtime_conversation;
|
||||
mod remote_env;
|
||||
mod remote_models;
|
||||
|
||||
@@ -126,6 +126,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -162,6 +163,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: next_model.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -221,6 +223,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -257,6 +260,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: next_model.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -398,6 +402,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: image_model_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -418,6 +423,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: text_model_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -525,6 +531,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: image_model_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -545,6 +552,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: image_model_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -655,6 +663,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: image_model_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -675,6 +684,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: text_model_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -787,6 +797,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: image_model_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -815,6 +826,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: image_model_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -969,6 +981,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: large_model_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -1027,6 +1040,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: smaller_model_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
|
||||
@@ -121,6 +121,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -144,6 +145,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: preturn_context_diff_cwd,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -222,6 +224,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd_one.clone(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -245,6 +248,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd_two,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -354,6 +358,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
|
||||
final_output_json_schema: None,
|
||||
cwd: resume_override_cwd,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: resumed.session_configured.model.clone(),
|
||||
effort: resumed.config.model_reasoning_effort,
|
||||
|
||||
@@ -96,6 +96,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: None,
|
||||
|
||||
@@ -102,6 +102,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -1024,6 +1024,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_call() {
|
||||
.features
|
||||
.disable(Feature::GhostCommit)
|
||||
.expect("test config should allow feature update");
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::Never);
|
||||
})
|
||||
.build(&server)
|
||||
.await
|
||||
@@ -1040,7 +1041,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_call() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TokenCount(_))).await;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
logs_assert(|lines: &[&str]| {
|
||||
let line = lines
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use codex_core::ForkSnapshot;
|
||||
use codex_core::config::Constrained;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_protocol::models::DeveloperInstructions;
|
||||
@@ -419,7 +420,13 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
|
||||
fork_config.permissions.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted);
|
||||
let forked = initial
|
||||
.thread_manager
|
||||
.fork_thread(usize::MAX, fork_config, rollout_path, false, None)
|
||||
.fork_thread(
|
||||
ForkSnapshot::Interrupted,
|
||||
fork_config,
|
||||
rollout_path,
|
||||
/*persist_extended_history*/ false,
|
||||
/*parent_trace*/ None,
|
||||
)
|
||||
.await?;
|
||||
forked
|
||||
.thread
|
||||
@@ -436,14 +443,14 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
|
||||
let body4 = req4.single_request().body_json();
|
||||
let input4 = body4["input"].as_array().expect("input array");
|
||||
let permissions_fork = permissions_texts(input4);
|
||||
assert_eq!(permissions_fork.len(), permissions_base.len() + 2);
|
||||
assert_eq!(permissions_fork.len(), permissions_base.len() + 1);
|
||||
assert_eq!(
|
||||
&permissions_fork[..permissions_base.len()],
|
||||
permissions_base.as_slice()
|
||||
);
|
||||
let new_permissions = &permissions_fork[permissions_base.len()..];
|
||||
assert_eq!(new_permissions.len(), 2);
|
||||
assert_eq!(new_permissions[0], new_permissions[1]);
|
||||
assert_eq!(new_permissions.len(), 1);
|
||||
assert_eq!(permissions_fork, permissions_resume);
|
||||
assert!(!permissions_base.contains(&new_permissions[0]));
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -103,6 +103,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -153,6 +154,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result<
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -210,6 +212,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -273,6 +276,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -324,6 +328,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -361,6 +366,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -427,6 +433,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -464,6 +471,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -543,6 +551,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -580,6 +589,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: test.config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -696,6 +706,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: remote_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -814,6 +825,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: remote_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -851,6 +863,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: remote_slug.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
|
||||
@@ -16,7 +16,7 @@ use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::TempDirExt;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
@@ -689,7 +689,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
|
||||
let new_cwd = TempDir::new().unwrap();
|
||||
let writable = TempDir::new().unwrap();
|
||||
let new_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::try_from(writable.path()).unwrap()],
|
||||
writable_roots: vec![writable.abs()],
|
||||
read_only_access: Default::default(),
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: true,
|
||||
@@ -703,6 +703,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
|
||||
}],
|
||||
cwd: new_cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: new_policy.clone(),
|
||||
model: "o3".to_string(),
|
||||
effort: Some(ReasoningEffort::High),
|
||||
@@ -813,8 +814,9 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: default_cwd.clone(),
|
||||
cwd: default_cwd.to_path_buf(),
|
||||
approval_policy: default_approval_policy,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: default_sandbox_policy.clone(),
|
||||
model: default_model.clone(),
|
||||
effort: default_effort,
|
||||
@@ -833,8 +835,9 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: default_cwd.clone(),
|
||||
cwd: default_cwd.to_path_buf(),
|
||||
approval_policy: default_approval_policy,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: default_sandbox_policy.clone(),
|
||||
model: default_model.clone(),
|
||||
effort: default_effort,
|
||||
@@ -937,8 +940,9 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: default_cwd.clone(),
|
||||
cwd: default_cwd.to_path_buf(),
|
||||
approval_policy: default_approval_policy,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: default_sandbox_policy.clone(),
|
||||
model: default_model,
|
||||
effort: default_effort,
|
||||
@@ -957,8 +961,9 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: default_cwd.clone(),
|
||||
cwd: default_cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: "o3".to_string(),
|
||||
effort: Some(ReasoningEffort::High),
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
#![cfg(not(target_os = "windows"))]
|
||||
|
||||
use core_test_support::responses::mount_function_call_agent_response;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore = "disabled until we enable read_file tool"]
|
||||
async fn read_file_tool_returns_requested_lines() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = test_codex().build(&server).await?;
|
||||
|
||||
let file_path = test.cwd.path().join("sample.txt");
|
||||
std::fs::write(&file_path, "first\nsecond\nthird\nfourth\n")?;
|
||||
let file_path = file_path.to_string_lossy().to_string();
|
||||
|
||||
let call_id = "read-file-call";
|
||||
let arguments = json!({
|
||||
"file_path": file_path,
|
||||
"offset": 2,
|
||||
"limit": 2,
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let mocks = mount_function_call_agent_response(&server, call_id, &arguments, "read_file").await;
|
||||
|
||||
test.submit_turn("please inspect sample.txt").await?;
|
||||
|
||||
let req = mocks.completion.single_request();
|
||||
let (output_text_opt, _) = req
|
||||
.function_call_output_content_and_success(call_id)
|
||||
.expect("output present");
|
||||
let output_text = output_text_opt.expect("output text present");
|
||||
assert_eq!(output_text, "L2: second\nL3: third");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -173,6 +173,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<(
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
|
||||
model: requested_model.to_string(),
|
||||
effort: None,
|
||||
@@ -231,6 +232,7 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning()
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
|
||||
model: requested_model.to_string(),
|
||||
effort: None,
|
||||
@@ -394,6 +396,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: REMOTE_MODEL_SLUG.to_string(),
|
||||
effort: None,
|
||||
@@ -612,6 +615,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: model.to_string(),
|
||||
effort: None,
|
||||
|
||||
@@ -193,6 +193,7 @@ async fn submit_turn(
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -145,6 +145,7 @@ async fn submit_turn(
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -138,6 +138,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -255,6 +256,7 @@ where
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -28,7 +28,7 @@ fn resume_history(
|
||||
let turn_ctx = TurnContextItem {
|
||||
turn_id: Some(turn_id.clone()),
|
||||
trace_id: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
approval_policy: config.permissions.approval_policy.value(),
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_protocol::protocol::ReviewTarget;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::load_sse_fixture_with_id_from_str;
|
||||
use core_test_support::responses::ResponseMock;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
@@ -817,7 +818,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() {
|
||||
let codex_home = Arc::new(TempDir::new().unwrap());
|
||||
let initial_cwd_path = initial_cwd.path().to_path_buf();
|
||||
let codex = new_conversation_for_server(&server, codex_home.clone(), move |config| {
|
||||
config.cwd = initial_cwd_path;
|
||||
config.cwd = initial_cwd_path.abs();
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: fixture.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -325,6 +326,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: fixture.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -528,6 +530,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
|
||||
final_output_json_schema: None,
|
||||
cwd: fixture.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: text_only_model_slug.to_string(),
|
||||
effort: None,
|
||||
@@ -642,6 +645,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: fixture.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -803,6 +807,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: fixture.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1049,6 +1054,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: fixture.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -45,6 +45,7 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() ->
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: REQUESTED_MODEL.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -143,6 +144,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: REQUESTED_MODEL.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -228,6 +230,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: REQUESTED_MODEL.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
@@ -277,6 +280,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: REQUESTED_MODEL.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
|
||||
@@ -86,7 +86,7 @@ fn tool_search_output_tools(request: &ResponsesRequest, call_id: &str) -> Vec<Va
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn configure_apps(config: &mut Config, apps_base_url: &str) {
|
||||
fn configure_apps_without_tool_search(config: &mut Config, apps_base_url: &str) {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Apps)
|
||||
@@ -105,6 +105,14 @@ fn configure_apps(config: &mut Config, apps_base_url: &str) {
|
||||
config.model_catalog = Some(model_catalog);
|
||||
}
|
||||
|
||||
fn configure_apps(config: &mut Config, apps_base_url: &str) {
|
||||
configure_apps_without_tool_search(config, apps_base_url);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ToolSearch)
|
||||
.expect("test config should allow feature update");
|
||||
}
|
||||
|
||||
fn configured_builder(apps_base_url: String) -> TestCodexBuilder {
|
||||
test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
@@ -169,6 +177,45 @@ async fn search_tool_flag_adds_tool_search() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn tool_search_disabled_by_default_exposes_apps_tools_directly() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let apps_server = AppsTestServer::mount_searchable(&server).await?;
|
||||
let mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
configure_apps_without_tool_search(config, apps_server.chatgpt_base_url.as_str())
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn_with_policies(
|
||||
"list tools",
|
||||
AskForApproval::Never,
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let body = mock.single_request().body_json();
|
||||
let tools = tool_names(&body);
|
||||
assert!(!tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME));
|
||||
assert!(tools.iter().any(|name| name == CALENDAR_CREATE_TOOL));
|
||||
assert!(tools.iter().any(|name| name == CALENDAR_LIST_TOOL));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -72,7 +72,7 @@ async fn wait_for_snapshot(codex_home: &Path) -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
async fn wait_for_file_contents(path: &Path) -> Result<String> {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let deadline = Instant::now() + Duration::from_secs(15);
|
||||
loop {
|
||||
match fs::read_to_string(path).await {
|
||||
Ok(contents) => return Ok(contents),
|
||||
@@ -164,6 +164,7 @@ async fn run_snapshot_command_with_options(
|
||||
final_output_json_schema: None,
|
||||
cwd,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -254,6 +255,7 @@ async fn run_shell_command_snapshot_with_options(
|
||||
final_output_json_schema: None,
|
||||
cwd,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -324,6 +326,7 @@ async fn run_tool_turn_on_harness(
|
||||
final_output_json_schema: None,
|
||||
cwd,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -528,7 +531,9 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> {
|
||||
let script = "apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: snapshot-apply.txt\n+hello from snapshot\n*** End Patch\nEOF\n";
|
||||
let args = json!({
|
||||
"command": script,
|
||||
"timeout_ms": 1_000,
|
||||
// The intercepted apply_patch path self-invokes codex, which can take
|
||||
// longer than a second in Bazel macOS test environments.
|
||||
"timeout_ms": 5_000,
|
||||
});
|
||||
let call_id = "shell-snapshot-apply-patch";
|
||||
let responses = vec![
|
||||
@@ -555,6 +560,7 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.clone(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model,
|
||||
effort: None,
|
||||
@@ -569,7 +575,32 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> {
|
||||
let snapshot_content = fs::read_to_string(&snapshot_path).await?;
|
||||
assert_posix_snapshot_sections(&snapshot_content);
|
||||
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
let mut saw_patch_begin = false;
|
||||
let mut patch_end = None;
|
||||
wait_for_event(&codex, |ev| match ev {
|
||||
EventMsg::PatchApplyBegin(begin) if begin.call_id == call_id => {
|
||||
saw_patch_begin = true;
|
||||
false
|
||||
}
|
||||
EventMsg::PatchApplyEnd(end) if end.call_id == call_id => {
|
||||
patch_end = Some(end.clone());
|
||||
false
|
||||
}
|
||||
EventMsg::TurnComplete(_) => true,
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
saw_patch_begin,
|
||||
"expected apply_patch to emit PatchApplyBegin"
|
||||
);
|
||||
let patch_end = patch_end.expect("expected apply_patch to emit PatchApplyEnd");
|
||||
assert!(
|
||||
patch_end.success,
|
||||
"expected apply_patch to finish successfully: stdout={:?} stderr={:?}",
|
||||
patch_end.stdout, patch_end.stderr,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
wait_for_file_contents(&target).await?,
|
||||
|
||||
@@ -65,6 +65,7 @@ async fn submit_turn_with_policies(
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy,
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: None,
|
||||
|
||||
@@ -74,6 +74,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: core/tests/suite/compact_resume_fork.rs
|
||||
expression: "context_snapshot::format_labeled_requests_snapshot(\"rollback trims pre-turn override context updates before the follow-up request\",\n&[(\"rolled-back turn request\", &requests[1]),\n(\"follow-up request after rollback\", &requests[2]),],\n&ContextSnapshotOptions::default().strip_capability_instructions().render_mode(ContextSnapshotRenderMode::KindWithTextPrefix\n{ max_chars: 96 }),)"
|
||||
---
|
||||
Scenario: rollback trims pre-turn override context updates before the follow-up request
|
||||
|
||||
## rolled-back turn request
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
02:message/user:turn 1 user
|
||||
03:message/assistant:turn 1 assistant
|
||||
04:message/developer:<collaboration_mode>ROLLED_BACK_DEV_INSTRUCTIONS</collaboration_mode>
|
||||
05:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
|
||||
06:message/user:turn 2 user
|
||||
|
||||
## follow-up request after rollback
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
02:message/user:turn 1 user
|
||||
03:message/assistant:turn 1 assistant
|
||||
04:message/developer:<collaboration_mode>ROLLED_BACK_DEV_INSTRUCTIONS</collaboration_mode>
|
||||
05:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
|
||||
06:message/user:follow-up user
|
||||
+1
-3
@@ -23,6 +23,4 @@ Scenario: rollback past compaction replay after rollback
|
||||
01:message/user:<COMPACTION_SUMMARY>\nSUMMARY_ONLY_CONTEXT
|
||||
02:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
04:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
05:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
06:message/user:AFTER_ROLLBACK
|
||||
04:message/user:AFTER_ROLLBACK
|
||||
|
||||
@@ -395,6 +395,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result<
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: test.session_configured.model.clone(),
|
||||
effort: None,
|
||||
|
||||
@@ -85,6 +85,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -155,6 +156,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -235,6 +237,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -330,6 +333,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<()
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -433,6 +437,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -42,6 +42,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -358,6 +359,7 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -486,6 +486,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: fixture.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -192,6 +192,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -325,6 +326,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -407,6 +409,7 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -492,6 +495,7 @@ async fn unified_exec_respects_workdir_override() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -589,6 +593,7 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -668,6 +673,7 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -748,6 +754,7 @@ async fn unified_exec_full_lifecycle_with_background_end_event() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -882,6 +889,7 @@ async fn unified_exec_emits_terminal_interaction_for_write_stdin() -> Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1023,6 +1031,7 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<(
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1187,6 +1196,7 @@ async fn unified_exec_emits_one_begin_and_one_end_event() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1289,6 +1299,7 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1411,6 +1422,7 @@ async fn unified_exec_defaults_to_pipe() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1505,6 +1517,7 @@ async fn unified_exec_can_enable_tty() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1590,6 +1603,7 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1725,6 +1739,7 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1897,6 +1912,7 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<()
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1978,6 +1994,7 @@ async fn unified_exec_keeps_long_running_session_after_turn_end() -> Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -2070,6 +2087,7 @@ async fn unified_exec_interrupt_preserves_long_running_session() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -2178,6 +2196,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -2317,6 +2336,7 @@ PY
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -2435,6 +2455,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -2535,6 +2556,7 @@ PY
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -2634,6 +2656,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
// Important!
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: session_model,
|
||||
@@ -2743,6 +2766,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -2842,6 +2866,7 @@ async fn unified_exec_runs_on_all_platforms() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -2981,6 +3006,7 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> {
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -10,6 +10,7 @@ use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::assert_regex_match;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
@@ -46,7 +47,7 @@ async fn user_shell_cmd_ls_and_cat_in_temp_dir() {
|
||||
let server = start_mock_server().await;
|
||||
let cwd_path = cwd.path().to_path_buf();
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config.cwd = cwd_path;
|
||||
config.cwd = cwd_path.abs();
|
||||
});
|
||||
let codex = builder
|
||||
.build(&server)
|
||||
@@ -176,6 +177,7 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()>
|
||||
final_output_json_schema: None,
|
||||
cwd: fixture.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: fixture.session_configured.model.clone(),
|
||||
effort: None,
|
||||
|
||||
@@ -41,7 +41,6 @@ use image::load_from_memory;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use std::io::Cursor;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tokio::time::Duration;
|
||||
use wiremock::BodyPrintLimit;
|
||||
@@ -78,11 +77,6 @@ fn find_image_message(body: &Value) -> Option<&Value> {
|
||||
image_messages(body).into_iter().next()
|
||||
}
|
||||
|
||||
fn absolute_path(path: &Path) -> anyhow::Result<codex_utils_absolute_path::AbsolutePathBuf> {
|
||||
codex_utils_absolute_path::AbsolutePathBuf::try_from(path.to_path_buf())
|
||||
.map_err(|err| anyhow::anyhow!("invalid absolute path {}: {err}", path.display()))
|
||||
}
|
||||
|
||||
fn png_bytes(width: u32, height: u32, rgba: [u8; 4]) -> anyhow::Result<Vec<u8>> {
|
||||
let image = ImageBuffer::from_pixel(width, height, Rgba(rgba));
|
||||
let mut cursor = Cursor::new(Vec::new());
|
||||
@@ -91,14 +85,11 @@ fn png_bytes(width: u32, height: u32, rgba: [u8; 4]) -> anyhow::Result<Vec<u8>>
|
||||
}
|
||||
|
||||
async fn create_workspace_directory(test: &TestCodex, rel_path: &str) -> anyhow::Result<PathBuf> {
|
||||
let abs_path = test.config.cwd.join(rel_path);
|
||||
let abs_path = test.config.cwd.join(rel_path)?;
|
||||
test.fs()
|
||||
.create_directory(
|
||||
&absolute_path(&abs_path)?,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
)
|
||||
.create_directory(&abs_path, CreateDirectoryOptions { recursive: true })
|
||||
.await?;
|
||||
Ok(abs_path)
|
||||
Ok(abs_path.into_path_buf())
|
||||
}
|
||||
|
||||
async fn write_workspace_file(
|
||||
@@ -106,19 +97,14 @@ async fn write_workspace_file(
|
||||
rel_path: &str,
|
||||
contents: Vec<u8>,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
let abs_path = test.config.cwd.join(rel_path);
|
||||
let abs_path = test.config.cwd.join(rel_path)?;
|
||||
if let Some(parent) = abs_path.parent() {
|
||||
test.fs()
|
||||
.create_directory(
|
||||
&absolute_path(parent)?,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
)
|
||||
.create_directory(&parent, CreateDirectoryOptions { recursive: true })
|
||||
.await?;
|
||||
}
|
||||
test.fs()
|
||||
.write_file(&absolute_path(&abs_path)?, contents)
|
||||
.await?;
|
||||
Ok(abs_path)
|
||||
test.fs().write_file(&abs_path, contents).await?;
|
||||
Ok(abs_path.into_path_buf())
|
||||
}
|
||||
|
||||
async fn write_workspace_png(
|
||||
@@ -168,8 +154,9 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> {
|
||||
path: abs_path.clone(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -239,7 +226,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
|
||||
let cwd = config.cwd.clone();
|
||||
|
||||
let rel_path = "assets/example.png";
|
||||
let abs_path = cwd.join(rel_path);
|
||||
let abs_path = cwd.join(rel_path)?;
|
||||
let original_width = 2304;
|
||||
let original_height = 864;
|
||||
write_workspace_png(
|
||||
@@ -276,8 +263,9 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -310,7 +298,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
|
||||
_ => unreachable!("stored event must be ViewImageToolCall"),
|
||||
};
|
||||
assert_eq!(tool_event.call_id, call_id);
|
||||
assert_eq!(tool_event.path, abs_path);
|
||||
assert_eq!(tool_event.path, abs_path.to_path_buf());
|
||||
|
||||
let req = mock.single_request();
|
||||
let body = req.body_json();
|
||||
@@ -416,8 +404,9 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -514,8 +503,9 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -605,8 +595,9 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -704,8 +695,9 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -814,8 +806,9 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_feat
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -917,6 +910,7 @@ await codex.emitImage(out);
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1036,6 +1030,7 @@ console.log(out.type);
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1126,8 +1121,9 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1201,8 +1197,9 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1254,7 +1251,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
|
||||
} = &test;
|
||||
|
||||
let rel_path = "missing/example.png";
|
||||
let abs_path = config.cwd.join(rel_path);
|
||||
let abs_path = config.cwd.join(rel_path)?;
|
||||
|
||||
let call_id = "view-image-missing";
|
||||
let arguments = serde_json::json!({ "path": rel_path }).to_string();
|
||||
@@ -1281,8 +1278,9 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1403,8 +1401,9 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: model_slug.to_string(),
|
||||
effort: None,
|
||||
@@ -1477,8 +1476,9 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()>
|
||||
path: abs_path.clone(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: config.cwd.clone(),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
|
||||
@@ -156,6 +156,7 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_configured.model.clone(),
|
||||
effort: None,
|
||||
|
||||
Reference in New Issue
Block a user