feat: add in-memory executor filesystem

This commit is contained in:
starr-openai
2026-05-14 15:32:19 -07:00
parent d346957288
commit dc58274566
7 changed files with 1223 additions and 0 deletions
+44
View File
@@ -1,8 +1,10 @@
use super::*;
use crate::config::ConfigBuilder;
use codex_exec_server::InMemoryFileSystem;
use codex_exec_server::LOCAL_FS;
use codex_features::Feature;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::test_support::test_path_buf;
use core_test_support::PathBufExt;
use core_test_support::TempDirExt;
use pretty_assertions::assert_eq;
@@ -85,6 +87,24 @@ async fn make_config_with_project_root_markers(
config
}
async fn make_virtual_config(
cwd: AbsolutePathBuf,
limit: usize,
instructions: Option<&str>,
) -> Config {
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 = cwd;
config.project_doc_max_bytes = limit;
config.user_instructions = instructions.map(ToOwned::to_owned);
config
}
/// AGENTS.md missing – should yield `None`.
#[tokio::test]
async fn no_doc_file_returns_none() {
@@ -295,6 +315,30 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
assert_eq!(res, "parent doc\n\nchild doc");
}
#[tokio::test]
async fn injected_filesystem_supports_virtual_absolute_agents_paths() {
let fs = InMemoryFileSystem::new();
let repo_root =
AbsolutePathBuf::try_from(test_path_buf("/virtual/repo")).expect("absolute repo root");
let nested = AbsolutePathBuf::try_from(test_path_buf("/virtual/repo/workspace/crate_a"))
.expect("absolute nested path");
assert!(!repo_root.as_path().exists());
fs.seed_file(&repo_root.join(".git"), b"gitdir: fake\n".to_vec())
.expect("seed repo marker");
fs.seed_file(&repo_root.join("AGENTS.md"), b"root doc".to_vec())
.expect("seed root doc");
fs.seed_file(&nested.join("AGENTS.md"), b"crate doc".to_vec())
.expect("seed nested doc");
let cfg = make_virtual_config(nested, /*limit*/ 4096, /*instructions*/ None).await;
let res = AgentsMdManager::new(&cfg)
.user_instructions_with_fs(&fs)
.await
.expect("virtual doc expected");
assert_eq!(res, "root doc\n\ncrate doc");
}
#[tokio::test]
async fn instruction_sources_include_global_before_agents_md_docs() {
let tmp = tempfile::tempdir().expect("tempdir");
+1
View File
@@ -36,6 +36,7 @@ pub use codex_file_system::ExecutorFileSystem;
pub use codex_file_system::FileMetadata;
pub use codex_file_system::FileSystemResult;
pub use codex_file_system::FileSystemSandboxContext;
pub use codex_file_system::InMemoryFileSystem;
pub use codex_file_system::ReadDirectoryEntry;
pub use codex_file_system::RemoveOptions;
pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
+5
View File
@@ -3,4 +3,9 @@ load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "file-system",
crate_name = "codex_file_system",
deps_extra = [
"@crates//:pretty_assertions",
"@crates//:tempfile",
"@crates//:tokio",
],
)
+5
View File
@@ -13,6 +13,11 @@ codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
serde = { workspace = true, features = ["derive"] }
[dev-dependencies]
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt"] }
[lib]
test = false
doctest = false
+575
View File
@@ -0,0 +1,575 @@
use crate::CopyOptions;
use crate::CreateDirectoryOptions;
use crate::ExecutorFileSystem;
use crate::FileMetadata;
use crate::FileSystemResult;
use crate::FileSystemSandboxContext;
use crate::ReadDirectoryEntry;
use crate::RemoveOptions;
use async_trait::async_trait;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeMap;
use std::io;
use std::sync::RwLock;
#[derive(Clone, Debug)]
enum EntryKind {
Directory,
File(Vec<u8>),
}
#[derive(Clone, Debug)]
struct Entry {
kind: EntryKind,
created_at_ms: i64,
modified_at_ms: i64,
}
#[derive(Debug)]
struct State {
entries: BTreeMap<AbsolutePathBuf, Entry>,
next_timestamp_ms: i64,
}
impl State {
fn next_timestamp_ms(&mut self) -> i64 {
let timestamp = self.next_timestamp_ms;
self.next_timestamp_ms += 1;
timestamp
}
}
/// Pure in-memory implementation of [`ExecutorFileSystem`] for tests and
/// injected-fs call sites that must avoid host filesystem access.
#[derive(Debug, Default)]
pub struct InMemoryFileSystem {
state: RwLock<State>,
}
impl Default for State {
fn default() -> Self {
Self {
entries: BTreeMap::new(),
next_timestamp_ms: 1,
}
}
}
impl InMemoryFileSystem {
pub fn new() -> Self {
Self::default()
}
pub fn seed_directory(&self, path: &AbsolutePathBuf) -> io::Result<()> {
let mut state = self.state.write().expect("in-memory fs write lock");
create_directory_all(&mut state, path)
}
pub fn seed_file(
&self,
path: &AbsolutePathBuf,
contents: impl Into<Vec<u8>>,
) -> io::Result<()> {
let mut state = self.state.write().expect("in-memory fs write lock");
ensure_parent_directories(&mut state, path)?;
if is_directory(&state, path) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("path already exists: {}", path.display()),
));
}
let timestamp = state.next_timestamp_ms();
state.entries.insert(
path.clone(),
Entry {
kind: EntryKind::File(contents.into()),
created_at_ms: timestamp,
modified_at_ms: timestamp,
},
);
Ok(())
}
pub fn exists(&self, path: &AbsolutePathBuf) -> bool {
let state = self.state.read().expect("in-memory fs read lock");
entry_exists(&state, path)
}
pub fn file_contents(&self, path: &AbsolutePathBuf) -> Option<Vec<u8>> {
let state = self.state.read().expect("in-memory fs read lock");
match state.entries.get(path) {
Some(Entry {
kind: EntryKind::File(contents),
..
}) => Some(contents.clone()),
Some(Entry {
kind: EntryKind::Directory,
..
})
| None => None,
}
}
}
#[async_trait]
impl ExecutorFileSystem for InMemoryFileSystem {
async fn read_file(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
reject_sandbox_context(sandbox)?;
let state = self.state.read().expect("in-memory fs read lock");
match state.entries.get(path) {
Some(Entry {
kind: EntryKind::File(contents),
..
}) => Ok(contents.clone()),
Some(Entry {
kind: EntryKind::Directory,
..
}) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot read a directory as a file",
)),
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("path not found: {}", path.display()),
)),
}
}
async fn write_file(
&self,
path: &AbsolutePathBuf,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let mut state = self.state.write().expect("in-memory fs write lock");
ensure_file_parent_exists(&state, path)?;
if is_directory(&state, path) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot overwrite a directory with a file",
));
}
let timestamp = state.next_timestamp_ms();
let created_at_ms = state
.entries
.get(path)
.map(|entry| entry.created_at_ms)
.unwrap_or(timestamp);
state.entries.insert(
path.clone(),
Entry {
kind: EntryKind::File(contents),
created_at_ms,
modified_at_ms: timestamp,
},
);
Ok(())
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let mut state = self.state.write().expect("in-memory fs write lock");
if is_root(path) {
return if options.recursive {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("directory already exists: {}", path.display()),
))
};
}
if let Some(entry) = state.entries.get(path) {
return if matches!(entry.kind, EntryKind::Directory) && options.recursive {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("path already exists: {}", path.display()),
))
};
}
if options.recursive {
create_directory_all(&mut state, path)
} else {
ensure_directory_parent_exists(&state, path)?;
insert_directory(&mut state, path);
Ok(())
}
}
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
reject_sandbox_context(sandbox)?;
let state = self.state.read().expect("in-memory fs read lock");
if is_root(path) {
return Ok(directory_metadata(0, 0));
}
let entry = state.entries.get(path).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("path not found: {}", path.display()),
)
})?;
Ok(match &entry.kind {
EntryKind::Directory => directory_metadata(entry.created_at_ms, entry.modified_at_ms),
EntryKind::File(_) => FileMetadata {
is_directory: false,
is_file: true,
is_symlink: false,
created_at_ms: entry.created_at_ms,
modified_at_ms: entry.modified_at_ms,
},
})
}
async fn read_directory(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
reject_sandbox_context(sandbox)?;
let state = self.state.read().expect("in-memory fs read lock");
if !is_root(path) && !is_directory(&state, path) {
return if state.entries.contains_key(path) {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot read directory entries from a file",
))
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("path not found: {}", path.display()),
))
};
}
let mut entries = Vec::new();
for (entry_path, entry) in &state.entries {
if entry_path.parent().as_ref() != Some(path) {
continue;
}
entries.push(ReadDirectoryEntry {
file_name: file_name(entry_path)?,
is_directory: matches!(entry.kind, EntryKind::Directory),
is_file: matches!(entry.kind, EntryKind::File(_)),
});
}
Ok(entries)
}
async fn remove(
&self,
path: &AbsolutePathBuf,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
if is_root(path) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot remove the virtual filesystem root",
));
}
let mut state = self.state.write().expect("in-memory fs write lock");
let Some(entry) = state.entries.get(path).cloned() else {
return if options.force {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("path not found: {}", path.display()),
))
};
};
match entry.kind {
EntryKind::File(_) => {
state.entries.remove(path);
Ok(())
}
EntryKind::Directory => {
let has_children = state
.entries
.keys()
.any(|candidate| candidate.parent().as_ref() == Some(path));
if has_children && !options.recursive {
return Err(io::Error::new(
io::ErrorKind::DirectoryNotEmpty,
format!("directory is not empty: {}", path.display()),
));
}
state
.entries
.retain(|candidate, _| !candidate.starts_with(path));
Ok(())
}
}
}
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
if destination_path == source_path || destination_path.starts_with(source_path) {
let source_is_directory = {
let state = self.state.read().expect("in-memory fs read lock");
is_root(source_path) || is_directory(&state, source_path)
};
if source_is_directory {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"fs/copy cannot copy a directory to itself or one of its descendants",
));
}
}
let snapshot = {
let state = self.state.read().expect("in-memory fs read lock");
snapshot_entry(&state, source_path)?
};
let mut state = self.state.write().expect("in-memory fs write lock");
match snapshot {
SnapshotEntry::File { contents } => {
ensure_file_parent_exists(&state, destination_path)?;
if is_directory(&state, destination_path) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot copy a file over a directory",
));
}
let timestamp = state.next_timestamp_ms();
state.entries.insert(
destination_path.clone(),
Entry {
kind: EntryKind::File(contents),
created_at_ms: timestamp,
modified_at_ms: timestamp,
},
);
Ok(())
}
SnapshotEntry::Directory { entries } => {
if !options.recursive {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"fs/copy requires recursive: true when sourcePath is a directory",
));
}
create_directory_all(&mut state, destination_path)?;
for (source_entry, snapshot_entry) in entries {
let relative =
source_entry
.strip_prefix(source_path.as_path())
.map_err(|err| {
io::Error::other(format!("failed to strip prefix: {err}"))
})?;
let destination_entry = destination_path.join(relative);
match snapshot_entry {
SnapshotDirectoryEntry::Directory => {
create_directory_all(&mut state, &destination_entry)?;
}
SnapshotDirectoryEntry::File(contents) => {
ensure_parent_directories(&mut state, &destination_entry)?;
let timestamp = state.next_timestamp_ms();
state.entries.insert(
destination_entry,
Entry {
kind: EntryKind::File(contents),
created_at_ms: timestamp,
modified_at_ms: timestamp,
},
);
}
}
}
Ok(())
}
}
}
}
enum SnapshotEntry {
File {
contents: Vec<u8>,
},
Directory {
entries: Vec<(AbsolutePathBuf, SnapshotDirectoryEntry)>,
},
}
enum SnapshotDirectoryEntry {
Directory,
File(Vec<u8>),
}
fn snapshot_entry(state: &State, source_path: &AbsolutePathBuf) -> io::Result<SnapshotEntry> {
if is_root(source_path) || is_directory(state, source_path) {
let mut entries = Vec::new();
for (path, entry) in &state.entries {
if !path.starts_with(source_path) || path == source_path {
continue;
}
entries.push((
path.clone(),
match &entry.kind {
EntryKind::Directory => SnapshotDirectoryEntry::Directory,
EntryKind::File(contents) => SnapshotDirectoryEntry::File(contents.clone()),
},
));
}
return Ok(SnapshotEntry::Directory { entries });
}
match state.entries.get(source_path) {
Some(Entry {
kind: EntryKind::File(contents),
..
}) => Ok(SnapshotEntry::File {
contents: contents.clone(),
}),
Some(Entry {
kind: EntryKind::Directory,
..
}) => unreachable!("directory case handled above"),
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("path not found: {}", source_path.display()),
)),
}
}
fn create_directory_all(state: &mut State, path: &AbsolutePathBuf) -> io::Result<()> {
if is_root(path) {
return Ok(());
}
if let Some(parent) = path.parent() {
create_directory_all(state, &parent)?;
}
if let Some(entry) = state.entries.get(path) {
return if matches!(entry.kind, EntryKind::Directory) {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("path already exists: {}", path.display()),
))
};
}
insert_directory(state, path);
Ok(())
}
fn insert_directory(state: &mut State, path: &AbsolutePathBuf) {
let timestamp = state.next_timestamp_ms();
state.entries.insert(
path.clone(),
Entry {
kind: EntryKind::Directory,
created_at_ms: timestamp,
modified_at_ms: timestamp,
},
);
}
fn ensure_parent_directories(state: &mut State, path: &AbsolutePathBuf) -> io::Result<()> {
let Some(parent) = path.parent() else {
return Ok(());
};
create_directory_all(state, &parent)
}
fn ensure_file_parent_exists(state: &State, path: &AbsolutePathBuf) -> io::Result<()> {
ensure_directory_parent_exists(state, path)
}
fn ensure_directory_parent_exists(state: &State, path: &AbsolutePathBuf) -> io::Result<()> {
let Some(parent) = path.parent() else {
return Ok(());
};
if is_root(&parent) || is_directory(state, &parent) {
return Ok(());
}
if state.entries.contains_key(&parent) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("parent is not a directory: {}", parent.display()),
));
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("parent directory not found: {}", parent.display()),
))
}
fn directory_metadata(created_at_ms: i64, modified_at_ms: i64) -> FileMetadata {
FileMetadata {
is_directory: true,
is_file: false,
is_symlink: false,
created_at_ms,
modified_at_ms,
}
}
fn entry_exists(state: &State, path: &AbsolutePathBuf) -> bool {
is_root(path) || state.entries.contains_key(path)
}
fn is_directory(state: &State, path: &AbsolutePathBuf) -> bool {
is_root(path)
|| matches!(
state.entries.get(path),
Some(Entry {
kind: EntryKind::Directory,
..
})
)
}
fn is_root(path: &AbsolutePathBuf) -> bool {
path.parent().is_none()
}
fn file_name(path: &AbsolutePathBuf) -> io::Result<String> {
path.file_name()
.map(|name| name.to_string_lossy().into_owned())
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"path does not have a file name",
)
})
}
fn reject_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> {
if sandbox.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"in-memory filesystem operations do not accept sandbox context",
));
}
Ok(())
}
+4
View File
@@ -12,6 +12,10 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use std::io;
use std::path::Path;
mod in_memory;
pub use in_memory::InMemoryFileSystem;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CreateDirectoryOptions {
pub recursive: bool,
+589
View File
@@ -0,0 +1,589 @@
use codex_file_system::CopyOptions;
use codex_file_system::CreateDirectoryOptions;
use codex_file_system::ExecutorFileSystem;
use codex_file_system::FileSystemSandboxContext;
use codex_file_system::InMemoryFileSystem;
use codex_file_system::RemoveOptions;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::test_support::test_path_buf;
use pretty_assertions::assert_eq;
use std::io;
use std::path::PathBuf;
fn path(unix_path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(test_path_buf(unix_path)).expect("test path should be absolute")
}
fn path_buf(path: PathBuf) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(path).expect("path should be absolute")
}
fn sandbox_context() -> FileSystemSandboxContext {
FileSystemSandboxContext::from_permission_profile(PermissionProfile::default())
}
#[tokio::test]
async fn read_write_and_inspection_work_for_virtual_file_paths() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let root = path("/virtual/project");
let file = path("/virtual/project/notes.txt");
fs.seed_directory(&root)?;
assert!(fs.exists(&root));
fs.write_file(&file, b"hello".to_vec(), /*sandbox*/ None)
.await?;
assert_eq!(fs.read_file(&file, /*sandbox*/ None).await?, b"hello");
assert_eq!(fs.file_contents(&file), Some(b"hello".to_vec()));
assert!(fs.exists(&file));
Ok(())
}
#[tokio::test]
async fn reads_do_not_fall_back_to_host_files() -> io::Result<()> {
let tempdir = tempfile::tempdir()?;
let host_file = tempdir.path().join("host-only.txt");
std::fs::write(&host_file, "host contents")?;
let host_file = path_buf(host_file);
let err = InMemoryFileSystem::new()
.read_file(&host_file, /*sandbox*/ None)
.await
.expect_err("host-only file should be invisible");
assert_eq!(err.kind(), io::ErrorKind::NotFound);
Ok(())
}
#[tokio::test]
async fn root_paths_exist_without_explicit_seeding() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let root = path("/");
assert!(fs.exists(&root));
assert_eq!(
fs.get_metadata(&root, /*sandbox*/ None).await?,
codex_file_system::FileMetadata {
is_directory: true,
is_file: false,
is_symlink: false,
created_at_ms: 0,
modified_at_ms: 0,
}
);
assert_eq!(
fs.read_directory(&root, /*sandbox*/ None).await?,
Vec::new()
);
Ok(())
}
#[tokio::test]
async fn directory_metadata_and_read_directory_are_deterministic() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let dir = path("/virtual/project");
fs.seed_file(&path("/virtual/project/zeta.txt"), b"z".to_vec())?;
fs.seed_directory(&path("/virtual/project/alpha"))?;
fs.seed_file(&path("/virtual/project/middle.txt"), b"m".to_vec())?;
let metadata = fs.get_metadata(&dir, /*sandbox*/ None).await?;
assert_eq!(metadata.is_directory, true);
assert_eq!(metadata.is_file, false);
assert_eq!(metadata.is_symlink, false);
let entries = fs.read_directory(&dir, /*sandbox*/ None).await?;
assert_eq!(
entries,
vec![
codex_file_system::ReadDirectoryEntry {
file_name: "alpha".to_string(),
is_directory: true,
is_file: false,
},
codex_file_system::ReadDirectoryEntry {
file_name: "middle.txt".to_string(),
is_directory: false,
is_file: true,
},
codex_file_system::ReadDirectoryEntry {
file_name: "zeta.txt".to_string(),
is_directory: false,
is_file: true,
},
]
);
Ok(())
}
#[tokio::test]
async fn read_directory_only_returns_immediate_children() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let dir = path("/virtual/project");
fs.seed_file(&path("/virtual/project/nested/deep.txt"), b"deep".to_vec())?;
fs.seed_file(&path("/virtual/project/top.txt"), b"top".to_vec())?;
assert_eq!(
fs.read_directory(&dir, /*sandbox*/ None).await?,
vec![
codex_file_system::ReadDirectoryEntry {
file_name: "nested".to_string(),
is_directory: true,
is_file: false,
},
codex_file_system::ReadDirectoryEntry {
file_name: "top.txt".to_string(),
is_directory: false,
is_file: true,
},
]
);
Ok(())
}
#[tokio::test]
async fn create_directory_honors_recursive_behavior() {
let fs = InMemoryFileSystem::new();
let deep_dir = path("/virtual/project/a/b");
let err = fs
.create_directory(
&deep_dir,
CreateDirectoryOptions { recursive: false },
/*sandbox*/ None,
)
.await
.expect_err("non-recursive create should require existing parent");
assert_eq!(err.kind(), io::ErrorKind::NotFound);
fs.create_directory(
&deep_dir,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await
.expect("recursive create should build ancestors");
assert!(fs.exists(&path("/virtual/project/a")));
assert!(fs.exists(&deep_dir));
}
#[tokio::test]
async fn create_directory_handles_existing_entries() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let dir = path("/virtual/project");
fs.seed_directory(&dir)?;
fs.create_directory(
&dir,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
let err = fs
.create_directory(
&dir,
CreateDirectoryOptions { recursive: false },
/*sandbox*/ None,
)
.await
.expect_err("non-recursive create should reject existing directory");
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
fs.seed_file(&path("/virtual/project/file.txt"), b"file".to_vec())?;
let err = fs
.create_directory(
&path("/virtual/project/file.txt"),
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await
.expect_err("directory create should reject existing file");
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
Ok(())
}
#[tokio::test]
async fn write_file_requires_existing_parent_directory() {
let fs = InMemoryFileSystem::new();
let err = fs
.write_file(
&path("/virtual/project/missing/notes.txt"),
b"hello".to_vec(),
/*sandbox*/ None,
)
.await
.expect_err("write should not create arbitrary parent directories");
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[tokio::test]
async fn file_writes_preserve_created_at_and_update_modified_at() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let file = path("/virtual/project/notes.txt");
fs.seed_directory(&path("/virtual/project"))?;
fs.write_file(&file, b"first".to_vec(), /*sandbox*/ None)
.await?;
let first = fs.get_metadata(&file, /*sandbox*/ None).await?;
fs.write_file(&file, b"second".to_vec(), /*sandbox*/ None)
.await?;
let second = fs.get_metadata(&file, /*sandbox*/ None).await?;
assert_eq!(second.created_at_ms, first.created_at_ms);
assert!(second.modified_at_ms > first.modified_at_ms);
assert_eq!(fs.file_contents(&file), Some(b"second".to_vec()));
Ok(())
}
#[tokio::test]
async fn write_and_seed_file_reject_directory_targets() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let dir = path("/virtual/project");
fs.seed_directory(&dir)?;
let err = fs
.write_file(&dir, b"contents".to_vec(), /*sandbox*/ None)
.await
.expect_err("write should reject directory target");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let err = fs
.seed_file(&dir, b"contents".to_vec())
.expect_err("seed should reject directory target");
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
Ok(())
}
#[tokio::test]
async fn read_file_and_read_directory_reject_wrong_entry_kinds() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let dir = path("/virtual/project");
let file = path("/virtual/project/notes.txt");
fs.seed_file(&file, b"hello".to_vec())?;
let err = fs
.read_file(&dir, /*sandbox*/ None)
.await
.expect_err("reading a directory as a file should fail");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let err = fs
.read_directory(&file, /*sandbox*/ None)
.await
.expect_err("reading a file as a directory should fail");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
Ok(())
}
#[tokio::test]
async fn read_file_text_rejects_invalid_utf8() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let file = path("/virtual/project/bytes.bin");
fs.seed_file(&file, vec![0xff])?;
let err = fs
.read_file_text(&file, /*sandbox*/ None)
.await
.expect_err("invalid utf8 should fail");
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
Ok(())
}
#[tokio::test]
async fn remove_honors_recursive_and_force_behavior() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let dir = path("/virtual/project");
fs.seed_file(&path("/virtual/project/child.txt"), b"c".to_vec())?;
let err = fs
.remove(
&dir,
RemoveOptions {
recursive: false,
force: false,
},
/*sandbox*/ None,
)
.await
.expect_err("non-recursive remove should reject non-empty directories");
assert_eq!(err.kind(), io::ErrorKind::DirectoryNotEmpty);
fs.remove(
&dir,
RemoveOptions {
recursive: true,
force: false,
},
/*sandbox*/ None,
)
.await?;
assert!(!fs.exists(&dir));
fs.remove(
&path("/virtual/project/missing.txt"),
RemoveOptions {
recursive: false,
force: true,
},
/*sandbox*/ None,
)
.await?;
Ok(())
}
#[tokio::test]
async fn remove_supports_files_and_rejects_root() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let file = path("/virtual/project/file.txt");
fs.seed_file(&file, b"payload".to_vec())?;
fs.remove(
&file,
RemoveOptions {
recursive: false,
force: false,
},
/*sandbox*/ None,
)
.await?;
assert!(!fs.exists(&file));
let err = fs
.remove(
&path("/"),
RemoveOptions {
recursive: true,
force: false,
},
/*sandbox*/ None,
)
.await
.expect_err("root removal should fail");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
Ok(())
}
#[tokio::test]
async fn copy_supports_files_and_recursive_directories() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let source_dir = path("/virtual/source");
let source_file = path("/virtual/source/file.txt");
fs.seed_file(&source_file, b"payload".to_vec())?;
fs.seed_file(
&path("/virtual/source/nested/inner.txt"),
b"nested".to_vec(),
)?;
fs.copy(
&source_file,
&path("/virtual/copied.txt"),
CopyOptions { recursive: false },
/*sandbox*/ None,
)
.await?;
assert_eq!(
fs.read_file(&path("/virtual/copied.txt"), /*sandbox*/ None)
.await?,
b"payload"
);
let err = fs
.copy(
&source_dir,
&path("/virtual/dir-copy"),
CopyOptions { recursive: false },
/*sandbox*/ None,
)
.await
.expect_err("directory copy requires recursive");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
fs.copy(
&source_dir,
&path("/virtual/dir-copy"),
CopyOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
assert_eq!(
fs.read_file(
&path("/virtual/dir-copy/nested/inner.txt"),
/*sandbox*/ None
)
.await?,
b"nested"
);
let err = fs
.copy(
&source_dir,
&path("/virtual/source/descendant"),
CopyOptions { recursive: true },
/*sandbox*/ None,
)
.await
.expect_err("copying a directory into a descendant should fail");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
Ok(())
}
#[tokio::test]
async fn copy_handles_empty_directories_and_missing_sources() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let source_dir = path("/virtual/source");
let empty_dir = path("/virtual/source/empty");
fs.seed_directory(&empty_dir)?;
fs.copy(
&source_dir,
&path("/virtual/destination"),
CopyOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
assert!(fs.exists(&path("/virtual/destination/empty")));
let err = fs
.copy(
&path("/virtual/missing"),
&path("/virtual/destination/missing"),
CopyOptions { recursive: true },
/*sandbox*/ None,
)
.await
.expect_err("copy should reject missing source");
assert_eq!(err.kind(), io::ErrorKind::NotFound);
Ok(())
}
#[tokio::test]
async fn copy_rejects_file_over_directory() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let source_file = path("/virtual/source/file.txt");
let destination_dir = path("/virtual/destination");
fs.seed_file(&source_file, b"payload".to_vec())?;
fs.seed_directory(&destination_dir)?;
let err = fs
.copy(
&source_file,
&destination_dir,
CopyOptions { recursive: false },
/*sandbox*/ None,
)
.await
.expect_err("copy should reject file over directory");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
Ok(())
}
#[tokio::test]
async fn inspection_helpers_report_missing_paths_and_directories() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let dir = path("/virtual/project");
let missing = path("/virtual/missing");
fs.seed_directory(&dir)?;
assert!(fs.exists(&dir));
assert!(!fs.exists(&missing));
assert_eq!(fs.file_contents(&dir), None);
assert_eq!(fs.file_contents(&missing), None);
Ok(())
}
#[tokio::test]
async fn sandbox_context_is_rejected_for_all_operations() -> io::Result<()> {
let fs = InMemoryFileSystem::new();
let sandbox = sandbox_context();
let dir = path("/virtual/project");
let file = path("/virtual/project/file.txt");
fs.seed_file(&file, b"contents".to_vec())?;
let err = fs
.read_file(&file, Some(&sandbox))
.await
.expect_err("read_file should reject sandbox");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let err = fs
.write_file(&file, b"contents".to_vec(), Some(&sandbox))
.await
.expect_err("write_file should reject sandbox");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let err = fs
.create_directory(
&dir,
CreateDirectoryOptions { recursive: true },
Some(&sandbox),
)
.await
.expect_err("create_directory should reject sandbox");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let err = fs
.get_metadata(&dir, Some(&sandbox))
.await
.expect_err("get_metadata should reject sandbox");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let err = fs
.read_directory(&dir, Some(&sandbox))
.await
.expect_err("read_directory should reject sandbox");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let err = fs
.remove(
&file,
RemoveOptions {
recursive: false,
force: false,
},
Some(&sandbox),
)
.await
.expect_err("remove should reject sandbox");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let err = fs
.copy(
&file,
&path("/virtual/project/copy.txt"),
CopyOptions { recursive: false },
Some(&sandbox),
)
.await
.expect_err("copy should reject sandbox");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
Ok(())
}