@loadstrike/loadstrike-sdk 1.0.10101 → 1.0.10801

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.
@@ -1,8 +1,8 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import os from "node:os";
4
4
  import { resolve } from "node:path";
5
- import { DEFAULT_LICENSING_API_BASE_URL, LoadStrikeLocalClient } from "./local.js";
5
+ import { LoadStrikeLocalClient } from "./local.js";
6
6
  import { DistributedClusterAgent, DistributedClusterCoordinator } from "./cluster.js";
7
7
  import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStore, RedisCorrelationStoreOptions, TrackingFieldSelector } from "./correlation.js";
8
8
  import { EndpointAdapterFactory } from "./transports.js";
@@ -85,42 +85,6 @@ export class LoadStrikePluginData {
85
85
  return this.tables;
86
86
  }
87
87
  }
88
- export class LoadStrikeReportData {
89
- constructor(scenarioStats = []) {
90
- if (!Array.isArray(scenarioStats) || scenarioStats.some((value) => !value || typeof value !== "object")) {
91
- throw new TypeError("Scenario stats cannot contain null values.");
92
- }
93
- this.scenarioStats = scenarioStats.map((value, index) => attachScenarioStatsAliases(normalizeScenarioStatsValue(value, index)));
94
- }
95
- static create(...scenarioStats) {
96
- return new LoadStrikeReportData(scenarioStats);
97
- }
98
- static Create(...scenarioStats) {
99
- return LoadStrikeReportData.create(...scenarioStats);
100
- }
101
- static fromRunResult(result) {
102
- return new LoadStrikeReportData((result?.scenarioStats ?? result?.ScenarioStats ?? []));
103
- }
104
- findScenarioStats(scenarioName) {
105
- return this.scenarioStats.find((value) => value.scenarioName === scenarioName);
106
- }
107
- getScenarioStats(scenarioName) {
108
- const value = this.findScenarioStats(scenarioName);
109
- if (!value) {
110
- throw new Error(`Scenario stats not found: ${scenarioName}`);
111
- }
112
- return value;
113
- }
114
- FindScenarioStats(scenarioName) {
115
- return this.findScenarioStats(scenarioName);
116
- }
117
- GetScenarioStats(scenarioName) {
118
- return this.getScenarioStats(scenarioName);
119
- }
120
- get ScenarioStats() {
121
- return this.scenarioStats;
122
- }
123
- }
124
88
  class MeasurementAccumulator {
125
89
  constructor() {
126
90
  this.count = 0;
@@ -731,28 +695,10 @@ export class LoadStrikeContext {
731
695
  .configureContext(this);
732
696
  return runner.run();
733
697
  }
734
- async runDetailed(...argsOrSingleArray) {
735
- const args = normalizeRunArgsInput(argsOrSingleArray);
736
- if (!this.registeredScenarios.length) {
737
- throw new Error("At least one scenario must be added before running the context.");
738
- }
739
- if (args.length) {
740
- this.mergeValues(extractContextOverridesFromArgs(args), args);
741
- }
742
- const runner = LoadStrikeRunner
743
- .create()
744
- .addScenarios(...this.registeredScenarios)
745
- .configureContext(this);
746
- return runner.runDetailed();
747
- }
748
698
  async Run(...argsOrSingleArray) {
749
699
  const args = normalizeRunArgsInput(argsOrSingleArray);
750
700
  return this.run(args);
751
701
  }
752
- async RunDetailed(...argsOrSingleArray) {
753
- const args = normalizeRunArgsInput(argsOrSingleArray);
754
- return this.runDetailed(args);
755
- }
756
702
  toRunnerOptions() {
757
703
  const normalizedValues = normalizeRunContextCollectionShapes(this.values);
758
704
  return {
@@ -766,7 +712,6 @@ export class LoadStrikeContext {
766
712
  coordinatorTargetScenarios: normalizedValues.CoordinatorTargetScenarios,
767
713
  natsServerUrl: normalizedValues.NatsServerUrl,
768
714
  runnerKey: normalizedValues.RunnerKey,
769
- licenseValidationServerUrl: normalizedValues.LicenseValidationServerUrl,
770
715
  licenseValidationTimeoutSeconds: normalizedValues.LicenseValidationTimeoutSeconds,
771
716
  configPath: normalizedValues.ConfigPath,
772
717
  infraConfigPath: normalizedValues.InfraConfigPath,
@@ -779,8 +724,6 @@ export class LoadStrikeContext {
779
724
  reportFileName: normalizedValues.ReportFileName,
780
725
  reportFolderPath: normalizedValues.ReportFolderPath,
781
726
  reportFormats: normalizedValues.ReportFormats,
782
- reportFinalizer: normalizedValues.ReportFinalizer,
783
- detailedReportFinalizer: normalizedValues.DetailedReportFinalizer,
784
727
  reportingIntervalSeconds: normalizedValues.ReportingIntervalSeconds,
785
728
  minimumLogLevel: normalizedValues.MinimumLogLevel,
786
729
  loggerConfig: normalizedValues.LoggerConfig,
@@ -788,6 +731,7 @@ export class LoadStrikeContext {
788
731
  sinkRetryCount: normalizedValues.SinkRetryCount,
789
732
  sinkRetryBackoffMs: normalizedValues.SinkRetryBackoffMs,
790
733
  runtimePolicies: normalizedValues.RuntimePolicies,
734
+ runtimePolicyErrorMode: normalizedRuntimePolicyErrorMode(normalizedValues.RuntimePolicyErrorMode),
791
735
  scenarioCompletionTimeoutSeconds: normalizedValues.ScenarioCompletionTimeoutSeconds,
792
736
  clusterCommandTimeoutSeconds: normalizedValues.ClusterCommandTimeoutSeconds,
793
737
  restartIterationMaxAttempts: normalizedValues.RestartIterationMaxAttempts,
@@ -833,28 +777,6 @@ export class LoadStrikeContext {
833
777
  ConfigureContext(context) {
834
778
  return this.configureContext(context);
835
779
  }
836
- buildLicenseValidationPayload() {
837
- const scenarios = this.registeredScenarios.map((scenario) => ({
838
- Name: scenario.name,
839
- MaxFailCount: scenario.getMaxFailCount(),
840
- RestartIterationOnFail: scenario.shouldRestartIterationOnFail(),
841
- WithoutWarmUp: scenario.isWithoutWarmUp(),
842
- WarmUpDurationSeconds: scenario.getWarmUpDurationSeconds(),
843
- Weight: scenario.getWeight(),
844
- LoadSimulations: [...scenario.getSimulations()],
845
- Thresholds: [...scenario.getThresholds()],
846
- Tracking: scenario.getTrackingConfiguration() ?? {},
847
- LicenseFeatures: scenario.getLicenseFeatures()
848
- }));
849
- return {
850
- Context: this.toObject(),
851
- Scenarios: scenarios,
852
- RunArgs: [...this.runArgs]
853
- };
854
- }
855
- BuildLicenseValidationPayload() {
856
- return this.buildLicenseValidationPayload();
857
- }
858
780
  displayConsoleMetrics(enable) {
859
781
  return this.DisplayConsoleMetrics(enable);
860
782
  }
@@ -928,12 +850,6 @@ export class LoadStrikeContext {
928
850
  WithRunnerKey(runnerKey) {
929
851
  return this.mergeValues({ RunnerKey: requireNonEmpty(runnerKey, "Runner key must be provided.") });
930
852
  }
931
- withLicenseValidationServerUrl(serverUrl) {
932
- return this.WithLicenseValidationServerUrl(serverUrl);
933
- }
934
- WithLicenseValidationServerUrl(serverUrl) {
935
- return this.mergeValues({ LicenseValidationServerUrl: requireNonEmpty(serverUrl, "License validation server URL must be provided.") });
936
- }
937
853
  withLicenseValidationTimeout(timeoutSeconds) {
938
854
  return this.WithLicenseValidationTimeout(timeoutSeconds);
939
855
  }
@@ -986,24 +902,6 @@ export class LoadStrikeContext {
986
902
  }
987
903
  return this.mergeValues({ ReportFormats: normalized });
988
904
  }
989
- withReportFinalizer(handler) {
990
- return this.WithReportFinalizer(handler);
991
- }
992
- WithReportFinalizer(handler) {
993
- if (typeof handler !== "function") {
994
- throw new TypeError("Report finalizer must be provided.");
995
- }
996
- return this.mergeValues({ ReportFinalizer: handler });
997
- }
998
- withDetailedReportFinalizer(handler) {
999
- return this.WithDetailedReportFinalizer(handler);
1000
- }
1001
- WithDetailedReportFinalizer(handler) {
1002
- if (typeof handler !== "function") {
1003
- throw new TypeError("Detailed report finalizer must be provided.");
1004
- }
1005
- return this.mergeValues({ DetailedReportFinalizer: handler });
1006
- }
1007
905
  withReportingInterval(intervalSeconds) {
1008
906
  return this.WithReportingInterval(intervalSeconds);
1009
907
  }
@@ -1026,6 +924,26 @@ export class LoadStrikeContext {
1026
924
  validateNamedReportingSinks(sinks);
1027
925
  return this.mergeValues({ ReportingSinks: sinks });
1028
926
  }
927
+ withRuntimePolicies(...policies) {
928
+ return this.WithRuntimePolicies(...policies);
929
+ }
930
+ WithRuntimePolicies(...policies) {
931
+ if (!policies.length) {
932
+ throw new Error("At least one runtime policy should be provided.");
933
+ }
934
+ if (policies.some((policy) => policy == null)) {
935
+ throw new Error("Runtime policy collection cannot contain null values.");
936
+ }
937
+ return this.mergeValues({ RuntimePolicies: policies });
938
+ }
939
+ withRuntimePolicyErrorMode(mode) {
940
+ return this.WithRuntimePolicyErrorMode(mode);
941
+ }
942
+ WithRuntimePolicyErrorMode(mode) {
943
+ return this.mergeValues({
944
+ RuntimePolicyErrorMode: normalizedRuntimePolicyErrorMode(mode)
945
+ });
946
+ }
1029
947
  withWorkerPlugins(...plugins) {
1030
948
  return this.WithWorkerPlugins(...plugins);
1031
949
  }
@@ -1121,7 +1039,7 @@ export class LoadStrikeContext {
1121
1039
  }
1122
1040
  }
1123
1041
  export class LoadStrikeScenario {
1124
- constructor(name, runHandler, initHandler, cleanHandler, loadSimulations, thresholds, trackingConfiguration, maxFailCount, withoutWarmUpValue, warmUpDurationSeconds, weight, restartIterationOnFail, licenseFeatures) {
1042
+ constructor(name, runHandler, initHandler, cleanHandler, loadSimulations, thresholds, trackingConfiguration, maxFailCount, withoutWarmUpValue, warmUpDurationSeconds, weight, restartIterationOnFail) {
1125
1043
  this.name = name;
1126
1044
  this.runHandler = runHandler;
1127
1045
  this.initHandler = initHandler;
@@ -1134,18 +1052,23 @@ export class LoadStrikeScenario {
1134
1052
  this.warmUpDurationSeconds = warmUpDurationSeconds;
1135
1053
  this.weight = weight;
1136
1054
  this.restartIterationOnFail = restartIterationOnFail;
1137
- this.licenseFeatures = [...licenseFeatures];
1138
1055
  }
1139
1056
  static create(name, runHandler) {
1140
1057
  const scenarioName = requireNonEmpty(name, "Scenario name must be provided.");
1141
1058
  if (typeof runHandler !== "function") {
1142
1059
  throw new TypeError("Scenario run handler must be provided.");
1143
1060
  }
1144
- return new LoadStrikeScenario(scenarioName, runHandler, undefined, undefined, [], [], undefined, 0, false, 0, 1, false, []);
1061
+ return new LoadStrikeScenario(scenarioName, runHandler, undefined, undefined, [], [], undefined, 0, false, 0, 1, false);
1145
1062
  }
1146
1063
  static Create(name, runHandler) {
1147
1064
  return LoadStrikeScenario.create(name, runHandler);
1148
1065
  }
1066
+ static createAsync(name, runHandler) {
1067
+ return LoadStrikeScenario.create(name, runHandler);
1068
+ }
1069
+ static CreateAsync(name, runHandler) {
1070
+ return LoadStrikeScenario.createAsync(name, runHandler);
1071
+ }
1149
1072
  static empty(name) {
1150
1073
  return LoadStrikeScenario.create(name, () => LoadStrikeResponse.ok());
1151
1074
  }
@@ -1159,37 +1082,43 @@ export class LoadStrikeScenario {
1159
1082
  if (typeof handler !== "function") {
1160
1083
  throw new TypeError("Init handler must be provided.");
1161
1084
  }
1162
- return new LoadStrikeScenario(this.name, this.runHandler, handler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.licenseFeatures);
1085
+ return new LoadStrikeScenario(this.name, this.runHandler, handler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail);
1086
+ }
1087
+ withInitAsync(handler) {
1088
+ return this.withInit(handler);
1163
1089
  }
1164
1090
  withClean(handler) {
1165
1091
  if (typeof handler !== "function") {
1166
1092
  throw new TypeError("Clean handler must be provided.");
1167
1093
  }
1168
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, handler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.licenseFeatures);
1094
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, handler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail);
1095
+ }
1096
+ withCleanAsync(handler) {
1097
+ return this.withClean(handler);
1169
1098
  }
1170
1099
  withMaxFailCount(maxFailCount) {
1171
1100
  if (!Number.isFinite(maxFailCount)) {
1172
1101
  throw new RangeError("maxFailCount should be a finite number.");
1173
1102
  }
1174
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, Math.trunc(maxFailCount), this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.licenseFeatures);
1103
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, Math.trunc(maxFailCount), this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail);
1175
1104
  }
1176
1105
  withoutWarmUp() {
1177
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, true, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.licenseFeatures);
1106
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, true, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail);
1178
1107
  }
1179
1108
  withWarmUpDuration(durationSeconds) {
1180
1109
  if (!Number.isFinite(durationSeconds)) {
1181
1110
  throw new RangeError("Warmup duration should be a finite number.");
1182
1111
  }
1183
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, durationSeconds, this.weight, this.restartIterationOnFail, this.licenseFeatures);
1112
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, durationSeconds, this.weight, this.restartIterationOnFail);
1184
1113
  }
1185
1114
  withWeight(weight) {
1186
1115
  if (!Number.isFinite(weight)) {
1187
1116
  throw new RangeError("Weight should be a finite number.");
1188
1117
  }
1189
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, Math.trunc(weight), this.restartIterationOnFail, this.licenseFeatures);
1118
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, Math.trunc(weight), this.restartIterationOnFail);
1190
1119
  }
