Skip to content

feat(auth): add backup restore manager#76

Open
ndycode wants to merge 24 commits intogit-plan/01-reset-safetyfrom
git-plan/02-backup-restore-manager
Open

feat(auth): add backup restore manager#76
ndycode wants to merge 24 commits intogit-plan/01-reset-safetyfrom
git-plan/02-backup-restore-manager

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 12, 2026

Summary

  • add named-backup restore to codex auth login, including the zero-account recovery path and fallback prompt aliases
  • harden restore path resolution, restore assessment, and Windows retry behavior so only backups inside backups/ participate in restore flows
  • preserve duplicate-only metadata refreshes while still treating true duplicate-only restores as a no-op, with targeted regression coverage

Validation

  • npm test -- test/storage.test.ts test/codex-manager-cli.test.ts test/cli.test.ts
  • npm run lint

Notes

  • scope stays limited to the backup-restore manager and its storage/test coverage

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this PR wires a named-backup restore manager into codex auth login — covering the zero-account recovery path, concurrent assessment fan-out with Promise.allSettled, a fresh re-assessment before confirmation to keep the merge summary current, and a restore-backup fallback alias in the non-TTY prompt. path traversal is hardened via assertNamedBackupRestorePath using path.relative, symlinks are rejected, and windows filesystem contention is handled with a retryTransientFilesystemOperation helper (EBUSY/ENOTEMPTY on all platforms, EPERM/EAGAIN on win32).

key concerns:

  • importAccounts never enforces ACCOUNT_LIMITS.MAX_ACCOUNTS inside the storage transaction — the limit check in assessNamedBackupRestore is advisory. two concurrent interactive sessions on windows can both pass the assessment independently, serialise at the mutex, and the second import silently pushes the account count above the cap. adding the check before persist() (and a matching vitest case) would close this TOCTOU window.
  • haveEquivalentAccountRows uses JSON.stringify for order-sensitive comparison — safe today since deduplicateAccounts is order-preserving, but deserves an inline comment to protect future refactors.
  • the storagePath field added to transactionSnapshotContext and the guard in exportAccounts correctly prevent stale-snapshot leakage when the active path changes mid-transaction — good hardening.
  • removeWithRetry applied to the outer export/import afterEach closes the windows CI flake path noted in prior review rounds.

Confidence Score: 3/5

  • safe to merge with low risk, but the unguarded account-limit in importAccounts should be fixed before the restore path ships to avoid silent cap breaches on Windows under concurrent use.
  • the path traversal hardening, windows retry logic, and error-catch layering are solid. the core restore flow (assess → confirm → re-assess → import) is correct and well-tested. score is held back by the TOCTOU gap: importAccounts does not enforce ACCOUNT_LIMITS.MAX_ACCOUNTS inside the storage transaction, so the assessment-level limit gate can be raced past by concurrent sessions — a real concern given the PR's own focus on windows concurrency safety.
  • lib/storage.tsimportAccounts transaction block needs a deduplicatedAccounts.length > ACCOUNT_LIMITS.MAX_ACCOUNTS guard before persist(). test/storage.test.ts — needs a companion test for the concurrent-exceed scenario.

Important Files Changed

Filename Overview
lib/storage.ts adds listNamedBackups, assessNamedBackupRestore, restoreNamedBackup, createNamedBackup, and importAccounts(changed flag). path-traversal guard (assertNamedBackupRestorePath) and windows retry logic (retryTransientFilesystemOperation) are solid. critical gap: importAccounts never checks ACCOUNT_LIMITS.MAX_ACCOUNTS inside the transaction — the assessment gate is advisory only and a concurrent import race on windows can silently exceed the cap.
lib/codex-manager.ts runBackupRestoreManager wired correctly — listNamedBackups, concurrent assessments with Promise.allSettled, user confirm, fresh re-assessment before import, and error catch at every layer. quota-refresh awaited before quota-touching modes. restore mode correctly skips quota-wait. the outer runAuthLogin try/catch around runBackupRestoreManager prevents crashes.
lib/cli.ts adds restore-backup LoginMode and fallback aliases (u, backup, restore, restore-backup) to promptLoginModeFallback. clean, no issues.
lib/ui/auth-menu.ts adds Recovery section heading and Restore From Backup menu item above Danger Zone. authMenuFocusKey extended. no issues.
lib/ui/copy.ts adds recovery, restoreBackup, restoreBackupConfirm, restoreBackupSuccess copy strings. fallback prompt updated with u alias. no issues.
test/codex-manager-cli.test.ts thorough coverage: zero-account restore, confirm decline, import failure, duplicate-only no-op, listing failure, partial assessment failure, concurrent assessment limit, reassessment before confirm, reassessment becoming ineligible, reassessment error, timestamp formatting. missing: concurrent-import-exceeds-limit scenario.
test/storage.test.ts outer export/import afterEach now uses removeWithRetry for windows robustness. new named backup integration tests cover create/list, assess+restore, explicit-null currentStorage, duplicate rejection, manual filenames, post-assessment deletion, invalid JSON, path traversal, symlink guard, and windows retry permutations. missing: concurrent import exceeding MAX_ACCOUNTS.
test/cli.test.ts adds fallback alias coverage for u and restore → restore-backup mode. straightforward, no issues.
docs/reference/storage-paths.md documents named backup paths for default and project-scoped layouts, restore workflow steps, and adds codex auth login to verification commands. accurate and complete.

Sequence Diagram

sequenceDiagram
    participant U as User (codex auth login)
    participant M as runAuthLogin loop
    participant BRM as runBackupRestoreManager
    participant S as storage.ts
    participant FS as Filesystem (backups/)

    M->>BRM: mode === "restore-backup"
    BRM->>S: listNamedBackups()
    S->>FS: readdir(backupRoot) [retryTransientFsOp]
    FS-->>S: [entries]
    S-->>BRM: NamedBackupMetadata[]

    BRM->>S: assessNamedBackupRestore(name, {currentStorage})\n[chunked, NAMED_BACKUP_ASSESS_CONCURRENCY=8]
    S->>FS: readdir + stat + loadBackupCandidate [retryTransientFsOp]
    S->>S: assertNamedBackupRestorePath (traversal check)
    S->>S: deduplicateAccounts → check wouldExceedLimit
    S-->>BRM: BackupRestoreAssessment[]

    BRM->>U: select(assessments menu)
    U-->>BRM: { type: "restore", assessment }

    BRM->>S: assessNamedBackupRestore(name, {currentStorage: fresh}) [re-assess]
    S-->>BRM: latestAssessment (eligibleForRestore check)

    BRM->>U: confirm(restoreBackupConfirm(...))
    U-->>BRM: true

    BRM->>S: importAccounts(latestAssessment.backup.path)
    S->>S: withAccountStorageTransaction (storageMutex acquired)
    S->>FS: read backup file [retryTransientFsOp]
    S->>S: deduplicateAccounts → haveEquivalentAccountRows
    alt changed
        S->>FS: persist newStorage (WAL write)
        S-->>BRM: ImportAccountsResult { changed: true }
    else no-op
        S-->>BRM: ImportAccountsResult { changed: false }
    end

    BRM-->>M: return (errors caught, continue loop)
Loading

