Skip to content

feat(auth): add backup restore manager#76

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

feat(auth): add backup restore manager#76
ndycode wants to merge 17 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 entry points to the auth login recovery flow, including zero-account recovery and fallback prompt aliases
  • harden restore assessment and backup listing for Windows-friendly behavior, including bounded concurrency, transient lock retries, context-specific backup-directory errors, and restore failures that stay inside the login menu
  • tighten the remaining review follow-ups by keeping backup reassessment failures inside the menu loop, making the named-backup matcher explicit, and narrowing the traversal regression to the actual path-separator guard
  • document and test the restore workflow for named backups, including manual backup files and restore regressions

Validation

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

Notes

  • this PR stays scoped to the existing backup-restore work; no unrelated cleanup is included

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 the codex auth login recovery flow, enabling users to list, assess, and merge backup files from the backups/ directory — including when no accounts are currently saved. the restore path is properly serialised through the existing storage mutex, errors are contained inside the menu loop, and a pre-confirm re-assessment catches concurrent account changes between the list display and the import.

  • lib/storage.ts — adds listNamedBackups, assessNamedBackupRestore, restoreNamedBackup, createNamedBackup, getNamedBackupsDirectoryPath, and a retryTransientFilesystemOperation helper; bounded concurrency via NAMED_BACKUP_LIST_CONCURRENCY; case-insensitive .JSON/.json extension matching; storagePath added to transaction context to detect cross-path exportAccounts misuse
  • lib/codex-manager.ts — adds runBackupRestoreManager with chunked assessments, stale-backup re-check before confirmation, and full error containment; flattens the runAuthLogin double-loop so the menu is accessible with zero accounts
  • lib/cli.ts / lib/ui/auth-menu.ts / lib/ui/copy.ts — adds restore-backup mode with fallback aliases (u, backup, restore) and a Recovery section in the UI
  • test/storage.test.ts — 15 new integration tests covering the full restore surface including TOCTOU, path traversal rejection, symlink blocking, transient retry, concurrency cap, and concurrent-restore serialisation; afterEach upgraded to removeWithRetry for windows CI
  • test/codex-manager-cli.test.ts — 8 new unit tests covering zero-account entry, confirmation decline, restore failure containment, partial assessment skip, concurrency limit, re-assessment before confirm, and epoch timestamp hint

two issues flagged: EPERM is included in the retryable error set unconditionally — on linux/macOS it is a permanent permission denial, not a transient lock, so retrying wastes ~150 ms before propagating the real error (should be win32-only); and a symlink-detection error in findExistingNamedBackupPath leaks through the shared catch block, logging a misleading "Failed to read named backup directory" message instead of the actual symlink rejection.

Confidence Score: 3/5

  • safe to merge after fixing EPERM platform-gate; no data-loss or token-leak risk in the current code
  • the core restore path is well-tested and properly mutex-serialised; path traversal is blocked, symlinks are rejected, and errors stay inside the menu loop. two issues lower confidence: EPERM retry on linux/macOS can mask genuine permission errors with a 150 ms delay (concurrency safety concern for non-Windows CI), and a symlink error surfaces through a misleading log branch. neither is data-corrupting but both touch the windows filesystem safety story that is a stated merge requirement
  • lib/storage.ts — isRetryableFilesystemErrorCode (EPERM platform gate) and findExistingNamedBackupPath (symlink log branch)

Important Files Changed

