Skip to content

feat(auth): add backup restore manager#76

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

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

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 12, 2026

Summary

  • tighten named-backup restore containment and transient filesystem retry behavior in the restore/list flows
  • baseline restore assessment and import counts on deduplicated current data so duplicate snapshots cannot skew imported/skipped math
  • clarify the quota-refresh drain gate in the login menu and add focused regressions for containment and retry behavior

Why

These follow-up fixes close the remaining review gaps around backup-path safety, Windows-heavy retry semantics, and restore accounting accuracy.

Validation

  • npx eslint lib/storage.ts lib/codex-manager.ts test/storage.test.ts
  • npx vitest run test/storage.test.ts --maxWorkers=1
  • npx vitest run test/codex-manager-cli.test.ts --maxWorkers=1

Risk

  • low to medium: touches restore-path containment, retry classification, and restore/import accounting in the backup manager flow

Rollback

  • revert commit 16cc8d3 on git-plan/02-backup-restore-manager

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 flow into codex auth login, hardening the path containment layer (assertNamedBackupRestorePath + resolvePathForNamedBackupContainment), adding windows-aware retry logic (retryTransientFilesystemOperation for EAGAIN/EBUSY/EPERM), and keeping CLI auth state in sync after a successful restore via autoSyncActiveAccountToCodex. the new runBackupRestoreManager function in lib/codex-manager.ts correctly drains any in-flight quota refresh before entering the restore menu, re-assesses the selected backup with a fresh storage snapshot before confirmation, and propagates containment errors as hard failures while gracefully skipping other assessment failures.

key areas to check:

  • windows concurrency: retryTransientFilesystemOperation covers readdir, readFile, and stat in the listing/assessment path; importAccounts adds a retry on readFile; the test suite verifies retry exhaustion (EAGAIN × 7 attempts) and partial-chunk retry on EBUSY. ENOTEMPTY is only needed for test-cleanup (removeWithRetry) and is correctly absent from the production retry list.
  • token safety: log calls in the listing/assessment path log path and stringified error only — no account data or token fields are included. collapseWhitespace on error messages further reduces leakage surface in user-facing output.
  • containment regression tests added: test/storage.test.ts covers symlinked backup roots, symlinked subdirectories, injected readdir entries with path traversal, EPERM from realpathSync, and the happy path for leading-dot names that stay inside the boundary.
  • one latent gap: resolvePathForNamedBackupContainment falls back to the raw resolvePath result for non-existent files, meaning symlinks in intermediate path components are not resolved. not exploitable today (backup name separators are blocked upstream), but a risk surface if assertNamedBackupRestorePath is called with compound paths in new code.
  • skipped count semantics: max(0, normalized.accounts.length - imported) overstates skipped when a backup file has internal duplicates; imported + skipped > total becomes possible, which is confusing in the success message.
  • missing test assertion: the "metadata refresh wording" test in test/codex-manager-cli.test.ts doesn't assert that autoSyncActiveAccountToCodex fires for the imported=0, changed=true path.

Confidence Score: 3/5

  • safe to merge after addressing the containment fallback gap and the skipped-count cosmetic issue; the metadata-only sync assertion is low-risk but worth locking down
  • core restore flow, path containment, and retry logic are solid and well-tested. the resolvePathForNamedBackupContainment fallback is a latent gap that isn't exploitable today but is an exported function, making it a regression risk. the skipped overcount is a cosmetic bug that will show confusing output to users. the missing sync assertion in the metadata-refresh test path leaves a behavioural guarantee unchecked.
  • lib/storage.ts — resolvePathForNamedBackupContainment fallback and importAccounts skipped count; test/codex-manager-cli.test.ts — metadata-refresh sync assertion

Important Files Changed

