Skip to content

refactor: migrate the last two hand-rolled retry loops to withRetry - #585

Merged
ndycode merged 1 commit into
mainfrom
claude/audit-66-retry-loop-migration
Jun 11, 2026
Merged

refactor: migrate the last two hand-rolled retry loops to withRetry#585
ndycode merged 1 commit into
mainfrom
claude/audit-66-retry-loop-migration

Conversation

@ndycode

@ndycode ndycode commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Closes out the audit's §4.2 retry consolidation (finding M7): the last two hand-rolled retry loops in the codebase now go through the shared withRetry in lib/fs-retry.ts. After this, grep "for (let attempt" over lib/ finds nothing — every retry policy is declared, not re-implemented.

What Changed

lib/storage.tsrenameTempToPath (the temp→final rename inside the account save path): the manual EPERM/EBUSY loop becomes

withRetry(() => fs.rename(tempPath, path), {
	maxAttempts: 5,
	backoffMs: (attempt) => 10 * 2 ** (attempt - 1),
	retryableCodes: ["EPERM", "EBUSY"],
})

Policy is byte-identical (same codes, same 5 attempts, same 10/20/40/80ms schedule) with one deliberate difference, called out in a code comment: the old loop slept once more (160ms) after the final failed attempt before rethrowing; withRetry documents and implements "no delay after the final attempt", so exhaustion now surfaces 160ms sooner.

lib/config.tssavePluginConfig env-path branch: the mtime CAS loop becomes withRetry over the whole read-merge-write operation (retryableCodes: ["ESTALE"], 3 attempts, no delay). Each retry naturally re-stats, re-reads, and re-merges because the reads live inside the operation. The Aborting config save … unreadable error carries no code, so it still propagates immediately, exactly as before.

test/storage.test.ts — two new tests pin the rename policy through the real saveAccounts path: EPERM twice then success (asserts exactly 3 attempts and the saved file content), and ENOSPC failing fast with exactly 1 attempt. Both start with vi.restoreAllMocks(): the neighbouring environment-failing tests in sandboxed containers can leak an fs.rename spy when they die mid-test, and a passthrough binding captured from a leaked spy recurses into the new test's own mock. (That same leak is why the pre-existing staged-rename test appears in the §7 environment baseline.)