Comments Outside Diff (1)

  1. lib/storage.ts, line 2842-2865 (link)

    TOCTOU race: importAccounts doesn't enforce ACCOUNT_LIMITS.MAX_ACCOUNTS inside the transaction

    assessNamedBackupRestore checks wouldExceedLimit and sets eligibleForRestore: false when the merge would push over the cap. runBackupRestoreManager gates on that flag before calling importAccounts. But the actual import executes inside withAccountStorageTransaction, reads the current accounts at that point, and writes the merged result without ever consulting ACCOUNT_LIMITS.MAX_ACCOUNTS.

    on windows, two concurrent codex auth login sessions can both pass the assessment check independently (both see the current count as below the cap), both receive user confirmation, and then both enter withAccountStorageTransaction in sequence. the second import operates on the already-merged state from the first and can silently push the stored account count above MAX_ACCOUNTS with no error returned to the caller.

    the fix is to check the merged count inside the transaction before calling persist:

    const deduplicatedAccounts = deduplicateAccounts(merged);
    const imported = deduplicatedAccounts.length - existingAccounts.length;
    const skipped = normalized.accounts.length - imported;
    
    // enforce limit inside the lock so concurrent restores can't race past it
    if (deduplicatedAccounts.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
        throw new Error(
            `Import would exceed maximum of ${ACCOUNT_LIMITS.MAX_ACCOUNTS} accounts`,
        );
    }
    
    const changed = !haveEquivalentAccountRows(deduplicatedAccounts, existingAccounts);

    this matches the error message already used in assessNamedBackupRestore, and all callers of importAccounts already wrap in try/catch.

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/storage.ts
Line: 2842-2865

Comment:
**TOCTOU race: `importAccounts` doesn't enforce `ACCOUNT_LIMITS.MAX_ACCOUNTS` inside the transaction**

`assessNamedBackupRestore` checks `wouldExceedLimit` and sets `eligibleForRestore: false` when the merge would push over the cap. `runBackupRestoreManager` gates on that flag before calling `importAccounts`. But the actual import executes inside `withAccountStorageTransaction`, reads the current accounts *at that point*, and writes the merged result without ever consulting `ACCOUNT_LIMITS.MAX_ACCOUNTS`.

on windows, two concurrent `codex auth login` sessions can both pass the assessment check independently (both see the current count as below the cap), both receive user confirmation, and then both enter `withAccountStorageTransaction` in sequence. the second import operates on the already-merged state from the first and can silently push the stored account count above `MAX_ACCOUNTS` with no error returned to the caller.

the fix is to check the merged count inside the transaction before calling `persist`:

```typescript
const deduplicatedAccounts = deduplicateAccounts(merged);
const imported = deduplicatedAccounts.length - existingAccounts.length;
const skipped = normalized.accounts.length - imported;

// enforce limit inside the lock so concurrent restores can't race past it
if (deduplicatedAccounts.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
    throw new Error(
        `Import would exceed maximum of ${ACCOUNT_LIMITS.MAX_ACCOUNTS} accounts`,
    );
}

const changed = !haveEquivalentAccountRows(deduplicatedAccounts, existingAccounts);
```

this matches the error message already used in `assessNamedBackupRestore`, and all callers of `importAccounts` already wrap in try/catch.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: test/storage.test.ts
Line: 2722-2748

Comment:
**missing vitest coverage for the concurrent-limit race in `importAccounts`**

the integration tests cover the happy path, duplicate-only, deleted-after-assessment, and all the windows-retry paths. there's no test that exercises the TOCTOU scenario described above: two sequential `importAccounts` calls where the first succeeds and the second would push the stored count over `ACCOUNT_LIMITS.MAX_ACCOUNTS`.

a minimal case:

```typescript
it("rejects a second import that would exceed MAX_ACCOUNTS", async () => {
    // fill to one below the limit
    await saveAccounts({ version: 3, activeIndex: 0, accounts: nearLimitAccounts });
    await fs.writeFile(exportPath, JSON.stringify({ version: 3, activeIndex: 0, accounts: [extraAccount] }));

    // first import fits
    const first = await importAccounts(exportPath);
    expect(first.imported).toBe(1);

    // second import with another extra account now exceeds the cap
    await fs.writeFile(exportPath2, JSON.stringify({ version: 3, activeIndex: 0, accounts: [anotherAccount] }));
    await expect(importAccounts(exportPath2)).rejects.toThrow(/exceed maximum/);
});
```

without this test and the corresponding guard, the limit check stays advisory-only and the TOCTOU race goes untested.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: lib/storage.ts
Line: 870-883

Comment:
**`haveEquivalentAccountRows` is order-sensitive — spurious writes possible if dedup reorders**

`JSON.stringify` comparison is strict about key insertion order. if `deduplicateAccounts` ever produces the same logical accounts but in a different order than what's stored on disk (e.g. after a schema migration that normalises key order), `changed` would be `true` and a write would be issued even though the real content is unchanged.

this only matters for the duplicate-only path where `changed` is the sole write gate, and it's benign today since `deduplicateAccounts` appears to be order-preserving. worth a comment to future-proof:

