Skip to content

fix(ui): harden dashboard health summary#125

Closed
ndycode wants to merge 5 commits intodevfrom
clean/pr101-dashboard-health-summary
Closed

fix(ui): harden dashboard health summary#125
ndycode wants to merge 5 commits intodevfrom
clean/pr101-dashboard-health-summary

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 19, 2026

Summary

  • clean replacement for bloated #101
  • narrows the review back to the dashboard health-summary slice only
  • based on dev from fresh branch clean/pr101-dashboard-health-summary

What Changed

  • hardens the login menu health summary so restore, rollback, sync, and doctor state stay accurate without dragging later stack work into the diff
  • keeps the scope to the dashboard/login path and matching CLI coverage in test/codex-manager-cli.test.ts
  • preserves current dev behavior while resolving the drift that made the old PR hard to review safely

Validation

  • npx vitest run test/index.test.ts test/index-retry.test.ts test/codex-manager-cli.test.ts test/cli-auth-menu.test.ts test/storage.test.ts test/paths.test.ts test/codex-cli-sync.test.ts test/live-account-sync.test.ts test/documentation.test.ts
  • npm run lint
  • npm run typecheck
  • npm test
  • npm run build

Docs and Governance Checklist

  • README updated (if user-visible behavior changed)
  • docs/getting-started.md updated (if onboarding flow changed)
  • docs/features.md updated (if capability surface changed)
  • relevant docs/reference/* pages updated (if commands/settings/paths changed)
  • docs/upgrade.md updated (if migration behavior changed)
  • SECURITY.md and CONTRIBUTING.md reviewed for alignment

Risk and Rollback

  • Risk level: low
  • Rollback plan: revert 9d2bf18

Additional Notes

  • Supersedes #101.
  • This replacement diff is 5 files instead of the old 43-file PR.

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

Greptile Summary

this pr adds a read-only health-summary row to the dashboard login menu, surfacing pool size, sync state, restore readiness, rollback status, and a lightweight doctor scan on every menu repaint. the implementation is well-scoped: io is parallelized via Promise.allSettled and Promise.all, errors are isolated and redacted before logging (important on windows where paths like C:\sensitive\rollback.json can appear in errno messages), and a sanitizeTerminalText pass protects the tui from ansi/control-character injection.

key changes:

  • buildLoginMenuHealthSummary assembles the five-part label/hint from parallel async reads with per-component fallbacks
  • summarizeReadOnlyDoctorState adds a read-only pass over token fingerprints (sha-256, 12-char hex) to count duplicates without exposing raw tokens
  • readLatestSyncHistorySync gains a 3-attempt retry loop using Atomics.wait-based sleepSync for transient windows file locks
  • showAuthMenu renders the summary as a disabled, sanitized row under a new "Health Summary" heading
  • 8 new integration tests cover the happy path, all failure modes, and the sanitization boundary

two items to address before merge:

  • EACCES should be removed from RETRYABLE_LATEST_READ_CODES — it is a permanent permission denial on windows, not a transient lock, so retrying wastes 70 ms of main-thread blocking on every menu iteration for affected machines
  • the sleepSync / Atomics.wait main-thread block is acceptable for a cli sync function but should be noted — persistent locks will add up to 70 ms of event-loop freeze per menu keystroke

Confidence Score: 3/5

  • safe to merge after removing EACCES from the retry set; everything else is well-guarded
  • the core implementation is solid — parallelized io, per-component error isolation, path redaction, and sanitized tui output are all correct. the EACCES retry issue is a real windows-safety concern that adds unnecessary blocking delay on machines with permission-denied files, lowering confidence slightly. the event-loop blocking from Atomics.wait is intentional but worth documenting. test coverage is comprehensive with 8 new cases.
  • lib/sync-history.ts — EACCES in RETRYABLE_LATEST_READ_CODES and the Atomics.wait main-thread block

Important Files Changed

Filename Overview
lib/sync-history.ts adds retry loop with synchronous Atomics.wait sleep for EBUSY/EPERM/EACCES on readLatestSyncHistorySync; EACCES should be removed from the retryable set as it is not transient on windows, and the main-thread blocking warrants explicit documentation
lib/codex-manager.ts adds buildLoginMenuHealthSummary with parallelized I/O via Promise.allSettled, SHA-256 fingerprinting for read-only duplicate-token detection, redacted filesystem error logging, and parallel Promise.all in the menu loop — solid implementation with comprehensive fallback handling
lib/ui/auth-menu.ts adds AuthMenuReadOnlyRow interface and conditionally renders a sanitized disabled health-summary row with heading separator in showAuthMenu; control-character sanitization via sanitizeTerminalText is correctly applied to both label and hint
test/codex-manager-cli.test.ts adds 8 new integration tests covering the happy path, load failures, rollback states, sync states, empty-account short-circuit, UI rendering, and control-character sanitization; mock for getActionableNamedBackupRestores is missing the allAssessments field in one test
test/sync-history.test.ts adds isolated vi.doMock("node:fs") test that verifies EBUSY retry succeeds on second attempt; correct use of vi.resetModules in finally block to avoid test pollution

Sequence Diagram

sequenceDiagram
    participant RL as runAuthLogin (while loop)
    participant FA as loadFlaggedAccounts()
    participant BHS as buildLoginMenuHealthSummary()
    participant GANB as getActionableNamedBackupRestores()
    participant GLRP as getLatestCodexCliSyncRollbackPlan()
    participant SLCS as summarizeLatestCodexCliSyncState()
    participant RLSH as readLatestSyncHistorySync()
    participant SRDS as summarizeReadOnlyDoctorState()
    participant PM as promptLoginMode → showAuthMenu

    RL->>+FA: Promise.all([...])
    RL->>+BHS: Promise.all([...])

    BHS->>+GANB: Promise.allSettled([...])
    BHS->>+GLRP: Promise.allSettled([...])
    GANB-->>-BHS: restoresResult (fulfilled | rejected)
    GLRP-->>-BHS: rollbackResult (fulfilled | rejected)

    BHS->>+SLCS: try/catch
    SLCS->>+RLSH: readLatestSyncHistorySync()
    note over RLSH: retry ×3 on EBUSY/EPERM<br/>sleepSync(10→20→40 ms)<br/>blocks event loop
    RLSH-->>-SLCS: SyncHistoryEntry | null
    SLCS-->>-BHS: {label, hint}

    BHS->>+SRDS: try/catch
    note over SRDS: SHA-256 fingerprint<br/>duplicate-token check
    SRDS-->>-BHS: DoctorReadOnlySummary

    BHS-->>-RL: DashboardHealthSummary | null
    FA-->>-RL: flaggedStorage

    RL->>+PM: promptLoginMode({healthSummary, flaggedCount})
    PM-->>-RL: MenuResult
Loading

Comments Outside Diff (1)

  1. lib/sync-history.ts, line 9 (link)

    P1 EACCES is not transient on Windows — retrying it wastes 70 ms

    EBUSY and EPERM can represent transient exclusive locks on Windows (antivirus, indexer, another process mid-write), which is why RETRYABLE_REMOVE_CODES includes them. EACCES is different: it signals a permanent permission denial (Access is denied.). Unlike the write-path set (RETRYABLE_REMOVE_CODES) which deliberately omits EACCES, the new read-path set includes it. On any machine where the sync-history file has incorrect ACLs the retry loop will burn the full 10 + 20 + 40 = 70 ms of synchronous main-thread blocking on every health-summary build — and every menu repaint inside the while (true) loop.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: lib/sync-history.ts
    Line: 9
    
    Comment:
    **`EACCES` is not transient on Windows — retrying it wastes 70 ms**
    
    `EBUSY` and `EPERM` can represent transient exclusive locks on Windows (antivirus, indexer, another process mid-write), which is why `RETRYABLE_REMOVE_CODES` includes them. `EACCES` is different: it signals a permanent permission denial (`Access is denied.`). Unlike the write-path set (`RETRYABLE_REMOVE_CODES`) which deliberately omits `EACCES`, the new read-path set includes it. On any machine where the sync-history file has incorrect ACLs the retry loop will burn the full `10 + 20 + 40 = 70 ms` of synchronous main-thread blocking on every health-summary build — and every menu repaint inside the `while (true)` loop.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/sync-history.ts
Line: 9

Comment:
**`EACCES` is not transient on Windows — retrying it wastes 70 ms**

`EBUSY` and `EPERM` can represent transient exclusive locks on Windows (antivirus, indexer, another process mid-write), which is why `RETRYABLE_REMOVE_CODES` includes them. `EACCES` is different: it signals a permanent permission denial (`Access is denied.`). Unlike the write-path set (`RETRYABLE_REMOVE_CODES`) which deliberately omits `EACCES`, the new read-path set includes it. On any machine where the sync-history file has incorrect ACLs the retry loop will burn the full `10 + 20 + 40 = 70 ms` of synchronous main-thread blocking on every health-summary build — and every menu repaint inside the `while (true)` loop.

```suggestion
const RETRYABLE_LATEST_READ_CODES = new Set(["EBUSY", "EPERM"]);
```

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

---

This is a comment left during a code review.
Path: lib/sync-history.ts
Line: 16-21

Comment:
**`Atomics.wait` blocks the entire Node.js event loop on the main thread**

`Atomics.wait` on the main thread is permitted in Node.js (unlike browsers) but it hard-parks the event loop — no timers, no I/O callbacks, nothing runs. `readLatestSyncHistorySync` is already fully sync, so the intent is clear, but because it is called from inside `buildLoginMenuHealthSummary` which runs on every iteration of the `while (true)` menu loop, a persistently-busy file means every single keystroke adds up to 70 ms of event-loop freeze. on windows, where file-system contention from antivirus/indexers is common, this will surface as a visibly sluggish menu.

if the function must stay synchronous, the sleep is unavoidable. worth documenting the block duration explicitly and keeping the retry budget tight (current `10→20→40 ms` is already minimal). if async were ever an option, `setImmediate`-based retry with `Promise.allSettled` would avoid the freeze entirely.

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

---

This is a comment left during a code review.
Path: test/codex-manager-cli.test.ts
Line: 610-613

Comment:
**mock missing `allAssessments` field**

`buildLoginMenuHealthSummary` only reads `assessments.length` and `totalBackups` today so this doesn't break the test, but the mock diverges from the actual return type of `getActionableNamedBackupRestores` (which includes `allAssessments: []`). if the production code is later updated to use `allAssessments`, the test will silently pass with `undefined` instead of an empty array and won't catch the regression.

```suggestion
		getActionableNamedBackupRestoresMock.mockResolvedValue({
			assessments: [{}],
			allAssessments: [{}],
			totalBackups: 2,
		});
```

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

Last reviewed commit: "fix(login-menu): pre..."

@chatgpt-codex-connector
Copy link

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
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

walkthrough

this pr adds a read-only health summary to the auth login flow. the cli builds a health summary from persisted sync/rollback and doctor storage reads and passes it into the ui prompt, which may render it as a disabled "health summary" row before accounts.

changes

Cohort / File(s) Summary
login flow integration
lib/cli.ts:line, lib/codex-manager.ts:line
added health-summary computation and wired buildLoginMenuHealthSummary() into the auth login path; promptLoginMode now can receive healthSummary.
health summary computation
lib/codex-manager.ts:line
new summarizers and helpers: summarizeLatestCodexCliSyncState(), summarizeReadOnlyDoctorState(), formatLoginMenuRollbackHint(), buildLoginMenuHealthSummary(); added getReadOnlyDoctorRefreshTokenFingerprint() (sha256 truncation) and related types DashboardHealthSummary/DoctorReadOnlySummary.
ui rendering / copy
lib/ui/auth-menu.ts:line, lib/ui/copy.ts:line
introduced AuthMenuReadOnlyRow and AuthMenuOptions.healthSummary; showAuthMenu conditionally renders a sanitized, disabled health-summary row under a "Health Summary" heading; added UI_COPY.mainMenu.healthSummary.
tests
test/codex-manager-cli.test.ts:line
updated expectations for promptLoginMode to include explicit healthSummary: undefined when absent; adjusted restore call expectations; added tests for successful health summary, fallback on read failures (asserting sanitized warnings), and rendering/sanitization in showAuthMenu.

sequence diagram(s)

sequenceDiagram
    autonumber
    actor user
    participant cli as "cli\n(lib/cli.ts)"
    participant codex as "codex-manager\n(lib/codex-manager.ts)"
    participant storage as "storage"
    participant ui as "auth-menu\n(lib/ui/auth-menu.ts)"

    user->>cli: start auth login
    cli->>codex: buildLoginMenuHealthSummary(currentStorage)
    codex->>storage: read backups / rollbacks / last sync / doctor state
    storage-->>codex: returns results (or errors)
    codex-->>cli: healthSummary (or undefined/fallback)
    cli->>ui: promptLoginMode(..., healthSummary)
    ui->>user: render menu (includes disabled health row if provided)
Loading

estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

suggested labels

bug

notes for reviewer

  • missing regression tests: add a focused test for getReadOnlyDoctorRefreshTokenFingerprint() to validate normalization across path separators and edge token shapes (lib/codex-manager.ts:line, test/codex-manager-cli.test.ts:line).
  • windows edge case: getReadOnlyDoctorRefreshTokenFingerprint() normalizes refresh-token keys but lacks explicit handling of backslash vs forward-slash; add windows-path tests and normalization verification (lib/codex-manager.ts:line).
  • concurrency risk: buildLoginMenuHealthSummary() reads multiple storage pieces via parallel reads (Promise.allSettled) without transactional guarantees; document eventual-consistency and consider a single read snapshot or retry to avoid mixed-state hints (lib/codex-manager.ts:line).
  • fingerprint collision: truncated sha256 to 12 hex chars reduces entropy (~48 bits); call this out in code comments and tests where duplicate detection is asserted (lib/codex-manager.ts:line).
  • error scrubbing: tests assert warnings omit raw errors, but ensure all catch branches uniformly scrub sensitive details before logging (lib/codex-manager.ts:line, test/codex-manager-cli.test.ts:line).
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed title is fully related to the main change: it clearly summarizes the dashboard health summary hardening.
Description check ✅ Passed PR description follows template structure with all required sections completed: summary, what changed, validation checklist marked, docs/governance checklist, risk level, and rollback plan provided.

✏️ 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 clean/pr101-dashboard-health-summary
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch clean/pr101-dashboard-health-summary
📝 Coding Plan
  • Generate coding plan for human review comments

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ui/auth-menu.ts`:
- Around line 614-637: Add a focused test that passes adversarial control/ANSI
characters into the healthSummary prop and asserts sanitizeTerminalText strips
or neutralizes them before rendering: call the code path that constructs
healthSummaryLabel/healthSummaryHint (which use sanitizeTerminalText) and render
the menu, then assert the output for the disabled health-summary row (label from
UI_COPY.mainMenu.healthSummary and the disabled row created after
healthSummaryLabel) contains no raw control sequences (e.g., "\x1b[31m", "\n",
"\x00") and only the sanitized text; target the existing spec around the health
summary rendering and ensure the test fails if sanitizeTerminalText does not
remove/escape those characters.

In `@test/codex-manager-cli.test.ts`:
- Line 788: Remove the duplicate mock default by deleting the later call to
getLastCodexCliSyncRunMock.mockReturnValue(null); so the test relies on the
existing default already established earlier (keep the original mock setup and
eliminate this redundant reassignment to avoid fixture noise).
- Around line 4450-4507: The test currently only asserts pool count and rollback
hint in the health summary; add assertions to also verify the sync and restore
sections so regressions in those slices fail. After invoking
runCodexMultiAuthCli and before finishing the test, extend the
promptLoginModeMock expectation to check the passed healthSummary includes
sync-related fields (from getLastCodexCliSyncRunMock) and restore-related fields
(from getActionableNamedBackupRestoresMock /
getLatestCodexCliSyncRollbackPlanMock) — e.g. use expect.objectContaining for
healthSummary.sync (or label containing "Sync" and counts/added/updated details)
and healthSummary.restore (or label containing "Restore"/"Backups" and
totalBackups/assessments info) when verifying promptLoginModeMock was called.
Ensure you reference promptLoginModeMock and the mocked fixtures
getActionableNamedBackupRestoresMock, getLastCodexCliSyncRunMock, and
getLatestCodexCliSyncRollbackPlanMock so the assertions map to the mocked
values.
🪄 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: 10f40d66-1cbf-43d4-ad9f-11a4f2edf758

📥 Commits

Reviewing files that changed from the base of the PR and between 115f93f and 9d2bf18.

📒 Files selected for processing (5)
  • lib/cli.ts
  • lib/codex-manager.ts
  • lib/ui/auth-menu.ts
  • lib/ui/copy.ts
  • test/codex-manager-cli.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/cli.ts
  • lib/ui/copy.ts
  • lib/ui/auth-menu.ts
  • lib/codex-manager.ts
test/**

⚙️ CodeRabbit configuration file

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

Files:

  • test/codex-manager-cli.test.ts
🔇 Additional comments (8)
lib/ui/copy.ts (1)

12-12: looks good for the new main menu copy key.

lib/ui/copy.ts:12 (line [12]) cleanly adds the label and keeps the copy structure consistent. no windows io, concurrency, or sensitive logging surface is introduced here.

lib/cli.ts (1)

7-11: health summary plumbing is clean and backward-compatible.

lib/cli.ts:98 (line [98]) keeps healthSummary optional, and lib/cli.ts:273-276 forwards it without mutating state. this is a safe pass-through with no added windows fs edge, queue behavior, or concurrency risk.

Also applies to: 96-99, 273-276

lib/ui/auth-menu.ts (1)

52-60: typed api shape for read-only health rows is solid.

lib/ui/auth-menu.ts:52-60 (line [52] and line [59]) introduces a clear, narrow contract and keeps the option optional for compatibility.

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

3629-3631: good contract pin for omitted health summary.

test/codex-manager-cli.test.ts:3629-3631 and test/codex-manager-cli.test.ts:4580-4584 lock the new second-arg contract to healthSummary: undefined instead of letting the option bag disappear silently.

Also applies to: 4580-4584


3977-3977: good i/o guard rails around the health-summary reads.

test/codex-manager-cli.test.ts:3977, test/codex-manager-cli.test.ts:4271, and test/codex-manager-cli.test.ts:4575-4578 pin the single backup scan and the skipped rollback/sync reads. that is the right regression surface for this login flow.

Also applies to: 4271-4271, 4575-4578


4510-4560: good windows redaction and ui-row coverage.

test/codex-manager-cli.test.ts:4510-4560 protects the warning sanitization path for windows-style rollback locations, and test/codex-manager-cli.test.ts:4587-4627 pins the summary row as disabled directly under the heading.

Also applies to: 4587-4627

lib/codex-manager.ts (2)

3411-3417: good privacy boundary in the read-only doctor path.

the summary stays read-only and only uses a short hash for duplicate-token detection, so the login flow never needs to surface raw refresh tokens or mutate storage just to render health. lib/codex-manager.ts:3411-3417, lib/codex-manager.ts:4007-4094. As per coding guidelines, check for logging that leaks tokens or emails.

Also applies to: 4007-4094


4355-4363: the prompt wiring is nicely isolated.

keeping healthSummary optional here preserves the existing login menu behavior when summary generation returns null. lib/codex-manager.ts:4355-4363.

Comment on lines +4104 to +4215
function formatLoginMenuRollbackHint(
rollbackPlan: Awaited<ReturnType<typeof getLatestCodexCliSyncRollbackPlan>>,
rollbackPlanLoadFailed = false,
): string {
if (rollbackPlanLoadFailed) {
return "rollback state unavailable";
}
if (rollbackPlan.status === "ready") {
const accountCount = rollbackPlan.accountCount;
if (
typeof accountCount === "number" &&
Number.isFinite(accountCount) &&
accountCount >= 0
) {
return `checkpoint ready for ${Math.trunc(accountCount)} account(s)`;
}
return "checkpoint ready";
}
return rollbackPlan.snapshot ? "checkpoint unavailable" : "no rollback checkpoint available";
}

async function buildLoginMenuHealthSummary(
storage: AccountStorageV3,
): Promise<DashboardHealthSummary | null> {
if (storage.accounts.length === 0) {
return null;
}
const enabledCount = storage.accounts.filter(
(account) => account.enabled !== false,
).length;
const disabledCount = storage.accounts.length - enabledCount;
let actionableRestores: Awaited<
ReturnType<typeof getActionableNamedBackupRestores>
> = {
assessments: [],
allAssessments: [],
totalBackups: 0,
};
let rollbackPlan: Awaited<
ReturnType<typeof getLatestCodexCliSyncRollbackPlan>
> = {
status: "unavailable",
reason: "rollback state unavailable",
snapshot: null,
};
let rollbackPlanLoadFailed = false;
let syncSummary = {
label: "unknown",
hint: "Sync state unavailable.",
};
let doctorSummary: DoctorReadOnlySummary = {
severity: "warn",
label: "unknown",
hint: "Doctor state unavailable.",
};
try {
actionableRestores = await getActionableNamedBackupRestores({
currentStorage: storage,
});
} catch (error) {
logLoginMenuHealthSummaryWarning(
"Failed to load login menu restore health summary state",
error,
);
}
try {
rollbackPlan = await getLatestCodexCliSyncRollbackPlan();
} catch (error) {
rollbackPlanLoadFailed = true;
logLoginMenuHealthSummaryWarning(
"Failed to load login menu rollback health summary state",
error,
);
}
try {
syncSummary = summarizeLatestCodexCliSyncState();
} catch (error) {
logLoginMenuHealthSummaryWarning(
"Failed to summarize login menu sync state",
error,
);
}
try {
doctorSummary = summarizeReadOnlyDoctorState(storage);
} catch (error) {
logLoginMenuHealthSummaryWarning(
"Failed to summarize login menu doctor state",
error,
);
}
const restoreLabel =
actionableRestores.totalBackups > 0
? `${actionableRestores.assessments.length}/${actionableRestores.totalBackups} ready`
: "none";
const rollbackLabel = rollbackPlan.status === "ready" ? "ready" : "none";
const accountLabel =
disabledCount > 0
? `${enabledCount}/${storage.accounts.length} enabled`
: `${storage.accounts.length} active`;
const hintParts = [
`Accounts: ${enabledCount} enabled / ${disabledCount} disabled / ${storage.accounts.length} total`,
`Sync: ${syncSummary.hint}`,
`Restore backups: ${actionableRestores.assessments.length} actionable of ${actionableRestores.totalBackups} total`,
`Rollback: ${formatLoginMenuRollbackHint(rollbackPlan, rollbackPlanLoadFailed)}`,
`Doctor: ${doctorSummary.hint}`,
];

return {
label:
`Pool ${accountLabel} | Sync ${syncSummary.label} | Restore ${restoreLabel} | ` +
`Rollback ${rollbackLabel} | Doctor ${doctorSummary.label}`,
hint: hintParts.join(" | "),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

don't collapse unreadable restore/rollback state into none.

getLatestCodexCliSyncRollbackPlan() already returns status: "unavailable" with a reason when rollback history cannot be read, but this path still renders rollback none. the restore catch path does the same thing by falling back to zero counts. on windows ebusy/eacces, the health row will tell users there is nothing to restore or roll back when the state is actually unknown. please carry an explicit unavailable state through the label and hint, and add a vitest regression for the unreadable-filesystem branch. lib/codex-manager.ts:4104-4215, lib/codex-cli/sync.ts:567-587. As per coding guidelines, focus on auth rotation, windows filesystem io, and concurrency.

possible fix
 function formatLoginMenuRollbackHint(
 	rollbackPlan: Awaited<ReturnType<typeof getLatestCodexCliSyncRollbackPlan>>,
 	rollbackPlanLoadFailed = false,
 ): string {
 	if (rollbackPlanLoadFailed) {
 		return "rollback state unavailable";
 	}
 	if (rollbackPlan.status === "ready") {
 		const accountCount = rollbackPlan.accountCount;
 		if (
 			typeof accountCount === "number" &&
 			Number.isFinite(accountCount) &&
 			accountCount >= 0
 		) {
 			return `checkpoint ready for ${Math.trunc(accountCount)} account(s)`;
 		}
 		return "checkpoint ready";
 	}
-	return rollbackPlan.snapshot ? "checkpoint unavailable" : "no rollback checkpoint available";
+	const reason = collapseWhitespace(rollbackPlan.reason);
+	if (reason.toLowerCase().includes("unavailable")) {
+		return reason;
+	}
+	return rollbackPlan.snapshot ? "checkpoint unavailable" : "no rollback checkpoint available";
 }
 
 async function buildLoginMenuHealthSummary(
 	storage: AccountStorageV3,
 ): Promise<DashboardHealthSummary | null> {
@@
 	let rollbackPlanLoadFailed = false;
+	let restoreStateUnavailable = false;
@@
 	try {
 		actionableRestores = await getActionableNamedBackupRestores({
 			currentStorage: storage,
 		});
 	} catch (error) {
+		restoreStateUnavailable = true;
 		logLoginMenuHealthSummaryWarning(
-			"Failed to load login menu restore health summary state",
+			"Failed to load login menu restore health summary state",
 			error,
 		);
 	}
@@
+	const rollbackHint = formatLoginMenuRollbackHint(
+		rollbackPlan,
+		rollbackPlanLoadFailed,
+	);
 	const restoreLabel =
-		actionableRestores.totalBackups > 0
+		restoreStateUnavailable
+			? "unavailable"
+			: actionableRestores.totalBackups > 0
 			? `${actionableRestores.assessments.length}/${actionableRestores.totalBackups} ready`
 			: "none";
-	const rollbackLabel = rollbackPlan.status === "ready" ? "ready" : "none";
+	const rollbackLabel =
+		rollbackPlan.status === "ready"
+			? "ready"
+			: rollbackHint.toLowerCase().includes("unavailable")
+				? "unavailable"
+				: "none";
@@
-		`Rollback: ${formatLoginMenuRollbackHint(rollbackPlan, rollbackPlanLoadFailed)}`,
+		`Rollback: ${rollbackHint}`,

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/codex-manager.ts`:
- Around line 4096-4102: Replace the raw console.warn in
logLoginMenuHealthSummaryWarning with the module's structured logger (the
createLogger instance used elsewhere, e.g., log.warn) to keep observability
consistent: call log.warn and pass the same message built from context and
getRedactedFilesystemErrorLabel(error) and include the original error (or safe
metadata) as structured metadata so the warning is emitted through the central
logger instead of console.warn.
- Around line 3960-4005: The test suite lacks coverage for
summarizeLatestCodexCliSyncState and the file-read path doesn't retry on Windows
EBUSY errors; add a Vitest unit test exercising summarizeLatestCodexCliSyncState
(covering null latest run, various outcome labels, counts, and message trimming)
and modify readLatestSyncHistorySync to retry on EBUSY similar to the existing
retryable logic (use the same retry/backoff strategy and reuse the
RETRYABLE_REMOVE_CODES pattern), ensuring
getLastCodexCliSyncRun/summarizeLatestCodexCliSyncState callpaths are exercised
and the in-memory cache behavior remains unchanged.

In `@test/codex-manager-cli.test.ts`:
- Around line 4496-4505: The test only asserts the aggregated "Doctor 3
warnings" label; update the test that uses promptLoginModeMock to also assert
the doctor-specific detail inside healthSummary.hint by adding a fixture that
shares a refresh token (simulate the duplicate refresh-token fingerprint branch)
and then expect.objectContaining({ healthSummary: expect.objectContaining({
hint: expect.stringMatching(/Doctor: .*duplicate refresh
token|doctor.*refresh-token/iu) }) }) to ensure the hint contains the
doctor-specific message; locate the assertion around promptLoginModeMock in
test/codex-manager-cli.test.ts and modify the fixture setup to make one account
share a refresh token, keep vitest determinism, and avoid mocking real secrets.
- Around line 4525-4547: The mocks throw plain Error instances so the sanitizer
never sees errno-style errors and the Windows path redaction branch isn't
exercised; update the mocked failures (getActionableNamedBackupRestoresMock,
getLatestCodexCliSyncRollbackPlanMock, getLastCodexCliSyncRunMock) to produce
errno-style errors (i.e., Error objects with a code property set to "EBUSY" or
use a NodeJS.ErrnoException shape) and include the Windows path in the error
message (e.g., "EBUSY rollback at C:\\sensitive\\rollback.json") so
runCodexMultiAuthCli's warning sanitizer is forced to strip the path but keep
the errno code when invoked by the test that imports runCodexMultiAuthCli and
calls ["auth","login"] with promptLoginModeMock resolving to cancel. Ensure
mocks use vitest mockResolvedValue/mockRejectedValue or mockImplementation
consistently so the assertions about warningOutput containing the [UNKNOWN]
alternative and not containing the raw path/EBUSY code remain valid.
🪄 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: cff20fb2-d6ee-408a-b7ab-1cd6c92cf26b

📥 Commits

Reviewing files that changed from the base of the PR and between 9d2bf18 and 560d0a1.

📒 Files selected for processing (2)
  • lib/codex-manager.ts
  • test/codex-manager-cli.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/codex-manager-cli.test.ts
lib/**

⚙️ CodeRabbit configuration file

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

Files:

  • lib/codex-manager.ts
🔇 Additional comments (8)
lib/codex-manager.ts (8)

4125-4236: buildLoginMenuHealthSummary addresses past review, but verify regression test.

the implementation now correctly:

  • uses Promise.allSettled at lib/codex-manager.ts:4160-4165 for parallel i/o
  • tracks restoreStateUnavailable and rollbackPlanLoadFailed flags
  • surfaces "unavailable" labels instead of "none" when filesystem reads fail (lib/codex-manager.ts:4204-4215)

this addresses the past review comment. however, the past review also requested "add a vitest regression for the unreadable-filesystem branch"—please confirm this test exists in test/codex-manager-cli.test.ts. the pr objectives mention "error fallback" tests, but verify they cover the ebusy/eacces path specifically. as per coding guidelines, verify every change cites affected tests (vitest) and that new queues handle ebusy/429 scenarios.

#!/bin/bash
# verify test coverage for unavailable/error fallback states
rg -n -C3 'unavailable|restoreStateUnavailable|rollbackPlanLoadFailed|EBUSY|EACCES' test/codex-manager-cli.test.ts

1-1: lgtm on the crypto import.

using node:crypto for sha256 fingerprinting is the right call here—avoids rolling your own hash.


3358-3367: types look clean.

DashboardHealthSummary and DoctorReadOnlySummary are well-scoped for the health-summary slice.


3411-3417: sha256 fingerprinting is appropriate here.

12 hex chars (48 bits) is plenty for local duplicate detection. critically, this avoids logging or exposing raw refresh tokens in the health summary. as per coding guidelines, focus on auth rotation, windows filesystem io, and concurrency—no token leakage detected.


4007-4094: read-only doctor checks look solid.

lib/codex-manager.ts:4046-4055 uses fingerprinted tokens for duplicate detection, avoiding raw token exposure. the severity escalation logic (ok → warn → error) is correct.

one note: the pr summary mentions greptile flagged "uncovered branches in new summarize helpers"—confirm test coverage for the error/warn paths in test/codex-manager-cli.test.ts.


4104-4123: rollback hint handles unavailable state correctly.

lib/codex-manager.ts:4108-4110 now returns "rollback state unavailable" when rollbackPlanLoadFailed is true, rather than collapsing to "none". this addresses the concern from the past review about windows ebusy/eacces scenarios.


3375-3384: integration wiring looks correct.

lib/codex-manager.ts:3375 awaits buildLoginMenuHealthSummary and passes it to promptLoginMode at line 3381. the ?? undefined coercion ensures type compatibility with the optional parameter.


3952-3958: simple helper, no issues.

formatCountNoun is a straightforward pluralization utility. works as expected.

Comment on lines +3960 to +4005
function summarizeLatestCodexCliSyncState(): {
label: string;
hint: string;
} {
const latestRun = getLastCodexCliSyncRun();
if (!latestRun) {
return {
label: "none",
hint: "No Codex CLI sync result recorded yet.",
};
}

const outcomeLabel =
latestRun.outcome === "changed"
? "changed"
: latestRun.outcome === "noop"
? "noop"
: latestRun.outcome === "disabled"
? "disabled"
: latestRun.outcome === "unavailable"
? "unavailable"
: "error";
const when = formatRelativeDateShort(latestRun.runAt);
const counts = [
latestRun.summary.addedAccountCount > 0
? `add ${latestRun.summary.addedAccountCount}`
: null,
latestRun.summary.updatedAccountCount > 0
? `update ${latestRun.summary.updatedAccountCount}`
: null,
latestRun.summary.destinationOnlyPreservedCount > 0
? `preserve ${latestRun.summary.destinationOnlyPreservedCount}`
: null,
latestRun.summary.selectionChanged ? "selection changed" : null,
].filter((value): value is string => value !== null);
const hintParts = [
`Latest sync ${outcomeLabel}${when ? ` ${when}` : ""}`,
counts.length > 0 ? counts.join(" | ") : "no account deltas recorded",
latestRun.message?.trim() || undefined,
].filter((value): value is string => Boolean(value));

return {
label: `${outcomeLabel}${when ? ` ${when}` : ""}`,
hint: hintParts.join(" | "),
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# check if getLastCodexCliSyncRun does synchronous file I/O
ast-grep --pattern 'function getLastCodexCliSyncRun($$$) {
  $$$
}'

# also check for readFileSync or other sync patterns
rg -n 'getLastCodexCliSyncRun|readFileSync' --type=ts lib/codex-cli/sync.ts

Repository: ndycode/codex-multi-auth

Length of output: 234


🏁 Script executed:

#!/bin/bash
# Get implementation of getLastCodexCliSyncRun
sed -n '320,360p' lib/codex-cli/sync.ts

# Check what data structure stores the sync run and how it's populated
rg -n 'latestCodexCliSyncRun|lastCodexCliSyncRun' lib/codex-cli/sync.ts -A 2 -B 2

# Look for readFileSync or other blocking file operations in the file
rg -n 'readFileSync|readSync|fs\.' lib/codex-cli/sync.ts

Repository: ndycode/codex-multi-auth

Length of output: 2919


🏁 Script executed:

#!/bin/bash
# Find readLatestSyncHistorySync implementation
rg -n 'readLatestSyncHistorySync' lib/codex-cli/sync.ts -A 15

# Also check where it's called from
rg -n 'readLatestSyncHistorySync' lib/

Repository: ndycode/codex-multi-auth

Length of output: 1497


🏁 Script executed:

#!/bin/bash
# Get implementation of readLatestSyncHistorySync
sed -n '259,290p' lib/sync-history.ts

# Also check for any readFileSync calls in sync-history.ts
rg -n 'readFileSync|readSync|fs\.read' lib/sync-history.ts

Repository: ndycode/codex-multi-auth

Length of output: 1473


🏁 Script executed:

#!/bin/bash
# Check for tests covering getLastCodexCliSyncRun
rg -n 'getLastCodexCliSyncRun' test/ 

# Check for tests of readLatestSyncHistorySync
rg -n 'readLatestSyncHistorySync' test/

# Check error handling for windows file scenarios
sed -n '259,280p' lib/sync-history.ts

# Look at the coding guidelines context - check for EBUSY handling in sync code
rg -n 'EBUSY|EAGAIN' lib/codex-cli/sync.ts lib/sync-history.ts

Repository: ndycode/codex-multi-auth

Length of output: 3215


🏁 Script executed:

#!/bin/bash
# Check if this function is new in the change or existing
git log --oneline -1 lib/codex-manager.ts

# Check the context around line 3960-4005 in git to see if this is new code
git show --stat lib/codex-manager.ts | head -20

# Look for any tests added for summarizeLatestCodexCliSyncState specifically
rg -n 'summarizeLatestCodexCliSyncState' test/

Repository: ndycode/codex-multi-auth

Length of output: 457


🏁 Script executed:

#!/bin/bash
# Check if summarizeLatestCodexCliSyncState has test coverage
rg -n 'summarizeLatestCodexCliSyncState' test/

# Check if the function is directly tested or only used in integration tests
rg -B5 -A10 'summarizeLatestCodexCliSyncState' lib/codex-manager.ts | head -40

# Look for windows-specific file locking tests in sync-history
rg -n 'windows|EBUSY' test/sync-history.test.ts

Repository: ndycode/codex-multi-auth

Length of output: 808


add test coverage for summarizeLatestCodexCliSyncState and retry on file lock errors.

blocking file i/o confirmed: summarizeLatestCodexCliSyncState() calls getLastCodexCliSyncRun() which calls readLatestSyncHistorySync() at lib/sync-history.ts:261 using readFileSync(). in-memory cache at lib/codex-cli/sync.ts:153 prevents repeated blocks after first call, and fire-and-forget async load at lib/codex-cli/sync.ts:330 refills it. however, no regression tests exist for summarizeLatestCodexCliSyncState() itself, and readLatestSyncHistorySync() doesn't retry on EBUSY despite windows file lock risks—compare to RETRYABLE_REMOVE_CODES at lib/sync-history.ts:13. add vitest coverage for this function and implement EBUSY retry logic in readLatestSyncHistorySync().

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

In `@lib/codex-manager.ts` around lines 3960 - 4005, The test suite lacks coverage
for summarizeLatestCodexCliSyncState and the file-read path doesn't retry on
Windows EBUSY errors; add a Vitest unit test exercising
summarizeLatestCodexCliSyncState (covering null latest run, various outcome
labels, counts, and message trimming) and modify readLatestSyncHistorySync to
retry on EBUSY similar to the existing retryable logic (use the same
retry/backoff strategy and reuse the RETRYABLE_REMOVE_CODES pattern), ensuring
getLastCodexCliSyncRun/summarizeLatestCodexCliSyncState callpaths are exercised
and the in-memory cache behavior remains unchanged.

@ndycode ndycode closed this Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant