@@ -9,9 +9,12 @@ import {
99 isNewer ,
1010 maybeNotifyUpdate ,
1111 parseVersion ,
12+ scheduleUpdateCheck ,
13+ showPendingUpdateNotice ,
1214} from "./version-check.js" ;
1315
1416const CACHE_FILE = path . join ( os . tmpdir ( ) , "olostep-cli-version-cache.json" ) ;
17+ const NOTICE_FILE = path . join ( os . tmpdir ( ) , "olostep-cli-update-notice.json" ) ;
1518
1619function clearCache ( ) {
1720 try { fs . unlinkSync ( CACHE_FILE ) ; } catch { /* ignore */ }
@@ -211,3 +214,153 @@ describe("maybeNotifyUpdate", () => {
211214 await expect ( maybeNotifyUpdate ( "1.0.0" ) ) . resolves . toBeUndefined ( ) ;
212215 } ) ;
213216} ) ;
217+
218+ describe ( "showPendingUpdateNotice" , ( ) => {
219+ const ORIGINAL_TTY = ( process . stderr as any ) . isTTY ;
220+
221+ function clearNotice ( ) {
222+ try { fs . unlinkSync ( NOTICE_FILE ) ; } catch { /* ignore */ }
223+ }
224+
225+ function writeNotice ( data : object ) {
226+ fs . writeFileSync ( NOTICE_FILE , JSON . stringify ( data ) , "utf8" ) ;
227+ }
228+
229+ beforeEach ( ( ) => {
230+ clearNotice ( ) ;
231+ delete process . env . OLOSTEP_NO_UPDATE_NOTICE ;
232+ delete process . env . OLOSTEP_NO_UPDATE_CHECK ;
233+ } ) ;
234+
235+ afterEach ( ( ) => {
236+ ( process . stderr as any ) . isTTY = ORIGINAL_TTY ;
237+ delete process . env . OLOSTEP_NO_UPDATE_NOTICE ;
238+ delete process . env . OLOSTEP_NO_UPDATE_CHECK ;
239+ clearNotice ( ) ;
240+ } ) ;
241+
242+ it ( "prints to stderr when a newer notice file exists" , ( ) => {
243+ ( process . stderr as any ) . isTTY = true ;
244+ writeNotice ( { latest : "9.9.9" } ) ;
245+ const chunks : string [ ] = [ ] ;
246+ const orig = process . stderr . write . bind ( process . stderr ) ;
247+ process . stderr . write = ( chunk : any ) => { chunks . push ( String ( chunk ) ) ; return true ; } ;
248+ showPendingUpdateNotice ( "1.0.0" ) ;
249+ process . stderr . write = orig ;
250+ expect ( chunks . join ( "" ) ) . toContain ( "9.9.9" ) ;
251+ expect ( fs . existsSync ( NOTICE_FILE ) ) . toBe ( false ) ;
252+ } ) ;
253+
254+ it ( "does nothing when no notice file exists" , ( ) => {
255+ ( process . stderr as any ) . isTTY = true ;
256+ const chunks : string [ ] = [ ] ;
257+ const orig = process . stderr . write . bind ( process . stderr ) ;
258+ process . stderr . write = ( chunk : any ) => { chunks . push ( String ( chunk ) ) ; return true ; } ;
259+ showPendingUpdateNotice ( "1.0.0" ) ;
260+ process . stderr . write = orig ;
261+ expect ( chunks ) . toHaveLength ( 0 ) ;
262+ } ) ;
263+
264+ it ( "deletes the notice file even when version is not newer" , ( ) => {
265+ ( process . stderr as any ) . isTTY = true ;
266+ writeNotice ( { latest : "0.0.1" } ) ;
267+ showPendingUpdateNotice ( "1.0.0" ) ;
268+ expect ( fs . existsSync ( NOTICE_FILE ) ) . toBe ( false ) ;
269+ } ) ;
270+
271+ it ( "skips when stderr is not a TTY" , ( ) => {
272+ ( process . stderr as any ) . isTTY = false ;
273+ writeNotice ( { latest : "9.9.9" } ) ;
274+ const chunks : string [ ] = [ ] ;
275+ const orig = process . stderr . write . bind ( process . stderr ) ;
276+ process . stderr . write = ( chunk : any ) => { chunks . push ( String ( chunk ) ) ; return true ; } ;
277+ showPendingUpdateNotice ( "1.0.0" ) ;
278+ process . stderr . write = orig ;
279+ expect ( chunks ) . toHaveLength ( 0 ) ;
280+ // Notice file should NOT be deleted when we skip (TTY check)
281+ expect ( fs . existsSync ( NOTICE_FILE ) ) . toBe ( true ) ;
282+ } ) ;
283+
284+ it ( "skips when OLOSTEP_NO_UPDATE_CHECK is set" , ( ) => {
285+ ( process . stderr as any ) . isTTY = true ;
286+ process . env . OLOSTEP_NO_UPDATE_CHECK = "1" ;
287+ writeNotice ( { latest : "9.9.9" } ) ;
288+ showPendingUpdateNotice ( "1.0.0" ) ;
289+ expect ( fs . existsSync ( NOTICE_FILE ) ) . toBe ( true ) ;
290+ } ) ;
291+
292+ it ( "skips when invokedSubcommand === 'update'" , ( ) => {
293+ ( process . stderr as any ) . isTTY = true ;
294+ writeNotice ( { latest : "9.9.9" } ) ;
295+ showPendingUpdateNotice ( "1.0.0" , "update" ) ;
296+ expect ( fs . existsSync ( NOTICE_FILE ) ) . toBe ( true ) ;
297+ } ) ;
298+
299+ it ( "never throws on malformed notice file" , ( ) => {
300+ ( process . stderr as any ) . isTTY = true ;
301+ fs . writeFileSync ( NOTICE_FILE , "not-json" , "utf8" ) ;
302+ expect ( ( ) => showPendingUpdateNotice ( "1.0.0" ) ) . not . toThrow ( ) ;
303+ } ) ;
304+ } ) ;
305+
306+ describe ( "scheduleUpdateCheck" , ( ) => {
307+ function clearNotice ( ) {
308+ try { fs . unlinkSync ( NOTICE_FILE ) ; } catch { /* ignore */ }
309+ }
310+
311+ beforeEach ( ( ) => {
312+ clearCache ( ) ;
313+ clearNotice ( ) ;
314+ delete process . env . OLOSTEP_NO_UPDATE_NOTICE ;
315+ delete process . env . OLOSTEP_NO_UPDATE_CHECK ;
316+ } ) ;
317+
318+ afterEach ( ( ) => {
319+ vi . unstubAllGlobals ( ) ;
320+ delete process . env . OLOSTEP_NO_UPDATE_NOTICE ;
321+ delete process . env . OLOSTEP_NO_UPDATE_CHECK ;
322+ clearCache ( ) ;
323+ clearNotice ( ) ;
324+ } ) ;
325+
326+ it ( "writes a notice file when a newer version is found" , async ( ) => {
327+ fs . writeFileSync (
328+ CACHE_FILE ,
329+ JSON . stringify ( { latest : "9.9.9" , checkedAt : Date . now ( ) } ) ,
330+ ) ;
331+ scheduleUpdateCheck ( "1.0.0" ) ;
332+ // scheduleUpdateCheck is fire-and-forget; wait for the microtask to settle
333+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
334+ expect ( fs . existsSync ( NOTICE_FILE ) ) . toBe ( true ) ;
335+ const data = JSON . parse ( fs . readFileSync ( NOTICE_FILE , "utf8" ) ) ;
336+ expect ( data . latest ) . toBe ( "9.9.9" ) ;
337+ } ) ;
338+
339+ it ( "does not write a notice file when already on latest" , async ( ) => {
340+ fs . writeFileSync (
341+ CACHE_FILE ,
342+ JSON . stringify ( { latest : "1.0.0" , checkedAt : Date . now ( ) } ) ,
343+ ) ;
344+ scheduleUpdateCheck ( "1.0.0" ) ;
345+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
346+ expect ( fs . existsSync ( NOTICE_FILE ) ) . toBe ( false ) ;
347+ } ) ;
348+
349+ it ( "skips when OLOSTEP_NO_UPDATE_CHECK is set" , async ( ) => {
350+ process . env . OLOSTEP_NO_UPDATE_CHECK = "1" ;
351+ const fetchSpy = vi . fn ( ) ;
352+ vi . stubGlobal ( "fetch" , fetchSpy ) ;
353+ scheduleUpdateCheck ( "1.0.0" ) ;
354+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
355+ expect ( fetchSpy ) . not . toHaveBeenCalled ( ) ;
356+ expect ( fs . existsSync ( NOTICE_FILE ) ) . toBe ( false ) ;
357+ } ) ;
358+
359+ it ( "skips when invokedSubcommand === 'update'" , async ( ) => {
360+ const fetchSpy = vi . fn ( ) ;
361+ vi . stubGlobal ( "fetch" , fetchSpy ) ;
362+ scheduleUpdateCheck ( "1.0.0" , "update" ) ;
363+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
364+ expect ( fetchSpy ) . not . toHaveBeenCalled ( ) ;
365+ } ) ;
366+ } ) ;
0 commit comments