use super::*;
use crate::PolicyCell;
use kernel::{Address, Mode, RunPolicy};
fn tool(root: &Path) -> EditTool {
let work = Address::parse("work").unwrap();
let domain = kernel::WriteDomain::new(vec![work.clone()]).unwrap();
std::fs::create_dir_all(root.join("work")).unwrap();
let policy = PolicyCell::new(RunPolicy::of(Mode::Work)).reader();
EditTool::new(root, work, domain, policy).unwrap()
}
fn call(path: &str, base: &str, old: &str, new: &str) -> ToolCall {
let mut args = Map::new();
for (k, v) in [
("path", path),
("base_version", base),
("old", old),
("new", new),
] {
args.insert(k.to_owned(), Value::String(v.to_owned()));
}
ToolCall {
id: "call-1".to_owned(),
name: ToolName::parse("edit").unwrap(),
args: Payload::new(args).unwrap(),
}
}
#[test]
fn an_edit_against_the_version_it_saw_lands_and_echoes_a_diff() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
std::fs::write(tmp.path().join("work/a.txt"), "one\ntwo\nthree\n").unwrap();
let version = version_of(b"one\ntwo\nthree\n");
let outcome = tool
.invoke(&call("work/a.txt", &version, "two", "TWO"))
.unwrap();
let result = serde_json::to_value(&outcome.result).unwrap();
assert_eq!(
std::fs::read_to_string(tmp.path().join("work/a.txt")).unwrap(),
"one\nTWO\nthree\n"
);
let diff = result["diff"].as_str().unwrap();
assert!(diff.contains("-two"), "{diff}");
assert!(diff.contains("+TWO"), "{diff}");
assert_eq!(
result["new_version"].as_str().unwrap(),
version_of(b"one\nTWO\nthree\n")
);
}
#[test]
fn a_file_that_moved_refuses_and_names_the_version_it_is_at_now() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
std::fs::write(tmp.path().join("work/a.txt"), "current\n").unwrap();
let err = match tool.invoke(&call("work/a.txt", "0000000000000000", "current", "next")) {
Err(err) => err,
Ok(_) => panic!("a stale base_version must refuse"),
};
assert_eq!(*err.code(), AxCode::VersionConflict);
assert!(err.subject().contains(&version_of(b"current\n")));
assert_eq!(
std::fs::read_to_string(tmp.path().join("work/a.txt")).unwrap(),
"current\n"
);
}
#[test]
fn base_version_new_creates_the_file_and_parents() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
let outcome = tool
.invoke(&call("work/room/notes.md", "new", "", "first line\n"))
.unwrap();
assert_eq!(
std::fs::read_to_string(tmp.path().join("work/room/notes.md")).unwrap(),
"first line\n"
);
let result = serde_json::to_value(&outcome.result).unwrap();
assert_eq!(
result["new_version"].as_str().unwrap(),
version_of(b"first line\n")
);
assert!(result["diff"].as_str().unwrap().contains("+first line"));
}
#[test]
fn creating_over_an_existing_file_is_a_version_conflict_naming_the_real_version() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
std::fs::write(tmp.path().join("work/a.txt"), "already\n").unwrap();
let err = match tool.invoke(&call("work/a.txt", "new", "", "other\n")) {
Err(err) => err,
Ok(_) => panic!("an existing file must refuse creation"),
};
assert_eq!(*err.code(), AxCode::VersionConflict);
assert!(err.subject().contains(&version_of(b"already\n")));
assert_eq!(
std::fs::read_to_string(tmp.path().join("work/a.txt")).unwrap(),
"already\n"
);
}
#[test]
fn creating_with_a_nonempty_old_is_refused_with_the_form_to_use() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
let err = match tool.invoke(&call("work/b.txt", "new", "something", "content")) {
Err(err) => err,
Ok(_) => panic!("a create replaces nothing"),
};
assert_eq!(*err.code(), AxCode::InvalidArgs);
assert!(err.recovery().contains("old:\"\""));
}
#[test]
fn a_path_outside_the_write_domain_is_refused_before_the_disk_is_touched() {
let tmp = tempfile::tempdir().unwrap();
let elsewhere = tempfile::tempdir().unwrap();
let beyond = elsewhere.path().join("abs.txt");
std::fs::write(tmp.path().join("outside.txt"), "x").unwrap();
let tool = tool(tmp.path());
for hostile in ["outside.txt", "elsewhere/f.md", ".sprawling/ledger/x"] {
let err = match tool.invoke(&call(hostile, "new", "", "y")) {
Err(err) => err,
Ok(_) => panic!("{hostile} must be refused"),
};
assert_eq!(*err.code(), AxCode::OutsideWriteDomain, "{hostile}");
let alternative = err
.gate()
.map(|gate| gate.alternative().to_owned())
.unwrap_or_default();
assert!(alternative.contains("work"), "{hostile}: {err}");
}
for illegal in ["../evil.txt", "work//x"] {
let err = match tool.invoke(&call(illegal, "new", "", "y")) {
Err(err) => err,
Ok(_) => panic!("{illegal} must be refused"),
};
assert_eq!(*err.code(), AxCode::InvalidArgs, "{illegal}");
}
let err = match tool.invoke(&call(beyond.to_str().unwrap(), "new", "", "y")) {
Err(err) => err,
Ok(_) => panic!("{} must be refused", beyond.display()),
};
assert_eq!(
(
err.code(),
err.recovery().contains("`exec`"),
beyond.exists()
),
(&AxCode::GateDenied, true, false),
"{err:?}"
);
assert_eq!(
std::fs::read_to_string(tmp.path().join("outside.txt")).unwrap(),
"x"
);
}
#[test]
fn a_missing_file_names_the_create_form_in_its_recovery() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
let version = version_of(b"whatever");
let err = match tool.invoke(&call("work/absent.md", &version, "a", "b")) {
Err(err) => err,
Ok(_) => panic!("a missing file must refuse a non-create edit"),
};
assert!(err.recovery().contains("base_version:\"new\""), "{err}");
}
#[test]
fn zero_or_many_matches_refuse_with_the_count() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
std::fs::write(tmp.path().join("work/a.txt"), "x\nx\n").unwrap();
let version = version_of(b"x\nx\n");
let err = match tool.invoke(&call("work/a.txt", &version, "x", "y")) {
Err(err) => err,
Ok(_) => panic!("an ambiguous match must refuse"),
};
assert_eq!(*err.code(), AxCode::InvalidArgs);
assert!(err.subject().contains("2 times"), "{}", err.subject());
let err = match tool.invoke(&call("work/a.txt", &version, "absent", "y")) {
Err(err) => err,
Ok(_) => panic!("a missing match must refuse"),
};
assert!(err.subject().contains("0 times"), "{}", err.subject());
}
#[test]
fn a_call_for_another_tool_is_refused_not_routed() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
let mut wrong = call("work/a.txt", "v", "a", "b");
wrong.name = ToolName::parse("exec").unwrap();
let err = match tool.invoke(&wrong) {
Err(err) => err,
Ok(_) => panic!("identity is fail-closed"),
};
assert_eq!(*err.code(), AxCode::InvalidArgs);
}
#[test]
fn a_documents_tool_writes_markdown_and_refuses_code_by_name() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("hall")).unwrap();
let hall = Address::parse("hall").unwrap();
let domain = kernel::WriteDomain::documents(vec![hall.clone()]).unwrap();
let tool = EditTool::new(
tmp.path(),
Address::parse("hall/mayor").unwrap(),
domain,
PolicyCell::new(RunPolicy::of(Mode::Work)).reader(),
)
.unwrap();
tool.invoke(&call(
"hall/note.md",
"new",
"",
"# note
",
))
.unwrap();
assert!(tmp.path().join("hall/note.md").is_file());
let refused = tool
.invoke(&call("hall/lex.rs", "new", "", "fn main() {}"))
.unwrap_err();
assert_eq!(*refused.code(), AxCode::OutsideWriteDomain);
assert!(
refused.subject().contains("hall/lex.rs"),
"the refusal names the file, not the room"
);
let plan = tool
.invoke(&call("hall/Roadmap.md", "new", "", "| 0 |"))
.unwrap_err();
assert_eq!(*plan.code(), AxCode::OutsideWriteDomain);
}
#[cfg(windows)]
fn place_link(_file: bool, from: &Path, to: &Path) -> bool {
std::process::Command::new("cmd")
.args([
"/c",
"mklink",
"/J",
&to.display().to_string(),
&from.display().to_string(),
])
.output()
.is_ok_and(|out| out.status.success())
}
#[cfg(not(windows))]
fn place_link(_file: bool, from: &Path, to: &Path) -> bool {
std::os::unix::fs::symlink(from, to).is_ok()
}
#[test]
fn an_edit_through_a_link_to_a_reserved_path_is_refused() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
std::fs::create_dir_all(tmp.path().join(".git").join("hooks")).unwrap();
let hook = tmp.path().join(".git").join("hooks").join("pre-run");
std::fs::write(&hook, b"hook-body").unwrap();
if !place_link(
true,
&tmp.path().join(".git").join("hooks"),
&tmp.path().join("work").join("alias-dir"),
) {
return;
}
let version = version_of(b"hook-body");
let refused = tool
.invoke(&call(
"work/alias-dir/pre-run",
&version,
"hook-body",
"escalated",
))
.unwrap_err();
assert_eq!(*refused.code(), AxCode::OutsideWriteDomain);
assert_eq!(
std::fs::read(&hook).unwrap(),
b"hook-body",
"the reserved file the link reaches is byte-identical"
);
}
#[test]
fn creating_below_a_link_leaves_the_link_target_untouched() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
let hooks = tmp.path().join(".git").join("hooks");
std::fs::create_dir_all(&hooks).unwrap();
if !place_link(true, &hooks, &tmp.path().join("work").join("alias-dir")) {
return;
}
let refused = tool
.invoke(&call("work/alias-dir/sub/x.md", CREATES, "", "body"))
.unwrap_err();
assert_eq!(*refused.code(), AxCode::OutsideWriteDomain);
assert_eq!(
std::fs::read_dir(&hooks).unwrap().count(),
0,
"no directory is made through the link"
);
}
#[test]
fn an_edit_through_a_hard_link_leaves_the_reserved_file_whole() {
let tmp = tempfile::tempdir().unwrap();
let tool = tool(tmp.path());
std::fs::create_dir_all(tmp.path().join(".git").join("hooks")).unwrap();
let hook = tmp.path().join(".git").join("hooks").join("pre-run");
std::fs::write(&hook, b"hook-body").unwrap();
if !std::fs::hard_link(&hook, tmp.path().join("work").join("a.txt")).is_ok() {
return;
}
let version = version_of(b"hook-body");
let outcome = tool.invoke(&call("work/a.txt", &version, "hook-body", "escalated"));
if outcome.is_ok() {
assert_eq!(
std::fs::read_to_string(tmp.path().join("work").join("a.txt")).unwrap(),
"escalated",
"the write lands at the name it was given"
);
}
assert_eq!(
std::fs::read(&hook).unwrap(),
b"hook-body",
"the reserved file behind the other name is byte-identical: the write never landed"
);
}
#[test]
fn a_city_placed_below_a_link_is_edited_like_any_other() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("real")).unwrap();
if !place_link(true, &tmp.path().join("real"), &tmp.path().join("placed")) {
return;
}
let city = tmp.path().join("placed");
let tool = tool(&city);
std::fs::write(city.join("work/a.txt"), "draft\n").unwrap();
let edited = tool.invoke(&call(
"work/a.txt",
&version_of(b"draft\n"),
"draft",
"final",
));
let created = tool.invoke(&call("work/b.txt", "new", "", "made\n"));
assert_eq!(
(
edited.map(|_| ()),
created.map(|_| ()),
std::fs::read_to_string(tmp.path().join("real/work/a.txt")).unwrap(),
std::fs::read_to_string(tmp.path().join("real/work/b.txt")).unwrap(),
),
(Ok(()), Ok(()), "final\n".to_owned(), "made\n".to_owned())
);
}