@suffolklitlab/alkiln 5.12.0 → 5.13.0-logs2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.env.example +29 -0
  2. package/.github/workflows/github_server.yml +59 -12
  3. package/.github/workflows/playground.yml +11 -4
  4. package/CHANGELOG.md +41 -1
  5. package/CONTRIBUTING.md +98 -83
  6. package/README.md +1 -1
  7. package/action.yml +69 -29
  8. package/action_for_github_server/action.yml +65 -31
  9. package/docs/decisions/006_which_log_level_to_pick_2024_09_5.md +46 -0
  10. package/docs/decisions/adopt_architectural_decision_records.md +58 -0
  11. package/docs/decisions/detect_sign_in.md +124 -0
  12. package/docs/decisions/pdfjs_version_2024_08_08.md +18 -0
  13. package/docs/decisions/reports_and_logs_2024_08_11.md +141 -0
  14. package/docs/decisions/update_puppeteer_2024_07_28.md +62 -0
  15. package/docs/story_tables.md +1 -1
  16. package/lib/docassemble/docassemble_api_REST.js +67 -1
  17. package/lib/docassemble/docassemble_api_interface.js +225 -18
  18. package/lib/docassemble/server_install.js +23 -9
  19. package/lib/docassemble/setup.js +33 -20
  20. package/lib/docassemble/takedown.js +27 -7
  21. package/lib/run_cucumber.js +83 -33
  22. package/lib/scope.js +222 -82
  23. package/lib/steps.js +206 -62
  24. package/lib/utils/files.js +8 -6
  25. package/lib/utils/log.js +477 -84
  26. package/lib/utils/reports.js +14 -5
  27. package/lib/utils/session_vars.js +25 -18
  28. package/lib/utils/time.js +44 -0
  29. package/npm-shrinkwrap.json +635 -193
  30. package/package.json +8 -8
  31. package/setup.py +1 -1
  32. package/tests/log_codes/check_codes.sh +163 -108
  33. package/tests/log_codes/log_code_expected_instances.txt +213 -0
  34. package/tests/unit_tests/getAllFields.test.js +5 -0
  35. package/tests/unit_tests/getMatchingRows.test.js +5 -0
  36. package/tests/unit_tests/log.test.js +20 -0
  37. package/tests/unit_tests/normalizeTable.test.js +5 -0
  38. package/tests/log_codes/deleted_codes.json +0 -8