Filename Overview
lib/storage.ts adds listNamedBackups, assessNamedBackupRestore, restoreNamedBackup, and supporting helpers; bounded concurrency and transient-lock retry logic are solid, but EPERM is retried on all platforms (should be win32-only) and a symlink detection error leaks through a misleading log branch
lib/codex-manager.ts adds runBackupRestoreManager with proper try/catch around listing, re-assessment before confirm, and error containment so failures stay inside the menu loop; also flattens the runAuthLogin double-loop so the menu is reachable with zero accounts
lib/cli.ts adds restore-backup LoginMode and four fallback aliases (u, backup, restore, restore-backup); straightforward, well-tested
test/storage.test.ts comprehensive named-backup integration tests covering create/list/assess/restore, uppercase .JSON extension, TOCTOU delete/corrupt, path traversal rejection, symlink blocking, EBUSY/EAGAIN/EPERM retry, concurrency limit, and concurrent-restore serialization; afterEach upgraded to removeWithRetry
test/codex-manager-cli.test.ts eight new unit tests covering the full restore manager flow: zero-account entry, confirmation decline, restore failure containment, partial assessment failure, concurrency limit, pre-confirm re-assessment, reassessment failure, and epoch timestamp display

Sequence Diagram

sequenceDiagram
    participant User
    participant runAuthLogin
    participant runBackupRestoreManager
    participant listNamedBackups
    participant assessNamedBackupRestore
    participant select
    participant confirm
    participant restoreNamedBackup
    participant importAccounts

    User->>runAuthLogin: codex auth login
    runAuthLogin->>runAuthLogin: promptLoginMode → restore-backup
    runAuthLogin->>runBackupRestoreManager: call (displaySettings)

    runBackupRestoreManager->>listNamedBackups: list backup dir (retry EBUSY/EAGAIN/EPERM)
    listNamedBackups-->>runBackupRestoreManager: NamedBackupMetadata[]

    loop chunks of NAMED_BACKUP_LIST_CONCURRENCY
        runBackupRestoreManager->>assessNamedBackupRestore: assess(name, {currentStorage})
        assessNamedBackupRestore-->>runBackupRestoreManager: BackupRestoreAssessment
    end

    runBackupRestoreManager->>select: show backup menu
    User->>select: pick backup
    select-->>runBackupRestoreManager: {type: "restore", assessment}

    Note over runBackupRestoreManager: re-assess with fresh loadAccounts() to detect concurrent writes
    runBackupRestoreManager->>assessNamedBackupRestore: re-assess(name, {currentStorage: fresh})
    assessNamedBackupRestore-->>runBackupRestoreManager: latestAssessment

    runBackupRestoreManager->>confirm: "Merge N accounts into M current (K after dedupe)?"
    User->>confirm: yes
    confirm-->>runBackupRestoreManager: true

    runBackupRestoreManager->>restoreNamedBackup: restore(name)
    restoreNamedBackup->>importAccounts: importAccounts(backupPath)
    Note over importAccounts: runs inside withAccountStorageTransaction (mutex-serialised)
    importAccounts-->>restoreNamedBackup: {imported, skipped, total}
    restoreNamedBackup-->>runBackupRestoreManager: result

    runBackupRestoreManager-->>runAuthLogin: return (errors caught, stay in menu)
    runAuthLogin->>runAuthLogin: continue loop → promptLoginMode
Loading

Comments Outside Diff (1)

  1. lib/storage.ts, line 1628-1632 (link)

    EPERM retry is platform-unsafe

    EPERM is included in the retryable set unconditionally. on windows, antivirus/file-indexing tools can transiently deny access to a file — retrying EPERM there is correct. on linux and macOS, EPERM from readdir, readFile, or stat means the process genuinely lacks the needed permission; that condition does not resolve on retry, so the function wastes up to 150 ms (10 + 20 + 40 + 80 ms) before propagating what was a permanent error from the start.

Fix All in Codex

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

Comment:
**`EPERM` retry is platform-unsafe**

`EPERM` is included in the retryable set unconditionally. on windows, antivirus/file-indexing tools can transiently deny access to a file — retrying `EPERM` there is correct. on linux and macOS, `EPERM` from `readdir`, `readFile`, or `stat` means the process genuinely lacks the needed permission; that condition does not resolve on retry, so the function wastes up to 150 ms (10 + 20 + 40 + 80 ms) before propagating what was a permanent error from the start.

