Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 178 additions & 86 deletions codex-rs/app-server/tests/common/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,159 @@ use codex_features::Feature;
use std::collections::BTreeMap;
use std::path::Path;

/// Composes the standard mock Responses provider with test-specific configuration.
pub struct MockResponsesConfig {
provider_id: String,
provider_name: String,
provider_base_url: String,
model: String,
approval_policy: String,
sandbox_mode: String,
features: BTreeMap<Feature, bool>,
root_config: Vec<String>,
provider_config: Vec<String>,
extra_config: Vec<String>,
}

impl MockResponsesConfig {
pub fn new(server_uri: &str) -> Self {
Self {
provider_id: "mock_provider".to_string(),
provider_name: "Mock provider for test".to_string(),
provider_base_url: format!("{server_uri}/v1"),
model: "mock-model".to_string(),
approval_policy: "never".to_string(),
sandbox_mode: "read-only".to_string(),
features: BTreeMap::new(),
root_config: Vec::new(),
provider_config: Vec::new(),
extra_config: Vec::new(),
}
}

pub fn with_model_provider(mut self, provider_id: &str) -> Self {
self.provider_id = provider_id.to_string();
self
}

pub fn with_provider_name(mut self, provider_name: &str) -> Self {
self.provider_name = provider_name.to_string();
self
}

pub fn with_provider_base_url(mut self, provider_base_url: &str) -> Self {
self.provider_base_url = provider_base_url.to_string();
self
}

pub fn with_model(mut self, model: &str) -> Self {
self.model = model.to_string();
self
}

pub fn with_approval_policy(mut self, approval_policy: &str) -> Self {
self.approval_policy = approval_policy.to_string();
self
}

pub fn with_sandbox_mode(mut self, sandbox_mode: &str) -> Self {
self.sandbox_mode = sandbox_mode.to_string();
self
}

pub fn enable_feature(mut self, feature: Feature) -> Self {
self.features.insert(feature, true);
self
}

pub fn disable_feature(mut self, feature: Feature) -> Self {
self.features.insert(feature, false);
self
}

pub fn with_features(mut self, features: &BTreeMap<Feature, bool>) -> Self {
self.features.extend(
features
.iter()
.map(|(&feature, &enabled)| (feature, enabled)),
);
self
}

pub fn with_root_config(mut self, config: &str) -> Self {
self.root_config.push(config.to_string());
self
}

pub fn with_provider_config(mut self, config: &str) -> Self {
self.provider_config.push(config.to_string());
self
}

pub fn with_extra_config(mut self, config: &str) -> Self {
self.extra_config.push(config.to_string());
self
}

pub fn write(self, codex_home: &Path) -> std::io::Result<()> {
let Self {
provider_id,
provider_name,
provider_base_url,
model,
approval_policy,
sandbox_mode,
features,
root_config,
provider_config,
extra_config,
} = self;
let root_config = root_config.join("\n");
let provider_config = provider_config.join("\n");
let extra_config = extra_config.join("\n");
let feature_entries = features
.into_iter()
.map(|(feature, enabled)| {
let key = FEATURES
.iter()
.find(|spec| spec.id == feature)
.map(|spec| spec.key)
.expect("feature should have a config key");
format!("{key} = {enabled}")
})
.collect::<Vec<_>>()
.join("\n");
let feature_config = if feature_entries.is_empty() {
String::new()
} else {
format!("[features]\n{feature_entries}\n\n")
};

std::fs::write(
codex_home.join("config.toml"),
format!(
r#"
model = "{model}"
approval_policy = "{approval_policy}"
sandbox_mode = "{sandbox_mode}"
{root_config}
model_provider = "{provider_id}"

{feature_config}[model_providers.{provider_id}]
name = "{provider_name}"
base_url = "{provider_base_url}"
wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
{provider_config}

{extra_config}
"#
),
)
}
}

pub fn write_mock_responses_config_toml(
codex_home: &Path,
server_uri: &str,
Expand All @@ -12,97 +165,36 @@ pub fn write_mock_responses_config_toml(
model_provider_id: &str,
compact_prompt: &str,
) -> std::io::Result<()> {
// Phase 1: build the features block for config.toml.
let mut features = BTreeMap::new();
for (feature, enabled) in feature_flags {
features.insert(*feature, *enabled);
}
let feature_entries = features
.into_iter()
.map(|(feature, enabled)| {
let key = FEATURES
.iter()
.find(|spec| spec.id == feature)
.map(|spec| spec.key)
.expect("feature should have a config key");
format!("{key} = {enabled}")
})
.collect::<Vec<_>>()
.join("\n");
// Phase 2: build provider-specific config bits.
let requires_line = match requires_openai_auth {
Some(true) => "requires_openai_auth = true\n".to_string(),
Some(false) | None => String::new(),
};
let provider_name = if matches!(requires_openai_auth, Some(true)) {
"OpenAI"
} else {
"Mock provider for test"
};
let provider_block = format!(
r#"
[model_providers.{model_provider_id}]
name = "{provider_name}"
base_url = "{server_uri}/v1"
wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
supports_websockets = false
{requires_line}
"#
);
let openai_base_url_line = if model_provider_id == "openai" {
format!("openai_base_url = \"{server_uri}/v1\"\n")
} else {
String::new()
};
// Phase 3: write the final config file.
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"
compact_prompt = "{compact_prompt}"
model_auto_compact_token_limit = {auto_compact_limit}

model_provider = "{model_provider_id}"
{openai_base_url_line}

[features]
{feature_entries}
{provider_block}
"#
),
)
let mut config = MockResponsesConfig::new(server_uri)
.with_model_provider(model_provider_id)
.with_features(feature_flags)
.with_root_config(&format!(
"compact_prompt = \"{compact_prompt}\"\nmodel_auto_compact_token_limit = {auto_compact_limit}"
))
.with_provider_config("supports_websockets = false");

