fix: add preuninstall hook and uninstall CLI command (#468) - #473
Conversation
…p on removal The postinstall script binds the Codex desktop app and installs OS-level launchers, but no preuninstall hook existed to reverse these changes on `npm uninstall -g`. This left residual config entries, cache dirs, and OS shortcuts after uninstall. Changes: - scripts/install-codex-auth-utils.js: add removePluginFromList() (inverse of normalizePluginList) - scripts/preuninstall.js: new npm lifecycle script reversing all postinstall operations - package.json: wire preuninstall script - lib/codex-manager/commands/uninstall.ts: new CLI command for manual cleanup of existing installs - lib/codex-manager.ts: register uninstall command - lib/codex-manager/help.ts: document uninstall in Repair section Fixes #468 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tall - Pass existing `clearAccounts` from storage.ts to runUninstallCommand so that --clear-accounts flag actually removes stored credentials instead of silently doing nothing - Fix dynamic import path from ../../../scripts to ../../../../scripts — from dist/lib/codex-manager/commands/ three levels up only reaches dist/, not the package root; four levels are required Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…scripts lib/storage/transactions.ts: initialize releaseLock with a no-op so TypeScript strict mode does not flag it as potentially uninitialized, and so callers are protected if the Promise constructor execution model ever changes (currently the executor always runs synchronously). scripts/postinstall.js: remove npm_config_ignore_scripts from isCiEnvironment(). When this flag is true npm skips lifecycle scripts entirely, so the check is unreachable in postinstall/preuninstall context. In CLI context (runPreuninstallCleanup, uninstall command) it incorrectly treats developer machines that have set ignore-scripts=true in their .npmrc as CI environments, causing cleanup to be silently skipped and leaving artifacts behind. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e ordering lib/runtime/app-bind.ts: remove premature statePath write before router starts. The state file was written twice in bindCodexAppRuntimeRotationLocked — once with port=0 and a stale boundConfigHash, then again with the real port after the router started. A crash between the two writes left a state file whose boundConfigHash did not match the config on disk, causing unbind to take the merge path instead of the clean backup-restore path, leaving config.toml in a partially-merged state. Now state is only written once, after the correct port is known. A crash before that write leaves a backup but no state, which the unbind path already handles correctly via the `backup.existed` branch. lib/storage/transactions.ts: add missing loadCurrentFlagged dep and pass currentFlagged as the third argument to the handler in withAccountAndFlaggedStorageTransaction, matching the signature of the storage.ts implementation. The divergence meant any caller using the transactions.ts version directly and expecting a third arg would receive undefined, silently operating on stale flagged-account state. lib/codex-manager/repair-commands.ts: move saveQuotaCache to immediately after withAccountStorageTransaction completes, before any console output. Previously account storage was saved then quota cache was saved only after all output was printed. An interruption between the two saves left account storage with refreshed tokens but quota cache stale, causing incorrect quota decisions on the next invocation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The per-key mutex relies on a subtle invariant: releaseCurrent() and the map identity check run synchronously with no await between them, so JS single-threading guarantees no concurrent caller can interleave. A code reviewer flagged this pattern as a potential race. Add a comment explaining exactly why it is safe, so the pattern is not incorrectly refactored in the future. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Restore hoisted readPromise in stream-failover.ts to prevent the soft/hard timeout race from silently dropping the chunk that ends a stall. - Surface --clear-accounts as a hard warning + non-zero exit when no clearAccounts handler is wired in this build, so the irreversible flag never silently no-ops. - Skip removing the shared bun.lock when other Codex plugins remain installed (preuninstall hook + uninstall CLI). bun.lock is a cross-plugin lockfile; only remove it when this is the last plugin or when Codex.json is missing. - Move isCiEnvironment into install-codex-auth-utils.js (it was being imported from there but lived in postinstall.js, so the preuninstall hook would crash with TypeError at runtime). - Forward env/home into resolveInstallPaths in preuninstall.js so the default config/cache paths can be exercised under test. - Restore package.json line endings so the diff against main is just the new preuninstall script entry. Tests: add coverage for parseAbsoluteExpirationMs (numeric seconds, ms, string seconds, and unparseable expires_at fallback), the uninstall CLI command (parsing, dry-run, plugin removal, bun.lock guard, clear-accounts wiring, partial-failure exit code), removePluginFromList in install-codex-auth-utils, and the preuninstall script (CI short-circuit, bun.lock guard branches, dry-run safety). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous pass left two edge cases that could surprise users:
1. The bun.lock-safety predicate `pluginsRemaining === null` treated
"Codex.json is missing" the same as "config exists but couldn't be
parsed" or "config has no plugins[] field". A corrupt config with
other plugins listed would still cause us to delete the shared
bun.lock.
Replace the boolean with an explicit "safe" / "uncertain" state.
We only mark "safe" when we are *certain* no other plugins remain:
- File ENOENT → safe (nothing to protect)
- Parse error / read fail → uncertain (be conservative)
- File ok, no plugins[] → uncertain (we don't know what's installed)
- File ok, plugins[]=[] → safe
- File ok, plugins[]≠[] → uncertain
2. My loadDefaultLauncher() helper swallowed import errors and silently
skipped launcher removal. Restore the original behavior: let import
failures propagate to the surrounding try/catch so the user sees a
"launcher removal skipped: ..." warning and the command exits with
a non-zero status.
Tests: add corrupt-config and missing-plugins-array regression tests
for both the TS uninstall command and the JS preuninstall script.
Add a launcher-import-failure test to lock in the warning path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 WalkthroughWalkthroughthis PR adds a full Changesuninstall flow
device auth expires_at parsing
runtime & storage robustness
Sequence DiagramsequenceDiagram
actor user as user
participant cli as codex-multi-auth cli
participant unbind as unbindCodexAppRuntimeRotation()
participant launcher as os launcher removal
participant config as Codex.json
participant cache as node_modules & bun.lock
participant accounts as clearAccounts
user->>cli: codex-multi-auth uninstall [--clear-accounts]
cli->>cli: parse args (`lib/codex-manager/commands/uninstall.ts:71-128`)
cli->>unbind: attempt unbind (optional,dynamic) (`scripts/preuninstall.js:65-87`)
unbind-->>cli: ok / warn
cli->>launcher: remove launcher (optional,dynamic) (`scripts/preuninstall.js:88-103`)
launcher-->>cli: ok / warn
cli->>config: read & edit Codex.json with `removePluginFromList` (`lib/codex-manager/commands/uninstall.ts:158-363`, `scripts/install-codex-auth-utils.js:90-98`)
config-->>cli: updated plugins, compute bun.lock safety
cli->>cache: remove node_modules (retry) and conditionally bun.lock (`lib/codex-manager/commands/uninstall.ts:158-363`)
cache-->>cli: ok / warn
alt --clear-accounts
cli->>accounts: call clearAccounts() if wired
accounts-->>cli: ok / warn
end
cli-->>user: json or human summary and exit code 0/1
missing or notable test/edge coverage
estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📋 Issue PlannerBuilt with CodeRabbit's Coding Plans for faster development and fewer bugs. View plan used: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
Adds two cases to test/transactions.test.ts that exercise the third
handler argument added to withAccountAndFlaggedStorageTransaction:
- when loadCurrentFlagged is wired, the resolved flagged storage is
forwarded to the handler verbatim (and the dep is invoked once);
- when loadCurrentFlagged is omitted, the handler receives the
empty-default { version: 1, accounts: [] } so callers like
repair-commands.ts that don't wire the dep don't silently see a
stale view of flagged storage.
Closes the open greptile P2 on PR #473.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Lifts greptile's confidence ceiling by closing the three remaining in-diff findings called out on PR #473. - uninstall.ts: dry-run now reads (but does not write) Codex.json so bunLockState is computed accurately, and the dry-run log reports the real "Would remove" vs "Would skip" decision instead of always saying "skip" because bunLockState was stuck on "uncertain". - test/uninstall-command.test.ts: the launcher-failure test was trivially true because of a `|| code === 0` fallback. Inject a removeLauncher mock that throws and assert specifically on the warning path + non-zero exit, so the skip-warning contract is actually pinned. - test/uninstall-command.test.ts and test/preuninstall.test.ts: replace bare `rmSync` in afterEach with `removeWithRetry` from test/helpers/remove-with-retry.js, matching the project's documented Windows-safety convention (see test/AGENTS.md). Local verification: - npx tsc --noEmit → clean - npx vitest run test/uninstall-command.test.ts test/preuninstall.test.ts → 29/29 pass - npx vitest run test/transactions.test.ts → 4/4 pass - npx vitest run test/index.test.ts (isolated) → 134/134 pass (the 5 timeouts in the full-parallel suite are a pre-existing flake in unrelated runtime-toast tests, not regressions from this change). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/codex-manager/commands/rotation.ts (1)
443-473: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winadd a vitest for the new helper-status size guard.
the 1 mb cap now exists in both
lib/codex-manager/commands/rotation.ts:445-473andlib/runtime/runtime-current-account.ts:130-156, but i do not see a focused regression test in the provided context for oversized or unreadable status files. this path silently downgrades tonull, so the two copies can drift again without a test.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 current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/codex-manager/commands/rotation.ts` around lines 443 - 473, Add a vitest that asserts readAppRuntimeHelperStatus (in rotation.ts) returns null when the helper-status file exceeds MAX_STATUS_FILE_BYTES (1MB) and similarly add/extend a test for the analogous guard in runtime-current-account.ts; create a temporary status file >1MB and verify the function returns null (and does not throw or leak contents), also include a test case for unreadable/corrupted JSON to ensure it returns null; reference readAppRuntimeHelperStatus and the size-guard constant MAX_STATUS_FILE_BYTES to locate the code, and ensure tests run on Windows-style paths/permissions to catch EBUSY-like behaviors without logging sensitive fields.
🤖 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/codex-manager.ts`:
- Around line 3609-3611: Add a vitest that exercises the dispatcher branch by
calling runCodexMultiAuthCli(["uninstall", "--clear-accounts"]) and asserting
the supplied storage/uninstall handler (the handler passed into
runUninstallCommand) is invoked with clearAccounts=true; update or add to
test/uninstall-command.test.ts alongside the existing cases (around 331-373) to
mirror the handler-based call path instead of directly invoking
runUninstallCommand. Ensure the test registers a spy/mock for the storage
handler used by runUninstallCommand so the assertion verifies the dispatcher
wiring, and keep the test free of sensitive logging (no tokens/emails) and
robust to Windows FS/E BUSY or 429-style retry semantics in other tests by using
deterministic mocks.
In `@lib/codex-manager/commands/uninstall.ts`:
- Around line 207-217: The uninstall command currently calls await
loadDefaultLauncher() before checking dryRun, causing preview (--dry-run) to
import the launcher module; change the logic in uninstall.ts so
loadDefaultLauncher() is only called when not in dryRun (i.e., move the await
loadDefaultLauncher() behind a !dryRun branch and only call removeLauncher when
!dryRun), and update deps handling so deps.removeLauncher still overrides
behavior; also add a regression test in test/uninstall-command.test.ts that runs
the default uninstall with --dry-run (no deps override) to assert it completes
without requiring scripts/codex-app-launcher.js to be present.
In `@lib/codex-manager/repair-commands.ts`:
- Around line 1493-1495: The code currently awaits saveQuotaCache() after
account-storage commits and lets any saveQuotaCache error bubble up, turning a
partial success into a hard failure; change the block in runFix() that calls
saveQuotaCache(workingQuotaCache) to catch errors (especially EBUSY/EPERM on
Windows) and do not rethrow—log a warning via the existing logger and record a
partial-failure marker on the run result (e.g.,
result.partialFailures.quotaCache = { code: err.code, message: err.message }) so
the overall run still succeeds; add a vitest that mocks saveQuotaCache to throw
an EBUSY/EPERM and asserts account save succeeded while the returned run report
contains the quotaCache partialFailure and the process does not throw.
In `@lib/request/stream-failover.ts`:
- Around line 230-236: Add a regression test in test/stream-failover.test.ts
that forces the pump() promise to reject while it is actively calling
controller.enqueue() so the .catch branch at lib/request/stream-failover.ts (the
pump().catch((err) => { try { controller.error(err) } catch { } })) is executed;
specifically, create a reader/read loop that starts pump(), then cancel the
reader (or close the underlying stream) in the middle of an enqueue so
controller.enqueue() throws and pump() rejects, and assert that no double error
is emitted and the code does not throw (i.e., stream handling swallows the
secondary controller.error() throw). Ensure the test references pump(),
controller.enqueue(), controller.error(), and uses cancel()/reader.cancel() to
trigger the rejection during enqueue.
In `@lib/runtime/app-bind.ts`:
- Around line 696-699: The branch that reuses existingState currently only
checks router !== null and can reuse a stale or dead router; update the
condition in the else-if that references existingState to also require
router.state === "running" && isProcessAlive(router.pid) (and ensure
spawnDetached is considered) before assigning port = existingState.port and
baseUrl = existingState.baseUrl so we only reuse a live router; additionally add
a vitest that simulates a stale status/dead-router (router.state !== "running"
or isProcessAlive returns false) to verify the code falls back and does not
rewrite config.toml to a non-listening baseUrl.
In `@lib/runtime/config-toml.ts`:
- Around line 171-174: Add a unit test that exercises the originalLine === null
branch of restoreTopLevelResponseStorage: create an "original" TOML string that
does NOT contain disable_response_storage, create a "current" TOML string that
does contain the residue line disable_response_storage = false (the state
produced by enableTopLevelResponseStorage when the key didn't previously exist),
call restoreTopLevelResponseStorage with those inputs, and assert the returned
config does NOT contain any disable_response_storage line; reference
restoreTopLevelResponseStorage and enableTopLevelResponseStorage in
lib/runtime/config-toml.ts and add the test alongside the existing tests in
test/app-bind.test.ts.
- Around line 138-141: The fallback that appends originalLine when !handled
currently pushes the model_provider into the last TOML section; update the
restore path in rewriteTopLevelModelProvider to mirror the write path by finding
the first section header (line starting with '[') in output and splicing
originalLine immediately before that index, falling back to push if no section
header exists; also add a unit test that simulates currentConfig missing the
runtime provider while originalConfig contains model_provider to ensure the
restored line lands in the root table (refer to function
rewriteTopLevelModelProvider and the restore loop handling !handled &&
originalLine).
In `@lib/runtime/hydrate-emails.ts`:
- Around line 88-97: The code builds patchById keyed only by accountId which
collapses entries with undefined accountId and lets duplicate accountIds
overwrite each other; update the merge in hydrate-emails.ts so patches are keyed
by a stable original-array identifier (e.g., use the accountsCopy index or a
composite key like `${accountId ?? 'undefined'}:${index}`) and then apply
patches by matching that stable key to the corresponding storage.accounts entry
(use the same index or stored composite key) instead of
patchById.get(account.accountId); add a vitest regression test that hydrates
multiple accounts with accountId === undefined to ensure no cross-contamination,
and ensure any logging in this flow does not print tokens/emails and conforms to
existing EBUSY/429 retry handling expectations.
In `@scripts/preuninstall.js`:
- Around line 119-156: During dry-run the code skips reading Codex.json so
bunLockState remains "uncertain"; fix by applying the same bun.lock safety
decision logic used in the uninstall preview branch to the dryRun path: when
dryRun is true, still attempt to determine bunLockState by reading
paths.configPath (using readFile/JSON.parse and Array.isArray(config.plugins))
and setting bunLockState to "safe" when the file is ENOENT or when
config.plugins becomes empty after removePluginFromList, otherwise "uncertain";
keep the existing error handling (fileError/code === "ENOENT") and the
withFileOperationRetry/writeFile usage for the non-dry path, and add a vitest
regression in test/preuninstall.test.ts that asserts dry-run logs report
bun.lock as safe for both ENOENT and single-plugin (codex-multi-auth only)
scenarios so the dry-run output matches the preview decision table implemented
in uninstall.ts.
In `@test/preuninstall.test.ts`:
- Around line 53-242: Add a concurrency regression test that spawns two
simultaneous runPreuninstallCleanup invocations against the same temp home:
create a temp home using makeTempHome(), env via envFor(home), resolve paths
with resolveTempPaths(home), write Codex.json with plugins
["codex-multi-auth","other"], then call
Promise.all([runPreuninstallCleanup(opts), runPreuninstallCleanup(opts)]) where
opts includes env, home, log, unbindCodexApp, removeLauncher, clearCache;
finally read and JSON.parse(paths.configPath) and assert the resulting
config.plugins does not contain "codex-multi-auth" and still contains "other";
name the test "concurrent invocations leave Codex.json in a consistent state"
and add it to the same describe("runPreuninstallCleanup", ...) block.
- Around line 39-51: The test's resolveTempPaths duplicates production path
logic and should be replaced by calling the real resolver: import the production
function resolveInstallPaths (from the module that currently defines it, e.g.,
lib/storage/paths or lib/runtime-paths) and invoke resolveInstallPaths with the
temporary home value instead of duplicating platform-specific logic in
resolveTempPaths; ensure the test uses the same return shape/field names as
resolveInstallPaths (map/rename fields if necessary) so assertions remain
identical to production behavior.
- Line 112: The test contains a trivially-true filesystem assertion: remove or
change the existsSync(paths.cacheBunLock) check because clearCache (the stub at
clearCache) never touches the fs so the file written earlier will always exist;
either delete the existsSync assertion or switch the test to call the real
clearCache implementation (instead of the stub) and then assert the file remains
present, and keep the meaningful check
expect(removedBunLock).not.toHaveBeenCalled() intact to validate removal was not
invoked.
In `@test/transactions.test.ts`:
- Around line 41-97: Add a deterministic vitest regression that exercises the
withStorageLock mutex-release path: write a queued-transaction test using
withAccountAndFlaggedStorageTransaction where you call it twice in sequence (or
trigger queueing) with the first handler rejecting (throwing) and the second
resolving, and assert the second handler runs despite the first rejection;
reference the withStorageLock implementation in lib/storage/transactions.ts and
the withAccountAndFlaggedStorageTransaction helper, use vi.fn() spies for
handlers and loadCurrentFlagged/loadCurrent stubs, ensure the test awaits both
transactions and verifies the mutex was released (e.g., second handler called
once and no hanging promise), and keep the test deterministic using async/await
(no real timers or filesystem races).
In `@test/uninstall-command.test.ts`:
- Around line 117-399: Add a deterministic Vitest that targets the retry branch
in the uninstall cleanup loop by making one filesystem call (e.g.,
fs.promises.rm or fs.promises.writeFile/readFile used by runUninstallCommand)
fail once with an Error { code: "EBUSY" } (or "EPERM") and then succeed on the
next call; use vi.spyOn or vi.fn to stub the specific fs.promises method
referenced by the retry loop in lib/codex-manager/commands/uninstall.ts, have
the stub throw the EBUSY error on its first invocation and return normally
thereafter, call runUninstallCommand (or the same helper used across the tests)
with appropriate stubbed paths, and assert the command returns success and that
the filesystem state is correct (i.e., retry succeeded) while ensuring the test
is deterministic and cleans up mocks.
---
Outside diff comments:
In `@lib/codex-manager/commands/rotation.ts`:
- Around line 443-473: Add a vitest that asserts readAppRuntimeHelperStatus (in
rotation.ts) returns null when the helper-status file exceeds
MAX_STATUS_FILE_BYTES (1MB) and similarly add/extend a test for the analogous
guard in runtime-current-account.ts; create a temporary status file >1MB and
verify the function returns null (and does not throw or leak contents), also
include a test case for unreadable/corrupted JSON to ensure it returns null;
reference readAppRuntimeHelperStatus and the size-guard constant
MAX_STATUS_FILE_BYTES to locate the code, and ensure tests run on Windows-style
paths/permissions to catch EBUSY-like behaviors without logging sensitive
fields.
🪄 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: 8ab68f1c-5ef9-483f-9803-e1a943782855
📒 Files selected for processing (20)
lib/auth/device-auth.tslib/codex-manager.tslib/codex-manager/commands/rotation.tslib/codex-manager/commands/uninstall.tslib/codex-manager/help.tslib/codex-manager/repair-commands.tslib/request/stream-failover.tslib/runtime/app-bind.tslib/runtime/config-toml.tslib/runtime/hydrate-emails.tslib/runtime/runtime-current-account.tslib/storage/transactions.tspackage.jsonscripts/install-codex-auth-utils.jsscripts/preuninstall.jstest/device-auth.test.tstest/install-codex-auth.test.tstest/preuninstall.test.tstest/transactions.test.tstest/uninstall-command.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 (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/runtime/hydrate-emails.tslib/codex-manager/help.tslib/runtime/runtime-current-account.tslib/codex-manager/commands/rotation.tslib/codex-manager.tslib/request/stream-failover.tslib/storage/transactions.tslib/codex-manager/repair-commands.tslib/runtime/config-toml.tslib/runtime/app-bind.tslib/codex-manager/commands/uninstall.tslib/auth/device-auth.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/transactions.test.tstest/device-auth.test.tstest/preuninstall.test.tstest/uninstall-command.test.tstest/install-codex-auth.test.ts
🔇 Additional comments (2)
lib/request/stream-failover.ts (1)
92-96: hoistedreadPromisecomment is accurate — lgtm.the comment correctly explains the invariant: a single in-flight
reader.read()promise is shared across the soft and hard timeout windows. re-issuingreader.read()after a soft timeout would silently drop the first chunk if it resolved between the two calls.package.json (1)
112-112: ⚡ Quick winno action needed — guard is already in place.
the top-level invocation in
scripts/preuninstall.js:169-180correctly guards against unhandled rejections with.catch((error) => { ... process.exitCode = 0; }). the pattern prevents npm from blocking uninstall if a dynamicimport()fails beforerunPreuninstallCleanup()completes.
Six correctness fixes flagged by CodeRabbit's review of f447ac4: CRITICAL — lib/runtime/hydrate-emails.ts patchById Map keyed on accountId collapsed every account whose accountId was undefined into one slot, so one account's hydrated refreshToken/accessToken could overwrite another's on save. accountsCopy was built via .map on storage.accounts, so it has the same length and order — switch to index-stable patching instead. MAJOR — lib/codex-manager/repair-commands.ts saveQuotaCache ran without a try/catch after account-storage was already persisted, so a Windows EBUSY/EPERM during quota cache write hard-failed the whole run despite the account fixes being committed. Trap the error, surface it as a quotaCacheSaveError field on the JSON output, and emit a warning line in the human path. MAJOR — lib/runtime/config-toml.ts restoreTopLevelModelProvider's "originalLine missing from currentConfig" recovery path appended the line at output tail. When currentConfig ended inside a [section], the appended line landed inside that section, producing invalid TOML. Splice it before the first section header instead, mirroring how the bind-time rewrite resolves the table boundary. MAJOR — lib/runtime/app-bind.ts The else-if branch that reused existingState only checked router !== null. A stale status JSON left by a dead router would satisfy that and have us write config.toml pointing at a port nothing is listening on. Tighten the predicate to require router.state === "running" && isProcessAlive(router.pid), matching the routerIsUsable check above. MINOR — lib/codex-manager/commands/uninstall.ts loadDefaultLauncher() was awaited unconditionally before the dryRun check, so dry-run preview would fail on systems where the launcher module isn't on disk yet. Defer the import behind !dryRun so dry-run truly is a no-op preview. MINOR — scripts/preuninstall.js Same defect as uninstall.ts had before the previous commit: dry-run skipped the Codex.json read entirely, so bunLockState stayed "uncertain" and the dry-run log always reported "Would skip bun.lock". Restructure so the read/parse/decision runs in both branches and only the writeFile is gated on !dryRun. Also wraps the bare readFile in withFileOperationRetry to match the rest of the uninstall paths. Local verification: - npx tsc --noEmit → clean - npx vitest run on 7 affected suites (uninstall-command, preuninstall, transactions, app-bind, codex-manager-best-command, codex-manager-rotation-command, codex-manager-monitor-command) → 96/96 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the actionable test gaps from CodeRabbit's review of f447ac4 plus regression coverage for the correctness fixes in fd8097e. New file: test/hydrate-emails.test.ts - "does not collapse two accounts that share an undefined accountId" pins the CRITICAL bug that the patchById Map fix in fd8097e addressed (one account's hydrated tokens overwriting another's when both have accountId === undefined). - "preserves untouched accounts when only some are hydrated" pins the index-stable patch semantics so a partial hydrate cycle cannot drop or reorder rows. New file: test/config-toml-restore.test.ts - 4 cases covering restoreTopLevelModelProvider, including the splice-before-first-section path that fd8097e introduced (a bare `output.push(originalLine)` would have landed the line inside the trailing section, producing invalid TOML). - 3 cases covering restoreTopLevelResponseStorage, including the originalLine-absent branch that drops bind-time residue. test/preuninstall.test.ts - "dry-run computes bunLockSafe from real Codex.json (sole plugin → safe)" pins the dry-run state-machine fix in fd8097e. - "dry-run reports bunLockSafe=false when other plugins remain" pins the conservative-default branch. - Drop a trivially-true `existsSync(paths.cacheBunLock)` assertion flagged by CodeRabbit (clearCache is a stub that never touches the filesystem in that case, so the assertion was vacuous). test/uninstall-command.test.ts - "dry-run does not load the launcher module when deps.removeLauncher is omitted" pins the dry-run launcher fix in fd8097e. - "dry-run reports bun.lock as safe-to-remove when this is the only plugin" pins the dry-run state-machine fix in the CLI path. test/transactions.test.ts - "releases the storage lock when a queued transaction rejects" pins the withStorageLock release contract called out by CodeRabbit; queues two transactions where the first throws and asserts the second still runs. All five suites pass locally: - npx vitest run on the touched files → 47/47. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Greptile flagged this as the only remaining P1 on PR #473 (their "Confidence Score: 4/5" anchor): restoreTopLevelModelProvider got the post-loop `!handled && originalLine` splice in fd8097e, but the parallel restoreTopLevelResponseStorage was left without it. If the bind-written `disable_response_storage = false` line was stripped from config.toml between bind and unbind, the user's original `disable_response_storage = true` setting was silently lost. Mirror the fix: when nothing in `currentConfig` matched the top-level disable_response_storage line, splice the originalLine in before the first section header so it lands in the root table (a tail append would land it inside whatever [section] comes last and break TOML semantics). Adds two regression cases to test/config-toml-restore.test.ts — "splices the original line before the first section when current omits it" pins the splice path; "appends the original line at tail when current has no section header" pins the no-section fallback. Local verification: - npx vitest run on config-toml-restore + app-bind + uninstall-command + preuninstall → 64/64 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@coderabbitai review The original CHANGES_REQUESTED findings have been addressed across commits |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 132622
one minor tech-debt item worth a follow-up (not a blocker):
would you like me to open a follow-up issue for that duplication? [approve] |
Five new vitest cases plus two refactors that close out the last
batch of CodeRabbit comments on the uninstall PR.
New file: test/codex-manager-uninstall-dispatch.test.ts (F1)
- "forwards a working clearAccounts handler when --clear-accounts
is set" exercises the dispatcher branch in lib/codex-manager.ts
by calling runCodexMultiAuthCli(["uninstall","--clear-accounts"])
with vi.mock-injected runUninstallCommand and storage.clearAccounts;
asserts the storage handler is actually invoked. The flag-only
wiring previously regressed once in this PR's history.
- "forwards a clearAccounts handler regardless of whether the flag
is set" pins the dispatcher contract: deps.clearAccounts is
always present and runUninstallCommand owns the gating.
New file: test/uninstall-ebusy-retry.test.ts (F14)
- vi.mock("node:fs/promises") with a one-shot EBUSY injector
forces a single readFile or rm call to throw EBUSY exactly once.
- Two cases drive runUninstallCommand through its withFileOperationRetry
loop: one for the Codex.json read path, one for the cache
rm path. Both assert exit 0 and no "skipped" warnings, proving
the retry transparently absorbed the transient failure.
test/codex-manager-cli.test.ts (F3)
- "treats a quota cache save failure as a partial-success warning,
not a hard failure" rejects saveQuotaCache once with EBUSY after
account fixes were already persisted, asserts runFix returns 0
and the JSON payload includes quotaCacheSaveError so callers can
surface the cache failure without losing the primary fix.
test/preuninstall.test.ts
- "concurrent invocations leave Codex.json valid and free of
codex-multi-auth" (F11): runs two runPreuninstallCleanup calls
in parallel against the same temp home and asserts the resulting
Codex.json is valid JSON without codex-multi-auth.
- resolveTempPaths refactor (F10): the local helper that duplicated
the platform-specific configBase/cacheBase logic is replaced with
a call to the production resolveInstallPaths so the test cannot
silently drift from real install/uninstall paths.
test/stream-failover.test.ts (F4)
- "does not emit an unhandled rejection if releasing the upstream
reader throws" pins the safety net at lib/request/stream-failover.ts:230.
Wraps an upstream reader so its releaseLock throws, drives the
response through withStreamingFailover, asserts the consumer's
body errors (no hang) and that no unhandled rejection escaped.
Local verification:
- npx tsc --noEmit → clean
- npx vitest run on 8 affected suites → 63/63 (the new files plus
every previously-touched test file).
- npx vitest run test/codex-manager-cli.test.ts -t "quota cache save
failure" → 1/1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 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/codex-manager/repair-commands.ts`:
- Around line 1494-1496: Multiple call sites invoke saveQuotaCache (e.g., the
call inside saveQuotaCache in repair-commands.ts, and the callers in
lib/codex-manager.ts and lib/codex-manager/commands/forecast.ts) without error
handling, causing EBUSY windows race issues; update those callers to wrap
saveQuotaCache in a try/catch or extract a small helper (e.g.,
trySaveQuotaCache(cache): Promise<string|null>) that performs the save inside
try/catch and returns any error message (or null on success), then replace
direct calls to saveQuotaCache with calls to the new helper or add equivalent
try/catch blocks around saveQuotaCache in the identified functions
(saveQuotaCache, forecast handlers and codex-manager callers) so failures are
logged/handled instead of crashing.
In `@scripts/preuninstall.js`:
- Around line 9-14: There are two divergent implementations of
removePluginFromList (one used by scripts/preuninstall.js and another in
lib/codex-manager/commands/uninstall.ts) that differ in null-handling;
consolidate by exporting a single canonical removePluginFromList from
install-codex-auth-utils.js that normalizes input (e.g., const list = (input ||
[]).filter(Boolean)) before removing the target plugin, then update callers
(preuninstall.js and uninstall.ts) to import and use that shared function so
both CLI and pre-uninstall behavior match exactly.
In `@test/codex-manager-cli.test.ts`:
- Around line 10931-10989: The test for quota cache save failure currently shows
the warning but does not verify that primary account mutations persist before
the quota cache fails. To fix this, enhance the test by adding a stale-token
scenario that forces a token refresh, ensuring an account change occurs and
persists first. Then, assert this persistence explicitly before confirming the
quota cache save failure results in a soft warning rather than a hard failure.
Focus on modifying the test function (the one with the description about quota
cache save failure) in test/codex-manager-cli.test.ts.
In `@test/config-toml-restore.test.ts`:
- Around line 1-46: Add a new test in test/config-toml-restore.test.ts that
feeds restoreTopLevelModelProvider CRLF-terminated strings (use '\r\n' line
endings) for both original and current inputs and asserts the restored output
preserves CRLFs and the model_provider line positioning (e.g., equality when
full round-trip, or contains provider before first section when current omits
it); reference the restoreTopLevelModelProvider function to locate where to call
it and mirror one of the existing test scenarios but with '\r\n' line endings to
ensure Windows line-ending behavior is covered.
In `@test/hydrate-emails.test.ts`:
- Around line 59-127: Add a regression test to assert the "changed === false"
path in hydrateRuntimeEmails by ensuring saveAccounts is not called when every
queuedRefresh returns { type: "failed" }; create a minimal storage row via
makeStorage (e.g., one account with refreshToken), stub queuedRefresh to always
return a failed TokenResult, set
extractAccountId/extractAccountEmail/sanitizeEmail/shouldUpdateAccountIdFromToken
to return values that prevent any update, call hydrateRuntimeEmails with the
saveAccounts spy, and expect saveAccounts.not.toHaveBeenCalled() to guard
against regressions that unconditionally call saveAccounts.
- Around line 96-126: The test "preserves untouched accounts when only some are
hydrated" declares saveAccounts as a mock (saveAccounts = vi.fn(...)) but never
asserts it was called; add an assertion after the call to hydrateRuntimeEmails
to verify saveAccounts was invoked exactly once
(expect(saveAccounts).toHaveBeenCalledTimes(1)) so the test fails if
saveAccounts is not called or called multiple times; locate the saveAccounts
mock in that test and place the assertion right after the
storage/email/accountId expectations.
- Around line 8-28: withTestEnv restores environment variables in the finally
block synchronously which can occur before the async work inside fn() completes;
update withTestEnv to either use vitest's vi.stubEnv/vi.unstubAllEnvs to manage
env vars automatically or make withTestEnv async and await fn() inside the try
block so the finally block runs only after the returned promise resolves; change
the function signature (withTestEnv) and its call sites accordingly and ensure
hydrateRuntimeEmails (and other callers) see the intended env state for the
duration of their async operations.
In `@test/stream-failover.test.ts`:
- Around line 209-259: The test currently never triggers the pump().catch()
regression because the patched releaseLock()'s throw is swallowed inside
releaseCurrentReader()'s try-catch; instead arrange a secondary failure inside
pump() by making controller.error throw (for example patch the upstream
ReadableStream's start to provide a controller whose error method throws or mark
the controller as closed so controller.error() throws) so that pump() itself
throws and the outer .catch path in pump() is exercised; update the test that
calls withStreamingFailover/response.text to assert the specific expected error
message (pin the rejection message) and remove relying on unscoped rejection
behavior, referencing the test helpers and functions releaseCurrentReader, pump,
withStreamingFailover, and response.text to locate where to change the
monkey-patch and assertions.
In `@test/transactions.test.ts`:
- Around line 99-120: The current test uses withAccountStorageTransaction to
exercise the withStorageLock finally path, but the past review requested
coverage for withAccountAndFlaggedStorageTransaction as well; add a parallel
test case that mirrors the existing "releases the storage lock when a queued
transaction rejects" scenario but calls withAccountAndFlaggedStorageTransaction
instead of withAccountStorageTransaction (use the same deps, failing/succeeding
helpers and order assertions) to ensure both lock chains are covered if they
diverge in future refactors; keep the same assertions and error/success
expectations as the original test.
In `@test/uninstall-command.test.ts`:
- Around line 29-42: The test currently duplicates production path-resolution
logic in pathsForTempHome; replace that duplication by importing and using the
production helper resolveUninstallPaths from the module under test instead of
pathsForTempHome. Remove the local pathsForTempHome function, import
resolveUninstallPaths, and call it with the temporary home value to derive
configPath, cacheNodeModules, cacheBunLock, configDir and cacheDir so the test
uses the exact production logic.
In `@test/uninstall-ebusy-retry.test.ts`:
- Around line 61-77: The test duplicates platform-specific path resolution in
pathsForTempHome; replace it by importing and calling the production resolver
(e.g., resolveUninstallPaths or resolveInstallPaths) so tests follow real logic.
Remove the pathsForTempHome function, add an import for the appropriate resolver
used in production, call that resolver with the temporary home (and any platform
override/mock if needed) and use the returned
configDir/configPath/cacheNodeModules/cacheBunLock values in the test, matching
the approach used in test/preuninstall.test.ts where the production resolver is
invoked.
🪄 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: ee1c4865-0a93-471c-b43d-cef3a796bfb9
📒 Files selected for processing (15)
lib/codex-manager/commands/uninstall.tslib/codex-manager/repair-commands.tslib/runtime/app-bind.tslib/runtime/config-toml.tslib/runtime/hydrate-emails.tsscripts/preuninstall.jstest/codex-manager-cli.test.tstest/codex-manager-uninstall-dispatch.test.tstest/config-toml-restore.test.tstest/hydrate-emails.test.tstest/preuninstall.test.tstest/stream-failover.test.tstest/transactions.test.tstest/uninstall-command.test.tstest/uninstall-ebusy-retry.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 (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/hydrate-emails.test.tstest/config-toml-restore.test.tstest/codex-manager-uninstall-dispatch.test.tstest/transactions.test.tstest/stream-failover.test.tstest/uninstall-ebusy-retry.test.tstest/codex-manager-cli.test.tstest/preuninstall.test.tstest/uninstall-command.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/runtime/hydrate-emails.tslib/runtime/config-toml.tslib/codex-manager/repair-commands.tslib/runtime/app-bind.tslib/codex-manager/commands/uninstall.ts
🔇 Additional comments (19)
lib/codex-manager/repair-commands.ts (2)
1493-1505: lgtm – quota cache save failure correctly downgraded to partial-success warning.the try/catch captures EBUSY/EPERM (common on windows) and stores the message in
quotaCacheSaveErrorinstead of rejecting after the account-storage commit already landed. the test attest/codex-manager-cli.test.ts:10933-10990validates that the run completes with exit 0 and surfacesquotaCacheSaveErrorin the json payload.
1519-1519: output paths surface the cache-save error correctly.json payload includes
quotaCacheSaveErrorat line 1519, and non-json mode prints a styled warning at lines 1556-1563. no sensitive data (tokens/emails) leaks into the error message since it's derived from the filesystem exception.Also applies to: 1556-1563
lib/runtime/config-toml.ts (2)
138-149: lgtm — splice-before-section correctly mirrors the write path.the fix at
lib/runtime/config-toml.ts:141-147now matchesrewriteTopLevelModelProvider(lines 62-65), ensuring restoredmodel_providerlands in the root table. verified bytest/config-toml-restore.test.ts:15-31.
180-204: lgtm — residue removal and splice logic are correct.
lib/runtime/config-toml.ts:180-183drops bind-writtendisable_response_storage = falsewhenoriginalLineis null (covered bytest/config-toml-restore.test.ts:56-65). the post-loop splice at lines 196-202 handles the edge case where the bind-written line was stripped by downstream tooling, putting the user's original setting back into the root table.test/config-toml-restore.test.ts (1)
48-104: test coverage forrestoreTopLevelResponseStorageis comprehensive.the suite covers:
- bind-time residue removal when original lacks the key (
test/config-toml-restore.test.ts:56-65)- splice-before-section when current omits the line (
test/config-toml-restore.test.ts:67-87)- in-section settings preserved (
test/config-toml-restore.test.ts:96-103)this addresses the prior audit finding at
lib/runtime/config-toml.ts:180-183.lib/codex-manager/commands/uninstall.ts (3)
64-69: divergent null-handling frominstall-codex-auth-utils.js.the
removePluginFromListatlib/codex-manager/commands/uninstall.ts:64-69does not pre-filter withlist.filter(Boolean), while the version inscripts/install-codex-auth-utils.jsmay. this divergence was already flagged in the preuninstall.js review — consolidate to avoid subtle mismatches.
207-226: lgtm: launcher import deferred to non-dry-run path.
lib/codex-manager/commands/uninstall.ts:210-219now only callsloadDefaultLauncher()inside the!dryRunbranch, addressing the prior review finding about dry-run requiring the launcher module to be present.
26-39: note: duplicatewithFileOperationRetryimplementation.
lib/codex-manager/commands/uninstall.ts:26-39duplicates the retry logic fromlib/fs-retry.js. coderabbitai already flagged this as tech debt for consolidation in a follow-up. not blocking, but worth tracking.lib/runtime/app-bind.ts (2)
696-720: lgtm: router liveness check now gates existingState reuse.the condition at
lib/runtime/app-bind.ts:696-703correctly gates onrouter?.state === "running" && isProcessAlive(router.pid)before reusingexistingState.port, which addresses the prior review finding. the orphan-stop fallback at lines 712-716 is also properly wrapped in.catch()for best-effort cleanup.
759-763: lgtm: warning log on orphan router does not leak sensitive data.the warning at
lib/runtime/app-bind.ts:759-762logs only the PID, which is safe. cleanup continues regardless, matching the stated intent.scripts/preuninstall.js (1)
48-222: lgtm: cleanup flow correctly computes bunLockState in both dry-run and real modes.the code at
scripts/preuninstall.js:119-170now readsCodex.jsoneven during dry-run and correctly setsbunLockStateto"safe"only when the resulting plugins list is empty or the file is missing. the sequential error isolation and best-effort semantics match the stated intent.test/preuninstall.test.ts (2)
40-44: lgtm: test now uses production path resolver.
test/preuninstall.test.ts:42-43callsresolveInstallPathsfrom the production module, eliminating the drift risk flagged in prior review.
268-302: lgtm: concurrency regression test added.the test at
test/preuninstall.test.ts:268-302exercises two parallelrunPreuninstallCleanupinvocations and asserts the resultingCodex.jsonis valid JSON withcodex-multi-authremoved. this satisfies the coding guideline requirement for concurrency regression cases.test/codex-manager-uninstall-dispatch.test.ts (1)
45-70: lgtm: dispatcher wiring tests are deterministic and verify clearAccounts forwarding.
test/codex-manager-uninstall-dispatch.test.ts:46-69validates the dispatcher always wiresclearAccountsand that invoking it callsstorage.clearAccounts. clean separation from command-level tests intest/uninstall-command.test.ts.test/uninstall-ebusy-retry.test.ts (1)
79-151: lgtm: deterministic EBUSY retry regression tests.
test/uninstall-ebusy-retry.test.ts:80-112andtest/uninstall-ebusy-retry.test.ts:114-150inject one-shot EBUSY errors and verify the retry loop recovers. this satisfies the coding guideline requirement for windows filesystem behavior regression cases.test/uninstall-command.test.ts (1)
117-466: lgtm: comprehensive coverage for runUninstallCommand.
test/uninstall-command.test.ts:117-466covers help, unknown options, dry-run behavior, plugin removal, bun.lock state decision table (safe/uncertain), partial failure handling,--clear-accountsgating, and JSON output. the regression tests at lines 161-192 and 194-226 pin the fixes for launcher import deferral and bun.lock dry-run computation.test/transactions.test.ts (2)
41-71: lgtm —loadCurrentFlaggedforwarding correctly validated.
vi.fn()spy attest/transactions.test.ts:46verifies the dep is invoked exactly once, andtoEqualat line 50 confirms the return value is passed as the third argument to the handler — matchinglib/storage/transactions.ts:92-94exactly.
73-97: lgtm — default flagged-storage fallback correctly validated.
seenarray attest/transactions.test.ts:74captures the value the handler receives, andtoEqual([{ version: 1, accounts: [] }])at line 96 matches the literal constructed atlib/storage/transactions.ts:95. deterministic with no spy needed here.lib/runtime/hydrate-emails.ts (1)
87-97: LGTM — index-stable patch correctly resolves theaccountIdcollision.the
accountsCopy[index] ?? accountmerge is safe:accountsCopyis always built with the same length and order asstorage.accounts(via.map), so there are no out-of-bounds risks, and the??correctly falls back to the original entry fornullslots.saveAccountsis called exactly once under thechangedguard. logging atlib/runtime/hydrate-emails.ts:81emits no token or email data — compliant with the no-leak rule.
…mantics The "does not mutate loaded quota cache when live fix display save fails" case still asserted that runCodexMultiAuthCli rejects with the EBUSY error. fd8097e (saveQuotaCache try/catch wrap) intentionally downgrades that path to a partial-success warning so account fixes that were already committed don't get clobbered by a Windows EBUSY/ EPERM. Update the assertion to match: the run resolves with exit 0, the loaded cache is still NOT mutated, and saveQuotaCache was called once with the expected snapshot. The mutation check that the test was originally about (originalQuotaCache stays empty) is preserved. Local verification: - npx vitest run test/codex-manager-cli.test.ts → 191/191 pass. - Full suite minus test/index.test.ts → 262/262 files, 3777/3777 tests pass. (test/index.test.ts has pre-existing parallel-worker module-load timeouts on dynamic imports inside test bodies; it passes 134/134 when run in isolation. None of the timing-out tests reference files this PR changed.) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eleven findings from CodeRabbit's review of 3fa7011 — all addressed. REAL BUGS (P0) test/hydrate-emails.test.ts withTestEnv finally-before-await The helper was non-async; `try { return Promise.resolve(fn()) } finally` restored env vars BEFORE fn's first await resolved. Make the helper async and `await fn()` so env restoration genuinely waits for the test body to finish. test/stream-failover.test.ts honest pump-rejection coverage The previous "does not emit an unhandled rejection if releasing the upstream reader throws" test passed for the wrong reason — releaseLock's secondary throw was absorbed by releaseCurrentReader's own try/catch and never reached pump().catch(). Rename and re-document it to describe what it actually verifies (the releaseCurrentReader swallow), and add a second case that drives a consumer-cancel through the pump teardown path so we have real coverage of cancellation cleanup without claiming pump().catch() coverage we don't have. PRODUCTION CHANGES lib/codex-manager.ts (runHealthCheck) lib/codex-manager/commands/forecast.ts (json + display branches) Wrap saveQuotaCache in the same try/catch that fd8097e gave repair-commands.ts so a transient Windows EBUSY/EPERM here does not abort the run after primary mutations were already committed. Both sites emit a console.warn with the original error. lib/codex-manager/commands/uninstall.ts removePluginFromList Match scripts/install-codex-auth-utils.js by pre-filtering with `list.filter(Boolean)` so the two implementations cannot drift on a stray null entry in Codex.json. TEST HARDENING (P1) test/codex-manager-cli.test.ts quota-cache failure ordering Strengthen "treats a quota cache save failure as a partial-success warning" to make the account stale, mock queuedRefresh to return a fresh token, and assert saveAccounts was called once with the refreshed accessToken/refreshToken — pinning the ordering invariant that account fixes commit BEFORE the quota cache save attempt. test/codex-manager-cli.test.ts forecast quota cache resilience Update the two pre-existing "does not mutate loaded quota cache when live forecast (json|display) save fails" cases to reflect the new partial-success semantics: forecast resolves to 0, the loaded cache stays unmutated, saveQuotaCache was still called once. test/hydrate-emails.test.ts saveAccounts assertions + changed===false Add the missing `expect(saveAccounts).toHaveBeenCalledTimes(1)` to "preserves untouched accounts when only some are hydrated", and add a third case proving saveAccounts is NOT called when every queuedRefresh fails (changed===false short-circuit). test/transactions.test.ts parallel withAccountAndFlaggedStorageTransaction Add a queued-rejection mutex-release regression for the flagged-transaction helper so a future refactor that splits the lock chain between the two helpers can't silently regress only one of them. NITPICK REFACTORS (P2) test/config-toml-restore.test.ts CRLF round-trip case Add a Windows-authored config (\r\n line endings) and verify that restoreTopLevelModelProvider preserves CRLF end-to-end (no bare \n leak). Required by coding guidelines for Windows fs discipline. test/uninstall-command.test.ts and test/uninstall-ebusy-retry.test.ts Replace the duplicated platform-specific `pathsForTempHome` helpers with a thin wrapper around the production `resolveUninstallPaths` so the test fixtures cannot drift from real install/uninstall behavior. configDir/cacheDir come from path.dirname() of the production-resolved file paths. Local verification: - npx tsc --noEmit → clean - npx vitest run on 8 affected suites → 67/67 - npx vitest run test/codex-manager-cli.test.ts → 191/191 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug-fix release shipping the uninstall-cleanup work merged via #473. Highlights: - New preuninstall lifecycle hook + codex-multi-auth uninstall CLI so npm uninstall reverses every postinstall change cleanly. - Critical hydrate-emails fix: accounts sharing accountId === undefined no longer collapse into one Map slot and copy the wrong account's refreshed tokens. - runFix now downgrades quota-cache save failures (Windows EBUSY/EPERM) to a partial-success warning instead of hard-failing after primary account fixes already committed. - restoreTopLevelModelProvider / restoreTopLevelResponseStorage now splice the recovered original line in front of the first [section] header instead of appending at tail (which previously produced invalid TOML). - app-bind requires a verifiably alive router (state==="running" plus isProcessAlive(pid)) before reusing existingState — no more config.toml rewrites pointing at dead-router ports. See docs/releases/v2.1.6.md for the full changelog. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug-fix release shipping the uninstall-cleanup work merged via #473. Highlights: - New preuninstall lifecycle hook + codex-multi-auth uninstall CLI so npm uninstall reverses every postinstall change cleanly. - Critical hydrate-emails fix: accounts sharing accountId === undefined no longer collapse into one Map slot and copy the wrong account's refreshed tokens. - runFix now downgrades quota-cache save failures (Windows EBUSY/EPERM) to a partial-success warning instead of hard-failing after primary account fixes already committed. - restoreTopLevelModelProvider / restoreTopLevelResponseStorage now splice the recovered original line in front of the first [section] header instead of appending at tail (which previously produced invalid TOML). - app-bind requires a verifiably alive router (state==="running" plus isProcessAlive(pid)) before reusing existingState — no more config.toml rewrites pointing at dead-router ports. See docs/releases/v2.1.6.md for the full changelog. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Resolves #468 —
npm uninstall codex-multi-authleft residual artifacts (launcher entries, plugin entries inCodex.json, cached node_modules, app-bind rotation state).npm preuninstalllifecycle script (scripts/preuninstall.js) that runs cleanup automatically on package removal, exits 0 even on partial failures, and skips in CI /--ignore-scriptscontexts.codex-multi-auth uninstall [--dry-run] [--json] [--clear-accounts]CLI for manual remediation on existing installs.bun.lockdeletion: only removed when this plugin is the sole entry or no config exists; preserved when other plugins remain or config is corrupt.model_provider/disable_response_storage, storage transactionreleaseLockinit +currentFlaggedwiring, app-bind double-write removal, and quota cache save ordering.Why this PR
This is a re-open of #469 (closed) with all CodeRabbit / greptile audit findings addressed in subsequent commits:
c58444dwiresclearAccountsas a function (not boolean) and corrects the launcher import path.e38f3b2initializesreleaseLockas a typed no-op and removes the false CI heuristic.4cfbe24/abfdadf/da0cec5/68b2850address the remaining app-bind, transaction, and quota cache findings.904ebaarepairs test regressions caused by the audit fixes.Branch is rebased on the latest
main(0 commits behind).Test plan
npm testpasses locally (vitest specs includetest/uninstall-command.test.ts,test/preuninstall.test.ts,test/install-codex-auth.test.ts,test/device-auth.test.ts).node scripts/preuninstall.jsis a no-op in CI (npm_config_ignore_scripts=true).codex-multi-auth uninstall --dry-run --jsonreports planned actions without mutation.codex-multi-auth uninstallremoves the plugin entry fromCodex.json, restores the launcher, unbinds app rotation, and clearsnode_modulescache.bun.lockis preserved when other plugins remain inCodex.json.--clear-accountsclears stored credentials when supplied.🤖 Generated with 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
adds
npm preuninstalllifecycle cleanup andcodex-multi-auth uninstallCLI to reverse postinstall artifacts (app-bind, launcher,Codex.jsonplugin entry,node_modulescache). bundles correctness fixes surfaced during testing:restoreTopLevelResponseStoragemissing!handled && originalLinesplice,withStorageLockuninitialisedreleaseLock,hydrateRuntimeEmailspatch-by-index to handle accounts sharingundefined accountId,parseAbsoluteExpirationMsfor device-authexpires_at, andpump().catch()for stream-failover. all previously flagged findings (barermSync,loadCurrentFlaggedwiring,config-tomlrestoration,hydrate-emailscoverage) are addressed in this revision.Confidence Score: 5/5
safe to merge — only P2 findings remain; no blocking bugs or security issues found
all P1 findings from prior review rounds have been addressed; the two remaining findings are both P2 (misleading dry-run log and non-atomic Codex.json write); test coverage is comprehensive including EBUSY retry, concurrent writes, dry-run idempotency, and all bun.lock decision-table branches; windows filesystem safety conventions (removeWithRetry in afterEach) are correctly followed
lib/codex-manager/commands/uninstall.ts and scripts/preuninstall.js — Codex.json write should be made atomic to match the install path pattern
Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant CLI as codex-multi-auth CLI participant Uninstall as uninstall.ts participant AppBind as app-bind.ts participant Launcher as codex-app-launcher.js participant Config as Codex.json participant Cache as Cache dirs User->>CLI: uninstall flags CLI->>Uninstall: runUninstallCommand Uninstall->>AppBind: unbindCodexAppRuntimeRotation AppBind-->>Uninstall: ok or partial failure Uninstall->>Launcher: remove launcher Launcher-->>Uninstall: ok or partial failure Uninstall->>Config: read parse removePluginFromList Config-->>Uninstall: bunLockState computed Uninstall->>Config: write updated plugins Uninstall->>Cache: rm node_modules with EBUSY retry alt bunLockState is safe Uninstall->>Cache: rm bun.lock end alt clear-accounts flag set Uninstall->>Uninstall: deps.clearAccounts end Uninstall-->>CLI: exit 0 or 1Comments Outside Diff (5)
test/preuninstall.test.ts, line 1197-1203 (link)rmSyncin afterEach — windows EBUSY/EPERM riskboth new test files use
rmSync(root, { recursive: true, force: true })directly inafterEachcleanup. the project anti-pattern (test/AGENTS.md) explicitly says "do not use barefs.rmin test cleanup; useremoveWithRetryfor Windows safety." on Windows these temp dirs can still have open handles immediately after a test completes, causingEBUSY/EPERMfailures on CI. the same pattern appears intest/uninstall-command.test.tsafterEachas well.Context Used: test/AGENTS.md (source)
Prompt To Fix With AI
lib/request/stream-failover.ts, line 512-518 (link)pump().catch()path has no vitest coveragethe change from
void pump()topump().catch((err) => controller.error(err))ensures unhandled async errors from the pump coroutine reach theReadableStreamconsumer instead of becoming silent unhandled rejections. this is a meaningful concurrency correctness fix, buttest/stream-failover.test.tshas no test case that triggers an error thrown directly from thepump()body (as opposed to errors originating inside the primary or failover stream reads). a fake-timer test that injects a pump-level throw would pin the contract.Prompt To Fix With AI
lib/runtime/config-toml.ts, line 587-591 (link)!handled && originalLinerestoration fixthe
!handled && originalLineguard added torestoreTopLevelModelProvideris a real bug fix — without it, if themodel_providerline appears nowhere incurrentConfig(e.g. the bind wrote it but it was later stripped by another tool), the original line is never appended back. there are no tests in the current suite exercising this path for eitherrestoreTopLevelModelProvideror the parallelrestoreTopLevelResponseStorageoriginalLine-absent branch. both are called on every unbind; untested regressions here would silently corruptconfig.toml.Prompt To Fix With AI
lib/runtime/hydrate-emails.ts, line 622-633 (link)storage.accountshas no vitest coveragethe switch from
storage.accounts = accountsCopyto building apatchByIdmap and merging peraccountIdis a correctness fix — it avoids dropping accounts that weren't included inaccountsCopy. no test in the repo exercises this function (hydrateRuntimeEmailshas no dedicated test file). a missed regression here could silently lose account records on hydration whenaccountsCopyis a partial slice.Prompt To Fix With AI
lib/runtime/config-toml.ts, line 154-191 (link)!handled && originalLineguard missing fromrestoreTopLevelResponseStoragerestoreTopLevelModelProviderreceived a post-loop block (lines 138-149) that splicesoriginalLineback into the root table when the bind-writtenmodel_providerline was stripped fromcurrentConfig.restoreTopLevelResponseStoragenever got the equivalent block.Concrete failure: user originally had
disable_response_storage = true. Bind overwrites it withdisable_response_storage = false. User or another tool deletes that line. On unbind,handledstaysfalse,originalLineis non-null, but no code re-inserts it — the setting is silently dropped and the user's preference is lost. The same fix pattern applies:Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (8): Last reviewed commit: "fix: address coderabbit's third-round fi..." | Re-trigger Greptile