Download codex-rs/core/src/agents_md_tests.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 60.2 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core/src/agents_md_tests.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/core/src/agents_md_tests.rs
-
curl -L -o agents_md_tests.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core/src/agents_md_tests.rs
60.2 kB
| use super::*; | |
| use crate::config::ConfigBuilder; | |
| use crate::config::PermissionProfileSnapshot; | |
| use crate::context::ContextualUserFragment; | |
| use crate::environment_selection::EnvironmentConfigOrigin; | |
| use crate::environment_selection::TurnEnvironmentSnapshot; | |
| use crate::environment_selection::TurnEnvironmentState; | |
| use crate::session::turn_context::TurnEnvironment; | |
| use codex_config::ConfigLayerEntry; | |
| use codex_config::ConfigLayerStack; | |
| use codex_config::ConfigRequirements; | |
| use codex_config::ConfigRequirementsToml; | |
| use codex_exec_server::CopyOptions; | |
| use codex_exec_server::CreateDirectoryOptions; | |
| use codex_exec_server::Environment; | |
| use codex_exec_server::ExecutorFileSystemFuture; | |
| use codex_exec_server::FileMetadata; | |
| use codex_exec_server::FileSystemReadStream; | |
| use codex_exec_server::FileSystemSandboxContext; | |
| use codex_exec_server::GetMetadataOptions; | |
| use codex_exec_server::LOCAL_FS; | |
| use codex_exec_server::ReadDirectoryEntry; | |
| use codex_exec_server::ReadFileOptions; | |
| use codex_exec_server::RemoveOptions; | |
| use codex_exec_server::WalkOptions; | |
| use codex_exec_server::WalkOutcome; | |
| use codex_exec_server::WriteFileOptions; | |
| use codex_extension_api::Instructions; | |
| use codex_features::Feature; | |
| use codex_protocol::config_types::WindowsSandboxLevel; | |
| use codex_protocol::models::PermissionProfile; | |
| use codex_protocol::protocol::EnvironmentConfig; | |
| use codex_protocol::protocol::EnvironmentConfigState; | |
| use codex_protocol::protocol::TurnEnvironmentSelection; | |
| use codex_utils_absolute_path::AbsolutePathBuf; | |
| use codex_utils_path_uri::PathUri; | |
| use core_test_support::PathBufExt; | |
| use core_test_support::TempDirExt; | |
| use core_test_support::create_directory_symlink; | |
| use pretty_assertions::assert_eq; | |
| use std::fs; | |
| use std::io; | |
| use std::ops::Deref; | |
| use std::ops::DerefMut; | |
| use std::path::PathBuf; | |
| use std::sync::Arc; | |
| use std::sync::Mutex; | |
| use tempfile::TempDir; | |
| use tokio::sync::Notify; | |
| use tokio::sync::Semaphore; | |
| enum InjectedFailure { | |
| MetadataNotFound, | |
| Metadata(io::ErrorKind), | |
| MetadataBlocked, | |
| MetadataBlockedByFilenamePrefix(&'static str), | |
| MetadataPending, | |
| Read(io::ErrorKind), | |
| } | |
| struct FailingFileSystem { | |
| path: AbsolutePathBuf, | |
| failure: InjectedFailure, | |
| metadata_calls: Arc<MetadataCallCounts>, | |
| } | |
| struct MetadataCallCounts { | |
| paths: Mutex<Vec<PathUri>>, | |
| started: Notify, | |
| release: Semaphore, | |
| } | |
| impl Default for MetadataCallCounts { | |
| fn default() -> Self { | |
| Self { | |
| paths: Mutex::new(Vec::new()), | |
| started: Notify::new(), | |
| release: Semaphore::new(0), | |
| } | |
| } | |
| } | |
| impl FailingFileSystem { | |
| async fn canonicalize( | |
| &self, | |
| _path: &PathUri, | |
| _sandbox: Option<&FileSystemSandboxContext>, | |
| ) -> io::Result<PathUri> { | |
| unreachable!("canonicalize should not be called") | |
| } | |
| async fn read_file( | |
| &self, | |
| path: &PathUri, | |
| options: ReadFileOptions, | |
| sandbox: Option<&FileSystemSandboxContext>, | |
| ) -> io::Result<Vec<u8>> { | |
| if path.to_abs_path()? == self.path | |
| && let InjectedFailure::Read(kind) = self.failure | |
| { | |
| return Err(io::Error::new(kind, "injected read failure")); | |
| } | |
| LOCAL_FS.read_file(path, options, sandbox).await | |
| } | |
| async fn write_file( | |
| &self, | |
| _path: &PathUri, | |
| _contents: Vec<u8>, | |
| _options: WriteFileOptions, | |
| _sandbox: Option<&FileSystemSandboxContext>, | |
| ) -> io::Result<()> { | |
| unreachable!("write_file should not be called") | |
| } | |
| async fn create_directory( | |
| &self, | |
| _path: &PathUri, | |
| _create_directory_options: CreateDirectoryOptions, | |
| _sandbox: Option<&FileSystemSandboxContext>, | |
| ) -> io::Result<()> { | |
| unreachable!("create_directory should not be called") | |
| } | |
| async fn get_metadata( | |
| &self, | |
| path: &PathUri, | |
| options: GetMetadataOptions, | |
| sandbox: Option<&FileSystemSandboxContext>, | |
| ) -> io::Result<FileMetadata> { | |
| self.metadata_calls | |
| .paths | |
| .lock() | |
| .expect("metadata paths lock") | |
| .push(path.clone()); | |
| self.metadata_calls.started.notify_one(); | |
| if matches!(self.failure, InjectedFailure::MetadataNotFound) { | |
| return Err(io::Error::from(io::ErrorKind::NotFound)); | |
| } | |
| let path_abs = path.to_abs_path()?; | |
| match self.failure { | |
| InjectedFailure::Metadata(kind) if path_abs == self.path => { | |
| Err(io::Error::new(kind, "injected metadata failure")) | |
| } | |
| InjectedFailure::MetadataBlocked if path_abs == self.path => { | |
| self.metadata_calls | |
| .release | |
| .acquire() | |
| .await | |
| .expect("metadata release semaphore") | |
| .forget(); | |
| LOCAL_FS.get_metadata(path, options, sandbox).await | |
| } | |
| InjectedFailure::MetadataBlockedByFilenamePrefix(prefix) | |
| if path_abs | |
| .file_name() | |
| .and_then(|name| name.to_str()) | |
| .is_some_and(|name| name.starts_with(prefix)) => | |
| { | |
| self.metadata_calls | |
| .release | |
| .acquire() | |
| .await | |
| .expect("metadata release semaphore") | |
| .forget(); | |
| LOCAL_FS.get_metadata(path, options, sandbox).await | |
| } | |
| InjectedFailure::MetadataPending if path_abs == self.path => { | |
| std::future::pending().await | |
| } | |
| InjectedFailure::MetadataNotFound | |
| | InjectedFailure::Metadata(_) | |
| | InjectedFailure::MetadataBlocked | |
| | InjectedFailure::MetadataBlockedByFilenamePrefix(_) | |
| | InjectedFailure::MetadataPending | |
| | InjectedFailure::Read(_) => LOCAL_FS.get_metadata(path, options, sandbox).await, | |
| } | |
| } | |
| async fn read_directory( | |
| &self, | |
| _path: &PathUri, | |
| _sandbox: Option<&FileSystemSandboxContext>, | |
| ) -> io::Result<Vec<ReadDirectoryEntry>> { | |
| unreachable!("read_directory should not be called") | |
| } | |
| async fn remove( | |
| &self, | |
| _path: &PathUri, | |
| _remove_options: RemoveOptions, | |
| _sandbox: Option<&FileSystemSandboxContext>, | |
| ) -> io::Result<()> { | |
| unreachable!("remove should not be called") | |
| } | |
| async fn copy( | |
| &self, | |
| _source_path: &PathUri, | |
| _destination_path: &PathUri, | |
| _copy_options: CopyOptions, | |
| _sandbox: Option<&FileSystemSandboxContext>, | |
| ) -> io::Result<()> { | |
| unreachable!("copy should not be called") | |
| } | |
| } | |
| impl ExecutorFileSystem for FailingFileSystem { | |
| fn canonicalize<'a>( | |
| &'a self, | |
| path: &'a PathUri, | |
| sandbox: Option<&'a FileSystemSandboxContext>, | |
| ) -> ExecutorFileSystemFuture<'a, PathUri> { | |
| Box::pin(FailingFileSystem::canonicalize(self, path, sandbox)) | |
| } | |
| fn read_file<'a>( | |
| &'a self, | |
| path: &'a PathUri, | |
| options: ReadFileOptions, | |
| sandbox: Option<&'a FileSystemSandboxContext>, | |
| ) -> ExecutorFileSystemFuture<'a, Vec<u8>> { | |
| Box::pin(FailingFileSystem::read_file(self, path, options, sandbox)) | |
| } | |
| fn read_file_stream<'a>( | |
| &'a self, | |
| _path: &'a PathUri, | |
| _sandbox: Option<&'a FileSystemSandboxContext>, | |
| ) -> ExecutorFileSystemFuture<'a, FileSystemReadStream> { | |
| Box::pin(async { | |
| Err(io::Error::new( | |
| io::ErrorKind::Unsupported, | |
| "failing filesystem does not support streaming reads", | |
| )) | |
| }) | |
| } | |
| fn write_file<'a>( | |
| &'a self, | |
| path: &'a PathUri, | |
| contents: Vec<u8>, | |
| options: WriteFileOptions, | |
| sandbox: Option<&'a FileSystemSandboxContext>, | |
| ) -> ExecutorFileSystemFuture<'a, ()> { | |
| Box::pin(FailingFileSystem::write_file( | |
| self, path, contents, options, sandbox, | |
| )) | |
| } | |
| fn create_directory<'a>( | |
| &'a self, | |
| path: &'a PathUri, | |
| options: CreateDirectoryOptions, | |
| sandbox: Option<&'a FileSystemSandboxContext>, | |
| ) -> ExecutorFileSystemFuture<'a, ()> { | |
| Box::pin(FailingFileSystem::create_directory( | |
| self, path, options, sandbox, | |
| )) | |
| } | |
| fn get_metadata<'a>( | |
| &'a self, | |
| path: &'a PathUri, | |
| options: GetMetadataOptions, | |
| sandbox: Option<&'a FileSystemSandboxContext>, | |
| ) -> ExecutorFileSystemFuture<'a, FileMetadata> { | |
| Box::pin(FailingFileSystem::get_metadata( | |
| self, path, options, sandbox, | |
| )) | |
| } | |
| fn read_directory<'a>( | |
| &'a self, | |
| path: &'a PathUri, | |
| sandbox: Option<&'a FileSystemSandboxContext>, | |
| ) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> { | |
| Box::pin(FailingFileSystem::read_directory(self, path, sandbox)) | |
| } | |
| fn walk<'a>( | |
| &'a self, | |
| _path: &'a PathUri, | |
| _options: WalkOptions, | |
| _sandbox: Option<&'a FileSystemSandboxContext>, | |
| ) -> ExecutorFileSystemFuture<'a, WalkOutcome> { | |
| Box::pin(async { unreachable!("walk should not be called") }) | |
| } | |
| fn remove<'a>( | |
| &'a self, | |
| path: &'a PathUri, | |
| options: RemoveOptions, | |
| sandbox: Option<&'a FileSystemSandboxContext>, | |
| ) -> ExecutorFileSystemFuture<'a, ()> { | |
| Box::pin(FailingFileSystem::remove(self, path, options, sandbox)) | |
| } | |
| fn copy<'a>( | |
| &'a self, | |
| source_path: &'a PathUri, | |
| destination_path: &'a PathUri, | |
| options: CopyOptions, | |
| sandbox: Option<&'a FileSystemSandboxContext>, | |
| ) -> ExecutorFileSystemFuture<'a, ()> { | |
| Box::pin(FailingFileSystem::copy( | |
| self, | |
| source_path, | |
| destination_path, | |
| options, | |
| sandbox, | |
| )) | |
| } | |
| } | |
| struct TestConfig { | |
| config: Config, | |
| user_instructions: Option<Instructions>, | |
| } | |
| impl Deref for TestConfig { | |
| type Target = Config; | |
| fn deref(&self) -> &Self::Target { | |
| &self.config | |
| } | |
| } | |
| impl DerefMut for TestConfig { | |
| fn deref_mut(&mut self) -> &mut Self::Target { | |
| &mut self.config | |
| } | |
| } | |
| async fn get_user_instructions(config: &TestConfig) -> Option<String> { | |
| load_agents_md(config).await.map(|loaded| loaded.text()) | |
| } | |
| async fn load_agents_md(config: &TestConfig) -> Option<LoadedAgentsMd> { | |
| let environments = resolved_local_environments([("local", config.config.cwd.clone())]); | |
| load_project_instructions( | |
| &config.config, | |
| config.user_instructions.clone(), | |
| &environments, | |
| ) | |
| .await | |
| .expect("project instructions should load") | |
| } | |
| async fn agents_md_paths(config: &TestConfig) -> std::io::Result<Vec<PathUri>> { | |
| super::agents_md_paths( | |
| &config.config, | |
| &PathUri::from_abs_path(&config.cwd), | |
| LOCAL_FS.as_ref(), | |
| /*sandbox*/ None, | |
| ) | |
| .await | |
| } | |
| fn resolved_local_environments<const N: usize>( | |
| environments: [(&str, AbsolutePathBuf); N], | |
| ) -> TurnEnvironmentSnapshot { | |
| TurnEnvironmentSnapshot { | |
| environments: environments | |
| .into_iter() | |
| .map(|(environment_id, cwd)| { | |
| TurnEnvironmentState::Ready(TurnEnvironment::new( | |
| TurnEnvironmentSelection { | |
| environment_id: environment_id.to_string(), | |
| cwd: PathUri::from_abs_path(&cwd), | |
| workspace_roots: Vec::new(), | |
| config: EnvironmentConfigState::Ready(EnvironmentConfig { | |
| allow_login_shell: true, | |
| workspace_roots: Vec::new(), | |
| windows_sandbox_level: WindowsSandboxLevel::Disabled, | |
| windows_sandbox_private_desktop: true, | |
| use_legacy_landlock: false, | |
| permission_profile: PermissionProfileSnapshot::legacy( | |
| PermissionProfile::read_only(), | |
| ), | |
| shell_environment_policy: Default::default(), | |
| exec_policy: None, | |
| mcp_policy: None, | |
| network_policy: None, | |
| selected_capability_roots: Vec::new(), | |
| }), | |
| }, | |
| EnvironmentConfigOrigin::Thread, | |
| Arc::new( | |
| Environment::create_for_tests(/*exec_server_url*/ None) | |
| .expect("local environment"), | |
| ), | |
| /*shell*/ None, | |
| )) | |
| }) | |
| .collect(), | |
| } | |
| } | |
| fn project_provenance(path: AbsolutePathBuf, cwd: AbsolutePathBuf) -> InstructionProvenance { | |
| InstructionProvenance::Project { | |
| source_path: PathUri::from_abs_path(&path), | |
| environment_id: "local".to_string(), | |
| cwd: PathUri::from_abs_path(&cwd), | |
| } | |
| } | |
| fn foreign_agents_md_uses_environment_native_paths() { | |
| let (cwd, rendered_cwd) = if cfg!(windows) { | |
| ( | |
| PathUri::parse("file:///codex%20runtime").expect("POSIX cwd URI"), | |
| "/codex runtime", | |
| ) | |
| } else { | |
| ( | |
| PathUri::parse("file:///C:/codex%20runtime").expect("Windows cwd URI"), | |
| r"C:\codex runtime", | |
| ) | |
| }; | |
| let source_path = cwd.join("AGENTS.md").expect("AGENTS.md URI"); | |
| let loaded = LoadedAgentsMd { | |
| user_instructions: None, | |
| thread_instructions: None, | |
| entries: vec![InstructionEntry { | |
| contents: "remote instructions".to_string(), | |
| provenance: InstructionProvenance::Project { | |
| source_path: source_path.clone(), | |
| environment_id: "remote".to_string(), | |
| cwd, | |
| }, | |
| }], | |
| }; | |
| assert_eq!( | |
| loaded.contextual_user_fragment().render(), | |
| format!( | |
| "# AGENTS.md instructions for {rendered_cwd} | |
| <INSTRUCTIONS> | |
| remote instructions | |
| </INSTRUCTIONS>" | |
| ) | |
| ); | |
| assert_eq!(loaded.sources().collect::<Vec<_>>(), vec![source_path]); | |
| } | |
| fn multi_environment_agents_md_renders_mixed_path_conventions() { | |
| let posix_cwd = PathUri::parse("file:///srv/project").expect("POSIX cwd URI"); | |
| let windows_cwd = PathUri::parse("file:///C:/workspace").expect("Windows cwd URI"); | |
| let posix_source = posix_cwd.join("AGENTS.md").expect("POSIX AGENTS.md URI"); | |
| let windows_source = windows_cwd | |
| .join("AGENTS.md") | |
| .expect("Windows AGENTS.md URI"); | |
| let loaded = LoadedAgentsMd { | |
| user_instructions: None, | |
| thread_instructions: None, | |
| entries: vec![ | |
| InstructionEntry { | |
| contents: "POSIX instructions".to_string(), | |
| provenance: InstructionProvenance::Project { | |
| source_path: posix_source.clone(), | |
| environment_id: "posix".to_string(), | |
| cwd: posix_cwd, | |
| }, | |
| }, | |
| InstructionEntry { | |
| contents: "Windows instructions".to_string(), | |
| provenance: InstructionProvenance::Project { | |
| source_path: windows_source.clone(), | |
| environment_id: "windows".to_string(), | |
| cwd: windows_cwd, | |
| }, | |
| }, | |
| ], | |
| }; | |
| assert_eq!( | |
| loaded.contextual_user_fragment().render(), | |
| r#"# AGENTS.md instructions | |
| <INSTRUCTIONS> | |
| for `posix` with root /srv/project | |
| POSIX instructions | |
| for `windows` with root C:\workspace | |
| Windows instructions | |
| </INSTRUCTIONS>"# | |
| ); | |
| assert_eq!( | |
| loaded.sources().collect::<Vec<_>>(), | |
| vec![posix_source, windows_source] | |
| ); | |
| } | |
| /// Helper that returns a `Config` pointing at `root` and using `limit` as | |
| /// the maximum number of bytes to embed from AGENTS.md. The caller can | |
| /// optionally specify a custom `instructions` string – when `None` the | |
| /// value is cleared to mimic a scenario where no system instructions have | |
| /// been configured. | |
| async fn make_config(root: &TempDir, limit: usize, instructions: Option<&str>) -> TestConfig { | |
| let codex_home = TempDir::new().unwrap(); | |
| let mut config = ConfigBuilder::default() | |
| .codex_home(codex_home.path().to_path_buf()) | |
| .build() | |
| .await | |
| .expect("defaults for test should always succeed"); | |
| config.cwd = root.abs(); | |
| config.project_doc_max_bytes = limit; | |
| let user_instructions = instructions.map(|text| Instructions { | |
| text: text.to_owned(), | |
| source: Some(config.codex_home.join(DEFAULT_AGENTS_MD_FILENAME)), | |
| }); | |
| TestConfig { | |
| config, | |
| user_instructions, | |
| } | |
| } | |
| async fn make_config_with_fallback( | |
| root: &TempDir, | |
| limit: usize, | |
| instructions: Option<&str>, | |
| fallbacks: &[&str], | |
| ) -> TestConfig { | |
| let mut config = make_config(root, limit, instructions).await; | |
| config.project_doc_fallback_filenames = fallbacks | |
| .iter() | |
| .map(std::string::ToString::to_string) | |
| .collect(); | |
| config | |
| } | |
| async fn make_config_with_project_root_markers( | |
| root: &TempDir, | |
| limit: usize, | |
| instructions: Option<&str>, | |
| markers: &[&str], | |
| ) -> TestConfig { | |
| let codex_home = TempDir::new().unwrap(); | |
| let cli_overrides = vec![( | |
| "project_root_markers".to_string(), | |
| TomlValue::Array( | |
| markers | |
| .iter() | |
| .map(|marker| TomlValue::String((*marker).to_string())) | |
| .collect(), | |
| ), | |
| )]; | |
| let mut config = ConfigBuilder::default() | |
| .codex_home(codex_home.path().to_path_buf()) | |
| .cli_overrides(cli_overrides) | |
| .build() | |
| .await | |
| .expect("defaults for test should always succeed"); | |
| config.cwd = root.abs(); | |
| config.project_doc_max_bytes = limit; | |
| let user_instructions = instructions.map(|text| Instructions { | |
| text: text.to_owned(), | |
| source: Some(config.codex_home.join(DEFAULT_AGENTS_MD_FILENAME)), | |
| }); | |
| TestConfig { | |
| config, | |
| user_instructions, | |
| } | |
| } | |
| /// AGENTS.md missing – should yield `None`. | |
| async fn no_doc_file_returns_none() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| let res = | |
| get_user_instructions(&make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await) | |
| .await; | |
| assert!( | |
| res.is_none(), | |
| "Expected None when AGENTS.md is absent and no system instructions provided" | |
| ); | |
| assert!(res.is_none(), "Expected None when AGENTS.md is absent"); | |
| } | |
| fn empty_loaded_instructions_are_empty() { | |
| let source = | |
| AbsolutePathBuf::from_absolute_path("/tmp/AGENTS.md").expect("absolute source path"); | |
| assert_eq!( | |
| LoadedAgentsMd::new_user(String::new(), source.clone()), | |
| LoadedAgentsMd::default() | |
| ); | |
| assert_eq!( | |
| LoadedAgentsMd::new_user(" \n\t".to_string(), source), | |
| LoadedAgentsMd::default() | |
| ); | |
| assert_eq!( | |
| LoadedAgentsMd::from_text_for_testing(String::new()), | |
| LoadedAgentsMd::default() | |
| ); | |
| assert_eq!( | |
| LoadedAgentsMd::from_text_for_testing(" \n\t"), | |
| LoadedAgentsMd::default() | |
| ); | |
| } | |
| fn loaded_instructions_with_only_empty_or_whitespace_entries_are_empty() { | |
| let empty = LoadedAgentsMd { | |
| user_instructions: None, | |
| thread_instructions: None, | |
| entries: vec![InstructionEntry { | |
| contents: String::new(), | |
| provenance: InstructionProvenance::Internal, | |
| }], | |
| }; | |
| let whitespace = LoadedAgentsMd { | |
| user_instructions: None, | |
| thread_instructions: None, | |
| entries: vec![InstructionEntry { | |
| contents: " \n\t".to_string(), | |
| provenance: InstructionProvenance::Internal, | |
| }], | |
| }; | |
| assert!(empty.is_empty()); | |
| assert!(whitespace.is_empty()); | |
| } | |
| /// Small file within the byte-limit is returned unmodified. | |
| async fn doc_smaller_than_limit_is_returned() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "hello world").unwrap(); | |
| let res = | |
| get_user_instructions(&make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await) | |
| .await | |
| .expect("doc expected"); | |
| assert_eq!( | |
| res, "hello world", | |
| "The document should be returned verbatim when it is smaller than the limit and there are no existing instructions" | |
| ); | |
| } | |
| async fn project_doc_invalid_utf8_uses_lossy_text() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| let path = tmp.path().join("AGENTS.md"); | |
| fs::write(&path, b"project\xFF doc").unwrap(); | |
| let config = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| let res = load_agents_md(&config).await.expect("doc expected").text(); | |
| assert_eq!(res, "project\u{FFFD} doc"); | |
| } | |
| /// Oversize file is truncated to `project_doc_max_bytes`. | |
| async fn doc_larger_than_limit_is_truncated() { | |
| const LIMIT: usize = 1024; | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| let huge = "A".repeat(LIMIT * 2); // 2 KiB | |
| fs::write(tmp.path().join("AGENTS.md"), &huge).unwrap(); | |
| let res = get_user_instructions(&make_config(&tmp, LIMIT, /*instructions*/ None).await) | |
| .await | |
| .expect("doc expected"); | |
| assert_eq!(res.len(), LIMIT, "doc should be truncated to LIMIT bytes"); | |
| assert_eq!(res, huge[..LIMIT]); | |
| } | |
| async fn total_byte_limit_truncates_later_project_docs() { | |
| let repo = tempfile::tempdir().expect("tempdir"); | |
| fs::write(repo.path().join(".git"), "").unwrap(); | |
| fs::write(repo.path().join("AGENTS.md"), "root").unwrap(); | |
| let nested = repo.path().join("nested"); | |
| fs::create_dir(&nested).unwrap(); | |
| fs::write(nested.join("AGENTS.md"), "abcdef").unwrap(); | |
| let mut config = make_config(&repo, /*limit*/ 7, /*instructions*/ None).await; | |
| config.cwd = nested.abs(); | |
| let loaded = load_agents_md(&config).await.expect("project instructions"); | |
| let expected = LoadedAgentsMd { | |
| user_instructions: None, | |
| thread_instructions: None, | |
| entries: vec![ | |
| InstructionEntry { | |
| contents: "root".to_string(), | |
| provenance: project_provenance( | |
| repo.path().join("AGENTS.md").abs(), | |
| config.cwd.clone(), | |
| ), | |
| }, | |
| InstructionEntry { | |
| contents: "abc".to_string(), | |
| provenance: project_provenance(config.cwd.join("AGENTS.md"), config.cwd.clone()), | |
| }, | |
| ], | |
| }; | |
| assert_eq!(loaded, expected); | |
| assert_eq!(loaded.text(), "root\n\nabc"); | |
| } | |
| /// Unreadable ancestor markers must not hide readable instructions in the selected cwd. | |
| async fn read_agents_md_loads_cwd_instructions_when_parent_markers_are_unreadable() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| let nested = tmp.path().join("nested"); | |
| fs::create_dir(&nested).unwrap(); | |
| fs::write(nested.join("AGENTS.md"), "project doc").unwrap(); | |
| let mut config = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| config.cwd = nested.abs(); | |
| let marker_path = tmp.path().join(".git").abs(); | |
| let fs = FailingFileSystem { | |
| path: marker_path, | |
| failure: InjectedFailure::Metadata(io::ErrorKind::PermissionDenied), | |
| metadata_calls: Arc::default(), | |
| }; | |
| let cwd = config.cwd.clone(); | |
| let loaded = read_agents_md( | |
| &config.config, | |
| &fs, | |
| "local", | |
| &PathUri::from_abs_path(&cwd), | |
| config.project_doc_max_bytes, | |
| /*sandbox*/ None, | |
| ) | |
| .await | |
| .expect("unreadable parent markers should not hide cwd instructions") | |
| .expect("cwd instructions"); | |
| assert_eq!(loaded.text(), "project doc"); | |
| } | |
| async fn read_agents_md_propagates_read_errors() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "project doc").unwrap(); | |
| let config = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| let fs = FailingFileSystem { | |
| path: config.cwd.join("AGENTS.md"), | |
| failure: InjectedFailure::Read(io::ErrorKind::PermissionDenied), | |
| metadata_calls: Arc::default(), | |
| }; | |
| let cwd = config.cwd.clone(); | |
| let err = read_agents_md( | |
| &config.config, | |
| &fs, | |
| "local", | |
| &PathUri::from_abs_path(&cwd), | |
| config.project_doc_max_bytes, | |
| /*sandbox*/ None, | |
| ) | |
| .await | |
| .expect_err("read error"); | |
| assert_eq!(err.kind(), io::ErrorKind::PermissionDenied); | |
| } | |
| async fn read_agents_md_ignores_files_removed_after_discovery() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "project doc").unwrap(); | |
| let config = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| let fs = FailingFileSystem { | |
| path: config.cwd.join("AGENTS.md"), | |
| failure: InjectedFailure::Read(io::ErrorKind::NotFound), | |
| metadata_calls: Arc::default(), | |
| }; | |
| let cwd = config.cwd.clone(); | |
| let loaded = read_agents_md( | |
| &config.config, | |
| &fs, | |
| "local", | |
| &PathUri::from_abs_path(&cwd), | |
| config.project_doc_max_bytes, | |
| /*sandbox*/ None, | |
| ) | |
| .await | |
| .expect("removed file is recoverable"); | |
| assert_eq!(loaded, None); | |
| } | |
| async fn marker_search_does_not_wait_for_a_higher_ancestor() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join(".git"), "").unwrap(); | |
| fs::write(tmp.path().join("AGENTS.md"), "project doc").unwrap(); | |
| let nested = tmp.path().join("nested"); | |
| fs::create_dir(&nested).unwrap(); | |
| let mut config = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| config.cwd = nested.abs(); | |
| let pending_marker = tmp | |
| .path() | |
| .parent() | |
| .expect("tempdir parent") | |
| .join(".git") | |
| .abs(); | |
| let fs = FailingFileSystem { | |
| path: pending_marker, | |
| failure: InjectedFailure::MetadataPending, | |
| metadata_calls: Arc::default(), | |
| }; | |
| let cwd = PathUri::from_abs_path(&config.cwd); | |
| let paths = tokio::time::timeout( | |
| std::time::Duration::from_secs(1), | |
| super::agents_md_paths(&config.config, &cwd, &fs, /*sandbox*/ None), | |
| ) | |
| .await | |
| .expect("nearest marker should complete") | |
| .expect("AGENTS.md discovery"); | |
| assert_eq!( | |
| paths, | |
| vec![PathUri::from_abs_path( | |
| &tmp.path().join(DEFAULT_AGENTS_MD_FILENAME).abs() | |
| )] | |
| ); | |
| } | |
| async fn project_root_marker_search_limits_concurrent_probes_and_preserves_order() { | |
| const CONCURRENCY_LIMIT: usize = 256; | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "project doc").unwrap(); | |
| let nested = tmp.path().join("nested"); | |
| fs::create_dir_all(&nested).unwrap(); | |
| fs::write(nested.join("AGENTS.md"), "nested project doc").unwrap(); | |
| let markers = (0..=CONCURRENCY_LIMIT) | |
| .map(|index| format!(".project-root-{index}")) | |
| .collect::<Vec<_>>(); | |
| fs::write( | |
| tmp.path() | |
| .join(markers.last().expect("last project root marker")), | |
| "", | |
| ) | |
| .unwrap(); | |
| let marker_refs = markers.iter().map(String::as_str).collect::<Vec<_>>(); | |
| let mut config = make_config_with_project_root_markers( | |
| &tmp, | |
| /*limit*/ 4096, | |
| /*instructions*/ None, | |
| &marker_refs, | |
| ) | |
| .await; | |
| config.cwd = nested.abs(); | |
| let cwd = PathUri::from_abs_path(&config.cwd); | |
| let expected_initial_probes = markers | |
| .iter() | |
| .map(|marker| cwd.join(marker).expect("project root marker path")) | |
| .collect::<Vec<_>>(); | |
| let max_probe_count = markers.len() * config.cwd.ancestors().count(); | |
| let metadata_calls = Arc::new(MetadataCallCounts::default()); | |
| let fs = FailingFileSystem { | |
| path: config.cwd.join("unused"), | |
| failure: InjectedFailure::MetadataBlockedByFilenamePrefix(".project-root-"), | |
| metadata_calls: Arc::clone(&metadata_calls), | |
| }; | |
| let assertions = async { | |
| tokio::time::timeout(std::time::Duration::from_secs(5), async { | |
| loop { | |
| let started = metadata_calls.started.notified(); | |
| if metadata_calls | |
| .paths | |
| .lock() | |
| .expect("metadata paths lock") | |
| .len() | |
| >= CONCURRENCY_LIMIT | |
| { | |
| break; | |
| } | |
| started.await; | |
| } | |
| }) | |
| .await | |
| .expect("initial marker window should start"); | |
| assert_eq!( | |
| *metadata_calls.paths.lock().expect("metadata paths lock"), | |
| expected_initial_probes[..CONCURRENCY_LIMIT] | |
| ); | |
| metadata_calls.release.add_permits(1); | |
| tokio::time::timeout(std::time::Duration::from_secs(5), async { | |
| loop { | |
| let started = metadata_calls.started.notified(); | |
| if metadata_calls | |
| .paths | |
| .lock() | |
| .expect("metadata paths lock") | |
| .len() | |
| > CONCURRENCY_LIMIT | |
| { | |
| break; | |
| } | |
| started.await; | |
| } | |
| }) | |
| .await | |
| .expect("next marker probe should start"); | |
| assert_eq!( | |
| *metadata_calls.paths.lock().expect("metadata paths lock"), | |
| expected_initial_probes | |
| ); | |
| metadata_calls.release.add_permits(max_probe_count); | |
| }; | |
| let (paths, ()) = tokio::join!( | |
| super::agents_md_paths(&config.config, &cwd, &fs, /*sandbox*/ None), | |
| assertions | |
| ); | |
| let paths = paths.expect("AGENTS.md discovery"); | |
| assert_eq!( | |
| paths, | |
| vec![ | |
| PathUri::from_abs_path(&tmp.path().join(DEFAULT_AGENTS_MD_FILENAME).abs()), | |
| PathUri::from_abs_path(&nested.join(DEFAULT_AGENTS_MD_FILENAME).abs()), | |
| ] | |
| ); | |
| } | |
| async fn agents_md_search_starts_all_directory_probes() { | |
| const NESTING_DEPTH: usize = 9; | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join(".git"), "").unwrap(); | |
| fs::write(tmp.path().join("AGENTS.md"), "project doc").unwrap(); | |
| let mut nested = tmp.path().to_path_buf(); | |
| for depth in 0..NESTING_DEPTH { | |
| nested.push(format!("nested-{depth}")); | |
| } | |
| fs::create_dir_all(&nested).unwrap(); | |
| let mut config = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| config.cwd = nested.abs(); | |
| let cwd = PathUri::from_abs_path(&config.cwd); | |
| let mut search_dirs = config | |
| .cwd | |
| .ancestors() | |
| .take(NESTING_DEPTH + 1) | |
| .collect::<Vec<_>>(); | |
| search_dirs.reverse(); | |
| let expected_probes = search_dirs | |
| .into_iter() | |
| .map(|directory| PathUri::from_abs_path(&directory.join(LOCAL_AGENTS_MD_FILENAME))) | |
| .collect::<Vec<_>>(); | |
| let metadata_calls = Arc::new(MetadataCallCounts::default()); | |
| let fs = FailingFileSystem { | |
| path: tmp.path().join(LOCAL_AGENTS_MD_FILENAME).abs(), | |
| failure: InjectedFailure::MetadataBlocked, | |
| metadata_calls: Arc::clone(&metadata_calls), | |
| }; | |
| let search = tokio::spawn(async move { | |
| super::agents_md_paths(&config.config, &cwd, &fs, /*sandbox*/ None).await | |
| }); | |
| tokio::time::timeout(std::time::Duration::from_secs(5), async { | |
| loop { | |
| let started = metadata_calls.started.notified(); | |
| if expected_probes.iter().all(|candidate| { | |
| metadata_calls | |
| .paths | |
| .lock() | |
| .expect("metadata paths lock") | |
| .contains(candidate) | |
| }) { | |
| break; | |
| } | |
| started.await; | |
| } | |
| }) | |
| .await | |
| .expect("all directory probes should start"); | |
| let mut actual_probes = metadata_calls | |
| .paths | |
| .lock() | |
| .expect("metadata paths lock") | |
| .iter() | |
| .filter(|path| expected_probes.contains(path)) | |
| .map(ToString::to_string) | |
| .collect::<Vec<_>>(); | |
| actual_probes.sort(); | |
| let mut expected_probes = expected_probes | |
| .into_iter() | |
| .map(|path| path.to_string()) | |
| .collect::<Vec<_>>(); | |
| expected_probes.sort(); | |
| assert_eq!(actual_probes, expected_probes); | |
| metadata_calls.release.add_permits(1); | |
| let paths = tokio::time::timeout(std::time::Duration::from_secs(5), search) | |
| .await | |
| .expect("AGENTS.md search should complete") | |
| .expect("AGENTS.md search task") | |
| .expect("AGENTS.md discovery"); | |
| assert_eq!( | |
| paths, | |
| vec![PathUri::from_abs_path( | |
| &tmp.path().join(DEFAULT_AGENTS_MD_FILENAME).abs() | |
| )] | |
| ); | |
| } | |
| async fn empty_project_root_markers_only_probe_cwd_candidates() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "parent doc").unwrap(); | |
| let nested = tmp.path().join("nested"); | |
| fs::create_dir(&nested).unwrap(); | |
| fs::write(nested.join("AGENTS.md"), "cwd doc").unwrap(); | |
| let mut config = make_config_with_project_root_markers( | |
| &tmp, | |
| /*limit*/ 4096, | |
| /*instructions*/ None, | |
| &[], | |
| ) | |
| .await; | |
| config.cwd = nested.abs(); | |
| let metadata_calls = Arc::new(MetadataCallCounts::default()); | |
| let fs = FailingFileSystem { | |
| path: config.cwd.join("unused"), | |
| failure: InjectedFailure::Read(io::ErrorKind::PermissionDenied), | |
| metadata_calls: Arc::clone(&metadata_calls), | |
| }; | |
| let cwd = PathUri::from_abs_path(&config.cwd); | |
| let paths = super::agents_md_paths(&config.config, &cwd, &fs, /*sandbox*/ None) | |
| .await | |
| .expect("AGENTS.md discovery"); | |
| let override_path = cwd.join(LOCAL_AGENTS_MD_FILENAME).expect("override path"); | |
| let agents_path = cwd.join(DEFAULT_AGENTS_MD_FILENAME).expect("agents path"); | |
| assert_eq!(paths, vec![agents_path.clone()]); | |
| assert_eq!( | |
| metadata_calls | |
| .paths | |
| .lock() | |
| .expect("metadata paths lock") | |
| .clone(), | |
| vec![override_path, agents_path] | |
| ); | |
| } | |
| /// When `cwd` is nested inside a repo, the search should locate AGENTS.md | |
| /// placed at the repository root (identified by `.git`). | |
| async fn finds_doc_in_repo_root() { | |
| let repo = tempfile::tempdir().expect("tempdir"); | |
| // Simulate a git repository. Note .git can be a file or a directory. | |
| std::fs::write( | |
| repo.path().join(".git"), | |
| "gitdir: /path/to/actual/git/dir\n", | |
| ) | |
| .unwrap(); | |
| // Put the doc at the repo root. | |
| fs::write(repo.path().join("AGENTS.md"), "root level doc").unwrap(); | |
| // Now create a nested working directory: repo/workspace/crate_a | |
| let nested = repo.path().join("workspace/crate_a"); | |
| std::fs::create_dir_all(&nested).unwrap(); | |
| // Build config pointing at the nested dir. | |
| let mut cfg = make_config(&repo, /*limit*/ 4096, /*instructions*/ None).await; | |
| cfg.cwd = nested.abs(); | |
| let res = get_user_instructions(&cfg).await.expect("doc expected"); | |
| assert_eq!(res, "root level doc"); | |
| } | |
| /// Explicitly setting the byte-limit to zero disables project docs. | |
| async fn zero_byte_limit_disables_docs() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "something").unwrap(); | |
| let res = | |
| get_user_instructions(&make_config(&tmp, /*limit*/ 0, /*instructions*/ None).await).await; | |
| assert!( | |
| res.is_none(), | |
| "With limit 0 the function should return None" | |
| ); | |
| } | |
| /// User instructions precede project docs without consuming their byte budget. | |
| async fn merges_existing_instructions_with_agents_md() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "proj document").unwrap(); | |
| const INSTRUCTIONS: &str = "base instructions"; | |
| let limit = "proj doc".len(); | |
| let res = get_user_instructions(&make_config(&tmp, limit, Some(INSTRUCTIONS)).await) | |
| .await | |
| .expect("should produce a combined instruction string"); | |
| let expected = format!("{INSTRUCTIONS}{AGENTS_MD_SEPARATOR}{}", "proj doc"); | |
| assert_eq!(res, expected); | |
| } | |
| async fn multiple_environment_docs_use_labeled_layout_and_preserve_source_order() { | |
| let primary = tempfile::tempdir().expect("primary tempdir"); | |
| let secondary = tempfile::tempdir().expect("secondary tempdir"); | |
| fs::create_dir(primary.path().join(".git")).unwrap(); | |
| fs::write(primary.path().join("AGENTS.md"), "primary root doc").unwrap(); | |
| let primary_nested = primary.path().join("nested"); | |
| fs::create_dir(&primary_nested).unwrap(); | |
| fs::write(primary_nested.join("AGENTS.md"), "primary nested doc").unwrap(); | |
| fs::write(secondary.path().join("AGENTS.md"), "secondary doc").unwrap(); | |
| let mut config = make_config(&primary, /*limit*/ 4096, Some("global instructions")).await; | |
| config.cwd = primary_nested.abs(); | |
| let environments = resolved_local_environments([ | |
| ("primary", config.cwd.clone()), | |
| ("secondary", secondary.abs()), | |
| ]); | |
| let user_instructions = config.user_instructions.clone(); | |
| let loaded = load_project_instructions(&config.config, user_instructions, &environments) | |
| .await | |
| .expect("project instructions should load") | |
| .expect("instructions expected"); | |
| let inner = format!( | |
| r#"global instructions | |
| for `primary` with root {} | |
| primary root doc | |
| primary nested doc | |
| for `secondary` with root {} | |
| secondary doc"#, | |
| primary_nested.display(), | |
| secondary.path().display(), | |
| ); | |
| assert_eq!(loaded.environment_labeled_text(), inner); | |
| assert_eq!(loaded.text(), inner); | |
| let expected_fragment = format!( | |
| r#"# AGENTS.md instructions | |
| <INSTRUCTIONS> | |
| {inner} | |
| </INSTRUCTIONS>"# | |
| ); | |
| assert_eq!( | |
| loaded.contextual_user_fragment().render(), | |
| expected_fragment | |
| ); | |
| assert_eq!( | |
| loaded.sources().collect::<Vec<_>>(), | |
| vec![ | |
| PathUri::from_abs_path( | |
| config | |
| .user_instructions | |
| .as_ref() | |
| .expect("global instructions") | |
| .source | |
| .as_ref() | |
| .expect("global instruction source"), | |
| ), | |
| PathUri::from_abs_path(&primary.path().join("AGENTS.md").abs()), | |
| PathUri::from_abs_path(&primary_nested.join("AGENTS.md").abs()), | |
| PathUri::from_abs_path(&secondary.path().join("AGENTS.md").abs()), | |
| ] | |
| ); | |
| } | |
| async fn secondary_only_project_doc_uses_single_contributor_layout() { | |
| let primary = tempfile::tempdir().expect("primary tempdir"); | |
| let secondary = tempfile::tempdir().expect("secondary tempdir"); | |
| fs::write(secondary.path().join("AGENTS.md"), "secondary doc").unwrap(); | |
| let config = make_config(&primary, /*limit*/ 4096, Some("global instructions")).await; | |
| let environments = resolved_local_environments([ | |
| ("primary", config.cwd.clone()), | |
| ("secondary", secondary.abs()), | |
| ]); | |
| let user_instructions = config.user_instructions.clone(); | |
| let loaded = load_project_instructions(&config.config, user_instructions, &environments) | |
| .await | |
| .expect("project instructions should load") | |
| .expect("instructions expected"); | |
| let inner = format!("global instructions{AGENTS_MD_SEPARATOR}secondary doc"); | |
| assert_eq!(loaded.legacy_text(), inner); | |
| assert_eq!(loaded.text(), inner); | |
| let expected_fragment = format!( | |
| "# AGENTS.md instructions for {}\n\n<INSTRUCTIONS>\n{inner}\n</INSTRUCTIONS>", | |
| secondary.path().display() | |
| ); | |
| assert_eq!( | |
| loaded.contextual_user_fragment().render(), | |
| expected_fragment | |
| ); | |
| } | |
| async fn primary_only_project_doc_preserves_legacy_layout_with_multiple_bound_environments() { | |
| let primary = tempfile::tempdir().expect("primary tempdir"); | |
| let secondary = tempfile::tempdir().expect("secondary tempdir"); | |
| fs::write(primary.path().join("AGENTS.md"), "primary doc").unwrap(); | |
| let config = make_config(&primary, /*limit*/ 4096, Some("global instructions")).await; | |
| let environments = resolved_local_environments([ | |
| ("primary", config.cwd.clone()), | |
| ("secondary", secondary.abs()), | |
| ]); | |
| let user_instructions = config.user_instructions.clone(); | |
| let loaded = load_project_instructions(&config.config, user_instructions, &environments) | |
| .await | |
| .expect("project instructions should load") | |
| .expect("instructions expected"); | |
| let inner = format!("global instructions{AGENTS_MD_SEPARATOR}primary doc"); | |
| assert_eq!(loaded.legacy_text(), inner); | |
| assert_eq!(loaded.text(), inner); | |
| let expected_fragment = format!( | |
| "# AGENTS.md instructions for {}\n\n<INSTRUCTIONS>\n{inner}\n</INSTRUCTIONS>", | |
| primary.path().display() | |
| ); | |
| assert_eq!( | |
| loaded.contextual_user_fragment().render(), | |
| expected_fragment | |
| ); | |
| } | |
| async fn project_doc_byte_limit_is_shared_across_environments() { | |
| let primary = tempfile::tempdir().expect("primary tempdir"); | |
| let secondary = tempfile::tempdir().expect("secondary tempdir"); | |
| fs::write(primary.path().join("AGENTS.md"), "ABCDE").unwrap(); | |
| fs::write(secondary.path().join("AGENTS.md"), "VWXYZ").unwrap(); | |
| let config = make_config(&primary, /*limit*/ 7, /*instructions*/ None).await; | |
| let environments = resolved_local_environments([ | |
| ("primary", config.cwd.clone()), | |
| ("secondary", secondary.abs()), | |
| ]); | |
| let user_instructions = config.user_instructions.clone(); | |
| let loaded = load_project_instructions(&config.config, user_instructions, &environments) | |
| .await | |
| .expect("project instructions should load") | |
| .expect("instructions expected"); | |
| assert_eq!( | |
| loaded.text(), | |
| format!( | |
| "for `primary` with root {}\n\nABCDE\n\nfor `secondary` with root {}\n\nVW", | |
| primary.path().display(), | |
| secondary.path().display() | |
| ) | |
| ); | |
| } | |
| async fn full_primary_environment_budget_excludes_later_environment_docs() { | |
| const LIMIT: usize = 8; | |
| let primary = tempfile::tempdir().expect("primary tempdir"); | |
| let secondary = tempfile::tempdir().expect("secondary tempdir"); | |
| let primary_doc = "P".repeat(LIMIT); | |
| let secondary_doc = "S".repeat(LIMIT); | |
| fs::write(primary.path().join("AGENTS.md"), &primary_doc).unwrap(); | |
| fs::write(secondary.path().join("AGENTS.md"), &secondary_doc).unwrap(); | |
| let config = make_config(&primary, LIMIT, /*instructions*/ None).await; | |
| let environments = resolved_local_environments([ | |
| ("primary", config.cwd.clone()), | |
| ("secondary", secondary.abs()), | |
| ]); | |
| let loaded = load_project_instructions( | |
| &config.config, | |
| /*user_instructions*/ None, | |
| &environments, | |
| ) | |
| .await | |
| .expect("project instructions should load") | |
| .expect("instructions expected"); | |
| let project_bytes = loaded | |
| .entries | |
| .iter() | |
| .filter(|entry| matches!(&entry.provenance, InstructionProvenance::Project { .. })) | |
| .map(|entry| entry.contents.len()) | |
| .sum::<usize>(); | |
| assert_eq!(project_bytes, LIMIT); | |
| assert!(loaded.text().contains(&primary_doc)); | |
| assert!(!loaded.text().contains(&secondary_doc)); | |
| } | |
| async fn secondary_environment_invalid_utf8_does_not_suppress_other_docs() { | |
| let primary = tempfile::tempdir().expect("primary tempdir"); | |
| let secondary = tempfile::tempdir().expect("secondary tempdir"); | |
| fs::write(primary.path().join("AGENTS.md"), "primary doc").unwrap(); | |
| fs::write(secondary.path().join("AGENTS.md"), b"secondary\xFFdoc").unwrap(); | |
| let config = make_config(&primary, /*limit*/ 4096, /*instructions*/ None).await; | |
| let environments = resolved_local_environments([ | |
| ("primary", config.cwd.clone()), | |
| ("secondary", secondary.abs()), | |
| ]); | |
| let loaded = load_project_instructions( | |
| &config.config, | |
| /*user_instructions*/ None, | |
| &environments, | |
| ) | |
| .await | |
| .expect("project instructions should load") | |
| .expect("instructions expected"); | |
| assert!(loaded.text().contains("primary doc")); | |
| assert!(loaded.text().contains("secondary\u{FFFD}doc")); | |
| } | |
| /// If there are existing system instructions but AGENTS.md docs are | |
| /// missing we expect the original instructions to be returned unchanged. | |
| async fn keeps_existing_instructions_when_doc_missing() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| const INSTRUCTIONS: &str = "some instructions"; | |
| let res = | |
| get_user_instructions(&make_config(&tmp, /*limit*/ 4096, Some(INSTRUCTIONS)).await).await; | |
| assert_eq!(res, Some(INSTRUCTIONS.to_string())); | |
| } | |
| /// When both the repository root and the working directory contain | |
| /// AGENTS.md files, their contents are concatenated from root to cwd. | |
| async fn concatenates_root_and_cwd_docs() { | |
| let repo = tempfile::tempdir().expect("tempdir"); | |
| // Simulate a git repository. | |
| std::fs::write( | |
| repo.path().join(".git"), | |
| "gitdir: /path/to/actual/git/dir\n", | |
| ) | |
| .unwrap(); | |
| // Repo root doc. | |
| fs::write(repo.path().join("AGENTS.md"), "root doc").unwrap(); | |
| // Nested working directory with its own doc. | |
| let nested = repo.path().join("workspace/crate_a"); | |
| std::fs::create_dir_all(&nested).unwrap(); | |
| fs::write(nested.join("AGENTS.md"), "crate doc").unwrap(); | |
| let mut cfg = make_config(&repo, /*limit*/ 4096, /*instructions*/ None).await; | |
| cfg.cwd = nested.abs(); | |
| let loaded = load_agents_md(&cfg).await.expect("doc expected"); | |
| let root_agents = repo.path().join("AGENTS.md").abs(); | |
| let crate_agents = cfg.cwd.join("AGENTS.md"); | |
| let expected = LoadedAgentsMd { | |
| user_instructions: None, | |
| thread_instructions: None, | |
| entries: vec![ | |
| InstructionEntry { | |
| contents: "root doc".to_string(), | |
| provenance: project_provenance(root_agents.clone(), cfg.cwd.clone()), | |
| }, | |
| InstructionEntry { | |
| contents: "crate doc".to_string(), | |
| provenance: project_provenance(crate_agents.clone(), cfg.cwd.clone()), | |
| }, | |
| ], | |
| }; | |
| assert_eq!(loaded, expected); | |
| assert_eq!(loaded.text(), "root doc\n\ncrate doc"); | |
| assert_eq!( | |
| loaded.sources().collect::<Vec<_>>(), | |
| vec![ | |
| PathUri::from_abs_path(&root_agents), | |
| PathUri::from_abs_path(&crate_agents), | |
| ] | |
| ); | |
| } | |
| async fn project_root_markers_are_honored_for_agents_discovery() { | |
| let root = tempfile::tempdir().expect("tempdir"); | |
| fs::write(root.path().join(".codex-root"), "").unwrap(); | |
| fs::write(root.path().join("AGENTS.md"), "parent doc").unwrap(); | |
| let nested = root.path().join("dir1"); | |
| fs::create_dir_all(nested.join(".git")).unwrap(); | |
| fs::write(nested.join("AGENTS.md"), "child doc").unwrap(); | |
| let mut cfg = make_config_with_project_root_markers( | |
| &root, | |
| /*limit*/ 4096, | |
| /*instructions*/ None, | |
| &[".codex-root"], | |
| ) | |
| .await; | |
| cfg.cwd = nested.abs(); | |
| let discovery = agents_md_paths(&cfg).await.expect("discover paths"); | |
| let expected_parent = root.path().join("AGENTS.md").abs(); | |
| let expected_child = cfg.cwd.join("AGENTS.md"); | |
| assert_eq!(discovery.len(), 2); | |
| assert_eq!(discovery[0], PathUri::from_abs_path(&expected_parent)); | |
| assert_eq!(discovery[1], PathUri::from_abs_path(&expected_child)); | |
| let res = get_user_instructions(&cfg).await.expect("doc expected"); | |
| assert_eq!(res, "parent doc\n\nchild doc"); | |
| } | |
| async fn project_layers_do_not_override_project_root_markers() { | |
| let root = tempfile::tempdir().expect("tempdir"); | |
| fs::write(root.path().join(".git"), "").unwrap(); | |
| fs::write(root.path().join("AGENTS.md"), "root doc").unwrap(); | |
| let nested = root.path().join("nested"); | |
| fs::create_dir(&nested).unwrap(); | |
| fs::write(nested.join("AGENTS.md"), "nested doc").unwrap(); | |
| let mut config = make_config(&root, /*limit*/ 4096, /*instructions*/ None).await; | |
| config.cwd = nested.abs(); | |
| let project_layer = |dot_codex_folder: AbsolutePathBuf, marker: &str| { | |
| ConfigLayerEntry::new( | |
| ConfigLayerSource::Project { dot_codex_folder }, | |
| TomlValue::Table( | |
| [( | |
| "project_root_markers".to_string(), | |
| TomlValue::Array(vec![TomlValue::String(marker.to_string())]), | |
| )] | |
| .into_iter() | |
| .collect(), | |
| ), | |
| ) | |
| }; | |
| config.config_layer_stack = ConfigLayerStack::new( | |
| vec![ | |
| project_layer(root.path().join(".codex").abs(), ".ignored-root-marker"), | |
| project_layer(config.cwd.join(".codex"), ".ignored-nested-marker"), | |
| ], | |
| ConfigRequirements::default(), | |
| ConfigRequirementsToml::default(), | |
| ) | |
| .expect("valid project layer ordering"); | |
| let discovery = agents_md_paths(&config).await.expect("discover paths"); | |
| assert_eq!( | |
| discovery, | |
| vec![ | |
| PathUri::from_abs_path(&root.path().join("AGENTS.md").abs()), | |
| PathUri::from_abs_path(&config.cwd.join("AGENTS.md")), | |
| ] | |
| ); | |
| } | |
| async fn agents_md_paths_preserve_symlinked_cwd() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| let target = tmp.path().join("target"); | |
| fs::create_dir(&target).unwrap(); | |
| fs::write(target.join("AGENTS.md"), "project doc").unwrap(); | |
| let linked_cwd = tmp.path().join("linked"); | |
| create_directory_symlink(&target, &linked_cwd); | |
| let mut cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| cfg.cwd = linked_cwd.abs(); | |
| let discovery = agents_md_paths(&cfg).await.expect("discover paths"); | |
| assert_eq!( | |
| discovery, | |
| vec![PathUri::from_abs_path(&cfg.cwd.join("AGENTS.md"))] | |
| ); | |
| let res = get_user_instructions(&cfg).await.expect("doc expected"); | |
| assert_eq!(res, "project doc"); | |
| } | |
| async fn instruction_sources_include_global_before_agents_md_docs() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "project doc").unwrap(); | |
| let cfg = make_config(&tmp, /*limit*/ 4096, Some("global doc")).await; | |
| let global_agents = cfg.codex_home.join(DEFAULT_AGENTS_MD_FILENAME); | |
| fs::create_dir_all(&cfg.codex_home).unwrap(); | |
| fs::write(&global_agents, "global doc").unwrap(); | |
| let loaded = load_agents_md(&cfg).await.expect("instructions expected"); | |
| let project_agents = cfg.cwd.join("AGENTS.md"); | |
| let expected = LoadedAgentsMd { | |
| user_instructions: Some(Instructions { | |
| text: "global doc".to_string(), | |
| source: Some(global_agents.clone()), | |
| }), | |
| thread_instructions: None, | |
| entries: vec![InstructionEntry { | |
| contents: "project doc".to_string(), | |
| provenance: project_provenance(project_agents.clone(), cfg.cwd.clone()), | |
| }], | |
| }; | |
| assert_eq!(loaded, expected); | |
| assert_eq!( | |
| loaded.sources().collect::<Vec<_>>(), | |
| vec![ | |
| PathUri::from_abs_path(&global_agents), | |
| PathUri::from_abs_path(&project_agents), | |
| ] | |
| ); | |
| assert_eq!( | |
| loaded.text(), | |
| format!("global doc{AGENTS_MD_SEPARATOR}project doc") | |
| ); | |
| } | |
| /// AGENTS.override.md is preferred over AGENTS.md when both are present. | |
| async fn agents_local_md_preferred() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join(DEFAULT_AGENTS_MD_FILENAME), "versioned").unwrap(); | |
| fs::write(tmp.path().join(LOCAL_AGENTS_MD_FILENAME), "local").unwrap(); | |
| let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| let res = get_user_instructions(&cfg) | |
| .await | |
| .expect("local doc expected"); | |
| assert_eq!(res, "local"); | |
| let discovery = agents_md_paths(&cfg).await.expect("discover paths"); | |
| assert_eq!(discovery.len(), 1); | |
| assert_eq!( | |
| discovery[0].basename().as_deref(), | |
| Some(LOCAL_AGENTS_MD_FILENAME) | |
| ); | |
| } | |
| async fn fallback_paths_are_rejected_before_filesystem_probes() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| let mut cfg = make_config_with_project_root_markers( | |
| &tmp, | |
| /*limit*/ 4096, | |
| /*instructions*/ None, | |
| &[], | |
| ) | |
| .await; | |
| let windows_paths = [ | |
| r"..\AGENTS.md", | |
| r"nested\AGENTS.md", | |
| r"\AGENTS.md", | |
| r"C:\AGENTS.md", | |
| "C:AGENTS.md", | |
| r"\\server\share\AGENTS.md", | |
| r"\\?\UNC\server\share\AGENTS.md", | |
| r"\\.\pipe\instructions", | |
| "AGENTS.md:stream", | |
| ]; | |
| cfg.project_doc_fallback_filenames = [ | |
| "", | |
| ".", | |
| "..", | |
| "/AGENTS.md", | |
| "../AGENTS.md", | |
| "nested/AGENTS.md", | |
| "//server/share/AGENTS.md", | |
| "AGENTS\0.md", | |
| ] | |
| .into_iter() | |
| .chain(windows_paths) | |
| .chain(["WORKFLOW.md", "WORKFLOW.md", ".instructions.md"]) | |
| .map(str::to_owned) | |
| .collect(); | |
| // Backslashes and colons are ordinary filename characters on POSIX executors. | |
| for (cwd, extra_filenames) in [ | |
| ("file:///repo", windows_paths.as_slice()), | |
| ("file:///C:/repo", &[][..]), | |
| ] { | |
| let cwd: PathUri = cwd.parse().expect("cwd URI"); | |
| let metadata_calls = Arc::new(MetadataCallCounts::default()); | |
| let filesystem = FailingFileSystem { | |
| path: tmp.abs(), | |
| failure: InjectedFailure::MetadataNotFound, | |
| metadata_calls: Arc::clone(&metadata_calls), | |
| }; | |
| let paths = super::agents_md_paths(&cfg, &cwd, &filesystem, /*sandbox*/ None) | |
| .await | |
| .expect("discover paths"); | |
| assert_eq!(paths, Vec::<PathUri>::new()); | |
| assert_eq!( | |
| *metadata_calls.paths.lock().expect("metadata paths lock"), | |
| ["AGENTS.override.md", "AGENTS.md"] | |
| .into_iter() | |
| .chain(extra_filenames.iter().copied()) | |
| .chain(["WORKFLOW.md", ".instructions.md"]) | |
| .map(|name| cwd.join(name).expect("filename")) | |
| .collect::<Vec<_>>() | |
| ); | |
| } | |
| } | |
| /// When AGENTS.md is absent but a configured fallback exists, the fallback is used. | |
| async fn uses_configured_fallback_when_agents_missing() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("EXAMPLE.md"), "example instructions").unwrap(); | |
| let cfg = make_config_with_fallback( | |
| &tmp, | |
| /*limit*/ 4096, | |
| /*instructions*/ None, | |
| &["EXAMPLE.md"], | |
| ) | |
| .await; | |
| let res = get_user_instructions(&cfg) | |
| .await | |
| .expect("fallback doc expected"); | |
| assert_eq!(res, "example instructions"); | |
| } | |
| /// AGENTS.md remains preferred when both AGENTS.md and fallbacks are present. | |
| async fn agents_md_preferred_over_fallbacks() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "primary").unwrap(); | |
| fs::write(tmp.path().join("EXAMPLE.md"), "secondary").unwrap(); | |
| let cfg = make_config_with_fallback( | |
| &tmp, | |
| /*limit*/ 4096, | |
| /*instructions*/ None, | |
| &["EXAMPLE.md", ".example.md"], | |
| ) | |
| .await; | |
| let res = get_user_instructions(&cfg) | |
| .await | |
| .expect("AGENTS.md should win"); | |
| assert_eq!(res, "primary"); | |
| let discovery = agents_md_paths(&cfg).await.expect("discover paths"); | |
| assert_eq!(discovery.len(), 1); | |
| assert_eq!( | |
| discovery[0].basename().as_deref(), | |
| Some(DEFAULT_AGENTS_MD_FILENAME) | |
| ); | |
| } | |
| async fn agents_md_directory_is_ignored() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::create_dir(tmp.path().join("AGENTS.md")).unwrap(); | |
| let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| let res = get_user_instructions(&cfg).await; | |
| assert_eq!(res, None); | |
| let discovery = agents_md_paths(&cfg).await.expect("discover paths"); | |
| assert_eq!(discovery, Vec::<PathUri>::new()); | |
| } | |
| async fn agents_md_special_file_is_ignored() { | |
| use std::ffi::CString; | |
| use std::os::unix::ffi::OsStrExt; | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| let path = tmp.path().join("AGENTS.md"); | |
| let c_path = CString::new(path.as_os_str().as_bytes()).expect("path without nul"); | |
| // SAFETY: `c_path` is a valid, nul-terminated path and `mkfifo` does not | |
| // retain the pointer after the call. | |
| let rc = unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) }; | |
| assert_eq!(rc, 0); | |
| let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| let res = get_user_instructions(&cfg).await; | |
| assert_eq!(res, None); | |
| let discovery = agents_md_paths(&cfg).await.expect("discover paths"); | |
| assert_eq!(discovery, Vec::<PathUri>::new()); | |
| } | |
| async fn override_directory_falls_back_to_agents_md_file() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::create_dir(tmp.path().join(LOCAL_AGENTS_MD_FILENAME)).unwrap(); | |
| fs::write(tmp.path().join(DEFAULT_AGENTS_MD_FILENAME), "primary").unwrap(); | |
| let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| let res = get_user_instructions(&cfg) | |
| .await | |
| .expect("AGENTS.md should be used when override is a directory"); | |
| assert_eq!(res, "primary"); | |
| let discovery = agents_md_paths(&cfg).await.expect("discover paths"); | |
| assert_eq!(discovery.len(), 1); | |
| assert_eq!( | |
| discovery[0].basename().as_deref(), | |
| Some(DEFAULT_AGENTS_MD_FILENAME) | |
| ); | |
| } | |
| async fn skills_are_not_appended_to_agents_md() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "base doc").unwrap(); | |
| let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| create_skill( | |
| cfg.codex_home.to_path_buf(), | |
| "pdf-processing", | |
| "extract from pdfs", | |
| ); | |
| let res = get_user_instructions(&cfg) | |
| .await | |
| .expect("instructions expected"); | |
| assert_eq!(res, "base doc"); | |
| } | |
| async fn apps_feature_does_not_emit_user_instructions_by_itself() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| let mut cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| cfg.features | |
| .enable(Feature::Apps) | |
| .expect("test config should allow apps"); | |
| let res = get_user_instructions(&cfg).await; | |
| assert_eq!(res, None); | |
| } | |
| async fn apps_feature_does_not_append_to_agents_md_user_instructions() { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| fs::write(tmp.path().join("AGENTS.md"), "base doc").unwrap(); | |
| let mut cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await; | |
| cfg.features | |
| .enable(Feature::Apps) | |
| .expect("test config should allow apps"); | |
| let res = get_user_instructions(&cfg) | |
| .await | |
| .expect("instructions expected"); | |
| assert_eq!(res, "base doc"); | |
| } | |
| fn create_skill(codex_home: PathBuf, name: &str, description: &str) { | |
| let skill_dir = codex_home.join(format!("skills/{name}")); | |
| fs::create_dir_all(&skill_dir).unwrap(); | |
| let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n"); | |
| fs::write(skill_dir.join("SKILL.md"), content).unwrap(); | |
| } | |