mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
Merge e061022857 into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -131,6 +131,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
|
||||
/*include_timing_metrics*/ false,
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
@@ -266,6 +267,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
|
||||
/*include_timing_metrics*/ false,
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
@@ -387,6 +389,7 @@ async fn responses_respects_model_info_overrides_from_config() {
|
||||
/*include_timing_metrics*/ false,
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
|
||||
@@ -1334,6 +1334,10 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
|
||||
/*include_timing_metrics*/ false,
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
|
||||
/*concurrent_reasoning_summaries_enabled*/
|
||||
config
|
||||
.features
|
||||
.enabled(Feature::ConcurrentReasoningSummaries),
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
@@ -2377,6 +2381,9 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> {
|
||||
let TestCodex { codex, .. } = test_codex()
|
||||
.with_config(|config| {
|
||||
config.model_reasoning_summary = Some(ReasoningSummary::Concise);
|
||||
let _ = config
|
||||
.features
|
||||
.enable(Feature::ConcurrentReasoningSummaries);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
@@ -2413,6 +2420,55 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> {
|
||||
.and_then(|reasoning| reasoning.get("context")),
|
||||
None
|
||||
);
|
||||
pretty_assertions::assert_eq!(
|
||||
request_body
|
||||
.get("stream_options")
|
||||
.and_then(|stream_options| stream_options.get("reasoning_summary_delivery"))
|
||||
.and_then(|value| value.as_str()),
|
||||
Some("sequential_cutoff")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn sequential_cutoff_is_omitted_for_non_openai_provider() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let resp_mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
|
||||
)
|
||||
.await;
|
||||
let TestCodex { codex, .. } = test_codex()
|
||||
.with_config(|config| {
|
||||
config.model_provider.name = "mock".to_string();
|
||||
let _ = config
|
||||
.features
|
||||
.enable(Feature::ConcurrentReasoningSummaries);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let request_body = resp_mock.single_request().body_json();
|
||||
pretty_assertions::assert_eq!(request_body.get("stream_options"), None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2584,6 +2640,7 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> {
|
||||
.and_then(|reasoning| reasoning.get("summary")),
|
||||
None
|
||||
);
|
||||
pretty_assertions::assert_eq!(request_body.get("stream_options"), None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2949,6 +3006,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
/*include_timing_metrics*/ false,
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
|
||||
@@ -386,7 +386,13 @@ async fn responses_websocket_request_prewarm_reuses_connection() {
|
||||
]])
|
||||
.await;
|
||||
|
||||
let harness = websocket_harness_with_options(&server, /*runtime_metrics_enabled*/ true).await;
|
||||
let mut provider = websocket_provider(&server);
|
||||
provider.name = ModelProviderInfo::create_openai_provider(/*base_url*/ None).name;
|
||||
let harness = websocket_harness_with_provider_options(
|
||||
provider, /*runtime_metrics_enabled*/ true,
|
||||
/*concurrent_reasoning_summaries_enabled*/ true,
|
||||
)
|
||||
.await;
|
||||
let mut client_session = harness.client.new_session();
|
||||
let prompt = prompt_with_input(vec![message_item("hello")]);
|
||||
let responses_metadata = prewarm_metadata(&harness, /*turn_id*/ None);
|
||||
@@ -422,6 +428,10 @@ async fn responses_websocket_request_prewarm_reuses_connection() {
|
||||
|
||||
assert_eq!(warmup["type"].as_str(), Some("response.create"));
|
||||
assert_eq!(warmup["generate"].as_bool(), Some(false));
|
||||
assert_eq!(
|
||||
warmup["stream_options"]["reasoning_summary_delivery"].as_str(),
|
||||
Some("sequential_cutoff")
|
||||
);
|
||||
assert_eq!(warmup["tools"], serde_json::json!([]));
|
||||
let warmup_turn_metadata: serde_json::Value = serde_json::from_str(
|
||||
warmup["client_metadata"]["x-codex-turn-metadata"]
|
||||
@@ -436,6 +446,10 @@ async fn responses_websocket_request_prewarm_reuses_connection() {
|
||||
assert_eq!(follow_up["type"].as_str(), Some("response.create"));
|
||||
assert_eq!(follow_up["previous_response_id"].as_str(), Some("warm-1"));
|
||||
assert_eq!(follow_up["input"], serde_json::json!([]));
|
||||
assert_eq!(
|
||||
follow_up["stream_options"]["reasoning_summary_delivery"].as_str(),
|
||||
Some("sequential_cutoff")
|
||||
);
|
||||
|
||||
server.shutdown().await;
|
||||
}
|
||||
@@ -990,8 +1004,11 @@ async fn responses_websocket_v2_incremental_requests_are_reused_across_turns() {
|
||||
// use OpenAI provider to check metadata logic
|
||||
let mut provider = websocket_provider(&server);
|
||||
provider.name = ModelProviderInfo::create_openai_provider(/*base_url*/ None).name;
|
||||
let harness =
|
||||
websocket_harness_with_provider_options(provider, /*runtime_metrics_enabled*/ false).await;
|
||||
let harness = websocket_harness_with_provider_options(
|
||||
provider, /*runtime_metrics_enabled*/ false,
|
||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Turn one: initiate
|
||||
let prompt_one = prompt_with_input(vec![message_item("hello")]);
|
||||
@@ -2195,13 +2212,18 @@ async fn websocket_harness_with_options(
|
||||
server: &WebSocketTestServer,
|
||||
runtime_metrics_enabled: bool,
|
||||
) -> WebsocketTestHarness {
|
||||
websocket_harness_with_provider_options(websocket_provider(server), runtime_metrics_enabled)
|
||||
.await
|
||||
websocket_harness_with_provider_options(
|
||||
websocket_provider(server),
|
||||
runtime_metrics_enabled,
|
||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn websocket_harness_with_provider_options(
|
||||
provider: ModelProviderInfo,
|
||||
runtime_metrics_enabled: bool,
|
||||
concurrent_reasoning_summaries_enabled: bool,
|
||||
) -> WebsocketTestHarness {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
@@ -2212,6 +2234,12 @@ async fn websocket_harness_with_provider_options(
|
||||
.enable(Feature::RuntimeMetrics)
|
||||
.expect("test config should allow feature update");
|
||||
}
|
||||
if concurrent_reasoning_summaries_enabled {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ConcurrentReasoningSummaries)
|
||||
.expect("test config should allow feature update");
|
||||
}
|
||||
let config = Arc::new(config);
|
||||
let model_info = codex_core::test_support::construct_model_info_offline(MODEL, &config);
|
||||
let thread_id = ThreadId::new();
|
||||
@@ -2251,6 +2279,10 @@ async fn websocket_harness_with_provider_options(
|
||||
runtime_metrics_enabled,
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
|
||||
/*concurrent_reasoning_summaries_enabled*/
|
||||
config
|
||||
.features
|
||||
.enabled(Feature::ConcurrentReasoningSummaries),
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_config::test_support::CloudConfigBundleFixture;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
@@ -17,6 +18,7 @@ use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_target_windows;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_codex::turn_permission_fields;
|
||||
@@ -255,6 +257,84 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn malformed_custom_rules_preserve_managed_forbidden_prefix() -> Result<()> {
|
||||
skip_if_target_windows!(
|
||||
Ok(()),
|
||||
"managed prefix fixture uses POSIX executable semantics"
|
||||
);
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_cloud_config_bundle(
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||
r#"
|
||||
[rules]
|
||||
prefix_rules = [
|
||||
{ pattern = [{ token = "echo" }], decision = "forbidden" },
|
||||
]
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.expect("test config should allow feature update");
|
||||
let policy_path = config.codex_home.join("rules").join("broken.rules");
|
||||
fs::create_dir_all(
|
||||
policy_path
|
||||
.parent()
|
||||
.expect("policy directory must have a parent"),
|
||||
)
|
||||
.expect("create policy directory");
|
||||
fs::write(policy_path, "prefix_rule(").expect("write malformed policy file");
|
||||
});
|
||||
let server = start_mock_server().await;
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
let call_id = "managed-shell-forbidden";
|
||||
let args = json!({
|
||||
"cmd": "echo blocked",
|
||||
"yield_time_ms": 1_000,
|
||||
});
|
||||
|
||||
mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-managed-1"),
|
||||
ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?),
|
||||
ev_completed("resp-managed-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let results_mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-managed-1", "done"),
|
||||
ev_completed("resp-managed-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
test.submit_turn_with_approval_and_permission_profile(
|
||||
"run shell command",
|
||||
AskForApproval::Never,
|
||||
PermissionProfile::Disabled,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let output_item = results_mock.single_request().function_call_output(call_id);
|
||||
let output = output_item
|
||||
.get("output")
|
||||
.and_then(Value::as_str)
|
||||
.expect("function call output should include a string output payload");
|
||||
assert!(
|
||||
output.contains("policy forbids commands starting with `echo`"),
|
||||
"unexpected output: {output}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn shell_command_empty_script_with_collaboration_mode_does_not_panic() -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![cfg(not(target_os = "windows"))]
|
||||
|
||||
use anyhow::Ok;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
@@ -1134,6 +1135,98 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn sequential_cutoff_renders_done_summaries_for_active_reasoning_item() -> anyhow::Result<()>
|
||||
{
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
||||
let TestCodex { codex, .. } = test_codex()
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ConcurrentReasoningSummaries)
|
||||
.expect("test config should allow feature update");
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
let summary_done = |item_id: &str, summary_index: i64, text: &str| {
|
||||
serde_json::json!({
|
||||
"type": "response.reasoning_summary_text.done",
|
||||
"item_id": item_id,
|
||||
"summary_index": summary_index,
|
||||
"text": text,
|
||||
})
|
||||
};
|
||||
mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_reasoning_item_added("reasoning-1", &[""]),
|
||||
// Sequential cutoff renders atomic done events, not partial legacy events.
|
||||
ev_reasoning_summary_text_delta("partial"),
|
||||
summary_done("reasoning-1", 0, "step one"),
|
||||
summary_done("reasoning-1", 1, "step two"),
|
||||
ev_reasoning_item("reasoning-1", &["step one", "step two"], &[]),
|
||||
ev_message_item_added("message-1", ""),
|
||||
// Drop a stale completion rather than attaching it to the active message.
|
||||
summary_done("reasoning-1", 2, "late step"),
|
||||
ev_output_text_delta("Done"),
|
||||
ev_assistant_message("message-1", "Done"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "reason through it".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let reasoning_item = wait_for_event_match(&codex, |ev| match ev {
|
||||
EventMsg::ItemStarted(ItemStartedEvent {
|
||||
item: TurnItem::Reasoning(item),
|
||||
..
|
||||
}) => Some(item.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut summary_deltas = Vec::new();
|
||||
let mut summary_sections = Vec::new();
|
||||
loop {
|
||||
match wait_for_event(&codex, |_| true).await {
|
||||
EventMsg::ReasoningContentDelta(event) => {
|
||||
summary_deltas.push((event.item_id, event.delta, event.summary_index));
|
||||
}
|
||||
EventMsg::AgentReasoningSectionBreak(event) => {
|
||||
summary_sections.push((event.item_id, event.summary_index));
|
||||
}
|
||||
EventMsg::TurnComplete(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
summary_deltas,
|
||||
vec![
|
||||
(reasoning_item.id.clone(), "step one".to_string(), 0),
|
||||
(reasoning_item.id, "step two".to_string(), 1),
|
||||
]
|
||||
);
|
||||
assert_eq!(summary_sections, vec![("reasoning-1".to_string(), 1)]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user