P2 slice 3: opening and closing the books (plan 3.9, D-21, D-23)

One wanderbooks executable. The books are open while the viewer, an
agent session or a terminal holds them: a host process takes leases on
a socket, starts the server (the hidden mode `wanderbooks server`) with
a pipe on its input, and closes it 10 s after the last lease ends. No
background service. One host even in a race (run/host.lock).

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:19 -06:00
1 parent 168e5c9846
commit a298d154b6
31 files changed
+11937 -112

No files matched your search

+186 -1
View File
@@ -316,6 +316,8 @@ fn adopted_books_are_the_same_books_and_the_source_is_untouched() {
settings: None,
vault_dir: Some(vault.clone()),
vault_key: Some(key.clone()),
// As a preview read it: the same head, so it goes ahead.
expect_head: Some(source_head.clone()),
},
)
.unwrap();
@@ -724,8 +726,16 @@ fn adoption_leaves_a_read_only_source_as_it_was() {
drop(source_books(old.path(), true));
let before = snapshot_files(old.path());
chmod_tree(old.path(), 0o500, 0o400);
// Writable again however the test ends, so its folder can go.
struct Writable<'a>(&'a Path);
impl Drop for Writable<'_> {
fn drop(&mut self) {
chmod_tree(self.0, 0o700, 0o600);
}
}
let writable = Writable(old.path());
let report = root.adopt(old.path(), &id("acme"), &AdoptOptions::default());
chmod_tree(old.path(), 0o700, 0o600);
drop(writable);
let report = report.unwrap();
assert!(!report.source_lock_created);
assert_eq!(snapshot_files(old.path()), before, "the lock file included");
@@ -1099,3 +1109,178 @@ fn errors_other_than_not_found_are_never_taken_for_it() {
std::fs::set_permissions(&lock, std::fs::Permissions::from_mode(0o600)).unwrap();
assert!(root.list_books().unwrap().is_empty());
}
#[test]
fn an_adoption_preview_reads_the_source_and_writes_nothing() {
let home = tmp("preview");
let (root, _) = Root::setup(home.path(), None, None).unwrap();
let old = tmp("preview-src");
let s = source_books(old.path(), false);
let head = s.head();
drop(s);
// No lock file beside the journal: the preview must not make one.
let _ = std::fs::remove_file(old.path().join("wanderbooks.lock"));
let before = snapshot_files(old.path());
let found = root.adopt_preview(old.path()).unwrap();
assert_eq!(found.head, head);
assert_eq!(found.currency, Currency::USD);
assert!(!found.keeps_settings);
assert_eq!(
snapshot_files(old.path()),
before,
"the preview wrote to the source"
);
assert!(root.list_books().unwrap().is_empty());
// Books that keep their own settings say so.
let kept = tmp("preview-kept");
drop(source_books(kept.path(), true));
assert!(root.adopt_preview(kept.path()).unwrap().keeps_settings);
// The preview refuses what adoption refuses, naming no root.
let book = root
.create_book(&BookId::parse("acme").unwrap(), Currency::USD)
.unwrap();
drop(book);
let root_text = root.path().display().to_string();
for bad in [
root.book_dir(&BookId::parse("acme").unwrap()),
root.path().to_path_buf(),
home.path().join("missing"),
home.path().to_path_buf(),
] {
let e = root.adopt_preview(&bad).unwrap_err();
assert!(matches!(e, RootError::Adopt(_)), "{bad:?}: {e:?}");
assert!(!e.to_string().contains(&root_text), "{e}");
}
// A cut-off last line is refused, as adoption refuses it.
let cut = tmp("preview-cut");
drop(source_books(cut.path(), false));
let journal = cut.path().join(JOURNAL_FILE);
let mut raw = std::fs::read(&journal).unwrap();
raw.pop();
std::fs::write(&journal, raw).unwrap();
assert!(
matches!(root.adopt_preview(cut.path()), Err(RootError::Adopt(why)) if why.contains("cut off"))
);
}
#[test]
fn a_book_folder_is_made_private_and_a_shared_one_refused() {
let home = tmp("bookfolder");
let (root, _) = Root::setup(home.path(), None, None).unwrap();
drop(
root.create_book(&BookId::parse("acme").unwrap(), Currency::USD)
.unwrap(),
);
let book = root.book_dir(&BookId::parse("acme").unwrap());
let keys = ledger_store::root::private_book_folder(&book, "keys").unwrap();
assert_eq!(keys, book.join("keys"));
assert_eq!(mode(&keys), 0o700);
// Again: already there, fine.
assert_eq!(
ledger_store::root::private_book_folder(&book, "keys").unwrap(),
keys
);
// Ours, but others could enter it: made private again.
std::fs::set_permissions(&keys, std::fs::Permissions::from_mode(0o755)).unwrap();
ledger_store::root::private_book_folder(&book, "keys").unwrap();
assert_eq!(mode(&keys), 0o700);
std::os::unix::fs::symlink(home.path(), book.join("vault")).unwrap();
assert!(ledger_store::root::private_book_folder(&book, "vault").is_err());
}
#[test]
fn the_run_folder_is_private() {
let home = tmp("run");
let (root, _) = Root::setup(home.path(), None, None).unwrap();
let run = root.run_dir().unwrap();
assert_eq!(run, root.path().join("run"));
assert_eq!(mode(&run), 0o700);
std::fs::remove_dir(&run).unwrap();
assert_eq!(mode(&root.run_dir().unwrap()), 0o700, "remade private");
std::fs::set_permissions(&run, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(matches!(root.run_dir(), Err(RootError::Unsafe(_))));
}
#[test]
fn inside_a_root_is_known_by_its_marker() {
let home = tmp("inside");
let (root, _) = Root::setup(home.path(), None, None).unwrap();
use ledger_store::root::inside_a_root;
assert!(inside_a_root(root.path()));
assert!(inside_a_root(&root.path().join("books/acme")));
assert!(inside_a_root(&root.path().join("books/not/yet/made")));
assert!(!inside_a_root(home.path()));
// A path relative to where the process runs is taken from there.
let here = std::env::current_dir().unwrap();
let up = "../".repeat(here.components().count() - 1);
let relative = PathBuf::from(format!(
"{up}{}",
root.path()
.join("books/acme")
.display()
.to_string()
.trim_start_matches('/')
));
assert!(relative.is_relative());
assert!(inside_a_root(&relative));
let outside = PathBuf::from(format!(
"{up}{}",
home.path().display().to_string().trim_start_matches('/')
));
assert!(!inside_a_root(&outside));
assert!(!inside_a_root(&home.path().join("elsewhere")));
// Through a link, too: it is where the path leads that counts.
let link = home.path().join("link");
std::os::unix::fs::symlink(root.path().join("books"), &link).unwrap();
assert!(inside_a_root(&link));
assert!(!root.path().join(MARKER_FILE).is_dir());
}
#[test]
fn books_that_changed_since_the_preview_are_not_adopted() {
let home = tmp("expect");
let (root, _) = Root::setup(home.path(), None, None).unwrap();
let old = tmp("expect-src");
drop(source_books(old.path(), true));
let previewed = root.adopt_preview(old.path()).unwrap().head;
// The old server writes one more event after the preview.
let mut s = Store::open(old.path()).unwrap();
s.add_account("6100", "Rent", AccountType::Expense, None, false)
.unwrap();
let now = s.head();
// While it holds them, a preview is refused as adoption would be.
assert!(matches!(
root.adopt_preview(old.path()),
Err(RootError::Store(_, StoreError::Locked(_)))
));
drop(s);
let before = snapshot_files(old.path());
let e = root
.adopt(
old.path(),
&id("acme"),
&AdoptOptions {
expect_head: Some(previewed),
..AdoptOptions::default()
},
)
.unwrap_err();
assert!(
matches!(&e, RootError::Adopt(why) if why.contains("changed since the preview")),
"{e:?}"
);
assert!(root.list_books().unwrap().is_empty());
assert_eq!(snapshot_files(old.path()), before);
// Previewed again, it goes ahead.
let report = root
.adopt(
old.path(),
&id("acme"),
&AdoptOptions {
expect_head: Some(now.clone()),
..AdoptOptions::default()
},
)
.unwrap();
assert_eq!(report.source_head, now);
}