From 95f8fdf11d79ba2ad7e3612e245fb7e026d4ece0 Mon Sep 17 00:00:00 2001 From: ndycode Date: Thu, 23 Jul 2026 18:25:29 +0800 Subject: [PATCH 1/4] feat(check): show quota reset timestamps in check output (#633) `codex-multi-auth check` printed the percentage left in each quota window but never said when those windows reset, so users had to look the reset time up elsewhere. Append the absolute reset clock to each window in the `check` health line: live session OK (5h 100%, resets 18:10 | 7d 93%, resets 13:50 on Jul 29) Reuses the existing reset formatter from `lib/quota-probe.ts` (exported as `formatQuotaResetAt` and given an injectable `now` for deterministic tests) rather than introducing a second reset format. Times are local, 24-hour, and gain a day suffix once the reset is past midnight. The reset suffix is opt-in via `CompactQuotaFormatOptions.showReset`, so the width-constrained surfaces that share these formatters -- dashboard rows, the account menu, and `forecast` -- keep their existing output. `formatQuotaSnapshotForDashboard` is the only `check` caller and is the sole place that opts in. Also widen the `styleQuotaSummary` segment pattern to accept the reset suffix. Without this the appended text fails the anchored percent match and every quota segment silently falls back to the muted tone, losing the red/yellow/green quota colouring. A missing or malformed reset timestamp drops only the reset clause; the percentage and the account check itself are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Xetj6c94ZqD7a5n7G9M5Ro --- docs/features.md | 2 +- docs/reference/commands.md | 33 +++++ .../formatters/quota-formatters.ts | 54 ++++++- lib/codex-manager/health-check.ts | 12 +- lib/quota-probe.ts | 11 +- test/codex-manager-formatters.test.ts | 139 ++++++++++++++++++ 6 files changed, 241 insertions(+), 10 deletions(-) diff --git a/docs/features.md b/docs/features.md index ab321e0d9..489c47240 100644 --- a/docs/features.md +++ b/docs/features.md @@ -14,7 +14,7 @@ User-facing capability map for Codex CLI multi-account OAuth, account switching, | Explicit active-account switching | Persist a manual pin by index instead of relying on hidden state | `codex-multi-auth switch ` | | Clear manual pin | Drop the persisted pin so hybrid rotation resumes | `codex-multi-auth unpin` | | Workspace selection | List or set personal vs business/team workspaces under one account | `codex-multi-auth workspace [workspace]` | -| Fast and deep health checks | See whether the current pool is usable before a coding session | `codex-multi-auth check` | +| Fast and deep health checks | See whether the current pool is usable before a coding session, including when each quota window resets | `codex-multi-auth check` | | Flagged-account verification and restore | Recover accounts sidelined during prior failures | `codex-multi-auth verify-flagged` | --- diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 512f6e410..44aa0cc1e 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -44,6 +44,39 @@ Compatibility forms are supported for migrations and wrapper-routed environments --- +## `codex-multi-auth check` + +Live-probes each enabled account and prints one line per account. When +`showQuotaDetails` is on (the default, see [settings.md](settings.md)) the +healthy lines carry a compact quota summary with the percentage **left** in +each window and the absolute time that window resets: + +```console +$ codex-multi-auth check +Checking 2 account(s) with quick check + live check... +Model probe: gpt-5.6-sol | prompt family gpt-5.2 | tool search yes | computer use yes + ✓ Account 1 (Personal, 1@example.com) | live session OK (5h 100%, resets 18:10 | 7d 93%, resets 13:50 on Jul 29) + ✓ Account 2 (Personal, 2@example.com) | live session OK (5h 100%, resets 18:35 | 7d 98%, resets 14:15 on Jul 29) +``` + +Reset-time details: + +- Times are rendered in the **local system timezone** on a 24-hour clock. +- A reset later today prints as `HH:MM`; anything past midnight adds the day + (`HH:MM on Mon DD`). The year is omitted because both windows are at most + seven days long. +- Window labels come from the backend (`5h`, `7d`, …), and fall back to + `quota` when the backend omits the window length. +- When the backend does not return a reset timestamp for a window, that + window keeps its percentage and simply omits the reset clause. A missing or + malformed timestamp never fails the account check. +- Reset times are shown only by `check`. The dashboard, account menu, and + `forecast` keep their narrower percentage-only summaries. + +Turning `showQuotaDetails` off reduces the line to a bare `live session OK`. + +--- + ## Daily Use | Command | Description | diff --git a/lib/codex-manager/formatters/quota-formatters.ts b/lib/codex-manager/formatters/quota-formatters.ts index 5899dff29..1700815a8 100644 --- a/lib/codex-manager/formatters/quota-formatters.ts +++ b/lib/codex-manager/formatters/quota-formatters.ts @@ -3,6 +3,7 @@ import type { QuotaCacheEntry } from "../../quota-cache.js"; import { type CodexQuotaSnapshot, fetchCodexQuotaSnapshot, + formatQuotaResetAt, formatQuotaSnapshotLine, } from "../../quota-probe.js"; import { @@ -29,7 +30,9 @@ export function styleQuotaSummary(summary: string): string { if (/rate-limited/i.test(segment)) { return stylePromptText(segment, "danger"); } - const match = segment.match(/^([0-9a-zA-Z]+)\s+(\d{1,3})%$/); + const match = segment.match( + /^([0-9a-zA-Z]+)\s+(\d{1,3})%(,\s*resets\s+\S.*)?$/, + ); if (!match) { return stylePromptText(segment, "muted"); } @@ -42,18 +45,29 @@ export function styleQuotaSummary(summary: string): string { return stylePromptText(segment, "muted"); } const tone = quotaToneFromLeftPercent(leftPercent); - return `${stylePromptText(windowLabel, "muted")} ${stylePromptText(`${leftPercent}%`, tone)}`; + const resetSuffix = match[3] + ? stylePromptText(match[3], "muted") + : ""; + return `${stylePromptText(windowLabel, "muted")} ${stylePromptText(`${leftPercent}%`, tone)}${resetSuffix}`; }); return joinStyledSegments(rendered); } +/** + * Render the per-account health line printed by `codex-multi-auth check`. + * + * `check` is the only caller, and unlike the dashboard rows and account menu it + * prints one account per line with no width pressure, so it opts into the + * absolute reset clock for each quota window. + */ export function formatQuotaSnapshotForDashboard( snapshot: Awaited>, settings: DashboardDisplaySettings, + now = Date.now(), ): string { if (!settings.showQuotaDetails) return "live session OK"; - return `live session OK (${formatCompactQuotaSnapshot(snapshot)})`; + return `live session OK (${formatCompactQuotaSnapshot(snapshot, now, { showReset: true })})`; } export function quotaCacheEntryToSnapshot( @@ -87,30 +101,55 @@ function formatCompactQuotaWindowLabel( return `${windowMinutes}m`; } +/** + * Options shared by the compact quota formatters. + * + * `showReset` is opt-in so that space-constrained surfaces (dashboard rows, the + * account menu, forecast lines) keep their existing single-line width, while + * `codex-multi-auth check` can append the absolute reset time. + */ +export interface CompactQuotaFormatOptions { + showReset?: boolean; +} + function formatCompactQuotaPart( windowMinutes: number | undefined, usedPercent: number | undefined, + resetAtMs: number | undefined, + options: CompactQuotaFormatOptions, + now: number, ): string | null { const label = formatCompactQuotaWindowLabel(windowMinutes); if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent)) { return null; } const left = quotaLeftPercentFromUsed(usedPercent); - return `${label} ${left}%`; + const part = `${label} ${left}%`; + if (!options.showReset) return part; + // A missing or malformed reset timestamp must never drop the percentage. + const reset = formatQuotaResetAt(resetAtMs, now); + return reset ? `${part}, resets ${reset}` : part; } export function formatCompactQuotaSnapshot( snapshot: CodexQuotaSnapshot, now = Date.now(), + options: CompactQuotaFormatOptions = {}, ): string { const parts = [ formatCompactQuotaPart( snapshot.primary.windowMinutes, snapshot.primary.usedPercent, + snapshot.primary.resetAtMs, + options, + now, ), formatCompactQuotaPart( snapshot.secondary.windowMinutes, snapshot.secondary.usedPercent, + snapshot.secondary.resetAtMs, + options, + now, ), ].filter( (value): value is string => typeof value === "string" && value.length > 0, @@ -130,15 +169,22 @@ export function formatCompactQuotaSnapshot( export function formatAccountQuotaSummary( entry: QuotaCacheEntry, now = Date.now(), + options: CompactQuotaFormatOptions = {}, ): string { const parts = [ formatCompactQuotaPart( entry.primary.windowMinutes, entry.primary.usedPercent, + entry.primary.resetAtMs, + options, + now, ), formatCompactQuotaPart( entry.secondary.windowMinutes, entry.secondary.usedPercent, + entry.secondary.resetAtMs, + options, + now, ), ].filter( (value): value is string => typeof value === "string" && value.length > 0, diff --git a/lib/codex-manager/health-check.ts b/lib/codex-manager/health-check.ts index c16488db3..999bf271d 100644 --- a/lib/codex-manager/health-check.ts +++ b/lib/codex-manager/health-check.ts @@ -144,7 +144,11 @@ export async function runHealthCheck( quotaEmailFallbackState ?? undefined, ) || quotaCacheChanged; } - healthDetail = formatQuotaSnapshotForDashboard(snapshot, display); + healthDetail = formatQuotaSnapshotForDashboard( + snapshot, + display, + now, + ); codexAvailable += 1; } catch (error) { warnings += 1; @@ -252,7 +256,11 @@ export async function runHealthCheck( quotaEmailFallbackState ?? undefined, ) || quotaCacheChanged; } - healthyMessage = formatQuotaSnapshotForDashboard(snapshot, display); + healthyMessage = formatQuotaSnapshotForDashboard( + snapshot, + display, + now, + ); codexAvailable += 1; } catch (error) { warnings += 1; diff --git a/lib/quota-probe.ts b/lib/quota-probe.ts index a8e942a6b..43fd3b6e9 100644 --- a/lib/quota-probe.ts +++ b/lib/quota-probe.ts @@ -266,15 +266,20 @@ function formatQuotaWindowLabel(windowMinutes: number | undefined): string { * and produces no sensitive token data. * * @param resetAtMs - Timestamp in milliseconds since epoch; if `undefined`, non-finite, or <= 0, the function returns `undefined`. + * @param nowMs - Reference timestamp used for the "is today" comparison; defaults to the current time. * @returns The formatted reset time string, or `undefined` if `resetAtMs` is invalid or not provided. */ -function formatResetAt(resetAtMs: number | undefined): string | undefined { +export function formatQuotaResetAt( + resetAtMs: number | undefined, + nowMs = Date.now(), +): string | undefined { if (!resetAtMs || !Number.isFinite(resetAtMs) || resetAtMs <= 0) return undefined; const date = new Date(resetAtMs); if (!Number.isFinite(date.getTime())) return undefined; - const now = new Date(); + const now = new Date(nowMs); const sameDay = + Number.isFinite(now.getTime()) && now.getFullYear() === date.getFullYear() && now.getMonth() === date.getMonth() && now.getDate() === date.getDate(); @@ -305,7 +310,7 @@ function formatWindowSummary(label: string, window: CodexQuotaWindow): string { typeof used === "number" && Number.isFinite(used) ? Math.max(0, Math.min(100, Math.round(100 - used))) : undefined; - const reset = formatResetAt(window.resetAtMs); + const reset = formatQuotaResetAt(window.resetAtMs); let summary = label; if (left !== undefined) summary = `${summary} ${left}% left`; if (reset) summary = `${summary} (resets ${reset})`; diff --git a/test/codex-manager-formatters.test.ts b/test/codex-manager-formatters.test.ts index d6e28d800..55fd2eb2e 100644 --- a/test/codex-manager-formatters.test.ts +++ b/test/codex-manager-formatters.test.ts @@ -10,10 +10,16 @@ import { stringifyLogArgs, } from "../lib/codex-manager/formatters/text-style.js"; import { + formatAccountQuotaSummary, + formatCompactQuotaSnapshot, + formatQuotaSnapshotForDashboard, quotaCacheEntryToSnapshot, styleQuotaSummary, } from "../lib/codex-manager/formatters/quota-formatters.js"; +import type { DashboardDisplaySettings } from "../lib/dashboard-settings.js"; import type { QuotaCacheEntry } from "../lib/quota-cache.js"; +import { formatQuotaResetAt } from "../lib/quota-probe.js"; +import type { CodexQuotaSnapshot } from "../lib/quota-probe.js"; import { formatModelInspection, inspectRequestedModel, @@ -151,6 +157,139 @@ describe("quota formatters", () => { }); }); +// Reset-timestamp rendering for `codex-multi-auth check` (issue #633). +// The reset clock is local-timezone by design, so expectations are derived +// from the same Intl calls the formatter uses instead of hardcoding a zone. +describe("compact quota reset timestamps", () => { + // 2026-07-22T09:00 local; +2h stays on the same local day, +8d does not. + const NOW = new Date(2026, 6, 22, 9, 0, 0).getTime(); + const SAME_DAY = NOW + 2 * 60 * 60 * 1000; + const NEXT_WEEK = NOW + 8 * 24 * 60 * 60 * 1000; + + const localTime = (ms: number): string => + new Date(ms).toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + + const snapshot = ( + primaryReset: number | undefined, + secondaryReset: number | undefined, + ): CodexQuotaSnapshot => + ({ + status: "ok", + planType: "plus", + model: "gpt-5.3-codex", + primary: { + usedPercent: 0, + windowMinutes: 300, + resetAtMs: primaryReset, + }, + secondary: { + usedPercent: 7, + windowMinutes: 10080, + resetAtMs: secondaryReset, + }, + }) as unknown as CodexQuotaSnapshot; + + it("formatQuotaResetAt uses local 24h time, adds the day when not today", () => { + expect(formatQuotaResetAt(SAME_DAY, NOW)).toBe(localTime(SAME_DAY)); + expect(formatQuotaResetAt(NEXT_WEEK, NOW)).toBe( + `${localTime(NEXT_WEEK)} on ${new Date(NEXT_WEEK).toLocaleDateString( + undefined, + { month: "short", day: "2-digit" }, + )}`, + ); + }); + + it("formatQuotaResetAt rejects missing and malformed timestamps", () => { + expect(formatQuotaResetAt(undefined, NOW)).toBeUndefined(); + expect(formatQuotaResetAt(0, NOW)).toBeUndefined(); + expect(formatQuotaResetAt(-1, NOW)).toBeUndefined(); + expect(formatQuotaResetAt(Number.NaN, NOW)).toBeUndefined(); + expect(formatQuotaResetAt(Number.POSITIVE_INFINITY, NOW)).toBeUndefined(); + }); + + it("omits reset times unless the caller opts in", () => { + expect(formatCompactQuotaSnapshot(snapshot(SAME_DAY, NEXT_WEEK), NOW)).toBe( + "5h 100% | 7d 93%", + ); + }); + + it("appends a per-window reset time when showReset is set", () => { + expect( + formatCompactQuotaSnapshot(snapshot(SAME_DAY, NEXT_WEEK), NOW, { + showReset: true, + }), + ).toBe( + `5h 100%, resets ${formatQuotaResetAt(SAME_DAY, NOW)} | 7d 93%, resets ${formatQuotaResetAt(NEXT_WEEK, NOW)}`, + ); + }); + + it("keeps the percentage when a reset timestamp is missing or malformed", () => { + expect( + formatCompactQuotaSnapshot(snapshot(undefined, Number.NaN), NOW, { + showReset: true, + }), + ).toBe("5h 100% | 7d 93%"); + }); + + it("formatAccountQuotaSummary honors showReset the same way", () => { + const entry = { + status: "ok", + planType: "plus", + model: "gpt-5.3-codex", + fetchedAt: NOW, + primary: { usedPercent: 0, windowMinutes: 300, resetAtMs: SAME_DAY }, + secondary: { usedPercent: 7, windowMinutes: 10080, resetAtMs: undefined }, + } as unknown as QuotaCacheEntry; + expect(formatAccountQuotaSummary(entry, NOW)).toBe("5h 100% | 7d 93%"); + expect(formatAccountQuotaSummary(entry, NOW, { showReset: true })).toBe( + `5h 100%, resets ${formatQuotaResetAt(SAME_DAY, NOW)} | 7d 93%`, + ); + }); + + it("formatQuotaSnapshotForDashboard is the check line and shows resets", () => { + const display = { + showQuotaDetails: true, + } as unknown as DashboardDisplaySettings; + expect( + formatQuotaSnapshotForDashboard( + snapshot(SAME_DAY, NEXT_WEEK), + display, + NOW, + ), + ).toBe( + `live session OK (5h 100%, resets ${formatQuotaResetAt(SAME_DAY, NOW)} | 7d 93%, resets ${formatQuotaResetAt(NEXT_WEEK, NOW)})`, + ); + }); + + it("formatQuotaSnapshotForDashboard stays bare when quota details are off", () => { + const display = { + showQuotaDetails: false, + } as unknown as DashboardDisplaySettings; + expect( + formatQuotaSnapshotForDashboard( + snapshot(SAME_DAY, NEXT_WEEK), + display, + NOW, + ), + ).toBe("live session OK"); + }); + + it("styleQuotaSummary still tones segments that carry a reset suffix", () => { + expect(styleQuotaSummary("5h 80%, resets 14:05 | 7d 15%")).toBe( + "5h 80%, resets 14:05 | 7d 15%", + ); + expect(styleQuotaSummary("7d 150%, resets 14:05 on Jul 29")).toBe( + "7d 100%, resets 14:05 on Jul 29", + ); + // A trailing "resets" with no value is not a reset suffix. + expect(styleQuotaSummary("5h 80%, resets")).toBe("5h 80%, resets"); + }); +}); + describe("model formatters", () => { it("inspectRequestedModel reports remapping consistently", () => { const inspection = inspectRequestedModel("gpt-5.3-codex"); From 6ff2d108c81313da7ef80e71b4987782090febec Mon Sep 17 00:00:00 2001 From: ndycode Date: Thu, 23 Jul 2026 19:21:09 +0800 Subject: [PATCH 2/4] test(check): pin the reset timestamps on the real check output (#633) The existing command-level coverage asserted only that the health line contained "live session OK", so it passed whether or not the reset times rendered at all. The formatter was covered; the wiring was not. Add two tests that drive `runCodexMultiAuthCli(["auth", "check"])` through the real health-check and formatter, mocking only the network probe, and assert the printed line: one for both windows carrying a reset clock, one for a window whose reset timestamp is absent or zero. Verified these fail when `showReset` is flipped off, so they pin the behavior rather than passing vacuously. The reset clock is local-timezone, so the day suffix is matched loosely instead of pinned to one zone. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Xetj6c94ZqD7a5n7G9M5Ro --- test/codex-manager-cli.test.ts | 90 ++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/test/codex-manager-cli.test.ts b/test/codex-manager-cli.test.ts index 45c793192..d8df8a2bd 100644 --- a/test/codex-manager-cli.test.ts +++ b/test/codex-manager-cli.test.ts @@ -3279,6 +3279,96 @@ describe("codex manager cli commands", () => { ).toBe(true); }); + // Issue #633: `check` prints when each quota window resets. Asserted on the + // real printed line so the health-check -> formatter wiring is covered, not + // just the formatter in isolation. The reset clock is local-timezone, and a + // run started just before midnight pushes it to the next day, so the day + // suffix is optional here rather than pinned. + it("check prints the reset time for each quota window", async () => { + const now = Date.now(); + storageMocks.loadAccounts.mockResolvedValueOnce({ + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: [ + { + accountId: "acc_reset", + email: "reset@example.com", + refreshToken: "refresh-reset", + accessToken: "access-reset", + expiresAt: now + 60 * 60 * 1000, + addedAt: now - 1_000, + lastUsed: now - 1_000, + enabled: true, + }, + ], + }); + quotaProbeMocks.fetchCodexQuotaSnapshot.mockResolvedValueOnce({ + status: 200, + model: DEFAULT_MODEL, + primary: { + usedPercent: 70, + windowMinutes: 300, + resetAtMs: now + 3 * 60 * 60 * 1000, + }, + secondary: { + usedPercent: 10, + windowMinutes: 10080, + resetAtMs: now + 5 * 24 * 60 * 60 * 1000, + }, + }); + const logSpy = silenceConsole("log"); + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + + const exitCode = await runCodexMultiAuthCli(["auth", "check"]); + expect(exitCode).toBe(0); + + const line = logSpy.mock.calls + .map((call) => String(call[0])) + .find((text) => text.includes("live session OK")); + expect(line).toBeDefined(); + expect(line).toMatch( + /live session OK \(5h 30%, resets \d{2}:\d{2}( on \w{3} \d{2})? \| 7d 90%, resets \d{2}:\d{2} on \w{3} \d{2}\)/, + ); + }); + + it("check keeps the percentage when a window has no reset timestamp", async () => { + const now = Date.now(); + storageMocks.loadAccounts.mockResolvedValueOnce({ + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: [ + { + accountId: "acc_no_reset", + email: "no-reset@example.com", + refreshToken: "refresh-no-reset", + accessToken: "access-no-reset", + expiresAt: now + 60 * 60 * 1000, + addedAt: now - 1_000, + lastUsed: now - 1_000, + enabled: true, + }, + ], + }); + quotaProbeMocks.fetchCodexQuotaSnapshot.mockResolvedValueOnce({ + status: 200, + model: DEFAULT_MODEL, + primary: { usedPercent: 70, windowMinutes: 300 }, + secondary: { usedPercent: 10, windowMinutes: 10080, resetAtMs: 0 }, + }); + const logSpy = silenceConsole("log"); + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + + const exitCode = await runCodexMultiAuthCli(["auth", "check"]); + expect(exitCode).toBe(0); + + const line = logSpy.mock.calls + .map((call) => String(call[0])) + .find((text) => text.includes("live session OK")); + expect(line).toContain("live session OK (5h 30% | 7d 90%)"); + }); + it("does not label Codex-unavailable live checks as working now", async () => { const now = Date.now(); storageMocks.loadAccounts.mockResolvedValueOnce({ From 08fd20872aa409fa1e99a1185dd483faa6c3a0a0 Mon Sep 17 00:00:00 2001 From: ndycode Date: Thu, 23 Jul 2026 19:37:34 +0800 Subject: [PATCH 3/4] docs(check): correct how check treats disabled accounts (#633) The new `check` section claimed it live-probes "each enabled account". It does neither part of that: health-check.ts iterates every stored account, and re-enables a disabled one whose token is still usable (`account.enabled = true`). `fix` is the command that skips disabled accounts, which is where the wrong wording came from. Reported by CodeRabbit on PR #634. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Xetj6c94ZqD7a5n7G9M5Ro --- docs/reference/commands.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 44aa0cc1e..a824e94ae 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -46,10 +46,12 @@ Compatibility forms are supported for migrations and wrapper-routed environments ## `codex-multi-auth check` -Live-probes each enabled account and prints one line per account. When -`showQuotaDetails` is on (the default, see [settings.md](settings.md)) the -healthy lines carry a compact quota summary with the percentage **left** in -each window and the absolute time that window resets: +Live-probes every stored account and prints one line per account. Unlike +`fix`, `check` does not skip disabled accounts: an account whose token is +still usable is re-enabled as part of the run. When `showQuotaDetails` is on +(the default, see [settings.md](settings.md)) the healthy lines carry a +compact quota summary with the percentage **left** in each window and the +absolute time that window resets: ```console $ codex-multi-auth check From 5f3b37e48cdb0b0ab558f46c812ee0b07f4be173 Mon Sep 17 00:00:00 2001 From: ndycode Date: Thu, 23 Jul 2026 20:54:49 +0800 Subject: [PATCH 4/4] fix(check): drop the stale reference time, document locale-dependent dates Two review findings from PR #634, both valid. Greptile: health-check captures `now` once, before the sequential network probes, so threading it into the formatter made the "is today" decision go stale on a run that crosses local midnight. It also silently moved `isQuotaCacheEntryExhausted(snapshot, now)` from call time to run-start time, which was never intended. Reverting health-check.ts to main fixes both -- the formatter's `now` already defaults to `Date.now()` per call, and it stays injectable for tests. CodeRabbit: `formatQuotaResetAt` builds the date half with `toLocaleDateString(undefined, ...)`, so month text and field order follow the host locale. Documenting the output as `HH:MM on Mon DD` claimed a stability the formatter does not provide. The docs now say the date shape is locale-dependent and label the example as en-US. The same assumption was baked into the new command-level regex (`\w{3} \d{2}`), which matches only en-US -- it fails on de-DE (`29. Juli`), ja-JP, fr-FR, and en-GB (`29 Jul`). That assertion is now structural. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Xetj6c94ZqD7a5n7G9M5Ro --- docs/reference/commands.md | 8 +++++--- lib/codex-manager/health-check.ts | 12 ++---------- test/codex-manager-cli.test.ts | 8 +++++--- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/docs/reference/commands.md b/docs/reference/commands.md index a824e94ae..dccd91c33 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -64,9 +64,11 @@ Model probe: gpt-5.6-sol | prompt family gpt-5.2 | tool search yes | computer us Reset-time details: - Times are rendered in the **local system timezone** on a 24-hour clock. -- A reset later today prints as `HH:MM`; anything past midnight adds the day - (`HH:MM on Mon DD`). The year is omitted because both windows are at most - seven days long. +- A reset later today prints as `HH:MM`; anything past midnight appends the + date (`HH:MM on Jul 29` under an `en-US` locale). The date text and its + field order follow the host locale, so a non-English locale renders the + month differently — the shape is not a stable output contract. The year is + omitted because both windows are at most seven days long. - Window labels come from the backend (`5h`, `7d`, …), and fall back to `quota` when the backend omits the window length. - When the backend does not return a reset timestamp for a window, that diff --git a/lib/codex-manager/health-check.ts b/lib/codex-manager/health-check.ts index 999bf271d..c16488db3 100644 --- a/lib/codex-manager/health-check.ts +++ b/lib/codex-manager/health-check.ts @@ -144,11 +144,7 @@ export async function runHealthCheck( quotaEmailFallbackState ?? undefined, ) || quotaCacheChanged; } - healthDetail = formatQuotaSnapshotForDashboard( - snapshot, - display, - now, - ); + healthDetail = formatQuotaSnapshotForDashboard(snapshot, display); codexAvailable += 1; } catch (error) { warnings += 1; @@ -256,11 +252,7 @@ export async function runHealthCheck( quotaEmailFallbackState ?? undefined, ) || quotaCacheChanged; } - healthyMessage = formatQuotaSnapshotForDashboard( - snapshot, - display, - now, - ); + healthyMessage = formatQuotaSnapshotForDashboard(snapshot, display); codexAvailable += 1; } catch (error) { warnings += 1; diff --git a/test/codex-manager-cli.test.ts b/test/codex-manager-cli.test.ts index d8df8a2bd..c0e6136ca 100644 --- a/test/codex-manager-cli.test.ts +++ b/test/codex-manager-cli.test.ts @@ -3327,9 +3327,11 @@ describe("codex manager cli commands", () => { .map((call) => String(call[0])) .find((text) => text.includes("live session OK")); expect(line).toBeDefined(); - expect(line).toMatch( - /live session OK \(5h 30%, resets \d{2}:\d{2}( on \w{3} \d{2})? \| 7d 90%, resets \d{2}:\d{2} on \w{3} \d{2}\)/, - ); + // The date half of the suffix is produced by toLocaleDateString with an + // undefined locale, so its month text and field order follow the host. + // Assert the structure, not an en-US shape, or this fails off en-US. + expect(line).toMatch(/5h 30%, resets \d{2}:\d{2}/); + expect(line).toMatch(/7d 90%, resets \d{2}:\d{2} on \S/); }); it("check keeps the percentage when a window has no reset timestamp", async () => {