Validation

  • npm run typecheck (also in the pre-commit hook)
  • npx eslint lib/storage.ts lib/config.ts test/storage.test.ts --max-warnings=0
  • npm test -- test/config-save.test.ts test/fs-retry.test.ts test/account-save.test.ts test/unified-settings.test.ts — 73/73 (config-save's existing ESTALE re-read/re-merge tests pass unchanged against the migrated CAS)
  • npm test -- test/storage.test.ts — failure names are a strict subset (19) of the 23-name docs/audits/evidence/test-baseline-2026-06-10.txt environment block for this file; zero new names, and both new tests pass even in the poisoned full-suite ordering
  • npm run build deferred to CI

Docs and Governance Checklist

  • No user-visible command/setting/path surface changed; pure internal refactor with identical retry policies

Risk and Rollback

  • Risk level: low — both call sites keep their exact attempt counts, retryable code sets, and backoff schedules; the only timing change is dropping a pointless 160ms sleep before the rename loop's final rethrow. The ESTALE CAS semantics are pinned by existing config-save.test.ts cases that pass unchanged.
  • Rollback plan: revert the single commit; the two new tests fail against the restored hand-rolled loops only if the policy itself regresses, so they remain valid either way.

Additional Notes

  • §4.2's first batch (the withRetry helper itself plus the config/quota-cache/recovery/uninstall migrations) landed earlier in the cycle; this is the planned second, final batch.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB


Generated by Claude Code

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

completes §4.2 retry consolidation (audit finding M7): the last two hand-rolled retry loops in lib/ are replaced with withRetry from lib/fs-retry.ts, making every retry policy in the codebase declared rather than re-implemented.

  • lib/storage.tsrenameTempToPath adopts withRetry with the original EPERM/EBUSY codes, 5 attempts, and 10ms-doubling schedule; the only intentional behavioral change (explicitly documented) is that the 160ms sleep after the 5th failed attempt is dropped before rethrowing.
  • lib/config.ts — the ESTALE CAS loop in savePluginConfig moves to withRetry with backoffMs: 0 and 3 attempts; semantics are byte-identical including immediate rethrow for the code-less "unreadable" error.
  • test/storage.test.ts — two new tests pin the rename retry policy end-to-end: EPERM×2→success (3 attempts, correct file content) and ENOSPC→fast-fail (1 attempt).

Confidence Score: 4/5

safe to merge — both migrated call sites preserve their exact attempt counts, retryable code sets, and inter-attempt backoff schedules; the one deliberate change (dropping a trailing no-op 160ms sleep in the rename path) is low-risk and well-documented

the vi.restoreAllMocks() calls added inside the test bodies are the only rough edge: they work correctly against today's beforeEach (which sets up no spies), but if a spy-based fixture is ever added to that hook the broad restore will silently tear it down and produce confusing test failures

test/storage.test.ts — the vi.restoreAllMocks() pattern in the two new tests; all other changes are straightforward migrations

Important Files Changed

Filename Overview
lib/storage.ts migrates renameTempToPath to withRetry; backoff schedule (10/20/40/80ms, EPERM+EBUSY, 5 attempts) is byte-identical to the old loop — the only difference (explicitly documented) is dropping the trailing 160ms sleep after the 5th failed attempt before rethrowing
lib/config.ts migrates the ESTALE CAS loop in savePluginConfig to withRetry; semantics are identical — 3 attempts, no inter-attempt delay (backoffMs: 0), immediate rethrow on non-ESTALE errors and on the "unreadable" code-less error
test/storage.test.ts two new tests pin the rename retry policy (EPERM×2→success, ENOSPC→fast-fail); vi.restoreAllMocks() guard inside the test body is correct for today's beforeEach (no spies) but fragile if spy setup is ever added there

Sequence Diagram

sequenceDiagram
    participant Caller
    participant withRetry
    participant fs

    Note over Caller,fs: storage.ts — renameTempToPath (maxAttempts=5, EPERM/EBUSY)
    Caller->>withRetry: rename(tempPath, path)
    withRetry->>fs: rename() [attempt 1]
    fs-->>withRetry: EPERM/EBUSY → sleep 10ms
    withRetry->>fs: rename() [attempt 2]
    fs-->>withRetry: EPERM/EBUSY → sleep 20ms
    withRetry->>fs: rename() [attempt N ≤ 5]
    fs-->>withRetry: success or exhausted → rethrow immediately

    Note over Caller,fs: config.ts — savePluginConfig CAS (maxAttempts=3, ESTALE, no delay)
    Caller->>withRetry: stat+read+merge+write
    withRetry->>fs: writeJsonFileAtomicWithRetry [attempt 1]
    fs-->>withRetry: ESTALE → retry immediately
    withRetry->>fs: writeJsonFileAtomicWithRetry [attempt 2]
    fs-->>withRetry: ESTALE → retry immediately
    withRetry->>fs: writeJsonFileAtomicWithRetry [attempt 3]
    fs-->>withRetry: success or exhausted → rethrow
Loading

Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
test/storage.test.ts:3986-3987
**`vi.restoreAllMocks()` inside test body wipes `beforeEach` spies**

the current `beforeEach` for this describe block only calls `vi.useFakeTimers` and creates a temp dir — no spies — so this is safe today. but `vi.restoreAllMocks()` runs *after* `beforeEach` fires, meaning any spy added there in future (e.g. an `fs.stat` stub for a flaky fixture) would be silently torn down before this test runs. a narrower fix — e.g. `if (vi.isMockFunction(fs.rename)) vi.mocked(fs.rename).mockRestore()` — targets only the leaked spy without the broad side-effect.

### Issue 2 of 2
test/storage.test.ts:4023-4028
**same `vi.restoreAllMocks()` fragility in the ENOSPC test**

the identical broad-restore pattern appears in the second new test. the same concern applies: a future `beforeEach` spy on `fs.stat`, `fs.writeFile`, etc. would be silently cleared before this test body runs. both occurrences should use a targeted restore if the upstream spy-leak fix is deferred.

Reviews (1): Last reviewed commit: "refactor: migrate the last two hand-roll..." | Re-trigger Greptile

Closes out the audit's §4.2 retry consolidation: every retry loop in the
codebase now goes through lib/fs-retry.ts.

- storage.ts renameTempToPath: same policy expressed via withRetry
  (EPERM/EBUSY only, 5 attempts, 10ms-doubling schedule). One deliberate
  difference: the old loop slept once more (160ms) after the final
  failed attempt before rethrowing; withRetry rethrows immediately.
- config.ts savePluginConfig env-path branch: the mtime CAS loop is now
  withRetry over the read-merge-write operation (ESTALE only, 3
  attempts, no delay). The unreadable-abort error carries no code, so it
  still propagates immediately.

New tests pin the rename policy through the real saveAccounts path:
EPERM twice then success (3 attempts, file saved) and ENOSPC failing
fast without retry. Both guard against a leaked fs.rename spy from an
environment-failed predecessor (vi.restoreAllMocks first) so they stay
deterministic in sandboxed containers.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Summary

This PR completes the retry loop consolidation audit by migrating the final two manual retry implementations to the shared withRetry helper function. The changes are low-risk refactoring that improves code maintainability and consistency, with newly added regression tests covering both success and failure paths for the file rename operation.

Changes

lib/storage.ts

  • renameTempToPath: Replaced manual retry loop with withRetry helper (5 attempts, exponential backoff starting at 10ms, EPERM/EBUSY only)
  • Behavioral note: The new implementation rethrows immediately on final attempt exhaustion, whereas the previous code had an additional 160ms delay before rethrowing—this timing change is unlikely to impact production but should be verified if dependent on retry semantics

lib/config.ts

  • savePluginConfig (env-path branch): Converted mtime CAS loop from manual try/catch to withRetry wrapper (3 attempts, zero backoff, ESTALE only)
  • Non-coded errors (e.g., unreadable files) still fail fast as expected

test/storage.test.ts

  • Added two regression tests exercised through the real saveAccounts path:
    1. Validates EPERM transient failures retry and eventually succeed (multiple rename attempts expected)
    2. Validates non-lock errors (ENOSPC) fail fast without retry (single attempt expected)
  • Both tests include vi.restoreAllMocks() guards to prevent spy bleed across test isolation boundaries

Validation & Risk Assessment

  • Test coverage: New regression tests provide positive and negative coverage for rename retry behavior
  • Type safety & linting: Locally verified via typecheck and eslint
  • Risk level: Low (refactoring only; rollback is straightforward)
  • Architectural note: Consolidates on shared retry helper eliminates duplicate logic and normalizes backoff/attempt tracking across codebase

Walkthrough

two filesystem retry flows refactored from manual loops to shared withRetry helper. config-save now retries ESTALE with 3 attempts and no backoff; temp-file rename retries EPERM/EBUSY with 5 attempts and 10ms exponential backoff. new tests validate that EPERM rename failures are retried successfully while non-lock errors like ENOSPC reject immediately.

Changes

Retry logic refactoring to shared withRetry helper

Layer / File(s) Summary
Config save ESTALE retry refactor
lib/config.ts
savePluginConfig (lib/config.ts:883–908) replaced inline for loop with withRetry(3 attempts, ESTALE only). each attempt recomputes mtime, re-reads config, sanitizes, re-merges patch, and retries atomic JSON write with fresh expectedMtimeMs.
Storage rename EPERM/EBUSY retry with test coverage
lib/storage.ts, test/storage.test.ts
renameTempToPath (lib/storage.ts:1841–1851) replaced manual setTimeout delay loop with withRetry(5 attempts, 10ms exponential backoff, EPERM/EBUSY only). tests (test/storage.test.ts:3979–4059) assert transient EPERM rename failures retry and succeed, while ENOSPC fails immediately without retry.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • ndycode/codex-multi-auth#516: refactors shared filesystem retry behavior for rename/lock errors—both PRs align on selective retryability of transient EPERM/EBUSY codes.
  • ndycode/codex-multi-auth#71: hardened temp-to-final rename recovery in saveAccountsUnlocked flow; this PR's withRetry refactor replaces that earlier manual retry logic.
  • ndycode/codex-multi-auth#29: also modified account save retry logic for transient lock-related rename failures with corresponding test coverage.

review notes

missing config.ts regression test: lib/config.ts refactors ESTALE retry but no test coverage added to validate the retry path. flag whether existing config tests exercise the retry case or if new test is needed for ESTALE transient failure + recover.

windows edge case—eperm vs ebusy interaction: lib/storage.ts:1841–1851 retries on both EPERM and EBUSY. windows fs.rename throws EPERM for in-use file locks but may also throw EACCES. confirm retryableCodes list is exhaustive for windows temp-rename patterns.

concurrency risk—mtime race in config save: lib/config.ts:883–908 re-stats file on each attempt. if another process writes config between stat and write, expectedMtimeMs will mismatch and lose the write. confirm the cas-protected atomic write detects this stale mtime and allows the retry loop to iterate without silent data loss.

exponential backoff scaling: lib/storage.ts uses 10ms base with doubling. confirm the 10ms floor is appropriate for your filesystem lock contention profile (e.g., antivirus scanners on windows, concurrent editors on shared nfs). max attempt 5 with 10ms base → 320ms worst-case; validate this doesn't exceed caller timeout expectations.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format (refactor type), is 67 characters (under 72), uses lowercase imperative, and accurately summarizes the main refactoring effort.
Description check ✅ Passed Description includes all major required sections with substantive detail: Summary, What Changed (with code examples and policy comparisons), Validation (showing concrete test runs), Risk/Rollback, and governance checklist. Build is justifiably deferred to CI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/audit-66-retry-loop-migration
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/audit-66-retry-loop-migration

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

test/storage.test.ts

Oops! Something went wrong! :(

ESLint: 10.0.0

Error: The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.
at /node_modules/eslint/lib/config/config-loader.js:145:10
at async loadTypeScriptConfigFileWithJiti (/node_modules/eslint/lib/config/config-loader.js:144:3)
at async loadConfigFile (/node_modules/eslint/lib/config/config-loader.js:265:11)
at async ConfigLoader.calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:588:23)
at async #calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:369:19)
at async Promise.all (index 0)
at async findFiles (/node_modules/eslint/lib/eslint/eslint-helpers.js:635:25)
at async ESLint.lintFiles (/node_modules/eslint/lib/eslint/eslint.js:1014:21)
at async Object.execute (/node_modules/eslint/lib/cli.js:386:14)
at async main (/node_modules/eslint/bin/eslint.js:175:19)


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.

ndycode pushed a commit that referenced this pull request Jun 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/storage.ts`:
- Around line 1841-1851: The PR mentions tests for the rename retry logic but
the test file is missing; add test/storage.test.ts that exercises the
renameTempToPath path (which calls fs.rename via withRetry) and covers: (1)
transient EPERM/EBUSY failures on the first N attempts then success — assert the
final content is saved and that fs.rename was called the expected number of
times, and (2) ENOSPC failing immediately — assert only one attempt was made and
the error is rethrown; ensure mocks/spies around fs.rename are restored in
afterEach using vi.restoreAllMocks() and that the tests do not mock secrets or
skip assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6b999185-1700-4b0f-85a8-335037381d40

📥 Commits

Reviewing files that changed from the base of the PR and between 76004a8 and 70e2a06.

📒 Files selected for processing (3)
  • lib/config.ts
  • lib/storage.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 (11)
lib/**/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/*.ts: All public exports should flow through lib/index.ts or documented package subpaths
Module dependencies must stay acyclic (enforced by import-x/no-cycle in lint) and follow the layering: types/constants → storage → accounts → runtime → manager/CLI; lower layers never import from higher ones
Shared types/helpers should belong in the lower layer (e.g. storage/public-types.ts), with higher layers re-exporting for surface compatibility instead of lower layers importing back from facades like lib/storage.ts
Never import from dist/ in source tests or library code

Files:

  • lib/config.ts
  • lib/storage.ts
lib/**/*.{ts,tsx}

📄 CodeRabbit inference engine (lib/AGENTS.md)

Never suppress type errors in TypeScript code

Files:

  • lib/config.ts
  • lib/storage.ts
**/*.{ts,js,mts,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM only with "type": "module" in package.json; Node >= 18.17 required

Files:

  • lib/config.ts
  • test/storage.test.ts
  • lib/storage.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Do not use as any, @ts-ignore, or @ts-expect-error TypeScript assertions

Files:

  • lib/config.ts
  • test/storage.test.ts
  • lib/storage.ts
**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

OAuth callback server uses port 1455; do not hardcode OAuth ports—use existing constants/helpers

Files:

  • lib/config.ts
  • test/storage.test.ts
  • lib/storage.ts
**/*.{js,ts}

📄 CodeRabbit inference engine (README.md)

**/*.{js,ts}: Use JSON format for machine-readable output in diagnostic and reporting commands (status, check, report, monitor, why-selected)
Implement interactive terminal dashboard with hotkeys (Up/Down for navigation, Enter for selection, 1-9 for quick switch, / for search, ? for help, Q for back)
Support device authentication flow via --device-auth flag and manual OAuth callback paste fallback via --manual flag for headless environments
Run npm version check during normal forwarded Codex startup and print upgrade notices only on interactive TTY or when CODEX_MULTI_AUTH_DEBUG=1 is set

Files:

  • lib/config.ts
  • test/storage.test.ts
  • lib/storage.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/config.ts
  • lib/storage.ts
**

⚙️ CodeRabbit configuration file

**: # PROJECT KNOWLEDGE BASE

Generated: 2026-04-25
Commit: a87e005
Validated: 2026-06-10 against commit 98d9819 (repo audit; claims re-checked against the tree, content not regenerated)
Branch: main
Package version: 2.3.0-beta.2

OVERVIEW

codex-multi-auth is a Codex CLI-first OAuth account manager and optional forwarding wrapper for the official Codex CLI. The installed codex-multi-auth entrypoint handles account-management commands locally, codex-multi-auth-codex forwards official Codex commands through this package's wrapper when explicitly used, and runtime rotation can route live Responses traffic through a localhost account-rotation proxy by default. The plugin-host entrypoint remains exported for compatibility, but the primary product surface is the account manager, optional wrapper, storage, runtime proxy, and repair tooling.

STRUCTURE

./
├── scripts/
│   ├── codex.js              # codex-multi-auth-codex wrapper, official CLI forwarder, shadow CODEX_HOME/runtime proxy setup
│   ├── codex-multi-auth.js   # standalone package CLI entrypoint
│   ├── codex-routing.js      # auth command and compatibility alias routing
│   ├── codex-bin-resolver.js # official Codex binary discovery
│   ├── codex-app-router.js   # persistent localhost router for packaged Codex app bind
│   └── codex-app-launcher.js # reversible user-level app launcher routing helper
├── index.ts                  # optional plugin-host runtime entry
├── lib/                      # core runtime logic (see lib/AGENTS.md)
│   ├── auth/                 # OAuth flow, PKCE, callback server
│   ├── runtime/              # Codex CLI/app integration helpers, app bind, live sync, runtime observability
│   ├── request/              # request transform, SSE, failover, backoff
│   ├── storage/              # path resolution, migrations, backups, restore, import/export
│   ├── codex-cli/            # Codex CLI state sync and writer helpers
│   ├── codex-manager/        # command modules and...

Files:

  • lib/config.ts
  • test/storage.test.ts
  • lib/storage.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

test/**/*.test.ts: Vitest globals (describe, it, expect) are enabled and should be used without explicit imports
Maintain 80% coverage threshold across statements, branches, functions, and lines
Use removeWithRetry for Windows filesystem cleanup instead of bare fs.rm to handle EBUSY/EPERM/ENOTEMPTY backoff
Use source files in tests, not compiled dist/ files; test the source directly
Do not skip tests without justification; include rationale if a test must be skipped
Relax ESLint rules for test files as specified in eslint.config.js

Files:

  • test/storage.test.ts
test/**/storage.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

Test concurrent access patterns, worktree migration, and forged pointer handling in storage tests

Files:

  • test/storage.test.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
🧠 Learnings (2)
📚 Learning: 2026-06-04T06:14:18.093Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/scheduling-strategy-config.test.ts:1-1
Timestamp: 2026-06-04T06:14:18.093Z
Learning: In ndycode/codex-multi-auth, do not flag explicit imports from "vitest" (e.g., describe, it, expect, beforeEach/afterEach, etc.) in test files as issues—even if the Vitest config sets `globals: true`. The repo’s established convention is to keep these imports for consistency with neighboring tests; removing them would make files outliers.

Applied to files:

  • test/storage.test.ts
📚 Learning: 2026-06-04T06:14:24.975Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/runtime-rotation-proxy.test.ts:2478-2491
Timestamp: 2026-06-04T06:14:24.975Z
Learning: In ndycode/codex-multi-auth test files (e.g. `test/*.test.ts`), when creating V3 storage fixtures for accounts, it’s an intentional convention to use `as never` for deliberately minimal stored-account objects that only include `refreshToken`, `addedAt`, and `lastUsed`. Do not treat `as never` here as a type-safety problem: optional/other fields are expected to be populated by the runtime during execution, and the cast is used solely to keep the fixture minimal and consistent across existing tests.

Applied to files:

  • test/storage.test.ts
🔇 Additional comments (3)
lib/config.ts (1)

883-908: estale cas retry is already covered by existing vitest; remove the “missing coverage” claim

lib/config.ts:883-908 wraps the config save cas loop with withRetry(... retryableCodes: ["ESTALE"]). the estale retry behavior is already exercised in test/config-save.test.ts:249-273 (config-09), where fs.rename is mocked to throw an error with code = "ESTALE" and the test asserts the final saved content keeps concurrentKey plus the requested patch. there’s also a mtime cas path covered in test/config-save.test.ts:151-156 (config-08) that simulates a concurrent write between read and rename and asserts the merge result.

optional: if you want stronger guarantees on the exact retry count (not just end-state), mock getConfigFileMtimeMs / writeJsonFileAtomicWithRetry directly and assert attempt counts around the estale throw.

			> Likely an incorrect or invalid review comment.
test/storage.test.ts (2)

3979-4022: LGTM!


4024-4058: LGTM!

Comment thread lib/storage.ts
@ndycode
ndycode merged commit 8e0e3ee into main Jun 11, 2026
2 checks passed
ndycode pushed a commit that referenced this pull request Jun 11, 2026
Second §4.3 slice (M11), unblocked by #585's merge: the three
'Aborting config save because <path> is unreadable.' throws in
savePluginConfig now go through one unreadableConfigSaveError helper
returning StorageError with the config path, an UNREADABLE code, an
actionable hint, and the classifier's message as the cause. Messages
are byte-identical, so existing rejects.toThrow("unreadable")
assertions and CLI output are unchanged.

New test pins the class, path, code, and exact message through the real
savePluginConfig env path.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
luo178 pushed a commit to luo178/codex-multi-auth that referenced this pull request Jun 23, 2026
First slice of the audit's §4.3 error-contract adoption (M11). The two
startup guards in startRuntimeRotationProxy — the loopback-only host
refusal and the missing clientApiKey check — now throw
CodexValidationError with field/expected metadata (and the offending
host in context) instead of bare Error. Messages are byte-identical, so
existing message-matching callers and tests are unaffected; callers can
now branch on instanceof or the stable CODEX_VALIDATION_ERROR code.

Documents the guarantee in docs/reference/error-contracts.md (new Typed
Error Classes section) and pins it with a test asserting the class,
field, and context from both guards.

The remaining bare throws in savePluginConfig are deferred until ndycode#585
merges - they sit inside the ESTALE CAS block that PR rewrites.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants