Skip to content

fix(startup) [BRNS-DESK-025]: the data guard sees the wipe, so settings.json is restored - #77

Open
sebastian-ssvlabs wants to merge 1 commit into
mainfrom
fix/desk-025-data-guard-existed
Open

fix(startup) [BRNS-DESK-025]: the data guard sees the wipe, so settings.json is restored#77
sebastian-ssvlabs wants to merge 1 commit into
mainfrom
fix/desk-025-data-guard-existed

Conversation

@sebastian-ssvlabs

@sebastian-ssvlabs sebastian-ssvlabs commented Aug 14, 2026

Copy link
Copy Markdown

Summary

  • After an external wipe of ~/.brains (app cleaner, DMG swap — the 2026-07-12 incident), the guard's second defence now actually fires: settings.json is restored from the newest ~/.brains-backups snapshot instead of the user silently starting over with defaults.
  • The if !existed restore branch was unreachable dead code: startup_guard sampled dir.exists() itself, and by then the logger had already created ~/.brains/logs/. So existed was always true — which is why the repro log reads existed=true on a dir that had just been moved away.
  • run() now takes the dir-existed observation as its first filesystem touch and threads it into startup_guard(); the guard's logic moved into a run_guard() that takes the paths and boot facts as arguments, which is what makes it testable at all.
  • Closes BRNS-DESK-025 (P2, effort S, desktop / data-integrity).

Boot ordering — why existed was always true

order src-tauri/src/lib.rs effect on ~/.brains
1 TeeLog { file: open_log_file() } (run(), top) create_dir_all(data_dir()/logs)creates the dir
2 ~320 lines of builder wiring
3 .setup()startup_guard(version) let existed = dir.exists() → always true

Only the !existed term was dead. Wipe detection still worked through the marker/runs half of fresh (!marker.exists() && runs_count <= 1), which is why the recovery prompt kept appearing while the settings restore never ran — the log line in the ticket is both symptoms in one message.

The fix keeps the logger first (boot diagnostics need it) and keeps the marker/runs term (dropping it would change wipe detection for partial wipes that leave the marker). It only moves when the fact is observed: let data_dir_existed = storage::data_dir().exists(); as the first statement of run(), with a comment stating that invariant at the capture site.

startup_guard gained the parameter with no local fallback — the plan allowed one "if some call site genuinely boots without the early capture", and there is exactly one call site (main.rsrun()setup()). A silent dir.exists() fallback would just reintroduce the bug for any future caller that forgot.

Two smaller things the ticket did not ask for, both needed to land the tests it did ask for:

  • run_guard(dir, backups, existed, prior_evidence, app_version) -> GuardReport — pure w.r.t. paths. Necessary because storage::home_dir() resolves through getpwuid() and ignores $HOME, so a test cannot point the guard at a temp dir by env var. The home-dependent work (claude_home_sessions_exist, the recoverable-session count) stays in startup_guard, so the tests touch nothing outside their temp dir.
  • GuardReport.existed is now reported (and surfaces in the data_guard_status command / RUST_LOG=debug diagnostics). The ticket's test asks to assert "the reported existed is false", and the report had no such field; it is also the value you want visible when reproducing this class of bug. Purely additive for the frontend, which reads the report as any.

Verification

Ran locally:

  • cargo fmt --manifest-path src-tauri/Cargo.toml — clean, and the pre-commit hook's Rust format check passed.
  • git diff reviewed line by line; no frontend file is touched (src/routes/+page.svelte already consumes the report untyped), so prettier/eslint had nothing to run against.
  • Both new tests written against this file's existing idioms (std::env::temp_dir() + process::id() prefix, 20 ms sleep for mtime ordering, same remove_dir_all teardown) and checked by inspection.

Only CI can prove:

  • cargo test was NOT run locally — this checkout has no Rust build environment (no cargo target dir, no .svelte-kit), so cargo test --manifest-path src-tauri/Cargo.toml in CI is the first real compile of these two tests:
    • wiped_data_dir_restores_settings_from_newest_backup — dir absent + two backup snapshots → existed == false, settings_restored == true, fresh_but_prior_use == true, and the restored file is the newest snapshot's.
    • normal_boot_does_not_restore_settings — dir present with marker + matching .app-version → nothing restored, live settings.json untouched, no false recovery flag.
  • cargo clippy and npm run check were not run (clippy is warn-mode in ci.yml).
  • The end-to-end repro (mv ~/.brains away, launch, grep data-guard, ls -a ~/.brains) was not performed — it needs a GUI app build. The acceptance criterion is exercised at the unit level instead; the one link the tests do not cover is run() passing its capture through to setup(), which is the three-line change in lib.rs.

CI result (added after opening)

All checks green on f144011. Both new tests pass on ubuntu, macOS and Windows, so the compile-by-inspection above is now confirmed by a real build on all three targets.

The first Windows run failed on commands::printing::tests::stash_then_take_returns_the_document_once, which this PR does not touch — those tests share a global PRINT_JOB and race under the parallel test runner. A rerun of that job alone passed with no code change.

Note

No conflict with PR #74 (fix/desk-053-portable-backup-names, targeting dev): different branch and tree, and none of its backup-naming changes are imported here. This branch touches the restore path and the boot ordering only — backup directory names are read by mtime, not parsed.

…gs.json is restored

startup_guard computed `existed` itself, but the logger's file target creates
~/.brains/logs/ ~320 lines earlier in run(), so the check always said true and
the `if !existed` settings auto-restore branch was unreachable dead code.

run() now samples data_dir().exists() as its first filesystem touch and threads
the boot fact into startup_guard(); the guard's decision logic moves to
run_guard(), which takes the data dir, the backups root and the boot facts as
arguments so it is testable without a real home dir (home_dir() reads getpwuid,
not $HOME). Wipe detection semantics are unchanged.

@nir-ssvlabs nir-ssvlabs left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is real rather than nominal, which was the thing worth checking: let data_dir_existed = storage::data_dir().exists() sits at lib.rs:141, genuinely ahead of the logger init at 145 that creates the dir, and data_dir() is a pure path join with no create_dir anywhere in it — so the capture observes the pre-boot state instead of manufacturing it. The description's "keeps the logger first" reads as if the capture came after; it doesn't, and the comment at the capture site states the invariant it depends on.

Taking the parameter with no fallback is the right call and worth saying so: exactly one call site exists (lib.rs:468), and a silent dir.exists() default would have re-created the original bug for the next caller while looking defensive. Extracting run_guard to take paths as arguments is what makes any of this testable given home_dir() resolves through getpwuid() and ignores $HOME.

Checked: the capture's position against the logger, data_dir() for side effects, the signature for a fallback, and that startup_guard has a single call site. Not read: the restore path itself — unchanged here; this PR only fixes whether it is reached.

Merge: ✅ into main once Rust (Windows) lands — it was still pending at review time.

@stefan-ssv-labs stefan-ssv-labs left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ review-pr: clean against BRNS-DESK-025 — the pre-logger existence capture, restore path, regression tests, and normal-boot guard are present.

@Chris-ssvlabs Chris-ssvlabs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants