mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
e64b469bbc
## Why Workspace-specific `AGENTS.md` loading needs to use the selected environment filesystem so remote workspaces and child agents read instructions from their actual environment instead of the host filesystem. The app-server should report the same instruction sources the initialized thread actually loaded, rather than independently rescanning configuration and filesystem state. ## What changed - Introduce `LoadedAgentsMd` to retain ordered user, project, and internal instructions with their provenance. - Load and canonicalize workspace `AGENTS.md` paths through the primary `EnvironmentManager` environment, then render the loaded instructions when constructing turn context. - Expose cached loaded instruction sources from initialized threads and use them for app-server start, resume, and fork responses. - Preserve global `CODEX_HOME` loading and separator behavior while excluding empty project files that did not supply model-visible instructions. - Add integration coverage for CLI injection, selected-environment provenance and rendering, empty environment selection, and cached sources on loaded-thread resume. ## Validation - `just test -p codex-core agents_md` - `just test -p codex-core selected_environment_sources_match_model_visible_instructions` - `just test -p codex-exec agents_md` - `just test -p codex-app-server instruction_sources` - `just test -p codex-app-server --status-level fail`
195 lines
6.6 KiB
Rust
195 lines
6.6 KiB
Rust
use anyhow::Result;
|
|
use codex_exec_server::CreateDirectoryOptions;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
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 std::sync::Arc;
|
|
use tempfile::TempDir;
|
|
|
|
async fn agents_instructions(mut builder: TestCodexBuilder) -> Result<String> {
|
|
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 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
|
|
.fs()
|
|
.canonicalize(
|
|
&test.executor_environment().cwd().join("AGENTS.md"),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await?;
|
|
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(())
|
|
}
|