1+ // SPDX-FileCopyrightText: Max Health Inc.
2+ // SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Commercial
3+
14/**
25 * Inferno SMART App Launch STU2.2 Compliance Test Runner
36 *
@@ -535,8 +538,7 @@ async function runStandaloneLaunchTests(sessionId, browser) {
535538 return await waitForSimpleTestCompletion ( sessionId , run . id , browser ) ;
536539
537540 } catch ( error ) {
538- console . error ( 'Standalone Launch tests error:' , error . message ) ;
539- throw error ;
541+ return groupFailure ( 'Standalone Launch' , error ) ;
540542 }
541543}
542544
@@ -571,10 +573,7 @@ async function runTokenIntrospectionTests(sessionId, browser) {
571573 return await waitForSimpleTestCompletion ( sessionId , run . id , browser ) ;
572574
573575 } catch ( error ) {
574- console . error ( 'Token Introspection tests error:' , error . message ) ;
575- // Don't throw — Token Introspection failure shouldn't block the pipeline
576- console . log ( 'WARNING: Token Introspection tests failed but continuing...' ) ;
577- return null ;
576+ return groupFailure ( 'Token Introspection' , error ) ;
578577 }
579578}
580579
@@ -669,9 +668,7 @@ async function runEhrLaunchTests(sessionId, browser) {
669668 return await waitForEhrLaunchCompletion ( sessionId , run . id , browser ) ;
670669
671670 } catch ( error ) {
672- console . error ( 'EHR Launch tests error:' , error . message ) ;
673- console . log ( 'WARNING: EHR Launch tests failed but continuing...' ) ;
674- return null ;
671+ return groupFailure ( 'EHR Launch' , error ) ;
675672 }
676673}
677674
@@ -856,10 +853,7 @@ async function runBackendServicesTests(sessionId) {
856853 return await waitForSimpleTestCompletion ( sessionId , run . id ) ;
857854
858855 } catch ( error ) {
859- console . error ( 'Backend Services tests error:' , error . message ) ;
860- // Don't throw — Backend Services failure shouldn't block the pipeline yet
861- console . log ( 'WARNING: Backend Services tests failed but continuing...' ) ;
862- return null ;
856+ return groupFailure ( 'Backend Services' , error ) ;
863857 }
864858}
865859
@@ -1009,6 +1003,17 @@ async function getSessionResults(sessionId) {
10091003 return response . json ( ) ;
10101004}
10111005
1006+ /**
1007+ * Record a group that blew up, so the run can report every group's outcome and
1008+ * still fail. Three of the four groups used to log "WARNING: ... but continuing"
1009+ * and return null that nothing inspected, so a run reported success with most of
1010+ * the suite never executed; the fourth threw, which aborted the groups after it.
1011+ */
1012+ function groupFailure ( group , error ) {
1013+ console . error ( `${ group } tests error:` , error . message ) ;
1014+ return { group, ok : false , error : error . message } ;
1015+ }
1016+
10121017async function printResults ( results ) {
10131018 console . log ( '\n========================================' ) ;
10141019 console . log ( ' TEST RESULTS SUMMARY ' ) ;
@@ -1018,7 +1023,17 @@ async function printResults(results) {
10181023 let failed = 0 ;
10191024 let skipped = 0 ;
10201025 let errors = 0 ;
1021-
1026+ // 'omit' is Inferno declining a test that does not apply here (the TLS tests
1027+ // in local non-TLS mode). Not a failure, but it must be counted: it used to
1028+ // fall through to the default branch, inflating `total` while landing in no
1029+ // bucket at all.
1030+ let omitted = 0 ;
1031+ // A test that never reached a verdict: still 'wait'ing for a user action the
1032+ // automation never completed, or cancelled. These are the ones that made the
1033+ // job green while a launch flow was broken.
1034+ let incomplete = 0 ;
1035+ const incompleteTests = [ ] ;
1036+
10221037 // Collect detailed failure info for later
10231038 const failedTests = [ ] ;
10241039
@@ -1068,14 +1083,43 @@ async function printResults(results) {
10681083 } ) ;
10691084 }
10701085 break ;
1086+ case 'omit' :
1087+ omitted ++ ;
1088+ console . log ( `− OMIT: ${ title } ` ) ;
1089+ if ( result . result_message ) {
1090+ console . log ( ` Reason: ${ result . result_message . substring ( 0 , 120 ) } ` ) ;
1091+ }
1092+ break ;
1093+ case 'wait' :
1094+ case 'cancel' :
1095+ incomplete ++ ;
1096+ incompleteTests . push ( { title, status, test_id : result . test_id } ) ;
1097+ console . log ( `⧗ INCOMPLETE (${ status } ): ${ title } ` ) ;
1098+ break ;
10711099 default :
1072- console . log ( `? ${ status . toUpperCase ( ) } : ${ title } ` ) ;
1100+ // An unrecognised status is counted as incomplete rather than ignored:
1101+ // a new Inferno state must not be able to pass the gate by being unknown.
1102+ incomplete ++ ;
1103+ incompleteTests . push ( { title, status, test_id : result . test_id } ) ;
1104+ console . log ( `⧗ INCOMPLETE (unrecognised status "${ status } "): ${ title } ` ) ;
10731105 }
10741106 }
10751107
10761108 console . log ( '\n========================================' ) ;
1077- console . log ( `Total: ${ results . length } | Passed: ${ passed } | Failed: ${ failed } | Skipped: ${ skipped } | Errors: ${ errors } ` ) ;
1109+ console . log ( `Total: ${ results . length } | Passed: ${ passed } | Failed: ${ failed } | Incomplete: ${ incomplete } | Skipped: ${ skipped } | Omitted: ${ omitted } | Errors: ${ errors } ` ) ;
1110+ const accountedFor = passed + failed + skipped + errors + omitted + incomplete ;
1111+ if ( accountedFor !== results . length ) {
1112+ console . log ( `WARNING: ${ results . length - accountedFor } result(s) landed in no bucket — the tally is wrong.` ) ;
1113+ }
10781114 console . log ( '========================================\n' ) ;
1115+
1116+ if ( incompleteTests . length > 0 ) {
1117+ console . log ( 'The following tests never reached a verdict:' ) ;
1118+ for ( const t of incompleteTests ) {
1119+ console . log ( ` ⧗ [${ t . status } ] ${ t . title } ` ) ;
1120+ }
1121+ console . log ( '' ) ;
1122+ }
10791123
10801124 // Print detailed failure analysis
10811125 if ( failedTests . length > 0 ) {
@@ -1167,7 +1211,11 @@ async function printResults(results) {
11671211 }
11681212 }
11691213
1170- return { passed, failed, skipped, errors, total : results . length } ;
1214+ return {
1215+ passed, failed, skipped, errors, omitted, incomplete,
1216+ total : results . length ,
1217+ unaccounted : results . length - accountedFor ,
1218+ } ;
11711219}
11721220
11731221async function main ( ) {
@@ -1257,27 +1305,51 @@ async function main() {
12571305 // Get all results across all groups
12581306 const results = await getSessionResults ( session . id ) ;
12591307 const summary = await printResults ( results ) ;
1260-
1308+
1309+ const groupFailures = [ standaloneResult , introspectionResult , backendServicesResult , ehrLaunchResult ]
1310+ . filter ( r => r && r . ok === false ) ;
1311+
1312+ if ( groupFailures . length > 0 ) {
1313+ console . error ( '\nTest groups that did not run to completion:' ) ;
1314+ for ( const f of groupFailures ) {
1315+ console . error ( ` ✗ ${ f . group } : ${ f . error } ` ) ;
1316+ }
1317+ }
1318+
12611319 // Output for GitHub Actions
12621320 if ( process . env . GITHUB_OUTPUT ) {
12631321 const fs = require ( 'fs' ) ;
12641322 fs . appendFileSync ( process . env . GITHUB_OUTPUT , `passed=${ summary . passed } \n` ) ;
12651323 fs . appendFileSync ( process . env . GITHUB_OUTPUT , `failed=${ summary . failed } \n` ) ;
12661324 fs . appendFileSync ( process . env . GITHUB_OUTPUT , `total=${ summary . total } \n` ) ;
1325+ fs . appendFileSync ( process . env . GITHUB_OUTPUT , `incomplete=${ summary . incomplete } \n` ) ;
1326+ fs . appendFileSync ( process . env . GITHUB_OUTPUT , `omitted=${ summary . omitted } \n` ) ;
1327+ fs . appendFileSync ( process . env . GITHUB_OUTPUT , `group_failures=${ groupFailures . length } \n` ) ;
12671328 }
1268-
1269- // Exit with error if any tests failed OR no tests ran
1270- if ( summary . failed > 0 || summary . total === 0 ) {
1271- console . error ( `\n❌ Tests failed: ${ summary . total === 0 ? 'No tests completed' : `${ summary . failed } failed, ${ summary . errors } errors out of ${ summary . total } ` } ` ) ;
1329+
1330+ // A run is only a pass if every test reached a verdict and every group ran.
1331+ // Counting only `failed` let a green result hide tests still stuck in `wait`
1332+ // because their launch flow never completed.
1333+ const reasons = [ ] ;
1334+ if ( summary . total === 0 ) reasons . push ( 'no tests ran' ) ;
1335+ if ( summary . failed > 0 ) reasons . push ( `${ summary . failed } failed` ) ;
1336+ if ( summary . incomplete > 0 ) reasons . push ( `${ summary . incomplete } never reached a verdict` ) ;
1337+ if ( summary . unaccounted !== 0 ) reasons . push ( `${ summary . unaccounted } unaccounted for` ) ;
1338+ if ( groupFailures . length > 0 ) {
1339+ reasons . push ( `${ groupFailures . length } group(s) failed to run: ${ groupFailures . map ( f => f . group ) . join ( ', ' ) } ` ) ;
1340+ }
1341+
1342+ if ( reasons . length > 0 ) {
1343+ console . error ( `\n❌ Compliance run failed — ${ reasons . join ( '; ' ) } (out of ${ summary . total } tests, ${ summary . errors } errors)` ) ;
12721344 process . exit ( 1 ) ;
12731345 }
1274-
1346+
12751347 if ( summary . errors > 0 ) {
12761348 console . warn ( `\n⚠️ ${ summary . errors } test(s) had internal errors (not compliance failures) — ${ summary . passed } passed out of ${ summary . total } ` ) ;
12771349 }
1278-
1279- console . log ( `\n✅ All ${ summary . passed } tests passed!` ) ;
1280-
1350+
1351+ console . log ( `\n✅ All ${ summary . passed } tests passed${ summary . omitted > 0 ? ` ( ${ summary . omitted } omitted as not applicable)` : '' } !` ) ;
1352+
12811353 } catch ( error ) {
12821354 console . error ( 'Test execution failed:' , error . message ) ;
12831355 process . exit ( 1 ) ;
0 commit comments