@@ -19,6 +19,93 @@ import { releaseManifest } from '../scripts/package-cli-release.mjs';
1919import { validateReleaseReports } from '../scripts/publish-cli-release.mjs' ;
2020import { compareVersions , releaseCli } from '../scripts/release-cli.mjs' ;
2121import { compareRuns , validateRun , validateRequest , validateToolOutput , median , selectScenarios } from '../scripts/perf/report.mjs' ;
22+ import { copyMcodeToolsArtifact , downloadMcodeToolsArtifact , MCODE_TOOLS_ARTIFACT } from '../scripts/lib/mcode-tools-artifact.mjs' ;
23+
24+ test ( 'artifact download recovers from TLS reset and interrupted response bodies' , async ( ) => {
25+ const reset = new TypeError ( 'fetch failed' , { cause : Object . assign ( new Error ( 'connection reset' ) , { code : 'ECONNRESET' } ) } ) ;
26+ const interrupted = new TypeError ( 'terminated' , { cause : Object . assign ( new Error ( 'socket closed' ) , { code : 'UND_ERR_SOCKET' } ) } ) ;
27+ const signals = [ ] , delays = [ ] , warnings = [ ] ;
28+ const result = await downloadMcodeToolsArtifact ( async ( url , { signal } ) => {
29+ assert . equal ( url , MCODE_TOOLS_ARTIFACT . url ) ;
30+ assert . ok ( signal instanceof AbortSignal ) ;
31+ signals . push ( signal ) ;
32+ if ( signals . length === 1 ) throw reset ;
33+ if ( signals . length === 2 ) return new Response ( new ReadableStream ( {
34+ start ( controller ) { controller . enqueue ( new Uint8Array ( [ 1 , 2 ] ) ) ; } ,
35+ pull ( controller ) { controller . error ( interrupted ) ; } ,
36+ } ) ) ;
37+ return new Response ( 'complete archive' ) ;
38+ } , { wait : async ms => delays . push ( ms ) , warn : message => warnings . push ( message ) } ) ;
39+ assert . equal ( result . toString ( ) , 'complete archive' ) ;
40+ assert . equal ( new Set ( signals ) . size , 3 ) ;
41+ assert . deepEqual ( delays , [ 1000 , 2000 ] ) ;
42+ assert . equal ( warnings . length , 2 ) ;
43+ assert . match ( warnings [ 0 ] , / 1 \/ 3 .* E C O N N R E S E T / ) ;
44+ assert . match ( warnings [ 1 ] , / 2 \/ 3 .* U N D _ E R R _ S O C K E T / ) ;
45+ } ) ;
46+
47+ test ( 'artifact download retries temporary HTTP failures and releases rejected bodies' , async ( ) => {
48+ for ( const status of [ 408 , 429 , 500 , 502 , 503 , 504 ] ) {
49+ let calls = 0 , cancelled = false ;
50+ const delays = [ ] ;
51+ const result = await downloadMcodeToolsArtifact ( async ( ) => {
52+ if ( ++ calls > 1 ) return new Response ( 'ok' ) ;
53+ return new Response ( new ReadableStream ( { cancel ( ) { cancelled = true ; } } ) , { status } ) ;
54+ } , { wait : async ms => delays . push ( ms ) , warn ( ) { } } ) ;
55+ assert . equal ( result . toString ( ) , 'ok' ) ;
56+ assert . equal ( calls , 2 ) ;
57+ assert . equal ( cancelled , true ) ;
58+ assert . deepEqual ( delays , [ 1000 ] ) ;
59+ }
60+ } ) ;
61+
62+ test ( 'artifact download bounds Retry-After delays and rejects permanent failures immediately' , async ( ) => {
63+ for ( const [ header , expectedDelay ] of [ [ '5' , 5000 ] , [ '999999' , 30000 ] , [ 'invalid' , 1000 ] , [ '0' , 1000 ] ] ) {
64+ let calls = 0 ;
65+ const delays = [ ] ;
66+ await downloadMcodeToolsArtifact ( async ( ) => ++ calls === 1
67+ ? new Response ( null , { status : 429 , headers : { 'Retry-After' : header } } ) : new Response ( 'ok' ) ,
68+ { wait : async ms => delays . push ( ms ) , warn ( ) { } } ) ;
69+ assert . deepEqual ( delays , [ expectedDelay ] ) ;
70+ }
71+ const certificateError = new TypeError ( 'fetch failed' , { cause : Object . assign ( new Error ( 'certificate expired' ) , { code : 'CERT_HAS_EXPIRED' } ) } ) ;
72+ for ( const failure of [ new Response ( null , { status : 403 } ) , new Response ( null , { status : 404 } ) , certificateError ] ) {
73+ let calls = 0 ;
74+ await assert . rejects ( downloadMcodeToolsArtifact ( async ( ) => {
75+ calls ++ ;
76+ if ( failure instanceof Error ) throw failure ;
77+ return failure ;
78+ } , { wait : async ( ) => assert . fail ( 'permanent failures must not wait' ) , warn : ( ) => assert . fail ( 'permanent failures must not retry' ) } ) ) ;
79+ assert . equal ( calls , 1 ) ;
80+ }
81+ } ) ;
82+
83+ test ( 'artifact download stops after three timeouts and preserves the final cause' , async ( ) => {
84+ const timeout = new DOMException ( 'The operation timed out' , 'TimeoutError' ) ;
85+ let calls = 0 ;
86+ const delays = [ ] ;
87+ await assert . rejects ( downloadMcodeToolsArtifact ( async ( ) => { calls ++ ; throw timeout ; } ,
88+ { wait : async ms => delays . push ( ms ) , warn ( ) { } } ) , error => {
89+ assert . match ( error . message , / a f t e r 3 a t t e m p t s / ) ;
90+ assert . equal ( error . cause , timeout ) ;
91+ return true ;
92+ } ) ;
93+ assert . equal ( calls , 3 ) ;
94+ assert . deepEqual ( delays , [ 1000 , 2000 ] ) ;
95+ } ) ;
96+
97+ test ( 'artifact download never retries or caches an archive that fails integrity' , async t => {
98+ const root = mkdtempSync ( path . join ( tmpdir ( ) , 'mcode-artifact-download-' ) ) ;
99+ t . after ( ( ) => rmSync ( root , { recursive : true , force : true } ) ) ;
100+ let calls = 0 ;
101+ await assert . rejects ( copyMcodeToolsArtifact ( root , path . join ( root , 'dist' ) , async ( ) => {
102+ calls ++ ;
103+ return new Response ( 'corrupt archive' ) ;
104+ } ) , / i n t e g r i t y m i s m a t c h / ) ;
105+ assert . equal ( calls , 1 ) ;
106+ assert . equal ( existsSync ( path . join ( root , '.cache' , 'artifacts' , 'code-0.3.11.tgz' ) ) , false ) ;
107+ assert . equal ( existsSync ( path . join ( root , 'dist' ) ) , false ) ;
108+ } ) ;
22109
23110test ( 'performance defaults to the 100-round suite; long history requires explicit selection' , ( ) => {
24111 const config = JSON . parse ( readFileSync ( new URL ( '../scripts/perf/config.json' , import . meta. url ) , 'utf8' ) ) ;
0 commit comments