package/lib/steps.js CHANGED
@@ -5,6 +5,7 @@ const {
5
5
  Given,
6
6
  After,
7
7
  Before,
8
+ AfterStep,
8
9
  BeforeAll,
9
10
  AfterAll,
10
11
  setDefaultTimeout //,
@@ -18,10 +19,11 @@ const { v4: uuidv4 } = require('uuid');
18
19
 
19
20
  // Ours
20
21
  const scope = require('./scope');
21
- const log = require('./utils/log');
22
+ const Log = require(`./utils/log`);
22
23
  const session_vars = require('./utils/session_vars');
23
24
  const files = require('./utils/files' );
24
25
  const reports = require(`./utils/reports`);
26
+ const da_api = require('./docassemble/docassemble_api_interface');
25
27
 
26
28
  /* Of Note:
27
29
  - We're using `*=` for selectors because sometimes da text has funny characters in it that are hard to anticipate
@@ -81,8 +83,13 @@ let click_with = {
81
83
  pc: 'click',
82
84
  };
83
85
 
84
- BeforeAll(async () => {
86
+ let log = null;
87
+
88
+ BeforeAll(async function() {
85
89
  scope.showif_timeout = 600;
90
+ // TODO: Do these really need to be maps? As sets, they may make the output
91
+ // more predictable to the user, but they add complexity.
92
+ scope.scenarios = new Map();
86
93
  scope.report = new Map();
87
94
 
88
95
  // Start tracking whether server is up and running
@@ -91,29 +98,29 @@ BeforeAll(async () => {
91
98
  // Store path names for files and docs created for the tests
92
99
  scope.paths = {};
93
100
 
94
- // TODO: figure out how to test file and folder creation stuff,
95
- // especially when setup is excluded from the flow.
96
-
97
- // Make the folder to store all the files for this run of the tests
98
- scope.paths.artifacts = files.make_artifacts_folder();
99
- // Save its name in a file. `session_vars` can get it back later.
100
- // The value we are saving is used by at least run_cucumber.js.
101
- session_vars.save_artifacts_path_name( scope.paths.artifacts );
101
+ // // Make the folder to store all the files for this run of the tests
102
+ // scope.paths.artifacts = files.make_artifacts_folder();
103
+ // // Save its name in a file. `session_vars` can get it back later.
104
+ // // The value we are saving is used by at least run_cucumber.js.
105
+
106
+ // A little indirect, but the artifacts path should always have already been
107
+ // created by now, either by `run_cucumber.js` or the relevant test
108
+ scope.paths.artifacts = session_vars.get_artifacts_path_name();
109
+ log = new Log({ path: scope.paths.artifacts, context: `steps` });
110
+ scope.set_log({ log_obj: log });
102
111
 
103
- scope.paths.report = `${ scope.paths.artifacts }/report.txt`;
112
+ scope.paths.debug_log = `${ scope.paths.artifacts }/${ log.debug_log_filename }`;
113
+ scope.paths.report_log = `${ scope.paths.artifacts }/${ log.report_log_filename }`;
114
+ scope.paths.report = `${ scope.paths.artifacts }/${ log.report_filename }`;
104
115
  scope.paths.failures = `${ scope.paths.artifacts }/${ log.unexpected_filename }`;
105
- scope.paths.debug_log = `${ scope.paths.artifacts }/${ log.debug_log_file }`;
106
- fs.writeFileSync( scope.paths.debug_log, "" );
107
116
 
108
117
  reports.create( scope );
109
118
  });
110
119
 
111
120
  Before(async (scenario) => {
112
121
 
113
- // Useful for those with a lot of tests
114
- // Is unable to print the first Scenario name for some reason
115
- console.log('');
116
- process.stdout.write(`Scenario: ${ scenario.pickle.name }: `);
122
+ // Start the running "progress bar" for the Scenario
123
+ log.stdout({}, `\nScenario: ${ scenario.pickle.name }: `);
117
124
 
118
125
  // Create browser, which can't be created in BeforeAll() where .driver doesn't exist for some reason
119
126
  if (!scope.browser) {
@@ -127,13 +134,16 @@ Before(async (scenario) => {
127
134
  }
128
135
  }
129
136
  // Clean up all previously existing pages
130
- for (const page of await scope.browser.pages()) {
137
+ for (const page of await scope.browser.pages()) {
131
138
  await page.close();
132
139
  }
133
140
  // Make a new page
134
141
  scope.page = await scope.browser.newPage()
135
142
 
136
143
  scope.scenario_id = uuidv4();
144
+ scope.scenarios.set( scope.scenario_id, {} );
145
+ scope.scenarios.get( scope.scenario_id ).interviews = {};
146
+ scope.scenarios.get( scope.scenario_id ).api_keys = [];
137
147
  scope.expected_status = null;
138
148
  scope.expected_in_report = null;
139
149
  scope.expected_not_in_report = null;
@@ -179,7 +189,7 @@ Given(/I start the interview at "([^"]+)"/, {timeout: -1}, async (file_name) =>
179
189
 
180
190
  // Load the right interview page
181
191
  await scope.load( scope, file_name );
182
-
192
+
183
193
  // The page timeout is set in here too in case someone set a custom timeout
184
194
  // just before a page was loaded for the Scenario. Move this to just after page creation?
185
195
  // The function itself won't really timeout here, so we don't need to wrap it in
@@ -274,21 +284,27 @@ Given(/I go to "([^"]+)"/i, {timeout: -1}, async ( url ) => {
274
284
  return result;
275
285
  });
276
286
 
277
- Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^"]+)" and(?: the password)? "([^"]+)"(?: SECRETs)?/i, {timeout: -1}, async ( email, password ) => {
287
+ Given(
288
+ /I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^"]+)",?(?: and)?(?: the password)? "([^"]+)"(?: SECRETs)?(?:,?(?: and)?(?: the API key)? "([^"]+)")?/i,
289
+ { timeout: -1 },
290
+ async ( email, password, api_key ) => {
278
291
  /** Uses the names of environment variables (most often GitHub SECRETs) to
279
- * log into an account on the user's server. Must be SECRETs to stay secure.
292
+ * log into an account on the user's server. Must be secure.
280
293
  *
281
294
  * Variations:
282
- * I sign into the server with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
283
- * I sign on with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
284
- * I log in with "USER1_EMAIL" and "USER1_PASSWORD"
295
+ * I sign into the server with the email "USER2_EMAIL" and the password "USER2_PASSWORD"
296
+ * I sign on with the email "USER2_EMAIL" and the password "USER2_PASSWORD"
297
+ * I log in with "USER2_EMAIL" and "USER2_PASSWORD"
298
+ * I sign in with the email "USER2_EMAIL", the password "USER2_PASSWORD", and the API key "USER2_API_KEY"
299
+ * I sign in with "USER2_EMAIL", "USER2_PASSWORD", "USER2_API_KEY"
285
300
  */
286
301
  // `page` timeout will deal with custom timeout
287
302
  // Couldn't test the timeout for tapping the button because there's not
288
303
  // enough of a pause between initial navigation and pressing button.
289
304
  await scope.steps.sign_in( scope, {
290
305
  email_secret_name: email,
291
- password_secret_name: password
306
+ password_secret_name: password,
307
+ api_key_secret_name: api_key
292
308
  });
293
309
  });
