@@ -364,3 +364,236 @@ describe('Pushy server config', () => {
364364 expect ( restartApp ) . toHaveBeenCalled ( ) ;
365365 } ) ;
366366} ) ;
367+
368+ describe ( 'downloadUpdate fallback chain' , ( ) => {
369+ const setupDownloadMocks = ( {
370+ downloadPatchFromPpk = mock ( ( ) => Promise . resolve ( ) ) ,
371+ downloadPatchFromPackage = mock ( ( ) => Promise . resolve ( ) ) ,
372+ downloadFullUpdate = mock ( ( ) => Promise . resolve ( ) ) ,
373+ } : {
374+ downloadPatchFromPpk ?: ReturnType < typeof mock > ;
375+ downloadPatchFromPackage ?: ReturnType < typeof mock > ;
376+ downloadFullUpdate ?: ReturnType < typeof mock > ;
377+ } = { } ) => {
378+ setupClientMocks ( {
379+ downloadPatchFromPpk,
380+ downloadPatchFromPackage,
381+ downloadFullUpdate,
382+ } ) ;
383+
384+ // Override setTimeout to skip real backoff delays in retry tests
385+ const realSetTimeout = globalThis . setTimeout . bind ( globalThis ) ;
386+ globalThis . setTimeout = ( ( fn : ( ...args : any [ ] ) => void , _ms ?: number ) =>
387+ realSetTimeout ( fn , 0 ) ) as unknown as typeof setTimeout ;
388+
389+ // Mock testUrls to return urls directly (skip actual HEAD ping)
390+ mock . module ( '../utils' , ( ) => ( {
391+ __esModule : true ,
392+ assertWeb : ( ) => true ,
393+ computeProgress : ( received : number , total : number ) =>
394+ total > 0 ? Math . floor ( ( received / total ) * 100 ) : 0 ,
395+ DEFAULT_FETCH_TIMEOUT_MS : 5000 ,
396+ emptyObj : { } ,
397+ fetchWithTimeout : mock ( ( ) => Promise . resolve ( ) ) ,
398+ info : mock ( ( ) => { } ) ,
399+ joinUrls : ( paths : string [ ] , fileName ?: string ) =>
400+ fileName ? paths . map ( p => `${ p } /${ fileName } ` ) : undefined ,
401+ log : mock ( ( ) => { } ) ,
402+ noop : ( ) => { } ,
403+ promiseAny : mock ( ( ) => Promise . resolve ( ) ) ,
404+ testUrls : ( urls ?: string [ ] ) =>
405+ Promise . resolve ( urls ?. [ 0 ] || null ) ,
406+ } ) ) ;
407+
408+ return { downloadPatchFromPpk, downloadPatchFromPackage, downloadFullUpdate } ;
409+ } ;
410+
411+ const updateInfo = {
412+ update : true as const ,
413+ hash : 'new-hash' ,
414+ diff : 'diff.ppk' ,
415+ pdiff : 'pdiff.ppk' ,
416+ full : 'full.ppk' ,
417+ paths : [ 'https://cdn.example.com' ] ,
418+ name : 'v2.0' ,
419+ description : 'test update' ,
420+ } ;
421+
422+ test ( 'uses diff when available' , async ( ) => {
423+ const { downloadPatchFromPpk } = setupDownloadMocks ( ) ;
424+ const { Pushy, sharedState } = await importFreshClient ( 'dl-diff-ok' ) ;
425+ sharedState . downloadedHash = undefined ;
426+ const client = new Pushy ( { appKey : 'demo-app' } ) ;
427+
428+ const hash = await client . downloadUpdate ( updateInfo ) ;
429+
430+ expect ( hash ) . toBe ( 'new-hash' ) ;
431+ expect ( downloadPatchFromPpk ) . toHaveBeenCalledTimes ( 1 ) ;
432+ } ) ;
433+
434+ test ( 'falls back to pdiff when diff fails' , async ( ) => {
435+ const { downloadPatchFromPpk, downloadPatchFromPackage } =
436+ setupDownloadMocks ( {
437+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
438+ } ) ;
439+ const { Pushy, sharedState } = await importFreshClient ( 'dl-fallback-pdiff' ) ;
440+ sharedState . downloadedHash = undefined ;
441+ const client = new Pushy ( { appKey : 'demo-app' } ) ;
442+
443+ const hash = await client . downloadUpdate ( updateInfo ) ;
444+
445+ expect ( hash ) . toBe ( 'new-hash' ) ;
446+ expect ( downloadPatchFromPpk ) . toHaveBeenCalledTimes ( 1 ) ;
447+ expect ( downloadPatchFromPackage ) . toHaveBeenCalledTimes ( 1 ) ;
448+ } ) ;
449+
450+ test ( 'falls back to full when diff and pdiff fail' , async ( ) => {
451+ const { downloadPatchFromPpk, downloadPatchFromPackage, downloadFullUpdate } =
452+ setupDownloadMocks ( {
453+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
454+ downloadPatchFromPackage : mock ( ( ) =>
455+ Promise . reject ( Error ( 'pdiff fail' ) ) ,
456+ ) ,
457+ } ) ;
458+ const { Pushy, sharedState } = await importFreshClient ( 'dl-fallback-full' ) ;
459+ sharedState . downloadedHash = undefined ;
460+ const client = new Pushy ( { appKey : 'demo-app' } ) ;
461+
462+ const hash = await client . downloadUpdate ( updateInfo ) ;
463+
464+ expect ( hash ) . toBe ( 'new-hash' ) ;
465+ expect ( downloadPatchFromPpk ) . toHaveBeenCalledTimes ( 1 ) ;
466+ expect ( downloadPatchFromPackage ) . toHaveBeenCalledTimes ( 1 ) ;
467+ expect ( downloadFullUpdate ) . toHaveBeenCalledTimes ( 1 ) ;
468+ } ) ;
469+
470+ test ( 'throws when all download methods fail' , async ( ) => {
471+ setupDownloadMocks ( {
472+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
473+ downloadPatchFromPackage : mock ( ( ) => Promise . reject ( Error ( 'pdiff fail' ) ) ) ,
474+ downloadFullUpdate : mock ( ( ) => Promise . reject ( Error ( 'full fail' ) ) ) ,
475+ } ) ;
476+ const { Pushy, sharedState } = await importFreshClient ( 'dl-all-fail' ) ;
477+ sharedState . downloadedHash = undefined ;
478+ const client = new Pushy ( { appKey : 'demo-app' , maxRetries : 0 } ) ;
479+
480+ await expect ( client . downloadUpdate ( updateInfo ) ) . rejects . toThrow (
481+ 'error_full_patch_failed' ,
482+ ) ;
483+ } ) ;
484+
485+ test ( 'retries download when maxRetries is set' , async ( ) => {
486+ let callCount = 0 ;
487+ const { downloadFullUpdate } = setupDownloadMocks ( {
488+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
489+ downloadPatchFromPackage : mock ( ( ) => Promise . reject ( Error ( 'pdiff fail' ) ) ) ,
490+ downloadFullUpdate : mock ( ( ) => {
491+ callCount ++ ;
492+ if ( callCount === 1 ) {
493+ return Promise . reject ( Error ( 'full fail attempt 1' ) ) ;
494+ }
495+ return Promise . resolve ( ) ;
496+ } ) ,
497+ } ) ;
498+ const { Pushy, sharedState } = await importFreshClient ( 'dl-retry-ok' ) ;
499+ sharedState . downloadedHash = undefined ;
500+ const client = new Pushy ( { appKey : 'demo-app' , maxRetries : 2 } ) ;
501+
502+ const hash = await client . downloadUpdate ( updateInfo ) ;
503+
504+ expect ( hash ) . toBe ( 'new-hash' ) ;
505+ expect ( downloadFullUpdate ) . toHaveBeenCalledTimes ( 2 ) ;
506+ } ) ;
507+
508+ test ( 'defaults to 3 retries when maxRetries is not set' , async ( ) => {
509+ const { downloadFullUpdate } = setupDownloadMocks ( {
510+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
511+ downloadPatchFromPackage : mock ( ( ) => Promise . reject ( Error ( 'pdiff fail' ) ) ) ,
512+ downloadFullUpdate : mock ( ( ) => Promise . reject ( Error ( 'full fail' ) ) ) ,
513+ } ) ;
514+ const { Pushy, sharedState } = await importFreshClient ( 'dl-default-retries' ) ;
515+ sharedState . downloadedHash = undefined ;
516+ const client = new Pushy ( { appKey : 'demo-app' } ) ;
517+
518+ await expect ( client . downloadUpdate ( updateInfo ) ) . rejects . toThrow ( ) ;
519+ // 1 initial + 3 retries = 4 calls
520+ expect ( downloadFullUpdate ) . toHaveBeenCalledTimes ( 4 ) ;
521+ } ) ;
522+
523+ test ( 'exhausts retries and throws on persistent failure' , async ( ) => {
524+ setupDownloadMocks ( {
525+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
526+ downloadPatchFromPackage : mock ( ( ) => Promise . reject ( Error ( 'pdiff fail' ) ) ) ,
527+ downloadFullUpdate : mock ( ( ) => Promise . reject ( Error ( 'full fail' ) ) ) ,
528+ } ) ;
529+ const { Pushy, sharedState } = await importFreshClient ( 'dl-retry-exhaust' ) ;
530+ sharedState . downloadedHash = undefined ;
531+ const client = new Pushy ( { appKey : 'demo-app' , maxRetries : 2 } ) ;
532+
533+ await expect ( client . downloadUpdate ( updateInfo ) ) . rejects . toThrow (
534+ 'error_full_patch_failed' ,
535+ ) ;
536+ } ) ;
537+ } ) ;
538+
539+ describe ( 'Cresc class' , ( ) => {
540+ test ( 'uses Cresc server endpoints' , async ( ) => {
541+ setupClientMocks ( ) ;
542+
543+ const { Cresc } = await importFreshClient ( 'cresc-endpoints' ) ;
544+ const client = new Cresc ( { appKey : 'demo-app' } ) ;
545+
546+ expect ( client . getConfiguredCheckEndpoints ( ) ) . toEqual ( [
547+ 'https://api.cresc.dev' ,
548+ 'https://api.cresc.app' ,
549+ ] ) ;
550+ } ) ;
551+
552+ test ( 'defaults locale to en for Cresc' , async ( ) => {
553+ setupClientMocks ( ) ;
554+ // Override i18n mock AFTER setupClientMocks to avoid being overwritten
555+ const setLocale = mock ( ( ) => { } ) ;
556+ mock . module ( '../i18n' , ( ) => ( {
557+ default : {
558+ t : ( key : string ) => key ,
559+ setLocale,
560+ } ,
561+ } ) ) ;
562+
563+ const { Cresc } = await importFreshClient ( 'cresc-locale' ) ;
564+ const client = new Cresc ( { appKey : 'demo-app' } ) ;
565+
566+ expect ( client . clientType ) . toBe ( 'Cresc' ) ;
567+ expect ( setLocale ) . toHaveBeenCalledWith ( 'en' ) ;
568+ } ) ;
569+
570+ test ( 'Cresc is instance of Pushy' , async ( ) => {
571+ setupClientMocks ( ) ;
572+
573+ const { Cresc, Pushy } = await importFreshClient ( 'cresc-instanceof' ) ;
574+ const client = new Cresc ( { appKey : 'demo-app' } ) ;
575+
576+ expect ( client ) . toBeInstanceOf ( Pushy ) ;
577+ expect ( client ) . toBeInstanceOf ( Cresc ) ;
578+ } ) ;
579+
580+ test ( 'Cresc custom server overrides default endpoints' , async ( ) => {
581+ setupClientMocks ( ) ;
582+
583+ const { Cresc } = await importFreshClient ( 'cresc-custom-server' ) ;
584+ const client = new Cresc ( {
585+ appKey : 'demo-app' ,
586+ server : {
587+ main : [ 'https://custom.example.com' ] ,
588+ queryUrls : [ 'https://q.example.com' ] ,
589+ } ,
590+ } ) ;
591+
592+ expect ( client . getConfiguredCheckEndpoints ( ) ) . toEqual ( [
593+ 'https://custom.example.com' ,
594+ ] ) ;
595+ expect ( client . options . server ?. queryUrls ) . toEqual ( [
596+ 'https://q.example.com' ,
597+ ] ) ;
598+ } ) ;
599+ } ) ;
0 commit comments