chore: expose codex_home via Config (#941)
This commit is contained in:
@@ -610,7 +610,7 @@ async fn submission_loop(
|
|||||||
// `instructions` value into the Session struct.
|
// `instructions` value into the Session struct.
|
||||||
let session_id = Uuid::new_v4();
|
let session_id = Uuid::new_v4();
|
||||||
let rollout_recorder =
|
let rollout_recorder =
|
||||||
match RolloutRecorder::new(session_id, instructions.clone()).await {
|
match RolloutRecorder::new(&config, session_id, instructions.clone()).await {
|
||||||
Ok(r) => Some(r),
|
Ok(r) => Some(r),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("failed to initialise rollout recorder: {e}");
|
tracing::warn!("failed to initialise rollout recorder: {e}");
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ pub struct Config {
|
|||||||
|
|
||||||
/// Maximum number of bytes to include from an AGENTS.md project doc file.
|
/// Maximum number of bytes to include from an AGENTS.md project doc file.
|
||||||
pub project_doc_max_bytes: usize,
|
pub project_doc_max_bytes: usize,
|
||||||
|
|
||||||
|
/// Directory containing all Codex state (defaults to `~/.codex` but can be
|
||||||
|
/// overridden by the `CODEX_HOME` environment variable).
|
||||||
|
pub codex_home: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Base config deserialized from ~/.codex/config.toml.
|
/// Base config deserialized from ~/.codex/config.toml.
|
||||||
@@ -132,8 +136,8 @@ impl ConfigToml {
|
|||||||
/// Attempt to parse the file at `~/.codex/config.toml`. If it does not
|
/// Attempt to parse the file at `~/.codex/config.toml`. If it does not
|
||||||
/// exist, return a default config. Though if it exists and cannot be
|
/// exist, return a default config. Though if it exists and cannot be
|
||||||
/// parsed, report that to the user and force them to fix it.
|
/// parsed, report that to the user and force them to fix it.
|
||||||
fn load_from_toml() -> std::io::Result<Self> {
|
fn load_from_toml(codex_home: &Path) -> std::io::Result<Self> {
|
||||||
let config_toml_path = codex_dir()?.join("config.toml");
|
let config_toml_path = codex_home.join("config.toml");
|
||||||
match std::fs::read_to_string(&config_toml_path) {
|
match std::fs::read_to_string(&config_toml_path) {
|
||||||
Ok(contents) => toml::from_str::<Self>(&contents).map_err(|e| {
|
Ok(contents) => toml::from_str::<Self>(&contents).map_err(|e| {
|
||||||
tracing::error!("Failed to parse config.toml: {e}");
|
tracing::error!("Failed to parse config.toml: {e}");
|
||||||
@@ -161,7 +165,7 @@ where
|
|||||||
|
|
||||||
match permissions {
|
match permissions {
|
||||||
Some(raw_permissions) => {
|
Some(raw_permissions) => {
|
||||||
let base_path = codex_dir().map_err(serde::de::Error::custom)?;
|
let base_path = find_codex_home().map_err(serde::de::Error::custom)?;
|
||||||
|
|
||||||
let converted = raw_permissions
|
let converted = raw_permissions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -194,18 +198,25 @@ impl Config {
|
|||||||
/// ~/.codex/config.toml, ~/.codex/instructions.md, embedded defaults, and
|
/// ~/.codex/config.toml, ~/.codex/instructions.md, embedded defaults, and
|
||||||
/// any values provided in `overrides` (highest precedence).
|
/// any values provided in `overrides` (highest precedence).
|
||||||
pub fn load_with_overrides(overrides: ConfigOverrides) -> std::io::Result<Self> {
|
pub fn load_with_overrides(overrides: ConfigOverrides) -> std::io::Result<Self> {
|
||||||
let cfg: ConfigToml = ConfigToml::load_from_toml()?;
|
// Resolve the directory that stores Codex state (e.g. ~/.codex or the
|
||||||
|
// value of $CODEX_HOME) so we can embed it into the resulting
|
||||||
|
// `Config` instance.
|
||||||
|
let codex_home = find_codex_home()?;
|
||||||
|
|
||||||
|
let cfg: ConfigToml = ConfigToml::load_from_toml(&codex_home)?;
|
||||||
tracing::warn!("Config parsed from config.toml: {cfg:?}");
|
tracing::warn!("Config parsed from config.toml: {cfg:?}");
|
||||||
let codex_dir = codex_dir().ok();
|
|
||||||
Self::load_from_base_config_with_overrides(cfg, overrides, codex_dir.as_deref())
|
Self::load_from_base_config_with_overrides(cfg, overrides, codex_home)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_from_base_config_with_overrides(
|
/// Meant to be used exclusively for tests: `load_with_overrides()` should
|
||||||
|
/// be used in all other cases.
|
||||||
|
pub fn load_from_base_config_with_overrides(
|
||||||
cfg: ConfigToml,
|
cfg: ConfigToml,
|
||||||
overrides: ConfigOverrides,
|
overrides: ConfigOverrides,
|
||||||
codex_dir: Option<&Path>,
|
codex_home: PathBuf,
|
||||||
) -> std::io::Result<Self> {
|
) -> std::io::Result<Self> {
|
||||||
let instructions = Self::load_instructions(codex_dir);
|
let instructions = Self::load_instructions(Some(&codex_home));
|
||||||
|
|
||||||
// Destructure ConfigOverrides fully to ensure all overrides are applied.
|
// Destructure ConfigOverrides fully to ensure all overrides are applied.
|
||||||
let ConfigOverrides {
|
let ConfigOverrides {
|
||||||
@@ -308,6 +319,7 @@ impl Config {
|
|||||||
mcp_servers: cfg.mcp_servers,
|
mcp_servers: cfg.mcp_servers,
|
||||||
model_providers,
|
model_providers,
|
||||||
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES),
|
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES),
|
||||||
|
codex_home,
|
||||||
};
|
};
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
@@ -328,27 +340,29 @@ impl Config {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Meant to be used exclusively for tests: `load_with_overrides()` should
|
|
||||||
/// be used in all other cases.
|
|
||||||
pub fn load_default_config_for_test() -> Self {
|
|
||||||
#[expect(clippy::expect_used)]
|
|
||||||
Self::load_from_base_config_with_overrides(
|
|
||||||
ConfigToml::default(),
|
|
||||||
ConfigOverrides::default(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.expect("defaults for test should always succeed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_model() -> String {
|
fn default_model() -> String {
|
||||||
OPENAI_DEFAULT_MODEL.to_string()
|
OPENAI_DEFAULT_MODEL.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the Codex configuration directory, which is `~/.codex`.
|
/// Returns the path to the Codex configuration directory, which can be
|
||||||
/// Does not verify that the directory exists.
|
/// specified by the `CODEX_HOME` environment variable. If not set, defaults to
|
||||||
pub fn codex_dir() -> std::io::Result<PathBuf> {
|
/// `~/.codex`.
|
||||||
|
///
|
||||||
|
/// - If `CODEX_HOME` is set, the value will be canonicalized and this
|
||||||
|
/// function will Err if the path does not exist.
|
||||||
|
/// - If `CODEX_HOME` is not set, this function does not verify that the
|
||||||
|
/// directory exists.
|
||||||
|
fn find_codex_home() -> std::io::Result<PathBuf> {
|
||||||
|
// Honor the `CODEX_HOME` environment variable when it is set to allow users
|
||||||
|
// (and tests) to override the default location.
|
||||||
|
if let Ok(val) = std::env::var("CODEX_HOME") {
|
||||||
|
if !val.is_empty() {
|
||||||
|
return PathBuf::from(val).canonicalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut p = home_dir().ok_or_else(|| {
|
let mut p = home_dir().ok_or_else(|| {
|
||||||
std::io::Error::new(
|
std::io::Error::new(
|
||||||
std::io::ErrorKind::NotFound,
|
std::io::ErrorKind::NotFound,
|
||||||
@@ -361,8 +375,8 @@ pub fn codex_dir() -> std::io::Result<PathBuf> {
|
|||||||
|
|
||||||
/// Returns the path to the folder where Codex logs are stored. Does not verify
|
/// Returns the path to the folder where Codex logs are stored. Does not verify
|
||||||
/// that the directory exists.
|
/// that the directory exists.
|
||||||
pub fn log_dir() -> std::io::Result<PathBuf> {
|
pub fn log_dir(cfg: &Config) -> std::io::Result<PathBuf> {
|
||||||
let mut p = codex_dir()?;
|
let mut p = cfg.codex_home.clone();
|
||||||
p.push("log");
|
p.push("log");
|
||||||
Ok(p)
|
Ok(p)
|
||||||
}
|
}
|
||||||
@@ -470,20 +484,26 @@ mod tests {
|
|||||||
assert!(msg.contains("not-a-real-permission"));
|
assert!(msg.contains("not-a-real-permission"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Users can specify config values at multiple levels that have the
|
struct PrecedenceTestFixture {
|
||||||
/// following precedence:
|
cwd: TempDir,
|
||||||
///
|
codex_home: TempDir,
|
||||||
/// 1. custom command-line argument, e.g. `--model o3`
|
cfg: ConfigToml,
|
||||||
/// 2. as part of a profile, where the `--profile` is specified via a CLI
|
model_provider_map: HashMap<String, ModelProviderInfo>,
|
||||||
/// (or in the config file itelf)
|
openai_provider: ModelProviderInfo,
|
||||||
/// 3. as an entry in `config.toml`, e.g. `model = "o3"`
|
openai_chat_completions_provider: ModelProviderInfo,
|
||||||
/// 4. the default value for a required field defined in code, e.g.,
|
}
|
||||||
/// `crate::flags::OPENAI_DEFAULT_MODEL`
|
|
||||||
///
|
impl PrecedenceTestFixture {
|
||||||
/// Note that profiles are the recommended way to specify a group of
|
fn cwd(&self) -> PathBuf {
|
||||||
/// configuration options together.
|
self.cwd.path().to_path_buf()
|
||||||
#[test]
|
}
|
||||||
fn test_precedence_overrides_then_profile_then_config_toml() -> std::io::Result<()> {
|
|
||||||
|
fn codex_home(&self) -> PathBuf {
|
||||||
|
self.codex_home.path().to_path_buf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_test_fixture() -> std::io::Result<PrecedenceTestFixture> {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
model = "o3"
|
model = "o3"
|
||||||
approval_policy = "unless-allow-listed"
|
approval_policy = "unless-allow-listed"
|
||||||
@@ -526,6 +546,8 @@ disable_response_storage = true
|
|||||||
// a parent folder, either.
|
// a parent folder, either.
|
||||||
std::fs::write(cwd.join(".git"), "gitdir: nowhere")?;
|
std::fs::write(cwd.join(".git"), "gitdir: nowhere")?;
|
||||||
|
|
||||||
|
let codex_home_temp_dir = TempDir::new().unwrap();
|
||||||
|
|
||||||
let openai_chat_completions_provider = ModelProviderInfo {
|
let openai_chat_completions_provider = ModelProviderInfo {
|
||||||
name: "OpenAI using Chat Completions".to_string(),
|
name: "OpenAI using Chat Completions".to_string(),
|
||||||
base_url: "https://api.openai.com/v1".to_string(),
|
base_url: "https://api.openai.com/v1".to_string(),
|
||||||
@@ -547,94 +569,143 @@ disable_response_storage = true
|
|||||||
.expect("openai provider should exist")
|
.expect("openai provider should exist")
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
|
Ok(PrecedenceTestFixture {
|
||||||
|
cwd: cwd_temp_dir,
|
||||||
|
codex_home: codex_home_temp_dir,
|
||||||
|
cfg,
|
||||||
|
model_provider_map,
|
||||||
|
openai_provider,
|
||||||
|
openai_chat_completions_provider,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Users can specify config values at multiple levels that have the
|
||||||
|
/// following precedence:
|
||||||
|
///
|
||||||
|
/// 1. custom command-line argument, e.g. `--model o3`
|
||||||
|
/// 2. as part of a profile, where the `--profile` is specified via a CLI
|
||||||
|
/// (or in the config file itelf)
|
||||||
|
/// 3. as an entry in `config.toml`, e.g. `model = "o3"`
|
||||||
|
/// 4. the default value for a required field defined in code, e.g.,
|
||||||
|
/// `crate::flags::OPENAI_DEFAULT_MODEL`
|
||||||
|
///
|
||||||
|
/// Note that profiles are the recommended way to specify a group of
|
||||||
|
/// configuration options together.
|
||||||
|
#[test]
|
||||||
|
fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||||
|
let fixture = create_test_fixture()?;
|
||||||
|
|
||||||
let o3_profile_overrides = ConfigOverrides {
|
let o3_profile_overrides = ConfigOverrides {
|
||||||
config_profile: Some("o3".to_string()),
|
config_profile: Some("o3".to_string()),
|
||||||
cwd: Some(cwd.clone()),
|
cwd: Some(fixture.cwd()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let o3_profile_config =
|
let o3_profile_config: Config = Config::load_from_base_config_with_overrides(
|
||||||
Config::load_from_base_config_with_overrides(cfg.clone(), o3_profile_overrides, None)?;
|
fixture.cfg.clone(),
|
||||||
|
o3_profile_overrides,
|
||||||
|
fixture.codex_home(),
|
||||||
|
)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Config {
|
Config {
|
||||||
model: "o3".to_string(),
|
model: "o3".to_string(),
|
||||||
model_provider_id: "openai".to_string(),
|
model_provider_id: "openai".to_string(),
|
||||||
model_provider: openai_provider.clone(),
|
model_provider: fixture.openai_provider.clone(),
|
||||||
approval_policy: AskForApproval::Never,
|
approval_policy: AskForApproval::Never,
|
||||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||||
disable_response_storage: false,
|
disable_response_storage: false,
|
||||||
instructions: None,
|
instructions: None,
|
||||||
notify: None,
|
notify: None,
|
||||||
cwd: cwd.clone(),
|
cwd: fixture.cwd(),
|
||||||
mcp_servers: HashMap::new(),
|
mcp_servers: HashMap::new(),
|
||||||
model_providers: model_provider_map.clone(),
|
model_providers: fixture.model_provider_map.clone(),
|
||||||
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
||||||
|
codex_home: fixture.codex_home(),
|
||||||
},
|
},
|
||||||
o3_profile_config
|
o3_profile_config
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||||
|
let fixture = create_test_fixture()?;
|
||||||
|
|
||||||
let gpt3_profile_overrides = ConfigOverrides {
|
let gpt3_profile_overrides = ConfigOverrides {
|
||||||
config_profile: Some("gpt3".to_string()),
|
config_profile: Some("gpt3".to_string()),
|
||||||
cwd: Some(cwd.clone()),
|
cwd: Some(fixture.cwd()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let gpt3_profile_config = Config::load_from_base_config_with_overrides(
|
let gpt3_profile_config = Config::load_from_base_config_with_overrides(
|
||||||
cfg.clone(),
|
fixture.cfg.clone(),
|
||||||
gpt3_profile_overrides,
|
gpt3_profile_overrides,
|
||||||
None,
|
fixture.codex_home(),
|
||||||
)?;
|
)?;
|
||||||
let expected_gpt3_profile_config = Config {
|
let expected_gpt3_profile_config = Config {
|
||||||
model: "gpt-3.5-turbo".to_string(),
|
model: "gpt-3.5-turbo".to_string(),
|
||||||
model_provider_id: "openai-chat-completions".to_string(),
|
model_provider_id: "openai-chat-completions".to_string(),
|
||||||
model_provider: openai_chat_completions_provider,
|
model_provider: fixture.openai_chat_completions_provider.clone(),
|
||||||
approval_policy: AskForApproval::UnlessAllowListed,
|
approval_policy: AskForApproval::UnlessAllowListed,
|
||||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||||
disable_response_storage: false,
|
disable_response_storage: false,
|
||||||
instructions: None,
|
instructions: None,
|
||||||
notify: None,
|
notify: None,
|
||||||
cwd: cwd.clone(),
|
cwd: fixture.cwd(),
|
||||||
mcp_servers: HashMap::new(),
|
mcp_servers: HashMap::new(),
|
||||||
model_providers: model_provider_map.clone(),
|
model_providers: fixture.model_provider_map.clone(),
|
||||||
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
||||||
|
codex_home: fixture.codex_home(),
|
||||||
};
|
};
|
||||||
assert_eq!(expected_gpt3_profile_config.clone(), gpt3_profile_config);
|
|
||||||
|
assert_eq!(expected_gpt3_profile_config, gpt3_profile_config);
|
||||||
|
|
||||||
// Verify that loading without specifying a profile in ConfigOverrides
|
// Verify that loading without specifying a profile in ConfigOverrides
|
||||||
// uses the default profile from the config file.
|
// uses the default profile from the config file (which is "gpt3").
|
||||||
let default_profile_overrides = ConfigOverrides {
|
let default_profile_overrides = ConfigOverrides {
|
||||||
cwd: Some(cwd.clone()),
|
cwd: Some(fixture.cwd()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let default_profile_config = Config::load_from_base_config_with_overrides(
|
let default_profile_config = Config::load_from_base_config_with_overrides(
|
||||||
cfg.clone(),
|
fixture.cfg.clone(),
|
||||||
default_profile_overrides,
|
default_profile_overrides,
|
||||||
None,
|
fixture.codex_home(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
assert_eq!(expected_gpt3_profile_config, default_profile_config);
|
assert_eq!(expected_gpt3_profile_config, default_profile_config);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||||
|
let fixture = create_test_fixture()?;
|
||||||
|
|
||||||
let zdr_profile_overrides = ConfigOverrides {
|
let zdr_profile_overrides = ConfigOverrides {
|
||||||
config_profile: Some("zdr".to_string()),
|
config_profile: Some("zdr".to_string()),
|
||||||
cwd: Some(cwd.clone()),
|
cwd: Some(fixture.cwd()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let zdr_profile_config =
|
let zdr_profile_config = Config::load_from_base_config_with_overrides(
|
||||||
Config::load_from_base_config_with_overrides(cfg.clone(), zdr_profile_overrides, None)?;
|
fixture.cfg.clone(),
|
||||||
assert_eq!(
|
zdr_profile_overrides,
|
||||||
Config {
|
fixture.codex_home(),
|
||||||
model: "o3".to_string(),
|
)?;
|
||||||
model_provider_id: "openai".to_string(),
|
let expected_zdr_profile_config = Config {
|
||||||
model_provider: openai_provider.clone(),
|
model: "o3".to_string(),
|
||||||
approval_policy: AskForApproval::OnFailure,
|
model_provider_id: "openai".to_string(),
|
||||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
model_provider: fixture.openai_provider.clone(),
|
||||||
disable_response_storage: true,
|
approval_policy: AskForApproval::OnFailure,
|
||||||
instructions: None,
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||||
notify: None,
|
disable_response_storage: true,
|
||||||
cwd: cwd.clone(),
|
instructions: None,
|
||||||
mcp_servers: HashMap::new(),
|
notify: None,
|
||||||
model_providers: model_provider_map.clone(),
|
cwd: fixture.cwd(),
|
||||||
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
mcp_servers: HashMap::new(),
|
||||||
},
|
model_providers: fixture.model_provider_map.clone(),
|
||||||
zdr_profile_config
|
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
||||||
);
|
codex_home: fixture.codex_home(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(expected_zdr_profile_config, zdr_profile_config);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
//! Root of the `codex-core` library.
|
//! Root of the `codex-core` library.
|
||||||
|
|
||||||
// Prevent accidental direct writes to stdout/stderr in library code. All
|
// Prevent accidental direct writes to stdout/stderr in library code. All
|
||||||
// user‑visible output must go through the appropriate abstraction (e.g.,
|
// user-visible output must go through the appropriate abstraction (e.g.,
|
||||||
// the TUI or the tracing stack).
|
// the TUI or the tracing stack).
|
||||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||||
|
|
||||||
mod chat_completions;
|
mod chat_completions;
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
mod client_common;
|
mod client_common;
|
||||||
pub mod codex;
|
pub mod codex;
|
||||||
|
|||||||
@@ -137,7 +137,8 @@ mod tests {
|
|||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::Config;
|
use crate::config::ConfigOverrides;
|
||||||
|
use crate::config::ConfigToml;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -147,12 +148,19 @@ mod tests {
|
|||||||
/// value is cleared to mimic a scenario where no system instructions have
|
/// value is cleared to mimic a scenario where no system instructions have
|
||||||
/// been configured.
|
/// been configured.
|
||||||
fn make_config(root: &TempDir, limit: usize, instructions: Option<&str>) -> Config {
|
fn make_config(root: &TempDir, limit: usize, instructions: Option<&str>) -> Config {
|
||||||
let mut cfg = Config::load_default_config_for_test();
|
let codex_home = TempDir::new().unwrap();
|
||||||
cfg.cwd = root.path().to_path_buf();
|
let mut config = Config::load_from_base_config_with_overrides(
|
||||||
cfg.project_doc_max_bytes = limit;
|
ConfigToml::default(),
|
||||||
|
ConfigOverrides::default(),
|
||||||
|
codex_home.path().to_path_buf(),
|
||||||
|
)
|
||||||
|
.expect("defaults for test should always succeed");
|
||||||
|
|
||||||
cfg.instructions = instructions.map(ToOwned::to_owned);
|
config.cwd = root.path().to_path_buf();
|
||||||
cfg
|
config.project_doc_max_bytes = limit;
|
||||||
|
|
||||||
|
config.instructions = instructions.map(ToOwned::to_owned);
|
||||||
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AGENTS.md missing – should yield `None`.
|
/// AGENTS.md missing – should yield `None`.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use tokio::sync::mpsc::Sender;
|
|||||||
use tokio::sync::mpsc::{self};
|
use tokio::sync::mpsc::{self};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::config::codex_dir;
|
use crate::config::Config;
|
||||||
use crate::models::ResponseItem;
|
use crate::models::ResponseItem;
|
||||||
|
|
||||||
/// Folder inside `~/.codex` that holds saved rollouts.
|
/// Folder inside `~/.codex` that holds saved rollouts.
|
||||||
@@ -49,12 +49,16 @@ impl RolloutRecorder {
|
|||||||
/// Attempt to create a new [`RolloutRecorder`]. If the sessions directory
|
/// Attempt to create a new [`RolloutRecorder`]. If the sessions directory
|
||||||
/// cannot be created or the rollout file cannot be opened we return the
|
/// cannot be created or the rollout file cannot be opened we return the
|
||||||
/// error so the caller can decide whether to disable persistence.
|
/// error so the caller can decide whether to disable persistence.
|
||||||
pub async fn new(uuid: Uuid, instructions: Option<String>) -> std::io::Result<Self> {
|
pub async fn new(
|
||||||
|
config: &Config,
|
||||||
|
uuid: Uuid,
|
||||||
|
instructions: Option<String>,
|
||||||
|
) -> std::io::Result<Self> {
|
||||||
let LogFileInfo {
|
let LogFileInfo {
|
||||||
file,
|
file,
|
||||||
session_id,
|
session_id,
|
||||||
timestamp,
|
timestamp,
|
||||||
} = create_log_file(uuid)?;
|
} = create_log_file(config, uuid)?;
|
||||||
|
|
||||||
// Build the static session metadata JSON first.
|
// Build the static session metadata JSON first.
|
||||||
let timestamp_format: &[FormatItem] = format_description!(
|
let timestamp_format: &[FormatItem] = format_description!(
|
||||||
@@ -154,9 +158,9 @@ struct LogFileInfo {
|
|||||||
timestamp: OffsetDateTime,
|
timestamp: OffsetDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_log_file(session_id: Uuid) -> std::io::Result<LogFileInfo> {
|
fn create_log_file(config: &Config, session_id: Uuid) -> std::io::Result<LogFileInfo> {
|
||||||
// Resolve ~/.codex/sessions and create it if missing.
|
// Resolve ~/.codex/sessions and create it if missing.
|
||||||
let mut dir = codex_dir()?;
|
let mut dir = config.codex_home.clone();
|
||||||
dir.push(SESSIONS_SUBDIR);
|
dir.push(SESSIONS_SUBDIR);
|
||||||
fs::create_dir_all(&dir)?;
|
fs::create_dir_all(&dir)?;
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,15 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use codex_core::Codex;
|
use codex_core::Codex;
|
||||||
use codex_core::config::Config;
|
|
||||||
use codex_core::error::CodexErr;
|
use codex_core::error::CodexErr;
|
||||||
use codex_core::protocol::AgentMessageEvent;
|
use codex_core::protocol::AgentMessageEvent;
|
||||||
use codex_core::protocol::ErrorEvent;
|
use codex_core::protocol::ErrorEvent;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
|
mod test_support;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use test_support::load_default_config_for_test;
|
||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
|
||||||
@@ -57,7 +59,8 @@ async fn spawn_codex() -> Result<Codex, CodexErr> {
|
|||||||
std::env::set_var("OPENAI_STREAM_MAX_RETRIES", "2");
|
std::env::set_var("OPENAI_STREAM_MAX_RETRIES", "2");
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = Config::load_default_config_for_test();
|
let codex_home = TempDir::new().unwrap();
|
||||||
|
let config = load_default_config_for_test(&codex_home);
|
||||||
let (agent, _init_id) = Codex::spawn(config, std::sync::Arc::new(Notify::new())).await?;
|
let (agent, _init_id) = Codex::spawn(config, std::sync::Arc::new(Notify::new())).await?;
|
||||||
|
|
||||||
Ok(agent)
|
Ok(agent)
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use codex_core::Codex;
|
use codex_core::Codex;
|
||||||
use codex_core::ModelProviderInfo;
|
use codex_core::ModelProviderInfo;
|
||||||
use codex_core::config::Config;
|
|
||||||
use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||||
use codex_core::protocol::ErrorEvent;
|
use codex_core::protocol::ErrorEvent;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
|
mod test_support;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use test_support::load_default_config_for_test;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use wiremock::Match;
|
use wiremock::Match;
|
||||||
use wiremock::Mock;
|
use wiremock::Mock;
|
||||||
@@ -108,7 +110,8 @@ async fn keeps_previous_response_id_between_tasks() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Init session
|
// Init session
|
||||||
let mut config = Config::load_default_config_for_test();
|
let codex_home = TempDir::new().unwrap();
|
||||||
|
let mut config = load_default_config_for_test(&codex_home);
|
||||||
config.model_provider = model_provider;
|
config.model_provider = model_provider;
|
||||||
let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new());
|
let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new());
|
||||||
let (codex, _init_id) = Codex::spawn(config, ctrl_c.clone()).await.unwrap();
|
let (codex, _init_id) = Codex::spawn(config, ctrl_c.clone()).await.unwrap();
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use codex_core::Codex;
|
use codex_core::Codex;
|
||||||
use codex_core::ModelProviderInfo;
|
use codex_core::ModelProviderInfo;
|
||||||
use codex_core::config::Config;
|
|
||||||
use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
|
mod test_support;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use test_support::load_default_config_for_test;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use wiremock::Mock;
|
use wiremock::Mock;
|
||||||
use wiremock::MockServer;
|
use wiremock::MockServer;
|
||||||
@@ -96,7 +98,8 @@ async fn retries_on_early_close() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new());
|
let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new());
|
||||||
let mut config = Config::load_default_config_for_test();
|
let codex_home = TempDir::new().unwrap();
|
||||||
|
let mut config = load_default_config_for_test(&codex_home);
|
||||||
config.model_provider = model_provider;
|
config.model_provider = model_provider;
|
||||||
let (codex, _init_id) = Codex::spawn(config, ctrl_c).await.unwrap();
|
let (codex, _init_id) = Codex::spawn(config, ctrl_c).await.unwrap();
|
||||||
|
|
||||||
|
|||||||
23
codex-rs/core/tests/test_support.rs
Normal file
23
codex-rs/core/tests/test_support.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#![allow(clippy::expect_used)]
|
||||||
|
|
||||||
|
// Helpers shared by the integration tests. These are located inside the
|
||||||
|
// `tests/` tree on purpose so they never become part of the public API surface
|
||||||
|
// of the `codex-core` crate.
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use codex_core::config::Config;
|
||||||
|
use codex_core::config::ConfigOverrides;
|
||||||
|
use codex_core::config::ConfigToml;
|
||||||
|
|
||||||
|
/// Returns a default `Config` whose on-disk state is confined to the provided
|
||||||
|
/// temporary directory. Using a per-test directory keeps tests hermetic and
|
||||||
|
/// avoids clobbering a developer’s real `~/.codex`.
|
||||||
|
pub fn load_default_config_for_test(codex_home: &TempDir) -> Config {
|
||||||
|
Config::load_from_base_config_with_overrides(
|
||||||
|
ConfigToml::default(),
|
||||||
|
ConfigOverrides::default(),
|
||||||
|
codex_home.path().to_path_buf(),
|
||||||
|
)
|
||||||
|
.expect("defaults for test should always succeed")
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ pub fn run_main(cli: Cli) -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let log_dir = codex_core::config::log_dir()?;
|
let log_dir = codex_core::config::log_dir(&config)?;
|
||||||
std::fs::create_dir_all(&log_dir)?;
|
std::fs::create_dir_all(&log_dir)?;
|
||||||
// Open (or create) your log file, appending to it.
|
// Open (or create) your log file, appending to it.
|
||||||
let mut log_file_opts = OpenOptions::new();
|
let mut log_file_opts = OpenOptions::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user