mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
## Why Repeated repository skill discovery performs remote metadata requests even when no `.agents/skills` directories exist. Reuse verified empty results to avoid those round trips while watching for changes that require a new scan. ## What changed - Add the Linux executor capability `fs_watch_missing`, the `fs/watchMissing` request, and `fs/missingChanged` notifications. - Register watches in the background and reuse empty results only after the executor acknowledges coverage. Key reuse by filesystem, working directory, and project root markers. - Invalidate observations on relevant filesystem or mount changes, watch supersession, or connection loss. Clear cached observations on forced reloads and cache resets. - Retain live discovery while registration is pending or watching is unsupported, unavailable, or cannot verify the observation. ## Testing Add tests for empty-result reuse and invalidation, skill directory creation before watch acknowledgement, unsupported executors, idle WebSocket notifications, reconnection, case aliases, and bind mounts and unmounts. Include an ignored manual latency comparison for repeated empty remote scans. GitOrigin-RevId: 3ad499decefb184a99c068afa01d0dcdd8605c7f
174 lines
5.2 KiB
Rust
174 lines
5.2 KiB
Rust
//! Exercises missing-path watches across mount changes on an idle executor WebSocket.
|
|
|
|
use super::common::exec_server::ExecServerHarness;
|
|
use super::common::exec_server::exec_server;
|
|
use super::discover_root;
|
|
use super::initialize;
|
|
use super::write_file;
|
|
use anyhow::Context as _;
|
|
use codex_exec_server_protocol::FS_MISSING_CHANGED_METHOD;
|
|
use codex_exec_server_protocol::FS_WATCH_MISSING_METHOD;
|
|
use codex_exec_server_protocol::JSONRPCMessage;
|
|
use codex_exec_server_protocol::JSONRPCNotification;
|
|
use codex_exec_server_protocol::JSONRPCResponse;
|
|
use codex_utils_path_uri::PathUri;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
const PARENT_NAMESPACE: &str = "CODEX_TEST_MISSING_WATCH_PARENT_MOUNT_NS";
|
|
|
|
#[test]
|
|
fn bind_mount_and_unmount_reveal_skills() -> anyhow::Result<()> {
|
|
// The parent removes the fixtures after the child's private mounts disappear.
|
|
let tmp = tempfile::tempdir()?;
|
|
let output = Command::new("unshare")
|
|
.args([
|
|
"--user",
|
|
"--map-root-user",
|
|
"--mount",
|
|
"--propagation",
|
|
"private",
|
|
"--",
|
|
])
|
|
.arg(std::env::current_exe()?)
|
|
.args([
|
|
"--exact",
|
|
"mount_watch::in_private_namespace",
|
|
"--ignored",
|
|
"--nocapture",
|
|
])
|
|
.env(PARENT_NAMESPACE, std::fs::read_link("/proc/self/ns/mnt")?)
|
|
.env("LC_ALL", "C")
|
|
.env("TMPDIR", tmp.path())
|
|
.output();
|
|
if let Err(error) = &output
|
|
&& error.kind() == std::io::ErrorKind::NotFound
|
|
{
|
|
eprintln!("skipping mount watch test: unshare unavailable");
|
|
return Ok(());
|
|
}
|
|
let output = output?;
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
if output.status.code() == Some(1)
|
|
&& stderr.starts_with("unshare:")
|
|
&& [
|
|
"Operation not permitted",
|
|
"Permission denied",
|
|
"Read-only file system",
|
|
"Function not implemented",
|
|
]
|
|
.iter()
|
|
.any(|reason| stderr.contains(reason))
|
|
{
|
|
eprintln!("skipping mount watch test: {stderr}");
|
|
return Ok(());
|
|
}
|
|
assert!(
|
|
output.status.success() && String::from_utf8_lossy(&output.stdout).contains("1 passed;"),
|
|
"{output:?}"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "run by bind_mount_and_unmount_reveal_skills in a private Linux mount namespace"]
|
|
async fn in_private_namespace() -> anyhow::Result<()> {
|
|
let parent = std::env::var_os(PARENT_NAMESPACE).context("use the namespace wrapper")?;
|
|
assert_ne!(std::fs::read_link("/proc/self/ns/mnt")?.as_os_str(), parent);
|
|
let fixture = tempfile::tempdir()?;
|
|
let empty = fixture.path().join("empty/.agents");
|
|
let populated = fixture.path().join("populated/.agents");
|
|
std::fs::create_dir_all(&empty)?;
|
|
write_file(
|
|
&populated.join("skills/mounted/SKILL.md"),
|
|
"---\nname: mounted\ndescription: Mount test\n---\n",
|
|
)?;
|
|
let mut server = exec_server().await?;
|
|
initialize(&mut server).await?;
|
|
|
|
assert_reveals_skills(
|
|
&mut server,
|
|
&empty,
|
|
/*watch_id*/ 1,
|
|
Command::new("mount")
|
|
.arg("--bind")
|
|
.arg(&populated)
|
|
.arg(&empty),
|
|
)
|
|
.await?;
|
|
run(Command::new("umount").arg(&empty))?;
|
|
|
|
run(Command::new("mount")
|
|
.arg("--bind")
|
|
.arg(&empty)
|
|
.arg(&populated))?;
|
|
assert_reveals_skills(
|
|
&mut server,
|
|
&populated,
|
|
/*watch_id*/ 2,
|
|
Command::new("umount").arg(&populated),
|
|
)
|
|
.await?;
|
|
server.shutdown().await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn assert_reveals_skills(
|
|
server: &mut ExecServerHarness,
|
|
agents: &Path,
|
|
watch_id: u64,
|
|
change: &mut Command,
|
|
) -> anyhow::Result<()> {
|
|
let root = PathUri::from_host_native_path(agents)?;
|
|
let before = discover_root(server, "repository", root.clone()).await?;
|
|
assert_eq!((before.error, before.skills), (None, vec![]));
|
|
|
|
let id = server
|
|
.send_request(
|
|
FS_WATCH_MISSING_METHOD,
|
|
serde_json::json!({ "watchId": watch_id, "missing": [root.join("skills")?] }),
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
server.next_event().await?,
|
|
JSONRPCMessage::Response(JSONRPCResponse {
|
|
id,
|
|
result: serde_json::json!({ "watching": true })
|
|
})
|
|
);
|
|
run(change)?;
|
|
assert_eq!(
|
|
server
|
|
.next_event()
|
|
.await
|
|
.context("expected a mount change on the idle socket")?,
|
|
JSONRPCMessage::Notification(JSONRPCNotification {
|
|
method: FS_MISSING_CHANGED_METHOD.to_string(),
|
|
params: Some(serde_json::json!({ "watchId": watch_id })),
|
|
})
|
|
);
|
|
|
|
let after = discover_root(server, "repository", root.clone()).await?;
|
|
let skills: Vec<_> = after
|
|
.skills
|
|
.into_iter()
|
|
.map(|skill| skill.instructions.path)
|
|
.collect();
|
|
assert_eq!(
|
|
(after.error, skills),
|
|
(None, vec![root.join("skills/mounted/SKILL.md")?])
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn run(command: &mut Command) -> anyhow::Result<()> {
|
|
let output = command.output()?;
|
|
anyhow::ensure!(
|
|
output.status.success(),
|
|
"{command:?}: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
Ok(())
|
|
}
|