P2 slice 5: files in and out

Files reach the books through the server: uploads by hash and size,
then filed; exports (the CPA package, the 1099 worksheet, W-9s) handed
over as downloads from the server, never read off disk by the agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-20 11:14:21 -06:00
1 parent d1f3ad891a
commit b26c099a68
44 files changed
+5075 -2739

No files matched your search

+127 -52
View File
@@ -72,7 +72,7 @@ impl Drop for Mcp {
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
/// A test's dir and the folders beside it (`-inbox`, `-outside`),
/// A test's dir and the folder beside it (`-outside`),
/// removed when the test ends, pass or panic.
struct TestDir(PathBuf);
@@ -92,11 +92,7 @@ impl AsRef<std::path::Path> for TestDir {
impl Drop for TestDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
for beside in ["-inbox", "-outside"] {
let p = PathBuf::from(format!("{}{beside}", self.0.display()));
let _ = std::fs::remove_dir_all(&p);
let _ = std::fs::remove_file(&p);
}
let _ = std::fs::remove_dir_all(format!("{}-outside", self.0.display()));
}
}
@@ -119,12 +115,15 @@ fn outside(dir: &Path) -> PathBuf {
d
}
/// The server's import root, beside the books dir `dir`: W-9 files handed
/// to books.set_w9 by path. Never where the server writes TINs.
fn inbox(dir: &Path) -> PathBuf {
let d = PathBuf::from(format!("{}-inbox", dir.display()));
std::fs::create_dir_all(&d).unwrap();
d
/// `bytes` as a tool takes them inline: base64.
fn b64(bytes: &[u8]) -> String {
use base64::Engine as _;
base64::engine::general_purpose::STANDARD.encode(bytes)
}
/// The SHA-256 (hex) a file given inline comes with.
fn sha(bytes: &[u8]) -> String {
ledger_store::journal::sha256_hex(bytes)
}
/// `wanderbooks.toml` with a company name, the receipt threshold, and
@@ -169,9 +168,6 @@ impl Mcp {
.into_iter()
.flatten()
})
// Files handed over by path come only from here.
.arg("--import-root")
.arg(inbox(dir))
.env("XDG_RUNTIME_DIR", &run)
// Never the user's real vault key (the default key file is
// under XDG_CONFIG_HOME); the tests name theirs anyway.
@@ -519,11 +515,9 @@ impl Mcp {
}
self.tool("books.set_w9", args)
}
/// A W-9 file outside the books; it shows the TIN, like the real one.
fn w9_file(&self, name: &str, tin: &str) -> PathBuf {
let p = inbox(&self.books_dir()).join(name);
std::fs::write(&p, format!("%PDF-1.4 Form W-9 TIN {tin}")).unwrap();
p
/// A W-9 file (a PDF); it shows the TIN, like the real one.
fn w9_file(&self, tin: &str) -> Vec<u8> {
format!("%PDF-1.4 Form W-9 TIN {tin}").into_bytes()
}
/// Pay `vendor` `amount` from the bank in 2026, tagged.
fn pay(&mut self, vendor: &str, amount: &str) {
@@ -626,9 +620,9 @@ fn set_w9_refusals_write_nothing_anywhere() {
let mut m = books("refusals");
let acme = m.party_id("Acme Design");
m.add_party("IRS", &["tax_agency"]);
let missing = inbox(&m.books_dir()).join("no-such-w9.pdf");
let txt = inbox(&m.books_dir()).join("w9.txt");
std::fs::write(&txt, "W-9").unwrap();
let pdf = m.w9_file(SSN);
let txt = b"W-9";
let big = [b"%PDF-1.4 ".as_slice(), &vec![b' '; 256 * 1024]].concat();
for (party, extra) in [
("Acme Design", json!({"tin": "57193284", "tin_type": "ssn"})), // eight digits
(
@@ -653,14 +647,6 @@ fn set_w9_refusals_write_nothing_anywhere() {
"Acme Design",
json!({"tin": 291122846.0, "tin_type": "ssn"}),
), // a float
(
"Acme Design",
json!({"tin": SSN, "tin_type": "ssn", "w9_file_path": missing.to_str().unwrap()}),
),
(
"Acme Design",
json!({"tin": SSN, "tin_type": "ssn", "w9_file_path": txt.to_str().unwrap()}),
),
(
"Acme Design",
json!({"tin": SSN, "tin_type": "ssn", "clear": true}),
@@ -680,6 +666,51 @@ fn set_w9_refusals_write_nothing_anywhere() {
out.err_text()
);
}
// The W-9 file, each refused for its own reason.
let long = "!".repeat((256 * 1024usize).div_ceil(3) * 4 + 4);
for (extra, why) in [
(
json!({"w9_content_base64": b64(txt), "w9_sha256": sha(txt)}),
"a PDF, PNG, or JPEG",
),
(
json!({"w9_content_base64": b64(&pdf)}),
"comes with its sha256",
),
(
json!({"w9_content_base64": b64(&pdf), "w9_sha256": sha(txt)}),
"lost on the way",
),
(
json!({"w9_content_base64": "!!not base64", "w9_sha256": sha(&pdf)}),
"not valid base64",
),
(
json!({"w9_content_base64": b64(&big), "w9_sha256": sha(&big)}),
"upload it with books.begin_upload",
),
(
json!({"w9_content_base64": long, "w9_sha256": sha(&big)}),
"inline is at most",
),
(json!({"w9_upload_id": "u1"}), "takes files inline only"),
(
json!({"w9_upload_id": "u1", "w9_content_base64": b64(&pdf), "w9_sha256": sha(&pdf)}),
"not both",
),
(
json!({"w9_content_base64": b64(&pdf), "w9_sha256": sha(&pdf), "w9_file_name": "../w9.pdf"}),
"no folder",
),
] {
let mut args = json!({"tin": SSN, "tin_type": "ssn"});
for (k, v) in extra.as_object().unwrap() {
args[k] = v.clone();
}
let why_given = m.w9("Acme Design", args).err_text();
assert!(why_given.contains(why), "{extra}: {why_given}");
assert!(!holds_tin(why_given.as_bytes(), SSN_DIGITS), "{why_given}");
}
assert!(
!m.vault().join(format!("tin-{acme}.bin")).exists(),
"no record written"
@@ -739,10 +770,10 @@ fn the_facts_never_disagree_with_the_vault() {
fn the_w9_file_goes_to_the_vault_never_the_documents() {
let mut m = books("w9-file");
let acme = m.party_id("Acme Design");
let src = m.w9_file("acme-w9.pdf", SSN);
let src = m.w9_file(SSN);
let Out::Ok(p) = m.w9(
"Acme Design",
json!({"tin": SSN, "tin_type": "ssn", "w9_file_path": src.to_str().unwrap()}),
json!({"tin": SSN, "tin_type": "ssn", "w9_content_base64": b64(&src), "w9_sha256": sha(&src)}),
) else {
panic!("set_w9 with a file")
};
@@ -760,8 +791,9 @@ fn the_w9_file_goes_to_the_vault_never_the_documents() {
json!({"party": "Acme Design", "save_to": to.to_str().unwrap()}),
);
assert_eq!(out["path"], json!(to.display().to_string()));
assert_eq!(out["file_name"], "acme-w9.pdf");
assert_eq!(std::fs::read(&to).unwrap(), std::fs::read(&src).unwrap());
// Given inline, the file is named for its type.
assert_eq!(out["file_name"], "w9.pdf");
assert_eq!(std::fs::read(&to).unwrap(), src);
assert!(!holds_tin(out.to_string().as_bytes(), SSN_DIGITS));
#[cfg(unix)]
{
@@ -804,10 +836,10 @@ fn the_w9_file_goes_to_the_vault_never_the_documents() {
);
// A W-9 file alone (the TIN typed in later) records the vault too.
m.add_party("Solo", &["contractor"]);
let solo = m.w9_file("solo.png", "000");
let solo = m.w9_file("000");
let Out::Ok(p) = m.w9(
"Solo",
json!({"tin_last4": "0000", "w9_file_path": solo.to_str().unwrap()}),
json!({"tin_last4": "0000", "w9_content_base64": b64(&solo), "w9_sha256": sha(&solo)}),
) else {
panic!("a W-9 file alone")
};
@@ -815,22 +847,25 @@ fn the_w9_file_goes_to_the_vault_never_the_documents() {
assert_eq!(p["w9"]["kept_at"], "wanderbooks vault");
}
/// A W-9 file is a PDF, PNG or JPEG, by its extension in any case
/// (.pdf, .png, .jpg, .jpeg): each is taken into the vault and comes back
/// byte for byte, under its own name.
/// A W-9 file is a PDF, PNG or JPEG, judged by its own first bytes: each
/// is taken into the vault and comes back byte for byte, named for its
/// type.
#[test]
fn every_w9_file_type_is_taken() {
let mut m = books("w9-types");
for (n, name) in ["w9.pdf", "w9.png", "w9.jpg", "w9.jpeg", "W9.JPG"]
.iter()
.enumerate()
for (n, (bytes, name)) in [
(b"%PDF-1.4 Form W-9".as_slice(), "w9.pdf"),
(b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR W-9".as_slice(), "w9.png"),
(b"\xff\xd8\xff\xe0\0\x10JFIF W-9".as_slice(), "w9.jpg"),
]
.iter()
.enumerate()
{
let party = format!("Payee {n}");
m.add_party(&party, &["contractor"]);
let src = m.w9_file(name, "000");
let Out::Ok(p) = m.w9(
&party,
json!({"tin_last4": "0000", "w9_file_path": src.to_str().unwrap()}),
json!({"tin_last4": "0000", "w9_content_base64": b64(bytes), "w9_sha256": sha(bytes)}),
) else {
panic!("{name} refused")
};
@@ -841,17 +876,57 @@ fn every_w9_file_type_is_taken() {
json!({"party": party, "save_to": to.to_str().unwrap()}),
);
assert_eq!(out["file_name"], *name);
assert_eq!(std::fs::read(&to).unwrap(), std::fs::read(&src).unwrap());
assert_eq!(std::fs::read(&to).unwrap(), *bytes);
}
// Its own name, when given; given alone, a name is refused.
m.add_party("Payee N", &["contractor"]);
let bytes = b"%PDF-1.4 Form W-9 named";
let Out::Ok(_) = m.w9(
"Payee N",
json!({"tin_last4": "0000", "w9_content_base64": b64(bytes), "w9_sha256": sha(bytes),
"w9_file_name": "payee-n-w9.pdf"}),
) else {
panic!("named W-9 refused")
};
let to = m.outside().join("copy-named");
let out = m.ok(
"books.get_w9",
json!({"party": "Payee N", "save_to": to.to_str().unwrap()}),
);
assert_eq!(out["file_name"], "payee-n-w9.pdf");
let why = m
.w9(
"Payee N",
json!({"tin_last4": "0000", "w9_file_name": "x.pdf"}),
)
.err_text();
assert!(why.contains("w9_file_name names the W-9"), "{why}");
// Anything else is refused, near misses too.
m.add_party("Payee X", &["contractor"]);
for bytes in [
b"PDF-1.4 no percent".as_slice(),
b"\x89PNG\r\n no".as_slice(),
b"\xff\xd8 short".as_slice(),
b"GIF89a".as_slice(),
] {
let why = m
.w9(
"Payee X",
json!({"tin_last4": "0000", "w9_content_base64": b64(bytes), "w9_sha256": sha(bytes)}),
)
.err_text();
assert!(why.contains("PDF, PNG, or JPEG"), "{why}");
}
assert_eq!(m.party("Payee X", 2026)["party"]["w9"], Value::Null);
}
#[test]
fn a_locked_vault_says_so_and_never_guesses() {
let mut m = books("locked");
let src = m.w9_file("w9.pdf", SSN);
let src = m.w9_file(SSN);
m.w9(
"Acme Design",
json!({"tin": SSN, "tin_type": "ssn", "w9_file_path": src.to_str().unwrap()}),
json!({"tin": SSN, "tin_type": "ssn", "w9_content_base64": b64(&src), "w9_sha256": sha(&src)}),
);
m.pay("Acme Design", "700.00");
let key = std::fs::read(m.key()).unwrap();
@@ -1003,10 +1078,10 @@ fn the_1099_worksheet_carries_full_tins_only_when_asked() {
fn purge_deletes_the_vault_records_and_journals_why() {
let mut m = books("purge");
let acme = m.party_id("Acme Design");
let src = m.w9_file("w9.pdf", SSN);
let src = m.w9_file(SSN);
m.w9(
"Acme Design",
json!({"tin": SSN, "tin_type": "ssn", "w9_file_path": src.to_str().unwrap()}),
json!({"tin": SSN, "tin_type": "ssn", "w9_content_base64": b64(&src), "w9_sha256": sha(&src)}),
);
// Clearing while the vault holds it: refused.
let why = m
@@ -1086,10 +1161,10 @@ fn no_full_tin_leaves_the_vault_except_the_worksheet_when_asked() {
"no-leak",
"[[company.members]]\nname = \"Mo\"\nshare = \"100%\"\ncapital_account = \"3000\"\n",
);
let src = m.w9_file("acme-w9.pdf", SSN);
let src = m.w9_file(SSN);
m.w9(
"Acme Design",
json!({"tin": SSN, "tin_type": "ssn", "w9_file_path": src.to_str().unwrap()}),
json!({"tin": SSN, "tin_type": "ssn", "w9_content_base64": b64(&src), "w9_sha256": sha(&src)}),
);
m.w9("Rent Co", json!({"tin": EIN, "tin_type": "ein"}));
m.ok("books.add_vendor", json!({"name": "Rent Co"}));