```typescript
// NOTE: comparison is order-sensitive. deduplicateAccounts must preserve
// the existing-first ordering produced by [...existingAccounts, ...importAccounts]
// for this early-exit to be correct.
function haveEquivalentAccountRows(
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: da40e70

Context used:

  • Rule used - What: Every code change must explain how it defend... (source)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

this pr adds a named-backup restore feature: storage APIs to create/list/assess/restore named backups, cli/login support for a new restore-backup mode, ui menu items for recovery, a backup-restore manager in lib/codex-manager.ts, and extensive tests around named backups.

Changes

Cohort / File(s) Summary
Documentation
docs/reference/storage-paths.md
added canonical and project-scoped named backup paths and restore workflow steps.
CLI
lib/cli.ts
added restore-backup to LoginMode and wired fallback keys (u, backup, restore) to the new mode (lib/cli.ts:line ~1).
Storage Layer
lib/storage.ts
introduced named-backup public APIs and types (NamedBackupMetadata, BackupRestoreAssessment, NAMED_BACKUP_LIST_CONCURRENCY, listNamedBackups(), getNamedBackupsDirectoryPath(), createNamedBackup(), assessNamedBackupRestore(), restoreNamedBackup()), concurrency-aware listing, retry helpers, and propagated storagePath through transactions (lib/storage.ts:line ~1-400).
Manager / Orchestration
lib/codex-manager.ts
added runBackupRestoreManager(displaySettings) and BackupMenuAction; manager lists backups, batches assessments (concurrency-limited), re-assesses on selection, prompts confirm, and performs restore; integrated into login flow and refactored auth loop (lib/codex-manager.ts:line ~1-500).
UI
lib/ui/auth-menu.ts, lib/ui/copy.ts
added restore-backup action and menu item under a "Recovery" heading; added UI_COPY.mainMenu.recovery and restoreBackup strings (lib/ui/auth-menu.ts:line ~1, lib/ui/copy.ts:line ~1).
Tests
test/codex-manager-cli.test.ts, test/storage.test.ts, test/cli.test.ts
added mocks and tests for listing/assessing/restoring named backups, confirm prompt, concurrency and re-assessment behaviours, and fallback mapping for login prompts; extensive new tests and fixtures added (test/...:line ~1-700).

Sequence diagram

sequenceDiagram
    participant user as User
    participant auth as Auth Menu
    participant mgr as BackupRestoreManager
    participant storage as Storage
    participant fs as File System

    user->>auth: select "restore from backup"
    auth->>mgr: runBackupRestoreManager()
    mgr->>storage: listNamedBackups()
    storage->>fs: read backups dir
    fs-->>storage: backup entries
    storage-->>mgr: NamedBackupMetadata[]
    mgr->>storage: assessNamedBackupRestore(name) (batched)
    storage->>fs: read backup file
    fs-->>storage: backup content
    storage-->>mgr: BackupRestoreAssessment
    mgr->>user: display menu with hints
    user->>mgr: choose backup
    mgr->>storage: reassess assessNamedBackupRestore(name)
    mgr->>user: prompt confirm()
    user->>mgr: confirm
    mgr->>storage: restoreNamedBackup(name)
    storage->>fs: merge/import accounts
    fs-->>storage: write results
    storage-->>mgr: {imported, total, skipped}
    mgr->>user: show result / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

reviewable concerns

  • missing regression tests: add tests for corrupted backup files mid-restore and concurrent restores of the same named backup (lib/storage.ts:line ~120, test/storage.test.ts:line ~200, test/codex-manager-cli.test.ts:line ~150).
  • concurrency risk: filesystem-based read/merge/write has no cross-process locking; concurrent restores or concurrent writes can race and corrupt data (lib/storage.ts:line ~300, lib/codex-manager.ts:line ~420). consider file locks or atomic-rename strategies.
  • windows path escaping: ensure findExistingNamedBackupPath and resolve helpers properly normalize and prevent path traversal and backslash issues on windows (lib/storage.ts:line ~120). add windows-style path tests.
  • ui confirm robustness: confirm() can block or behave unexpectedly in non-tty contexts; tests mock it but do not cover timeouts or blocked prompts (lib/codex-manager.ts:line ~340, test/codex-manager-cli.test.ts:line ~50). add tests for confirm timeouts and fallback behavior.
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR description covers summary, validation steps, and scope; however, lacks explicit Docs and Governance Checklist section from template with individual checkboxes for README, getting-started.md, features.md, reference docs, upgrade.md, SECURITY.md, and CONTRIBUTING.md. Add or explicitly confirm status of Docs and Governance Checklist items (README, getting-started.md, features.md, docs/reference/*, upgrade.md, SECURITY.md, CONTRIBUTING.md). Also add Risk level and Rollback plan section before merging.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed title follows conventional commits format with type (feat), scope (auth), and lowercase imperative summary within 72 character limit.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch git-plan/02-backup-restore-manager
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/storage.test.ts">

<violation number="1" location="test/storage.test.ts:316">
P2: Use retry-based cleanup instead of bare `fs.rm` in test teardown to avoid Windows EBUSY/EPERM/ENOTEMPTY flakiness.</violation>
</file>

<file name="lib/storage.ts">

<violation number="1" location="lib/storage.ts:475">
P2: Backup name normalization in path resolution breaks restore for listed backup files whose filenames contain spaces or other non-normalized characters.</violation>
</file>

<file name="test/codex-manager-cli.test.ts">

<violation number="1" location="test/codex-manager-cli.test.ts:1609">
P3: `vi.doMock` here is never unmocked. `vi.resetModules()` does not clear the mock registry, so this confirm mock will leak into later tests and can change their behavior. Add a `vi.doUnmock`/`vi.unmock` cleanup (e.g., in `afterEach`) to keep tests isolated.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/storage.test.ts">

<violation number="1" location="test/storage.test.ts:132">
P2: Replace the bare `fs.rm` in test cleanup with retry/backoff to avoid Windows CI flakes on transient file locks.</violation>
</file>

<file name="lib/storage.ts">

<violation number="1" location="lib/storage.ts:476">
P2: `resolveNamedBackupPath` allows path traversal via unsanitized backup names before directory confinement.</violation>
</file>

<file name="lib/cli.ts">

<violation number="1" location="lib/cli.ts:283">
P2: `restore-backup` is wired only in the TUI path; the fallback login prompt cannot select this new mode.</violation>
</file>

<file name="lib/codex-manager.ts">

<violation number="1" location="lib/codex-manager.ts:4171">
P2: Handle restoreNamedBackup errors so a missing/invalid backup or limit breach doesn’t crash the auth menu.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/cli.ts (1)

49-61: ⚠️ Potential issue | 🟠 Major

the new mode is still unreachable in the readline fallback menu.

this only wires restore-backup through the tty path. promptLoginModeFallback() in lib/cli.ts:174-243 and its prompt copy in lib/ui/copy.ts:135-137 still never advertise or parse the mode, so users in fallback environments cannot reach the restore flow even though runAuthLogin() handles it at lib/codex-manager.ts:3909-3919. please add a vitest for the fallback branch.

As per coding guidelines, lib/**: focus on auth rotation, windows filesystem io, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios.

Also applies to: 291-292

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/cli.ts` around lines 49 - 61, The fallback readline menu never exposes or
parses the new "restore-backup" LoginMode, so add it to
promptLoginModeFallback() (ensure the option label and value include
"restore-backup") and update the fallback prompt text in lib/ui/copy.ts to
advertise the restore flow (matching how runAuthLogin() in lib/codex-manager.ts
handles "restore-backup"); add a vitest that exercises the fallback branch to
assert the prompt lists and correctly parses "restore-backup", and update any
affected tests to reference the new behavior; while modifying prompts/tests,
verify any updated auth rotation or queue logic touched by this change properly
retries or surfaces EBUSY/429 errors per existing concurrency and Windows IO
guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/reference/storage-paths.md`:
- Line 25: Update docs/reference/storage-paths.md to document the restore entry
point and user workflow for the new backup paths: add a short step-by-step
showing "codex auth login" → select recovery → choose 'restore from backup' and
point readers to the interactive restore implementation in lib/codex-manager.ts
(see the interactive flow around the blocks at ~3909-3919 and the restore logic
at ~4211-4291) so users know how to consume files under
~/.codex/multi-auth/backups/<name>.json; also add or link an upgrade note that
mentions the changed CLI workflow and any new npm scripts introduced so README,
SECURITY, and docs remain consistent with the shipped flags and behavior.

In `@lib/codex-manager.ts`:
- Around line 3909-3920: The recovery menu (restore-backup) is never reachable
because runAuthLogin() only enters the menu loop when
existingStorage.accounts.length > 0; hoist the recovery entry so
runBackupRestoreManager(...) can be invoked even when existingStorage.accounts
is empty: move the restore-backup handling (call to runBackupRestoreManager and
its try/catch) to execute before the non-empty accounts guard in runAuthLogin,
ensuring the menu can present the recovery path on fresh/reset/missing storage;
add a vitest that simulates zero-account storage and asserts the restore flow is
offered and that runBackupRestoreManager is called; update any affected tests
referenced in the auth/login suite and ensure new queue logic (if added) handles
EBUSY and 429 retries per concurrency guidelines.

In `@lib/storage.ts`:
- Around line 1712-1728: loadBackupCandidate currently converts any read error
into a permanent invalid-backup result; change it to detect transient Windows/IO
errors (EPERM, EBUSY, EAGAIN) and retry the read (call to loadAccountsFromPath)
with an exponential/backoff loop (same retry envelope used elsewhere in this
module) before returning an error, and apply the same retry logic to the paired
fs.stat call in this file that checks backup file metadata; add a vitest that
simulates a transient busy/EPERM/EBUSY/EAGAIN failure on the first N attempts
and succeeds thereafter to assert the retry path is exercised and cite/update
the affected tests.
- Around line 1580-1609: The function listNamedBackups currently swallows all
readdir errors except ENOENT by logging and returning an empty array; change the
catch in listNamedBackups so that only ENOENT returns [] while any other error
is re-thrown (preserve the original error object) so callers like
runBackupRestoreManager surface real filesystem errors (EPERM/EBUSY). Update
tests: add a vitest that simulates an unreadable backup directory (make readdir
throw EPERM/EBUSY) and assert that listNamedBackups rejects/throws rather than
returning an empty array. Keep references to getNamedBackupRoot/getStoragePath
for locating the path and ensure loadBackupCandidate/buildNamedBackupMetadata
logic is untouched.

In `@test/codex-manager-cli.test.ts`:
- Around line 2365-2428: Add a regression test that covers the confirm=false
path so restore is blocked: duplicate the existing "restores a named backup..."
test setup (use setInteractiveTTY, mock loadAccountsMock, listNamedBackupsMock,
assessNamedBackupRestoreMock, promptLoginModeMock and selectMock) but make
confirmMock resolve to false and then call
runCodexMultiAuthCli(["auth","login"]); assert exitCode is non-zero (or
indicates cancellation) and that restoreNamedBackupMock was not called; keep
other expectations similar (list/assess called once, select used) to ensure the
safety gate implemented by confirmMock is exercised.

In `@test/storage.test.ts`:
- Around line 1111-1185: The tests cover happy paths but lack regression cases
that ensure restoreNamedBackup and assessNamedBackupRestore properly surface
failures for invalid or missing backup payloads; add deterministic vitest tests
in test/storage.test.ts that (1) create a named backup file with invalid JSON
and assert assessNamedBackupRestore(...) and restoreNamedBackup(...) both
reject/throw, (2) create a backup JSON missing required fields (e.g., no
"accounts") and assert both functions reject with a clear error, and (3) assert
behavior is consistent when a backup file is empty or truncated; use the same
helper functions (createNamedBackup, listNamedBackups, assessNamedBackupRestore,
restoreNamedBackup, clearAccounts, loadAccounts) and filesystem writes to inject
the malformed payloads so the tests reproduce the error-path regressions
deterministically under vitest.

---

Outside diff comments:
In `@lib/cli.ts`:
- Around line 49-61: The fallback readline menu never exposes or parses the new
"restore-backup" LoginMode, so add it to promptLoginModeFallback() (ensure the
option label and value include "restore-backup") and update the fallback prompt
text in lib/ui/copy.ts to advertise the restore flow (matching how
runAuthLogin() in lib/codex-manager.ts handles "restore-backup"); add a vitest
that exercises the fallback branch to assert the prompt lists and correctly
parses "restore-backup", and update any affected tests to reference the new
behavior; while modifying prompts/tests, verify any updated auth rotation or
queue logic touched by this change properly retries or surfaces EBUSY/429 errors
per existing concurrency and Windows IO guidelines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f8fa6ad5-4c6c-4110-af7b-d6c8d1c2f62c

📥 Commits

Reviewing files that changed from the base of the PR and between d67ca3d and 7443074.

📒 Files selected for processing (8)
  • docs/reference/storage-paths.md
  • lib/cli.ts
  • lib/codex-manager.ts
  • lib/storage.ts
  • lib/ui/auth-menu.ts
  • lib/ui/copy.ts
  • test/codex-manager-cli.test.ts
  • test/storage.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (3)
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/cli.ts
  • lib/ui/auth-menu.ts
  • lib/codex-manager.ts
  • lib/ui/copy.ts
  • lib/storage.ts
docs/**

⚙️ CodeRabbit configuration file

keep README, SECURITY, and docs consistent with actual CLI flags and workflows. whenever behavior changes, require updated upgrade notes and mention new npm scripts.

Files:

  • docs/reference/storage-paths.md
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/storage.test.ts
  • test/codex-manager-cli.test.ts
🔇 Additional comments (3)
test/storage.test.ts (1)

333-336: good windows-safe teardown change.

line 335 in test/storage.test.ts:335 uses removeWithRetry, which reduces eperm/ebusy teardown flakes on windows.

test/codex-manager-cli.test.ts (2)

506-539: good deterministic mock reset/default setup.

line 506 through line 539 in test/codex-manager-cli.test.ts correctly resets and reseeds new restore-related mocks, which prevents cross-test state bleed.


2430-2500: good restore-failure recovery coverage.

line 2430 through line 2500 in test/codex-manager-cli.test.ts verifies restore failure is surfaced and the login flow continues, which is the right user-safety behavior.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/storage.test.ts">

<violation number="1" location="test/storage.test.ts:1208">
P3: Duplicate test case: this restore-after-deletion scenario is already covered earlier in the same file.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

@coderabbitai full review

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

@greptileai

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

✅ Actions performed

Full review triggered.

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

@cubic-dev-ai review this PR

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Mar 14, 2026

@cubic-dev-ai review this PR

@ndycode I have started the AI code review. It will take a few minutes to complete.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/storage.test.ts">

<violation number="1" location="test/storage.test.ts:1351">
P2: Use `removeWithRetry` instead of bare `fs.rm` to avoid transient Windows file-lock failures in tests.</violation>
</file>

<file name="lib/storage.ts">

<violation number="1" location="lib/storage.ts:1807">
P2: Do not swallow non-ENOENT errors when scanning named backups; rethrow them so restore reports the real filesystem failure instead of a misleading “file not found.”</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

@greptileai

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

@cubic-dev-ai review this PR

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

@coderabbitai full review

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Mar 14, 2026

@cubic-dev-ai review this PR

@ndycode I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

✅ Actions performed

Full review triggered.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 9 files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
lib/codex-manager.ts (1)

4209-4213: ⚠️ Potential issue | 🟡 Minor

add manager-level vitest for unreadable backup directory path

lib/codex-manager.ts:4209-4213 depends on listNamedBackups() surfacing real fs failures. please add a manager-path regression in test/codex-manager-cli.test.ts:2365-2799 that mocks listNamedBackups rejecting with eperm/ebusy and asserts the restore flow reports failure (not a misleading empty-backup state).

i can draft the vitest case and expected assertions if you want.
As per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-manager.ts` around lines 4209 - 4213, The restore flow currently
treats errors from listNamedBackups() as "no backups" — update tests to ensure
real FS errors surface: add a manager-level vitest in
test/codex-manager-cli.test.ts that mocks listNamedBackups to reject with an
EPERM/EBUSY-like error and asserts the CLI/restore command (the code path that
calls getNamedBackupsDirectoryPath() and listNamedBackups()) returns a failure
exit/throws and prints an error message (not the "No named backups found..."
message); reference mocking the listNamedBackups function and exercising the
restore invocation so that the implementation for listNamedBackups/restore
surfaces filesystem errors instead of treating them as empty result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/codex-manager-cli.test.ts`:
- Around line 2365-2881: Add a deterministic regression test that simulates a
manager-level EPERM from backup discovery by having listNamedBackupsMock reject
once with an error object whose code === "EPERM" (e.g. Object.assign(new
Error(...), { code: "EPERM" }) ), then call
runCodexMultiAuthCli(["auth","login"]) with interactive TTY and
promptLoginModeMock set to enter/exit the restore-backup flow; assert the CLI
handled the EPERM (e.g. promptLoginModeMock was called again / the flow returned
to the login menu) and that a warning or error was logged (spy on console.warn
or console.error), ensuring this suite covers the EPERM path alongside the
existing success/failure cases referenced by listNamedBackupsMock,
promptLoginModeMock, selectMock, and runCodexMultiAuthCli.

In `@test/storage.test.ts`:
- Around line 1209-1309: Add a deterministic regression test that creates a
symlink/junction inside the backups directory that points outside the backups
root and asserts that listNamedBackups() does not surface that symlink entry and
that assessNamedBackupRestore()/restoreNamedBackup() reject attempts to use its
name; specifically, in the new test use the same helpers around
createNamedBackup/assessNamedBackupRestore/restoreNamedBackup/listNamedBackups
to create a real file outside the backups folder, create a symlink (or Windows
junction) to it inside the backups folder, then call listNamedBackups()
expecting the symlink entry to be absent and call
assessNamedBackupRestore(symlinkName) and restoreNamedBackup(symlinkName)
expecting them to throw; ensure the test is deterministic (use vitest
utilities), cleans up the symlink/junction, and does not rely on real secrets or
external network.
- Around line 1249-1300: The tests for deleted/corrupt backups currently only
assert that restoreNamedBackup(...) throws, but do not verify it didn't mutate
active storage; after the rejection add assertions that loadAccounts() (and/or
getActive accounts via the existing storage API such as loadAccounts()) still
returns an empty state to ensure restoreNamedBackup() is atomic and leaves
active storage untouched; update both test cases that call
restoreNamedBackup("deleted-after-assessment") and
restoreNamedBackup("invalid-after-assessment") to await loadAccounts() and
expect the returned accounts/state to remain empty (use the same helpers
clearAccounts() and loadAccounts() used elsewhere in the file).

---

Duplicate comments:
In `@lib/codex-manager.ts`:
- Around line 4209-4213: The restore flow currently treats errors from
listNamedBackups() as "no backups" — update tests to ensure real FS errors
surface: add a manager-level vitest in test/codex-manager-cli.test.ts that mocks
listNamedBackups to reject with an EPERM/EBUSY-like error and asserts the
CLI/restore command (the code path that calls getNamedBackupsDirectoryPath() and
listNamedBackups()) returns a failure exit/throws and prints an error message
(not the "No named backups found..." message); reference mocking the
listNamedBackups function and exercising the restore invocation so that the
implementation for listNamedBackups/restore surfaces filesystem errors instead
of treating them as empty result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4009a67a-dfe7-4ea8-8a2b-d3537be183d8

📥 Commits

Reviewing files that changed from the base of the PR and between 17d943e and f5a3e00.

📒 Files selected for processing (4)
  • lib/codex-manager.ts
  • lib/storage.ts
  • test/codex-manager-cli.test.ts
  • test/storage.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/codex-manager.ts
  • lib/storage.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/codex-manager-cli.test.ts
  • test/storage.test.ts
🔇 Additional comments (2)
lib/storage.ts (1)

1581-1650: good hardening on backup listing and transient fs retries

lib/storage.ts:1581-1650 now bounds fan-out and retries transient eperm/ebusy/eagain failures before bubbling errors. this is the right direction for windows filesystem stability and concurrency control.

lib/codex-manager.ts (1)

4218-4245: nice fail-open assessment fan-out with bounded concurrency

lib/codex-manager.ts:4218-4245 uses chunked promise.allsettled with NAMED_BACKUP_LIST_CONCURRENCY, so one bad backup assessment no longer blocks restore options for healthy backups.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/storage.test.ts">

<violation number="1" location="test/storage.test.ts:1348">
P3: The traversal test matcher is still too broad (`not found`), so it can pass on generic missing-file failures instead of validating the security guard.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
lib/storage.ts (1)

2682-2692: 🧹 Nitpick | 🔵 Trivial

transaction path guard works but lacks windows path normalization.

the comparison at lib/storage.ts:2685 is a direct string equality check. on windows, equivalent paths can differ by case (e.g., C:\Users\... vs c:\users\...). the past review at these lines noted this concern.

test at test/storage.test.ts:373-399 exercises the different-path scenario but doesn't cover case-variant paths on windows.

consider adding path normalization for windows:

+ const normalizePath = (p: string): string =>
+   process.platform === "win32" ? p.toLowerCase() : p;
  const currentStoragePath = getStoragePath();
  const storage = transactionState?.active
-   ? transactionState.storagePath === currentStoragePath
+   ? normalizePath(transactionState.storagePath) === normalizePath(currentStoragePath)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 2682 - 2692, The direct string equality check
comparing transactionState.storagePath to getStoragePath() fails on Windows due
to case-insensitive paths; update the guard in the transaction snapshot logic
(where transactionSnapshotContext, getStoragePath, transactionState.storagePath,
snapshot, and withAccountStorageTransaction are used around exportAccounts) to
normalize both paths before comparing: import/require Node's path,
resolve/normalize both storage paths and, when process.platform === 'win32',
compare their lowercased forms (or use path.win32.normalize) so equivalent
Windows paths match; keep the thrown Error message ("exportAccounts called
inside an active transaction for a different storage path") intact when the
normalized paths differ.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/codex-manager.ts`:
- Line 4210: The variable "backups" is declared without a type causing a
noImplicitAnyLet lint error; change its declaration to explicitly type it as the
resolved value of listNamedBackups(): use "let backups: NamedBackupMetadata[] |
undefined;" (or "const backups: NamedBackupMetadata[]" if you can await
immediately) and ensure the code that assigns "backups = await
listNamedBackups(...)" (reference listNamedBackups and the backups variable in
lib/codex-manager.ts) matches the new type so the linter no longer reports an
implicit any.