Filename Overview
lib/storage.ts adds listNamedBackups, assessNamedBackupRestore, restoreNamedBackup, assertNamedBackupRestorePath and retryTransientFilesystemOperation; resolvePathForNamedBackupContainment falls back to unresolved path for non-existent files, creating a latent containment gap; skipped count in importAccounts can exceed total when the backup itself has internal duplicates
lib/codex-manager.ts adds runBackupRestoreManager and wires restore-backup mode into the unified login loop; correctly drains in-flight quota refresh before restore, re-assesses after selection, and syncs CLI auth state after successful restore; test gaps for metadata-only sync
test/storage.test.ts good depth of new named-backup integration tests covering path containment, retry, deduplication, and metadata-refresh paths; outer afterEach updated to removeWithRetry; named backup tests share the same afterEach block so also get retry on cleanup
test/codex-manager-cli.test.ts extensive restore-menu test coverage; correctly mocks assertNamedBackupRestorePath indirection; missing assertion that autoSyncActiveAccountToCodex fires for metadata-only (imported=0, changed=true) restore path

Sequence Diagram

sequenceDiagram
    participant U as User
    participant M as runAuthLogin loop
    participant R as runBackupRestoreManager
    participant S as storage.ts
    participant C as Codex CLI

    U->>M: selects "Restore From Backup"
    M->>M: drain pendingMenuQuotaRefresh
    M->>R: runBackupRestoreManager(displaySettings)
    R->>S: listNamedBackups({ candidateCache })
    S-->>R: NamedBackupMetadata[]
    R->>S: assessNamedBackupRestore(name, { currentStorage, candidateCache })
    Note over S: cache evicted after first use
    S-->>R: BackupRestoreAssessment[]
    R->>U: select(items) — show backup menu
    U->>R: selects backup entry
    R->>S: assessNamedBackupRestore(name, { currentStorage: fresh }) — re-assess
    S-->>R: latestAssessment
    R->>U: confirm(message)
    U->>R: confirmed
    R->>S: assertNamedBackupRestorePath(path, backupDir)
    S-->>R: validatedBackupPath
    R->>S: importAccounts(validatedBackupPath)
    Note over S: retryTransientFilesystemOperation on EAGAIN/EBUSY/EPERM(win32)
    S-->>R: { imported, skipped, total, changed }
    alt changed === false
        R->>U: "All accounts already exist"
    else changed === true
        R->>C: autoSyncActiveAccountToCodex()
        C-->>R: synced
        R->>U: success message
    end
    R-->>M: return
    M->>U: back to login loop
Loading

Comments Outside Diff (1)

  1. lib/storage.ts, line 1826-1833 (link)

    symlink traversal gap for non-existent paths in containment fallback

    resolvePathForNamedBackupContainment falls back to the raw resolvePath result when the target doesn't exist yet:

    if (!existsSync(resolvedPath)) {
        return resolvedPath;   // symlinks in intermediate dirs are NOT resolved
    }

    if a symlinked subdirectory exists inside backupRoot (e.g. backups/linked/ -> /tmp/) but the target file hasn't been created yet, a call like assertNamedBackupRestorePath("backups/linked/steal.json", backupRoot) would pass the relative() containment check — the path looks like "linked/steal.json" relative to backupRoot even though its real destination is /tmp/steal.json.

    this isn't reachable today because buildNamedBackupPath rejects names with separators and findExistingNamedBackupPath only matches direct directory entries. but assertNamedBackupRestorePath is exported, so a future caller passing a compound path for a not-yet-existent file could bypass containment.

    safer fallback: use realpathSync.native on the longest existing prefix of the path, or resolve each path component individually.

Fix All in Codex

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

Comment:
**symlink traversal gap for non-existent paths in containment fallback**

`resolvePathForNamedBackupContainment` falls back to the raw `resolvePath` result when the target doesn't exist yet:

```ts
if (!existsSync(resolvedPath)) {
    return resolvedPath;   // symlinks in intermediate dirs are NOT resolved
}
```

if a symlinked subdirectory exists inside `backupRoot` (e.g. `backups/linked/ -> /tmp/`) but the target file hasn't been created yet, a call like `assertNamedBackupRestorePath("backups/linked/steal.json", backupRoot)` would pass the `relative()` containment check — the path looks like `"linked/steal.json"` relative to `backupRoot` even though its real destination is `/tmp/steal.json`.

this isn't reachable today because `buildNamedBackupPath` rejects names with separators and `findExistingNamedBackupPath` only matches direct directory entries. but `assertNamedBackupRestorePath` is exported, so a future caller passing a compound path for a not-yet-existent file could bypass containment.

