@@ -1001,12 +1001,14 @@ describe('downloadUpdate fallback chain', () => {
10011001 downloadFullUpdate = mock ( ( ) => Promise . resolve ( ) ) ,
10021002 addProgressListener = mock ( ( ) => ( { remove : mock ( ( ) => { } ) } ) ) ,
10031003 setLocalHashInfo = mock ( ( ) => { } ) ,
1004+ fetchWithTimeout = mock ( ( ) => Promise . resolve ( ) ) ,
10041005 } : {
10051006 downloadPatchFromPpk ?: ReturnType < typeof mock > ;
10061007 downloadPatchFromPackage ?: ReturnType < typeof mock > ;
10071008 downloadFullUpdate ?: ReturnType < typeof mock > ;
10081009 addProgressListener ?: ReturnType < typeof mock > ;
10091010 setLocalHashInfo ?: ReturnType < typeof mock > ;
1011+ fetchWithTimeout ?: ReturnType < typeof mock > ;
10101012 } = { } ) => {
10111013 setupClientMocks ( {
10121014 downloadPatchFromPpk,
@@ -1029,7 +1031,7 @@ describe('downloadUpdate fallback chain', () => {
10291031 ? Math . min ( 100 , Math . max ( 0 , Math . floor ( ( received / total ) * 100 ) ) )
10301032 : 0 ,
10311033 DEFAULT_FETCH_TIMEOUT_MS : 5000 ,
1032- fetchWithTimeout : mock ( ( ) => Promise . resolve ( ) ) ,
1034+ fetchWithTimeout,
10331035 info : mock ( ( ) => { } ) ,
10341036 joinUrls : ( paths : string [ ] , fileName ?: string ) =>
10351037 fileName ? paths . map ( ( p ) => `${ p } /${ fileName } ` ) : undefined ,
@@ -1043,9 +1045,19 @@ describe('downloadUpdate fallback chain', () => {
10431045 downloadPatchFromPpk,
10441046 downloadPatchFromPackage,
10451047 downloadFullUpdate,
1048+ fetchWithTimeout,
10461049 } ;
10471050 } ;
10481051
1052+ // The telemetry POSTs a download sent, as { type, detail } pairs.
1053+ const serverReports = ( fetchWithTimeout : ReturnType < typeof mock > ) =>
1054+ fetchWithTimeout . mock . calls
1055+ . filter ( ( [ url ] : any [ ] ) => String ( url ) . includes ( '/report/' ) )
1056+ . map ( ( [ , init ] : any [ ] ) : { type : string ; detail ?: string } => {
1057+ const { type, detail } = JSON . parse ( String ( init . body ) ) ;
1058+ return { type, detail } ;
1059+ } ) ;
1060+
10491061 const updateInfo = {
10501062 update : true as const ,
10511063 hash : 'new-hash' ,
@@ -1555,6 +1567,50 @@ describe('downloadUpdate fallback chain', () => {
15551567 ) ;
15561568 } ) ;
15571569
1570+ test ( 'a download rescued after a network error is not a patch failure' , async ( ) => {
1571+ const timeout : any = Error ( '请求超时。' ) ;
1572+ timeout . code = 'DOWNLOAD_FAILED' ;
1573+ const { fetchWithTimeout } = setupDownloadMocks ( {
1574+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( timeout ) ) ,
1575+ } ) ;
1576+ const { Pushy, sharedState } = await importFreshClient (
1577+ 'dl-fallback-transport-report'
1578+ ) ;
1579+ sharedState . downloadedHash = undefined ;
1580+ const client = new Pushy ( { appKey : 'demo-app' , maxRetries : 0 } ) ;
1581+
1582+ expect ( await client . downloadUpdate ( updateInfo ) ) . toBe ( 'new-hash' ) ;
1583+
1584+ expect ( serverReports ( fetchWithTimeout ) ) . toEqual ( [
1585+ { type : 'download_success' } ,
1586+ ] ) ;
1587+ } ) ;
1588+
1589+ test ( 'a rescued patch failure is reported once, with only the patch error' , async ( ) => {
1590+ const patchError : any = Error ( 'hpatch failed' ) ;
1591+ patchError . code = 'PATCH_FAILED' ;
1592+ const timeout : any = Error ( '请求超时。' ) ;
1593+ timeout . code = 'DOWNLOAD_FAILED' ;
1594+ const { fetchWithTimeout } = setupDownloadMocks ( {
1595+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( patchError ) ) ,
1596+ downloadPatchFromPackage : mock ( ( ) => Promise . reject ( timeout ) ) ,
1597+ } ) ;
1598+ const { Pushy, sharedState } = await importFreshClient (
1599+ 'dl-fallback-patch-report'
1600+ ) ;
1601+ sharedState . downloadedHash = undefined ;
1602+ const client = new Pushy ( { appKey : 'demo-app' , maxRetries : 0 } ) ;
1603+
1604+ expect ( await client . downloadUpdate ( updateInfo ) ) . toBe ( 'new-hash' ) ;
1605+
1606+ // The test i18n renders bare keys: the pdiff timeout is not in detail.
1607+ expect (
1608+ serverReports ( fetchWithTimeout ) . filter (
1609+ ( { type } ) => type === 'patch_fail'
1610+ )
1611+ ) . toEqual ( [ { type : 'patch_fail' , detail : 'error_diff_failed' } ] ) ;
1612+ } ) ;
1613+
15581614 test ( 'a `[PATCH_FAILED] ` message prefix classifies like a code property (bridge contract)' , async ( ) => {
15591615 // Harmony's bridge cannot set `code` on a rejection and prefixes the
15601616 // message instead; the strategy loop must treat it as the same
0 commit comments