In `@lib/storage.ts`:
- Around line 1628-1650: The retryTransientFilesystemOperation backoff uses
strict exponential delays without jitter, which can cause thundering-herd under
contention; update retryTransientFilesystemOperation to add randomized jitter to
the sleep before retry (similar to renameFileWithRetry), e.g. compute baseDelay
= 10 * 2 ** attempt and apply a small random factor (like multiply by 0.5–1.5 or
add +/- jitter) when awaiting the sleep in the existing await new Promise(...)
call, preserve the same max attempts and keep isRetryableFilesystemErrorCode and
error handling unchanged.

In `@test/codex-manager-cli.test.ts`:
- Around line 2788-2872: Add a new deterministic vitest case mirroring the
existing "reassesses a backup..." test but make the second call to
assessNamedBackupRestoreMock reject with an error whose code is 'EBUSY' or
'EPERM' (simulate the reassessment rejection path in lib/codex-manager.ts at the
reassess branch), then assert that runCodexMultiAuthCli(["auth","login"])
returns to the menu (promptLoginModeMock resolves back to menu) and that
restoreNamedBackupMock is not called; reference and reuse the same mocks used in
the file (assessNamedBackupRestoreMock, listNamedBackupsMock,
promptLoginModeMock, selectMock, confirmMock, restoreNamedBackupMock) and assert
appropriate expectations: assessNamedBackupRestoreMock called twice with
"named-backup" (second call rejected), promptLoginModeMock was invoked to return
to menu, and restoreNamedBackupMock was not invoked.

