Repository navigation
Show Free plan usage in zro status - #33
Conversation
When /api/cli/status sends billing.free, status shows the plan as Free with the share of today's usage left and when it resets. Servers without the field keep the existing output.
zro-inference
left a comment
There was a problem hiding this comment.
Nice, tight PR: parseAccountStatus learns an optional billing.free field and printAccountStatus gains a Free branch plus a shared, well-named bar() helper and formatTimeUntil(), so Free users finally see their daily share and reset time instead of a bare none. The refactor of the paid path to reuse bar(ratio) is behavior-preserving and the Pro fixture test still passes. One blocker: the new Free bar is fed the raw server-supplied remainingPercent with no clamp, so an out-of-range value from any control plane crashes zro status mid-render with a RangeError and exit 1; several smaller robustness/coverage gaps (explicit free: null, the untested remainingPercent === null production path, and a $0.00 top-up line for Free) are noted inline.
JenClaude command reference
Reply with @MoonMathJen on this PR to trigger commands:
| Command | Example | What it does |
|---|---|---|
| Re-review | @MoonMathJen rereview |
Re-review after changes (full or scoped — see below) |
| Stop | @MoonMathJen stop |
Cancel in-progress review |
| Stop + summarize | @MoonMathJen stop and summarize |
Post partial findings so far |
| Focus specialty | @MoonMathJen only security |
Review only the named specialty |
| Skip specialty | @MoonMathJen skip test-coverage |
Review everything except the named specialty |
| Change model | @MoonMathJen pr-review-model: kimi-k2.7-coder |
Run this review with a specific model |
| Add context | @MoonMathJen focus on auth edge cases |
Free-text guidance passed to reviewers |
Specialties: correctness, security, test-coverage, adversarial
Any text in the comment beyond a recognized command is passed to the reviewers as context. Use it to point them at areas that matter, explain what changed since the last review, or call out constraints (e.g. "skip generated files", "the auth flow is the riskiest part here"). Hard review rules still win over reviewer guidance.
JenClaude metrics: cost $0.000, time 14m 28s, tokens 3,162,284.
| io.stdout.write( | ||
| free.remainingPercent === null | ||
| ? ` ${resets}\n` | ||
| : ` ${bar(free.remainingPercent / 100)} ${colors.strong(`${free.remainingPercent}%`)} ` + |
There was a problem hiding this comment.
[P1] Unclamped remainingPercent crashes zro status
free.remainingPercent arrives from /api/cli/status and is only validated as a finite number (line 825, via hasNumbers) — there is no range check. bar() then computes Math.round((p / 100) * 24) and does "░".repeat(24 - filled), so anything below ~-2 or above ~102 yields a negative repeat count and throws RangeError: Invalid count value: -12. The throw escapes printAccountStatus after the ACCOUNT header and Plan Free line are already written, so the user gets truncated output, no activity block, a raw Invalid count value on stderr and exit 1. This is reachable with a self-hosted or older control plane (ZRO_AUTH_URL can point anywhere), and a negative remaining percent is the natural encoding for an over-quota Free user. Note the paid path is safe only because line 872 clamps before calling bar().
Suggested fix: clamp inside bar() — const filled = Math.round(Math.min(Math.max(ratio, 0), 1) * width) — and clamp the printed percentage the same way so label and bar agree; add tests with remainingPercent: 150 and -5.
| const activity = asRecord(root.activity30d); | ||
| const planValue = billing.plan; | ||
| const plan = planValue === null ? null : asRecord(planValue); | ||
| const free = billing.free === undefined ? null : asRecord(billing.free); |
There was a problem hiding this comment.
[P2] Explicit free: null degrades the entire ACCOUNT block
The branch mirrors the plan field one line above but drops the null case: billing.free === undefined ? null : asRecord(billing.free). Since asRecord(null) returns {}, a server that serializes the absent optional plan as JSON null produces a non-null free that then fails the guard at 824-827 (typeof free.resetsAt !== "string"), parseAccountStatus returns null, and zro status prints ◇ Details unavailable — losing plan, top-up credits and 30-day activity, with --json reporting accountStatus: "unavailable". A free object that omits remainingPercent fails identically, because undefined !== null. This is exactly the trust-boundary case the PR claims is safe (omission works, explicit null does not).
Suggested fix: const freeRaw = billing.free; const free = freeRaw === null || freeRaw === undefined ? null : asRecord(freeRaw);, treat remainingPercent === undefined like null in the guard, and add tests for free: null, a missing remainingPercent, and an unparseable resetsAt asserting the account still renders.
| ` ${bar(ratio)} ${colors.strong(`${Math.round(ratio * 100)}%`)} ${colors.muted("used")}\n`, | ||
| ); | ||
| } | ||
| io.stdout.write( |
There was a problem hiding this comment.
[P2] Free accounts still get a dollar top-up line
The PR states "No dollar amounts are shown for Free", but the Top-up credits line below is unconditional, so the new test's own Free fixture renders ◇ Top-up credits $0.00 left · $0.00 total — an always-zero money line directly under a percentage. A Free account cannot hold pack balance (buying a pack moves it off Free), so this is pure noise that contradicts the stated contract, and the new test only asserts not.toContain("none"), so it cannot catch the mismatch.
Suggested fix: skip the Top-up line (or its money values) when free is set and packs.total === 0, or print "No credits" in words; then assert the intended behavior in the test (expect(output).not.toContain("$")).
| io.stdout.write(` ${colors.good("◆")} Plan ${colors.strong("Free")}\n`); | ||
| const resets = colors.muted(`resets in ${formatTimeUntil(free.resetsAt)}`); | ||
| io.stdout.write( | ||
| free.remainingPercent === null |
There was a problem hiding this comment.
[P2] The remainingPercent === null branch is the production path but is untested
The server sets remainingPercent: null whenever the budget lookup returns nothing (budget fetch failure, account not yet reconciled onto Free), so this branch — rendering just resets in 5h 12m with no bar and no percentage — is the one real Free users will often hit. The new test only exercises remainingPercent: 64, so an inverted ternary or a stray ${free.remainingPercent}% would ship silently.
Suggested fix: add a second status test with free: { remainingPercent: null, resetsAt: ... } asserting the output contains resets in and does not contain % left today or undefined.
zro statusshows a Free plan account as Free, with the share of today's usage left and when it resets:It reads
billing.free(remainingPercent,resetsAt) from/api/cli/status, which moonmath-ai/zro-inference#370 adds. With a server that does not send it, the output stays as before ("Plan none"). No dollar amounts are shown for Free.Tests: typecheck, 124 passed (2 skipped), including a new status test for a Free account.