safer fallback: use `realpathSync.native` on the longest existing prefix of the path, or resolve each path component individually.

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: 2981-2984

Comment:
**`skipped` overstated when backup has internal duplicates**

`skipped` is computed against `normalized.accounts.length`, which is the raw count from the backup file before deduplication against itself. if the backup has duplicate entries (e.g. `[A, A]`) and the current storage is empty:

- `imported = 1` (only one unique account lands)
- `skipped = max(0, 2 - 1) = 1`
- `total = 1`

the user would then see `"Imported 1, skipped 1, total 1"` — where `imported + skipped > total`, which is confusing.

consider computing `skipped` against the deduplicated backup count instead:

```suggestion
		const skipped = Math.max(
			0,
			deduplicateAccounts([...normalized.accounts]).length - imported,
		);
```

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/codex-manager-cli.test.ts
Line: 2659-2682

Comment:
**missing assertion: `autoSyncActiveAccountToCodex` for metadata-only restore path**

the test verifies the UI copy and that `importAccounts` was called, but doesn't assert that `setCodexCliActiveSelectionMock` fires when `changed: true` and `imported: 0`. the sync call matters here because a metadata-only restore can refresh a stale `accessToken` or `expiresAt` in CLI auth state — which is one of the main reasons the "metadata refresh" path exists.

add:

```ts
expect(setCodexCliActiveSelectionMock).toHaveBeenCalledOnce();
```

after the `logSpy` assertions to lock down this behaviour going forward. this is the same assertion used in "restores a named backup from the login recovery menu".

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

Last reviewed commit: 16cc8d3

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 named-backup restore support: new storage APIs for create/list/assess/restore, cli/ui wiring for a restore-backup login mode, a backup-restore manager in lib/codex-manager.ts, and broad tests exercising the feature.

Changes