```suggestion
function isRetryableFilesystemErrorCode(
	code: string | undefined,
): code is "EPERM" | "EBUSY" | "EAGAIN" {
	if (code === "EBUSY" || code === "EAGAIN") return true;
	return code === "EPERM" && process.platform === "win32";
}
```

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: 1816-1830

Comment:
**symlink error leaks through misleading log branch**

when a matching entry turns out to be a symlink, the inner `throw new Error("Named backup "..." is not a regular backup file")` is caught by the outer `catch`. because that error has no `.code`, it is not `"ENOENT"`, so it falls through to:

```ts
log.warn("Failed to read named backup directory", { ... });
throw error;
```

the warning says "Failed to read named backup directory" even though the directory was read fine — it was the entry type that was wrong. the symlink case should be re-thrown directly, before reaching the shared catch, so the log message stays accurate and the error carries its real context:

```ts
if (entry.isSymbolicLink() || !entry.isFile()) {
  // throw directly — bypass the generic catch so the log stays accurate
  throw Object.assign(
    new Error(`Named backup "${entryBaseName}" is not a regular backup file`),
    { code: "EBADF" },
  );
}
```

alternatively, check `error.message` in the catch before logging. as-is, windows logs show the misleading directory-level message whenever a backup entry is a symlink.

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

Last reviewed commit: ee7a402

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 | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (2 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.
Description check ✅ Passed the description is well-structured and covers all key areas: summary of intent, technical changes across files, validation steps, and known issues flagged by greptile review.

✏️ 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 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="test/codex-manager-cli.test.ts">

<violation number="1" location="test/codex-manager-cli.test.ts:2639">
P2: The new restore-menu concurrency test can deadlock when the implementation uses fewer than 8 workers, because deferred assessments are only released at `pending.length === 8` or on the final backup.</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.

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="test/storage.test.ts">

<violation number="1" location="test/storage.test.ts:1351">
P2: Use `removeWithRetry` instead of direct `fs.rm` here to avoid Windows EBUSY/EPERM test flakes.</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

🤖 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 4224-4230: The current fan-out uses Promise.all in the chunk
mapping (the chunk variable building assessments into assessments.push(...await
Promise.all(...))) which causes one rejected assessNamedBackupRestore to abort
the whole chunk; change it to use Promise.allSettled (or individually catch
per-assessment) so successful assessments still return while failures are logged
and skipped, ensuring assessNamedBackupRestore(backup.name, { currentStorage })
rejections don't throw the whole flow. Add two vitest cases in
test/codex-manager-cli.test.ts: (1) a regression where one
assessNamedBackupRestore rejects but others are selectable/rendered, asserting
the restore menu still shows healthy backups; (2) a fan-out test that validates
chunking honors the configured cap and does not exceed parallel requests. Also
ensure any retry/queue logic around assessNamedBackupRestore handles transient
EBUSY/429 with backoff, that logging avoids leaking tokens/emails, and update
assertions in affected tests to reference the new non-fail-fast behavior.

In `@lib/storage.ts`:
- Around line 1581-1624: The directory reads in listNamedBackups and
findExistingNamedBackupPath must be retried on transient Windows filesystem
errors: replace the direct fs.readdir calls inside listNamedBackups and the
confirm-time lookup in findExistingNamedBackupPath with calls wrapped by
retryTransientFilesystemOperation(async () => fs.readdir(...)), so transient
EBUSY/EPERM/EAGAIN are retried but genuine ENOENT still returns [] (in
listNamedBackups) or bubbles after retries (in findExistingNamedBackupPath);
preserve existing behavior around loadBackupCandidate and
buildNamedBackupMetadata, keep logging but avoid leaking secrets, and add a
vitest that injects transient errors (EBUSY/EAGAIN) for the backup directory
read to assert retries happen and final behavior matches current tests (cite the
restore tests in test/codex-manager-cli.test.ts).

In `@test/codex-manager-cli.test.ts`:
- Around line 2541-2611: The test currently verifies the error log and exit code
but doesn't assert the UI loop resumed; update the test that exercises
runCodexMultiAuthCli(["auth","login"]) to assert promptLoginModeMock was invoked
twice to prove control returned to the login menu (use
expect(promptLoginModeMock).toHaveBeenCalledTimes(2)); locate the failing test
around the it("catches restore failures and returns to the login menu"...) block
and add that assertion (you can also assert selectMock was called for the
restore selection if helpful) so the "stay in menu" flow is locked in.

In `@test/storage.test.ts`:
- Around line 24-25: The test currently hardcodes the concurrency limit value
(the magic number 8) in the concurrency regression assertion; replace that
literal with the canonical constant NAMED_BACKUP_LIST_CONCURRENCY so the test
tracks the real contract if the limit changes, update assertions around
listNamedBackups (and any related assertions between lines referenced) to
reference NAMED_BACKUP_LIST_CONCURRENCY rather than 8, and keep the test
deterministic under vitest by relying on the exported constant used by the
implementation instead of a hardcoded value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b96886a9-579e-418c-bdc7-dc80336851df

📥 Commits

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

📒 Files selected for processing (8)
  • docs/reference/storage-paths.md
  • lib/cli.ts
  • lib/codex-manager.ts
  • lib/storage.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). (3)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Greptile Review
  • 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/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/cli.test.ts
  • test/codex-manager-cli.test.ts
  • test/storage.test.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
🔇 Additional comments (2)
test/storage.test.ts (1)

334-337: good windows-safe teardown update.

using removeWithRetry at test/storage.test.ts:336 (line 336) is the right move for transient windows file locks and keeps this suite less flaky.

as per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.

lib/codex-manager.ts (1)

3826-3830: good fix: restore menu is now reachable with zero accounts.

lib/codex-manager.ts:3827 (line 3827) now falls back to createEmptyAccountStorage(), so recovery actions can render even when storage is missing/empty.

@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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

✅ Actions performed

Full review triggered.

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

3 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="lib/codex-manager.ts">

<violation number="1" location="lib/codex-manager.ts:4227">
P2: The restore menu does redundant filesystem work by loading every backup in `listNamedBackups()` and then reloading every backup again in `assessNamedBackupRestore()`, which can noticeably slow menu rendering with many backups.</violation>
</file>

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

<violation number="1" location="lib/storage.ts:1799">
P2: Restore lookup can fail for backups that are listed with uppercase `.JSON` extensions because matching is stricter than listing on non-Windows filesystems.</violation>
</file>

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

<violation number="1" location="test/storage.test.ts:1268">
P2: Use `removeWithRetry` instead of `fs.unlink` here to avoid transient Windows file-lock flakes in this restore test.</violation>
</file>

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

