@@ -16,7 +16,7 @@ import { detectPlatformFromFile } from '../utils/file-type-detector';
1616import platform from '../utils/platform' ;
1717
1818export interface MaestroRunAssets {
19- logs ?: string [ ] ;
19+ logs ?: Record < string , string > ;
2020 video ?: string | false ;
2121 screenshots ?: string [ ] ;
2222}
@@ -663,7 +663,7 @@ export default class Maestro {
663663 }
664664
665665 // Download artifacts if requested
666- if ( this . options . downloadArtifacts && this . options . artifactsOutputDir ) {
666+ if ( this . options . downloadArtifacts ) {
667667 await this . downloadArtifacts ( status . runs ) ;
668668 }
669669
@@ -857,33 +857,83 @@ export default class Maestro {
857857 ) ;
858858 }
859859
860- private async downloadFile ( url : string , filePath : string ) : Promise < void > {
861- try {
862- const response = await axios . get ( url , {
863- responseType : 'arraybuffer' ,
864- headers : {
865- 'User-Agent' : utils . getUserAgent ( ) ,
866- } ,
867- } ) ;
860+ private async downloadFile (
861+ url : string ,
862+ filePath : string ,
863+ retries = 3 ,
864+ ) : Promise < void > {
865+ let lastError : unknown ;
868866
869- await fs . promises . writeFile ( filePath , response . data ) ;
870- } catch ( error ) {
871- throw new TestingBotError ( `Failed to download file from ${ url } ` , {
872- cause : error ,
873- } ) ;
867+ for ( let attempt = 1 ; attempt <= retries ; attempt ++ ) {
868+ try {
869+ const response = await axios . get ( url , {
870+ responseType : 'arraybuffer' ,
871+ timeout : 60000 , // 60 second timeout for large files
872+ } ) ;
873+
874+ await fs . promises . writeFile ( filePath , response . data ) ;
875+ return ;
876+ } catch ( error ) {
877+ lastError = error ;
878+
879+ // Don't retry on 4xx errors (client errors like 403, 404)
880+ if ( axios . isAxiosError ( error ) && error . response ?. status ) {
881+ const status = error . response . status ;
882+ if ( status >= 400 && status < 500 ) {
883+ break ;
884+ }
885+ }
886+
887+ // Wait before retrying (exponential backoff)
888+ if ( attempt < retries ) {
889+ await this . sleep ( 1000 * attempt ) ;
890+ }
891+ }
892+ }
893+
894+ // Extract detailed error message
895+ let errorDetail = '' ;
896+ if ( axios . isAxiosError ( lastError ) ) {
897+ if ( lastError . response ) {
898+ errorDetail = `HTTP ${ lastError . response . status } : ${ lastError . response . statusText } ` ;
899+ } else if ( lastError . code ) {
900+ errorDetail = lastError . code ;
901+ } else if ( lastError . message ) {
902+ errorDetail = lastError . message ;
903+ }
904+ } else if ( lastError instanceof Error ) {
905+ errorDetail = lastError . message ;
906+ } else if ( lastError ) {
907+ errorDetail = String ( lastError ) ;
874908 }
909+
910+ throw new TestingBotError (
911+ `Failed to download file${ errorDetail ? `: ${ errorDetail } ` : '' } ` ,
912+ {
913+ cause : lastError ,
914+ } ,
915+ ) ;
875916 }
876917
877- private generateArtifactZipName ( ) : string {
878- // Use -- build option if provided, otherwise generate timestamp-based name
879- if ( this . options . build ) {
880- const sanitizedBuild = this . options . build . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, '_ ' ) ;
881- return `${ sanitizedBuild } .zip` ;
918+ private async generateArtifactZipName ( outputDir : string ) : Promise < string > {
919+ if ( ! this . options . build ) {
920+ // Generate unique name with timestamp
921+ const timestamp = new Date ( ) . toISOString ( ) . replace ( / [: . ] / g, '- ' ) ;
922+ return `maestro_artifacts_ ${ timestamp } .zip` ;
882923 }
883924
884- // Generate unique name with timestamp
885- const timestamp = new Date ( ) . toISOString ( ) . replace ( / [: .] / g, '-' ) ;
886- return `maestro_artifacts_${ timestamp } .zip` ;
925+ const baseName = this . options . build . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, '_' ) ;
926+ const fileName = `${ baseName } .zip` ;
927+ const filePath = path . join ( outputDir , fileName ) ;
928+
929+ try {
930+ await fs . promises . access ( filePath ) ;
931+ // File exists, append timestamp
932+ return `${ baseName } _${ Date . now ( ) } .zip` ;
933+ } catch {
934+ // File doesn't exist, use base name
935+ return fileName ;
936+ }
887937 }
888938
889939 private async downloadArtifacts ( runs : MaestroRunInfo [ ] ) : Promise < void > {
@@ -919,13 +969,17 @@ export default class Maestro {
919969 await fs . promises . mkdir ( runDir , { recursive : true } ) ;
920970
921971 // Download logs
922- if ( runDetails . assets . logs && runDetails . assets . logs . length > 0 ) {
972+ if (
973+ runDetails . assets . logs &&
974+ Object . keys ( runDetails . assets . logs ) . length > 0
975+ ) {
923976 const logsDir = path . join ( runDir , 'logs' ) ;
924977 await fs . promises . mkdir ( logsDir , { recursive : true } ) ;
925978
926- for ( let i = 0 ; i < runDetails . assets . logs . length ; i ++ ) {
927- const logUrl = runDetails . assets . logs [ i ] ;
928- const logFileName = path . basename ( logUrl ) || `log_${ i } .txt` ;
979+ for ( const [ logName , logUrl ] of Object . entries (
980+ runDetails . assets . logs ,
981+ ) ) {
982+ const logFileName = `${ logName } .txt` ;
929983 const logPath = path . join ( logsDir , logFileName ) ;
930984
931985 try {
@@ -949,7 +1003,7 @@ export default class Maestro {
9491003 await fs . promises . mkdir ( videoDir , { recursive : true } ) ;
9501004
9511005 const videoUrl = runDetails . assets . video ;
952- const videoFileName = path . basename ( videoUrl ) || 'video.mp4' ;
1006+ const videoFileName = 'video.mp4' ;
9531007 const videoPath = path . join ( videoDir , videoFileName ) ;
9541008
9551009 try {
@@ -973,12 +1027,8 @@ export default class Maestro {
9731027
9741028 for ( let i = 0 ; i < runDetails . assets . screenshots . length ; i ++ ) {
9751029 const screenshotUrl = runDetails . assets . screenshots [ i ] ;
976- const screenshotFileName =
977- path . basename ( screenshotUrl ) || `screenshot_${ i } .png` ;
978- const screenshotPath = path . join (
979- screenshotsDir ,
980- screenshotFileName ,
981- ) ;
1030+ const screenshotFileName = `screenshot_${ i } .png` ;
1031+ const screenshotPath = path . join ( screenshotsDir , screenshotFileName ) ;
9821032
9831033 try {
9841034 await this . downloadFile ( screenshotUrl , screenshotPath ) ;
@@ -1023,7 +1073,7 @@ export default class Maestro {
10231073 }
10241074 }
10251075
1026- const zipFileName = this . generateArtifactZipName ( ) ;
1076+ const zipFileName = await this . generateArtifactZipName ( outputDir ) ;
10271077 const zipFilePath = path . join ( outputDir , zipFileName ) ;
10281078
10291079 if ( ! this . options . quiet ) {
0 commit comments