@loadstrike/loadstrike-sdk 1.0.20801 → 1.0.21101
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.
- package/dist/cjs/cluster.js +27 -2
- package/dist/cjs/correlation.js +132 -0
- package/dist/cjs/local.js +113 -6
- package/dist/cjs/reporting.js +12 -0
- package/dist/cjs/runtime.js +929 -14
- package/dist/esm/cluster.js +27 -2
- package/dist/esm/correlation.js +132 -0
- package/dist/esm/local.js +113 -6
- package/dist/esm/reporting.js +12 -0
- package/dist/esm/runtime.js +929 -14
- package/dist/types/cluster.d.ts +21 -1
- package/dist/types/correlation.d.ts +132 -0
- package/dist/types/local.d.ts +6 -0
- package/dist/types/reporting.d.ts +12 -0
- package/dist/types/runtime.d.ts +868 -0
- package/package.json +1 -1
package/dist/cjs/cluster.js
CHANGED
|
@@ -5,6 +5,10 @@ exports.planAgentScenarioAssignments = planAgentScenarioAssignments;
|
|
|
5
5
|
const node_crypto_1 = require("node:crypto");
|
|
6
6
|
const transports_js_1 = require("./transports.js");
|
|
7
7
|
class LocalClusterCoordinator {
|
|
8
|
+
/**
|
|
9
|
+
* Exposes the public constructor operation.
|
|
10
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
11
|
+
*/
|
|
8
12
|
constructor(options = {}) {
|
|
9
13
|
this.options = options;
|
|
10
14
|
}
|
|
@@ -64,6 +68,9 @@ class LocalClusterCoordinator {
|
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
exports.LocalClusterCoordinator = LocalClusterCoordinator;
|
|
71
|
+
/**
|
|
72
|
+
* Exposes the plan agent scenario assignments operation. Use this when interacting with the SDK through this surface.
|
|
73
|
+
*/
|
|
67
74
|
function planAgentScenarioAssignments(scenarioNames, agentCount, options = {}) {
|
|
68
75
|
const totalAgents = Math.max(agentCount, 0);
|
|
69
76
|
const assignments = Array.from({ length: totalAgents }, () => []);
|
|
@@ -119,10 +126,14 @@ function planAgentScenarioAssignments(scenarioNames, agentCount, options = {}) {
|
|
|
119
126
|
return assignments;
|
|
120
127
|
}
|
|
121
128
|
class DistributedClusterCoordinator {
|
|
129
|
+
/**
|
|
130
|
+
* Exposes the public constructor operation.
|
|
131
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
132
|
+
*/
|
|
122
133
|
constructor(options) {
|
|
123
134
|
this.options = options;
|
|
124
135
|
}
|
|
125
|
-
async dispatch(assignments) {
|
|
136
|
+
async dispatch(assignments, buildAgentRunToken) {
|
|
126
137
|
const expected = Math.max(this.options.expectedAgentResults, 0);
|
|
127
138
|
const timeoutMs = Math.max(this.options.commandTimeoutMs ?? 120000, 1);
|
|
128
139
|
const runSubject = this.buildRunSubject();
|
|
@@ -144,6 +155,9 @@ class DistributedClusterCoordinator {
|
|
|
144
155
|
targetScenarios: assignments[i] ?? [],
|
|
145
156
|
replySubject
|
|
146
157
|
};
|
|
158
|
+
if (buildAgentRunToken) {
|
|
159
|
+
command.agentRunToken = await buildAgentRunToken(command);
|
|
160
|
+
}
|
|
147
161
|
await commandProducer.produce({
|
|
148
162
|
headers: {
|
|
149
163
|
"x-cluster-command-id": commandId,
|
|
@@ -205,6 +219,10 @@ class DistributedClusterCoordinator {
|
|
|
205
219
|
}
|
|
206
220
|
exports.DistributedClusterCoordinator = DistributedClusterCoordinator;
|
|
207
221
|
class DistributedClusterAgent {
|
|
222
|
+
/**
|
|
223
|
+
* Exposes the public constructor operation.
|
|
224
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
225
|
+
*/
|
|
208
226
|
constructor(options) {
|
|
209
227
|
this.commandConsumer = null;
|
|
210
228
|
this.options = options;
|
|
@@ -254,7 +272,13 @@ class DistributedClusterAgent {
|
|
|
254
272
|
try {
|
|
255
273
|
let result;
|
|
256
274
|
try {
|
|
257
|
-
const nodeResult = await execute({
|
|
275
|
+
const nodeResult = await execute({
|
|
276
|
+
scenarioNames: command.targetScenarios,
|
|
277
|
+
commandId: command.commandId,
|
|
278
|
+
agentRunToken: command.agentRunToken,
|
|
279
|
+
agentIndex: command.agentIndex,
|
|
280
|
+
agentCount: command.agentCount
|
|
281
|
+
});
|
|
258
282
|
result = {
|
|
259
283
|
commandId: command.commandId,
|
|
260
284
|
agentId: this.options.agentId,
|
|
@@ -305,6 +329,7 @@ function parseRunCommand(value) {
|
|
|
305
329
|
agentIndex: numberOrDefault(value.agentIndex, numberOrDefault(value.AgentIndex, 0)),
|
|
306
330
|
agentCount: numberOrDefault(value.agentCount, numberOrDefault(value.AgentCount, 0)),
|
|
307
331
|
targetScenarios,
|
|
332
|
+
agentRunToken: stringOrDefault(value.agentRunToken, stringOrDefault(value.AgentRunToken, "")),
|
|
308
333
|
replySubject: stringOrDefault(value.replySubject, stringOrDefault(value.ReplySubject, ""))
|
|
309
334
|
};
|
|
310
335
|
}
|
package/dist/cjs/correlation.js
CHANGED
|
@@ -4,6 +4,10 @@ exports.__loadstrikeTestExports = exports.CrossPlatformTrackingRuntime = exports
|
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const redis_1 = require("redis");
|
|
6
6
|
class TrackingPayloadBuilder {
|
|
7
|
+
/**
|
|
8
|
+
* Exposes the public constructor operation.
|
|
9
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
10
|
+
*/
|
|
7
11
|
constructor() {
|
|
8
12
|
this.Headers = {};
|
|
9
13
|
this.body = new Uint8Array();
|
|
@@ -11,12 +15,24 @@ class TrackingPayloadBuilder {
|
|
|
11
15
|
get Body() {
|
|
12
16
|
return new Uint8Array(this.body);
|
|
13
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Sets the payload body on the builder.
|
|
20
|
+
* Use this when a tracking payload should be created from raw content.
|
|
21
|
+
*/
|
|
14
22
|
setBody(body) {
|
|
15
23
|
this.body = normalizeTrackingBodyToBytes(body);
|
|
16
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Sets the payload body on the builder.
|
|
27
|
+
* Use this when a tracking payload should be created from raw content.
|
|
28
|
+
*/
|
|
17
29
|
SetBody(body) {
|
|
18
30
|
this.setBody(body);
|
|
19
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Builds the configured payload or helper object.
|
|
34
|
+
* Use this when all builder inputs are ready to be materialized.
|
|
35
|
+
*/
|
|
20
36
|
build() {
|
|
21
37
|
return createTrackingPayload({
|
|
22
38
|
headers: { ...this.Headers },
|
|
@@ -27,6 +43,10 @@ class TrackingPayloadBuilder {
|
|
|
27
43
|
jsonConvertSettings: cloneRecord(this.JsonConvertSettings)
|
|
28
44
|
});
|
|
29
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Builds the configured payload or helper object.
|
|
48
|
+
* Use this when all builder inputs are ready to be materialized.
|
|
49
|
+
*/
|
|
30
50
|
Build() {
|
|
31
51
|
return this.build();
|
|
32
52
|
}
|
|
@@ -45,6 +65,10 @@ class RedisCorrelationStoreOptions {
|
|
|
45
65
|
set EntryTtl(value) {
|
|
46
66
|
this.EntryTtlSeconds = value;
|
|
47
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Exposes the public validate operation.
|
|
70
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
71
|
+
*/
|
|
48
72
|
validate() {
|
|
49
73
|
if (!this.ConnectionString.trim()) {
|
|
50
74
|
throw new Error("Redis ConnectionString must be provided.");
|
|
@@ -56,6 +80,10 @@ class RedisCorrelationStoreOptions {
|
|
|
56
80
|
throw new RangeError("Redis EntryTtl must be greater than zero.");
|
|
57
81
|
}
|
|
58
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Exposes the public Validate operation.
|
|
85
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
86
|
+
*/
|
|
59
87
|
Validate() {
|
|
60
88
|
this.validate();
|
|
61
89
|
}
|
|
@@ -65,12 +93,24 @@ class CorrelationStoreConfiguration {
|
|
|
65
93
|
constructor() {
|
|
66
94
|
this.Kind = "InMemory";
|
|
67
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Creates an in-memory correlation store configuration.
|
|
98
|
+
* Use this when tracking state can stay local to the process.
|
|
99
|
+
*/
|
|
68
100
|
static inMemory() {
|
|
69
101
|
return new CorrelationStoreConfiguration();
|
|
70
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Creates an in-memory correlation store configuration.
|
|
105
|
+
* Use this when tracking state can stay local to the process.
|
|
106
|
+
*/
|
|
71
107
|
static InMemory() {
|
|
72
108
|
return CorrelationStoreConfiguration.inMemory();
|
|
73
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Creates a Redis-backed correlation store configuration.
|
|
112
|
+
* Use this when tracking state must survive across processes or cluster nodes.
|
|
113
|
+
*/
|
|
74
114
|
static redisStore(options) {
|
|
75
115
|
if (!(options instanceof RedisCorrelationStoreOptions)) {
|
|
76
116
|
throw new TypeError("Redis correlation store options must be provided.");
|
|
@@ -80,9 +120,17 @@ class CorrelationStoreConfiguration {
|
|
|
80
120
|
configuration.Redis = options;
|
|
81
121
|
return configuration;
|
|
82
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Creates a Redis-backed correlation store configuration.
|
|
125
|
+
* Use this when tracking state must survive across processes or cluster nodes.
|
|
126
|
+
*/
|
|
83
127
|
static RedisStore(options) {
|
|
84
128
|
return CorrelationStoreConfiguration.redisStore(options);
|
|
85
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Exposes the public validate operation.
|
|
132
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
133
|
+
*/
|
|
86
134
|
validate() {
|
|
87
135
|
if (this.Kind === "Redis") {
|
|
88
136
|
if (!(this.Redis instanceof RedisCorrelationStoreOptions)) {
|
|
@@ -91,6 +139,10 @@ class CorrelationStoreConfiguration {
|
|
|
91
139
|
this.Redis.validate();
|
|
92
140
|
}
|
|
93
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Exposes the public Validate operation.
|
|
144
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
145
|
+
*/
|
|
94
146
|
Validate() {
|
|
95
147
|
this.validate();
|
|
96
148
|
}
|
|
@@ -103,6 +155,10 @@ class InMemoryCorrelationStore {
|
|
|
103
155
|
this.sourceOccurrences = new Map();
|
|
104
156
|
this.destinationOccurrences = new Map();
|
|
105
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Sets the current gauge value.
|
|
160
|
+
* Use this when the latest observed value should replace the previous one.
|
|
161
|
+
*/
|
|
106
162
|
set(entry) {
|
|
107
163
|
const normalized = normalizeCorrelationEntry(entry);
|
|
108
164
|
const queue = this.pendingByTrackingId.get(normalized.trackingId) ?? [];
|
|
@@ -110,10 +166,18 @@ class InMemoryCorrelationStore {
|
|
|
110
166
|
this.pendingByTrackingId.set(normalized.trackingId, queue);
|
|
111
167
|
this.pendingByEventId.set(normalized.eventId ?? "", normalized);
|
|
112
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Exposes the public get operation.
|
|
171
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
172
|
+
*/
|
|
113
173
|
get(trackingId) {
|
|
114
174
|
const queue = this.pendingByTrackingId.get(trackingId);
|
|
115
175
|
return queue && queue.length > 0 ? hydrateCorrelationEntry(queue[0]) : null;
|
|
116
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Exposes the public delete operation.
|
|
179
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
180
|
+
*/
|
|
117
181
|
delete(trackingId) {
|
|
118
182
|
const queue = this.pendingByTrackingId.get(trackingId);
|
|
119
183
|
if (!queue || queue.length === 0) {
|
|
@@ -129,6 +193,10 @@ class InMemoryCorrelationStore {
|
|
|
129
193
|
}
|
|
130
194
|
this.pendingByTrackingId.set(trackingId, queue);
|
|
131
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Exposes the public all operation.
|
|
198
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
199
|
+
*/
|
|
132
200
|
all() {
|
|
133
201
|
const entries = [];
|
|
134
202
|
for (const queue of this.pendingByTrackingId.values()) {
|
|
@@ -138,21 +206,37 @@ class InMemoryCorrelationStore {
|
|
|
138
206
|
}
|
|
139
207
|
return entries;
|
|
140
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Exposes the public collectExpired operation.
|
|
211
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
212
|
+
*/
|
|
141
213
|
collectExpired(nowMs = Date.now(), maxCount = 0) {
|
|
142
214
|
void nowMs;
|
|
143
215
|
void maxCount;
|
|
144
216
|
return [];
|
|
145
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Exposes the public incrementSourceOccurrences operation.
|
|
220
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
221
|
+
*/
|
|
146
222
|
incrementSourceOccurrences(trackingId) {
|
|
147
223
|
const next = (this.sourceOccurrences.get(trackingId) ?? 0) + 1;
|
|
148
224
|
this.sourceOccurrences.set(trackingId, next);
|
|
149
225
|
return next;
|
|
150
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Exposes the public incrementDestinationOccurrences operation.
|
|
229
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
230
|
+
*/
|
|
151
231
|
incrementDestinationOccurrences(trackingId) {
|
|
152
232
|
const next = (this.destinationOccurrences.get(trackingId) ?? 0) + 1;
|
|
153
233
|
this.destinationOccurrences.set(trackingId, next);
|
|
154
234
|
return next;
|
|
155
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Exposes the public registerSource operation.
|
|
238
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
239
|
+
*/
|
|
156
240
|
registerSource(trackingId, sourceTimestampUtc) {
|
|
157
241
|
const entry = normalizeCorrelationEntry({
|
|
158
242
|
trackingId,
|
|
@@ -165,6 +249,10 @@ class InMemoryCorrelationStore {
|
|
|
165
249
|
this.set(entry);
|
|
166
250
|
return entry;
|
|
167
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Exposes the public tryMatchDestination operation.
|
|
254
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
255
|
+
*/
|
|
168
256
|
tryMatchDestination(trackingId, destinationTimestampUtc) {
|
|
169
257
|
const source = this.get(trackingId);
|
|
170
258
|
if (!source) {
|
|
@@ -176,6 +264,10 @@ class InMemoryCorrelationStore {
|
|
|
176
264
|
destinationTimestampUtc
|
|
177
265
|
});
|
|
178
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Exposes the public tryExpire operation.
|
|
269
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
270
|
+
*/
|
|
179
271
|
tryExpire(eventId) {
|
|
180
272
|
const entry = this.pendingByEventId.get(eventId);
|
|
181
273
|
if (!entry) {
|
|
@@ -518,10 +610,18 @@ class RedisCorrelationStore {
|
|
|
518
610
|
}
|
|
519
611
|
exports.RedisCorrelationStore = RedisCorrelationStore;
|
|
520
612
|
class TrackingFieldSelector {
|
|
613
|
+
/**
|
|
614
|
+
* Exposes the public constructor operation.
|
|
615
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
616
|
+
*/
|
|
521
617
|
constructor(location, path) {
|
|
522
618
|
this.kind = normalizeTrackingFieldLocation(location);
|
|
523
619
|
this.path = String(path ?? "");
|
|
524
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* Parses a selector or configuration expression.
|
|
623
|
+
* Use this when a tracking selector should be created from its text form.
|
|
624
|
+
*/
|
|
525
625
|
static parse(value) {
|
|
526
626
|
const normalized = (value ?? "").trim();
|
|
527
627
|
const [kindRaw, ...rest] = normalized.split(":");
|
|
@@ -531,9 +631,17 @@ class TrackingFieldSelector {
|
|
|
531
631
|
}
|
|
532
632
|
return new TrackingFieldSelector(kindRaw.trim(), path);
|
|
533
633
|
}
|
|
634
|
+
/**
|
|
635
|
+
* Parses a selector or configuration expression.
|
|
636
|
+
* Use this when a tracking selector should be created from its text form.
|
|
637
|
+
*/
|
|
534
638
|
static Parse(value) {
|
|
535
639
|
return TrackingFieldSelector.parse(value);
|
|
536
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Attempts to parse a selector or configuration expression.
|
|
643
|
+
* Use this when invalid input should be handled without throwing.
|
|
644
|
+
*/
|
|
537
645
|
static tryParse(value) {
|
|
538
646
|
try {
|
|
539
647
|
return {
|
|
@@ -548,6 +656,10 @@ class TrackingFieldSelector {
|
|
|
548
656
|
};
|
|
549
657
|
}
|
|
550
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Attempts to parse a selector or configuration expression.
|
|
661
|
+
* Use this when invalid input should be handled without throwing.
|
|
662
|
+
*/
|
|
551
663
|
static TryParse(value) {
|
|
552
664
|
return TrackingFieldSelector.tryParse(value);
|
|
553
665
|
}
|
|
@@ -557,6 +669,10 @@ class TrackingFieldSelector {
|
|
|
557
669
|
get Path() {
|
|
558
670
|
return this.path;
|
|
559
671
|
}
|
|
672
|
+
/**
|
|
673
|
+
* Extracts a value from the provided payload.
|
|
674
|
+
* Use this when a tracking selector should read data from a transport payload.
|
|
675
|
+
*/
|
|
560
676
|
extract(payload) {
|
|
561
677
|
if (this.kind === "header") {
|
|
562
678
|
const headers = payload.headers ?? {};
|
|
@@ -586,9 +702,17 @@ class TrackingFieldSelector {
|
|
|
586
702
|
}
|
|
587
703
|
return String(current);
|
|
588
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Exposes the public toString operation.
|
|
707
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
708
|
+
*/
|
|
589
709
|
toString() {
|
|
590
710
|
return `${this.kind}:${this.path}`;
|
|
591
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Exposes the public ToString operation.
|
|
714
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
715
|
+
*/
|
|
592
716
|
ToString() {
|
|
593
717
|
return this.toString();
|
|
594
718
|
}
|
|
@@ -602,6 +726,10 @@ function normalizeTrackingFieldLocation(location) {
|
|
|
602
726
|
throw new Error("Tracking field location must be Header or Json.");
|
|
603
727
|
}
|
|
604
728
|
class CrossPlatformTrackingRuntime {
|
|
729
|
+
/**
|
|
730
|
+
* Exposes the public constructor operation.
|
|
731
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
732
|
+
*/
|
|
605
733
|
constructor(options) {
|
|
606
734
|
this.trackingIdDisplayByEventId = new Map();
|
|
607
735
|
this.gatherByDisplayByComparisonKey = new Map();
|
|
@@ -819,6 +947,10 @@ class CrossPlatformTrackingRuntime {
|
|
|
819
947
|
this.gatherByDisplayByComparisonKey.set(comparisonKey, gatherKey);
|
|
820
948
|
return gatherKey;
|
|
821
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Exposes the public isTimeoutCountedAsFailure operation.
|
|
952
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
953
|
+
*/
|
|
822
954
|
isTimeoutCountedAsFailure() {
|
|
823
955
|
return this.timeoutCountsAsFailure;
|
|
824
956
|
}
|
package/dist/cjs/local.js
CHANGED
|
@@ -86,6 +86,10 @@ const CI_ENVIRONMENT_VARIABLES = [
|
|
|
86
86
|
"HEROKU_TEST_RUN_ID"
|
|
87
87
|
];
|
|
88
88
|
class LoadStrikeLocalClient {
|
|
89
|
+
/**
|
|
90
|
+
* Exposes the public constructor operation.
|
|
91
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
92
|
+
*/
|
|
89
93
|
constructor(options = {}) {
|
|
90
94
|
this.signingKeyCache = new Map();
|
|
91
95
|
assertNoDisableLicenseEnforcementOption(options, "LoadStrikeLocalClient");
|
|
@@ -152,6 +156,17 @@ class LoadStrikeLocalClient {
|
|
|
152
156
|
const context = asRecord(request.Context);
|
|
153
157
|
const nodeType = normalizeNodeType(pickValue(context, "NodeType", "nodeType"));
|
|
154
158
|
if (nodeType === "agent") {
|
|
159
|
+
const agentExecutionToken = stringOrDefault(pickValue(context, "AgentExecutionToken", "agentExecutionToken"), "").trim();
|
|
160
|
+
if (!agentExecutionToken) {
|
|
161
|
+
throw new Error("Agent node type cannot run directly. Start it from a coordinator session after controller license validation.");
|
|
162
|
+
}
|
|
163
|
+
const agentCommandId = stringOrDefault(pickValue(context, "AgentCommandId", "agentCommandId"), "").trim();
|
|
164
|
+
if (!agentCommandId) {
|
|
165
|
+
throw new Error("Agent execution is missing the coordinator command identifier.");
|
|
166
|
+
}
|
|
167
|
+
const requestedFeatures = collectRequestedFeatures(request);
|
|
168
|
+
const sessionId = stringOrDefault(pickValue(context, "SessionId", "sessionId"), "").trim();
|
|
169
|
+
await this.verifySignedAgentExecutionToken(agentExecutionToken, request, requestedFeatures, sessionId, agentCommandId, resolveTargetScenarioNames(context));
|
|
155
170
|
return {};
|
|
156
171
|
}
|
|
157
172
|
const runnerKey = stringOrDefault(context.RunnerKey, "").trim();
|
|
@@ -258,6 +273,92 @@ class LoadStrikeLocalClient {
|
|
|
258
273
|
enforceEntitlementClaims(parsed.payload, requestedFeatures);
|
|
259
274
|
enforceRuntimePolicyClaims(parsed.payload, request);
|
|
260
275
|
}
|
|
276
|
+
async createAgentExecutionToken(controllerRunToken, sessionId, commandId, agentIndex, agentCount, targetScenarios) {
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const timer = setTimeout(() => controller.abort(), this.licenseValidationTimeoutMs);
|
|
279
|
+
try {
|
|
280
|
+
const payload = {
|
|
281
|
+
ControllerRunToken: controllerRunToken,
|
|
282
|
+
SessionId: sessionId,
|
|
283
|
+
CommandId: commandId,
|
|
284
|
+
AgentIndex: agentIndex,
|
|
285
|
+
AgentCount: agentCount,
|
|
286
|
+
TargetScenarios: [...targetScenarios]
|
|
287
|
+
};
|
|
288
|
+
const { response, json } = await this.postLicensingRequest("/api/v1/licenses/agent-token", payload, controller.signal);
|
|
289
|
+
const agentPayload = json;
|
|
290
|
+
const isValid = pickValue(agentPayload, "IsValid", "isValid");
|
|
291
|
+
if (!response.ok || isValid !== true) {
|
|
292
|
+
const details = stringOrDefault(pickValue(agentPayload, "Message", "message"), "No additional details provided.");
|
|
293
|
+
const denialCode = stringOrDefault(pickValue(agentPayload, "DenialCode", "denialCode"), "unknown_denial");
|
|
294
|
+
throw new Error(`Agent execution authorization denied. Reason: ${denialCode}. ${details}`);
|
|
295
|
+
}
|
|
296
|
+
const agentRunToken = stringOrDefault(pickValue(agentPayload, "AgentRunToken", "agentRunToken"), "").trim();
|
|
297
|
+
if (!agentRunToken) {
|
|
298
|
+
throw new Error("Agent execution authorization failed: token is missing.");
|
|
299
|
+
}
|
|
300
|
+
return agentRunToken;
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async verifySignedAgentExecutionToken(runToken, request, requestedFeatures, sessionId, commandId, targetScenarios) {
|
|
307
|
+
const parsed = parseJwt(runToken);
|
|
308
|
+
const keyId = stringOrDefault(parsed.header.kid, "default");
|
|
309
|
+
const algorithm = stringOrDefault(parsed.header.alg, "RS256").toUpperCase();
|
|
310
|
+
if (algorithm !== "RS256") {
|
|
311
|
+
throw new Error("Agent execution authorization failed: invalid token signature.");
|
|
312
|
+
}
|
|
313
|
+
const keyRecord = await this.getSigningKey(keyId, algorithm);
|
|
314
|
+
if (!verifyJwtSignature(runToken, keyRecord, algorithm)) {
|
|
315
|
+
throw new Error("Agent execution authorization failed: invalid token signature.");
|
|
316
|
+
}
|
|
317
|
+
enforceJwtLifetime(parsed.payload);
|
|
318
|
+
const tokenIssuer = stringOrDefault(parsed.payload.iss, "").trim();
|
|
319
|
+
if (!keyRecord.issuer || tokenIssuer !== keyRecord.issuer) {
|
|
320
|
+
throw new Error("Agent execution authorization failed: token issuer does not match.");
|
|
321
|
+
}
|
|
322
|
+
const rawAudience = parsed.payload.aud;
|
|
323
|
+
const tokenAudiences = Array.isArray(rawAudience)
|
|
324
|
+
? rawAudience.map((entry) => stringOrDefault(entry, "").trim()).filter((entry) => entry.length > 0)
|
|
325
|
+
: stringOrDefault(rawAudience, "").trim()
|
|
326
|
+
? [stringOrDefault(rawAudience, "").trim()]
|
|
327
|
+
: [];
|
|
328
|
+
if (!keyRecord.audience || !tokenAudiences.includes(keyRecord.audience)) {
|
|
329
|
+
throw new Error("Agent execution authorization failed: token audience does not match.");
|
|
330
|
+
}
|
|
331
|
+
const tokenKind = stringOrDefault(parsed.payload.token_kind, "").trim();
|
|
332
|
+
if (tokenKind !== "agent_execution") {
|
|
333
|
+
throw new Error("Agent execution authorization failed: token kind is invalid.");
|
|
334
|
+
}
|
|
335
|
+
const tokenNodeType = stringOrDefault(parsed.payload.node_type, "").trim().toLowerCase();
|
|
336
|
+
if (tokenNodeType !== "agent") {
|
|
337
|
+
throw new Error("Agent execution authorization failed: token node type is invalid.");
|
|
338
|
+
}
|
|
339
|
+
const tokenSessionId = stringOrDefault(parsed.payload.session_id, "").trim();
|
|
340
|
+
if (tokenSessionId !== sessionId) {
|
|
341
|
+
throw new Error("Agent execution authorization failed: session id does not match.");
|
|
342
|
+
}
|
|
343
|
+
const tokenCommandId = stringOrDefault(parsed.payload.command_id, "").trim();
|
|
344
|
+
if (tokenCommandId !== commandId) {
|
|
345
|
+
throw new Error("Agent execution authorization failed: command id does not match.");
|
|
346
|
+
}
|
|
347
|
+
const tokenTargetScenarios = collectClaimStrings(parsed.payload, "target_scenario")
|
|
348
|
+
.map((value) => value.trim())
|
|
349
|
+
.filter((value) => value.length > 0)
|
|
350
|
+
.sort();
|
|
351
|
+
const expectedTargetScenarios = [...targetScenarios]
|
|
352
|
+
.map((value) => value.trim())
|
|
353
|
+
.filter((value) => value.length > 0)
|
|
354
|
+
.sort();
|
|
355
|
+
if (tokenTargetScenarios.length !== expectedTargetScenarios.length
|
|
356
|
+
|| tokenTargetScenarios.some((value, index) => value !== expectedTargetScenarios[index])) {
|
|
357
|
+
throw new Error("Agent execution authorization failed: target scenarios do not match.");
|
|
358
|
+
}
|
|
359
|
+
enforceEntitlementClaims(parsed.payload, requestedFeatures);
|
|
360
|
+
enforceRuntimePolicyClaims(parsed.payload, request);
|
|
361
|
+
}
|
|
261
362
|
async getSigningKey(keyId, algorithm) {
|
|
262
363
|
const nowMs = Date.now();
|
|
263
364
|
const normalizedKeyId = stringOrDefault(keyId, "default").trim() || "default";
|
|
@@ -440,6 +541,7 @@ function generateSessionId() {
|
|
|
440
541
|
function collectRequestedFeatures(request) {
|
|
441
542
|
const context = asRecord(request.Context);
|
|
442
543
|
const scenarios = Array.isArray(request.Scenarios) ? request.Scenarios : [];
|
|
544
|
+
const nodeType = normalizeNodeType(pickValue(context, "NodeType", "nodeType"));
|
|
443
545
|
const features = new Set([
|
|
444
546
|
"core.runtime",
|
|
445
547
|
"reporting.formats.standard"
|
|
@@ -453,7 +555,9 @@ function collectRequestedFeatures(request) {
|
|
|
453
555
|
const hasAdvancedTargeting = normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")).length > 0
|
|
454
556
|
|| normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios")).length > 0
|
|
455
557
|
|| resolveTimeoutSeconds(context, "ClusterCommandTimeoutSeconds", "clusterCommandTimeoutSeconds", "ClusterCommandTimeoutMs", "clusterCommandTimeoutMs", 120) !== 120;
|
|
456
|
-
|
|
558
|
+
// Agent child contexts carry coordinator-assigned target slices. That internal
|
|
559
|
+
// state must not force the advanced targeting entitlement a second time.
|
|
560
|
+
if (hasAdvancedTargeting && nodeType !== "agent") {
|
|
457
561
|
features.add("cluster.targeting_and_tuning.advanced");
|
|
458
562
|
}
|
|
459
563
|
if (countCustomWorkerPlugins(context) > 0) {
|
|
@@ -1890,13 +1994,16 @@ function toContractNodeType(value) {
|
|
|
1890
1994
|
}
|
|
1891
1995
|
}
|
|
1892
1996
|
function countTargetedScenarios(context) {
|
|
1893
|
-
const values =
|
|
1894
|
-
...normalizeStringArray(pickValue(context, "TargetScenarios", "targetScenarios")),
|
|
1895
|
-
...normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")),
|
|
1896
|
-
...normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios"))
|
|
1897
|
-
];
|
|
1997
|
+
const values = resolveTargetScenarioNames(context);
|
|
1898
1998
|
return new Set(values.map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0)).size;
|
|
1899
1999
|
}
|
|
2000
|
+
function resolveTargetScenarioNames(context) {
|
|
2001
|
+
return [...new Set([
|
|
2002
|
+
...normalizeStringArray(pickValue(context, "TargetScenarios", "targetScenarios")),
|
|
2003
|
+
...normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")),
|
|
2004
|
+
...normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios"))
|
|
2005
|
+
])];
|
|
2006
|
+
}
|
|
1900
2007
|
function countCustomWorkerPlugins(context) {
|
|
1901
2008
|
const workerPlugins = asList(pickValue(context, "WorkerPlugins", "workerPlugins"));
|
|
1902
2009
|
let count = 0;
|
package/dist/cjs/reporting.js
CHANGED
|
@@ -1038,6 +1038,9 @@ function buildDotnetHtmlTabs(nodeStats) {
|
|
|
1038
1038
|
}
|
|
1039
1039
|
return tabs;
|
|
1040
1040
|
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Exposes the build dotnet txt report operation. Use this when interacting with the SDK through this surface.
|
|
1043
|
+
*/
|
|
1041
1044
|
function buildDotnetTxtReport(nodeStats) {
|
|
1042
1045
|
const scenarios = sortBySortIndex(reportArray(nodeStats, "scenarioStats", "ScenarioStats"));
|
|
1043
1046
|
const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
|
|
@@ -1084,6 +1087,9 @@ function buildDotnetTxtReport(nodeStats) {
|
|
|
1084
1087
|
}
|
|
1085
1088
|
return reportLines(lines);
|
|
1086
1089
|
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Exposes the build dotnet csv report operation. Use this when interacting with the SDK through this surface.
|
|
1092
|
+
*/
|
|
1087
1093
|
function buildDotnetCsvReport(nodeStats) {
|
|
1088
1094
|
const lines = ["ScenarioName,Requests,Ok,Fail,DurationSeconds,Rps"];
|
|
1089
1095
|
for (const scenario of sortBySortIndex(reportArray(nodeStats, "scenarioStats", "ScenarioStats"))) {
|
|
@@ -1093,6 +1099,9 @@ function buildDotnetCsvReport(nodeStats) {
|
|
|
1093
1099
|
}
|
|
1094
1100
|
return reportLines(lines);
|
|
1095
1101
|
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Exposes the build dotnet markdown report operation. Use this when interacting with the SDK through this surface.
|
|
1104
|
+
*/
|
|
1096
1105
|
function buildDotnetMarkdownReport(nodeStats) {
|
|
1097
1106
|
const scenarios = sortBySortIndex(reportArray(nodeStats, "scenarioStats", "ScenarioStats"));
|
|
1098
1107
|
const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
|
|
@@ -1132,6 +1141,9 @@ function buildDotnetMarkdownReport(nodeStats) {
|
|
|
1132
1141
|
}
|
|
1133
1142
|
return reportLines(lines);
|
|
1134
1143
|
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Exposes the build dotnet html report operation. Use this when interacting with the SDK through this surface.
|
|
1146
|
+
*/
|
|
1135
1147
|
function buildDotnetHtmlReport(nodeStats) {
|
|
1136
1148
|
const tabs = buildDotnetHtmlTabs(nodeStats);
|
|
1137
1149
|
const buttonsHtml = tabs.map(([tabId, title]) => `<button class="tab-btn" data-tab="${tabId}">${escapeHtml(title)}</button>${REPORT_EOL}`).join("");
|