Specify and plan M3b: the runner and the tools
A draft spec for the owner's review and 13 offline tasks with their given tests: shared tool arguments and host rules in proto, the sealed fetch target (M3a finding 14), the toolkit tools and SOCKS5 egress proxy, and brokerd's [runner], podman argument lists, runtime and proxy lifecycle. Each task's tests were run against a reference at that task's end state (560 to 638 tests, clippy clean); the reference is not in the repository. Adds the runner-unavailable runbook entry and tip T23 (ETXTBSY in script tests). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
//! `toolkit read_file` and `toolkit write_file`, run as `brokerd` runs them. Do not edit.
|
||||
|
||||
mod support;
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use proto::tools::{ReadFileArgs, WriteFileArgs};
|
||||
use support::{TempDir, json, toolkit};
|
||||
use toolkit::files::MAX_READ;
|
||||
|
||||
fn read(path: &str) -> support::Ran {
|
||||
toolkit(
|
||||
&["read_file"],
|
||||
&json(&ReadFileArgs {
|
||||
path: path.to_string(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn write(path: &str, content: &str) -> support::Ran {
|
||||
let args = WriteFileArgs {
|
||||
path: path.to_string(),
|
||||
content: content.to_string(),
|
||||
};
|
||||
toolkit(&["write_file"], &json(&args))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_is_read_exactly() {
|
||||
let dir = TempDir::new("read");
|
||||
let text = "line one\nline two, no newline at the end: ✓";
|
||||
std::fs::write(dir.at("a.md"), text).unwrap();
|
||||
let ran = read(&dir.at("a.md"));
|
||||
assert_eq!((ran.code, ran.stdout.as_str()), (0, text));
|
||||
assert_eq!(ran.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_file_is_empty_output() {
|
||||
let dir = TempDir::new("read-empty");
|
||||
std::fs::write(dir.at("e"), "").unwrap();
|
||||
let ran = read(&dir.at("e"));
|
||||
assert_eq!((ran.code, ran.stdout.as_str()), (0, ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_cannot_be_read_is_exit_1_with_one_line_for_the_model() {
|
||||
let dir = TempDir::new("read-bad");
|
||||
std::fs::write(dir.at("bin"), [0xff, 0xfe, 0x00]).unwrap();
|
||||
std::fs::write(dir.at("big"), vec![b'a'; MAX_READ + 1]).unwrap();
|
||||
std::fs::write(dir.at("exact"), vec![b'a'; MAX_READ]).unwrap();
|
||||
let cases = [
|
||||
(dir.at("missing"), "no such file"),
|
||||
(dir.at(""), "is a directory"),
|
||||
(dir.at("bin"), "not UTF-8 text"),
|
||||
(dir.at("big"), "larger than 1048576 bytes"),
|
||||
];
|
||||
for (path, why) in cases {
|
||||
let ran = read(&path);
|
||||
assert_eq!(ran.code, 1, "{path}");
|
||||
assert_eq!(ran.stdout, format!("read_file: {path}: {why}"));
|
||||
}
|
||||
let ran = read(&dir.at("exact"));
|
||||
assert_eq!(
|
||||
(ran.code, ran.stdout.len()),
|
||||
(0, MAX_READ),
|
||||
"exactly the limit is fine"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unreadable_file_names_the_error() {
|
||||
if is_root() {
|
||||
return; // root reads anything
|
||||
}
|
||||
let dir = TempDir::new("read-perm");
|
||||
std::fs::write(dir.at("secret"), "x").unwrap();
|
||||
std::fs::set_permissions(dir.at("secret"), std::fs::Permissions::from_mode(0o000)).unwrap();
|
||||
let ran = read(&dir.at("secret"));
|
||||
assert_eq!(ran.code, 1);
|
||||
assert!(
|
||||
ran.stdout
|
||||
.starts_with(&format!("read_file: {}: ", dir.at("secret"))),
|
||||
"{}",
|
||||
ran.stdout
|
||||
);
|
||||
assert!(ran.stdout.contains("ermission denied"), "{}", ran.stdout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_is_written_created_or_replaced() {
|
||||
let dir = TempDir::new("write");
|
||||
let ran = write(&dir.at("new.md"), "hello ✓\n");
|
||||
assert_eq!(ran.code, 0);
|
||||
assert_eq!(
|
||||
ran.stdout,
|
||||
format!("wrote 10 bytes to {}", dir.at("new.md"))
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(dir.at("new.md")).unwrap(),
|
||||
"hello ✓\n"
|
||||
);
|
||||
let ran = write(&dir.at("new.md"), "");
|
||||
assert_eq!(ran.stdout, format!("wrote 0 bytes to {}", dir.at("new.md")));
|
||||
assert_eq!(std::fs::read_to_string(dir.at("new.md")).unwrap(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_cannot_be_written_is_exit_1() {
|
||||
let dir = TempDir::new("write-bad");
|
||||
std::fs::create_dir(dir.at("sub")).unwrap();
|
||||
let cases = [
|
||||
(dir.at("nope/a.md"), "the directory does not exist"),
|
||||
(dir.at("sub"), "is a directory"),
|
||||
];
|
||||
for (path, why) in cases {
|
||||
let ran = write(&path, "x");
|
||||
assert_eq!(ran.code, 1, "{path}");
|
||||
assert_eq!(ran.stdout, format!("write_file: {path}: {why}"));
|
||||
}
|
||||
assert!(!dir.path().join("nope").exists(), "no directory is created");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn misuse_is_exit_2_with_nothing_on_standard_output() {
|
||||
let long = vec![b' '; toolkit::input::MAX_INPUT + 1];
|
||||
let cases: [(&[&str], &[u8], &str); 7] = [
|
||||
(
|
||||
&["read_file"],
|
||||
br#"{"path":"/a","mode":1}"#,
|
||||
"the arguments do not parse",
|
||||
),
|
||||
(&["read_file"], b"", "the arguments do not parse"),
|
||||
(
|
||||
&["write_file"],
|
||||
br#"{"path":"/a"}"#,
|
||||
"the arguments do not parse",
|
||||
),
|
||||
(&["read_file"], &[0xff, 0xfe], "not UTF-8"),
|
||||
(&["read_file"], &long, "larger than 2097152 bytes"),
|
||||
(&["format_disk"], b"{}", "unknown tool"),
|
||||
(&[], b"{}", "unknown tool"),
|
||||
];
|
||||
for (args, input, why) in cases {
|
||||
let ran = toolkit(args, input);
|
||||
assert_eq!(ran.code, 2, "{args:?}");
|
||||
assert_eq!(
|
||||
ran.stdout, "",
|
||||
"{args:?}: the model sees nothing of a misuse"
|
||||
);
|
||||
assert!(ran.stderr.contains(why), "{args:?}: {}", ran.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_root() -> bool {
|
||||
std::fs::read_to_string("/proc/self/status")
|
||||
.map(|s| s.lines().any(|l| l.starts_with("Uid:\t0\t")))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
Reference in New Issue
Block a user