294
310
 
@@ -313,7 +329,7 @@ Given(/the max(?:imum)? sec(?:ond)?s for each step (?:in this scenario )?is (\d+
313
329
  /* At any time, set a custom timeout for the scenario. */
314
330
  // TODO: Add 1 second to this to allow other steps to not fail, like "I wait n seconds"
315
331
  scope.timeout = parseInt( seconds ) * 1000;
316
-
332
+
317
333
  // Cucumber timeouts are made custom for each step that needs it
318
334
 
319
335
  // If there is no page yet, we've ensured custom timeout will still be set
@@ -383,11 +399,9 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
383
399
  from_story_table: true,
384
400
  });
385
401
 
386
- log.debug({
387
- type: `info`,
388
- code: `ALK0061`,
389
- pre: `Rows, including special rows:, ${JSON.stringify( ensured_var_data )}`,
390
- });
402
+ log.debug({ code: `ALK0061`, context: `table`, },
403
+ `Rows, including special rows:`, ensured_var_data
404
+ );
391
405
 
392
406
  // THIS FUNCTION ACTUALLY MATTERS
393
407
  // Some errors happen in there to be closer to the relevant code. We'd have to try/catch anyway.
@@ -637,7 +651,7 @@ Then('I arrive at the next page', async () => { // √
637
651
  // Link observations have the 'Escape' button link in mind
638
652
  Then('I should see the link {string}', async ( linkText ) => { // √
639
653
  /* Loosely match link visible text. Should probably deprecate this. */
640
- let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
654
+ let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${linkText}")]`);
641
655
 
642
656
  let msg = `Cannot find a link with the text "${ linkText }".`;
643
657
  if ( !link ) { reports.addToReport(scope, { type: `error`, code: `ALK0079`, value: msg }); }
@@ -653,16 +667,16 @@ Then('I should see the link to {string}', async ( url ) => { // √
653
667
  let msg = `Cannot find a link to "${ url }".`;
654
668
  if ( !link ) { reports.addToReport(scope, { type: `error`, code: `ALK0080`, value: msg }); }
655
669
  expect(link, msg ).to.exist;
656
-
670
+
657
671
  await scope.afterStep(scope);
658
672
  });
659
673
 
660
674
  Then(/the "([^"]+)" link leads to "([^"]+)"/, async (linkText, expected_url) => { // √
661
675
  /* Find link by its visible text and where it leads. */
662
- let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
676
+ let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${linkText}")]`);
663
677
  // TODO: Possibly add a check here for the link text, though note this
664
678
  // Step is not currently supported.
665
-
679
+
666
680
  let prop_obj = await link.getProperty('href');
667
681
  let actual_url = await prop_obj.jsonValue();
668
682
 
@@ -674,7 +688,7 @@ Then(/the "([^"]+)" link leads to "([^"]+)"/, async (linkText, expected_url) =>
674
688
  });
675
689
 
676
690
  Then(/the "([^"]+)" link opens in (a new window|the same window)/, async (linkText, which_window) => { // √
677
- let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
691
+ let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${linkText}")]`);
678
692
 
679
693
  let prop_obj = await link.getProperty('target');
680
694
  let target = await prop_obj.jsonValue();
@@ -756,7 +770,7 @@ Then(/I expect the baseline PDF "([^"]+)" and the new PDF "([^"]+)" to be the sa
756
770
  * Variations:
757
771
  * Then I expect the baseline PDF "simple-doc-Baseline.pdf" and the new PDF "simple-doc.pdf" to be the same
758
772
  */
759
-
773
+
760
774
  return await wrapPromiseWithTimeout(
761
775
  scope.steps.compare_pdfs(scope, {existing_pdf_path, new_pdf_path}),
762
776
  scope.timeout
@@ -880,7 +894,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
880
894
 
881
895
  // Set up timeout to stop infinite loops
882
896
  let page_timeout_id = setTimeout(async function() {
883
-
897
+
884
898
  // Make sure to only throw once
885
899
  if ( has_thrown ) { return; }
886
900
  else { has_thrown = true; }
@@ -959,7 +973,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
959
973
  } // ends if !has_content
960
974
 
961
975
  if ( buttons_exist ) {
962
- process.stdout.write(`\x1b[36m${ 'v' }\x1b[0m`); // pressed button
976
+ log.stdout({}, `\x1b[36m${ 'v' }\x1b[0m`); // pressed button
963
977
  }
964
978
 
965
979
  resolve();
@@ -1178,6 +1192,71 @@ Given(/the final Scenario status should be "([^"]+)"/i, async ( expected_status
1178
1192
  scope.expected_status = expected_status.toUpperCase();
1179
1193
  });
1180
1194
 
1195
+ Then(/of two interviews, I delete one __internal__/i, async () => {
1196
+ /** Test that an interview does get successfully deleted. This tests whether
1197
+ * the API code is working correctly.
1198
+ */
1199
+ // Make sure we have two interviews in `scope.scenario.interviews` (two session ids)
1200
+ let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
1201
+ let interview_keys = Object.keys( interviews );
1202
+ expect(
1203
+ interview_keys.length,
1204
+ `Of 2 interview sessions, ALKiln only saved ${ interview_keys.length }`
1205
+ ).to.equal( 2 );
1206
+
1207
+ // Check that both interviews exist on the server. 1 √ 2 √
1208
+ let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
1209
+ let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
1210
+ let visible_keys = Object.keys( visible_interviews );
1211
+ expect(
1212
+ visible_keys.length,
1213
+ `2 interviews should be on the server. Instead: "${ visible_keys.join(', ') }"`
1214
+ ).to.equal( 2 );
1215
+
1216
+ // Delete one. 1 x 2 √
1217
+ let to_delete = interviews[ interview_keys[ 0 ]];
1218
+ await da_api.quietly_try_to_delete_interviews({
1219
+ interviews: { [interview_keys[ 0 ]]: to_delete },
1220
+ api_keys
1221
+ });
1222
+
1223
+ // Check that only one session exists on the server. This tests that
1224
+ // 1. An interview has been deleted and
1225
+ // 2. We are fetching interviews correctly because we are getting one
1226
+ // back (e.g. not passing null into the api function)
1227
+ // 1 x 2 √
1228
+ let after_deletion = await da_api.get_interviews({ interviews, api_keys });
1229
+ let keys_after_deletion = Object.keys( after_deletion );
1230
+ expect(
1231
+ keys_after_deletion.length,
1232
+ `1 interviews should be on the server: ${ keys_after_deletion.join(', ') }`
1233
+ ).to.equal( 1 )
1234
+ // In After(), the other session should get deleted
1235
+ });
1236
+
1237
+ Then(/I fail to delete (\d) detected interview(?:s)? and get no error __internal__/i, async ( num ) => {
1238
+ /** Test that there is no error when ALKiln is unable to delete an interview. */
1239
+ num = parseInt( num );
1240
+ // Make sure we started an interview
1241
+ let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
1242
+ expect(
1243
+ Object.keys( interviews ).length,
1244
+ `The test should have saved ${ num } interview session(s)`
1245
+ ).to.equal( num );
1246
+
1247
+ // There should be no error about an unauthorized user
1248
+ let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
1249
+ let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
1250
+ let visible_keys = Object.keys( visible_interviews );
1251
+ expect(
1252
+ visible_keys.length,
1253
+ `User should detect 0 interviews on the server. Instead: "${ visible_keys.join(', ') }"`
1254
+ ).to.equal( 0 );
1255
+
1256
+ // Deleting with an anonymous user should fail silently
1257
+ await da_api.quietly_try_to_delete_interviews({ interviews, api_keys });
1258
+ });
1259
+
1181
1260
 
1182
1261
  //#####################################
1183
1262
  //#####################################
@@ -1185,11 +1264,22 @@ Given(/the final Scenario status should be "([^"]+)"/i, async ( expected_status
1185
1264
  //#####################################
1186
1265
  //#####################################
1187
1266
 
1267
+ AfterStep(async function({ result }) {
1268
+ // TODO: Abstract this
1269
+ if ( result.status === `PASSED` ) { log.stdout({ records_only: true }, `.`); }
1270
+ else if ( result.status === `FAILED` ) { log.stdout({ records_only: true }, `F`); }
1271
+ else if ( result.status === `UNDEFINED` ) { log.stdout({ records_only: true }, `U`); }
1272
+ else if ( result.status === `SKIPPED` ) { log.stdout({ records_only: true }, `-`); }
1273
+ else if ( result.status === `PENDING` ) { log.stdout({ records_only: true }, `P`); }
1274
+ });
1275
+
1188
1276
  After(async function(scenario) {
1189
1277
 
1190
1278
  // Log errors
1191
1279
  if ( scenario.result.message ) {
1192
- log.debug({ type: `info`, code: `ALK0091`, pre: `cucumber error: ${ scenario.result.message }` }); //, verbose: true });
1280
+ log.debug({ code: `ALK0091`, context: `scenario`, },
1281
+ scenario.result.message
1282
+ );
1193
1283
  }
1194
1284
 
1195
1285
  let changeable_test_status = scenario.result.status;
@@ -1218,11 +1308,9 @@ After(async function(scenario) {
1218
1308
  reports.saveFinalScenarioData( scope, { status: changeable_test_status });
1219
1309
 
1220
1310
  if (changeable_test_status !== `PASSED`) {
1221
- log.debug({
1222
- type: `error`,
1223
- code: `ALK0094`,
1224
- pre: `Non-passed status occurred while running test: ${changeable_test_status}`,
1225
- });
1311
+ log.debug({ code: `ALK0094`, level: `warn`, },
1312
+ `Non-passed status occurred while running test: ${ changeable_test_status }`
1313
+ );
1226
1314
 
1227
1315
  if ( scope.page ) {
1228
1316
 
@@ -1314,12 +1402,15 @@ After(async function(scenario) {
1314
1402
  // it as failed too.
1315
1403
  expected_status_was_met = false;
1316
1404
  // Keep it general. Users may see this message, e.g. for a11y tests.
1317
- let msg = `Test failed. Expected status: ${scope.expected_status}. `
1318
- + `ALKiln's assessed status: ${ changeable_test_status }. `
1405
+ let msg = `Test ${ changeable_test_status }. `
1406
+ + `Expected status: ${scope.expected_status}. `
1319
1407
  + `See the report for details.`;
1320
1408
  let code = `ALK0099`;
1321
- log.debug({ type: `error`, code: code, pre: msg, })
1322
- expected_status_err_msg = `🤕 ${ code } ERROR: ${ msg }`
1409
+ expected_status_err_msg = log.debug({ code: code, level: `error`, },
1410
+ msg
1411
+ );
1412
+ // Temp for debugging new log output
1413
+ expect( expected_status_err_msg ).to.equal(`🤕 ${ code } ERROR: ${ msg }`);
1323
1414
  }
1324
1415
  }
1325
1416
  scope.expected_status = null; // reset for next Scenario
@@ -1339,17 +1430,25 @@ After(async function(scenario) {
1339
1430
  // Make sure that tests which ALKiln has calculated as failed
1340
1431
  // cause a cucumber error later. Just changing the status to `FAILED`
1341
1432
  // causes unhelpful cucumber errors. TODO: Add to internal docs.
1342
- log.debug({
1343
- type: `error`,
1344
- code: force_failure_code,
1345
- pre: force_failure_msg
1346
- });
1433
+ // TODO: Should we move this to where the error is thrown?
1434
+ log.debug({ code: force_failure_code, level: `error`, },
1435
+ force_failure_msg
1436
+ );
1347
1437
  // TODO: Look into setDefinitionFunctionWrapper to catch errors
1348
1438
  // of failing tests that should indeed be failing:
1349
1439
  // https://github.com/cucumber/cucumber-js/blob/main/docs/support_files/api_reference.md#setdefinitionfunctionwrapperwrapper
1350
1440
  need_to_force_failed_status = true;
1351
1441
  }
1352
1442
 
1443
+ // Not sure if this is necessary
1444
+ if ( changeable_test_status === 'PASSED' ) {
1445
+ log.stdout({ records_only: true }, `.` );
1446
+ } else if ( changeable_test_status === `SKIPPED` ) {
1447
+ log.stdout({ records_only: true }, `-` );
1448
+ } else {
1449
+ log.stdout({ records_only: true }, changeable_test_status[0].toUpperCase() );
1450
+ }
1451
+
1353
1452
  let signout_succeeded = true;
1354
1453
  let signout_error_msg = ``;
1355
1454
  // If there is a page open, then sign out and close it
@@ -1372,7 +1471,7 @@ After(async function(scenario) {
1372
1471
  code: code,
1373
1472
  message: non_reload_report_msg,
1374
1473
  }, error });
1375
- } catch {
1474
+ } catch ( error ) {
1376
1475
  signout_error_msg = `🤕 ${ code } ERROR: ${ non_reload_report_msg }`;
1377
1476
  }
1378
1477
  } else {
@@ -1387,21 +1486,66 @@ After(async function(scenario) {
1387
1486
 
1388
1487
  } // ends if scope.page
1389
1488
 
1390
- let errors = []
1391
- if ( !expected_status_was_met ) { errors.push( expected_status_err_msg ); }
1392
- if ( !signout_succeeded ) { errors.push( signout_error_msg ); }
1393
- if ( !expected_status_was_met || !signout_succeeded ) {
1394
- throw new Error( `${errors.join( errors, '\n')}` );
1489
+ // Now that we're no longer on the interview page, one way or
1490
+ // another, try to delete the interviews created during the test
1491
+ // Authors running this manually get to choose what to do with
1492
+ // the interviews they're creating unless we hear otherwise.
1493
+ if ( session_vars.get_origin() !== `playground` ) {
1494
+ try {
1495
+ let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
1496
+ let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
1497
+ await da_api.quietly_try_to_delete_interviews({ interviews, api_keys });
1498
+ let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
1499
+ let visible_keys = Object.keys( visible_interviews );
1500
+ // Deleting interviews is nice, but not non-critical. Don't bother the
1501
+ // user with it. Don't want to cover up a more meaningful failure (like
1502
+ // going to the wrong file). If it becomes a problem we can track it down later.
1503
+ if ( visible_keys.length > 0 ) {
1504
+ // Can an API key ever see an interview that it can't delete?
1505
+ log.debug({ code: `ALK0197`, context: `internal`, level: `warn`, }
1506
+ `The API key can see ${ visible_keys.length } interviews that were not deleted:`,
1507
+ visible_keys
1508
+ );
1509
+ }
1510
+ } catch ( error ) {
1511
+ // Deleting interviews is nice, but not non-critical. Don't bother the
1512
+ // user with it. Don't want to cover up a more meaningful failure (like
1513
+ // going to the wrong file). Example of OK failure: the user owns the
1514
+ // interview is different than the API key that's trying to delete the
1515
+ // interview. If it becomes a problem we can track it down later.
1516
+ // Consider add "internal" to docs: "If you see this, make an
1517
+ // issue in ALKiln with your debug/verbose log"
1518
+ log.debug({ code: `ALK0198`, context: `internal`, level: `warn`, error },
1519
+ `Cleaning up interviews incomplete, but unimportant.`
1520
+ );
1521
+ }
1395
1522
  }
1396
1523
 
1524
+ // Collect all the relevant special errors
1525
+ let internal_errors = []
1526
+ if ( !expected_status_was_met ) { internal_errors.push( expected_status_err_msg ); }
1527
+ if ( !signout_succeeded ) { internal_errors.push( signout_error_msg ); }
1528
+
1397
1529
  // Theoretically, we should never get here:
1398
1530
  // If everything else worked, if we need to force an internal test failure
1399
1531
  if ( need_to_force_failed_status ) {
1400
- throw new Error( `🤕 ${force_failure_code} unreachable ERROR: ${force_failure_msg}` );
1532
+ if ( internal_errors.length > 0 ) {
1533
+ // Handle all the accumulated errors
1534
+ throw new Error( `${internal_errors.join( internal_errors, '\n')}` );
1535
+ }
1536
+ throw new Error( `🤕 ${ force_failure_code } unreachable ERROR: ${ force_failure_msg }` );
1537
+
1538
+ } else if ( internal_errors.length > 0 ) {
1539
+ log.debug({ code: `ALK0203`, context: `internal`, level: `warn`, },
1540
+ `There was no need to force failed status and yet there are error messages.`,
1541
+ internal_errors
1542
+ );
1401
1543
  }
1402
1544
 
1403
1545
  // Find the race condition.
1404
- log.debug({ type: `cucumber`, code: `ALK0100`, verbose: true, pre: `Scenario AfterStep() message`, data: scenario.result.message });
1546
+ log.debug({ code: `ALK0100`, context: `scenario`, },
1547
+ `Scenario After() message:`, scenario.result.message
1548
+ );
1405
1549
 
1406
1550
  });
1407
1551
 
@@ -22,18 +22,20 @@ files.readable_date = function() {
22
22
  }; // Ends files.readable_date()
23
23
 
24
24
  files.make_artifacts_folder = function (folder_name) {
25
- /** Makes a folder in the root of the project, either with the given name
26
- * or with a default name.
27
- *
28
- * Works for local development as well, unlike action.yml or the workflow file.
29
- */
25
+ /** Makes a folder in the root of the project if it doesn't
26
+ * already exist, either with the given name or with a default name.
27
+ *
28
+ * Works for local development as well, without action.yml or workflow files.
29
+ * */
30
30
  if (folder_name) {
31
31
  folder_name = safe_filename( folder_name );
32
32
  } else {
33
33
  folder_name = `alkiln-${ files.readable_date() }`
34
34
  }
35
35
 
36
- fs.mkdirSync( folder_name );
36
+ // `recursive` avoids errors if folder already exists
37
+ // https://nodejs.org/docs/latest-v18.x/api/fs.html#fspromisesmkdirpath-options
38
+ fs.mkdirSync( folder_name, { recursive: true });
37
39
  return folder_name;
38
40
  }; // Ends files.make_artifacts_folder()
39
41