merge: main into P11c (P11b's dock and books homes, the macOS harness root fix)
One conflict, ledger-host commands.rs: main moved the agent-program lookup into find_agent_in; kept that and carried the Windows parts into it (.exe/.cmd/.bat, npm's global folder). The harness already used default_root, so main's fix and ours agree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
commit
84cbe0359c
23 files changed
+3398
-91
No files matched your search
@@ -0,0 +1,152 @@
|
||||
//! `wanderbooks connect` makes the book's books home (P11b, D-27): a folder in
|
||||
//! the account's Documents holding the agent's pointer and nothing of the
|
||||
//! books. Run with a home of the test's own: nothing here touches the account's
|
||||
//! real Documents or data root.
|
||||
|
||||
#![cfg(unix)]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
const KNOBS: &str =
|
||||
"close_after=2500,backoff=50,exe_poll=100,init_wait=5000,queue_wait=30000,backup_quiet=300";
|
||||
|
||||
struct Home {
|
||||
dir: tempfile::TempDir,
|
||||
}
|
||||
|
||||
impl Home {
|
||||
fn new() -> Self {
|
||||
let dir = tempfile::Builder::new()
|
||||
.prefix("wb-books-home-")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_wanderbooks"))
|
||||
.args(["setup", "--home"])
|
||||
.arg(dir.path())
|
||||
.env_remove("XDG_DATA_HOME")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
Self { dir }
|
||||
}
|
||||
|
||||
fn root(&self) -> PathBuf {
|
||||
self.dir.path().join(".local/share/wanderbooks")
|
||||
}
|
||||
|
||||
fn cmd(&self, mode: &str, path: &Path) -> Command {
|
||||
let mut c = Command::new(env!("CARGO_BIN_EXE_wanderbooks"));
|
||||
c.arg(mode)
|
||||
.arg("--root")
|
||||
.arg(self.root())
|
||||
.args(["--test-knobs", KNOBS])
|
||||
.env_clear()
|
||||
.env("PATH", format!("{}:/usr/bin:/bin", path.display()))
|
||||
.env("HOME", self.dir.path())
|
||||
.stdin(Stdio::null());
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
/// A stand-in `claude` that accepts whatever it is told.
|
||||
fn fake_agent(home: &Home) -> PathBuf {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let bin = home.dir.path().join("agent-bin");
|
||||
std::fs::create_dir_all(&bin).unwrap();
|
||||
let script = bin.join("claude");
|
||||
std::fs::write(&script, "#!/bin/sh\nexit 0\n").unwrap();
|
||||
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
bin
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_makes_the_books_home_and_it_holds_no_books_data() {
|
||||
let home = Home::new();
|
||||
let out = home
|
||||
.cmd("books", Path::new("/nonexistent"))
|
||||
.args(["create", "beta"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
let docs = home.dir.path().join("Documents/Wanderbooks/beta");
|
||||
|
||||
// An agent that isn't installed: said, and no folder made for nothing.
|
||||
let out = home
|
||||
.cmd("connect", Path::new("/nonexistent"))
|
||||
.args(["claude", "--book", "beta"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!out.status.success());
|
||||
assert!(!docs.exists(), "a home for an agent that isn't there");
|
||||
|
||||
// Connected: the user-scope entry as ever, and the books home beside it.
|
||||
let bin = fake_agent(&home);
|
||||
let out = home
|
||||
.cmd("connect", &bin)
|
||||
.args(["claude", "--book", "beta"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
let said = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(
|
||||
said.contains("Claude Code reaches the books beta"),
|
||||
"{said}"
|
||||
);
|
||||
assert!(
|
||||
said.contains(&format!("The books home is {}", docs.display())),
|
||||
"{said}"
|
||||
);
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
for dir in [docs.clone(), docs.join(".claude")] {
|
||||
for e in std::fs::read_dir(&dir).unwrap() {
|
||||
let p = e.unwrap().path();
|
||||
if p.is_file() {
|
||||
names.push(
|
||||
p.strip_prefix(&docs)
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
names.sort();
|
||||
assert_eq!(
|
||||
names,
|
||||
[
|
||||
".claude/settings.json",
|
||||
".mcp.json",
|
||||
"AGENTS.md",
|
||||
"CLAUDE.md"
|
||||
]
|
||||
);
|
||||
// Nothing in it names the data root, a token or a key.
|
||||
let root = home.root();
|
||||
for n in &names {
|
||||
let text = std::fs::read_to_string(docs.join(n)).unwrap();
|
||||
assert!(!text.contains(root.to_str().unwrap()), "{n}: {text}");
|
||||
assert!(!text.contains("wb_"), "{n} holds a token: {text}");
|
||||
assert!(!text.contains("--root"), "{n}: {text}");
|
||||
}
|
||||
let mcp: serde_json::Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(docs.join(".mcp.json")).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
mcp["mcpServers"]["wanderbooks-beta"]["args"],
|
||||
serde_json::json!(["mcp", "--book", "beta"])
|
||||
);
|
||||
// Run again: still four files, the rule once.
|
||||
let out = home
|
||||
.cmd("connect", &bin)
|
||||
.args(["claude", "--book", "beta"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
let settings: serde_json::Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(docs.join(".claude/settings.json")).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
settings["permissions"]["allow"],
|
||||
serde_json::json!(["mcp__wanderbooks-beta__*"])
|
||||
);
|
||||
}
|
||||
@@ -74,6 +74,10 @@ impl Home {
|
||||
}
|
||||
}
|
||||
|
||||
/// The root `setup --home` made: the platform's data folder under the
|
||||
/// home (`.local/share` on Linux, `Library/Application Support` on
|
||||
/// macOS), never a Linux path spelled out (the portable macOS job
|
||||
/// found the server refusing a root that was never set up).
|
||||
pub fn root(&self) -> PathBuf {
|
||||
ledger_store::root::default_root(self.dir.path())
|
||||
}
|
||||
|
||||
@@ -3497,11 +3497,14 @@ fn cpa_package_holds_the_year_and_its_evidence() {
|
||||
// and the em dashes of the depreciation memos: written differently, so
|
||||
// said, the memos grouped (four lines for one name and four memos), in
|
||||
// warnings and the README alike.
|
||||
// (The audit notice P8 adds while no counted audit stands behind the
|
||||
// package is a warning too; it is not about the IIF.)
|
||||
let warnings: Vec<String> = out["warnings"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|w| w.as_str().unwrap().to_string())
|
||||
.filter(|w| !w.starts_with("No counted audit"))
|
||||
.collect();
|
||||
assert_eq!(warnings.len(), 4, "{out}");
|
||||
let readme = std::str::from_utf8(&files["README.txt"]).unwrap();
|
||||
|
||||
Reference in new issue
Block a user