@@ -46,6 +46,18 @@ export interface GenerateOptions {
4646 clientArgs ?: string [ ]
4747 /** Preamble lines defining the `$ELASTIC` invocation and `$RESPONSE` (default: the `elastic` binary). */
4848 preamble ?: string [ ]
49+ /**
50+ * If a `set` extraction is empty or `null`, try `.[0].id` (bare-array list
51+ * responses) and skip the script when still empty. Used by Cloud tests
52+ * against orgs that have no deployments or projects.
53+ */
54+ skipEmptySet ?: boolean
55+ /**
56+ * If a do-step exits non-zero and the JSON error mentions 404 (or a
57+ * hosted deployment with no version yet), skip the script. Used by Cloud
58+ * tests for endpoints the org or public QA API does not expose.
59+ */
60+ skipNotFound ?: boolean
4961}
5062
5163const DEFAULT_PREAMBLE = [ 'exec < /dev/null' , 'ELASTIC="elastic --json"' , 'RESPONSE=""' ]
@@ -57,6 +69,8 @@ export function generateScript (
5769) : GenerateResult {
5870 const clientArgs = opts . clientArgs ?? [ 'stack' , 'es' ]
5971 const preamble = opts . preamble ?? DEFAULT_PREAMBLE
72+ const skipEmptySet = opts . skipEmptySet === true
73+ const skipNotFound = opts . skipNotFound === true
6074 const actionMap = buildActionMap ( definitions )
6175 const skippedActions : string [ ] = [ ]
6276 const lines : string [ ] = [ ]
@@ -70,7 +84,7 @@ export function generateScript (
7084
7185 if ( testFile . teardown . length > 0 ) {
7286 const teardownBody : string [ ] = [ ]
73- renderSteps ( testFile . teardown , actionMap , clientArgs , teardownBody , skippedActions , ' ' )
87+ renderSteps ( testFile . teardown , actionMap , clientArgs , teardownBody , skippedActions , ' ' , false , skipEmptySet , skipNotFound )
7488 if ( ! hasExecutableLine ( teardownBody ) ) {
7589 teardownBody . push ( ' :' )
7690 }
@@ -98,13 +112,13 @@ export function generateScript (
98112
99113 if ( testFile . setup . length > 0 ) {
100114 lines . push ( '# --- Setup ---' )
101- hadSkippedDo = renderSteps ( testFile . setup , actionMap , clientArgs , lines , skippedActions , '' )
115+ hadSkippedDo = renderSteps ( testFile . setup , actionMap , clientArgs , lines , skippedActions , '' , false , skipEmptySet , skipNotFound )
102116 lines . push ( '' )
103117 }
104118
105119 for ( const section of testFile . tests ) {
106120 lines . push ( `# --- Test: ${ section . name } ---` )
107- renderSteps ( section . steps , actionMap , clientArgs , lines , skippedActions , '' , hadSkippedDo )
121+ renderSteps ( section . steps , actionMap , clientArgs , lines , skippedActions , '' , hadSkippedDo , skipEmptySet , skipNotFound )
108122 lines . push ( '' )
109123 }
110124
@@ -255,7 +269,9 @@ function renderSteps (
255269 lines : string [ ] ,
256270 skippedActions : string [ ] ,
257271 indent : string ,
258- initialHadSkippedDo = false
272+ initialHadSkippedDo = false ,
273+ skipEmptySet = false ,
274+ skipNotFound = false
259275) : boolean {
260276 // Assertions and set-steps read $RESPONSE, which is written by the most
261277 // recent successful `do`. If the last `do` was skipped (unmapped action,
@@ -279,7 +295,7 @@ function renderSteps (
279295 }
280296 // Only pass allowFailure from the setup→test propagation, not from
281297 // prior skipped steps within the same section (those are unrelated).
282- const result = renderDo ( step , actionMap , clientArgs , lines , skippedActions , indent , initialHadSkippedDo )
298+ const result = renderDo ( step , actionMap , clientArgs , lines , skippedActions , indent , initialHadSkippedDo , skipNotFound )
283299 if ( result === 'skipped' ) {
284300 responseFromLastDo = false
285301 hadSkippedDo = true
@@ -310,7 +326,7 @@ function renderSteps (
310326
311327 switch ( step . kind ) {
312328 case 'set' :
313- renderSet ( step , lines , indent )
329+ renderSet ( step , lines , indent , skipEmptySet )
314330 // If a variable was previously unset, it's now set
315331 for ( const varName of Object . values ( step . assignments ) ) {
316332 unsetVars . delete ( varName . toUpperCase ( ) . replace ( / [ ^ A - Z 0 - 9 ] / g, '_' ) )
@@ -360,7 +376,8 @@ function renderDo (
360376 lines : string [ ] ,
361377 skippedActions : string [ ] ,
362378 indent : string ,
363- allowFailure = false
379+ allowFailure = false ,
380+ skipNotFound = false
364381) : 'executed' | 'optional' | 'skipped' {
365382 if ( step . catch != null ) {
366383 lines . push ( `${ indent } # SKIPPED: catch not supported in MVP (catch: ${ step . catch } )` )
@@ -384,9 +401,25 @@ function renderDo (
384401 if ( optional ) {
385402 lines . push ( `${ indent } RESPONSE=$(${ cmd } ) || true` )
386403 return 'optional'
387- } else {
388- lines . push ( `${ indent } RESPONSE=$(${ cmd } )` )
389404 }
405+ if ( skipNotFound ) {
406+ // Handler errors go to stderr (`writeErr`); keep them out of $RESPONSE.
407+ const errFile = '"${TMPDIR:-/tmp}/elastic-cli-do-err.$$"'
408+ lines . push ( `${ indent } set +e` )
409+ lines . push ( `${ indent } RESPONSE=$(${ cmd } 2>${ errFile } )` )
410+ lines . push ( `${ indent } _ec=$?` )
411+ lines . push ( `${ indent } set -e` )
412+ lines . push ( `${ indent } if [ "$_ec" -ne 0 ]; then` )
413+ lines . push ( `${ indent } if jq -e '(.error.message | tostring | test("404|Could not determine the version"))' ${ errFile } >/dev/null 2>&1; then` )
414+ lines . push ( `${ indent } echo "SKIP: ${ step . action } not available"` )
415+ lines . push ( `${ indent } exit 0` )
416+ lines . push ( `${ indent } fi` )
417+ lines . push ( `${ indent } cat ${ errFile } >&2` )
418+ lines . push ( `${ indent } exit $_ec` )
419+ lines . push ( `${ indent } fi` )
420+ return 'executed'
421+ }
422+ lines . push ( `${ indent } RESPONSE=$(${ cmd } )` )
390423 return 'executed'
391424}
392425
@@ -510,11 +543,26 @@ function buildCommand (mapped: MappedAction, step: DoStep): string {
510543 return base
511544}
512545
513- function renderSet ( step : SetStep , lines : string [ ] , indent : string ) : void {
546+ function renderSet ( step : SetStep , lines : string [ ] , indent : string , skipEmptySet = false ) : void {
514547 for ( const [ responsePath , varName ] of Object . entries ( step . assignments ) ) {
515548 const bashVar = varName . toUpperCase ( ) . replace ( / [ ^ A - Z 0 - 9 ] / g, '_' )
516549 const jqPath = toJqPath ( responsePath )
517- lines . push ( `${ indent } ${ bashVar } =$(echo "$RESPONSE" | jq -r '${ jqPath } ')` )
550+ if ( skipEmptySet ) {
551+ // `try` so a nested path like `.regions[0].id` against a bare array
552+ // yields empty instead of aborting the script (jq cannot index an
553+ // array with a string).
554+ lines . push ( `${ indent } ${ bashVar } =$(echo "$RESPONSE" | jq -r 'try (${ jqPath } // empty) catch empty')` )
555+ lines . push ( `${ indent } if [ -z "$${ bashVar } " ] || [ "$${ bashVar } " = "null" ]; then` )
556+ // Serverless lists wrap items in `.items`; regions return a bare array.
557+ lines . push ( `${ indent } ${ bashVar } =$(echo "$RESPONSE" | jq -r 'if type=="array" then .[0].id // empty else .items[0].id // empty end')` )
558+ lines . push ( `${ indent } fi` )
559+ lines . push ( `${ indent } if [ -z "$${ bashVar } " ] || [ "$${ bashVar } " = "null" ]; then` )
560+ lines . push ( `${ indent } echo "SKIP: no ${ varName } in list response"` )
561+ lines . push ( `${ indent } exit 0` )
562+ lines . push ( `${ indent } fi` )
563+ } else {
564+ lines . push ( `${ indent } ${ bashVar } =$(echo "$RESPONSE" | jq -r '${ jqPath } ')` )
565+ }
518566 }
519567}
520568
0 commit comments