1191
1120
  withRestartIterationOnFail(shouldRestart) {
1192
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, Boolean(shouldRestart), this.licenseFeatures);
1121
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, Boolean(shouldRestart));
1193
1122
  }
1194
1123
  withCrossPlatformTracking(configuration) {
1195
1124
  if (!configuration || typeof configuration !== "object" || Array.isArray(configuration)) {
@@ -1206,25 +1135,19 @@ export class LoadStrikeScenario {
1206
1135
  ? mapRuntimeTrackingEndpointSpec(destinationSpec)
1207
1136
  : null;
1208
1137
  validateRuntimeTrackingConfiguration(copied, sourceEndpoint, destinationEndpoint);
1209
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, copied, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.licenseFeatures);
1138
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, copied, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail);
1210
1139
  }
1211
1140
  withLoadSimulations(...simulations) {
1212
1141
  if (!simulations.length) {
1213
1142
  throw new Error("At least one load simulation should be provided.");
1214
1143
  }
1215
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, simulations.map((simulation) => attachLoadSimulationProjection({ ...simulation })), this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.licenseFeatures);
1144
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, simulations.map((simulation) => attachLoadSimulationProjection({ ...simulation })), this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail);
1216
1145
  }
1217
1146
  withThresholds(...thresholds) {
1218
1147
  if (!thresholds.length) {
1219
1148
  throw new Error("At least one threshold should be provided.");
1220
1149
  }
1221
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, thresholds.map((threshold) => ({ ...threshold })), this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, this.licenseFeatures);
1222
- }
1223
- withLicenseFeatures(...features) {
1224
- const normalized = features
1225
- .map((value) => String(value ?? "").trim())
1226
- .filter((value) => value.length > 0);
1227
- return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, [...this.licenseFeatures, ...normalized]);
1150
+ return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, thresholds.map((threshold) => ({ ...threshold })), this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail);
1228
1151
  }