In `@test/storage.test.ts`:
- Around line 1345-1355: The test uses an overly broad alternation for
path-traversal errors; update the expectations in the two calls to
assessNamedBackupRestore and restoreNamedBackup so they assert the specific
error text thrown by the implementation (use the exact message or a tighter
regex from lib/storage.ts lines ~1861-1863 or the error/message exported by
lib/named-backup-export.ts) instead of the current wide /invalid|not
allowed|escape|traversal|not found|path separators/i pattern; either import the
exported error/message constant from lib/named-backup-export.ts (or copy the
exact string used in lib/storage.ts) and assert against that value (or a narrow
regex like /contains path separator/i) to make the test precise.

---

Duplicate comments:
In `@lib/storage.ts`:
- Around line 2682-2692: The direct string equality check comparing
transactionState.storagePath to getStoragePath() fails on Windows due to
case-insensitive paths; update the guard in the transaction snapshot logic
(where transactionSnapshotContext, getStoragePath, transactionState.storagePath,
snapshot, and withAccountStorageTransaction are used around exportAccounts) to
normalize both paths before comparing: import/require Node's path,
resolve/normalize both storage paths and, when process.platform === 'win32',
compare their lowercased forms (or use path.win32.normalize) so equivalent
Windows paths match; keep the thrown Error message ("exportAccounts called
inside an active transaction for a different storage path") intact when the
normalized paths differ.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: df6cb1b4-fc90-4962-b71a-828f6781fb52

📥 Commits

Reviewing files that changed from the base of the PR and between f5a3e00 and a12a748.

📒 Files selected for processing (4)
  • lib/codex-manager.ts
  • lib/storage.ts
  • test/codex-manager-cli.test.ts
  • test/storage.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/storage.test.ts
  • test/codex-manager-cli.test.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/codex-manager.ts
  • lib/storage.ts
🪛 Biome (2.4.6)
lib/codex-manager.ts

[error] 4210-4210: This variable implicitly has the any type.

(lint/suspicious/noImplicitAnyLet)

🔇 Additional comments (26)
lib/storage.ts (9)

119-141: interfaces look solid.

the nullable fields are typed correctly, and the structure captures all needed restore assessment info without exposing sensitive account data.


1581-1626: listing implementation handles windows edge cases well.

symlink filtering at lib/storage.ts:1588, case-insensitive .json matching at lib/storage.ts:1589, and bounded concurrency via chunks are all correct. the retry wrapper handles transient EBUSY/EPERM.

tests at test/storage.test.ts:1357-1411 (symlink), test/storage.test.ts:1249-1287 (uppercase .JSON), and test/storage.test.ts:1645-1696 (concurrency) cover these paths.


1669-1723: assessment logic handles edge cases correctly.

explicit null vs undefined distinction at lib/storage.ts:1680-1683 is important for zero-account recovery flows. test at test/storage.test.ts:1169-1207 verifies this behavior.


1725-1730: restore delegation is correct.

the function properly delegates to importAccounts which handles the storage mutex internally. test at test/storage.test.ts:1289-1315 verifies the TOCTOU edge case where backup is deleted after assessment but before restore.


1755-1773: backup candidate loading with retry is correct.

the retry wrapper at lib/storage.ts:1762-1764 handles transient windows errors. tests at test/storage.test.ts:1563-1603 verify retry behavior for both read and stat operations.


1787-1837: directory-listing-based path resolution is secure.

using readdir and matching entries at lib/storage.ts:1805-1816 prevents path traversal attacks. symlink rejection at lib/storage.ts:1817-1821 is a good defense. tests at test/storage.test.ts:1345-1355 verify traversal rejection and test/storage.test.ts:1357-1411 verifies symlink handling.


1839-1867: path resolution fallback logic is correct.

the function correctly distinguishes between "valid manual backup name that doesn't exist" (file-not-found at lib/storage.ts:1861-1863) vs "invalid name format" (rethrow at lib/storage.ts:1865). this allows manual backups with spaces like "Manual Backup.json" while still validating programmatic names.


2077-2119: metadata construction handles platform differences.

the fallback from birthtimeMs to ctimeMs at lib/storage.ts:2104 handles platforms where birthtime isn't available. retry wrapper at lib/storage.ts:2090 handles transient windows errors.


2256-2261: transaction state correctly captures storagePath at transaction start.

both withAccountStorageTransaction and withAccountAndFlaggedStorageTransaction capture storagePath before entering the transaction context, ensuring the path is consistent throughout.

Also applies to: 2283-2288

test/storage.test.ts (14)

335-338: cleanup using removeWithRetry is correct.

this prevents flaky test failures on windows where temp directories may be briefly locked.


373-399: transaction path guard test is correct.

the test properly exercises the error path when exportAccounts is called inside a transaction for a different storage path. the finally block ensures cleanup even if the test fails.


1112-1139: create and list test is deterministic with proper assertions.


1169-1207: explicit null currentStorage test is important for zero-account recovery.

this verifies the distinction between undefined (load from disk) and null (treat as empty), which is critical for the recovery flow.


1209-1287: manual backup tests cover important edge cases.

space-in-name at test/storage.test.ts:1213 and uppercase .JSON at test/storage.test.ts:1253 are critical for windows compatibility where users may create backups manually.


1289-1343: post-assessment failure tests verify atomicity.

assertions at test/storage.test.ts:1314 and test/storage.test.ts:1342 verify that loadAccounts() returns empty after failed restores, ensuring the restore operation is atomic.


1345-1355: path traversal tests cover both unix and windows styles.


1357-1411: symlink filtering test uses mock appropriately.

the mock approach tests the code path in lib/storage.ts:1588 and lib/storage.ts:1817. for real symlink/junction behavior on windows, the isSymbolicLink() method should work, but that's a node.js behavior rather than something we can unit test.


1413-1439: unreadable directory tests verify error propagation.

these tests ensure that EPERM errors bubble up rather than being swallowed, addressing the past review concern about masking real errors.


1441-1526: transient directory error retry tests are correct.

tests at test/storage.test.ts:1441-1484 (EBUSY) and test/storage.test.ts:1486-1526 (EAGAIN) verify the retry logic in retryTransientFilesystemOperation.


1528-1561: TOCTOU test for manual backups is correct.

this verifies the edge case where a user deletes a manual backup between assessment and restore.


1563-1643: read and stat retry tests verify candidate loading resilience.

these tests cover the retry logic in loadBackupCandidate (readFile) and buildNamedBackupMetadata (stat).


1645-1696: concurrency limit test uses exported constant.

this addresses the past review concern about hardcoding the magic number 8. the test now references NAMED_BACKUP_LIST_CONCURRENCY at test/storage.test.ts:1690-1691.


1698-1767: concurrent restore serialization test is critical.

this test verifies that the storage mutex properly serializes concurrent restore attempts, preventing the account limit from being exceeded. the assertion at test/storage.test.ts:1766 confirms exactly MAX_ACCOUNTS after both attempts.

lib/codex-manager.ts (2)

3825-3865: good recovery-flow hoist for zero-account login.

lib/codex-manager.ts:3825-3865 now drives the menu from existingStorage ?? createEmptyAccountStorage(), so restore actions are reachable even when account storage is empty. this closes the lockout path cleanly.


4235-4256: good non-fail-fast assessment fan-out.

lib/codex-manager.ts:4235-4256 uses promise.allsettled with chunking, so one backup assessment failure no longer aborts the whole restore list. this is the right concurrency behavior for windows/transient fs failures.

test/codex-manager-cli.test.ts (1)

2365-2913: strong restore-flow regression coverage here.

test/codex-manager-cli.test.ts:2365-2913 now covers confirm decline, restore failure recovery, windows eperm listing failure, partial assessment failure isolation, chunked concurrency cap, zero-account entry, and pre-confirm reassessment. this is a solid safety net for the new recovery path.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (2)
lib/storage.ts (2)

1847-1861: ⚠️ Potential issue | 🟠 Major

preserve validation errors from restore-path resolution.

at lib/storage.ts:1847-1861 (line [1847]), the catch path can replace buildNamedBackupPath validation errors with import file not found. this hides the real cause for invalid names and makes diagnosis harder.

please add a regression in test/storage.test.ts:1 that passes an invalid backup name and asserts the original validation error is surfaced.

proposed fix
 async function resolveNamedBackupRestorePath(name: string): Promise<string> {
 	const existingPath = await findExistingNamedBackupPath(name);
 	if (existingPath) {
 		return existingPath;
 	}
-	const requested = (name ?? "").trim();
-	const backupRoot = getNamedBackupRoot(getStoragePath());
-	const requestedWithExtension = requested.toLowerCase().endsWith(".json")
-		? requested
-		: `${requested}.json`;
-	try {
-		return buildNamedBackupPath(name);
-	} catch (error) {
-		const baseName = requestedWithExtension.toLowerCase().endsWith(".json")
-			? requestedWithExtension.slice(0, -".json".length)
-			: requestedWithExtension;
-		if (
-			requested.length > 0 &&
-			basename(requestedWithExtension) === requestedWithExtension &&
-			!requestedWithExtension.includes("..") &&
-			!/^[A-Za-z0-9_-]+$/.test(baseName)
-		) {
-			throw new Error(
-				`Import file not found: ${resolvePath(join(backupRoot, requestedWithExtension))}`,
-			);
-		}
-		throw error;
-	}
+	return buildNamedBackupPath(name);
 }

as per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 1847 - 1861, The catch block around
buildNamedBackupPath is swallowing validation errors and replacing them with a
generic "Import file not found" message; change the handler in the catch to
rethrow the original error for validation-related failures (i.e. when
buildNamedBackupPath(name) throws a validation error) and only fall back to the
existing requestedWithExtension/file-not-found logic when the original error is
specifically a missing-file resolution scenario; update the catch to inspect the
thrown error and either throw it (preserving the validation message) or proceed
with the requestedWithExtension basename/../ checks and the existing
resolvePath(join(backupRoot, requestedWithExtension)) error; also add a vitest
regression in test/storage.test.ts that calls the restore path resolution with
an invalid backup name and asserts the original validation error is surfaced
(reference buildNamedBackupPath, requestedWithExtension, requested, and the
file-not-found branch).

2681-2689: ⚠️ Potential issue | 🟠 Major

normalize transaction/storage path comparison on windows.

at lib/storage.ts:2681-2689 (line [2681]), raw string comparison can fail for same-path case variants on windows and incorrectly throw "different storage path".

please add a windows-style case-variant regression in test/storage.test.ts:1 for exportAccounts transaction guard.

proposed fix
 	const transactionState = transactionSnapshotContext.getStore();
-	const currentStoragePath = getStoragePath();
+	const normalizeStoragePathForComparison = (value: string): string => {
+		const resolved = resolvePath(value);
+		return process.platform === "win32" ? resolved.toLowerCase() : resolved;
+	};
+	const currentStoragePath = normalizeStoragePathForComparison(getStoragePath());
 	const storage = transactionState?.active
-		? transactionState.storagePath === currentStoragePath
+		? normalizeStoragePathForComparison(transactionState.storagePath) === currentStoragePath
 			? transactionState.snapshot
 			: (() => {
 					throw new Error(
 						"exportAccounts called inside an active transaction for a different storage path",
 					);
 				})()
 		: await withAccountStorageTransaction((current) => Promise.resolve(current));

as per coding guidelines, lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/storage.ts` around lines 2681 - 2689, The transaction path comparison in
exportAccounts uses raw string equality between transactionState.storagePath and
getStoragePath(), which can fail on Windows due to case and separator variants;
update the guard in lib/storage.ts to compare normalized, resolved paths (use
path.resolve + path.normalize) and, when process.platform === 'win32', compare
the lowercased normalized paths to ensure case-insensitive equality for Windows;
update references to transactionState.storagePath and getStoragePath() in that
block to use the normalized/resolved values and throw only if those normalized
values differ; also add a vitest regression in test/storage.test.ts that creates
a transaction with a Windows-style case-variant path and asserts exportAccounts
does not throw for the same logical path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/codex-manager.ts`:
- Around line 3834-3854: The pendingMenuQuotaRefresh background task can race
with foreground quota actions and overwrite fresher entries; modify the code
paths that start runHealthCheck, runForecast, and runFix to first serialize with
pendingMenuQuotaRefresh (e.g., if pendingMenuQuotaRefresh is non-null await it
or acquire a simple mutex before starting any read-modify-write on quotaCache)
so that refreshQuotaCacheForMenu and those functions never run concurrent
writers against quotaCache; update logic that uses pendingMenuQuotaRefresh,
countMenuQuotaRefreshTargets, and refreshQuotaCacheForMenu to ensure writers are
awaited/queued and still clear pendingMenuQuotaRefresh in finally; then add a
regression test in test/codex-manager-cli.test.ts that starts a
pendingMenuQuotaRefresh (mock or stub refreshQuotaCacheForMenu to delay) and
concurrently invokes runHealthCheck/runForecast/runFix, asserting the quotaCache
keys are preserved (no lost/stale keys) after both operations complete.

In `@test/codex-manager-cli.test.ts`:
- Around line 2475-2539: Add an assertion that the login-mode prompt was invoked
twice to verify the decline path returns to the recovery menu: after calling
runCodexMultiAuthCli([...]) add
expect(promptLoginModeMock).toHaveBeenCalledTimes(2) (reference mocks:
promptLoginModeMock, confirmMock, selectMock and the entry function
runCodexMultiAuthCli used in the test case "does not restore a named backup when
confirmation is declined"); this ensures the second mockResolvedValue for
promptLoginModeMock is consumed and the flow loops back rather than exiting
early.
- Around line 2958-2997: The test is brittle because it asserts a
locale-specific formatted date (new Date(0).toLocaleDateString()); update the
assertion in the test that checks selectMock's hint (backupItems?.[0]?.hint) to
avoid exact localized matching — instead assert that the hint contains the
stable prefix "updated " (e.g., expect(...).toContain("updated ")) or otherwise
verify that the updatedAt value is rendered (not dropped) by checking presence
of the word "updated" and a numeric year/epoch token, leaving
runCodexMultiAuthCli, selectMock and the backup assessment structure unchanged.

In `@test/storage.test.ts`:
- Around line 1645-1696: The test currently creates a fixed 12 backups which can
fail to saturate the limiter if NAMED_BACKUP_LIST_CONCURRENCY >= 12; change the
workload to be dynamic by deriving the number of backups from
NAMED_BACKUP_LIST_CONCURRENCY (e.g. const total = NAMED_BACKUP_LIST_CONCURRENCY
+ X) and use that total when creating backups with
saveAccounts/createNamedBackup and when asserting
expect(backups).toHaveLength(...); apply the same change in the other test
referenced (test/codex-manager-cli.test.ts around the create loop at line ~2719)
so both tests always exceed NAMED_BACKUP_LIST_CONCURRENCY and reliably exercise
the concurrency limiter used by listNamedBackups.
- Around line 1413-1526: Add a deterministic test that exercises the transient
"EPERM" retry path by modifying the existing retry tests: in either the "retries
transient backup directory errors while listing backups" or "while restoring
backups" block, change the readdir mock (the vi.spyOn(fs,
"readdir").mockImplementation used with backupRoot and originalReaddir) to throw
a NodeJS.ErrnoException with error.code = "EPERM" exactly once (increment
busyFailures) and then return originalReaddir for subsequent calls, then assert
the operation still succeeds and busyFailures === 1; ensure you reference
listNamedBackups or restoreNamedBackup and restore the spy with
readdirSpy.mockRestore() in finally so the test stays deterministic.

