use anyhow::Result; use codex_exec_server::CreateDirectoryOptions; use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::create_directory_symlink; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::test_codex::TestCodexBuilder; use core_test_support::test_codex::test_codex; use pretty_assertions::assert_eq; use std::sync::Arc; use tempfile::TempDir; async fn agents_instructions(mut builder: TestCodexBuilder) -> Result { let server = start_mock_server().await; let resp_mock = mount_sse_once( &server, sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), ) .await; let test = builder.build_with_remote_env(&server).await?; test.submit_turn("hello").await?; let request = resp_mock.single_request(); request .message_input_texts("user") .into_iter() .find(|text| text.starts_with("# AGENTS.md instructions for ")) .ok_or_else(|| anyhow::anyhow!("instructions message not found")) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn agents_override_is_preferred_over_agents_md() -> Result<()> { let instructions = agents_instructions(test_codex().with_workspace_setup(|cwd, fs| async move { let agents_md = cwd.join("AGENTS.md"); let override_md = cwd.join("AGENTS.override.md"); fs.write_file(&agents_md, b"base doc".to_vec(), /*sandbox*/ None) .await?; fs.write_file( &override_md, b"override doc".to_vec(), /*sandbox*/ None, ) .await?; Ok::<(), anyhow::Error>(()) })) .await?; assert!( instructions.contains("override doc"), "expected AGENTS.override.md contents: {instructions}" ); assert!( !instructions.contains("base doc"), "expected AGENTS.md to be ignored when override exists: {instructions}" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn configured_fallback_is_used_when_agents_candidate_is_directory() -> Result<()> { let instructions = agents_instructions( test_codex() .with_config(|config| { config.project_doc_fallback_filenames = vec!["WORKFLOW.md".to_string()]; }) .with_workspace_setup(|cwd, fs| async move { let agents_dir = cwd.join("AGENTS.md"); let fallback = cwd.join("WORKFLOW.md"); fs.create_directory( &agents_dir, CreateDirectoryOptions { recursive: true }, /*sandbox*/ None, ) .await?; fs.write_file(&fallback, b"fallback doc".to_vec(), /*sandbox*/ None) .await?; Ok::<(), anyhow::Error>(()) }), ) .await?; assert!( instructions.contains("fallback doc"), "expected fallback doc contents: {instructions}" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn agents_docs_are_concatenated_from_project_root_to_cwd() -> Result<()> { let instructions = agents_instructions( test_codex() .with_config(|config| { config.cwd = config.cwd.join("nested/workspace"); }) .with_workspace_setup(|cwd, fs| async move { let nested = cwd.clone(); let root = nested .parent() .and_then(|parent| parent.parent()) .expect("nested workspace should have a project root ancestor"); let root_agents = root.join("AGENTS.md"); let git_marker = root.join(".git"); let nested_agents = nested.join("AGENTS.md"); fs.create_directory( &nested, CreateDirectoryOptions { recursive: true }, /*sandbox*/ None, ) .await?; fs.write_file(&root_agents, b"root doc".to_vec(), /*sandbox*/ None) .await?; fs.write_file( &git_marker, b"gitdir: /tmp/mock-git-dir\n".to_vec(), /*sandbox*/ None, ) .await?; fs.write_file(&nested_agents, b"child doc".to_vec(), /*sandbox*/ None) .await?; Ok::<(), anyhow::Error>(()) }), ) .await?; let root_pos = instructions .find("root doc") .expect("expected root doc in AGENTS instructions"); let child_pos = instructions .find("child doc") .expect("expected child doc in AGENTS instructions"); assert!( root_pos < child_pos, "expected root doc before child doc: {instructions}" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn symlinked_cwd_uses_logical_parent_for_agents_discovery() -> Result<()> { let server = start_mock_server().await; let resp_mock = mount_sse_once( &server, sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), ) .await; let mut builder = test_codex() .with_config(|config| { config.cwd = config.cwd.join("logical-repo/workspace"); }) .with_workspace_setup(|cwd, _fs| async move { // Construct two sibling repositories with the configured cwd as a // directory symlink from the logical repository into the physical // repository: // // test-root/ // |-- logical-repo/ // | |-- .git // | |-- AGENTS.md ("logical parent doc") // | `-- workspace ------------> physical-repo/workspace/ // `-- physical-repo/ // |-- .git // |-- AGENTS.md ("physical parent doc") // `-- workspace/ // `-- AGENTS.md ("workspace doc") // // Discovery should walk the lexical path through logical-repo, // while opening logical-repo/workspace/AGENTS.md still follows the // symlink into physical-repo/workspace. let logical_root = cwd.parent().expect("symlink should have a parent"); let test_root = logical_root .parent() .expect("logical repository should have a parent"); let physical_root = test_root.join("physical-repo"); let physical_workspace = physical_root.join("workspace"); std::fs::create_dir_all(logical_root.as_path())?; std::fs::write(logical_root.join(".git"), "")?; std::fs::write(logical_root.join("AGENTS.md"), "logical parent doc")?; std::fs::create_dir_all(physical_workspace.as_path())?; std::fs::write(physical_root.join(".git"), "")?; std::fs::write(physical_root.join("AGENTS.md"), "physical parent doc")?; std::fs::write(physical_workspace.join("AGENTS.md"), "workspace doc")?; create_directory_symlink(physical_workspace.as_path(), cwd.as_path()); Ok(()) }); let test = builder.build(&server).await?; let logical_root = test .config .cwd .parent() .expect("symlink should have a parent"); assert_eq!( test.codex.instruction_sources().await, vec![ logical_root.join("AGENTS.md"), test.config.cwd.join("AGENTS.md") ] ); test.submit_turn("hello").await?; let instructions = resp_mock .single_request() .message_input_texts("user") .into_iter() .find(|text| text.starts_with("# AGENTS.md instructions for ")) .expect("instructions message"); assert!(instructions.contains("logical parent doc")); assert!(instructions.contains("workspace doc")); assert!(!instructions.contains("physical parent doc")); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn selected_environment_sources_match_model_visible_instructions() -> Result<()> { let server = start_mock_server().await; let resp_mock = mount_sse_once( &server, sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), ) .await; let home = Arc::new(TempDir::new()?); let global_agents = home.path().join("AGENTS.md"); std::fs::write(&global_agents, "global doc")?; let mut builder = test_codex() .with_home(home) .with_workspace_setup(|cwd, fs| async move { fs.write_file( &cwd.join("AGENTS.md"), b"project doc".to_vec(), /*sandbox*/ None, ) .await?; Ok::<(), anyhow::Error>(()) }); let test = builder.build_with_remote_env(&server).await?; let project_agents = test.config.cwd.join("AGENTS.md"); let global_agents = AbsolutePathBuf::try_from(global_agents).expect("absolute path"); assert_eq!( test.codex.instruction_sources().await, vec![global_agents, project_agents] ); test.submit_turn("hello").await?; let instructions = resp_mock .single_request() .message_input_texts("user") .into_iter() .find(|text| text.starts_with("# AGENTS.md instructions for ")) .expect("instructions message"); assert!(instructions.contains("global doc\n\n--- project-doc ---\n\nproject doc")); Ok(()) }