if model_provider_id == "openai" {
config = config.with_root_config(&format!("openai_base_url = \"{server_uri}/v1\""));
}
if matches!(requires_openai_auth, Some(true)) {
config = config
.with_provider_name("OpenAI")
.with_provider_config("requires_openai_auth = true");
}

config.write(codex_home)
}

pub fn write_mock_responses_config_toml_with_chatgpt_base_url(
codex_home: &Path,
server_uri: &str,
chatgpt_base_url: &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"
chatgpt_base_url = "{chatgpt_base_url}"

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
"#
),
)
MockResponsesConfig::new(server_uri)
.with_root_config(&format!("chatgpt_base_url = \"{chatgpt_base_url}\""))
.write(codex_home)
}

#[cfg(test)]
#[path = "config_tests.rs"]
mod tests;
72 changes: 72 additions & 0 deletions codex-rs/app-server/tests/common/config_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use super::*;
use tempfile::TempDir;

#[test]
fn mock_responses_config_composes_model_provider_features_and_extra_tables() {
let home = TempDir::new().expect("temporary CODEX_HOME");
MockResponsesConfig::new("http://127.0.0.1:1234")
.with_model("custom-model")
.with_model_provider("openai-custom")
.with_provider_name("OpenAI")
.with_provider_base_url("http://127.0.0.1:1234/api/codex")
.with_approval_policy("on-request")
.with_sandbox_mode("workspace-write")
.enable_feature(Feature::Personality)
.disable_feature(Feature::ShellSnapshot)
.with_root_config("chatgpt_base_url = \"http://127.0.0.1:1234\"")
.with_provider_config("requires_openai_auth = true")
.with_extra_config("[extra]\nenabled = true")
.write(home.path())
.expect("write composable mock Responses config");

let config =
std::fs::read_to_string(home.path().join("config.toml")).expect("read config.toml");
for expected in [
"model = \"custom-model\"",
"approval_policy = \"on-request\"",
"sandbox_mode = \"workspace-write\"",
"chatgpt_base_url = \"http://127.0.0.1:1234\"",
"model_provider = \"openai-custom\"",
"shell_snapshot = false",
"personality = true",
"[model_providers.openai-custom]\nname = \"OpenAI\"",
"base_url = \"http://127.0.0.1:1234/api/codex\"",
"requires_openai_auth = true",
"[extra]\nenabled = true",
] {
assert!(config.contains(expected), "config is missing {expected}");
}
}

#[test]
fn legacy_mock_responses_writer_preserves_provider_auth_and_feature_overrides() {
let home = TempDir::new().expect("temporary CODEX_HOME");
write_mock_responses_config_toml(
home.path(),
"http://127.0.0.1:1234",
&BTreeMap::from([
(Feature::Personality, true),
(Feature::ShellSnapshot, false),
]),
/*auto_compact_limit*/ 321,
Some(true),
"openai",
"compact this",
)
.expect("write legacy-compatible mock Responses config");

let config =
std::fs::read_to_string(home.path().join("config.toml")).expect("read config.toml");
for expected in [
"compact_prompt = \"compact this\"",
"model_auto_compact_token_limit = 321",
"openai_base_url = \"http://127.0.0.1:1234/v1\"",
"shell_snapshot = false",
"personality = true",
"[model_providers.openai]\nname = \"OpenAI\"",
"supports_websockets = false",
"requires_openai_auth = true",
] {
assert!(config.contains(expected), "config is missing {expected}");
}
}
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/common/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub use auth_fixtures::ChatGptIdTokenClaims;
pub use auth_fixtures::encode_id_token;
pub use auth_fixtures::write_chatgpt_auth;
use codex_app_server_protocol::JSONRPCResponse;
pub use config::MockResponsesConfig;
pub use config::write_mock_responses_config_toml;
pub use config::write_mock_responses_config_toml_with_chatgpt_base_url;
pub use core_test_support::PathBufExt;
Expand Down
Loading
Loading