use std::sync::atomic::{AtomicUsize, Ordering};
use io_harness::provider::{CompletionRequest, CompletionResponse, ToolCall, Usage};
use io_harness::tools::workspace::Wrote;
use io_harness::tools::Workspace;
use io_harness::{
resume_with, rewind, run_with, ApproveAll, Policy, Provider, Rewind, Store, TaskContract,
Verification,
};
use serde_json::json;
struct Mock {
script: Vec<Vec<ToolCall>>,
at: AtomicUsize,
}
impl Mock {
fn new(script: Vec<Vec<ToolCall>>) -> Self {
Self {
script,
at: AtomicUsize::new(0),
}
}
}
impl Provider for Mock {
async fn complete(&self, _req: CompletionRequest) -> io_harness::Result<CompletionResponse> {
let i = self.at.fetch_add(1, Ordering::SeqCst);
Ok(CompletionResponse {
tool_calls: self.script.get(i).cloned().unwrap_or_default(),
usage: Some(Usage {
total_tokens: 1,
..Default::default()
}),
..Default::default()
})
}
fn name(&self) -> &str {
"mock"
}
}
fn call(name: &str, args: serde_json::Value) -> ToolCall {
ToolCall {
name: name.into(),
arguments: args,
}
}
fn open_policy() -> Policy {
Policy::default()
.layer("test")
.allow_read("*")
.allow_write("*")
.allow_exec("*")
}
fn never_passes(root: &std::path::Path, steps: u32) -> TaskContract {
TaskContract::workspace("exercise the snapshot", root)
.with_verification(Verification::WorkspaceFileContains {
file: "unreachable.txt".into(),
needle: "never".into(),
})
.with_max_steps(steps)
}
async fn drive(
dir: &std::path::Path,
store: &Store,
script: Vec<Vec<ToolCall>>,
) -> (Workspace, i64) {
let steps = script.len() as u32;
let contract = never_passes(dir, steps);
let provider = Mock::new(script);
let result = run_with(&contract, &provider, store, &open_policy(), &ApproveAll)
.await
.unwrap();
(Workspace::with_policy(dir, open_policy()), result.run_id)
}
fn write(dir: &std::path::Path, name: &str, bytes: &[u8]) {
std::fs::write(dir.join(name), bytes).unwrap();
}
fn bytes(dir: &std::path::Path, name: &str) -> Vec<u8> {
std::fs::read(dir.join(name)).unwrap()
}
#[tokio::test]
async fn a_file_the_run_rewrote_is_put_back_and_a_file_it_created_is_removed() {
let dir = tempfile::tempdir().unwrap();
const ORIGINAL: &[u8] = b"the original notes\nsecond line\n";
write(dir.path(), "notes.md", ORIGINAL);
let store = Store::memory().unwrap();
let (ws, run_id) = drive(
dir.path(),
&store,
vec![
vec![call(
"write_file",
json!({ "path": "notes.md", "content": "rewritten by the run\n" }),
)],
vec![call(
"write_file",
json!({ "path": "made.md", "content": "brand new\n" }),
)],
],
)
.await;
assert_eq!(bytes(dir.path(), "notes.md"), b"rewritten by the run\n");
assert!(dir.path().join("made.md").exists());
assert_eq!(
rewind(&ws, &store, run_id, "notes.md").unwrap(),
Rewind::Restored(Wrote::Changed)
);
assert_eq!(bytes(dir.path(), "notes.md"), ORIGINAL);
assert_eq!(
rewind(&ws, &store, run_id, "made.md").unwrap(),
Rewind::Removed
);
assert!(!dir.path().join("made.md").exists());
assert_eq!(
rewind(&ws, &store, run_id, "made.md").unwrap(),
Rewind::Removed
);
}
#[tokio::test]
async fn a_path_the_run_never_wrote_is_not_recorded_and_is_left_alone() {
let dir = tempfile::tempdir().unwrap();
const UNTOUCHED: &[u8] = b"nobody asked about this file\n";
write(dir.path(), "other.md", UNTOUCHED);
let store = Store::memory().unwrap();
let (ws, run_id) = drive(
dir.path(),
&store,
vec![vec![call(
"write_file",
json!({ "path": "notes.md", "content": "only this one\n" }),
)]],
)
.await;
assert_eq!(
rewind(&ws, &store, run_id, "other.md").unwrap(),
Rewind::NotRecorded
);
assert_eq!(bytes(dir.path(), "other.md"), UNTOUCHED);
}
#[tokio::test]
async fn a_second_edit_does_not_move_the_restore_point() {
let dir = tempfile::tempdir().unwrap();
const ORIGINAL: &[u8] = b"one\ntwo\nthree\n";
write(dir.path(), "notes.md", ORIGINAL);
let store = Store::memory().unwrap();
let (ws, run_id) = drive(
dir.path(),
&store,
vec![
vec![call(
"write_file",
json!({ "path": "notes.md", "content": "first rewrite\nkeep\n" }),
)],
vec![call(
"edit_file",
json!({ "path": "notes.md", "search": "first rewrite", "replace": "second rewrite" }),
)],
],
)
.await;
assert_eq!(bytes(dir.path(), "notes.md"), b"second rewrite\nkeep\n");
assert_eq!(
rewind(&ws, &store, run_id, "notes.md").unwrap(),
Rewind::Restored(Wrote::Changed)
);
assert_eq!(bytes(dir.path(), "notes.md"), ORIGINAL);
}
#[tokio::test]
async fn contents_that_were_not_kept_are_reported_and_the_file_is_never_truncated() {
let dir = tempfile::tempdir().unwrap();
const BINARY: &[u8] = &[0xff, 0xfe, 0x00, 0x01, 0xff];
write(dir.path(), "logo.bin", BINARY);
let huge = vec![b'a'; (1 << 20) + 1];
write(dir.path(), "huge.txt", &huge);
let store = Store::memory().unwrap();
let (ws, run_id) = drive(
dir.path(),
&store,
vec![
vec![call(
"write_file",
json!({ "path": "logo.bin", "content": "the run overwrote the binary\n" }),
)],
vec![call(
"write_file",
json!({ "path": "huge.txt", "content": "the run overwrote the big one\n" }),
)],
],
)
.await;
match rewind(&ws, &store, run_id, "logo.bin").unwrap() {
Rewind::NotKept(why) => assert!(why.contains("UTF-8"), "unhelpful reason: {why}"),
other => panic!("expected NotKept for a file that was not text, got {other:?}"),
}
match rewind(&ws, &store, run_id, "huge.txt").unwrap() {
Rewind::NotKept(why) => assert!(why.contains("cap"), "unhelpful reason: {why}"),
other => panic!("expected NotKept for a file over the cap, got {other:?}"),
}
assert_eq!(
bytes(dir.path(), "logo.bin"),
b"the run overwrote the binary\n"
);
assert_eq!(
bytes(dir.path(), "huge.txt"),
b"the run overwrote the big one\n"
);
assert!(!bytes(dir.path(), "logo.bin").is_empty());
}
#[tokio::test]
async fn a_restore_point_survives_the_process_that_wrote_it() {
let dir = tempfile::tempdir().unwrap();
let db = tempfile::tempdir().unwrap();
let db = db.path().join("runs.db");
const ORIGINAL: &[u8] = b"what was there before the crash\n";
write(dir.path(), "notes.md", ORIGINAL);
let run_id = {
let store = Store::open(&db).unwrap();
let (_, run_id) = drive(
dir.path(),
&store,
vec![vec![call(
"write_file",
json!({ "path": "notes.md", "content": "written just before the crash\n" }),
)]],
)
.await;
run_id
};
let store = Store::open(&db).unwrap();
let ws = Workspace::with_policy(dir.path(), open_policy());
assert_eq!(
rewind(&ws, &store, run_id, "notes.md").unwrap(),
Rewind::Restored(Wrote::Changed)
);
assert_eq!(bytes(dir.path(), "notes.md"), ORIGINAL);
}
#[tokio::test]
async fn a_resume_rewinds_under_the_run_id_the_snapshots_were_written_against() {
let dir = tempfile::tempdir().unwrap();
let db = tempfile::tempdir().unwrap();
let db = db.path().join("runs.db");
const ORIGINAL: &[u8] = b"the state the run started from\n";
write(dir.path(), "notes.md", ORIGINAL);
let script = vec![vec![call(
"write_file",
json!({ "path": "notes.md", "content": "the run's work\n" }),
)]];
let run_id = {
let store = Store::open(&db).unwrap();
let (_, run_id) = drive(dir.path(), &store, script.clone()).await;
run_id
};
let store = Store::open(&db).unwrap();
let contract = never_passes(dir.path(), 1);
let resumed = resume_with(
&contract,
&Mock::new(script),
&store,
run_id,
&open_policy(),
&ApproveAll,
)
.await
.unwrap();
assert_eq!(
resumed.run_id, run_id,
"a resume must continue the run the snapshots belong to, not start one"
);
let ws = Workspace::with_policy(dir.path(), open_policy());
assert_eq!(
rewind(&ws, &store, resumed.run_id, "notes.md").unwrap(),
Rewind::Restored(Wrote::Changed)
);
assert_eq!(bytes(dir.path(), "notes.md"), ORIGINAL);
}
#[tokio::test]
async fn another_runs_restore_point_is_not_reachable_from_this_run() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), "notes.md", b"the original\n");
let store = Store::memory().unwrap();
let (ws, first) = drive(
dir.path(),
&store,
vec![vec![call(
"write_file",
json!({ "path": "notes.md", "content": "what the first run wrote\n" }),
)]],
)
.await;
let second = store
.start_run("a later run", dir.path().to_str().unwrap())
.unwrap();
assert_ne!(second, first);
assert_eq!(
rewind(&ws, &store, second, "notes.md").unwrap(),
Rewind::NotRecorded
);
assert_eq!(bytes(dir.path(), "notes.md"), b"what the first run wrote\n");
}