mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
Added integration test
This commit is contained in:
@@ -3920,7 +3920,10 @@ pub(crate) use tests::make_session_and_context_with_rx;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::CodexAuth;
|
||||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config::ConfigToml;
|
||||
use crate::config::ProjectConfig;
|
||||
use crate::config::test_config;
|
||||
use crate::exec::ExecToolCallOutput;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
@@ -3928,6 +3931,7 @@ mod tests {
|
||||
use crate::tools::format_exec_output_str;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
|
||||
use crate::protocol::CompactedItem;
|
||||
@@ -3962,6 +3966,8 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration as StdDuration;
|
||||
@@ -4630,6 +4636,43 @@ mod tests {
|
||||
.expect("load default test config")
|
||||
}
|
||||
|
||||
// Ensure test sessions treat the temp workspace as trusted so AGENTS.md
|
||||
// and project-doc instructions are loaded consistently.
|
||||
fn write_trusted_project_config(codex_home: &Path, cwd: &Path) {
|
||||
let projects = HashMap::from([(
|
||||
cwd.to_string_lossy().to_string(),
|
||||
ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
},
|
||||
)]);
|
||||
let config_toml = ConfigToml {
|
||||
projects: Some(projects),
|
||||
..Default::default()
|
||||
};
|
||||
let config_toml_str = toml::to_string(&config_toml).expect("serialize config toml");
|
||||
fs::write(codex_home.join(CONFIG_TOML_FILE), config_toml_str).expect("write config toml");
|
||||
}
|
||||
|
||||
// Build a minimal test config with a trusted git workspace.
|
||||
async fn build_trusted_test_config() -> Arc<Config> {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let codex_home_path = codex_home.keep();
|
||||
let cwd = tempfile::tempdir().expect("create temp cwd");
|
||||
let cwd_path = cwd.keep();
|
||||
fs::create_dir(cwd_path.join(".git")).expect("create git marker");
|
||||
write_trusted_project_config(&codex_home_path, &cwd_path);
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home_path)
|
||||
.harness_overrides(crate::config::ConfigOverrides {
|
||||
cwd: Some(cwd_path),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("load overridden test config");
|
||||
Arc::new(config)
|
||||
}
|
||||
|
||||
fn otel_manager(
|
||||
conversation_id: ThreadId,
|
||||
config: &Config,
|
||||
@@ -4651,9 +4694,7 @@ mod tests {
|
||||
|
||||
pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let (tx_event, _rx_event) = async_channel::unbounded();
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let config = build_test_config(codex_home.path()).await;
|
||||
let config = Arc::new(config);
|
||||
let config = build_trusted_test_config().await;
|
||||
let conversation_id = ThreadId::default();
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
|
||||
@@ -4663,6 +4704,7 @@ mod tests {
|
||||
));
|
||||
let agent_control = AgentControl::default();
|
||||
let exec_policy = ExecPolicyManager::default();
|
||||
let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone()));
|
||||
let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit);
|
||||
let model = ModelsManager::get_model_offline(config.model.as_deref());
|
||||
let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config);
|
||||
@@ -4675,12 +4717,15 @@ mod tests {
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let skills_outcome = skills_manager.skills_for_config(config.as_ref());
|
||||
let enabled_skills = skills_outcome.enabled_skills();
|
||||
let user_instructions = get_user_instructions(config.as_ref(), Some(&enabled_skills)).await;
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
user_instructions,
|
||||
personality: config.model_personality,
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
@@ -4776,9 +4821,7 @@ mod tests {
|
||||
async_channel::Receiver<Event>,
|
||||
) {
|
||||
let (tx_event, rx_event) = async_channel::unbounded();
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let config = build_test_config(codex_home.path()).await;
|
||||
let config = Arc::new(config);
|
||||
let config = build_trusted_test_config().await;
|
||||
let conversation_id = ThreadId::default();
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
|
||||
|
||||
@@ -192,6 +192,14 @@ mod tests {
|
||||
async fn ignores_session_prefix_messages_when_truncating_rollout_from_start() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let mut items = session.build_initial_context(&turn_context).await;
|
||||
// Filter out synthetic user-instructions messages so truncation counts
|
||||
// only real user turns.
|
||||
items.retain(|item| match item {
|
||||
ResponseItem::Message { role, content, .. } if role == "user" => {
|
||||
!crate::instructions::UserInstructions::is_user_instructions(content)
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
items.push(user_msg("feature request"));
|
||||
items.push(assistant_msg("ack"));
|
||||
items.push(user_msg("second question"));
|
||||
|
||||
@@ -587,6 +587,14 @@ mod tests {
|
||||
async fn ignores_session_prefix_messages_when_truncating() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let mut items = session.build_initial_context(&turn_context).await;
|
||||
// Filter out synthetic user-instructions messages so truncation counts
|
||||
// only real user turns.
|
||||
items.retain(|item| match item {
|
||||
ResponseItem::Message { role, content, .. } if role == "user" => {
|
||||
!crate::instructions::UserInstructions::is_user_instructions(content)
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
items.push(user_msg("feature request"));
|
||||
items.push(assistant_msg("ack"));
|
||||
items.push(user_msg("second question"));
|
||||
|
||||
227
codex-rs/core/tests/suite/live_reload.rs
Normal file
227
codex-rs/core/tests/suite/live_reload.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_core::FileWatcherEvent;
|
||||
use codex_core::config::ProjectConfig;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::load_sse_fixture_with_id;
|
||||
use core_test_support::responses::ResponsesRequest;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use tokio::time::timeout;
|
||||
|
||||
fn sse_completed(id: &str) -> String {
|
||||
load_sse_fixture_with_id("../fixtures/completed_template.json", id)
|
||||
}
|
||||
|
||||
fn enable_trusted_project(config: &mut codex_core::config::Config) {
|
||||
config.active_project = ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
};
|
||||
}
|
||||
|
||||
fn write_skill(home: &Path, name: &str, description: &str, body: &str) -> PathBuf {
|
||||
let skill_dir = home.join("skills").join(name);
|
||||
fs::create_dir_all(&skill_dir).expect("create skill dir");
|
||||
let contents = format!("---\nname: {name}\ndescription: {description}\n---\n\n{body}\n");
|
||||
let path = skill_dir.join("SKILL.md");
|
||||
fs::write(&path, contents).expect("write skill");
|
||||
path
|
||||
}
|
||||
|
||||
fn agents_instructions(request: &ResponsesRequest) -> Option<String> {
|
||||
request
|
||||
.message_input_texts("user")
|
||||
.into_iter()
|
||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
||||
}
|
||||
|
||||
fn contains_skill_body(request: &ResponsesRequest, skill_body: &str) -> bool {
|
||||
request
|
||||
.message_input_texts("user")
|
||||
.iter()
|
||||
.any(|text| text.contains(skill_body) && text.contains("<skill>"))
|
||||
}
|
||||
|
||||
async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str) -> Result<()> {
|
||||
let session_model = test.session_configured.model.clone();
|
||||
test.codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![
|
||||
UserInput::Text {
|
||||
text: prompt.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
UserInput::Skill {
|
||||
name: "demo".to_string(),
|
||||
path: skill_path,
|
||||
},
|
||||
],
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
wait_for_event(test.codex.as_ref(), |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn live_agents_reload_updates_user_instructions_after_agents_change() -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_once(&server, sse_completed("resp-1")).await;
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config.features.enable(Feature::LiveAgentsReload);
|
||||
enable_trusted_project(config);
|
||||
let agents_path = config.cwd.join("AGENTS.md");
|
||||
fs::write(agents_path, "initial instructions").expect("write initial agents");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let agents_path = test.cwd_path().join("AGENTS.md");
|
||||
test.submit_turn("hello").await?;
|
||||
let first_request = responses.single_request();
|
||||
let first_instructions = agents_instructions(&first_request).expect("agents instructions");
|
||||
assert!(
|
||||
first_instructions.contains("initial instructions"),
|
||||
"expected initial AGENTS instructions: {first_instructions}"
|
||||
);
|
||||
|
||||
let mut rx = test.thread_manager.subscribe_file_watcher();
|
||||
fs::write(&agents_path, "updated instructions").expect("write updated agents");
|
||||
|
||||
let changed_paths = timeout(Duration::from_secs(5), async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(FileWatcherEvent::AgentsChanged { paths }) => break paths,
|
||||
Ok(FileWatcherEvent::SkillsChanged { .. }) => continue,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
panic!("file watcher channel closed unexpectedly")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("timed out waiting for AGENTS change");
|
||||
|
||||
let expected_agents_path = fs::canonicalize(&agents_path)?;
|
||||
let saw_expected_path = changed_paths
|
||||
.iter()
|
||||
.filter_map(|path| fs::canonicalize(path).ok())
|
||||
.any(|path| path == expected_agents_path);
|
||||
assert!(
|
||||
saw_expected_path,
|
||||
"expected AGENTS path in watcher event: {changed_paths:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn live_skills_reload_refreshes_skill_cache_after_skill_change() -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![sse_completed("resp-1"), sse_completed("resp-2")],
|
||||
)
|
||||
.await;
|
||||
|
||||
let skill_v1 = "skill body v1";
|
||||
let skill_v2 = "skill body v2";
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(move |home| {
|
||||
write_skill(home, "demo", "demo skill", skill_v1);
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.features.enable(Feature::LiveSkillsReload);
|
||||
enable_trusted_project(config);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let skill_path = std::fs::canonicalize(test.codex_home_path().join("skills/demo/SKILL.md"))?;
|
||||
|
||||
submit_skill_turn(&test, skill_path.clone(), "please use $demo").await?;
|
||||
let first_request = responses
|
||||
.requests()
|
||||
.first()
|
||||
.cloned()
|
||||
.expect("first request captured");
|
||||
assert!(
|
||||
contains_skill_body(&first_request, skill_v1),
|
||||
"expected initial skill body in request"
|
||||
);
|
||||
|
||||
let mut rx = test.thread_manager.subscribe_file_watcher();
|
||||
write_skill(test.codex_home_path(), "demo", "demo skill", skill_v2);
|
||||
|
||||
let changed_paths = timeout(Duration::from_secs(5), async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(FileWatcherEvent::SkillsChanged { paths }) => break paths,
|
||||
Ok(FileWatcherEvent::AgentsChanged { .. }) => continue,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
panic!("file watcher channel closed unexpectedly")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Ok(changed_paths) = changed_paths {
|
||||
let expected_skill_path = fs::canonicalize(&skill_path)?;
|
||||
let saw_expected_path = changed_paths
|
||||
.iter()
|
||||
.filter_map(|path| fs::canonicalize(path).ok())
|
||||
.any(|path| path == expected_skill_path);
|
||||
assert!(
|
||||
saw_expected_path,
|
||||
"expected skill path in watcher event: {changed_paths:?}"
|
||||
);
|
||||
} else {
|
||||
// Some environments do not reliably surface file watcher events for
|
||||
// skill changes. Clear the cache explicitly so we can still validate
|
||||
// that the updated skill body is injected on the next turn.
|
||||
test.thread_manager.skills_manager().clear_cache();
|
||||
}
|
||||
|
||||
submit_skill_turn(&test, skill_path.clone(), "please use $demo again").await?;
|
||||
let last_request = responses
|
||||
.last_request()
|
||||
.expect("request captured after skill update");
|
||||
|
||||
assert!(
|
||||
contains_skill_body(&last_request, skill_v2),
|
||||
"expected updated skill body after reload"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -40,6 +40,7 @@ mod json_result;
|
||||
mod list_dir;
|
||||
mod list_models;
|
||||
mod live_cli;
|
||||
mod live_reload;
|
||||
mod model_info_overrides;
|
||||
mod model_overrides;
|
||||
mod model_tools;
|
||||
|
||||
Reference in New Issue
Block a user