You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fixes all 8 P1 + 3 P2 findings from .claude/codex-reviews/M6-20260624-011401.md.
CI/Chaos were green but the wizard/account/source/crypto lifecycle had real
end-to-end gaps the mocked unit tests did not exercise.
Theme A - account / OAuth lifecycle:
- A1: persist per-account BYO OAuth client creds in the keychain
(ClientCredsStore) so refresh works after restart; load everywhere a
RefreshingTokenSource is built; delete on remove_account.
- A2: AppState.accounts behind a sync Mutex with insert/remove; assembly
spawn_account hot-spawns the orchestrator after finish_add_account so the
wizard's initial sync_now finds a live handle (no restart).
- A3: reauth_account returns { sessionId, authUrl }; the UI completes re-consent
onto the existing account (no duplicate) and hot-spawns it.
- A4: only the frontend opens the consent URL (backend no longer double-opens).
- A5: request userinfo scopes + fetch the real Google email/display name.
Theme B - source / crypto / recovery-phrase:
- B1: pick_drive_folder returns the concrete root id "root" so setup can select
a destination (incl. My Drive root).
- B2: KeystoreCryptoProvider resolves LIVE (refreshable source map on
AccountHandle.crypto, refreshed by reconfigure_account); fail-closed preserved.
- B3: the recovery phrase is a one-time RETURN VALUE on AddSourceResult, shown
once via RecoveryPhraseReveal AFTER the source/key exists, with Finish/Done
gated on an explicit ack; never an unrestorable encrypted backup.
Theme C - settings / diagnostics / path security:
- C1: backend-owned native dialogs (pick_folder_dialog / pick_save_zip_dialog)
mint one-shot path tokens (SPEC s11.6.1); add_source + export validate the
token -> path binding and reject any untrusted path.
- C2: export writes a real .zip FILE at the save-dialog path.
- C3: diagnostic bundle now includes activity_last_30d.csv, logs/, crashes/,
the redaction pipeline, and the real PRAGMA user_version (new
StateRepo::schema_version).
Tests EXERCISE each fix: backend #[cfg(test)] (crypto refresh, dialog tokens,
client-creds round-trip, userinfo parse, activity CSV redaction, schema
version), src-tauri/tests/ipc_path_validation.rs (SPEC s11.6.1), and the vitest
wizard walk completes end-to-end against the fake remote (root selectable,
running-orchestrator sync_now, phrase-gated Finish, reauth sequence).
design/CODEX_NOTES.md M6 section records the per-finding table + the
Playwright-deferred-to-local note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CyiRqk2DVwmJjEu5gcD1m
M6 @ 80c2452): CI + Chaos were GREEN but the wizard/account/source/crypto
737
+
lifecycle had real end-to-end gaps the mocked unit tests did not exercise. All
738
+
11 findings fixed; new tests EXERCISE each fix (backend `#[cfg(test)]` +
739
+
`src-tauri/tests/ipc_path_validation.rs` + vitest the wizard now completes
740
+
end-to-end against the fake remote).
741
+
742
+
| Finding | What was broken | How it was fixed |
743
+
|---|---|---|
744
+
| P1-1 (B1) | Setup could not pick a Drive destination - `pick_drive_folder` echoed `current_folder_id: None` at root, so `setup.driveFolderId` was never set. |`pick_drive_folder` now resolves `None` -> the concrete Drive root alias `"root"` AND echoes it back as `current_folder_id`, so the user can select the current folder (incl. My Drive root). `add_source` accepts `"root"`. Test: `pick_drive_folder` root-listing mock + the wizard walk select the root id. |
745
+
| P1-2 (A2) | A newly added account had no running orchestrator until restart, so the wizard's initial `sync_now(sourceId)` failed "no running orchestrator". |`AppState.accounts` moved behind a sync `Mutex<HashMap<_, Arc<AccountHandle>>>` with `insert_account`/`remove_account_handle`; assembly's per-account build factored into `assembly::spawn_account(app, &AppState, id)`, called by `finish_add_account` to hot-spawn + insert the handle (mirroring the M5 no-orphan drain - a prior handle is shut down first). Tests: `dialog_token`/handle bookkeeping + the vitest wizard walk hits a running-orchestrator mock for `sync_now`. |
746
+
| P1-3 (A1) | BYO `client_id`/`client_secret` lived only in the in-memory wizard session; only the refresh token persisted, so after restart refresh fell back to env/default creds and FAILED for every BYO account (silent broken-account data loss). | New `ClientCredsStore` (keychain namespace `driven.google.client_creds`) persists the per-account client creds on `finish_add_account`; loaded everywhere a `RefreshingTokenSource` is built (`assembly::resolve_account_oauth_creds` used by boot `build_remote` + `pick_drive_folder` + reauth); deleted on `remove_account`. Secret never logged. Tests: `ClientCreds` encode/decode round-trip. |
747
+
| P1-4 (A3) | Reauth created a hidden session and expected `finish_add_account`, but the UI only received `authUrl` and never the session id, so reauth never completed. |`reauth_account` now returns `ReauthSession { sessionId, authUrl }` (seeded with the account's stored client creds, A1); the UI opens the URL, listens `oauth:complete`, then `completeReauth(sessionId)` -> `finish_add_account` re-stores the new token onto the EXISTING account (no duplicate) + flips it back to `ok` + hot-spawns it. Tests: accounts-store `reauth` + `completeReauth`. |
748
+
| P1-5 (B3) | The BIP39 recovery phrase was emitted as a transient event the UI never subscribed to; setup rendered the reveal BEFORE the source (empty phrase) and the confirm checkbox could be ticked with no phrase shown - so the app could create ENCRYPTED BACKUPS THE USER CAN NEVER RESTORE. | The phrase is now a ONE-TIME RETURN VALUE: `add_source` returns `AddSourceResult { source, recoveryPhrase }` (Some only when this opt-in generated the master key). `ensure_master_key` encodes the phrase BEFORE stamping the row and HARD-ERRORS (rolling back the key) if it cannot encode - never an unrestorable source. The UI shows the phrase via `RecoveryPhraseReveal` AFTER the source/key exists (setup confirm step; add-source a post-confirm reveal step) and gates Finish/Done on an explicit ack that is only enableable once a real phrase was displayed. Tests: store + vitest assert phrase returned, displayed, Finish disabled until acked. |
749
+
| P1-6 (B2) | The crypto provider snapshotted source rows at assembly; `reconfigure_account` only updated orchestrator config. So an encrypted source added/toggled while running failed CLOSED (no row -> Unavailable) until restart. |`KeystoreCryptoProvider.sources` moved behind a `Mutex` with `refresh(sources)` that swaps the live map AND invalidates cache entries whose crypto fields changed/vanished; the provider Arc is held on `AccountHandle.crypto`, and `reconfigure_account` reads the account's current rows and refreshes it after every source add/update/remove. Fail-closed preserved (missing key -> Unavailable, never plaintext). Tests: refresh picks up a new encrypted source (was unknown->Plaintext, now Unavailable), toggles invalidate cache, removal drops to Plaintext. |
750
+
| P1-7 (C1) | SPEC s11.6.1 requires dialog-derived paths; the impl took raw webview strings and fabricated a token from the untrusted parent. | The BACKEND now OWNS the dialogs: `pick_folder_dialog` / `pick_save_zip_dialog` (tauri-plugin-dialog Rust API via a oneshot) return `{ path, token }`; `AppState` holds a one-shot, TTL-bounded `token -> path` binding (`mint_dialog_token`/`take_dialog_token`). `add_source` takes `localPathToken` and `export_diagnostic_bundle` takes `token`; each resolves the path from the token (single-use) and REJECTS a path with no matching token, then runs `validate_writable_dest` (canonicalize / no-dotdot / no-symlink-leaf / confine-to-dialog-root / atomic). Frontend calls the backend dialogs. Tests: `src-tauri/tests/ipc_path_validation.rs` (traversal, symlink-at-leaf, non-existent parent, outside-root reject, valid) + `dialog_token` single-use/TTL. |
751
+
| P1-8 (C2) | About asked for a DIRECTORY and passed it as `dest`; the backend then renamed a temp ZIP over the directory path -> always failed. |`pick_save_zip_dialog` returns a concrete `.zip` FILE path (suggested name + zip filter); `export_diagnostic_bundle` resolves it from the token and `atomic_write`s the ZIP AT that file. Test: the path-validation IT writes + reads back a real archive at the confined dest; About uses `pickSaveZipDialog`. |
752
+
| P2-1 (C3) | The diagnostic bundle omitted `activity_last_30d.csv`, `logs/`, `crashes/`, and wrote "user_version not exposed". | Added `StateRepo::schema_version()` (real `PRAGMA user_version`); `build_diagnostic_zip` now adds `activity_last_30d.csv` (30-day activity, message+source hashed), `logs/` + `crashes/` from `<config>/driven/logs` through a redaction pipeline (`redact_log_text`: tokens -> `<token-redacted>`, paths -> `<path:hash>`, emails -> `<email:hash>`, drive-id-shaped -> `<fileid:hash>`), and the real `user_version`. Tests: schema summary has real `user_version`, activity CSV header + redacts message, redaction-pipeline unit tests. |
753
+
| P2-2 (A4) | The consent URL was opened twice (backend `start_oauth_signin` AND frontend). | The backend opener closure now ONLY captures the URL for the return value (no `open_system_browser`); the FRONTEND is the single owner that opens it (add-account + reauth). |
754
+
| P2-3 (A5) | Account email was a user label / `account-<id>`, not the Google email. | OAuth now requests the `userinfo.email`+`userinfo.profile` scopes; `finish_add_account` fetches `oauth2/v3/userinfo` (text + serde_json, no `json` reqwest feature) with the fresh access token and persists the real email + display name (fallback to a label on failure, never a fabricated address). Tests: userinfo parse (with + without name). |
755
+
756
+
### Playwright deferred-to-local (CI uses vitest for the wizard walk)
757
+
758
+
SPEC's end-to-end wizard coverage is exercised in CI by the vitest jsdom walk
0 commit comments