@@ -109,11 +109,11 @@ const renderSelector = async (
109109}
110110
111111test . each ( [
112- [ '2026-09-17T23:00:00Z' , '2026-09-19T06:00:00Z' , 10 , 15 , 'Off-peak 10/hr' , 0 ] ,
113- [ '2026-09-17T21:00:00Z' , '2026-09-18T22:00:00Z' , 15 , 10 , 'normally 15/hr' , 0 ] ,
114- [ '2026-09-17T23:00:00Z' , '2026-09-19T06:00:00Z' , 10 , 15 , 'Off-peak 10/hr' , 10 ] ,
115- [ '2026-09-17T21:00:00Z' , '2026-09-18T22:00:00Z' , 15 , 10 , 'normally 15/hr' , 10 ] ,
116- ] as const ) ( 'the mounted CLI picker catches up after multi-day sleep from %s' , async ( issued , resumed , before , after , explanation , discount ) => {
112+ [ '2026-09-17T23:00:00Z' , '2026-09-19T06:00:00Z' , 10 , 15 , 0 ] ,
113+ [ '2026-09-17T21:00:00Z' , '2026-09-18T22:00:00Z' , 15 , 10 , 0 ] ,
114+ [ '2026-09-17T23:00:00Z' , '2026-09-19T06:00:00Z' , 10 , 15 , 10 ] ,
115+ [ '2026-09-17T21:00:00Z' , '2026-09-18T22:00:00Z' , 15 , 10 , 10 ] ,
116+ ] as const ) ( 'the mounted CLI picker catches up after multi-day sleep from %s' , async ( issued , resumed , before , after , discount ) => {
117117 const now = Date . parse ( issued )
118118 const clock = spyOn ( Date , 'now' ) . mockReturnValue ( now )
119119 const realTimeout = globalThis . setTimeout
@@ -135,17 +135,19 @@ test.each([
135135 } )
136136 useFreebuffModelStore . getState ( ) . setSelectedModel ( id )
137137 const setup = await renderSelector ( 40 , undefined , 100 , now )
138- expect ( setup . captureCharFrame ( ) ) . toContain ( `│ ${ before - discount } Freebucks/hr` )
139- expect ( setup . captureCharFrame ( ) ) . toContain ( before === 10 ? 'normally 15/hr' : 'Off-peak 10/hr' )
138+ expect ( setup . captureCharFrame ( ) ) . toMatch ( new RegExp ( `│ +${ before - discount } Freebucks/hr` ) )
139+ expect ( setup . captureCharFrame ( ) ) . not . toContain ( 'Off-peak' )
140+ expect ( setup . captureCharFrame ( ) ) . not . toContain ( 'normally 15/hr' )
140141 expect ( setup . captureCharFrame ( ) ) . not . toContain ( 'Server fallback price notice' )
141142 expect ( wake ) . toBeDefined ( )
142143 flushSync ( ( ) => {
143144 clock . mockReturnValue ( Date . parse ( resumed ) )
144145 wake ! ( )
145146 } )
146147 await setup . renderOnce ( )
147- expect ( setup . captureCharFrame ( ) ) . toContain ( `│ ${ after - discount } Freebucks/hr` )
148- expect ( setup . captureCharFrame ( ) ) . toContain ( explanation )
148+ expect ( setup . captureCharFrame ( ) ) . toMatch ( new RegExp ( `│ +${ after - discount } Freebucks/hr` ) )
149+ expect ( setup . captureCharFrame ( ) ) . not . toContain ( 'Off-peak' )
150+ expect ( setup . captureCharFrame ( ) ) . not . toContain ( 'normally 15/hr' )
149151 } finally {
150152 cleanupRenderer ?.( )
151153 cleanupRenderer = undefined
@@ -154,6 +156,42 @@ test.each([
154156 }
155157} )
156158
159+ test . each ( [
160+ [ 'priceNotices' , 40 ] ,
161+ [ 'priceNotices' , 100 ] ,
162+ [ 'peak' , 40 ] ,
163+ [ 'peak' , 100 ] ,
164+ ] as const ) ( 'legacy %s copy stays hidden at %s columns while Flash remains selectable' , async ( payload , width ) => {
165+ const id = FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID
166+ const price = payload === 'peak' ? 25 : 10
167+ useFreebuffSessionStore . getState ( ) . setSession ( {
168+ status : 'none' ,
169+ accessTier : 'full' ,
170+ freebucks : {
171+ ...freebucksFixture ( 25 , { [ id ] : price } ) ,
172+ priceNotices : { [ id ] : 'Off-peak pricing · 15 Freebucks/hour at peak' } ,
173+ ...( payload === 'peak' ? {
174+ peak : {
175+ modelIds : [ id ] ,
176+ surcharge : 10 ,
177+ endsAt : new Date ( FIXED_NOW_MS + 3_600_000 ) . toISOString ( ) ,
178+ } ,
179+ } : { } ) ,
180+ } ,
181+ } )
182+ useFreebuffModelStore . getState ( ) . setSelectedModel ( id )
183+ const requested : string [ ] = [ ]
184+ const setup = await renderSelector ( 40 , async ( model ) => { requested . push ( model ) } , width )
185+ const frame = setup . captureCharFrame ( )
186+ expect ( frame ) . toContain ( '› DeepSeek V4.1 Flash' )
187+ expect ( frame ) . toContain ( 'Smart &' )
188+ expect ( frame ) . toMatch ( new RegExp ( `│ +${ price } Freebucks/hr` ) )
189+ expect ( frame ) . not . toMatch ( / O f f - p e a k | P e a k p r i c i n g | c h a r g e s d o u b l e | F r e e b u c k s \/ h o u r a t p e a k / )
190+ flushSync ( ( ) => setup . mockInput . pressEnter ( ) )
191+ await setup . renderOnce ( )
192+ expect ( requested ) . toEqual ( [ id ] )
193+ } )
194+
157195/**
158196 * LIMITED tier, which since 2026-08-31 is the only tier where the reward is a
159197 * MODEL you can select. At full access the reward is an extra premium session
@@ -928,7 +966,7 @@ describe('GLM selection uses the applicable meter', () => {
928966 )
929967
930968 test . each ( [ true , false ] ) (
931- 'an available first-tab discount shows only the discounted price and is named limited-time , available=%s' ,
969+ 'first-tab discount availability changes the displayed price without extra copy , available=%s' ,
932970 async ( available ) => {
933971 useFreebuffSessionStore . getState ( ) . setSession ( {
934972 status : 'none' ,
@@ -950,14 +988,14 @@ describe('GLM selection uses the applicable meter', () => {
950988 expect ( frame ) . toContain ( '5 Freebucks/hr' )
951989 expect ( frame ) . not . toContain ( '15 5 Freebucks/hr' )
952990 expect ( frame ) . not . toContain ( '15 Freebucks/hr' )
953- expect ( frame ) . toContain ( 'Limited-time first-tab discount' )
954991 } else {
955992 // In use elsewhere: the full price is the price, nothing struck, and
956993 // nothing advertised.
957994 expect ( frame ) . toContain ( '15 Freebucks/hr' )
958995 expect ( frame ) . not . toContain ( '15 15 Freebucks/hr' )
959- expect ( frame ) . not . toContain ( 'first-tab discount' )
960996 }
997+ expect ( frame ) . not . toContain ( 'first-tab discount' )
998+ expect ( frame ) . not . toContain ( 'Prices shown include the discount' )
961999 } ,
9621000 )
9631001
@@ -1142,6 +1180,34 @@ describe('a row the balance cannot cover', () => {
11421180 }
11431181 } )
11441182
1183+ test ( 'a pending paywall clears without discount prose when the balance refresh makes the row affordable' , async ( ) => {
1184+ const { setup, requested } = await renderUnaffordableLuna ( )
1185+ flushSync ( ( ) => setup . mockInput . pressEnter ( ) )
1186+ await setup . renderOnce ( )
1187+ expect ( setup . captureCharFrame ( ) ) . toContain ( `Not enough ${ FREEBUCKS_LABEL } ` )
1188+ expect ( requested ) . toEqual ( [ ] )
1189+
1190+ flushSync ( ( ) => useFreebuffSessionStore . getState ( ) . setSession ( {
1191+ status : 'none' ,
1192+ accessTier : 'full' ,
1193+ freebucks : applyFirstTabDiscount ( freebucksFixture ( 25 , {
1194+ [ FREEBUFF_GPT_5_6_LUNA_MODEL_ID ] : 20 ,
1195+ [ FREEBUFF_MIMO_V25_MODEL_ID ] : 10 ,
1196+ } ) , { amount : 10 , available : true } ) ,
1197+ } ) )
1198+ await setup . renderOnce ( )
1199+ const frame = setup . captureCharFrame ( )
1200+ expect ( frame ) . toContain ( '› GPT-5.6 Luna' )
1201+ expect ( frame ) . toMatch ( / │ + 1 0 F r e e b u c k s \/ h r / )
1202+ expect ( frame ) . not . toContain ( `Not enough ${ FREEBUCKS_LABEL } ` )
1203+ expect ( frame ) . not . toMatch ( / f i r s t - t a b d i s c o u n t | P r i c e s s h o w n i n c l u d e t h e d i s c o u n t / )
1204+ expect ( requested ) . toEqual ( [ ] )
1205+
1206+ flushSync ( ( ) => setup . mockInput . pressEnter ( ) )
1207+ await setup . renderOnce ( )
1208+ expect ( requested ) . toEqual ( [ FREEBUFF_GPT_5_6_LUNA_MODEL_ID ] )
1209+ } )
1210+
11451211 test ( 'a row closed for the hour stays inert — no wall to raise' , async ( ) => {
11461212 useFreebuffSessionStore . getState ( ) . setSession ( {
11471213 status : 'none' ,
0 commit comments