Cohort / File(s) Summary
Documentation
docs/reference/storage-paths.md
added canonical and project-scoped named backup paths and a restore workflow entry.
CLI
lib/cli.ts
added "restore-backup" to LoginMode and mapped fallback keys (u, backup, restore) to it (lib/cli.ts:line ~1).
Storage layer
lib/storage.ts
introduced named-backup public APIs/types (NamedBackupMetadata, BackupRestoreAssessment, ImportAccountsResult, NAMED_BACKUP_LIST_CONCURRENCY, NAMED_BACKUP_ASSESS_CONCURRENCY, listNamedBackups(), getNamedBackupsDirectoryPath(), createNamedBackup(), assessNamedBackupRestore(), restoreNamedBackup()), concurrency-aware listing/assessment, retry helpers, and propagated storagePath through transactions (lib/storage.ts:line ~1-520).
Manager / orchestration
lib/codex-manager.ts
added BackupMenuAction type and runBackupRestoreManager(displaySettings); manager lists backups, batches assessments (concurrency-limited), re-assesses on selection, prompts confirm, and performs restore; integrated into auth/login flow and refactored auth loop (lib/codex-manager.ts:line ~1-500).
UI / copy
lib/ui/auth-menu.ts, lib/ui/copy.ts
added restore-backup action and menu item under a recovery section; added copy entries and confirmation/success messages for restore flows (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 extensive tests and mocks for listing/assessing/restoring named backups, confirmation prompt, concurrency and re-assessment behavior, fallback mapping for login prompts, and many edge cases (test/codex-manager-cli.test.ts:line ~1-1200, test/storage.test.ts:line ~1-1300, test/cli.test.ts:line ~1-20).

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

    rect rgba(200,200,255,0.5)
    user->>auth: select "restore from backup"
    auth->>mgr: runBackupRestoreManager()
    end

    rect rgba(200,255,200,0.5)
    mgr->>storage: listNamedBackups()
    storage->>fs: read backups dir
    fs-->>storage: backup entries
    storage-->>mgr: NamedBackupMetadata[]
    end

    rect rgba(255,230,200,0.5)
    mgr->>storage: assessNamedBackupRestore(name) (batched)
    storage->>fs: read backup file(s)
    fs-->>storage: backup content
    storage-->>mgr: BackupRestoreAssessment[]
    mgr->>user: display menu with hints
    user->>mgr: choose backup
    mgr->>storage: reassessNamedBackupRestore(name)
    mgr->>user: prompt confirm()
    user->>mgr: confirm
    mgr->>storage: restoreNamedBackup(name)
    storage->>fs: merge/import accounts
    fs-->>storage: write results
    storage-->>mgr: {imported, skipped, total}
    mgr->>user: show result / error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

reviewable concerns

  • missing regression tests: no explicit tests for corrupted backup files encountered mid-restore or concurrent restores of the same named backup. add tests covering partial corruption during restore and concurrent restore attempts (lib/storage.ts:line ~300, test/storage.test.ts:line ~400).
  • concurrency risk: filesystem read/merge/write paths lack cross-process locking; concurrent restores or concurrent writes can race and corrupt data. consider file locks or atomic-replace strategies (lib/storage.ts:line ~300, lib/codex-manager.ts:line ~420).
  • windows path escaping: path resolution and candidate lookup need explicit windows path normalization and traversal protection; add windows-style path tests to validate findExistingNamedBackupPath and resolve helpers (lib/storage.ts:line ~120, test/storage.test.ts:line ~220).
  • ui confirm robustness: confirm() is mocked in tests but may block or fail in non-tty contexts; add tests for confirm timeouts, non-interactive environments, and ensure fallback behavior (lib/codex-manager.ts:line ~340, test/codex-manager-cli.test.ts:line ~50).
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The pull request description is detailed and addresses the stated review gaps, but contains unresolved issues flagged in the greptile review that reduce confidence in merge readiness. Address the three flagged issues before merge: (1) symlink traversal gap in resolvePathForNamedBackupContainment fallback (lib/storage.ts:1826-1833), (2) skipped count overstated when backup has internal duplicates (lib/storage.ts:2981-2984), and (3) missing assertion that autoSyncActiveAccountToCodex fires for metadata-only restore (test/codex-manager-cli.test.ts:2659-2682).
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title follows conventional commits format (feat scope: summary) and is 38 characters, well under 72 char limit. clearly references the main change: adding a backup restore manager to auth flows.

✏️ 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.

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:2066">
P2: `process.platform` is mocked outside the guarded `try/finally`, so setup failures can leak the win32 mock into later tests.</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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

✅ Actions performed

Full review triggered.

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/storage.ts (1)

2867-2873: ⚠️ Potential issue | 🟠 Major

the final restore read still skips the transient-fs retry envelope.

listing and assessment now retry transient EPERM/EBUSY/EAGAIN failures in lib/storage.ts:1606-1704 and lib/storage.ts:1869-1890, but the actual import here still does a one-shot fs.readFile(). that means a backup can pass the menu and then fail during the final restore under the same windows lock conditions. wrap this read in retryTransientFilesystemOperation() and add a vitest that forces one busy read during import.

patch sketch
-	const content = await fs.readFile(resolvedPath, "utf-8");
+	const content = await retryTransientFilesystemOperation(() =>
+		fs.readFile(resolvedPath, "utf-8"),
+	);

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.

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

2819-2827: ⚠️ Potential issue | 🟠 Major

normalize both storage paths before comparing them.

this guard still does a raw string comparison between transactionState.storagePath and getStoragePath(). on windows, the same location can come through with different casing or normalization, so exportNamedBackupFile() in lib/named-backup-export.ts:223-250 can now fail with a false "different storage path" error from the same logical root.

patch sketch
 	const transactionState = transactionSnapshotContext.getStore();
-	const currentStoragePath = getStoragePath();
+	const normalizeStoragePath = (value: string): string => {
+		const resolved = resolvePath(value);
+		return process.platform === "win32" ? resolved.toLowerCase() : resolved;
+	};
+	const currentStoragePath = normalizeStoragePath(getStoragePath());
 	const storage = transactionState?.active
-		? transactionState.storagePath === currentStoragePath
+		? normalizeStoragePath(transactionState.storagePath) === currentStoragePath
 			? transactionState.snapshot
 			: (() => {

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 2819 - 2827, The comparison between
transactionState.storagePath and getStoragePath() in the exportAccounts path
does a raw string compare and can fail on Windows due to differing
casing/normalization; update the guard in the exportAccounts logic to normalize
both sides before comparing (e.g., use a shared normalize/resolve approach such
as path.resolve + path.normalize or fs.realpathSync where appropriate) so
transactionState.storagePath and getStoragePath() are compared canonicalized,
and ensure exportNamedBackupFile() still receives the original intended path
after the check; modify the code around transactionState.storagePath,
getStoragePath(), and the exportAccounts guard to use that normalizer function.
🤖 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/cli.ts`:
- Around line 237-244: Add test assertions to cover the remaining fallback
aliases so the CLI mapping is fully exercised: in the existing CLI unit tests
that already check "u" and "restore" (near the current test block around the
other alias assertions), add cases asserting that passing "backup" and
"restore-backup" also produce { mode: "restore-backup" }. Target the same test
helper that parses CLI input (the test invoking the code path that reaches the
normalized checks in lib/cli.ts) so both "backup" and "restore-backup" are
explicitly asserted to return mode "restore-backup".

In `@lib/codex-manager.ts`:
- Around line 4353-4375: The restore path that calls
importAccounts(latestAssessment.backup.path) updates multi-auth storage but does
not push the restored active account into the codex CLI state; after detecting a
changed restore (i.e., result.changed is true and after you log the success
message), call autoSyncActiveAccountToCodex() to sync the active account back
into the codex CLI state (referencing importAccounts and
autoSyncActiveAccountToCodex in lib/codex-manager.ts); also add/extend the
restore vitest to cover the changed-restore case so the zero-account recovery
path no longer leaves downstream auth empty, and ensure any added logs do not
leak tokens or emails and that any new queue logic respects EBUSY/429 retry
behavior per project guidelines.

In `@lib/ui/copy.ts`:
- Around line 19-37: The UI copy for restoreBackupConfirm and
restoreBackupRefreshSuccess misleadingly shows exact numeric "refreshed" counts
while the restore flow does not compute updated-row counts—lib/storage.ts
currently computes `skipped` as every non-imported row and lib/codex-manager.ts
feeds that into the "refreshed" message; fix by changing the restore flow to
return an explicit updated/refreshed count from the storage layer (e.g., add an
`updated` or `refreshed` field to the result returned by the storage method used
in lib/storage.ts and use that in lib/codex-manager.ts) and then wire that
accurate count into restoreBackupRefreshSuccess and the zero-import branch used
by restoreBackupConfirm, or alternatively change those two copy strings to
non-numeric wording (e.g., "refreshed existing accounts" without a number) if
you cannot compute an exact count; also add a vitest regression that covers a
mixed case (some duplicates updated, some unchanged) to ensure the UI shows the
correct semantics moving forward.

In `@test/cli.test.ts`:
- Around line 707-719: The test should assert that the other two accepted
aliases also map to mode "restore-backup": update the "returns restore-backup
for fallback restore aliases" case that imports promptLoginMode to also call
mockRl.question.mockResolvedValueOnce("backup") and expect(promptLoginMode([{
index: 0 }])).resolves.toEqual({ mode: "restore-backup" }), and likewise
mockRl.question.mockResolvedValueOnce("restore-backup") with the same
expectation; keep using the existing mockRl.question flow and promptLoginMode
function to lock the full alias mapping.

In `@test/storage.test.ts`:
- Around line 454-466: The test's broad assertion
expect(writeFileSpy).not.toHaveBeenCalled() can fail due to unrelated fs writes;
instead scope assertions to the active storage target by checking
writeFileSpy.mock.calls (or using
expect(writeFileSpy).not.toHaveBeenCalledWith(...)) for the specific path(s)
used by importAccounts/exportPath (and the other case around lines 539–551).
Replace the global not-to-have-been-called with an assertion that no call's
first argument equals or contains exportPath (or the exact storage filename(s)
used by the storage layer), e.g. iterate writeFileSpy.mock.calls and assert none
reference the active storage path(s) so only writes to the target files cause
failures.
- Around line 2048-2081: The test currently only asserts that
restoreNamedBackup("Manual Backup") throws after the backup file is removed;
also assert that the active storage state remained unchanged to guarantee
atomicity—after the expect(...rejects.toThrow(...)) add assertions that check
the same state you had before restore was attempted (e.g., call whatever reads
active accounts/storage used elsewhere in this file such as
clearAccounts/readActiveAccounts/getAccounts or inspect testStoragePath
contents) to ensure no accounts were added/modified; use the existing helpers in
this test file (clearAccounts, assessNamedBackupRestore, restoreNamedBackup,
removeWithRetry) to capture pre-restore state, then re-read and assert equality
after the failed restore so the manual-backup deletion path is deterministic and
verifies restore atomicity.

---

Duplicate comments:
In `@lib/storage.ts`:
- Around line 2819-2827: The comparison between transactionState.storagePath and
getStoragePath() in the exportAccounts path does a raw string compare and can
fail on Windows due to differing casing/normalization; update the guard in the
exportAccounts logic to normalize both sides before comparing (e.g., use a
shared normalize/resolve approach such as path.resolve + path.normalize or
fs.realpathSync where appropriate) so transactionState.storagePath and
getStoragePath() are compared canonicalized, and ensure exportNamedBackupFile()
still receives the original intended path after the check; modify the code
around transactionState.storagePath, getStoragePath(), and the exportAccounts
guard to use that normalizer function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5ecccd83-d873-4ff5-9403-ab6206faf358

📥 Commits

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

📒 Files selected for processing (9)
  • 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/cli.test.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/copy.ts
  • lib/ui/auth-menu.ts
  • lib/codex-manager.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/cli.test.ts
  • test/storage.test.ts
  • test/codex-manager-cli.test.ts
🔇 Additional comments (19)
lib/cli.ts (2)

49-61: lgtm, new login mode variant added correctly.

the restore-backup mode variant integrates cleanly with the existing union. downstream handling in lib/codex-manager.ts wires this to runBackupRestoreManager.


299-300: tty path wiring looks correct.

the case "restore-backup" delegates cleanly; corresponding ui action type is introduced in lib/ui/auth-menu.ts. no token or email logging here.

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

9-12: mock wiring for named backup apis looks correct.

test/codex-manager-cli.test.ts:9-12 declares mocks for listNamedBackups, assessNamedBackupRestore, getNamedBackupsDirectoryPath, and importAccounts. these are properly wired into the storage mock at lines 110-113. the setup aligns with the public api surface in lib/storage.ts.

Also applies to: 110-113


197-201: confirm mock setup is clean.

test/codex-manager-cli.test.ts:197-201 introduces confirmMock for the ui confirmation dialog. this enables testing the confirm-decline path at line 2523 and the confirm-proceed path throughout the restore tests.


506-540: beforeeach reset for backup mocks covers defaults well.

test/codex-manager-cli.test.ts:506-540 resets all new mocks and provides sensible defaults. the default assessNamedBackupRestoreMock returns a valid, eligible assessment with eligibleForRestore: true. this prevents test pollution across cases.


2366-2431: restore happy path test is solid.

test/codex-manager-cli.test.ts:2366-2431 validates the full restore flow: list → assess → select → confirm → import. assertions verify listNamedBackupsMock, assessNamedBackupRestoreMock, confirmMock, and importAccountsMock interactions. the backup path is correctly passed through.


2480-2545: confirm-decline regression is now complete.

test/codex-manager-cli.test.ts:2480-2545 addresses the past review comment. it sets confirmMock.mockResolvedValueOnce(false) at line 2523 and asserts both that importAccountsMock is not called and that promptLoginModeMock.toHaveBeenCalledTimes(2) at line 2543, proving the flow returns to the menu.


2547-2620: restore failure regression proves menu loop resumes.

test/codex-manager-cli.test.ts:2547-2620 addresses the past review comment by asserting promptLoginModeMock.toHaveBeenCalledTimes(2) at line 2605. this locks in the "stay in menu flow" contract when importAccountsMock rejects.


2696-2726: eperm regression for backup listing is present.

test/codex-manager-cli.test.ts:2696-2726 covers the EPERM error code from listNamedBackupsMock and verifies the cli returns to the login menu. this addresses the past review comment about manager-level EPERM regression.


2796-2871: concurrency bounding test is well-designed.

test/codex-manager-cli.test.ts:2796-2871 imports NAMED_BACKUP_ASSESS_CONCURRENCY from the actual storage module, creates more backups than the limit, and tracks maxInFlight using deferred promises. the assertion at line 2868 confirms bounded concurrency is enforced. deterministic and covers the concurrency concern.


3138-3224: ebusy reassessment rejection regression is present.

test/codex-manager-cli.test.ts:3138-3224 addresses the past review comment. the second assessNamedBackupRestoreMock call rejects with EBUSY at line 3182, and the test asserts confirmMock and importAccountsMock are not called, with the error logged.


3226-3264: epoch timestamp hint test avoids locale brittleness.

test/codex-manager-cli.test.ts:3226-3264 addresses the past review comment about locale-specific date strings. line 3263 now asserts not.toContain("updated ") for updatedAt: 0, avoiding the hardcoded locale-dependent date.


3266-3358: recent timestamp formatting test uses date.now spy.

test/codex-manager-cli.test.ts:3266-3358 spies on Date.now at line 3270 to control the "now" reference, then asserts relative hints like "updated today", "updated yesterday", "updated 3d ago". the spy is restored in the finally block. deterministic.


3537-3657: quick check waits for in-flight quota refresh.

test/codex-manager-cli.test.ts:3537-3657 uses deferred promises to control fetch ordering and verifies the second fetch occurs after the first completes. the test ensures quota cache is populated for both accounts before the check proceeds. this covers a potential race condition.


2728-2794: partial assessment failure test is comprehensive.

test/codex-manager-cli.test.ts:2728-2794 simulates one backup throwing during assessment while another succeeds. the selectMock implementation at line 2770 verifies only healthy backups appear in the menu. the warning is asserted at line 2786.


2873-2959: reassessment freshness test validates toctou guard.

test/codex-manager-cli.test.ts:2873-2959 ensures assessNamedBackupRestoreMock is called twice (initial + pre-confirm) and that the confirmation prompt uses the refreshed assessment data. this locks in the toctou re-assessment behavior.


3041-3136: reassessment ineligibility test proves early exit.

test/codex-manager-cli.test.ts:3041-3136 covers the case where reassessment returns eligibleForRestore: false. the test confirms confirmMock and importAccountsMock are not called, and the appropriate message is logged.

test/storage.test.ts (2)

373-399: good regression for cross-path transaction guard

test/storage.test.ts:373 is a strong guardrail for the storage-path transaction boundary and catches a high-risk misuse path early.


1857-2365: solid windows retry and bounded-concurrency coverage

the regressions in test/storage.test.ts:1857 through test/storage.test.ts:2365 do a good job pinning transient fs retry behavior and NAMED_BACKUP_LIST_CONCURRENCY throttling.

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

@greptileai

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

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

@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 (replaces 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 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 and 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.

@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.

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:2017">
P1: Restore path containment is lexical-only and can be bypassed through symlinked backup roots.</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 15, 2026

@greptileai
@cubic-dev-ai review this PR

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Mar 15, 2026

@greptileai
@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.

No issues found across 9 files

@ndycode
Copy link
Owner Author

ndycode commented Mar 15, 2026

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

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Mar 15, 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 15, 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 (replaces 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 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 and 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.

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:2255">
P3: Avoid hardcoding `chunk-boundary-08`; derive the expected backup name from `NAMED_BACKUP_LIST_CONCURRENCY` so the test remains valid if concurrency changes.</violation>
</file>

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

<violation number="1" location="lib/storage.ts:2029">
P2: Reject symlinked backup roots in restore path validation. The current containment check follows `backupRoot` symlinks, so a symlinked backups directory can redirect restores outside the intended backups boundary.</violation>
</file>

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

Copilot AI review requested due to automatic review settings March 15, 2026 05:41
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a named-backup restore manager to the codex auth login flow, with stronger backup-path containment and transient filesystem retry behavior, plus expanded regression coverage.

Changes:

  • Introduces a “Restore From Backup” recovery path in the auth menu + CLI fallback aliases.
  • Adds named-backup listing/assessment/restore helpers in lib/storage.ts, including containment validation and transient filesystem retries.
  • Expands vitest coverage for restore safety, cache reuse, Windows FS flakiness (cleanup), and login recovery scenarios.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/storage.test.ts Adds coverage for import/restore behaviors, named backups, containment checks, and retry scenarios; switches cleanup to removeWithRetry.
test/codex-manager-cli.test.ts Adds extensive CLI orchestration coverage for the restore manager and login recovery paths.
test/cli.test.ts Verifies fallback aliases map to restore-backup.
lib/ui/copy.ts Adds menu copy and prompts for the restore flow.
lib/ui/auth-menu.ts Adds “Recovery” section + restore action in the interactive menu.
lib/storage.ts Implements named backup listing/assessment/restore, containment checks, retry helper, and import result “changed” tracking.
lib/codex-manager.ts Wires restore manager into auth login, syncs Codex CLI state after restore, and flattens login loop to support empty-storage recovery.
lib/cli.ts Extends login mode union and fallback parsing for restore aliases.
docs/reference/storage-paths.md Documents named backup paths and restore workflow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

lib/storage.ts Outdated
Comment on lines +1724 to +1727
if (code === "EBUSY" || code === "ENOTEMPTY" || code === "EAGAIN") {
return true;
}
return code === "EPERM" && process.platform === "win32";
Comment on lines +2079 to +2109
it("retries transient backup directory errors while listing backups", async () => {
await saveAccounts({
version: 3,
activeIndex: 0,
accounts: [
{
accountId: "retry-list-dir",
refreshToken: "ref-retry-list-dir",
addedAt: 1,
lastUsed: 1,
},
],
});
await createNamedBackup("retry-list-dir");
const backupRoot = join(dirname(testStoragePath), "backups");
const originalReaddir = fs.readdir.bind(fs);
let busyFailures = 0;
const readdirSpy = vi
.spyOn(fs, "readdir")
.mockImplementation(async (...args) => {
const [path] = args;
if (String(path) === backupRoot && busyFailures === 0) {
busyFailures += 1;
const error = new Error(
"backup directory busy",
) as NodeJS.ErrnoException;
error.code = "EBUSY";
throw error;
}
return originalReaddir(...(args as Parameters<typeof fs.readdir>));
});
Comment on lines +1809 to +1821
const mergedAccounts = deduplicateAccounts([
...currentAccounts,
...candidate.normalized.accounts,
]);
const wouldExceedLimit = mergedAccounts.length > ACCOUNT_LIMITS.MAX_ACCOUNTS;
const imported = wouldExceedLimit
? null
: mergedAccounts.length - currentAccounts.length;
const skipped = wouldExceedLimit
? null
: Math.max(0, candidate.normalized.accounts.length - (imported ?? 0));
const changed = !haveEquivalentAccountRows(mergedAccounts, currentAccounts);
const nothingToImport = !wouldExceedLimit && !changed;
lib/storage.ts Outdated
Comment on lines +2988 to +3000
const merged = [...existingAccounts, ...normalized.accounts];

if (merged.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
const deduped = deduplicateAccounts(merged);
if (deduped.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
throw new Error(
`Import would exceed maximum of ${ACCOUNT_LIMITS.MAX_ACCOUNTS} accounts (would have ${deduped.length})`,
);
}
const deduplicatedAccounts = deduplicateAccounts(merged);
if (deduplicatedAccounts.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
throw new Error(
`Import would exceed maximum of ${ACCOUNT_LIMITS.MAX_ACCOUNTS} accounts (would have ${deduplicatedAccounts.length})`,
);
}
const imported = deduplicatedAccounts.length - existingAccounts.length;
const skipped = Math.max(0, normalized.accounts.length - imported);
const changed = !haveEquivalentAccountRows(
deduplicatedAccounts,
existingAccounts,
);
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.

2 participants