1229
1152
  getSimulations() {
1230
1153
  return this.loadSimulations.map((simulation) => attachLoadSimulationProjection({ ...simulation }));
@@ -1250,9 +1173,6 @@ export class LoadStrikeScenario {
1250
1173
  shouldRestartIterationOnFail() {
1251
1174
  return this.restartIterationOnFail;
1252
1175
  }
1253
- getLicenseFeatures() {
1254
- return [...this.licenseFeatures];
1255
- }
1256
1176
  async invokeInit(context) {
1257
1177
  if (this.initHandler) {
1258
1178
  await this.initHandler(context);
@@ -1281,9 +1201,15 @@ export class LoadStrikeScenario {
1281
1201
  WithInit(handler) {
1282
1202
  return this.withInit(handler);
1283
1203
  }
1204
+ WithInitAsync(handler) {
1205
+ return this.withInitAsync(handler);
1206
+ }
1284
1207
  WithClean(handler) {
1285
1208
  return this.withClean(handler);
1286
1209
  }
1210
+ WithCleanAsync(handler) {
1211
+ return this.withCleanAsync(handler);
1212
+ }
1287
1213
  WithLoadSimulations(...simulations) {
1288
1214
  return this.withLoadSimulations(...simulations);
1289
1215
  }
@@ -1296,9 +1222,6 @@ export class LoadStrikeScenario {
1296
1222
  WithRestartIterationOnFail(shouldRestart) {
1297
1223
  return this.withRestartIterationOnFail(shouldRestart);
1298
1224
  }
1299
- WithLicenseFeatures(...features) {
1300
- return this.withLicenseFeatures(...features);
1301
- }
1302
1225
  WithThresholds(...thresholds) {
1303
1226
  return this.withThresholds(...thresholds);
1304
1227
  }
@@ -1361,9 +1284,6 @@ export class LoadStrikeRunner {
1361
1284
  static WithRunnerKey(context, runnerKey) {
1362
1285
  return context.WithRunnerKey(runnerKey);
1363
1286
  }
1364
- static WithLicenseValidationServerUrl(context, serverUrl) {
1365
- return context.WithLicenseValidationServerUrl(serverUrl);
1366
- }
1367
1287
  static WithLicenseValidationTimeout(context, timeoutSeconds) {
1368
1288
  return context.WithLicenseValidationTimeout(timeoutSeconds);
1369
1289
  }
@@ -1385,12 +1305,6 @@ export class LoadStrikeRunner {
1385
1305
  static WithReportFileName(context, reportFileName) {
1386
1306
  return context.WithReportFileName(reportFileName);
1387
1307
  }
1388
- static WithReportFinalizer(context, handler) {
1389
- return context.WithReportFinalizer(handler);
1390
- }
1391
- static WithDetailedReportFinalizer(context, handler) {
1392
- return context.WithDetailedReportFinalizer(handler);
1393
- }
1394
1308
  static WithReportFolder(context, reportFolderPath) {
1395
1309
  return context.WithReportFolder(reportFolderPath);
1396
1310
  }
@@ -1403,6 +1317,12 @@ export class LoadStrikeRunner {
1403
1317
  static WithReportingSinks(context, ...sinks) {
1404
1318
  return context.WithReportingSinks(...sinks);
1405
1319
  }
1320
+ static WithRuntimePolicies(context, ...policies) {
1321
+ return context.WithRuntimePolicies(...policies);
1322
+ }
1323
+ static WithRuntimePolicyErrorMode(context, mode) {
1324
+ return context.WithRuntimePolicyErrorMode(mode);
1325
+ }
1406
1326
  static WithScenarioCompletionTimeout(context, timeoutSeconds) {
1407
1327
  return context.WithScenarioCompletionTimeout(timeoutSeconds);
1408
1328
  }
@@ -1563,15 +1483,11 @@ export class LoadStrikeRunner {
1563
1483
  return this.buildContext();
1564
1484
  }
1565
1485
  async run(args = []) {
1566
- const detailed = await this.runDetailed(args);
1567
- return detailedToNodeStats(detailed);
1568
- }
1569
- async runDetailed(args = []) {
1570
1486
  if (this.contextConfigurators.length) {
1571
- return new LoadStrikeRunner(this.scenarios, this.buildContext().toRunnerOptions()).runDetailed(args);
1487
+ return new LoadStrikeRunner(this.scenarios, this.buildContext().toRunnerOptions()).run(args);
1572
1488
  }
1573
1489
  if (args.length) {
1574
- return this.buildContext().runDetailed(args);
1490
+ return this.buildContext().run(args);
1575
1491
  }
1576
1492
  const started = new Date();
1577
1493
  const scenarioStats = new Map();
@@ -1585,14 +1501,15 @@ export class LoadStrikeRunner {
1585
1501
  name: resolveSinkName(sink, index)
1586
1502
  }));
1587
1503
  const sinkErrors = [];
1504
+ const policyErrors = [];
1588
1505
  const sinkRetryCount = Math.max(this.options.sinkRetryCount ?? 2, 0);
1589
1506
  const sinkRetryBackoffMs = Math.max(this.options.sinkRetryBackoffMs ?? 25, 0);
1590
1507
  const policies = this.options.runtimePolicies ?? [];
1508
+ const runtimePolicyErrorMode = normalizedRuntimePolicyErrorMode(this.options.runtimePolicyErrorMode);
1591
1509
  const plugins = this.options.reportingSinks === undefined && this.options.workerPlugins === undefined &&
1592
1510
  this.options.runtimePolicies === undefined && this.options.customSettings === undefined &&
1593
1511
  this.options.globalCustomSettings === undefined && this.options.configPath === undefined &&
1594
1512
  this.options.infraConfigPath === undefined && this.options.infraConfig === undefined &&
1595
- this.options.reportFinalizer === undefined && this.options.detailedReportFinalizer === undefined &&
1596
1513
  this.options.loggerConfig === undefined && this.options.minimumLogLevel === undefined &&
1597
1514
  this.options.sinkRetryCount === undefined && this.options.sinkRetryBackoffMs === undefined &&
1598
1515
  this.options.reportsEnabled === false && this.options.displayConsoleMetrics === false &&
@@ -1607,7 +1524,6 @@ export class LoadStrikeRunner {
1607
1524
  const createdUtc = started.toISOString();
1608
1525
  const testInfo = buildTestInfo(this.options, createdUtc);
1609
1526
  const nodeInfo = buildNodeInfo(this.options);
1610
- const runLogger = createLogger(this.options.loggerConfig, this.options.minimumLogLevel);
1611
1527
  let pluginsStopped = false;
1612
1528
  let sinksStopped = false;
1613
1529
  let realtimeTimer = null;
@@ -1617,18 +1533,18 @@ export class LoadStrikeRunner {
1617
1533
  let licenseSession = null;
1618
1534
  const clusterMode = resolveClusterExecutionMode(this.options);
1619
1535
  const selectedScenarios = clusterMode === "local-coordinator" || clusterMode === "nats-coordinator"
1620
- ? await this.filterScenariosWithPolicies(this.scenarios, policies)
1621
- : await this.selectScenarios(policies);
1622
- const licenseValidationServerUrl = String(this.options.licenseValidationServerUrl ?? DEFAULT_LICENSING_API_BASE_URL).trim() || DEFAULT_LICENSING_API_BASE_URL;
1623
- licensePayload = this.buildLicenseValidationPayload();
1536
+ ? await this.filterScenariosWithPolicies(this.scenarios, policies, policyErrors, runtimePolicyErrorMode)
1537
+ : await this.selectScenarios(policies, policyErrors, runtimePolicyErrorMode);
1538
+ licensePayload = buildLicenseValidationPayload(this.options, this.scenarios);
1624
1539
  licenseClient = new LoadStrikeLocalClient({
1625
- licensingApiBaseUrl: licenseValidationServerUrl,
1626
1540
  licenseValidationTimeoutMs: Math.max((this.options.licenseValidationTimeoutSeconds ?? 10) * 1000, 1)
1627
1541
  });
1628
1542
  licenseSession = await licenseClient.acquireLicenseLease(licensePayload);
1629
1543
  if (clusterMode === "nats-agent") {
1630
1544
  return this.runAgentWithNats(createdUtc, testInfo, nodeInfo);
1631
1545
  }
1546
+ const loggerSetup = createLoggerSetup(this.options.loggerConfig, this.options.minimumLogLevel, this.options, testInfo, nodeInfo);
1547
+ const runLogger = loggerSetup.logger;
1632
1548
  const scenarioStartInfos = selectedScenarios.map((scenario, index) => {
1633
1549
  const startInfo = {
1634
1550
  scenarioName: scenario.name,
@@ -1695,7 +1611,7 @@ export class LoadStrikeRunner {
1695
1611
  const okCount = snapshot.reduce((sum, value) => sum + value.allOkCount, 0);
1696
1612
  const failCount = snapshot.reduce((sum, value) => sum + value.allFailCount, 0);
1697
1613
  const stamp = new Date().toTimeString().slice(0, 8);
1698
- runLogger.info(`[${stamp}] requests=${requestCount} ok=${okCount} fail=${failCount}`);
1614
+ console.log(`[${stamp}] requests=${requestCount} ok=${okCount} fail=${failCount}`);
1699
1615
  }
1700
1616
  }
1701
1617
  finally {
@@ -1703,7 +1619,7 @@ export class LoadStrikeRunner {
1703
1619
  }
1704
1620
  };
1705
1621
  const reportingIntervalMs = Math.max(Math.trunc((this.options.reportingIntervalSeconds ?? 5) * 1000), 1);
1706
- if (sinkStates.length > 0) {
1622
+ if (sinkStates.length > 0 || toBoolean(this.options.displayConsoleMetrics, true)) {
1707
1623
  realtimeTimer = setInterval(() => {
1708
1624
  void emitRealtimeSnapshot();
1709
1625
  }, reportingIntervalMs);
@@ -1718,12 +1634,12 @@ export class LoadStrikeRunner {
1718
1634
  if (clusterMode === "local-coordinator") {
1719
1635
  const aggregated = await this.runCoordinatorWithLocalAgents(selectedScenarios, testInfo, nodeInfo);
1720
1636
  metricStats = aggregated.metrics;
1721
- result = toDetailedRunResultFromNodeStats(aggregated, started.toISOString(), sinkErrors);
1637
+ result = toDetailedRunResultFromNodeStats(aggregated, started.toISOString(), sinkErrors, policyErrors);
1722
1638
  }
1723
1639
  else if (clusterMode === "nats-coordinator") {
1724
1640
  const aggregated = await this.runCoordinatorWithNats(selectedScenarios, testInfo, nodeInfo);
1725
1641
  metricStats = aggregated.metrics;
1726
- result = toDetailedRunResultFromNodeStats(aggregated, started.toISOString(), sinkErrors);
1642
+ result = toDetailedRunResultFromNodeStats(aggregated, started.toISOString(), sinkErrors, policyErrors);
1727
1643
  }
1728
1644
  else {
1729
1645
  const testAbortController = new AbortController();
@@ -1746,21 +1662,15 @@ export class LoadStrikeRunner {
1746
1662
  stopTestState,
1747
1663
  testAbortController,
1748
1664
  executeScenarioInvocation: (targetScenario, context, operation) => this.executeScenarioInvocation(targetScenario, context, operation),
1749
- invokeBeforeScenario: (runtimePolicies, scenarioName) => this.invokeBeforeScenario(runtimePolicies, scenarioName),
1750
- invokeAfterScenario: (runtimePolicies, scenarioName, stats) => this.invokeAfterScenario(runtimePolicies, scenarioName, stats)
1665
+ invokeBeforeScenario: (runtimePolicies, scenarioName) => this.invokeBeforeScenario(runtimePolicies, scenarioName, policyErrors, runtimePolicyErrorMode),
1666
+ invokeAfterScenario: (runtimePolicies, scenarioName, stats) => this.invokeAfterScenario(runtimePolicies, scenarioName, stats, policyErrors, runtimePolicyErrorMode),
1667
+ invokeBeforeStep: (runtimePolicies, scenarioName, stepName) => this.invokeBeforeStep(runtimePolicies, scenarioName, stepName, policyErrors, runtimePolicyErrorMode),
1668
+ invokeAfterStep: (runtimePolicies, scenarioName, stepName, reply) => this.invokeAfterStep(runtimePolicies, scenarioName, stepName, reply, policyErrors, runtimePolicyErrorMode)
1751
1669
  })));
1752
1670
  nodeInfo.currentOperation = stopTestState.value ? "Stop" : "Complete";
1753
- let scenarioStatList = Array.from(scenarioAccumulators.values())
1671
+ const scenarioStatList = Array.from(scenarioAccumulators.values())
1754
1672
  .map((value) => value.build(scenarioDurationsMs.get(value.scenarioName) ?? 0))
1755
1673
  .sort((left, right) => left.sortIndex - right.sortIndex);
1756
- const reportFinalizer = this.options.reportFinalizer;
1757
- if (typeof reportFinalizer === "function") {
1758
- const finalized = reportFinalizer(LoadStrikeReportData.create(...scenarioStatList));
1759
- if (!(finalized instanceof LoadStrikeReportData)) {
1760
- throw new TypeError("Report finalizer must return LoadStrikeReportData.");
1761
- }
1762
- scenarioStatList = [...finalized.scenarioStats];
1763
- }
1764
1674
  const metricValues = collectMetricValues(allRegisteredMetrics);
1765
1675
  metricStats = collectMetricStats(allRegisteredMetrics, Date.now() - started.getTime());
1766
1676
  const metricsByName = metricValues.reduce((accumulator, metric) => {
@@ -1774,35 +1684,42 @@ export class LoadStrikeRunner {
1774
1684
  result = {
1775
1685
  startedUtc: started.toISOString(),
1776
1686
  completedUtc: new Date().toISOString(),
1687
+ allBytes: builtScenarioStats.reduce((sum, x) => sum + x.allBytes, 0),
1777
1688
  allRequestCount: scenarioStatList.reduce((sum, x) => sum + x.allRequestCount, 0),
1778
1689
  allOkCount: scenarioStatList.reduce((sum, x) => sum + x.allOkCount, 0),
1779
1690
  allFailCount: scenarioStatList.reduce((sum, x) => sum + x.allFailCount, 0),
1780
1691
  failedThresholds: thresholdEvaluation.failedCount,
1692
+ durationMs: Math.max(Date.now() - started.getTime(), 0),
1781
1693
  nodeInfo,
1782
1694
  testInfo,
1695
+ thresholds: thresholdEvaluation.results.map((value) => ({ ...value })),
1783
1696
  thresholdResults: thresholdEvaluation.results,
1697
+ metricStats,
1784
1698
  metrics: metricValues,
1785
1699
  scenarioStats: builtScenarioStats,
1786
1700
  stepStats: builtStepStats,
1701
+ scenarioDurationsMs: Object.fromEntries(scenarioDurationsMs.entries()),
1787
1702
  pluginsData: [],
1788
1703
  disabledSinks: [],
1789
1704
  sinkErrors,
1790
- reportFiles: []
1705
+ policyErrors,
1706
+ reportFiles: [],
1707
+ logFiles: [...loggerSetup.logFiles],
1708
+ correlationRows: buildDetailedCorrelationRows(),
1709
+ failedCorrelationRows: buildDetailedFailedCorrelationRows()
1791
1710
  };
1792
1711
  }
1793
- result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, detailedToNodeStats(result, metricStats), pluginLifecycleErrors));
1794
- const detailedReportFinalizer = this.options.detailedReportFinalizer;
1795
- const detailedResult = attachRunResultAliases(result);
1796
- const finalizedResult = attachRunResultAliases(typeof detailedReportFinalizer === "function"
1797
- ? requireDetailedReportFinalizerResult(detailedReportFinalizer(detailedResult))
1798
- : detailedResult);
1799
- await this.emitFinalStats(sinkStates, detailedToNodeStats(finalizedResult, metricStats), sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
1712
+ result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, attachRunResultAliases(result), pluginLifecycleErrors));
1713
+ const finalizedResult = attachRunResultAliases(result);
1714
+ finalizedResult.logFiles = mergeStringArrays(finalizedResult.logFiles, loggerSetup.logFiles);
1800
1715
  finalizedResult.reportFiles = this.writeReports(finalizedResult);
1801
1716
  finalizedResult.disabledSinks = sinkStates.filter((x) => x.disabled).map((x) => x.name);
1802
1717
  await this.emitRunResult(sinkStates, finalizedResult, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
1803
1718
  await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
1804
1719
  sinksStopped = true;
1805
1720
  finalizedResult.disabledSinks = sinkStates.filter((x) => x.disabled).map((x) => x.name);
1721
+ finalizedResult.sinkErrors = sinkErrors
1722
+ .map((value) => attachSinkErrorAliases(normalizeSinkErrorValue(value)));
1806
1723
  return finalizedResult;
1807
1724
  }
1808
1725
  finally {
@@ -1813,7 +1730,7 @@ export class LoadStrikeRunner {
1813
1730
  await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
1814
1731
  }
1815
1732
  if (!pluginsStopped) {
1816
- await this.stopPlugins(plugins, pluginLifecycleErrors);
1733
+ await this.stopPlugins(plugins, pluginLifecycleErrors, runLogger);
1817
1734
  }
1818
1735
  if (licenseClient && licenseSession && licensePayload) {
1819
1736
  await licenseClient.releaseLicenseLease(licenseSession, licensePayload);
@@ -1823,10 +1740,7 @@ export class LoadStrikeRunner {
1823
1740
  async Run(args = []) {
1824
1741
  return this.run(args);
1825
1742
  }
1826
- async RunDetailed(args = []) {
1827
- return this.runDetailed(args);
1828
- }
1829
- async filterScenariosWithPolicies(scenarios, policies) {
1743
+ async filterScenariosWithPolicies(scenarios, policies, policyErrors, mode) {
1830
1744
  if (!policies.length) {
1831
1745
  return [...scenarios];
1832
1746
  }
@@ -1835,7 +1749,7 @@ export class LoadStrikeRunner {
1835
1749
  let allowed = true;
1836
1750
  for (const policy of policies) {
1837
1751
  if (policy.shouldRunScenario) {
1838
- const shouldRun = await policy.shouldRunScenario(scenario.name);
1752
+ const shouldRun = await this.invokePolicyCallback(policy, "shouldRunScenario", scenario.name, "", policyErrors, mode, () => policy.shouldRunScenario(scenario.name), true);
1839
1753
  if (!shouldRun) {
1840
1754
  allowed = false;
1841
1755
  break;
@@ -1868,6 +1782,7 @@ export class LoadStrikeRunner {
1868
1782
  localDevClusterEnabled: false,
1869
1783
  nodeType,
1870
1784
  natsServerUrl: undefined,
1785
+ reportFolderPath: resolve(this.options.reportFolderPath ?? "./reports", sanitizeReportFileName(machineName)),
1871
1786
  targetScenarios,
1872
1787
  agentTargetScenarios: targetScenarios,
1873
1788
  coordinatorTargetScenarios: [],
@@ -1876,14 +1791,16 @@ export class LoadStrikeRunner {
1876
1791
  reportingSinks: includeWorkerExtensions ? this.options.reportingSinks : [],
1877
1792
  workerPlugins: includeWorkerExtensions ? this.options.workerPlugins : []
1878
1793
  });
1879
- const childStats = await childRunner.run();
1794
+ const childResult = await childRunner.run();
1795
+ const childStats = detailedToNodeStats(childResult);
1880
1796
  return {
1881
1797
  ...childStats,
1882
1798
  nodeInfo: {
1883
1799
  ...childStats.nodeInfo,
1884
1800
  machineName,
1885
1801
  nodeType
1886
- }
1802
+ },
1803
+ logFiles: [...(childResult.logFiles ?? [])]
1887
1804
  };
1888
1805
  }
1889
1806
  async runCoordinatorWithLocalAgents(scenarios, testInfo, nodeInfo) {
@@ -1951,7 +1868,7 @@ export class LoadStrikeRunner {
1951
1868
  };
1952
1869
  });
1953
1870
  if (handled && handledStats) {
1954
- return toDetailedRunResultFromNodeStats(handledStats, startedUtc, []);
1871
+ return toDetailedRunResultFromNodeStats(handledStats, startedUtc, [], []);
1955
1872
  }
1956
1873
  await sleep(5);
1957
1874
  }
@@ -1961,15 +1878,17 @@ export class LoadStrikeRunner {
1961
1878
  await agent.dispose().catch(() => { });
1962
1879
  }
1963
1880
  }
1964
- async stopPlugins(plugins, pluginLifecycleErrors) {
1965
- void pluginLifecycleErrors;
1881
+ async stopPlugins(plugins, pluginLifecycleErrors, logger) {
1966
1882
  for (const plugin of plugins) {
1883
+ const pluginName = normalizePluginName(resolveWorkerPluginName(plugin));
1967
1884
  const stop = resolveWorkerPluginStop(plugin);
1968
1885
  if (stop) {
1969
1886
  try {
1970
1887
  await stop();
1971
1888
  }
1972
- catch {
1889
+ catch (error) {
1890
+ recordPluginLifecycleError(pluginLifecycleErrors, pluginName, "stop", error);
1891
+ logger?.warn?.(`Worker plugin ${pluginName} stop failed during shutdown: ${String(error ?? "unknown error")}`);
1973
1892
  }
1974
1893
  }
1975
1894
  const dispose = resolveWorkerPluginDispose(plugin);
@@ -1977,7 +1896,9 @@ export class LoadStrikeRunner {
1977
1896
  try {
1978
1897
  await dispose();
1979
1898
  }
1980
- catch {
1899
+ catch (error) {
1900
+ recordPluginLifecycleError(pluginLifecycleErrors, pluginName, "dispose", error);
1901
+ logger?.warn?.(`Worker plugin ${pluginName} dispose failed during shutdown: ${String(error ?? "unknown error")}`);
1981
1902
  }
1982
1903
  }
1983
1904
  }
@@ -1989,7 +1910,7 @@ export class LoadStrikeRunner {
1989
1910
  }
1990
1911
  return executeTrackedScenarioInvocation(tracking, context, () => scenario.execute(context), operation);
1991
1912
  }
1992
- async selectScenarios(policies) {
1913
+ async selectScenarios(policies, policyErrors, mode) {
1993
1914
  const scenarioMap = new Map(this.scenarios.map((x) => [x.name, x]));
1994
1915
  const nodeType = (this.options.nodeType ?? "SingleNode").toLowerCase();
1995
1916
  let selectedNames = this.options.targetScenarios;
@@ -2012,7 +1933,7 @@ export class LoadStrikeRunner {
2012
1933
  let allowed = true;
2013
1934
  for (const policy of policies) {
2014
1935
  if (policy.shouldRunScenario) {
2015
- const shouldRun = await policy.shouldRunScenario(scenario.name);
1936
+ const shouldRun = await this.invokePolicyCallback(policy, "shouldRunScenario", scenario.name, "", policyErrors, mode, () => policy.shouldRunScenario(scenario.name), true);
2016
1937
  if (!shouldRun) {
2017
1938
  allowed = false;
2018
1939
  break;
@@ -2025,9 +1946,6 @@ export class LoadStrikeRunner {
2025
1946
  }
2026
1947
  return filtered;
2027
1948
  }
2028
- buildLicenseValidationPayload() {
2029
- return buildLicenseValidationPayload(this.options, this.scenarios);
2030
- }
2031
1949
  async initializeSinks(sinkStates, context, infraConfig, retryCount, retryBackoffMs, sinkErrors) {
2032
1950
  for (const state of sinkStates) {
2033
1951
  await this.invokeSinkAction(state, "init", retryCount, retryBackoffMs, sinkErrors, false, true, async () => {
@@ -2062,19 +1980,10 @@ export class LoadStrikeRunner {
2062
1980
  });
2063
1981
  }
2064
1982
  }
2065
- async emitFinalStats(sinkStates, result, retryCount, retryBackoffMs, sinkErrors) {
2066
- for (const state of sinkStates) {
2067
- await this.invokeSinkAction(state, "final", retryCount, retryBackoffMs, sinkErrors, false, false, async () => {
2068
- const saveFinalStats = resolveSinkSaveFinalStats(state.sink);
2069
- if (saveFinalStats) {
2070
- await saveFinalStats(result);
2071
- }
2072
- });
2073
- }
2074
- }
2075
1983
  async stopSinks(sinkStates, retryCount, retryBackoffMs, sinkErrors) {
1984
+ const shutdownRetryCount = 0;
2076
1985
  for (const state of sinkStates) {
2077
- await this.invokeSinkAction(state, "stop", retryCount, retryBackoffMs, sinkErrors, true, false, async () => {
1986
+ await this.invokeSinkAction(state, "stop", shutdownRetryCount, retryBackoffMs, sinkErrors, true, true, async () => {
2078
1987
  const stop = resolveSinkStop(state.sink);
2079
1988
  if (stop) {
2080
1989
  await stop();
@@ -2082,11 +1991,9 @@ export class LoadStrikeRunner {
2082
1991
  });
2083
1992
  const dispose = resolveSinkDispose(state.sink);
2084
1993
  if (dispose) {
2085
- try {
1994
+ await this.invokeSinkAction(state, "dispose", shutdownRetryCount, retryBackoffMs, sinkErrors, true, true, async () => {
2086
1995
  await dispose();
2087
- }
2088
- catch {
2089
- }
1996
+ });
2090
1997
  }
2091
1998
  }
2092
1999
  }
@@ -2130,20 +2037,56 @@ export class LoadStrikeRunner {
2130
2037
  }
2131
2038
  }
2132
2039
  }
2133
- async invokeBeforeScenario(policies, scenarioName) {
2040
+ async invokeBeforeScenario(policies, scenarioName, policyErrors, mode) {
2134
2041
  for (const policy of policies) {
2135
2042
  if (policy.beforeScenario) {
2136
- await policy.beforeScenario(scenarioName);
2043
+ await this.invokePolicyCallback(policy, "beforeScenario", scenarioName, "", policyErrors, mode, () => policy.beforeScenario(scenarioName));
2137
2044
  }
2138
2045
  }
2139
2046
  }
2140
- async invokeAfterScenario(policies, scenarioName, stats) {
2047
+ async invokeAfterScenario(policies, scenarioName, stats, policyErrors, mode) {
2141
2048
  for (const policy of policies) {
2142
2049
  if (policy.afterScenario) {
2143
- await policy.afterScenario(scenarioName, stats);
2050
+ await this.invokePolicyCallback(policy, "afterScenario", scenarioName, "", policyErrors, mode, () => policy.afterScenario(scenarioName, stats));
2051
+ }
2052
+ }
2053
+ }
2054
+ async invokeBeforeStep(policies, scenarioName, stepName, policyErrors, mode) {
2055
+ for (const policy of policies) {
2056
+ if (policy.beforeStep) {
2057
+ await this.invokePolicyCallback(policy, "beforeStep", scenarioName, stepName, policyErrors, mode, () => policy.beforeStep(scenarioName, stepName));
2144
2058
  }
2145
2059
  }
2146
2060
  }
2061
+ async invokeAfterStep(policies, scenarioName, stepName, reply, policyErrors, mode) {
2062
+ for (const policy of policies) {
2063
+ if (policy.afterStep) {
2064
+ await this.invokePolicyCallback(policy, "afterStep", scenarioName, stepName, policyErrors, mode, () => policy.afterStep(scenarioName, stepName, reply));
2065
+ }
2066
+ }
2067
+ }
2068
+ async invokePolicyCallback(policy, callbackName, scenarioName, stepName, policyErrors, mode, callback, continueFallback) {
2069
+ try {
2070
+ return await callback();
2071
+ }
2072
+ catch (error) {
2073
+ const entry = this.buildRuntimePolicyError(policy, callbackName, scenarioName, stepName, error);
2074
+ policyErrors.push(entry);
2075
+ if (mode === "fail") {
2076
+ throw new RuntimePolicyCallbackError(`Runtime policy '${entry.policyName}' failed during ${callbackName}: ${entry.message}`);
2077
+ }
2078
+ return continueFallback;
2079
+ }
2080
+ }
2081
+ buildRuntimePolicyError(policy, callbackName, scenarioName, stepName, error) {
2082
+ return attachRuntimePolicyErrorAliases({
2083
+ policyName: resolveRuntimePolicyName(policy),
2084
+ callbackName,
2085
+ scenarioName,
2086
+ stepName,
2087
+ message: String(error ?? "runtime policy callback failed")
2088
+ });
2089
+ }
2147
2090
  writeReports(result) {
2148
2091
  const reportsEnabled = this.options.reportsEnabled ?? true;
2149
2092
  if (!reportsEnabled) {
@@ -2152,7 +2095,7 @@ export class LoadStrikeRunner {
2152
2095
  const reportFolder = resolve(this.options.reportFolderPath ?? "./reports");
2153
2096
  const reportFile = sanitizeReportFileName(this.options.reportFileName?.trim()
2154
2097
  ? this.options.reportFileName
2155
- : `${result.testInfo.testSuite}_${result.testInfo.testName}_${formatUtcReportTimestamp(new Date())}`);
2098
+ : `${result.testInfo.testSuite}_${result.testInfo.testName}_${resolveDefaultReportTimestamp(result)}`);
2156
2099
  const reportFormats = normalizeReportFormats(this.options.reportFormats ?? ["html", "txt", "csv", "md"]);
2157
2100
  mkdirSync(reportFolder, { recursive: true });
2158
2101
  const nodeStats = detailedToNodeStats(result);
@@ -2203,7 +2146,7 @@ function hasPluginRows(value) {
2203
2146
  return value.tables.some((table) => Array.isArray(table.rows) && table.rows.length > 0);
2204
2147
  }
2205
2148
  async function executeScenarioRuntime(args) {
2206
- const { scenario, scenarioIndex, scenarioCount, options, logger, nodeInfo, testInfo, policies, restartIterationMaxAttempts, allRegisteredMetrics, scenarioRuntimes, stepRuntimes, scenarioAccumulators, scenarioDurationsMs, stopTestState, testAbortController, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario } = args;
2149
+ const { scenario, scenarioIndex, scenarioCount, options, logger, nodeInfo, testInfo, policies, restartIterationMaxAttempts, allRegisteredMetrics, scenarioRuntimes, stepRuntimes, scenarioAccumulators, scenarioDurationsMs, stopTestState, testAbortController, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario, invokeBeforeStep, invokeAfterStep } = args;
2207
2150
  const scenarioStartedMs = Date.now();
2208
2151
  const scenarioContextData = {};
2209
2152
  const registeredMetrics = [];
@@ -2316,20 +2259,8 @@ async function executeScenarioRuntime(args) {
2316
2259
  },
2317
2260
  shouldStopScenario: () => stopScenario || scenarioCancellationToken.aborted,
2318
2261
  shouldStopTest: () => stopTestState.value || scenarioCancellationToken.aborted,
2319
- invokeBeforeStep: async (stepName) => {
2320
- for (const policy of policies) {
2321
- if (policy.beforeStep) {
2322
- await policy.beforeStep(scenario.name, stepName);
2323
- }
2324
- }
2325
- },
2326
- invokeAfterStep: async (stepName, reply) => {
2327
- for (const policy of policies) {
2328
- if (policy.afterStep) {
2329
- await policy.afterStep(scenario.name, stepName, reply);
2330
- }
2331
- }
2332
- }
2262
+ invokeBeforeStep: async (stepName) => invokeBeforeStep(policies, scenario.name, stepName),
2263
+ invokeAfterStep: async (stepName, reply) => invokeAfterStep(policies, scenario.name, stepName, reply)
2333
2264
  };
2334
2265
  attachScenarioContextAliases(context);
2335
2266
  const startedAt = Date.now();
@@ -2568,6 +2499,10 @@ async function executeScenarioRuntime(args) {
2568
2499
  await invokeAfterScenario(policies, scenario.name, runtime);
2569
2500
  }
2570
2501
  catch (error) {
2502
+ if (error instanceof RuntimePolicyCallbackError) {
2503
+ accumulator.setCurrentOperation("Error");
2504
+ throw error;
2505
+ }
2571
2506
  if (scenarioCancellationToken.aborted || testAbortController.signal.aborted) {
2572
2507
  accumulator.setCurrentOperation("Stop");
2573
2508
  }
@@ -2965,6 +2900,16 @@ function normalizeSinkErrorValue(value) {
2965
2900
  attempts: pickAliasNumber(source, "attempts", "Attempts")
2966
2901
  };
2967
2902
  }
2903
+ function normalizeRuntimePolicyErrorValue(value) {
2904
+ const source = asAliasRecord(value);
2905
+ return {
2906
+ policyName: pickAliasString(source, "policyName", "PolicyName"),
2907
+ callbackName: pickAliasString(source, "callbackName", "CallbackName"),
2908
+ scenarioName: pickAliasString(source, "scenarioName", "ScenarioName"),
2909
+ stepName: pickAliasString(source, "stepName", "StepName"),
2910
+ message: pickAliasString(source, "message", "Message")
2911
+ };
2912
+ }
2968
2913
  function normalizeMetricValueProjection(value) {
2969
2914
  const source = asAliasRecord(value);
2970
2915
  const scenarioName = pickAliasString(source, "scenarioName", "ScenarioName");
@@ -3307,6 +3252,16 @@ function attachSinkErrorAliases(error) {
3307
3252
  Attempts: "attempts"
3308
3253
  });
3309
3254
  }
3255
+ function attachRuntimePolicyErrorAliases(error) {
3256
+ const projected = normalizeRuntimePolicyErrorValue(error);
3257
+ return attachAliasMap(projected, {
3258
+ PolicyName: "policyName",
3259
+ CallbackName: "callbackName",
3260
+ ScenarioName: "scenarioName",
3261
+ StepName: "stepName",
3262
+ Message: "message"
3263
+ });
3264
+ }
3310
3265
  function attachMetricStatsAliases(stats) {
3311
3266
  const projected = normalizeMetricStatsValue(stats);
3312
3267
  projected.counters = projected.counters.map((value) => attachCounterStatsAliases(value));
@@ -3465,7 +3420,8 @@ function attachNodeStatsAliases(stats) {
3465
3420
  PluginsData: "pluginsData",
3466
3421
  DisabledSinks: "disabledSinks",
3467
3422
  SinkErrors: "sinkErrors",
3468
- ReportFiles: "reportFiles"
3423
+ ReportFiles: "reportFiles",
3424
+ LogFiles: "logFiles"
3469
3425
  });
3470
3426
  defineAliasProperty(projected, "StartedUtc", () => parseAliasDate(stats.startedUtc));
3471
3427
  defineAliasProperty(projected, "CompletedUtc", () => parseAliasDate(stats.completedUtc));
@@ -3476,17 +3432,30 @@ function attachRunResultAliases(result) {
3476
3432
  const source = asAliasRecord(result);
3477
3433
  const scenarioStats = pickAliasArray(source, "scenarioStats", "ScenarioStats")
3478
3434
  .map((value, index) => attachScenarioStatsAliases(normalizeScenarioStatsValue(value, index)));
3435
+ const findScenarioStats = (scenarioName) => scenarioStats.find((value) => value.scenarioName === scenarioName);
3436
+ const getScenarioStats = (scenarioName) => {
3437
+ const value = findScenarioStats(scenarioName);
3438
+ if (!value) {
3439
+ throw new Error(`Scenario stats not found: ${scenarioName}`);
3440
+ }
3441
+ return value;
3442
+ };
3479
3443
  const projected = {
3480
3444
  startedUtc: pickAliasDateToken(source, "startedUtc", "StartedUtc"),
3481
3445
  completedUtc: pickAliasDateToken(source, "completedUtc", "CompletedUtc"),
3446
+ allBytes: pickAliasNumber(source, "allBytes", "AllBytes"),
3482
3447
  allRequestCount: pickAliasNumber(source, "allRequestCount", "AllRequestCount"),
3483
3448
  allOkCount: pickAliasNumber(source, "allOkCount", "AllOkCount"),
3484
3449
  allFailCount: pickAliasNumber(source, "allFailCount", "AllFailCount"),
3485
3450
  failedThresholds: pickAliasNumber(source, "failedThresholds", "FailedThresholds"),
3451
+ durationMs: pickAliasNumber(source, "durationMs", "DurationMs", "Duration"),
3486
3452
  nodeInfo: attachNodeInfoAliases(pickAliasValue(source, "nodeInfo", "NodeInfo")),
3487
3453
  testInfo: attachTestInfoAliases(pickAliasValue(source, "testInfo", "TestInfo")),
3454
+ thresholds: pickAliasArray(source, "thresholds", "Thresholds")
3455
+ .map((value) => attachThresholdResultAliases(normalizeThresholdResultValue(value))),
3488
3456
  thresholdResults: pickAliasArray(source, "thresholdResults", "ThresholdResults")
3489
3457
  .map((value) => attachThresholdResultAliases(normalizeThresholdResultValue(value))),
3458
+ metricStats: attachMetricStatsAliases(normalizeMetricStatsValue(pickAliasValue(source, "metricStats", "MetricStats"), pickAliasNumber(source, "durationMs", "DurationMs", "Duration"))),
3490
3459
  metrics: pickAliasArray(source, "metrics", "Metrics")
3491
3460
  .map((value) => attachMetricValueAliases(normalizeMetricValueProjection(value))),
3492
3461
  scenarioStats,
@@ -3496,37 +3465,53 @@ function attachRunResultAliases(result) {
3496
3465
  : scenarioStats.flatMap((value) => value.stepStats)),
3497
3466
  pluginsData: pickAliasArray(source, "pluginsData", "PluginsData")
3498
3467
  .map((value) => normalizePluginData(pickAliasString(asAliasRecord(value), "pluginName", "PluginName"), value)),
3468
+ scenarioDurationsMs: pickAliasValue(source, "scenarioDurationsMs", "ScenarioDurationsMs") ?? {},
3499
3469
  disabledSinks: normalizeAliasStringArray(pickAliasValue(source, "disabledSinks", "DisabledSinks")),
3500
3470
  sinkErrors: pickAliasArray(source, "sinkErrors", "SinkErrors")
3501
3471
  .map((value) => attachSinkErrorAliases(normalizeSinkErrorValue(value))),
3502
- reportFiles: normalizeAliasStringArray(pickAliasValue(source, "reportFiles", "ReportFiles"))
3472
+ policyErrors: pickAliasArray(source, "policyErrors", "PolicyErrors")
3473
+ .map((value) => attachRuntimePolicyErrorAliases(normalizeRuntimePolicyErrorValue(value))),
3474
+ reportFiles: normalizeAliasStringArray(pickAliasValue(source, "reportFiles", "ReportFiles")),
3475
+ logFiles: normalizeAliasStringArray(pickAliasValue(source, "logFiles", "LogFiles")),
3476
+ correlationRows: pickAliasArray(source, "correlationRows", "CorrelationRows")
3477
+ .map((value) => ({ ...asAliasRecord(value) })),
3478
+ failedCorrelationRows: pickAliasArray(source, "failedCorrelationRows", "FailedCorrelationRows")
3479
+ .map((value) => ({ ...asAliasRecord(value) })),
3480
+ findScenarioStats,
3481
+ getScenarioStats,
3482
+ FindScenarioStats: findScenarioStats,
3483
+ GetScenarioStats: getScenarioStats
3503
3484
  };
3504
3485
  attachAliasMap(projected, {
3486
+ AllBytes: "allBytes",
3505
3487
  AllRequestCount: "allRequestCount",
3506
3488
  AllOkCount: "allOkCount",
3507
3489
  AllFailCount: "allFailCount",
3508
3490
  FailedThresholds: "failedThresholds",
3491
+ DurationMs: "durationMs",
3509
3492
  NodeInfo: "nodeInfo",
3510
3493
  TestInfo: "testInfo",
3494
+ Thresholds: "thresholds",
3511
3495
  ThresholdResults: "thresholdResults",
3496
+ MetricStats: "metricStats",
3512
3497
  Metrics: "metrics",
3513
3498
  ScenarioStats: "scenarioStats",
3514
3499
  StepStats: "stepStats",
3500
+ ScenarioDurationsMs: "scenarioDurationsMs",
3515
3501
  PluginsData: "pluginsData",
3516
3502
  DisabledSinks: "disabledSinks",
3517
3503
  SinkErrors: "sinkErrors",
3518
- ReportFiles: "reportFiles"
3504
+ PolicyErrors: "policyErrors",
3505
+ ReportFiles: "reportFiles",
3506
+ LogFiles: "logFiles",
3507
+ CorrelationRows: "correlationRows",
3508
+ FailedCorrelationRows: "failedCorrelationRows"
3519
3509
  });
3520
3510
  defineAliasProperty(projected, "StartedUtc", () => parseAliasDate(projected.startedUtc));
3521
3511
  defineAliasProperty(projected, "CompletedUtc", () => parseAliasDate(projected.completedUtc));
3512
+ defineAliasProperty(projected, "Duration", () => projected.durationMs);
3522
3513
  return projected;
3523
3514
  }
3524
- function requireDetailedReportFinalizerResult(result) {
3525
- if (!result || typeof result !== "object" || Array.isArray(result)) {
3526
- throw new TypeError("Detailed report finalizer must return LoadStrikeRunResult.");
3527
- }
3528
- return result;
3529
- }
3530
3515
  function resolveResponseCustomLatency(customLatencyMs, usesOverloadDefaults) {
3531
3516
  if (!Number.isFinite(customLatencyMs)) {
3532
3517
  return usesOverloadDefaults ? -1 : 0;
@@ -4124,9 +4109,10 @@ function detailedToNodeStats(result, metricStats) {
4124
4109
  scenarioStats,
4125
4110
  stepStats,
4126
4111
  pluginsData,
4127
- disabledSinks: [...result.disabledSinks],
4128
- sinkErrors: result.sinkErrors.map((sinkError) => ({ ...sinkError })),
4129
- reportFiles: [...result.reportFiles],
4112
+ disabledSinks: [...(result.disabledSinks ?? [])],
4113
+ sinkErrors: (result.sinkErrors ?? []).map((sinkError) => ({ ...sinkError })),
4114
+ reportFiles: [...(result.reportFiles ?? [])],
4115
+ logFiles: [...(result.logFiles ?? [])],
4130
4116
  findScenarioStats: (scenarioName) => scenarioStats.find((scenario) => scenario.scenarioName === scenarioName),
4131
4117
  getScenarioStats: (scenarioName) => {
4132
4118
  const value = scenarioStats.find((scenario) => scenario.scenarioName === scenarioName);
@@ -4193,6 +4179,7 @@ function buildEmptyNodeStats(args) {
4193
4179
  disabledSinks: [],
4194
4180
  sinkErrors: [],
4195
4181
  reportFiles: [],
4182
+ logFiles: [],
4196
4183
  findScenarioStats: (scenarioName) => undefined,
4197
4184
  getScenarioStats: (scenarioName) => {
4198
4185
  throw new Error(`Scenario stats not found: ${scenarioName}`);
@@ -4240,27 +4227,37 @@ function nodeStatsToClusterPayload(result) {
4240
4227
  thresholds: result.thresholds,
4241
4228
  pluginsData: result.pluginsData,
4242
4229
  nodeInfo: result.nodeInfo,
4243
- testInfo: result.testInfo
4230
+ testInfo: result.testInfo,
4231
+ logFiles: [...(result.logFiles ?? [])]
4244
4232
  };
4245
4233
  }
4246
- function toDetailedRunResultFromNodeStats(result, startedUtc, sinkErrors) {
4234
+ function toDetailedRunResultFromNodeStats(result, startedUtc, sinkErrors, policyErrors = []) {
4247
4235
  return attachRunResultAliases({
4248
4236
  startedUtc,
4249
4237
  completedUtc: result.completedUtc,
4238
+ allBytes: result.allBytes,
4250
4239
  allRequestCount: result.allRequestCount,
4251
4240
  allOkCount: result.allOkCount,
4252
4241
  allFailCount: result.allFailCount,
4253
4242
  failedThresholds: result.failedThresholds,
4243
+ durationMs: result.durationMs,
4254
4244
  nodeInfo: result.nodeInfo,
4255
4245
  testInfo: result.testInfo,
4246
+ thresholds: result.thresholds.map((value) => ({ ...value })),
4256
4247
  thresholdResults: result.thresholds.map((value) => ({ ...value })),
4248
+ metricStats: result.metrics,
4257
4249
  metrics: flattenMetricValues(result.metrics),
4258
4250
  scenarioStats: result.scenarioStats,
4259
4251
  stepStats: result.stepStats,
4252
+ scenarioDurationsMs: Object.fromEntries(result.scenarioStats.map((value) => [value.scenarioName, value.durationMs])),
4260
4253
  pluginsData: result.pluginsData.map((value) => normalizePluginData(value.pluginName, value)),
4261
4254
  disabledSinks: [...result.disabledSinks],
4262
4255
  sinkErrors: [...sinkErrors, ...result.sinkErrors],
4263
- reportFiles: [...result.reportFiles]
4256
+ policyErrors: policyErrors.map((value) => attachRuntimePolicyErrorAliases({ ...value })),
4257
+ reportFiles: [...result.reportFiles],
4258
+ logFiles: [...(result.logFiles ?? [])],
4259
+ correlationRows: buildDetailedCorrelationRows(),
4260
+ failedCorrelationRows: buildDetailedFailedCorrelationRows()
4264
4261
  });
4265
4262
  }
4266
4263
  function flattenMetricValues(metricStats) {
@@ -4353,6 +4350,7 @@ function clusterNodeResultToNodeStats(result, testInfo, fallbackNodeInfo) {
4353
4350
  disabledSinks: [],
4354
4351
  sinkErrors: [],
4355
4352
  reportFiles: [],
4353
+ logFiles: normalizeAliasStringArray(result.stats.logFiles),
4356
4354
  findScenarioStats: (scenarioName) => scenarioStats.find((value) => value.scenarioName === scenarioName),
4357
4355
  getScenarioStats: (scenarioName) => {
4358
4356
  const value = scenarioStats.find((scenario) => scenario.scenarioName === scenarioName);
@@ -4399,6 +4397,7 @@ function aggregateNodeStats(testInfo, coordinatorNodeInfo, nodes) {
4399
4397
  disabledSinks: [],
4400
4398
  sinkErrors: [],
4401
4399
  reportFiles: [],
4400
+ logFiles: mergeStringArrays(...nodes.map((value) => value.logFiles ?? [])),
4402
4401
  findScenarioStats: (scenarioName) => scenarioStats.find((value) => value.scenarioName === scenarioName),
4403
4402
  getScenarioStats: (scenarioName) => {
4404
4403
  const value = scenarioStats.find((scenario) => scenario.scenarioName === scenarioName);
@@ -4791,6 +4790,8 @@ class ManagedScenarioTrackingRuntime {
4791
4790
  this.timeoutSweepIntervalMs = Math.trunc(pickTrackingNumber(tracking, ["TimeoutSweepIntervalMs", "timeoutSweepIntervalMs"], pickTrackingNumber(tracking, ["TimeoutSweepIntervalSeconds", "timeoutSweepIntervalSeconds"], 1) * 1000));
4792
4791
  this.timeoutBatchSize = Math.trunc(pickTrackingNumber(tracking, ["TimeoutBatchSize", "timeoutBatchSize"], 200));
4793
4792
  this.timeoutCountsAsFailure = pickTrackingBoolean(tracking, "TimeoutCountsAsFailure", "timeoutCountsAsFailure", true);
4793
+ const trackingFieldValueCaseSensitive = pickTrackingBoolean(tracking, "TrackingFieldValueCaseSensitive", "trackingFieldValueCaseSensitive", true);
4794
+ const gatherByFieldValueCaseSensitive = pickTrackingBoolean(tracking, "GatherByFieldValueCaseSensitive", "gatherByFieldValueCaseSensitive", true);
4794
4795
  this.sourceAdapter = EndpointAdapterFactory.create(this.sourceEndpoint);
4795
4796
  this.destinationAdapter = this.destinationEndpoint ? EndpointAdapterFactory.create(this.destinationEndpoint) : null;
4796
4797
  this.correlationStore = mapRuntimeCorrelationStore(tracking, runNamespace);
@@ -4798,18 +4799,20 @@ class ManagedScenarioTrackingRuntime {
4798
4799
  sourceTrackingField: this.sourceEndpoint.trackingField,
4799
4800
  destinationTrackingField: this.destinationEndpoint?.trackingField,
4800
4801
  destinationGatherByField: this.destinationEndpoint?.gatherByField,
4802
+ trackingFieldValueCaseSensitive,
4803
+ gatherByFieldValueCaseSensitive,
4801
4804
  correlationTimeoutMs: this.correlationTimeoutMs,
4802
4805
  timeoutCountsAsFailure: this.timeoutCountsAsFailure,
4803
4806
  store: this.correlationStore ?? undefined,
4804
4807
  plugins: [
4805
4808
  {
4806
- onMatched: async (trackingId, _source, destination, latencyMs) => {
4809
+ onMatched: async (trackingId, _source, destination, latencyMs, gatherByValue) => {
4807
4810
  const eventId = this.shiftPendingValue(this.pendingEventIds, trackingId);
4808
4811
  const sourceTimestampUtc = this.shiftPendingValue(this.pendingSourceTimestamps, trackingId);
4809
4812
  const destinationTimestampUtc = new Date().toISOString();
4810
- const gatherByValue = this.destinationEndpoint?.gatherByField
4813
+ const observedGatherByValue = gatherByValue ?? (this.destinationEndpoint?.gatherByField
4811
4814
  ? readRuntimeTrackingId(destination, this.destinationEndpoint.gatherByField)
4812
- : undefined;
4815
+ : undefined);
4813
4816
  addCorrelationRow({
4814
4817
  occurredUtc: new Date().toISOString(),
4815
4818
  scenarioName: this.scenarioName,
@@ -4825,7 +4828,7 @@ class ManagedScenarioTrackingRuntime {
4825
4828
  isSuccess: true,
4826
4829
  isFailure: false,
4827
4830
  gatherByField: this.gatherByFieldExpression,
4828
- gatherByValue: gatherByValue ?? undefined
4831
+ gatherByValue: observedGatherByValue ?? undefined
4829
4832
  });
4830
4833
  this.resolveTrackingWaiter(trackingId, {
4831
4834
  status: "matched",
@@ -5596,10 +5599,10 @@ function normalizeRuntimeObservedTrackingPayload(payload, endpoint) {
5596
5599
  function readRuntimeTrackingId(payload, selector) {
5597
5600
  const normalized = selector.trim().toLowerCase();
5598
5601
  if (normalized.startsWith("header:")) {
5599
- const headerName = selector.slice("header:".length).trim().toLowerCase();
5602
+ const headerName = selector.slice("header:".length).trim();
5600
5603
  for (const [key, value] of Object.entries(payload.headers ?? {})) {
5601
5604
  const resolved = String(value ?? "").trim();
5602
- if (key.toLowerCase() === headerName && resolved) {
5605
+ if (key === headerName && resolved) {
5603
5606
  return resolved;
5604
5607
  }
5605
5608
  }
@@ -5875,6 +5878,49 @@ function createLogger(loggerConfig, minimumLogLevel) {
5875
5878
  };
5876
5879
  }
5877
5880
  }
5881
+ return wrapLoggerWithMinimumLevel(baseLogger, minimumLogLevel);
5882
+ }
5883
+ function createLoggerSetup(loggerConfig, minimumLogLevel, options, testInfo, nodeInfo) {
5884
+ if (typeof loggerConfig === "function") {
5885
+ return {
5886
+ logger: createLogger(loggerConfig, minimumLogLevel),
5887
+ logFiles: []
5888
+ };
5889
+ }
5890
+ const logFilePath = resolveDefaultLogFilePath(options, testInfo, nodeInfo);
5891
+ mkdirSync(resolve(options.reportFolderPath ?? "./reports"), { recursive: true });
5892
+ writeFileSync(logFilePath, "", "utf8");
5893
+ return {
5894
+ logger: wrapLoggerWithMinimumLevel(createDefaultLogger(logFilePath), minimumLogLevel),
5895
+ logFiles: [logFilePath]
5896
+ };
5897
+ }
5898
+ function createDefaultLogger(logFilePath) {
5899
+ const write = (level, message) => {
5900
+ const line = formatDefaultLoggerLine(level, message);
5901
+ const output = `${line}\n`;
5902
+ if (level === "debug") {
5903
+ console.debug(line);
5904
+ }
5905
+ else if (level === "info") {
5906
+ console.info(line);
5907
+ }
5908
+ else if (level === "warn") {
5909
+ console.warn(line);
5910
+ }
5911
+ else {
5912
+ console.error(line);
5913
+ }
5914
+ appendFileSync(logFilePath, output, "utf8");
5915
+ };
5916
+ return {
5917
+ debug: (message) => write("debug", message),
5918
+ info: (message) => write("info", message),
5919
+ warn: (message) => write("warn", message),
5920
+ error: (message) => write("error", message)
5921
+ };
5922
+ }
5923
+ function wrapLoggerWithMinimumLevel(baseLogger, minimumLogLevel) {
5878
5924
  const threshold = logLevelOrder(minimumLogLevel);
5879
5925
  return {
5880
5926
  debug: (message) => {
@@ -5899,6 +5945,16 @@ function createLogger(loggerConfig, minimumLogLevel) {
5899
5945
  }
5900
5946
  };
5901
5947
  }
5948
+ function formatDefaultLoggerLine(level, message) {
5949
+ const code = level === "debug"
5950
+ ? "DBG"
5951
+ : level === "info"
5952
+ ? "INF"
5953
+ : level === "warn"
5954
+ ? "WRN"
5955
+ : "ERR";
5956
+ return `${new Date().toISOString()} [${code}] ${message}`;
5957
+ }
5902
5958
  function logLevelOrder(level) {
5903
5959
  const normalized = String(level ?? "").trim().toLowerCase();
5904
5960
  if (!normalized) {
@@ -5930,11 +5986,41 @@ function formatUtcReportTimestamp(value) {
5930
5986
  const second = String(value.getUTCSeconds()).padStart(2, "0");
5931
5987
  return `${year}${month}${day}_${hour}${minute}${second}`;
5932
5988
  }
5989
+ function resolveDefaultReportTimestamp(result) {
5990
+ const createdUtc = result.testInfo?.createdUtc ?? result.startedUtc;
5991
+ const parsed = createdUtc ? new Date(createdUtc) : undefined;
5992
+ return parsed && !Number.isNaN(parsed.getTime())
5993
+ ? formatUtcReportTimestamp(parsed)
5994
+ : formatUtcReportTimestamp(new Date());
5995
+ }
5996
+ function resolveDefaultLogFilePath(options, testInfo, nodeInfo) {
5997
+ const reportFolder = resolve(options.reportFolderPath ?? "./reports");
5998
+ const parsedCreatedUtc = testInfo.createdUtc ? new Date(testInfo.createdUtc) : undefined;
5999
+ const timestamp = parsedCreatedUtc && !Number.isNaN(parsedCreatedUtc.getTime())
6000
+ ? formatUtcReportTimestamp(parsedCreatedUtc)
6001
+ : formatUtcReportTimestamp(new Date());
6002
+ const suffixParts = [];
6003
+ if (nodeInfo.nodeType !== "SingleNode") {
6004
+ suffixParts.push(nodeInfo.nodeType === "Coordinator" ? "coordinator" : "agent");
6005
+ }
6006
+ const localMachineName = os.hostname().trim().toLowerCase();
6007
+ if (nodeInfo.machineName.trim() &&
6008
+ (nodeInfo.nodeType !== "SingleNode" || nodeInfo.machineName.trim().toLowerCase() !== localMachineName)) {
6009
+ suffixParts.push(nodeInfo.machineName);
6010
+ }
6011
+ const suffix = suffixParts.length
6012
+ ? `-${suffixParts.map((value) => sanitizeReportFileName(value)).join("-")}`
6013
+ : "";
6014
+ return resolve(reportFolder, sanitizeReportFileName(`loadstrike-log-${timestamp}${suffix}.txt`));
6015
+ }
5933
6016
  function sanitizeReportFileName(value) {
5934
6017
  const normalized = String(value ?? "");
5935
6018
  return normalized
5936
6019
  .replace(/[<>:"/\\|?*\u0000-\u001F]/g, "_") || "loadstrike-run";
5937
6020
  }
6021
+ function mergeStringArrays(...values) {
6022
+ return Array.from(new Set(values.flatMap((value) => (value ?? []).filter((entry) => entry.trim().length > 0))));
6023
+ }
5938
6024
  function loadJsonObject(path) {
5939
6025
  try {
5940
6026
  const raw = readFileSync(resolve(path), "utf8");
@@ -6171,6 +6257,16 @@ function normalizeRunnerOptionCollectionShapes(options) {
6171
6257
  validateNamedWorkerPlugins(normalized.workerPlugins ?? []);
6172
6258
  return normalized;
6173
6259
  }
6260
+ function normalizedRuntimePolicyErrorMode(value) {
6261
+ const normalized = String(value ?? "fail").trim().toLowerCase();
6262
+ if (normalized === "fail") {
6263
+ return "fail";
6264
+ }
6265
+ if (normalized === "continue") {
6266
+ return "continue";
6267
+ }
6268
+ throw new Error("Runtime policy error mode must be either Fail or Continue.");
6269
+ }
6174
6270
  function extractContextOverridesFromConfig(config) {
6175
6271
  const rootConfig = asRecord(config);
6176
6272
  const loadStrikeSection = asRecord(tryReadConfigValue(rootConfig, "LoadStrike"));
@@ -6224,6 +6320,7 @@ function extractContextOverridesFromConfig(config) {
6224
6320
  setString("AgentGroup", "AgentGroup", "LoadStrike:AgentGroup");
6225
6321
  setString("NatsServerUrl", "NatsServerUrl", "LoadStrike:NatsServerUrl");
6226
6322
  setString("RunnerKey", "RunnerKey", "LoadStrike:RunnerKey");
6323
+ setString("RuntimePolicyErrorMode", "RuntimePolicyErrorMode", "LoadStrike:RuntimePolicyErrorMode");
6227
6324
  const nodeType = pick("NodeType", "LoadStrike:NodeType");
6228
6325
  if (nodeType != null) {
6229
6326
  const parsed = tryParseNodeTypeToken(nodeType);
@@ -6238,7 +6335,6 @@ function extractContextOverridesFromConfig(config) {
6238
6335
  patch.MinimumLogLevel = parsed;
6239
6336
  }
6240
6337
  }
6241
- setString("LicenseValidationServerUrl", "LicenseValidationServerUrl", "LoadStrike:LicenseValidation:ServerUrl", "LicenseValidation:ServerUrl");
6242
6338
  const agentsCount = toInt(pick("AgentsCount", "LoadStrike:AgentsCount"));
6243
6339
  if (agentsCount > 0) {
6244
6340
  patch.AgentsCount = agentsCount;
@@ -6373,7 +6469,6 @@ function toRunContext(options) {
6373
6469
  CoordinatorTargetScenarios: normalized.coordinatorTargetScenarios,
6374
6470
  NatsServerUrl: normalized.natsServerUrl,
6375
6471
  RunnerKey: normalized.runnerKey,
6376
- LicenseValidationServerUrl: normalized.licenseValidationServerUrl,
6377
6472
  LicenseValidationTimeoutSeconds: normalized.licenseValidationTimeoutSeconds,
6378
6473
  ConfigPath: normalized.configPath,
6379
6474
  InfraConfigPath: normalized.infraConfigPath,
@@ -6386,8 +6481,6 @@ function toRunContext(options) {
6386
6481
  ReportFileName: normalized.reportFileName,
6387
6482
  ReportFolderPath: normalized.reportFolderPath,
6388
6483
  ReportFormats: normalized.reportFormats,
6389
- ReportFinalizer: normalized.reportFinalizer,
6390
- DetailedReportFinalizer: normalized.detailedReportFinalizer,
6391
6484
  ReportingIntervalSeconds: normalized.reportingIntervalSeconds,
6392
6485
  MinimumLogLevel: normalized.minimumLogLevel,
6393
6486
  LoggerConfig: normalized.loggerConfig,
@@ -6395,6 +6488,7 @@ function toRunContext(options) {
6395
6488
  SinkRetryCount: normalized.sinkRetryCount,
6396
6489
  SinkRetryBackoffMs: normalized.sinkRetryBackoffMs,
6397
6490
  RuntimePolicies: normalized.runtimePolicies,
6491
+ RuntimePolicyErrorMode: normalized.runtimePolicyErrorMode,
6398
6492
  ScenarioCompletionTimeoutSeconds: normalized.scenarioCompletionTimeoutSeconds,
6399
6493
  ClusterCommandTimeoutSeconds: normalized.clusterCommandTimeoutSeconds,
6400
6494
  RestartIterationMaxAttempts: normalized.restartIterationMaxAttempts,
@@ -6403,6 +6497,26 @@ function toRunContext(options) {
6403
6497
  GlobalCustomSettings: normalized.globalCustomSettings
6404
6498
  };
6405
6499
  }
6500
+ class RuntimePolicyCallbackError extends Error {
6501
+ constructor(message) {
6502
+ super(message);
6503
+ this.name = "RuntimePolicyCallbackError";
6504
+ }
6505
+ }
6506
+ function resolveRuntimePolicyName(policy) {
6507
+ const namedPolicy = policy.policyName ?? policy.PolicyName;
6508
+ if (typeof namedPolicy === "string" && namedPolicy.trim()) {
6509
+ return namedPolicy;
6510
+ }
6511
+ const constructorValue = Reflect.get(policy, "constructor");
6512
+ const constructorName = constructorValue && typeof constructorValue === "object"
6513
+ ? Reflect.get(constructorValue, "name")
6514
+ : typeof constructorValue === "function"
6515
+ ? Reflect.get(constructorValue, "name")
6516
+ : "";
6517
+ const fallback = typeof constructorName === "string" ? constructorName.trim() : "";
6518
+ return fallback || "runtime-policy";
6519
+ }
6406
6520
  function buildLicenseValidationPayload(options, scenarios) {
6407
6521
  const context = {
6408
6522
  ...toRunContext(options)
@@ -6416,8 +6530,7 @@ function buildLicenseValidationPayload(options, scenarios) {
6416
6530
  Weight: scenario.getWeight(),
6417
6531
  LoadSimulations: [...scenario.getSimulations()],
6418
6532
  Thresholds: [...scenario.getThresholds()],
6419
- Tracking: scenario.getTrackingConfiguration() ?? {},
6420
- LicenseFeatures: scenario.getLicenseFeatures()
6533
+ Tracking: scenario.getTrackingConfiguration() ?? {}
6421
6534
  }));
6422
6535
  return {
6423
6536
  Context: context,
@@ -6466,7 +6579,6 @@ function looksLikeRunContext(value) {
6466
6579
  "ClusterId",
6467
6580
  "CoordinatorTargetScenarios",
6468
6581
  "RunnerKey",
6469
- "LicenseValidationServerUrl",
6470
6582
  "LicenseValidationTimeoutSeconds",
6471
6583
  "MinimumLogLevel",
6472
6584
  "LoggerConfig",
@@ -6476,8 +6588,6 @@ function looksLikeRunContext(value) {
6476
6588
  "ReportFileName",
6477
6589
  "ReportFolderPath",
6478
6590
  "ReportFormats",
6479
- "ReportFinalizer",
6480
- "DetailedReportFinalizer",
6481
6591
  "ReportingIntervalSeconds",
6482
6592
  "ReportingSinks",
6483
6593
  "SinkRetryCount",
@@ -6614,12 +6724,6 @@ function resolveSinkSaveRealtimeMetrics(sink) {
6614
6724
  ? method.bind(sink)
6615
6725
  : undefined;
6616
6726
  }
6617
- function resolveSinkSaveFinalStats(sink) {
6618
- const method = sink.saveFinalStats ?? sink.SaveFinalStats;
6619
- return typeof method === "function"
6620
- ? method.bind(sink)
6621
- : undefined;
6622
- }
6623
6727
  function resolveSinkSaveRunResult(sink) {
6624
6728
  const method = sink.saveRunResult ?? sink.SaveRunResult;
6625
6729
  return typeof method === "function"
@@ -6658,6 +6762,46 @@ function addCorrelationRow(row) {
6658
6762
  correlationRows.splice(0, correlationRows.length - MAX_CORRELATION_ROWS);
6659
6763
  }
6660
6764
  }
6765
+ function buildDetailedFailedCorrelationRows() {
6766
+ return [...failedResponseRows]
6767
+ .reverse()
6768
+ .map((row) => ({
6769
+ OccurredUtc: row.occurredUtc,
6770
+ Scenario: row.scenarioName,
6771
+ Source: row.sourceEndpoint,
6772
+ Destination: row.destinationEndpoint,
6773
+ RunMode: row.runMode,
6774
+ StatusCode: row.statusCode,
6775
+ TrackingId: row.trackingId ?? "",
6776
+ EventId: row.eventId ?? "",
6777
+ SourceTimestampUtc: row.sourceTimestampUtc ?? "",
6778
+ DestinationTimestampUtc: row.destinationTimestampUtc ?? "",
6779
+ LatencyMs: formatOptionalLatency(row.latencyMs),
6780
+ Message: row.message ?? ""
6781
+ }));
6782
+ }
6783
+ function buildDetailedCorrelationRows() {
6784
+ return [...correlationRows]
6785
+ .reverse()
6786
+ .map((row) => ({
6787
+ OccurredUtc: row.occurredUtc,
6788
+ Scenario: row.scenarioName,
6789
+ Source: row.sourceEndpoint,
6790
+ Destination: row.destinationEndpoint,
6791
+ RunMode: row.runMode,
6792
+ StatusCode: row.statusCode,
6793
+ IsSuccess: row.isSuccess,
6794
+ IsFailure: row.isFailure,
6795
+ GatherByField: row.gatherByField ?? "",
6796
+ GatherByValue: row.gatherByValue ?? "",
6797
+ TrackingId: row.trackingId ?? "",
6798
+ EventId: row.eventId ?? "",
6799
+ SourceTimestampUtc: row.sourceTimestampUtc ?? "",
6800
+ DestinationTimestampUtc: row.destinationTimestampUtc ?? "",
6801
+ LatencyMs: formatOptionalLatency(row.latencyMs),
6802
+ Message: row.message ?? ""
6803
+ }));
6804
+ }
6661
6805
  function resolveWorkerPlugins(customPlugins) {
6662
6806
  const registry = new Map();
6663
6807
  for (const plugin of createBuiltInWorkerPlugins()) {
@@ -6990,3 +7134,122 @@ function readConfiguredSinkName(sink) {
6990
7134
  async function sleep(ms) {
6991
7135
  await new Promise((resolve) => setTimeout(resolve, Math.max(ms, 0)));
6992
7136
  }
7137
+ export const __loadstrikeTestExports = {
7138
+ LoadStrikeContext,
7139
+ LoadStrikeCounter,
7140
+ LoadStrikeGauge,
7141
+ LoadStrikeMetric,
7142
+ LoadStrikeResponse,
7143
+ LoadStrikeRunner,
7144
+ LoadStrikeScenario,
7145
+ LoadStrikeSimulation,
7146
+ LoadStrikeStep,
7147
+ ManagedScenarioTrackingRuntime,
7148
+ ScenarioStatsAccumulator,
7149
+ StepStatsAccumulator,
7150
+ TrackingFieldSelector,
7151
+ addCorrelationRow,
7152
+ addFailedResponseRow,
7153
+ aggregateNodeStats,
7154
+ asRecord,
7155
+ assertNoDisableLicenseEnforcementOption,
7156
+ buildEmptyNodeStats,
7157
+ buildGroupedCorrelationRows,
7158
+ buildMeasurementPlaceholder,
7159
+ buildRichHtmlReport,
7160
+ buildThresholdCheckExpression,
7161
+ buildTrackingLeaseKey,
7162
+ buildTrackingRunNamespace,
7163
+ clusterNodeResultToNodeStats,
7164
+ combineAbortSignals,
7165
+ computeScenarioRequestCount,
7166
+ computeWarmUpIterations,
7167
+ createBuiltInWorkerPlugins,
7168
+ createLogger,
7169
+ createRuntimeRandom,
7170
+ delayWithAbort,
7171
+ detailedToNodeStats,
7172
+ evaluateThresholdsForScenarios,
7173
+ executeTrackedScenarioInvocation,
7174
+ extractContextOverridesFromArgs,
7175
+ extractContextOverridesFromConfig,
7176
+ findCaseInsensitiveKey,
7177
+ formatOptionalLatency,
7178
+ formatUtcReportTimestamp,
7179
+ hasPluginRows,
7180
+ inferRuntimeLegacyHttpResponseSource,
7181
+ isComparisonFailed,
7182
+ loadJsonObject,
7183
+ logLevelOrder,
7184
+ looksLikeRunContext,
7185
+ mapRuntimeCorrelationStore,
7186
+ mapRuntimeTrackingEndpointSpec,
7187
+ mergeDefinedRecord,
7188
+ normalizeCliOverrideKey,
7189
+ normalizeMetricValue,
7190
+ normalizeOptionalReportFormats,
7191
+ normalizeOptionalStringArray,
7192
+ normalizeReplyArguments,
7193
+ normalizePluginData,
7194
+ normalizeReportFormats,
7195
+ normalizeRunArgsInput,
7196
+ normalizeRunContextCollectionShapes,
7197
+ normalizeRunnerOptionCollectionShapes,
7198
+ normalizeRuntimeHttpTrackingPayloadSource,
7199
+ normalizeRuntimeTrackingPayload,
7200
+ normalizeStringArray,
7201
+ normalizeFailureArguments,
7202
+ normalizeTrackingRunMode,
7203
+ normalizeThresholdScope,
7204
+ parseAliasDate,
7205
+ parseMinimumLogLevelToken,
7206
+ parseNodeTypeToken,
7207
+ parseStrictBooleanToken,
7208
+ percentile,
7209
+ pickOptionalTrackingSelectorString,
7210
+ pickTrackingNumber,
7211
+ produceOrConsumeTrackingPayload,
7212
+ readConfiguredSinkName,
7213
+ readRuntimeRedisCorrelationStoreOptions,
7214
+ readRuntimeTrackingId,
7215
+ recordPluginLifecycleError,
7216
+ requireNonEmpty,
7217
+ attachScenarioStatsAliases,
7218
+ attachNodeStatsAliases,
7219
+ attachRunResultAliases,
7220
+ resolveClusterExecutionMode,
7221
+ resolveThresholdActualValue,
7222
+ resetCrossPlatformReportRegistries,
7223
+ resolveSinkDispose,
7224
+ resolveSinkInit,
7225
+ resolveSinkName,
7226
+ resolveSinkSaveRealtimeMetrics,
7227
+ resolveSinkSaveRealtimeStats,
7228
+ resolveSinkSaveRunResult,
7229
+ resolveSinkStart,
7230
+ resolveSinkStop,
7231
+ resolveWorkerPlugins,
7232
+ runtimeParseBodyAsObject,
7233
+ sanitizeReportFileName,
7234
+ sanitizeTrackingNamespacePart,
7235
+ setRuntimeJsonPathValue,
7236
+ stripCaseInsensitivePrefix,
7237
+ toBoolean,
7238
+ toDetailedRunResultFromNodeStats,
7239
+ toInt,
7240
+ toNullableInt,
7241
+ toNumber,
7242
+ toRunContext,
7243
+ toTrackingStringMap,
7244
+ tryParseDotnetDurationMs,
7245
+ tryParseMinimumLogLevelToken,
7246
+ tryParseNodeTypeToken,
7247
+ tryReadConfigValue,
7248
+ validateNamedReportingSinks,
7249
+ validateRegisteredScenarios,
7250
+ validateRuntimeRedisCorrelationStoreConfiguration,
7251
+ validateRuntimeTrackingConfiguration,
7252
+ validateScenarioNames,
7253
+ waitForTrackingOutcome,
7254
+ withLifecycleErrors
7255
+ };