assessments.push(
...(await Promise.all(
chunk.map((backup) =>
assessNamedBackupRestore(backup.name, { currentStorage }),
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: The restore menu does redundant filesystem work by loading every backup in listNamedBackups() and then reloading every backup again in assessNamedBackupRestore(), which can noticeably slow menu rendering with many backups.

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

<comment>The restore menu does redundant filesystem work by loading every backup in `listNamedBackups()` and then reloading every backup again in `assessNamedBackupRestore()`, which can noticeably slow menu rendering with many backups.</comment>

<file context>
@@ -4173,6 +4196,115 @@ export async function autoSyncActiveAccountToCodex(): Promise<boolean> {
+		assessments.push(
+			...(await Promise.all(
+				chunk.map((backup) =>
+					assessNamedBackupRestore(backup.name, { currentStorage }),
+				),
+			)),
</file context>
Fix with Cubic

@ndycode
Copy link
Owner Author

ndycode commented Mar 14, 2026

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

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Mar 14, 2026

@coderabbitai full review
@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

@ndycode — triggering a full review of pr #76 now. will cover the named-backup restore manager, windows-safe bounded concurrency in lib/storage.ts, the runBackupRestoreManager fan-out at lib/codex-manager.ts:452-456 (flagged as unbounded by greptile), transient-lock retry logic, and test coverage gaps including the missing bounded-concurrency regression test for assessment fan-out.

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

Comment on lines +1628 to +1650
function isRetryableFilesystemErrorCode(
code: string | undefined,
): code is "EPERM" | "EBUSY" | "EAGAIN" {
return code === "EPERM" || code === "EBUSY" || code === "EAGAIN";
}

async function retryTransientFilesystemOperation<T>(
operation: () => Promise<T>,
): Promise<T> {
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
return await operation();
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (!isRetryableFilesystemErrorCode(code) || attempt === 4) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 10 * 2 ** attempt));
}
}

throw new Error("Retry loop exhausted unexpectedly");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

retry logic is correct but consider adding jitter for heavily contested paths.

the exponential backoff at lib/storage.ts:1645 is good, but unlike renameFileWithRetry at lib/storage.ts:428-432 which adds jitter, this function doesn't. under heavy contention (e.g., multiple cli instances listing backups), synchronized retries could cause thundering herd.

low priority since listing is read-only, but worth noting for consistency.

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

In `@lib/storage.ts` around lines 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.

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

Comment on lines +3834 to +3854
if (shouldAutoFetchLimits && !pendingMenuQuotaRefresh) {
const staleCount = countMenuQuotaRefreshTargets(currentStorage, quotaCache, quotaTtlMs);
if (staleCount > 0) {
if (showFetchStatus) {
menuQuotaRefreshStatus = `${UI_COPY.mainMenu.loadingLimits} [0/${staleCount}]`;
}
pendingMenuQuotaRefresh = refreshQuotaCacheForMenu(
currentStorage,
quotaCache,
quotaTtlMs,
(current, total) => {
if (!showFetchStatus) return;
menuQuotaRefreshStatus = `${UI_COPY.mainMenu.loadingLimits} [${current}/${total}]`;
},
)
.then(() => undefined)
.catch(() => undefined)
.finally(() => {
menuQuotaRefreshStatus = undefined;
pendingMenuQuotaRefresh = null;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

serialize quota-cache writers before launching quota-touching actions.

at lib/codex-manager.ts:3834-3854 and lib/codex-manager.ts:3871-3893 (line [3834], line [3871]), pendingMenuQuotaRefresh can still be writing while runHealthCheck / runForecast / runFix starts another read-modify-write cycle on quota cache. this can clobber fresher entries with stale state.

add a guard and a regression in test/codex-manager-cli.test.ts:1 that simulates a pending menu refresh plus a foreground quota action, then asserts no key loss.

proposed fix
@@
 			if (menuResult.mode === "cancel") {
 				console.log("Cancelled.");
 				return 0;
 			}
+			const modeTouchesQuotaCache =
+				menuResult.mode === "check" ||
+				menuResult.mode === "deep-check" ||
+				menuResult.mode === "forecast" ||
+				menuResult.mode === "fix";
+			if (modeTouchesQuotaCache && pendingMenuQuotaRefresh) {
+				await pendingMenuQuotaRefresh;
+			}
 			if (menuResult.mode === "check") {
 				await runActionPanel("Quick Check", "Checking local session + live status", async () => {
 					await runHealthCheck({ forceRefresh: false, liveProbe: true });
 				}, displaySettings);
 				continue;

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.

Also applies to: 3871-3893

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

In `@lib/codex-manager.ts` around lines 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.

Comment on lines +2475 to +2539
it("does not restore a named backup when confirmation is declined", async () => {
setInteractiveTTY(true);
const now = Date.now();
loadAccountsMock.mockResolvedValue({
version: 3,
activeIndex: 0,
activeIndexByFamily: { codex: 0 },
accounts: [
{
email: "settings@example.com",
accountId: "acc_settings",
refreshToken: "refresh-settings",
accessToken: "access-settings",
expiresAt: now + 3_600_000,
addedAt: now - 1_000,
lastUsed: now - 1_000,
enabled: true,
},
],
});
const assessment = {
backup: {
name: "named-backup",
path: "/mock/backups/named-backup.json",
createdAt: null,
updatedAt: now,
sizeBytes: 128,
version: 3,
accountCount: 1,
schemaErrors: [],
valid: true,
loadError: undefined,
},
currentAccountCount: 1,
mergedAccountCount: 2,
imported: 1,
skipped: 0,
wouldExceedLimit: false,
eligibleForRestore: true,
error: undefined,
};
listNamedBackupsMock.mockResolvedValue([assessment.backup]);
assessNamedBackupRestoreMock.mockResolvedValue(assessment);
confirmMock.mockResolvedValueOnce(false);
promptLoginModeMock
.mockResolvedValueOnce({ mode: "restore-backup" })
.mockResolvedValueOnce({ mode: "cancel" });
selectMock.mockResolvedValueOnce({ type: "restore", assessment });

const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js");
const exitCode = await runCodexMultiAuthCli(["auth", "login"]);

expect(exitCode).toBe(0);
expect(listNamedBackupsMock).toHaveBeenCalledTimes(1);
expect(assessNamedBackupRestoreMock).toHaveBeenCalledWith(
"named-backup",
expect.objectContaining({
currentStorage: expect.objectContaining({
accounts: expect.any(Array),
}),
}),
);
expect(confirmMock).toHaveBeenCalledOnce();
expect(restoreNamedBackupMock).not.toHaveBeenCalled();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

assert the decline path loops back to login.

test/codex-manager-cli.test.ts:2475 currently only proves restoreNamedBackup() is not called. if the confirm=false branch starts exiting auth login instead of returning to the recovery menu, this still passes without consuming the second promptLoginModeMock response. add expect(promptLoginModeMock).toHaveBeenCalledTimes(2).

As per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.

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

In `@test/codex-manager-cli.test.ts` around lines 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.

Comment on lines +2958 to +2997
it("shows epoch backup timestamps in restore hints", async () => {
setInteractiveTTY(true);
loadAccountsMock.mockResolvedValue(null);
const assessment = {
backup: {
name: "epoch-backup",
path: "/mock/backups/epoch-backup.json",
createdAt: null,
updatedAt: 0,
sizeBytes: 128,
version: 3,
accountCount: 1,
schemaErrors: [],
valid: true,
loadError: undefined,
},
currentAccountCount: 0,
mergedAccountCount: 1,
imported: 1,
skipped: 0,
wouldExceedLimit: false,
eligibleForRestore: true,
error: undefined,
};
listNamedBackupsMock.mockResolvedValue([assessment.backup]);
assessNamedBackupRestoreMock.mockResolvedValue(assessment);
promptLoginModeMock
.mockResolvedValueOnce({ mode: "restore-backup" })
.mockResolvedValueOnce({ mode: "cancel" });
selectMock.mockResolvedValueOnce({ type: "back" });

const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js");
const exitCode = await runCodexMultiAuthCli(["auth", "login"]);

expect(exitCode).toBe(0);
const backupItems = selectMock.mock.calls[0]?.[0];
expect(backupItems?.[0]?.hint).toContain(
`updated ${new Date(0).toLocaleDateString()}`,
);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

avoid matching an exact localized date string here.

test/codex-manager-cli.test.ts:2994 hardcodes the result of toLocaleDateString(), which couples the regression to runner locale/time zone and to the current hint formatting in lib/codex-manager.ts:4258. the behavior that matters is that updatedAt: 0 is rendered instead of dropped. assert the "updated " prefix, or compare against the same shared formatter, instead of an exact locale-specific string.

As per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.

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

In `@test/codex-manager-cli.test.ts` around lines 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.

Comment on lines +1413 to +1526
it("rethrows unreadable backup directory errors while listing backups", async () => {
const readdirSpy = vi.spyOn(fs, "readdir");
const error = new Error("backup directory locked") as NodeJS.ErrnoException;
error.code = "EPERM";
readdirSpy.mockRejectedValue(error);

try {
await expect(listNamedBackups()).rejects.toMatchObject({ code: "EPERM" });
} finally {
readdirSpy.mockRestore();
}
});

it("rethrows unreadable backup directory errors while restoring backups", async () => {
const readdirSpy = vi.spyOn(fs, "readdir");
const error = new Error("backup directory locked") as NodeJS.ErrnoException;
error.code = "EPERM";
readdirSpy.mockRejectedValue(error);

try {
await expect(restoreNamedBackup("Manual Backup")).rejects.toMatchObject({
code: "EPERM",
});
} finally {
readdirSpy.mockRestore();
}
});

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>));
});

try {
const backups = await listNamedBackups();
expect(backups).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: "retry-list-dir", valid: true }),
]),
);
expect(busyFailures).toBe(1);
} finally {
readdirSpy.mockRestore();
}
});

it("retries transient backup directory errors while restoring backups", async () => {
await saveAccounts({
version: 3,
activeIndex: 0,
accounts: [
{
accountId: "retry-restore-dir",
refreshToken: "ref-retry-restore-dir",
addedAt: 1,
lastUsed: 1,
},
],
});
await createNamedBackup("retry-restore-dir");
await clearAccounts();
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 = "EAGAIN";
throw error;
}
return originalReaddir(...(args as Parameters<typeof fs.readdir>));
});

