Save and delete files in an aindrive drive without overwriting another change

A token that may write opens a writable folder. A save sends the file
whole through write_file with the revision it was read at, or "none" for
create-only, so aindrive refuses it as a conflict when the file moved on;
saves are capped at one 1 MiB transfer, which a table-driven encoder fits
into one call's fuel. A save whose answer is lost, or that the drive's
device timed out on, is reported with an unknown outcome. Deleting takes
one file; folders are refused because delete_path is recursive and
listings hide entries such as .git.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 23:01:34 +09:00
parent 7de6ea810c
commit f95c58f9e9
6 changed files with 615 additions and 54 deletions
+242 -3
View File
@@ -1,8 +1,11 @@
//! The provider against an in-memory aindrive. Live QA separately runs the
//! compiled component through Memona's host against the real service.
use super::{Aindrive, SESSION, download, test_host};
use crate::sdk::{EntryKind, ErrorCode, Guest, GuestReader, Locator, Setting};
use super::{Aindrive, SESSION, download, test_host, upload};
use crate::sdk::{
EntryKind, ErrorCode, Guest, GuestReader, GuestWriter, Locator, Setting,
WriteCondition,
};
use std::collections::BTreeMap;
const ENDPOINT: &str = "https://aindrive.ainetwork.ai/mcp/d/KQHGpl8hFXq8";
@@ -46,7 +49,9 @@ async fn opening_checks_the_token_against_the_drive_endpoint()
-> Result<(), ErrorCode> {
let drive = test_host::install(sample());
let capabilities = Aindrive::open("aindrive".to_owned(), config())?;
assert!(!capabilities.write && !capabilities.remove && !capabilities.watch);
assert!(capabilities.write && capabilities.remove);
assert!(capabilities.conditional_write);
assert!(!capabilities.mkdir && !capabilities.rename && !capabilities.watch);
let drive = drive.borrow();
let [seen] = drive.seen.as_slice() else {
return Err(ErrorCode::Internal);
@@ -285,3 +290,237 @@ async fn nothing_is_requested_before_a_connection_opens() {
assert_eq!(Aindrive::stat(at("/")).err(), Some(ErrorCode::Internal));
assert!(drive.borrow().seen.is_empty());
}
/// Saves `bytes` the way the host does: one push, then one finish.
fn save(
path: &str,
condition: WriteCondition,
bytes: &[u8],
) -> Result<Option<String>, ErrorCode> {
let writer = upload(&at(path), condition, Some(bytes.len() as u64))?;
writer.push(bytes.to_vec())?;
let info = writer.finish()?.ok_or(ErrorCode::Internal)?;
assert_eq!(info.size, bytes.len() as u64);
Ok(info.revision)
}
#[tokio::test]
async fn a_read_only_token_opens_a_read_only_folder() -> Result<(), ErrorCode> {
let _drive = test_host::install(test_host::Drive {
tools: Some(vec!["list_files", "read_file", "stat", "search"]),
..sample()
});
let capabilities = Aindrive::open("aindrive".to_owned(), config())?;
assert!(!capabilities.write && !capabilities.remove);
assert!(!capabilities.conditional_write);
Ok(())
}
#[tokio::test]
async fn a_save_on_the_revision_read_lands_and_reads_back()
-> Result<(), ErrorCode> {
let _drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
let (_, read) = read_from("/PAGE.md", 0, 4096)?;
let read = read.ok_or(ErrorCode::Internal)?;
let saved = save("/PAGE.md", WriteCondition::IfMatch(read), b"# Saved")?;
let (bytes, revision) = read_from("/PAGE.md", 0, 4096)?;
assert_eq!(bytes, b"# Saved");
assert_eq!(revision, saved);
assert_eq!(Aindrive::stat(at("/PAGE.md"))?.revision, saved);
Ok(())
}
#[tokio::test]
async fn a_save_on_a_stale_revision_is_a_conflict_and_changes_nothing()
-> Result<(), ErrorCode> {
let _drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
let stale = WriteCondition::IfMatch("m1-s8".to_owned());
assert_eq!(save("/PAGE.md", stale, b"lost"), Err(ErrorCode::Conflict));
let gone = WriteCondition::IfMatch("m1-s8".to_owned());
assert_eq!(save("/missing.md", gone, b"x"), Err(ErrorCode::Conflict));
assert_eq!(read_from("/PAGE.md", 0, 4096)?.0, b"# Studio");
Ok(())
}
#[tokio::test]
async fn create_only_refuses_anything_already_there() -> Result<(), ErrorCode> {
let _drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
for taken in ["/PAGE.md", "/notes"] {
assert_eq!(
save(taken, WriteCondition::CreateOnly, b"new"),
Err(ErrorCode::Conflict),
"{taken}"
);
}
assert!(
save("/outputs/new.md", WriteCondition::CreateOnly, b"new")?.is_some()
);
assert_eq!(read_from("/outputs/new.md", 0, 4096)?.0, b"new");
Ok(())
}
#[tokio::test]
async fn names_that_need_escaping_and_every_byte_survive_a_save()
-> Result<(), ErrorCode> {
let _drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
let bytes: Vec<u8> = (0..=255).rev().cycle().take(70_001).collect();
let path = "/notes/\"quoted\" 노트 {x}.bin";
save(path, WriteCondition::Overwrite, &bytes)?;
assert_eq!(read_from(path, 0, 1024 * 1024)?.0, bytes);
save("/empty.md", WriteCondition::Overwrite, b"")?;
assert_eq!(read_from("/empty.md", 0, 10)?.0, b"");
Ok(())
}
#[tokio::test]
async fn a_save_larger_than_one_transfer_is_refused_before_it_is_sent()
-> Result<(), ErrorCode> {
let drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
let over = Some(1024 * 1024 + 1);
assert_eq!(
upload(&at("/big.bin"), WriteCondition::Overwrite, over).err(),
Some(ErrorCode::Unsupported)
);
let writer = upload(&at("/big.bin"), WriteCondition::Overwrite, None)?;
writer.push(vec![1; 1024 * 1024])?;
assert_eq!(writer.push(vec![1]), Err(ErrorCode::Unsupported));
let tools: Vec<_> =
drive.borrow().seen.iter().map(|s| s.tool.clone()).collect();
assert_eq!(tools, [None]);
Ok(())
}
#[tokio::test]
async fn an_abandoned_or_finished_save_sends_nothing_more()
-> Result<(), ErrorCode> {
let drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
let abandoned = upload(&at("/PAGE.md"), WriteCondition::Overwrite, None)?;
abandoned.push(b"half".to_vec())?;
drop(abandoned);
let writer = upload(&at("/PAGE.md"), WriteCondition::Overwrite, None)?;
writer.push(b"whole".to_vec())?;
writer.finish()?;
assert_eq!(writer.finish().err(), Some(ErrorCode::InvalidInput));
assert_eq!(writer.push(b"more".to_vec()), Err(ErrorCode::InvalidInput));
let writes = drive
.borrow()
.seen
.iter()
.filter(|seen| seen.tool.as_deref() == Some("write_file"))
.count();
assert_eq!(writes, 1);
assert_eq!(read_from("/PAGE.md", 0, 4096)?.0, b"whole");
Ok(())
}
#[tokio::test]
async fn a_save_whose_answer_is_lost_has_an_unknown_outcome()
-> Result<(), ErrorCode> {
let _drive = test_host::install(test_host::Drive {
lose_reply: Some("write_file"),
..sample()
});
Aindrive::open("aindrive".to_owned(), config())?;
assert_eq!(
save("/PAGE.md", WriteCondition::Overwrite, b"landed"),
Err(ErrorCode::OutcomeUnknown)
);
// It did land, which is why a retry must look first.
assert_eq!(read_from("/PAGE.md", 0, 4096)?.0, b"landed");
Ok(())
}
#[tokio::test]
async fn a_device_that_goes_quiet_leaves_saves_unknown_and_reads_unavailable()
-> Result<(), ErrorCode> {
for tool in ["write_file", "stat"] {
let _drive = test_host::install(test_host::Drive {
fail: Some((tool, "[internal] agent timeout")),
..sample()
});
Aindrive::open("aindrive".to_owned(), config())?;
let expected = match tool {
"write_file" => ErrorCode::OutcomeUnknown,
_ => ErrorCode::Unavailable,
};
assert_eq!(
save("/PAGE.md", WriteCondition::Overwrite, b"x").err(),
(tool == "write_file").then_some(expected)
);
assert_eq!(
Aindrive::stat(at("/PAGE.md")).err(),
(tool == "stat").then_some(expected)
);
}
let _drive = test_host::install(test_host::Drive {
fail: Some(("write_file", "[internal] agent offline")),
..sample()
});
Aindrive::open("aindrive".to_owned(), config())?;
assert_eq!(
save("/PAGE.md", WriteCondition::Overwrite, b"x"),
Err(ErrorCode::Unavailable)
);
Ok(())
}
#[tokio::test]
async fn files_are_removed_and_folders_are_left_alone() -> Result<(), ErrorCode>
{
let drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
Aindrive::remove(at("/artworks.tsv"))?;
assert_eq!(
Aindrive::stat(at("/artworks.tsv")).err(),
Some(ErrorCode::NotFound)
);
for folder in ["/notes", "/outputs"] {
assert_eq!(
Aindrive::remove(at(folder)).err(),
Some(ErrorCode::Unsupported),
"{folder}"
);
}
assert!(drive.borrow().files.contains_key("notes/작가노트_2026.md"));
assert_eq!(
Aindrive::remove(at("/missing.md")).err(),
Some(ErrorCode::NotFound)
);
assert_eq!(
Aindrive::remove(at("/")).err(),
Some(ErrorCode::InvalidInput)
);
Ok(())
}
#[tokio::test]
async fn only_a_server_error_leaves_a_save_unknown() -> Result<(), ErrorCode> {
for (status, expected) in [
(502, ErrorCode::OutcomeUnknown),
(429, ErrorCode::Unavailable),
(403, ErrorCode::Denied),
] {
let drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
drive.borrow_mut().status = Some(status);
assert_eq!(
save("/PAGE.md", WriteCondition::Overwrite, b"x"),
Err(expected),
"{status}"
);
assert_eq!(
Aindrive::stat(at("/PAGE.md")).err(),
Some(match status {
502 | 429 => ErrorCode::Unavailable,
_ => expected,
})
);
}
Ok(())
}