---

Duplicate comments:
In `@lib/storage.ts`:
- Around line 1847-1861: The catch block around buildNamedBackupPath is
swallowing validation errors and replacing them with a generic "Import file not
found" message; change the handler in the catch to rethrow the original error
for validation-related failures (i.e. when buildNamedBackupPath(name) throws a
validation error) and only fall back to the existing
requestedWithExtension/file-not-found logic when the original error is
specifically a missing-file resolution scenario; update the catch to inspect the
thrown error and either throw it (preserving the validation message) or proceed
with the requestedWithExtension basename/../ checks and the existing
resolvePath(join(backupRoot, requestedWithExtension)) error; also add a vitest
regression in test/storage.test.ts that calls the restore path resolution with
an invalid backup name and asserts the original validation error is surfaced
(reference buildNamedBackupPath, requestedWithExtension, requested, and the
file-not-found branch).
- Around line 2681-2689: The transaction path comparison in exportAccounts uses
raw string equality between transactionState.storagePath and getStoragePath(),
which can fail on Windows due to case and separator variants; update the guard
in lib/storage.ts to compare normalized, resolved paths (use path.resolve +
path.normalize) and, when process.platform === 'win32', compare the lowercased
normalized paths to ensure case-insensitive equality for Windows; update
references to transactionState.storagePath and getStoragePath() in that block to
use the normalized/resolved values and throw only if those normalized values
differ; also add a vitest regression in test/storage.test.ts that creates a
transaction with a Windows-style case-variant path and asserts exportAccounts
does not throw for the same logical path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 72c679b6-32f2-4246-b6ef-7822d19589ee

