@@ -6,15 +6,40 @@ import {
66 lockupCells ,
77 lockupText ,
88 lockupWidth ,
9+ type LockupInput ,
910} from "./lockup"
11+ import { STALL_BLINK_BURST_MS , STALL_BLINK_CYCLE_MS , type RampPhase } from "./ramp"
1012import { UI } from "./theme"
1113
12- const still = ( nowMs = 0 ) => lockupCells ( { nowMs, still : true } )
14+ const idle = ( nowMs : number ) : LockupInput => ( {
15+ nowMs,
16+ still : true ,
17+ phase : null ,
18+ changedMs : 0 ,
19+ rampPhase : null ,
20+ stalledForMs : null ,
21+ } )
22+
23+ const live = (
24+ nowMs : number ,
25+ phase : string ,
26+ rampPhase : RampPhase ,
27+ stalledForMs : number | null ,
28+ ) : LockupInput => ( {
29+ nowMs,
30+ still : false ,
31+ phase,
32+ changedMs : 0 ,
33+ rampPhase,
34+ stalledForMs,
35+ } )
36+
37+ const still = ( nowMs = 0 ) => lockupCells ( idle ( nowMs ) )
1338
1439describe ( "brand lockup" , ( ) => {
1540 test ( "idle is the wordmark alone" , ( ) => {
1641 const cells = still ( )
17- expect ( cells ) . toHaveLength ( lockupWidth ( null ) )
42+ expect ( cells ) . toHaveLength ( lockupWidth ( idle ( 0 ) ) )
1843 expect ( lockupText ( cells ) ) . toBe ( LOCKUP_WORDMARK )
1944 // The mountain lives on the landing; one row cannot hold a silhouette.
2045 expect ( lockupText ( cells ) ) . not . toMatch ( / [ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ] / )
@@ -28,9 +53,16 @@ describe("brand lockup", () => {
2853 } )
2954
3055 test ( "a live turn swaps the wordmark for the phase" , ( ) => {
31- const cells = lockupCells ( { nowMs : 0 , still : false , phase : "thinking" } )
32- expect ( lockupText ( cells ) ) . toBe ( "thinking" )
33- expect ( lockupWidth ( "thinking" ) ) . toBe ( cells . length )
56+ const input : LockupInput = {
57+ nowMs : 0 ,
58+ still : false ,
59+ phase : "thinking" ,
60+ changedMs : 0 ,
61+ rampPhase : null ,
62+ stalledForMs : null ,
63+ }
64+ expect ( lockupText ( lockupCells ( input ) ) ) . toBe ( "thinking" )
65+ expect ( lockupWidth ( input ) ) . toBe ( lockupCells ( input ) . length )
3466 } )
3567
3668 test ( "the wordmark stays chrome-dim" , ( ) => {
@@ -46,6 +78,8 @@ describe("brand lockup", () => {
4678 still : false ,
4779 phase : "bash" ,
4880 changedMs : 0 ,
81+ rampPhase : null ,
82+ stalledForMs : null ,
4983 } )
5084 const tone = ( elapsed : number ) => at ( elapsed ) [ 0 ] ?. fg
5185 expect ( tone ( 0 ) ) . toBe ( UI . textFaint )
@@ -59,3 +93,108 @@ describe("brand lockup", () => {
5993 expect ( lockupText ( at ( 0 ) ) ) . toBe ( lockupText ( at ( LOCKUP_FADE_MS ) ) )
6094 } )
6195} )
96+
97+ describe ( "the live phase slot's pulse cell" , ( ) => {
98+ test ( "working keeps the word and leads it with a density cell" , ( ) => {
99+ const cells = lockupCells ( live ( 0 , "streaming 3 tok" , "working" , null ) )
100+ expect ( lockupText ( cells ) ) . toMatch ( / ^ [ ░ ▒ ▓ █ ] s t r e a m i n g 3 t o k $ / )
101+ for ( const cell of cells ) expect ( cell . fg ) . toBe ( UI . inFlight )
102+ } )
103+
104+ test ( "working's cell moves — the slot's glyphs change over a cycle" , ( ) => {
105+ const seen = new Set (
106+ [ 0 , 300 , 600 , 900 ] . map ( ( nowMs ) =>
107+ lockupText ( lockupCells ( live ( nowMs , "working" , "working" , null ) ) ) ,
108+ ) ,
109+ )
110+ expect ( seen . size ) . toBeGreaterThan ( 1 )
111+ } )
112+
113+ test ( "blocked holds one static cell — stillness is the signal" , ( ) => {
114+ const at = ( nowMs : number ) =>
115+ lockupText ( lockupCells ( live ( nowMs , "blocked" , "blocked" , null ) ) )
116+ expect ( at ( 0 ) ) . toBe ( "▌ blocked" )
117+ expect ( at ( STALL_BLINK_CYCLE_MS ) ) . toBe ( at ( 0 ) )
118+ expect ( at ( 60_000 ) ) . toBe ( at ( 0 ) )
119+ } )
120+
121+ test ( "working and blocked differ in glyph, not only in colour" , ( ) => {
122+ // Same word, same instant, colour stripped: the cell is the only thing
123+ // that can tell them apart, and it must.
124+ const distinct = new Set (
125+ [ 0 , 300 , 600 , 900 ] . map (
126+ ( nowMs ) =>
127+ `${ lockupText ( lockupCells ( live ( nowMs , "working" , "working" , null ) ) ) } |${ lockupText (
128+ lockupCells ( live ( nowMs , "working" , "blocked" , null ) ) ,
129+ ) } `,
130+ ) ,
131+ )
132+ for ( const pair of distinct ) {
133+ const [ moving , waiting ] = pair . split ( "|" )
134+ expect ( moving ) . not . toBe ( waiting )
135+ }
136+ } )
137+
138+ test ( "stalled blinks a bang against a block while the burst runs" , ( ) => {
139+ const on = lockupText ( lockupCells ( live ( 0 , "working" , "stalled" , 0 ) ) )
140+ const off = lockupText (
141+ lockupCells ( live ( STALL_BLINK_CYCLE_MS / 2 , "working" , "stalled" , 0 ) ) ,
142+ )
143+ expect ( on ) . toBe ( "█ working" )
144+ expect ( off ) . toBe ( "! working" )
145+ } )
146+
147+ test ( "stalled settles to a static bang once the burst has spent itself" , ( ) => {
148+ const past = STALL_BLINK_BURST_MS
149+ const at = ( nowMs : number ) =>
150+ lockupText ( lockupCells ( live ( nowMs , "working" , "stalled" , past + nowMs ) ) )
151+ expect ( at ( 0 ) ) . toBe ( "! working" )
152+ expect ( at ( STALL_BLINK_CYCLE_MS / 2 ) ) . toBe ( "! working" )
153+ expect ( at ( 120_000 ) ) . toBe ( "! working" )
154+ } )
155+
156+ test ( "a stall already older than the burst never blinks at all" , ( ) => {
157+ // A resumed session inherits stale activity; bursting at it would alarm
158+ // the operator about silence they were not present for.
159+ const resumed = STALL_BLINK_BURST_MS * 4
160+ for ( const nowMs of [ 0 , 225 , 450 , 675 ] ) {
161+ expect (
162+ lockupText ( lockupCells ( live ( nowMs , "working" , "stalled" , resumed ) ) ) ,
163+ ) . toBe ( "! working" )
164+ }
165+ } )
166+
167+ test ( "the stalled word stays legible — only the cell blinks" , ( ) => {
168+ for ( const nowMs of [ 0 , STALL_BLINK_CYCLE_MS / 2 ] ) {
169+ expect (
170+ lockupText ( lockupCells ( live ( nowMs , "bash" , "stalled" , 0 ) ) ) ,
171+ ) . toContain ( "bash" )
172+ }
173+ } )
174+
175+ test ( "working, blocked and stalled all read apart with no colour at all" , ( ) => {
176+ const glyph = ( rampPhase : RampPhase , stalledForMs : number | null ) =>
177+ lockupText ( lockupCells ( live ( 0 , "working" , rampPhase , stalledForMs ) ) ) [ 0 ]
178+ expect ( new Set ( [ glyph ( "blocked" , null ) , glyph ( "stalled" , 0 ) ] ) . size ) . toBe ( 2 )
179+ // Working sweeps the density glyphs; neither of the other two is one.
180+ const workingGlyphs = new Set (
181+ [ 0 , 300 , 600 , 900 ] . map (
182+ ( nowMs ) =>
183+ lockupText ( lockupCells ( live ( nowMs , "working" , "working" , null ) ) ) [ 0 ] ,
184+ ) ,
185+ )
186+ expect ( workingGlyphs . has ( glyph ( "blocked" , null ) ) ) . toBe ( false )
187+ } )
188+
189+ test ( "the slot's width never changes across a blink" , ( ) => {
190+ // A wide (CJK) and an astral label: the reservation is measured in columns
191+ // and the blink must not move it, whatever the label is made of.
192+ for ( const label of [ "読み込み中" , "a😀b" , "working" ] ) {
193+ const on = lockupWidth ( live ( 0 , label , "stalled" , 0 ) )
194+ const off = lockupWidth (
195+ live ( STALL_BLINK_CYCLE_MS / 2 , label , "stalled" , 0 ) ,
196+ )
197+ expect ( off ) . toBe ( on )
198+ }
199+ } )
200+ } )
0 commit comments