mirror of
https://github.com/openai/codex.git
synced 2026-05-04 21:32:21 +03:00
fix: move inline codex-rs/core unit tests into sibling files (#14444)
## Why PR #13783 moved the `codex.rs` unit tests into `codex_tests.rs`. This applies the same extraction pattern across the rest of `codex-rs/core` so the production modules stay focused on runtime code instead of large inline test blocks. Keeping the tests in sibling files also makes follow-up edits easier to review because product changes no longer have to share a file with hundreds or thousands of lines of test scaffolding. ## What changed - replaced each inline `mod tests { ... }` in `codex-rs/core/src/**` with a path-based module declaration - moved each extracted unit test module into a sibling `*_tests.rs` file, using `mod_tests.rs` for `mod.rs` modules - preserved the existing `cfg(...)` guards and module-local structure so the refactor remains structural rather than behavioral ## Testing - `cargo test -p codex-core --lib` (`1653 passed; 0 failed; 5 ignored`) - `just fix -p codex-core` - `cargo fmt --check` - `cargo shear`
This commit is contained in:
File diff suppressed because it is too large
Load Diff
1095
codex-rs/core/src/agent/control_tests.rs
Normal file
1095
codex-rs/core/src/agent/control_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -222,249 +222,5 @@ impl Drop for SpawnReservation {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn format_agent_nickname_adds_ordinals_after_reset() {
|
||||
assert_eq!(format_agent_nickname("Plato", 0), "Plato");
|
||||
assert_eq!(format_agent_nickname("Plato", 1), "Plato the 2nd");
|
||||
assert_eq!(format_agent_nickname("Plato", 2), "Plato the 3rd");
|
||||
assert_eq!(format_agent_nickname("Plato", 10), "Plato the 11th");
|
||||
assert_eq!(format_agent_nickname("Plato", 20), "Plato the 21st");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_depth_defaults_to_zero_for_root_sources() {
|
||||
assert_eq!(session_depth(&SessionSource::Cli), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_spawn_depth_increments_and_enforces_limit() {
|
||||
let session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: ThreadId::new(),
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
let child_depth = next_thread_spawn_depth(&session_source);
|
||||
assert_eq!(child_depth, 2);
|
||||
assert!(exceeds_thread_spawn_depth_limit(child_depth, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_thread_spawn_subagents_default_to_depth_zero() {
|
||||
let session_source = SessionSource::SubAgent(SubAgentSource::Review);
|
||||
assert_eq!(session_depth(&session_source), 0);
|
||||
assert_eq!(next_thread_spawn_depth(&session_source), 1);
|
||||
assert!(!exceeds_thread_spawn_depth_limit(1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reservation_drop_releases_slot() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
drop(reservation);
|
||||
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("slot released");
|
||||
drop(reservation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_holds_slot_until_release() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
let thread_id = ThreadId::new();
|
||||
reservation.commit(thread_id);
|
||||
|
||||
let err = match guards.reserve_spawn_slot(Some(1)) {
|
||||
Ok(_) => panic!("limit should be enforced"),
|
||||
Err(err) => err,
|
||||
};
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
|
||||
guards.release_spawned_thread(thread_id);
|
||||
let reservation = guards
|
||||
.reserve_spawn_slot(Some(1))
|
||||
.expect("slot released after thread removal");
|
||||
drop(reservation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_ignores_unknown_thread_id() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
let thread_id = ThreadId::new();
|
||||
reservation.commit(thread_id);
|
||||
|
||||
guards.release_spawned_thread(ThreadId::new());
|
||||
|
||||
let err = match guards.reserve_spawn_slot(Some(1)) {
|
||||
Ok(_) => panic!("limit should still be enforced"),
|
||||
Err(err) => err,
|
||||
};
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
|
||||
guards.release_spawned_thread(thread_id);
|
||||
let reservation = guards
|
||||
.reserve_spawn_slot(Some(1))
|
||||
.expect("slot released after real thread removal");
|
||||
drop(reservation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_is_idempotent_for_registered_threads() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
let first_id = ThreadId::new();
|
||||
reservation.commit(first_id);
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("slot reused");
|
||||
let second_id = ThreadId::new();
|
||||
reservation.commit(second_id);
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let err = match guards.reserve_spawn_slot(Some(1)) {
|
||||
Ok(_) => panic!("limit should still be enforced"),
|
||||
Err(err) => err,
|
||||
};
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
|
||||
guards.release_spawned_thread(second_id);
|
||||
let reservation = guards
|
||||
.reserve_spawn_slot(Some(1))
|
||||
.expect("slot released after second thread removal");
|
||||
drop(reservation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_spawn_keeps_nickname_marked_used() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
let agent_nickname = reservation
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve agent name");
|
||||
assert_eq!(agent_nickname, "alpha");
|
||||
drop(reservation);
|
||||
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
let agent_nickname = reservation
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("unused name should still be preferred");
|
||||
assert_eq!(agent_nickname, "beta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_nickname_resets_used_pool_when_exhausted() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
assert_eq!(first_name, "alpha");
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("name should be reused after pool reset");
|
||||
assert_eq!(second_name, "alpha the 2nd");
|
||||
let active_agents = guards
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn released_nickname_stays_used_until_pool_reset() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
assert_eq!(first_name, "alpha");
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("released name should still be marked used");
|
||||
assert_eq!(second_name, "beta");
|
||||
let second_id = ThreadId::new();
|
||||
second.commit(second_id);
|
||||
guards.release_spawned_thread(second_id);
|
||||
|
||||
let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot");
|
||||
let third_name = third
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("pool reset should permit a duplicate");
|
||||
let expected_names =
|
||||
HashSet::from(["alpha the 2nd".to_string(), "beta the 2nd".to_string()]);
|
||||
assert!(expected_names.contains(&third_name));
|
||||
let active_agents = guards
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_resets_advance_the_ordinal_suffix() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["Plato"])
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
assert_eq!(first_name, "Plato");
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["Plato"])
|
||||
.expect("reserve second agent name");
|
||||
let second_id = ThreadId::new();
|
||||
second.commit(second_id);
|
||||
assert_eq!(second_name, "Plato the 2nd");
|
||||
guards.release_spawned_thread(second_id);
|
||||
|
||||
let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot");
|
||||
let third_name = third
|
||||
.reserve_agent_nickname(&["Plato"])
|
||||
.expect("reserve third agent name");
|
||||
assert_eq!(third_name, "Plato the 3rd");
|
||||
let active_agents = guards
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 2);
|
||||
}
|
||||
}
|
||||
#[path = "guards_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
243
codex-rs/core/src/agent/guards_tests.rs
Normal file
243
codex-rs/core/src/agent/guards_tests.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn format_agent_nickname_adds_ordinals_after_reset() {
|
||||
assert_eq!(format_agent_nickname("Plato", 0), "Plato");
|
||||
assert_eq!(format_agent_nickname("Plato", 1), "Plato the 2nd");
|
||||
assert_eq!(format_agent_nickname("Plato", 2), "Plato the 3rd");
|
||||
assert_eq!(format_agent_nickname("Plato", 10), "Plato the 11th");
|
||||
assert_eq!(format_agent_nickname("Plato", 20), "Plato the 21st");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_depth_defaults_to_zero_for_root_sources() {
|
||||
assert_eq!(session_depth(&SessionSource::Cli), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_spawn_depth_increments_and_enforces_limit() {
|
||||
let session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: ThreadId::new(),
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
let child_depth = next_thread_spawn_depth(&session_source);
|
||||
assert_eq!(child_depth, 2);
|
||||
assert!(exceeds_thread_spawn_depth_limit(child_depth, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_thread_spawn_subagents_default_to_depth_zero() {
|
||||
let session_source = SessionSource::SubAgent(SubAgentSource::Review);
|
||||
assert_eq!(session_depth(&session_source), 0);
|
||||
assert_eq!(next_thread_spawn_depth(&session_source), 1);
|
||||
assert!(!exceeds_thread_spawn_depth_limit(1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reservation_drop_releases_slot() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
drop(reservation);
|
||||
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("slot released");
|
||||
drop(reservation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_holds_slot_until_release() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
let thread_id = ThreadId::new();
|
||||
reservation.commit(thread_id);
|
||||
|
||||
let err = match guards.reserve_spawn_slot(Some(1)) {
|
||||
Ok(_) => panic!("limit should be enforced"),
|
||||
Err(err) => err,
|
||||
};
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
|
||||
guards.release_spawned_thread(thread_id);
|
||||
let reservation = guards
|
||||
.reserve_spawn_slot(Some(1))
|
||||
.expect("slot released after thread removal");
|
||||
drop(reservation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_ignores_unknown_thread_id() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
let thread_id = ThreadId::new();
|
||||
reservation.commit(thread_id);
|
||||
|
||||
guards.release_spawned_thread(ThreadId::new());
|
||||
|
||||
let err = match guards.reserve_spawn_slot(Some(1)) {
|
||||
Ok(_) => panic!("limit should still be enforced"),
|
||||
Err(err) => err,
|
||||
};
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
|
||||
guards.release_spawned_thread(thread_id);
|
||||
let reservation = guards
|
||||
.reserve_spawn_slot(Some(1))
|
||||
.expect("slot released after real thread removal");
|
||||
drop(reservation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_is_idempotent_for_registered_threads() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
let first_id = ThreadId::new();
|
||||
reservation.commit(first_id);
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("slot reused");
|
||||
let second_id = ThreadId::new();
|
||||
reservation.commit(second_id);
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let err = match guards.reserve_spawn_slot(Some(1)) {
|
||||
Ok(_) => panic!("limit should still be enforced"),
|
||||
Err(err) => err,
|
||||
};
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
|
||||
guards.release_spawned_thread(second_id);
|
||||
let reservation = guards
|
||||
.reserve_spawn_slot(Some(1))
|
||||
.expect("slot released after second thread removal");
|
||||
drop(reservation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_spawn_keeps_nickname_marked_used() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
let agent_nickname = reservation
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve agent name");
|
||||
assert_eq!(agent_nickname, "alpha");
|
||||
drop(reservation);
|
||||
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
let agent_nickname = reservation
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("unused name should still be preferred");
|
||||
assert_eq!(agent_nickname, "beta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_nickname_resets_used_pool_when_exhausted() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
assert_eq!(first_name, "alpha");
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("name should be reused after pool reset");
|
||||
assert_eq!(second_name, "alpha the 2nd");
|
||||
let active_agents = guards
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn released_nickname_stays_used_until_pool_reset() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
assert_eq!(first_name, "alpha");
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("released name should still be marked used");
|
||||
assert_eq!(second_name, "beta");
|
||||
let second_id = ThreadId::new();
|
||||
second.commit(second_id);
|
||||
guards.release_spawned_thread(second_id);
|
||||
|
||||
let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot");
|
||||
let third_name = third
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("pool reset should permit a duplicate");
|
||||
let expected_names = HashSet::from(["alpha the 2nd".to_string(), "beta the 2nd".to_string()]);
|
||||
assert!(expected_names.contains(&third_name));
|
||||
let active_agents = guards
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_resets_advance_the_ordinal_suffix() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["Plato"])
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
assert_eq!(first_name, "Plato");
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["Plato"])
|
||||
.expect("reserve second agent name");
|
||||
let second_id = ThreadId::new();
|
||||
second.commit(second_id);
|
||||
assert_eq!(second_name, "Plato the 2nd");
|
||||
guards.release_spawned_thread(second_id);
|
||||
|
||||
let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot");
|
||||
let third_name = third
|
||||
.reserve_agent_nickname(&["Plato"])
|
||||
.expect("reserve third agent name");
|
||||
assert_eq!(third_name, "Plato the 3rd");
|
||||
let active_agents = guards
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 2);
|
||||
}
|
||||
@@ -309,685 +309,5 @@ Rules:
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::skills::SkillsManager;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn test_config_with_cli_overrides(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
) -> (TempDir, Config) {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
let home_path = home.path().to_path_buf();
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(home_path.clone())
|
||||
.cli_overrides(cli_overrides)
|
||||
.fallback_cwd(Some(home_path))
|
||||
.build()
|
||||
.await
|
||||
.expect("load test config");
|
||||
(home, config)
|
||||
}
|
||||
|
||||
async fn write_role_config(home: &TempDir, name: &str, contents: &str) -> PathBuf {
|
||||
let role_path = home.path().join(name);
|
||||
tokio::fs::write(&role_path, contents)
|
||||
.await
|
||||
.expect("write role config");
|
||||
role_path
|
||||
}
|
||||
|
||||
fn session_flags_layer_count(config: &Config) -> usize {
|
||||
config
|
||||
.config_layer_stack
|
||||
.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, true)
|
||||
.into_iter()
|
||||
.filter(|layer| layer.name == ConfigLayerSource::SessionFlags)
|
||||
.count()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_defaults_to_default_and_leaves_config_unchanged() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let before = config.clone();
|
||||
|
||||
apply_role_to_config(&mut config, None)
|
||||
.await
|
||||
.expect("default role should apply");
|
||||
|
||||
assert_eq!(before, config);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_returns_error_for_unknown_role() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
|
||||
let err = apply_role_to_config(&mut config, Some("missing-role"))
|
||||
.await
|
||||
.expect_err("unknown role should fail");
|
||||
|
||||
assert_eq!(err, "unknown agent_type 'missing-role'");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "No role requiring it for now"]
|
||||
async fn apply_explorer_role_sets_model_and_adds_session_flags_layer() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let before_layers = session_flags_layer_count(&config);
|
||||
|
||||
apply_role_to_config(&mut config, Some("explorer"))
|
||||
.await
|
||||
.expect("explorer role should apply");
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("gpt-5.1-codex-mini"));
|
||||
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::Medium));
|
||||
assert_eq!(session_flags_layer_count(&config), before_layers + 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_returns_unavailable_for_missing_user_role_file() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(PathBuf::from("/path/does/not/exist.toml")),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
let err = apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect_err("missing role file should fail");
|
||||
|
||||
assert_eq!(err, AGENT_TYPE_UNAVAILABLE_ERROR);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_returns_unavailable_for_invalid_user_role_toml() {
|
||||
let (home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let role_path = write_role_config(&home, "invalid-role.toml", "model = [").await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
let err = apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect_err("invalid role file should fail");
|
||||
|
||||
assert_eq!(err, AGENT_TYPE_UNAVAILABLE_ERROR);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_ignores_agent_metadata_fields_in_user_role_file() {
|
||||
let (home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"metadata-role.toml",
|
||||
r#"
|
||||
name = "archivist"
|
||||
description = "Role metadata"
|
||||
nickname_candidates = ["Hypatia"]
|
||||
developer_instructions = "Stay focused"
|
||||
model = "role-model"
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("role-model"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_preserves_unspecified_keys() {
|
||||
let (home, mut config) = test_config_with_cli_overrides(vec![(
|
||||
"model".to_string(),
|
||||
TomlValue::String("base-model".to_string()),
|
||||
)])
|
||||
.await;
|
||||
config.codex_linux_sandbox_exe = Some(PathBuf::from("/tmp/codex-linux-sandbox"));
|
||||
config.main_execve_wrapper_exe = Some(PathBuf::from("/tmp/codex-execve-wrapper"));
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"effort-only.toml",
|
||||
"developer_instructions = \"Stay focused\"\nmodel_reasoning_effort = \"high\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("base-model"));
|
||||
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High));
|
||||
assert_eq!(
|
||||
config.codex_linux_sandbox_exe,
|
||||
Some(PathBuf::from("/tmp/codex-linux-sandbox"))
|
||||
);
|
||||
assert_eq!(
|
||||
config.main_execve_wrapper_exe,
|
||||
Some(PathBuf::from("/tmp/codex-execve-wrapper"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_preserves_active_profile_and_model_provider() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.test-provider]
|
||||
name = "Test Provider"
|
||||
base_url = "https://example.com/v1"
|
||||
env_key = "TEST_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.test-profile]
|
||||
model_provider = "test-provider"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("test-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"empty-role.toml",
|
||||
"developer_instructions = \"Stay focused\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile.as_deref(), Some("test-profile"));
|
||||
assert_eq!(config.model_provider_id, "test-provider");
|
||||
assert_eq!(config.model_provider.name, "Test Provider");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_uses_role_profile_instead_of_current_profile() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.base-provider]
|
||||
name = "Base Provider"
|
||||
base_url = "https://base.example.com/v1"
|
||||
env_key = "BASE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[model_providers.role-provider]
|
||||
name = "Role Provider"
|
||||
base_url = "https://role.example.com/v1"
|
||||
env_key = "ROLE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "base-provider"
|
||||
|
||||
[profiles.role-profile]
|
||||
model_provider = "role-provider"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("base-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"profile-role.toml",
|
||||
"developer_instructions = \"Stay focused\"\nprofile = \"role-profile\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile.as_deref(), Some("role-profile"));
|
||||
assert_eq!(config.model_provider_id, "role-provider");
|
||||
assert_eq!(config.model_provider.name, "Role Provider");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_uses_role_model_provider_instead_of_current_profile_provider() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.base-provider]
|
||||
name = "Base Provider"
|
||||
base_url = "https://base.example.com/v1"
|
||||
env_key = "BASE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[model_providers.role-provider]
|
||||
name = "Role Provider"
|
||||
base_url = "https://role.example.com/v1"
|
||||
env_key = "ROLE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "base-provider"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("base-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"provider-role.toml",
|
||||
"developer_instructions = \"Stay focused\"\nmodel_provider = \"role-provider\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile, None);
|
||||
assert_eq!(config.model_provider_id, "role-provider");
|
||||
assert_eq!(config.model_provider.name, "Role Provider");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_uses_active_profile_model_provider_update() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.base-provider]
|
||||
name = "Base Provider"
|
||||
base_url = "https://base.example.com/v1"
|
||||
env_key = "BASE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[model_providers.role-provider]
|
||||
name = "Role Provider"
|
||||
base_url = "https://role.example.com/v1"
|
||||
env_key = "ROLE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "base-provider"
|
||||
model_reasoning_effort = "low"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("base-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"profile-edit-role.toml",
|
||||
r#"developer_instructions = "Stay focused"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "role-provider"
|
||||
model_reasoning_effort = "high"
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile.as_deref(), Some("base-profile"));
|
||||
assert_eq!(config.model_provider_id, "role-provider");
|
||||
assert_eq!(config.model_provider.name, "Role Provider");
|
||||
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(not(windows))]
|
||||
async fn apply_role_does_not_materialize_default_sandbox_workspace_write_fields() {
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
let (home, mut config) = test_config_with_cli_overrides(vec![
|
||||
(
|
||||
"sandbox_mode".to_string(),
|
||||
TomlValue::String("workspace-write".to_string()),
|
||||
),
|
||||
(
|
||||
"sandbox_workspace_write.network_access".to_string(),
|
||||
TomlValue::Boolean(true),
|
||||
),
|
||||
])
|
||||
.await;
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"sandbox-role.toml",
|
||||
r#"developer_instructions = "Stay focused"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
writable_roots = ["./sandbox-root"]
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
let role_layer = config
|
||||
.config_layer_stack
|
||||
.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, true)
|
||||
.into_iter()
|
||||
.rfind(|layer| layer.name == ConfigLayerSource::SessionFlags)
|
||||
.expect("expected a session flags layer");
|
||||
let sandbox_workspace_write = role_layer
|
||||
.config
|
||||
.get("sandbox_workspace_write")
|
||||
.and_then(TomlValue::as_table)
|
||||
.expect("role layer should include sandbox_workspace_write");
|
||||
assert_eq!(
|
||||
sandbox_workspace_write.contains_key("network_access"),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
sandbox_workspace_write.contains_key("exclude_tmpdir_env_var"),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
sandbox_workspace_write.contains_key("exclude_slash_tmp"),
|
||||
false
|
||||
);
|
||||
|
||||
match &*config.permissions.sandbox_policy {
|
||||
SandboxPolicy::WorkspaceWrite { network_access, .. } => {
|
||||
assert_eq!(*network_access, true);
|
||||
}
|
||||
other => panic!("expected workspace-write sandbox policy, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_takes_precedence_over_existing_session_flags_for_same_key() {
|
||||
let (home, mut config) = test_config_with_cli_overrides(vec![(
|
||||
"model".to_string(),
|
||||
TomlValue::String("cli-model".to_string()),
|
||||
)])
|
||||
.await;
|
||||
let before_layers = session_flags_layer_count(&config);
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"model-role.toml",
|
||||
"developer_instructions = \"Stay focused\"\nmodel = \"role-model\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("role-model"));
|
||||
assert_eq!(session_flags_layer_count(&config), before_layers + 1);
|
||||
}
|
||||
|
||||
#[cfg_attr(windows, ignore)]
|
||||
#[tokio::test]
|
||||
async fn apply_role_skills_config_disables_skill_for_spawned_agent() {
|
||||
let (home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let skill_dir = home.path().join("skills").join("demo");
|
||||
fs::create_dir_all(&skill_dir).expect("create skill dir");
|
||||
let skill_path = skill_dir.join("SKILL.md");
|
||||
fs::write(
|
||||
&skill_path,
|
||||
"---\nname: demo-skill\ndescription: demo description\n---\n\n# Body\n",
|
||||
)
|
||||
.expect("write skill");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"skills-role.toml",
|
||||
&format!(
|
||||
r#"developer_instructions = "Stay focused"
|
||||
|
||||
[[skills.config]]
|
||||
path = "{}"
|
||||
enabled = false
|
||||
"#,
|
||||
skill_path.display()
|
||||
),
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
let plugins_manager = Arc::new(PluginsManager::new(home.path().to_path_buf()));
|
||||
let skills_manager = SkillsManager::new(home.path().to_path_buf(), plugins_manager, true);
|
||||
let outcome = skills_manager.skills_for_config(&config);
|
||||
let skill = outcome
|
||||
.skills
|
||||
.iter()
|
||||
.find(|skill| skill.name == "demo-skill")
|
||||
.expect("demo skill should be discovered");
|
||||
|
||||
assert_eq!(outcome.is_skill_enabled(skill), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_tool_spec_build_deduplicates_user_defined_built_in_roles() {
|
||||
let user_defined_roles = BTreeMap::from([
|
||||
(
|
||||
"explorer".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("user override".to_string()),
|
||||
config_file: None,
|
||||
nickname_candidates: None,
|
||||
},
|
||||
),
|
||||
("researcher".to_string(), AgentRoleConfig::default()),
|
||||
]);
|
||||
|
||||
let spec = spawn_tool_spec::build(&user_defined_roles);
|
||||
|
||||
assert!(spec.contains("researcher: no description"));
|
||||
assert!(spec.contains("explorer: {\nuser override\n}"));
|
||||
assert!(spec.contains("default: {\nDefault agent.\n}"));
|
||||
assert!(!spec.contains("Explorers are fast and authoritative."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_tool_spec_lists_user_defined_roles_before_built_ins() {
|
||||
let user_defined_roles = BTreeMap::from([(
|
||||
"aaa".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("first".to_string()),
|
||||
config_file: None,
|
||||
nickname_candidates: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
let spec = spawn_tool_spec::build(&user_defined_roles);
|
||||
let user_index = spec.find("aaa: {\nfirst\n}").expect("find user role");
|
||||
let built_in_index = spec
|
||||
.find("default: {\nDefault agent.\n}")
|
||||
.expect("find built-in role");
|
||||
|
||||
assert!(user_index < built_in_index);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_tool_spec_marks_role_locked_model_and_reasoning_effort() {
|
||||
let tempdir = TempDir::new().expect("create temp dir");
|
||||
let role_path = tempdir.path().join("researcher.toml");
|
||||
fs::write(
|
||||
&role_path,
|
||||
"developer_instructions = \"Research carefully\"\nmodel = \"gpt-5\"\nmodel_reasoning_effort = \"high\"\n",
|
||||
)
|
||||
.expect("write role config");
|
||||
let user_defined_roles = BTreeMap::from([(
|
||||
"researcher".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("Research carefully.".to_string()),
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
let spec = spawn_tool_spec::build(&user_defined_roles);
|
||||
|
||||
assert!(spec.contains(
|
||||
"Research carefully.\n- This role's model is set to `gpt-5` and its reasoning effort is set to `high`. These settings cannot be changed."
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_tool_spec_marks_role_locked_reasoning_effort_only() {
|
||||
let tempdir = TempDir::new().expect("create temp dir");
|
||||
let role_path = tempdir.path().join("reviewer.toml");
|
||||
fs::write(
|
||||
&role_path,
|
||||
"developer_instructions = \"Review carefully\"\nmodel_reasoning_effort = \"medium\"\n",
|
||||
)
|
||||
.expect("write role config");
|
||||
let user_defined_roles = BTreeMap::from([(
|
||||
"reviewer".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("Review carefully.".to_string()),
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
let spec = spawn_tool_spec::build(&user_defined_roles);
|
||||
|
||||
assert!(spec.contains(
|
||||
"Review carefully.\n- This role's reasoning effort is set to `medium` and cannot be changed."
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn built_in_config_file_contents_resolves_explorer_only() {
|
||||
assert_eq!(
|
||||
built_in::config_file_contents(Path::new("missing.toml")),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
#[path = "role_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
680
codex-rs/core/src/agent/role_tests.rs
Normal file
680
codex-rs/core/src/agent/role_tests.rs
Normal file
@@ -0,0 +1,680 @@
|
||||
use super::*;
|
||||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::skills::SkillsManager;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn test_config_with_cli_overrides(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
) -> (TempDir, Config) {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
let home_path = home.path().to_path_buf();
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(home_path.clone())
|
||||
.cli_overrides(cli_overrides)
|
||||
.fallback_cwd(Some(home_path))
|
||||
.build()
|
||||
.await
|
||||
.expect("load test config");
|
||||
(home, config)
|
||||
}
|
||||
|
||||
async fn write_role_config(home: &TempDir, name: &str, contents: &str) -> PathBuf {
|
||||
let role_path = home.path().join(name);
|
||||
tokio::fs::write(&role_path, contents)
|
||||
.await
|
||||
.expect("write role config");
|
||||
role_path
|
||||
}
|
||||
|
||||
fn session_flags_layer_count(config: &Config) -> usize {
|
||||
config
|
||||
.config_layer_stack
|
||||
.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, true)
|
||||
.into_iter()
|
||||
.filter(|layer| layer.name == ConfigLayerSource::SessionFlags)
|
||||
.count()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_defaults_to_default_and_leaves_config_unchanged() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let before = config.clone();
|
||||
|
||||
apply_role_to_config(&mut config, None)
|
||||
.await
|
||||
.expect("default role should apply");
|
||||
|
||||
assert_eq!(before, config);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_returns_error_for_unknown_role() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
|
||||
let err = apply_role_to_config(&mut config, Some("missing-role"))
|
||||
.await
|
||||
.expect_err("unknown role should fail");
|
||||
|
||||
assert_eq!(err, "unknown agent_type 'missing-role'");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "No role requiring it for now"]
|
||||
async fn apply_explorer_role_sets_model_and_adds_session_flags_layer() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let before_layers = session_flags_layer_count(&config);
|
||||
|
||||
apply_role_to_config(&mut config, Some("explorer"))
|
||||
.await
|
||||
.expect("explorer role should apply");
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("gpt-5.1-codex-mini"));
|
||||
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::Medium));
|
||||
assert_eq!(session_flags_layer_count(&config), before_layers + 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_returns_unavailable_for_missing_user_role_file() {
|
||||
let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(PathBuf::from("/path/does/not/exist.toml")),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
let err = apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect_err("missing role file should fail");
|
||||
|
||||
assert_eq!(err, AGENT_TYPE_UNAVAILABLE_ERROR);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_returns_unavailable_for_invalid_user_role_toml() {
|
||||
let (home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let role_path = write_role_config(&home, "invalid-role.toml", "model = [").await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
let err = apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect_err("invalid role file should fail");
|
||||
|
||||
assert_eq!(err, AGENT_TYPE_UNAVAILABLE_ERROR);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_ignores_agent_metadata_fields_in_user_role_file() {
|
||||
let (home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"metadata-role.toml",
|
||||
r#"
|
||||
name = "archivist"
|
||||
description = "Role metadata"
|
||||
nickname_candidates = ["Hypatia"]
|
||||
developer_instructions = "Stay focused"
|
||||
model = "role-model"
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("role-model"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_preserves_unspecified_keys() {
|
||||
let (home, mut config) = test_config_with_cli_overrides(vec![(
|
||||
"model".to_string(),
|
||||
TomlValue::String("base-model".to_string()),
|
||||
)])
|
||||
.await;
|
||||
config.codex_linux_sandbox_exe = Some(PathBuf::from("/tmp/codex-linux-sandbox"));
|
||||
config.main_execve_wrapper_exe = Some(PathBuf::from("/tmp/codex-execve-wrapper"));
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"effort-only.toml",
|
||||
"developer_instructions = \"Stay focused\"\nmodel_reasoning_effort = \"high\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("base-model"));
|
||||
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High));
|
||||
assert_eq!(
|
||||
config.codex_linux_sandbox_exe,
|
||||
Some(PathBuf::from("/tmp/codex-linux-sandbox"))
|
||||
);
|
||||
assert_eq!(
|
||||
config.main_execve_wrapper_exe,
|
||||
Some(PathBuf::from("/tmp/codex-execve-wrapper"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_preserves_active_profile_and_model_provider() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.test-provider]
|
||||
name = "Test Provider"
|
||||
base_url = "https://example.com/v1"
|
||||
env_key = "TEST_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.test-profile]
|
||||
model_provider = "test-provider"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("test-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"empty-role.toml",
|
||||
"developer_instructions = \"Stay focused\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile.as_deref(), Some("test-profile"));
|
||||
assert_eq!(config.model_provider_id, "test-provider");
|
||||
assert_eq!(config.model_provider.name, "Test Provider");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_uses_role_profile_instead_of_current_profile() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.base-provider]
|
||||
name = "Base Provider"
|
||||
base_url = "https://base.example.com/v1"
|
||||
env_key = "BASE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[model_providers.role-provider]
|
||||
name = "Role Provider"
|
||||
base_url = "https://role.example.com/v1"
|
||||
env_key = "ROLE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "base-provider"
|
||||
|
||||
[profiles.role-profile]
|
||||
model_provider = "role-provider"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("base-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"profile-role.toml",
|
||||
"developer_instructions = \"Stay focused\"\nprofile = \"role-profile\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile.as_deref(), Some("role-profile"));
|
||||
assert_eq!(config.model_provider_id, "role-provider");
|
||||
assert_eq!(config.model_provider.name, "Role Provider");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_uses_role_model_provider_instead_of_current_profile_provider() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.base-provider]
|
||||
name = "Base Provider"
|
||||
base_url = "https://base.example.com/v1"
|
||||
env_key = "BASE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[model_providers.role-provider]
|
||||
name = "Role Provider"
|
||||
base_url = "https://role.example.com/v1"
|
||||
env_key = "ROLE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "base-provider"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("base-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"provider-role.toml",
|
||||
"developer_instructions = \"Stay focused\"\nmodel_provider = \"role-provider\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile, None);
|
||||
assert_eq!(config.model_provider_id, "role-provider");
|
||||
assert_eq!(config.model_provider.name, "Role Provider");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_uses_active_profile_model_provider_update() {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
tokio::fs::write(
|
||||
home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[model_providers.base-provider]
|
||||
name = "Base Provider"
|
||||
base_url = "https://base.example.com/v1"
|
||||
env_key = "BASE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[model_providers.role-provider]
|
||||
name = "Role Provider"
|
||||
base_url = "https://role.example.com/v1"
|
||||
env_key = "ROLE_PROVIDER_API_KEY"
|
||||
wire_api = "responses"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "base-provider"
|
||||
model_reasoning_effort = "low"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("write config.toml");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
config_profile: Some("base-profile".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"profile-edit-role.toml",
|
||||
r#"developer_instructions = "Stay focused"
|
||||
|
||||
[profiles.base-profile]
|
||||
model_provider = "role-provider"
|
||||
model_reasoning_effort = "high"
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.active_profile.as_deref(), Some("base-profile"));
|
||||
assert_eq!(config.model_provider_id, "role-provider");
|
||||
assert_eq!(config.model_provider.name, "Role Provider");
|
||||
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(not(windows))]
|
||||
async fn apply_role_does_not_materialize_default_sandbox_workspace_write_fields() {
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
let (home, mut config) = test_config_with_cli_overrides(vec![
|
||||
(
|
||||
"sandbox_mode".to_string(),
|
||||
TomlValue::String("workspace-write".to_string()),
|
||||
),
|
||||
(
|
||||
"sandbox_workspace_write.network_access".to_string(),
|
||||
TomlValue::Boolean(true),
|
||||
),
|
||||
])
|
||||
.await;
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"sandbox-role.toml",
|
||||
r#"developer_instructions = "Stay focused"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
writable_roots = ["./sandbox-root"]
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
let role_layer = config
|
||||
.config_layer_stack
|
||||
.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, true)
|
||||
.into_iter()
|
||||
.rfind(|layer| layer.name == ConfigLayerSource::SessionFlags)
|
||||
.expect("expected a session flags layer");
|
||||
let sandbox_workspace_write = role_layer
|
||||
.config
|
||||
.get("sandbox_workspace_write")
|
||||
.and_then(TomlValue::as_table)
|
||||
.expect("role layer should include sandbox_workspace_write");
|
||||
assert_eq!(
|
||||
sandbox_workspace_write.contains_key("network_access"),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
sandbox_workspace_write.contains_key("exclude_tmpdir_env_var"),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
sandbox_workspace_write.contains_key("exclude_slash_tmp"),
|
||||
false
|
||||
);
|
||||
|
||||
match &*config.permissions.sandbox_policy {
|
||||
SandboxPolicy::WorkspaceWrite { network_access, .. } => {
|
||||
assert_eq!(*network_access, true);
|
||||
}
|
||||
other => panic!("expected workspace-write sandbox policy, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_role_takes_precedence_over_existing_session_flags_for_same_key() {
|
||||
let (home, mut config) = test_config_with_cli_overrides(vec![(
|
||||
"model".to_string(),
|
||||
TomlValue::String("cli-model".to_string()),
|
||||
)])
|
||||
.await;
|
||||
let before_layers = session_flags_layer_count(&config);
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"model-role.toml",
|
||||
"developer_instructions = \"Stay focused\"\nmodel = \"role-model\"",
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("role-model"));
|
||||
assert_eq!(session_flags_layer_count(&config), before_layers + 1);
|
||||
}
|
||||
|
||||
#[cfg_attr(windows, ignore)]
|
||||
#[tokio::test]
|
||||
async fn apply_role_skills_config_disables_skill_for_spawned_agent() {
|
||||
let (home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
|
||||
let skill_dir = home.path().join("skills").join("demo");
|
||||
fs::create_dir_all(&skill_dir).expect("create skill dir");
|
||||
let skill_path = skill_dir.join("SKILL.md");
|
||||
fs::write(
|
||||
&skill_path,
|
||||
"---\nname: demo-skill\ndescription: demo description\n---\n\n# Body\n",
|
||||
)
|
||||
.expect("write skill");
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"skills-role.toml",
|
||||
&format!(
|
||||
r#"developer_instructions = "Stay focused"
|
||||
|
||||
[[skills.config]]
|
||||
path = "{}"
|
||||
enabled = false
|
||||
"#,
|
||||
skill_path.display()
|
||||
),
|
||||
)
|
||||
.await;
|
||||
config.agent_roles.insert(
|
||||
"custom".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: None,
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
);
|
||||
|
||||
apply_role_to_config(&mut config, Some("custom"))
|
||||
.await
|
||||
.expect("custom role should apply");
|
||||
|
||||
let plugins_manager = Arc::new(PluginsManager::new(home.path().to_path_buf()));
|
||||
let skills_manager = SkillsManager::new(home.path().to_path_buf(), plugins_manager, true);
|
||||
let outcome = skills_manager.skills_for_config(&config);
|
||||
let skill = outcome
|
||||
.skills
|
||||
.iter()
|
||||
.find(|skill| skill.name == "demo-skill")
|
||||
.expect("demo skill should be discovered");
|
||||
|
||||
assert_eq!(outcome.is_skill_enabled(skill), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_tool_spec_build_deduplicates_user_defined_built_in_roles() {
|
||||
let user_defined_roles = BTreeMap::from([
|
||||
(
|
||||
"explorer".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("user override".to_string()),
|
||||
config_file: None,
|
||||
nickname_candidates: None,
|
||||
},
|
||||
),
|
||||
("researcher".to_string(), AgentRoleConfig::default()),
|
||||
]);
|
||||
|
||||
let spec = spawn_tool_spec::build(&user_defined_roles);
|
||||
|
||||
assert!(spec.contains("researcher: no description"));
|
||||
assert!(spec.contains("explorer: {\nuser override\n}"));
|
||||
assert!(spec.contains("default: {\nDefault agent.\n}"));
|
||||
assert!(!spec.contains("Explorers are fast and authoritative."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_tool_spec_lists_user_defined_roles_before_built_ins() {
|
||||
let user_defined_roles = BTreeMap::from([(
|
||||
"aaa".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("first".to_string()),
|
||||
config_file: None,
|
||||
nickname_candidates: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
let spec = spawn_tool_spec::build(&user_defined_roles);
|
||||
let user_index = spec.find("aaa: {\nfirst\n}").expect("find user role");
|
||||
let built_in_index = spec
|
||||
.find("default: {\nDefault agent.\n}")
|
||||
.expect("find built-in role");
|
||||
|
||||
assert!(user_index < built_in_index);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_tool_spec_marks_role_locked_model_and_reasoning_effort() {
|
||||
let tempdir = TempDir::new().expect("create temp dir");
|
||||
let role_path = tempdir.path().join("researcher.toml");
|
||||
fs::write(
|
||||
&role_path,
|
||||
"developer_instructions = \"Research carefully\"\nmodel = \"gpt-5\"\nmodel_reasoning_effort = \"high\"\n",
|
||||
)
|
||||
.expect("write role config");
|
||||
let user_defined_roles = BTreeMap::from([(
|
||||
"researcher".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("Research carefully.".to_string()),
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
let spec = spawn_tool_spec::build(&user_defined_roles);
|
||||
|
||||
assert!(spec.contains(
|
||||
"Research carefully.\n- This role's model is set to `gpt-5` and its reasoning effort is set to `high`. These settings cannot be changed."
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_tool_spec_marks_role_locked_reasoning_effort_only() {
|
||||
let tempdir = TempDir::new().expect("create temp dir");
|
||||
let role_path = tempdir.path().join("reviewer.toml");
|
||||
fs::write(
|
||||
&role_path,
|
||||
"developer_instructions = \"Review carefully\"\nmodel_reasoning_effort = \"medium\"\n",
|
||||
)
|
||||
.expect("write role config");
|
||||
let user_defined_roles = BTreeMap::from([(
|
||||
"reviewer".to_string(),
|
||||
AgentRoleConfig {
|
||||
description: Some("Review carefully.".to_string()),
|
||||
config_file: Some(role_path),
|
||||
nickname_candidates: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
let spec = spawn_tool_spec::build(&user_defined_roles);
|
||||
|
||||
assert!(spec.contains(
|
||||
"Review carefully.\n- This role's reasoning effort is set to `medium` and cannot be changed."
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn built_in_config_file_contents_resolves_explorer_only() {
|
||||
assert_eq!(
|
||||
built_in::config_file_contents(Path::new("missing.toml")),
|
||||
None
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user