📥 Commits

Reviewing files that changed from the base of the PR and between a12a748 and ee7a402.

📒 Files selected for processing (4)
  • lib/codex-manager.ts
  • lib/storage.ts
  • test/codex-manager-cli.test.ts
  • test/storage.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/codex-manager.ts
  • lib/storage.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/storage.test.ts
  • test/codex-manager-cli.test.ts
🔇 Additional comments (2)
lib/codex-manager.ts (1)

4235-4256: good fail-soft assessment fan-out.

lib/codex-manager.ts:4235-4256 (line [4235]) uses promise.allsettled with per-item warnings, so one bad backup does not block other restore candidates. this is the right resilience behavior for recovery UX.

lib/storage.ts (1)

1581-1626: good windows retry + error surfacing in named-backup listing.

lib/storage.ts:1581-1626 and lib/storage.ts:1634-1650 (line [1581], line [1634]) correctly retry eperm/ebusy/eagain, return [] only for enoent, and rethrow other failures. this keeps transient windows behavior robust without masking real directory faults.

Also applies to: 1634-1650

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

@coderabbitai full review
@cubic-dev-ai review this PR
@greptileai

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Mar 14, 2026

@coderabbitai full review
@cubic-dev-ai review this PR
@greptileai

@ndycode I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

@ndycode — triggering a full review of pr #76 now. will cover:

  • lib/storage.ts — triple-negation in findExistingNamedBackupPath, the misleading catch in resolveNamedBackupRestorePath that swallows validation errors with "import file not found", NAMED_BACKUP_LIST_CONCURRENCY bounding, and symlink rejection + toctou guards
  • lib/codex-manager.tsrunBackupRestoreManager fan-out, re-assessment before confirm, error containment back to login menu
  • lib/cli.ts — new restore-backup login mode variant and fallback alias wiring
  • lib/ui/auth-menu.ts / lib/ui/copy.ts — recovery section ui and copy additions
  • test/storage.test.ts / test/codex-manager-cli.test.ts — concurrency regression, windows eperm retry, uppercase .JSON, toctou delete-after-assessment, partial assessment failure coverage
