11import { describe , expect , it } from "vitest" ;
22import type { UsageOutput } from "../usage/schemas" ;
3- import { formatResetTime , isUsageExceeded } from "./usageDisplay" ;
3+ import {
4+ codeUsageMeter ,
5+ formatResetTime ,
6+ formatUsdAmount ,
7+ isCodeUsageFreeTier ,
8+ isUsageExceeded ,
9+ } from "./usageDisplay" ;
410
511function makeUsage (
612 overrides : Partial < {
@@ -53,6 +59,92 @@ describe("isUsageExceeded", () => {
5359 } ) ;
5460} ) ;
5561
62+ describe ( "isCodeUsageFreeTier" , ( ) => {
63+ it . each ( [
64+ [ false , true ] ,
65+ [ true , false ] ,
66+ // Absent means unknown, never free.
67+ [ undefined , false ] ,
68+ ] as const ) ( "code_usage_subscribed=%s -> %s" , ( subscribed , expected ) => {
69+ expect ( isCodeUsageFreeTier ( { code_usage_subscribed : subscribed } ) ) . toBe (
70+ expected ,
71+ ) ;
72+ } ) ;
73+
74+ it ( "treats missing usage as not confirmed free" , ( ) => {
75+ expect ( isCodeUsageFreeTier ( null ) ) . toBe ( false ) ;
76+ expect ( isCodeUsageFreeTier ( undefined ) ) . toBe ( false ) ;
77+ } ) ;
78+ } ) ;
79+
80+ describe ( "codeUsageMeter" , ( ) => {
81+ it ( "prefers billing's org dollars when both numbers are present" , ( ) => {
82+ const meter = codeUsageMeter ( {
83+ ...makeUsage ( ) ,
84+ code_usage_subscribed : true ,
85+ ai_credits : { exhausted : false , used_usd : 12.4 , limit_usd : 50 } ,
86+ billing_period_end : "2026-06-01T00:00:00.000Z" ,
87+ } ) ;
88+ expect ( meter ) . toEqual ( {
89+ kind : "dollars" ,
90+ usedUsd : 12.4 ,
91+ limitUsd : 50 ,
92+ percent : 25 ,
93+ exceeded : false ,
94+ resetAt : "2026-06-01T00:00:00.000Z" ,
95+ } ) ;
96+ } ) ;
97+
98+ it ( "marks the dollars meter exceeded from the org bucket and falls back to the sustained reset" , ( ) => {
99+ const meter = codeUsageMeter ( {
100+ ...makeUsage ( ) ,
101+ code_usage_subscribed : false ,
102+ ai_credits : { exhausted : true , used_usd : 20 , limit_usd : 20 } ,
103+ } ) ;
104+ expect ( meter ) . toMatchObject ( {
105+ kind : "dollars" ,
106+ percent : 100 ,
107+ exceeded : true ,
108+ resetAt : "2026-05-01T13:00:00.000Z" ,
109+ } ) ;
110+ } ) ;
111+
112+ it . each ( [
113+ [ "missing numbers" , { exhausted : false } ] ,
114+ [ "null numbers" , { exhausted : false , used_usd : null , limit_usd : null } ] ,
115+ [ "a zero limit" , { exhausted : false , used_usd : 0 , limit_usd : 0 } ] ,
116+ ] ) ( "falls back to the free-tier valve bucket with %s" , ( _name , aiCredits ) => {
117+ const usage : UsageOutput = {
118+ ...makeUsage ( ) ,
119+ code_usage_subscribed : false ,
120+ ai_credits : aiCredits ,
121+ } ;
122+ expect ( codeUsageMeter ( usage ) ) . toEqual ( {
123+ kind : "bucket" ,
124+ bucket : usage . sustained ,
125+ } ) ;
126+ } ) ;
127+
128+ it ( "hides the meter for a subscribed or unknown org without dollars" , ( ) => {
129+ expect (
130+ codeUsageMeter ( { ...makeUsage ( ) , code_usage_subscribed : true } ) ,
131+ ) . toEqual ( { kind : "hidden" } ) ;
132+ expect ( codeUsageMeter ( makeUsage ( ) ) ) . toEqual ( { kind : "hidden" } ) ;
133+ expect ( codeUsageMeter ( null ) ) . toEqual ( { kind : "hidden" } ) ;
134+ } ) ;
135+ } ) ;
136+
137+ describe ( "formatUsdAmount" , ( ) => {
138+ it . each ( [
139+ [ 50 , "$50" ] ,
140+ [ 12.4 , "$12.40" ] ,
141+ [ 0.5 , "$0.50" ] ,
142+ [ 0 , "$0" ] ,
143+ ] ) ( "formats %s as %s" , ( amount , expected ) => {
144+ expect ( formatUsdAmount ( amount ) ) . toBe ( expected ) ;
145+ } ) ;
146+ } ) ;
147+
56148describe ( "formatResetTime" , ( ) => {
57149 const NOW = Date . parse ( "2026-05-01T12:00:00.000Z" ) ;
58150 const isoAt = ( msFromNow : number ) => new Date ( NOW + msFromNow ) . toISOString ( ) ;
@@ -73,6 +165,11 @@ describe("formatResetTime", () => {
73165 resetAt : isoAt ( 4 * 3600 * 1000 ) ,
74166 expected : "Resets in 4h" ,
75167 } ,
168+ {
169+ name : "rolls the hour instead of showing 60 minutes" ,
170+ resetAt : isoAt ( ( 23 * 3600 + 59 * 60 + 40 ) * 1000 ) ,
171+ expected : "Resets in 24h" ,
172+ } ,
76173 {
77174 name : "returns localized date when over 24h away" ,
78175 resetAt : isoAt ( 30 * 86400 * 1000 ) ,
0 commit comments