From 8cd2d2f80f5e8a3b3bec1cebb488f79bfbe1a2d7 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 29 Oct 2026 22:36:17 -0600 Subject: [PATCH 1/3] fix(settings): a repository on this computer may be a Windows drive path or a rooted Unix path on any system The portable Windows job (P11c run 6) failed ledger-core's `where_backups_go_is_checked`: `/srv/books.git` is not an absolute path to Windows, and `C:\books` was read as the host `C` of a networked address. `backup.git_remote` now reads a drive path (one letter, a colon, a slash or backslash) as a path on this computer, as git does, and a path from `/` as rooted on every system git runs on; the test holds the three forms on every OS. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp --- crates/ledger-core/src/settings.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/ledger-core/src/settings.rs b/crates/ledger-core/src/settings.rs index 1f20afb..8477eac 100644 --- a/crates/ledger-core/src/settings.rs +++ b/crates/ledger-core/src/settings.rs @@ -1198,13 +1198,22 @@ pub fn check(key: &Key, v: &Value) -> Result { } // A path on this computer is named from the root of its disk: // the server's working folder is no place to name one from. - let networked = match s.split_once("://") { - Some((scheme, _)) => scheme != "file", - None => s - .split_once(':') - .is_some_and(|(host, _)| !host.contains('/')), - }; - if !networked && !s.starts_with("file://") && !std::path::Path::new(&s).is_absolute() { + // `C:\books` is a Windows drive path, not the host `C` (git + // reads it the same way), and `/srv/books` is rooted on every + // system git runs on (on Windows through its own shell). + let drive_path = s.len() > 2 + && s.as_bytes()[0].is_ascii_alphabetic() + && &s[1..2] == ":" + && matches!(&s[2..3], "\\" | "/"); + let networked = !drive_path + && match s.split_once("://") { + Some((scheme, _)) => scheme != "file", + None => s + .split_once(':') + .is_some_and(|(host, _)| !host.contains('/')), + }; + let rooted = drive_path || s.starts_with('/') || std::path::Path::new(&s).is_absolute(); + if !networked && !s.starts_with("file://") && !rooted { return Err(format!( "{name} {s:?}: a repository on this computer is named from the root of its disk (such as /srv/books)" )); @@ -1854,6 +1863,8 @@ mod tests { "git@host:owner/books.git", "https://host/owner/books", "/srv/books.git", + "C:\\books\\acme.git", + "D:/books/acme.git", ] { assert_eq!(check(remote, &json!(ok)).unwrap(), json!(ok), "{ok}"); } From 66d3e5b2c4a3d284d5fe0e8cde996d414cc94d8b Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 29 Oct 2026 22:49:20 -0600 Subject: [PATCH 2/3] test(store): the root tests ask the platform where the data folder is and compare roots canonically The portable macOS job: five root tests spelled out `.local/share` or compared a chosen root against its /tmp spelling where macOS keeps /private/tmp. They use `platform_data_dir` and canonical paths now. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp --- crates/ledger-store/tests/root.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/ledger-store/tests/root.rs b/crates/ledger-store/tests/root.rs index 6654565..6d587f6 100644 --- a/crates/ledger-store/tests/root.rs +++ b/crates/ledger-store/tests/root.rs @@ -154,7 +154,7 @@ fn the_root_is_found_without_the_environment() { ); assert_eq!( default_root(home.path()), - home.path().join(".local/share/wanderbooks") + platform_data_dir(home.path()).join("wanderbooks") ); // An explicit root wins. let elsewhere = home.path().join("elsewhere"); @@ -203,6 +203,8 @@ fn setup_records_a_root_chosen_elsewhere_and_forgets_it_when_back_to_default() { let home = tmp("setup-elsewhere"); let chosen = home.path().join("my books"); let (_, report) = Root::setup(home.path(), Some(&chosen), None).unwrap(); + // Roots are kept as the system spells them (macOS: /tmp is /private/tmp). + let chosen = std::fs::canonicalize(&chosen).unwrap(); assert!(report.recorded); let pointer = platform_data_dir(home.path()).join(LOCATION_FILE); assert_eq!(mode(&pointer), 0o600); @@ -605,6 +607,7 @@ fn setup_run_again_keeps_the_recorded_root_and_never_moves_books() { let home = tmp("setup-keep"); let chosen = home.path().join("chosen"); let (root, _) = Root::setup(home.path(), Some(&chosen), None).unwrap(); + let chosen = std::fs::canonicalize(&chosen).unwrap(); drop(root.create_book(&id("acme"), Currency::USD).unwrap()); // Run again with nothing given (and XDG set): the recorded root stays. let (again, report) = Root::setup(home.path(), None, Some(&home.path().join("xdg"))).unwrap(); @@ -669,8 +672,12 @@ fn a_root_another_user_could_swap_is_refused() { fn missing_folders_above_the_root_are_made_private() { let home = tmp("parents"); let (root, _) = Root::setup(home.path(), None, None).unwrap(); - assert_eq!(mode(&home.path().join(".local")), 0o700); - assert_eq!(mode(&home.path().join(".local/share")), 0o700); + // Every folder setup made between the home and the root (`.local` and + // `.local/share` on Linux; `Library/Application Support` on macOS). + let data = platform_data_dir(home.path()); + for dir in data.ancestors().take_while(|d| *d != home.path()) { + assert_eq!(mode(dir), 0o700, "{}", dir.display()); + } assert_eq!(mode(&root.path().join(MARKER_FILE)), 0o600); } @@ -1119,8 +1126,9 @@ fn errors_other_than_not_found_are_never_taken_for_it() { )); // The data folder is a file: finding the root fails, never falls back. let odd = tmp("not-not-found-home"); - std::fs::create_dir(odd.path().join(".local")).unwrap(); - std::fs::write(odd.path().join(".local/share"), "x").unwrap(); + let data = platform_data_dir(odd.path()); + std::fs::create_dir_all(data.parent().unwrap()).unwrap(); + std::fs::write(&data, "x").unwrap(); assert!(matches!( Root::locate(odd.path(), None), Err(RootError::Io(..)) From 659a4266319a0090a77fc6b4107704aa979002ca Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 29 Oct 2026 22:55:59 -0600 Subject: [PATCH 3/3] test(cli): the demo-books start test needs the viewer; not in the server-only build The gate on 8ba104a1 passed every workspace test and failed only the server-only build: P5's `books_start_fits_8_kib_on_the_demo_books` builds the demo books with `ledger_gui`, which the no-viewer build does not carry. Gated on the `gui` feature like its neighbours. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp --- crates/ledger-cli/tests/agent_surface.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ledger-cli/tests/agent_surface.rs b/crates/ledger-cli/tests/agent_surface.rs index 04c97fe..abf66c1 100644 --- a/crates/ledger-cli/tests/agent_surface.rs +++ b/crates/ledger-cli/tests/agent_surface.rs @@ -505,7 +505,9 @@ fn a_mutating_call_answers_with_the_head_and_a_refusal_names_its_remedy() { /// Done-when (3): `books.start` is at most 8 KiB on the demo company too /// (the books the viewer ships with, a loose end of every kind), and says -/// what it cut. +/// what it cut. (The demo books are the viewer's: not in the server-only +/// build.) +#[cfg(feature = "gui")] #[test] fn books_start_fits_8_kib_on_the_demo_books() { let today = ledger_core::settings::today_in(None);