mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
This PR adds support for chained (layered) config.toml file merging for clients that use the app server interface. This feature already exists for the TUI, but it does not work for GUI clients. It does the following: * Changes code paths for new thread, resume thread, and fork thread to use the effective config based on the cwd. * Updates the `config/read` API to accept an optional `cwd` parameter. If specified, the API returns the effective config based on that cwd path. Also optionally includes all layers including project config files. If cwd is not specified, the API falls back on its older behavior where it considers only the global (non-project) config files when computing the effective config. The changes in codex_message_processor.rs look deceptively large. They mostly just involve moving existing blocks of code to a later point in some functions so it can use the cwd to calculate the config. This PR builds upon #9509 and should be reviewed and merged after that PR. Tested: * Verified change with (dependent, as-yet-uncommitted) changes to IDE Extension and confirmed correct behavior The full fix requires additional changes in the IDE Extension code base, but they depend on this PR.
139 lines
4.4 KiB
Rust
139 lines
4.4 KiB
Rust
use anyhow::Result;
|
|
use app_test_support::McpProcess;
|
|
use app_test_support::create_mock_responses_server_repeating_assistant;
|
|
use app_test_support::to_response;
|
|
use codex_app_server_protocol::JSONRPCNotification;
|
|
use codex_app_server_protocol::JSONRPCResponse;
|
|
use codex_app_server_protocol::RequestId;
|
|
use codex_app_server_protocol::ThreadStartParams;
|
|
use codex_app_server_protocol::ThreadStartResponse;
|
|
use codex_app_server_protocol::ThreadStartedNotification;
|
|
use codex_core::config::set_project_trust_level;
|
|
use codex_protocol::config_types::TrustLevel;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use std::path::Path;
|
|
use tempfile::TempDir;
|
|
use tokio::time::timeout;
|
|
|
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
|
|
|
#[tokio::test]
|
|
async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
|
// Provide a mock server and config so model wiring is valid.
|
|
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
|
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path(), &server.uri())?;
|
|
|
|
// Start server and initialize.
|
|
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
// Start a v2 thread with an explicit model override.
|
|
let req_id = mcp
|
|
.send_thread_start_request(ThreadStartParams {
|
|
model: Some("gpt-5.1".to_string()),
|
|
..Default::default()
|
|
})
|
|
.await?;
|
|
|
|
// Expect a proper JSON-RPC response with a thread id.
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
|
|
)
|
|
.await??;
|
|
let ThreadStartResponse {
|
|
thread,
|
|
model_provider,
|
|
..
|
|
} = to_response::<ThreadStartResponse>(resp)?;
|
|
assert!(!thread.id.is_empty(), "thread id should not be empty");
|
|
assert!(
|
|
thread.preview.is_empty(),
|
|
"new threads should start with an empty preview"
|
|
);
|
|
assert_eq!(model_provider, "mock_provider");
|
|
assert!(
|
|
thread.created_at > 0,
|
|
"created_at should be a positive UNIX timestamp"
|
|
);
|
|
|
|
// A corresponding thread/started notification should arrive.
|
|
let notif: JSONRPCNotification = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_notification_message("thread/started"),
|
|
)
|
|
.await??;
|
|
let started: ThreadStartedNotification =
|
|
serde_json::from_value(notif.params.expect("params must be present"))?;
|
|
assert_eq!(started.thread, thread);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn thread_start_respects_project_config_from_cwd() -> Result<()> {
|
|
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
|
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path(), &server.uri())?;
|
|
|
|
let workspace = TempDir::new()?;
|
|
let project_config_dir = workspace.path().join(".codex");
|
|
std::fs::create_dir_all(&project_config_dir)?;
|
|
std::fs::write(
|
|
project_config_dir.join("config.toml"),
|
|
r#"
|
|
model_reasoning_effort = "high"
|
|
"#,
|
|
)?;
|
|
set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?;
|
|
|
|
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
let req_id = mcp
|
|
.send_thread_start_request(ThreadStartParams {
|
|
cwd: Some(workspace.path().to_string_lossy().into_owned()),
|
|
..Default::default()
|
|
})
|
|
.await?;
|
|
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
|
|
)
|
|
.await??;
|
|
let ThreadStartResponse {
|
|
reasoning_effort, ..
|
|
} = to_response::<ThreadStartResponse>(resp)?;
|
|
|
|
assert_eq!(reasoning_effort, Some(ReasoningEffort::High));
|
|
Ok(())
|
|
}
|
|
|
|
// Helper to create a config.toml pointing at the mock model server.
|
|
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
|
|
let config_toml = codex_home.join("config.toml");
|
|
std::fs::write(
|
|
config_toml,
|
|
format!(
|
|
r#"
|
|
model = "mock-model"
|
|
approval_policy = "never"
|
|
sandbox_mode = "read-only"
|
|
|
|
model_provider = "mock_provider"
|
|
|
|
[model_providers.mock_provider]
|
|
name = "Mock provider for test"
|
|
base_url = "{server_uri}/v1"
|
|
wire_api = "responses"
|
|
request_max_retries = 0
|
|
stream_max_retries = 0
|
|
"#
|
|
),
|
|
)
|
|
}
|