@@ -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+ maxRetries = 0 ,
374+ } : {
375+ downloadPatchFromPpk ?: ReturnType < typeof mock > ;
376+ downloadPatchFromPackage ?: ReturnType < typeof mock > ;
377+ downloadFullUpdate ?: ReturnType < typeof mock > ;
378+ maxRetries ?: number ;
379+ } = { } ) => {
380+ setupClientMocks ( {
381+ downloadPatchFromPpk,
382+ downloadPatchFromPackage,
383+ downloadFullUpdate,
384+ } ) ;
385+
386+ // Mock testUrls to return urls directly (skip actual HEAD ping)
387+ mock . module ( '../utils' , ( ) => ( {
388+ __esModule : true ,
389+ assertWeb : ( ) => true ,
390+ computeProgress : ( received : number , total : number ) =>
391+ total > 0 ? Math . floor ( ( received / total ) * 100 ) : 0 ,
392+ DEFAULT_FETCH_TIMEOUT_MS : 5000 ,
393+ emptyObj : { } ,
394+ fetchWithTimeout : mock ( ( ) => Promise . resolve ( ) ) ,
395+ info : mock ( ( ) => { } ) ,
396+ joinUrls : ( paths : string [ ] , fileName ?: string ) =>
397+ fileName ? paths . map ( p => `${ p } /${ fileName } ` ) : undefined ,
398+ log : mock ( ( ) => { } ) ,
399+ noop : ( ) => { } ,
400+ promiseAny : mock ( ( ) => Promise . resolve ( ) ) ,
401+ testUrls : ( urls ?: string [ ] ) =>
402+ Promise . resolve ( urls ?. [ 0 ] || null ) ,
403+ } ) ) ;
404+
405+ return { downloadPatchFromPpk, downloadPatchFromPackage, downloadFullUpdate } ;
406+ } ;
407+
408+ const updateInfo = {
409+ update : true as const ,
410+ hash : 'new-hash' ,
411+ diff : 'diff.ppk' ,
412+ pdiff : 'pdiff.ppk' ,
413+ full : 'full.ppk' ,
414+ paths : [ 'https://cdn.example.com' ] ,
415+ name : 'v2.0' ,
416+ description : 'test update' ,
417+ } ;
418+
419+ test ( 'uses diff when available' , async ( ) => {
420+ const { downloadPatchFromPpk } = setupDownloadMocks ( ) ;
421+ const { Pushy, sharedState } = await importFreshClient ( 'dl-diff-ok' ) ;
422+ sharedState . downloadedHash = undefined ;
423+ const client = new Pushy ( { appKey : 'demo-app' } ) ;
424+
425+ const hash = await client . downloadUpdate ( updateInfo ) ;
426+
427+ expect ( hash ) . toBe ( 'new-hash' ) ;
428+ expect ( downloadPatchFromPpk ) . toHaveBeenCalledTimes ( 1 ) ;
429+ } ) ;
430+
431+ test ( 'falls back to pdiff when diff fails' , async ( ) => {
432+ const { downloadPatchFromPpk, downloadPatchFromPackage } =
433+ setupDownloadMocks ( {
434+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
435+ } ) ;
436+ const { Pushy, sharedState } = await importFreshClient ( 'dl-fallback-pdiff' ) ;
437+ sharedState . downloadedHash = undefined ;
438+ const client = new Pushy ( { appKey : 'demo-app' } ) ;
439+
440+ const hash = await client . downloadUpdate ( updateInfo ) ;
441+
442+ expect ( hash ) . toBe ( 'new-hash' ) ;
443+ expect ( downloadPatchFromPpk ) . toHaveBeenCalledTimes ( 1 ) ;
444+ expect ( downloadPatchFromPackage ) . toHaveBeenCalledTimes ( 1 ) ;
445+ } ) ;
446+
447+ test ( 'falls back to full when diff and pdiff fail' , async ( ) => {
448+ const { downloadPatchFromPpk, downloadPatchFromPackage, downloadFullUpdate } =
449+ setupDownloadMocks ( {
450+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
451+ downloadPatchFromPackage : mock ( ( ) =>
452+ Promise . reject ( Error ( 'pdiff fail' ) ) ,
453+ ) ,
454+ } ) ;
455+ const { Pushy, sharedState } = await importFreshClient ( 'dl-fallback-full' ) ;
456+ sharedState . downloadedHash = undefined ;
457+ const client = new Pushy ( { appKey : 'demo-app' } ) ;
458+
459+ const hash = await client . downloadUpdate ( updateInfo ) ;
460+
461+ expect ( hash ) . toBe ( 'new-hash' ) ;
462+ expect ( downloadPatchFromPpk ) . toHaveBeenCalledTimes ( 1 ) ;
463+ expect ( downloadPatchFromPackage ) . toHaveBeenCalledTimes ( 1 ) ;
464+ expect ( downloadFullUpdate ) . toHaveBeenCalledTimes ( 1 ) ;
465+ } ) ;
466+
467+ test ( 'throws when all download methods fail' , async ( ) => {
468+ setupDownloadMocks ( {
469+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
470+ downloadPatchFromPackage : mock ( ( ) => Promise . reject ( Error ( 'pdiff fail' ) ) ) ,
471+ downloadFullUpdate : mock ( ( ) => Promise . reject ( Error ( 'full fail' ) ) ) ,
472+ maxRetries : 0 ,
473+ } ) ;
474+ const { Pushy, sharedState } = await importFreshClient ( 'dl-all-fail' ) ;
475+ sharedState . downloadedHash = undefined ;
476+ const client = new Pushy ( { appKey : 'demo-app' , maxRetries : 0 } ) ;
477+
478+ await expect ( client . downloadUpdate ( updateInfo ) ) . rejects . toThrow (
479+ 'error_full_patch_failed' ,
480+ ) ;
481+ } ) ;
482+
483+ test ( 'retries download when maxRetries is set' , async ( ) => {
484+ let callCount = 0 ;
485+ const { downloadFullUpdate } = setupDownloadMocks ( {
486+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
487+ downloadPatchFromPackage : mock ( ( ) => Promise . reject ( Error ( 'pdiff fail' ) ) ) ,
488+ downloadFullUpdate : mock ( ( ) => {
489+ callCount ++ ;
490+ if ( callCount === 1 ) {
491+ return Promise . reject ( Error ( 'full fail attempt 1' ) ) ;
492+ }
493+ return Promise . resolve ( ) ;
494+ } ) ,
495+ maxRetries : 2 ,
496+ } ) ;
497+ const { Pushy, sharedState } = await importFreshClient ( 'dl-retry-ok' ) ;
498+ sharedState . downloadedHash = undefined ;
499+ const client = new Pushy ( { appKey : 'demo-app' , maxRetries : 2 } ) ;
500+
501+ const hash = await client . downloadUpdate ( updateInfo ) ;
502+
503+ expect ( hash ) . toBe ( 'new-hash' ) ;
504+ expect ( downloadFullUpdate ) . toHaveBeenCalledTimes ( 2 ) ;
505+ } ) ;
506+
507+ test ( 'defaults to 3 retries when maxRetries is not set' , async ( ) => {
508+ const { downloadFullUpdate } = setupDownloadMocks ( {
509+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
510+ downloadPatchFromPackage : mock ( ( ) => Promise . reject ( Error ( 'pdiff fail' ) ) ) ,
511+ downloadFullUpdate : mock ( ( ) => Promise . reject ( Error ( 'full fail' ) ) ) ,
512+ } ) ;
513+ const { Pushy, sharedState } = await importFreshClient ( 'dl-default-retries' ) ;
514+ sharedState . downloadedHash = undefined ;
515+ const client = new Pushy ( { appKey : 'demo-app' } ) ;
516+
517+ await expect ( client . downloadUpdate ( updateInfo ) ) . rejects . toThrow ( ) ;
518+ // 1 initial + 3 retries = 4 calls
519+ expect ( downloadFullUpdate ) . toHaveBeenCalledTimes ( 4 ) ;
520+ } ) ;
521+
522+ test ( 'exhausts retries and throws on persistent failure' , async ( ) => {
523+ setupDownloadMocks ( {
524+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
525+ downloadPatchFromPackage : mock ( ( ) => Promise . reject ( Error ( 'pdiff fail' ) ) ) ,
526+ downloadFullUpdate : mock ( ( ) => Promise . reject ( Error ( 'full fail' ) ) ) ,
527+ maxRetries : 2 ,
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