✅ Actions performed

Full review triggered.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 9 files

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

@coderabbitai full review
@cubic-dev-ai review this PR
@greptileai

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Mar 14, 2026

@coderabbitai full review
@cubic-dev-ai review this PR
@greptileai

@ndycode I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

@ndycode — triggering a full review of pr #76 now. scope will cover:

  • lib/storage.ts — triple-negation in findExistingNamedBackupPath, swallowed validation error in resolveNamedBackupRestorePath catch block replacing real errors with "import file not found", NAMED_BACKUP_LIST_CONCURRENCY bounding, symlink rejection, toctou re-assessment guards, and retryTransientFilesystemOperation eperm/ebusy/eagain coverage
  • lib/codex-manager.tsrunBackupRestoreManager fan-out concurrency, re-assessment-before-confirm flow, error containment back to login menu, formatRelativeDateShort edge cases, and the refactored runAuthLogin loop
  • lib/cli.tsrestore-backup mode variant, fallback alias wiring for "u"/"backup"/"restore"
  • lib/ui/auth-menu.ts / lib/ui/copy.ts — recovery section ui injection and copy string additions
  • test/storage.test.ts / test/codex-manager-cli.test.ts — concurrency regression, windows eperm retry, uppercase .JSON restore, toctou delete-after-assessment, partial assessment failure, symlink rejection, re-assessment freshness, and zero-account recovery paths
✅ Actions performed

Full review triggered.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 9 files

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/codex-manager.ts">

<violation number="1" location="lib/codex-manager.ts:4348">
P2: Calling `importAccounts` directly bypasses the final restore eligibility check, so a backup that becomes duplicate-only after confirmation can still be persisted as a no-op write.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

lib/storage.ts Outdated
)),
);
}
return backups.sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
Copy link

Choose a reason for hiding this comment

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

The backup sort will break if updatedAt is NaN. The nullish coalescing operator (??) only handles null and undefined, not NaN. When updatedAt is NaN, the expression (NaN ?? 0) returns NaN, and NaN - NaN also returns NaN, causing unpredictable sort behavior. The test at line 3115 confirms NaN timestamps are expected. Fix:

return backups.sort((left, right) => {
  const leftTime = Number.isFinite(left.updatedAt) ? left.updatedAt : 0;
  const rightTime = Number.isFinite(right.updatedAt) ? right.updatedAt : 0;
  return rightTime - leftTime;
});
Suggested change
return backups.sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
return backups.sort((left, right) => {
const leftTime = Number.isFinite(left.updatedAt) ? left.updatedAt : 0;
const rightTime = Number.isFinite(right.updatedAt) ? right.updatedAt : 0;
return rightTime - leftTime;
});

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/storage.ts">

<violation number="1" location="lib/storage.ts:2825">
P1: The new `imported === 0` early return can skip valid account updates. Duplicate-only imports may still replace existing entries with newer metadata, but this branch returns before `persist(newStorage)`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

@coderabbitai full review
@cubic-dev-ai review this PR
@greptileai

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Mar 14, 2026

@coderabbitai full review
@cubic-dev-ai review this PR
@greptileai

@ndycode I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

@ndycode — triggering a full review of pr #76 now. scope will cover:

  • lib/storage.ts — triple-negation in findExistingNamedBackupPath, swallowed validation error in resolveNamedBackupRestorePath catch replacing real errors with "import file not found", NAMED_BACKUP_LIST_CONCURRENCY bounding, symlink rejection, toctou re-assessment guards, retryTransientFilesystemOperation eperm/ebusy/eagain coverage, and the new storagePath transaction guard in exportAccounts
  • lib/codex-manager.tsrunBackupRestoreManager fan-out concurrency, re-assessment-before-confirm flow, error containment back to login menu, formatRelativeDateShort edge cases (midnight rollover, locale), and the refactored runAuthLogin loop
  • lib/cli.tsrestore-backup mode variant, fallback alias wiring for "u"/"backup"/"restore"
  • lib/ui/auth-menu.ts / lib/ui/copy.ts — recovery section ui injection and copy string additions
  • test/storage.test.ts / test/codex-manager-cli.test.ts — concurrency regression, windows eperm retry, uppercase .JSON restore, toctou delete-after-assessment, partial assessment failure, symlink rejection, re-assessment freshness, and zero-account recovery paths
✅ Actions performed

Full review triggered.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/storage.ts">

<violation number="1" location="lib/storage.ts:1728">
P2: Restore assessment incorrectly blocks metadata-only backup restores by treating `imported === 0` as a hard no-op.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const skipped = wouldExceedLimit
? null
: Math.max(0, candidate.normalized.accounts.length - (imported ?? 0));
const nothingToImport = !wouldExceedLimit && imported === 0;
Copy link

@cubic-dev-ai cubic-dev-ai bot Mar 14, 2026

Choose a reason for hiding this comment

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

P2: Restore assessment incorrectly blocks metadata-only backup restores by treating imported === 0 as a hard no-op.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/storage.ts, line 1728:

<comment>Restore assessment incorrectly blocks metadata-only backup restores by treating `imported === 0` as a hard no-op.</comment>

<file context>
@@ -1551,6 +1582,179 @@ export async function getRestoreAssessment(): Promise<RestoreAssessment> {
+	const skipped = wouldExceedLimit
+		? null
+		: Math.max(0, candidate.normalized.accounts.length - (imported ?? 0));
+	const nothingToImport = !wouldExceedLimit && imported === 0;
+
+	return {
</file context>
Suggested change
const nothingToImport = !wouldExceedLimit && imported === 0;
const changed = !haveEquivalentAccountRows(mergedAccounts, currentAccounts);
const nothingToImport = !wouldExceedLimit && !changed;
Fix with Cubic

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.

1 participant