try {
const result = await restoreNamedBackup("retry-restore-dir");
expect(result.total).toBe(1);
expect(busyFailures).toBe(1);
} finally {
readdirSpy.mockRestore();
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cover the transient eperm retry path too.

test/storage.test.ts:1413 only locks the permanent-eperm failure case, while test/storage.test.ts:1441 and test/storage.test.ts:1486 only lock successful retries for ebusy and eagain. if the named-backup retry path stops treating eperm as transient, windows backup discovery/restore can regress and this suite still stays green. add one deterministic success-after-eperm case on the list or restore path.

As per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.

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

In `@test/storage.test.ts` around lines 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.

Comment on lines +1645 to +1696
it("limits concurrent backup reads while listing backups", async () => {
const backupPaths: string[] = [];
for (let index = 0; index < 12; index += 1) {
await saveAccounts({
version: 3,
activeIndex: 0,
accounts: [
{
accountId: `concurrency-${index}`,
refreshToken: `ref-concurrency-${index}`,
addedAt: index + 1,
lastUsed: index + 1,
},
],
});
const backup = await createNamedBackup(`concurrency-${index}`);
backupPaths.push(backup.path);
}

const originalReadFile = fs.readFile.bind(fs);
const delayedPaths = new Set(backupPaths);
let activeReads = 0;
let peakReads = 0;
const readFileSpy = vi
.spyOn(fs, "readFile")
.mockImplementation(async (...args) => {
const [path] = args;
if (delayedPaths.has(String(path))) {
activeReads += 1;
peakReads = Math.max(peakReads, activeReads);
try {
await new Promise((resolve) => setTimeout(resolve, 10));
return await originalReadFile(
...(args as Parameters<typeof fs.readFile>),
);
} finally {
activeReads -= 1;
}
}
return originalReadFile(...(args as Parameters<typeof fs.readFile>));
});

try {
const backups = await listNamedBackups();
expect(backups).toHaveLength(12);
expect(peakReads).toBeLessThanOrEqual(
NAMED_BACKUP_LIST_CONCURRENCY,
);
} finally {
readFileSpy.mockRestore();
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

make the workload scale with the exported limit.

test/storage.test.ts:1647 always creates 12 backups. if NAMED_BACKUP_LIST_CONCURRENCY moves to 12 or higher, line 1690 still passes even with no throttling because the test never saturates the queue. derive the backup count from NAMED_BACKUP_LIST_CONCURRENCY + n here, and in test/codex-manager-cli.test.ts:2719, so the concurrency regressions keep exercising the limiter.

As per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.

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

In `@test/storage.test.ts` around lines 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.

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