norn-cli 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +9 -1
  2. package/CHANGELOG.md +23 -0
  3. package/dist/cli.js +246 -80
  4. package/package.json +1 -1
  5. package/out/assertionRunner.js +0 -537
  6. package/out/chatParticipant.js +0 -722
  7. package/out/cli/colors.js +0 -129
  8. package/out/cli/formatters/assertion.js +0 -75
  9. package/out/cli/formatters/index.js +0 -23
  10. package/out/cli/formatters/response.js +0 -106
  11. package/out/cli/formatters/summary.js +0 -187
  12. package/out/cli/redaction.js +0 -237
  13. package/out/cli/reporters/html.js +0 -634
  14. package/out/cli/reporters/index.js +0 -22
  15. package/out/cli/reporters/junit.js +0 -211
  16. package/out/cli.js +0 -989
  17. package/out/codeLensProvider.js +0 -248
  18. package/out/compareContentProvider.js +0 -85
  19. package/out/completionProvider.js +0 -2404
  20. package/out/contractDecorationProvider.js +0 -243
  21. package/out/coverageCalculator.js +0 -837
  22. package/out/coveragePanel.js +0 -545
  23. package/out/diagnosticProvider.js +0 -1113
  24. package/out/environmentProvider.js +0 -442
  25. package/out/extension.js +0 -1114
  26. package/out/httpClient.js +0 -269
  27. package/out/jsonFileReader.js +0 -320
  28. package/out/nornPrompt.js +0 -580
  29. package/out/nornapiParser.js +0 -326
  30. package/out/parser.js +0 -725
  31. package/out/responsePanel.js +0 -4674
  32. package/out/schemaGenerator.js +0 -393
  33. package/out/scriptRunner.js +0 -419
  34. package/out/sequenceRunner.js +0 -3046
  35. package/out/swaggerBodyIntellisenseCache.js +0 -147
  36. package/out/swaggerParser.js +0 -419
  37. package/out/test/coverageCalculator.test.js +0 -100
  38. package/out/test/extension.test.js +0 -48
  39. package/out/testProvider.js +0 -658
  40. package/out/validationCache.js +0 -245
@@ -1,3046 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractReturnVariables = extractReturnVariables;
4
- exports.bindSequenceArguments = bindSequenceArguments;
5
- exports.parseTagFilter = parseTagFilter;
6
- exports.sequenceMatchesTags = sequenceMatchesTags;
7
- exports.validateSequenceParameters = validateSequenceParameters;
8
- exports.extractSequences = extractSequences;
9
- exports.getSequenceAtLine = getSequenceAtLine;
10
- exports.countSequenceSteps = countSequenceSteps;
11
- exports.runSequence = runSequence;
12
- exports.runSequenceWithJar = runSequenceWithJar;
13
- const parser_1 = require("./parser");
14
- const httpClient_1 = require("./httpClient");
15
- const scriptRunner_1 = require("./scriptRunner");
16
- const assertionRunner_1 = require("./assertionRunner");
17
- const jsonFileReader_1 = require("./jsonFileReader");
18
- const nornapiParser_1 = require("./nornapiParser");
19
- /**
20
- * Applies header groups found in request text to a parsed request.
21
- * Handles lines like "Json" or "Auth" that are header group names.
22
- * Also strips header group names from the URL if they appear on the method line.
23
- */
24
- function applyHeaderGroupsToRequest(parsed, requestText, headerGroups, variables) {
25
- const lines = requestText.split('\n');
26
- const headerGroupNames = headerGroups.map(hg => hg.name);
27
- const headerGroupHeaders = {};
28
- const foundGroups = [];
29
- for (const line of lines) {
30
- const trimmed = line.trim();
31
- // Skip empty lines, comments, variable lines
32
- if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('var ')) {
33
- continue;
34
- }
35
- // Check if this is the HTTP method line - look for header groups at the end
36
- const methodMatch = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
37
- if (methodMatch) {
38
- const afterMethod = methodMatch[2];
39
- // Split by space and check trailing tokens for header group names
40
- const tokens = afterMethod.split(/\s+/);
41
- // Scan from the end to find header groups
42
- for (let i = tokens.length - 1; i >= 0; i--) {
43
- if (headerGroupNames.includes(tokens[i])) {
44
- foundGroups.push(tokens[i]);
45
- }
46
- else {
47
- break; // Stop at first non-header-group token from the end
48
- }
49
- }
50
- continue;
51
- }
52
- // Skip lines that look like headers (Name: Value)
53
- if (/^[A-Za-z0-9\-_]+\s*:\s*.+$/.test(trimmed)) {
54
- continue;
55
- }
56
- // Skip lines that look like JSON body
57
- if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"')) {
58
- continue;
59
- }
60
- // Check if this line contains header group names (can be multiple space-separated)
61
- const potentialGroups = trimmed.split(/\s+/);
62
- for (const groupName of potentialGroups) {
63
- if (headerGroupNames.includes(groupName)) {
64
- foundGroups.push(groupName);
65
- }
66
- }
67
- }
68
- // Collect headers from all found groups
69
- for (const groupName of foundGroups) {
70
- const group = headerGroups.find(hg => hg.name === groupName);
71
- if (group) {
72
- const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, variables);
73
- Object.assign(headerGroupHeaders, resolvedHeaders);
74
- }
75
- }
76
- // Strip header group names from the already-resolved URL
77
- let modifiedUrl = parsed.url;
78
- for (const groupName of foundGroups) {
79
- // Remove the header group name from URL (at the end with possible leading space)
80
- const endPattern = new RegExp(`\\s+${groupName}$`);
81
- modifiedUrl = modifiedUrl.replace(endPattern, '');
82
- }
83
- modifiedUrl = modifiedUrl.trim();
84
- // Build the result - update URL if modified, merge headers
85
- const result = {
86
- ...parsed,
87
- url: modifiedUrl
88
- };
89
- if (Object.keys(headerGroupHeaders).length > 0) {
90
- // Header groups go first, inline headers take precedence
91
- result.headers = { ...headerGroupHeaders, ...parsed.headers };
92
- }
93
- return result;
94
- }
95
- /**
96
- * Checks if a line is a "run <RequestName>" or "run <Name>(args)" command (for named requests/sequences)
97
- * Also supports retry/backoff options: "run Name retry 3 backoff 500 ms"
98
- */
99
- function isRunNamedRequestCommand(line) {
100
- const trimmed = line.trim();
101
- // Match "run <name>" or "run <name>(args)" but NOT "run bash", "run js", "run powershell"
102
- if (!trimmed.toLowerCase().startsWith('run ')) {
103
- return false;
104
- }
105
- const afterRun = trimmed.substring(4).trim();
106
- // Exclude script commands
107
- if (/^(bash|js|powershell)\s+/i.test(afterRun)) {
108
- return false;
109
- }
110
- // Must have a valid identifier (optionally followed by args in parentheses)
111
- // Also allow retry/backoff options at the end - use specific patterns
112
- return /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?(?:\s+retry\s+\d+)?(?:\s+backoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?)?$/i.test(afterRun);
113
- }
114
- /**
115
- * Parses a "run <RequestName>" or "run <Name>(args)" command.
116
- * Returns the request name, optional arguments, and retry/backoff options.
117
- */
118
- function parseRunNamedRequestCommand(line) {
119
- const trimmed = line.trim();
120
- // Extract retry/backoff options first using the shared function
121
- const { cleanedLine, retryCount, backoffMs } = (0, parser_1.extractRetryOptions)(trimmed);
122
- // Now parse the cleaned line for name and args
123
- const match = cleanedLine.match(/^run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?$/i);
124
- if (!match) {
125
- return null;
126
- }
127
- return {
128
- name: match[1],
129
- args: parseRunArguments(match[2] || ''),
130
- retryCount,
131
- backoffMs
132
- };
133
- }
134
- /**
135
- * Checks if a line is a return statement (return var1, var2)
136
- */
137
- function isReturnStatement(line) {
138
- return /^return\s+/i.test(line.trim());
139
- }
140
- /**
141
- * Parses a return statement and extracts the return expressions.
142
- * Supports:
143
- * return token, userId - variable names
144
- * return repaymentId.value - path into variable
145
- * return {{repaymentId.value}} - template syntax (also supported)
146
- */
147
- function parseReturnStatement(line) {
148
- const trimmed = line.trim();
149
- const match = trimmed.match(/^return\s+(.+)$/i);
150
- if (!match) {
151
- return null;
152
- }
153
- // Split by comma and trim each expression
154
- // Also strip {{ }} if present for convenience
155
- return match[1].split(',').map(v => {
156
- let expr = v.trim();
157
- // Remove {{ }} wrapper if present
158
- if (expr.startsWith('{{') && expr.endsWith('}}')) {
159
- expr = expr.slice(2, -2).trim();
160
- }
161
- return expr;
162
- }).filter(v => v.length > 0);
163
- }
164
- /**
165
- * Resolves a return expression to get the actual value.
166
- * Handles:
167
- * - Simple variable: "token" -> runtimeVariables["token"]
168
- * - Path expression: "repaymentId.value" -> parse JSON and get nested value
169
- * - Array access: "id[0].prop" -> array and object navigation
170
- */
171
- function resolveReturnExpression(expr, runtimeVariables) {
172
- // Extract the base variable name (before any . or [)
173
- const varNameMatch = expr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)/);
174
- if (!varNameMatch) {
175
- return null;
176
- }
177
- const varName = varNameMatch[1];
178
- const pathPart = expr.substring(varName.length);
179
- if (!(varName in runtimeVariables)) {
180
- return null;
181
- }
182
- const varValue = runtimeVariables[varName];
183
- // Simple variable (no path)
184
- if (!pathPart) {
185
- return { key: varName, value: varValue };
186
- }
187
- // Navigate the path
188
- try {
189
- // Parse the value if it's a string (could be JSON)
190
- let current = varValue;
191
- if (typeof current === 'string') {
192
- try {
193
- current = JSON.parse(current);
194
- }
195
- catch {
196
- // Not JSON, keep as string
197
- }
198
- }
199
- // Parse the path: convert [0] to .0 and split by .
200
- const normalizedPath = pathPart.replace(/^\.|^\[/, m => m === '.' ? '' : '[').replace(/\[(\d+)\]/g, '.$1');
201
- const parts = normalizedPath.split('.').filter(p => p !== '');
202
- for (const part of parts) {
203
- if (current === null || current === undefined) {
204
- return null;
205
- }
206
- current = current[part];
207
- }
208
- // Use the last part of the path as the key name
209
- const keyName = parts[parts.length - 1] || varName;
210
- return { key: keyName, value: current };
211
- }
212
- catch {
213
- // Navigation failed
214
- return { key: varName, value: varValue };
215
- }
216
- }
217
- /**
218
- * Extracts the return statement from sequence content.
219
- * Returns the list of return expressions, or null if no return statement.
220
- */
221
- function extractReturnVariables(content) {
222
- const lines = content.split('\n');
223
- for (const line of lines) {
224
- if (isReturnStatement(line)) {
225
- return parseReturnStatement(line);
226
- }
227
- }
228
- return null;
229
- }
230
- /**
231
- * Parses arguments from a run command.
232
- * Supports: run Name(arg1, arg2) or run Name(param: value, param2: value2)
233
- */
234
- function parseRunArguments(argsStr) {
235
- const args = [];
236
- if (!argsStr || !argsStr.trim()) {
237
- return args;
238
- }
239
- // Split by comma, respecting quoted strings and {{}}
240
- const parts = argsStr.split(/,(?=(?:[^"]*"[^"]*")*(?:[^{]*\{\{[^}]*\}\})*[^"]*$)/);
241
- for (const part of parts) {
242
- const trimmed = part.trim();
243
- if (!trimmed)
244
- continue;
245
- // Check for named argument: param: value
246
- const namedMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.+)$/);
247
- if (namedMatch) {
248
- let value = namedMatch[2].trim();
249
- // Strip quotes if present
250
- if ((value.startsWith('"') && value.endsWith('"')) ||
251
- (value.startsWith("'") && value.endsWith("'"))) {
252
- value = value.slice(1, -1);
253
- }
254
- args.push({
255
- name: namedMatch[1],
256
- value
257
- });
258
- }
259
- else {
260
- // Positional argument
261
- let value = trimmed;
262
- // Strip quotes if present
263
- if ((value.startsWith('"') && value.endsWith('"')) ||
264
- (value.startsWith("'") && value.endsWith("'"))) {
265
- value = value.slice(1, -1);
266
- }
267
- args.push({ value });
268
- }
269
- }
270
- return args;
271
- }
272
- /**
273
- * Checks if a line is a "var x = run SequenceName" or "var x = run SequenceName(args)" command.
274
- * Also supports retry/backoff options: "var x = run Name retry 3 backoff 500 ms"
275
- * Excludes script types (bash, powershell, js) which are handled by isRunCommand.
276
- */
277
- function isVarRunSequenceCommand(line) {
278
- const trimmed = line.trim();
279
- // First check the basic pattern - allow retry/backoff options at the end
280
- // Use specific patterns for retry (retry N) and backoff (backoff N [unit])
281
- if (!/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?(?:\s+retry\s+\d+)?(?:\s+backoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?)?\s*$/i.test(trimmed)) {
282
- return false;
283
- }
284
- // Exclude script types - these should be handled by isRunCommand
285
- const match = trimmed.match(/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)/i);
286
- if (match) {
287
- const name = match[1].toLowerCase();
288
- if (name === 'bash' || name === 'powershell' || name === 'js') {
289
- return false;
290
- }
291
- }
292
- return true;
293
- }
294
- /**
295
- * Parses a "var x = run SequenceName" or "var x = run SequenceName(args)" command.
296
- * Returns the variable name, sequence name, arguments, and retry/backoff options.
297
- */
298
- function parseVarRunSequenceCommand(line) {
299
- const trimmed = line.trim();
300
- // Extract retry/backoff options first using the shared function
301
- const { cleanedLine, retryCount, backoffMs } = (0, parser_1.extractRetryOptions)(trimmed);
302
- const match = cleanedLine.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?\s*$/i);
303
- if (!match) {
304
- return null;
305
- }
306
- return {
307
- varName: match[1],
308
- sequenceName: match[2],
309
- args: parseRunArguments(match[3] || ''),
310
- retryCount,
311
- backoffMs
312
- };
313
- }
314
- /**
315
- * Checks if a line is a plain "run SequenceName" or "run SequenceName(args)" command
316
- */
317
- function isRunSequenceCommand(line) {
318
- const trimmed = line.trim();
319
- // Must not start with "var" - that's handled by isVarRunSequenceCommand
320
- if (/^var\s+/i.test(trimmed))
321
- return false;
322
- return /^run\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?\s*$/i.test(trimmed);
323
- }
324
- /**
325
- * Parses a plain "run SequenceName" or "run SequenceName(args)" command.
326
- */
327
- function parseRunSequenceCommand(line) {
328
- const trimmed = line.trim();
329
- const match = trimmed.match(/^run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?\s*$/i);
330
- if (!match) {
331
- return null;
332
- }
333
- return {
334
- sequenceName: match[1],
335
- args: parseRunArguments(match[2] || '')
336
- };
337
- }
338
- /**
339
- * Binds arguments to parameters, resolving positional and named arguments.
340
- * Returns the resulting variables to inject into the sequence scope, or an error message.
341
- *
342
- * Rules:
343
- * - Positional args are bound in order
344
- * - Named args can be in any order
345
- * - Required params (no default) must be provided
346
- * - Optional params use default if not provided
347
- * - Cannot mix positional after named
348
- */
349
- function bindSequenceArguments(params, args, runtimeVariables) {
350
- const result = {};
351
- const boundParams = new Set();
352
- let positionalIndex = 0;
353
- let sawNamedArg = false;
354
- for (const arg of args) {
355
- if (arg.name) {
356
- // Named argument
357
- sawNamedArg = true;
358
- const param = params.find(p => p.name === arg.name);
359
- if (!param) {
360
- return { error: `Unknown parameter: "${arg.name}"` };
361
- }
362
- if (boundParams.has(arg.name)) {
363
- return { error: `Parameter "${arg.name}" provided multiple times` };
364
- }
365
- // Resolve variable references in the value
366
- let value = arg.value;
367
- if (value.startsWith('{{') && value.endsWith('}}')) {
368
- const varName = value.slice(2, -2);
369
- value = getVariableValueByPath(varName, runtimeVariables);
370
- }
371
- else if (runtimeVariables[value] !== undefined) {
372
- // Allow passing variable by name without braces
373
- value = runtimeVariables[value];
374
- }
375
- result[arg.name] = value;
376
- boundParams.add(arg.name);
377
- }
378
- else {
379
- // Positional argument
380
- if (sawNamedArg) {
381
- return { error: 'Positional arguments cannot follow named arguments' };
382
- }
383
- if (positionalIndex >= params.length) {
384
- return { error: `Too many arguments: expected ${params.length}, got more` };
385
- }
386
- const param = params[positionalIndex];
387
- // Resolve variable references in the value
388
- let value = arg.value;
389
- if (value.startsWith('{{') && value.endsWith('}}')) {
390
- const varName = value.slice(2, -2);
391
- value = getVariableValueByPath(varName, runtimeVariables);
392
- }
393
- else if (runtimeVariables[value] !== undefined) {
394
- // Allow passing variable by name without braces
395
- value = runtimeVariables[value];
396
- }
397
- result[param.name] = value;
398
- boundParams.add(param.name);
399
- positionalIndex++;
400
- }
401
- }
402
- // Check for missing required params and apply defaults
403
- for (const param of params) {
404
- if (!boundParams.has(param.name)) {
405
- if (param.defaultValue !== undefined) {
406
- result[param.name] = param.defaultValue;
407
- }
408
- else {
409
- return { error: `Missing required argument: "${param.name}"` };
410
- }
411
- }
412
- }
413
- return { variables: result };
414
- }
415
- /**
416
- * Helper to get a value by path from a variables object, supporting nested access.
417
- * Used for resolving argument values from runtime variables.
418
- */
419
- function getVariableValueByPath(path, variables) {
420
- const parts = path.split('.');
421
- let current = variables;
422
- for (const part of parts) {
423
- if (current === null || current === undefined) {
424
- return '';
425
- }
426
- if (typeof current === 'string') {
427
- // Try to parse as JSON
428
- try {
429
- current = JSON.parse(current);
430
- }
431
- catch {
432
- return '';
433
- }
434
- }
435
- current = current[part];
436
- }
437
- if (current === null || current === undefined) {
438
- return '';
439
- }
440
- if (typeof current === 'object') {
441
- return JSON.stringify(current);
442
- }
443
- return String(current);
444
- }
445
- /**
446
- * Parses a tag filter string into a TagFilter object.
447
- * Supports: "smoke" (simple) and "team(CustomerExp)" (key-value)
448
- */
449
- function parseTagFilter(filterStr) {
450
- const match = filterStr.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)(?:\(([^)]+)\))?$/);
451
- if (!match) {
452
- return { name: filterStr }; // Fallback to treating entire string as name
453
- }
454
- return {
455
- name: match[1],
456
- value: match[2]?.trim()
457
- };
458
- }
459
- /**
460
- * Checks if a sequence tag matches a filter.
461
- * Case-insensitive comparison for both name and value.
462
- * If filter has no value, matches any tag with that name.
463
- * If filter has a value, requires exact (case-insensitive) match.
464
- */
465
- function tagMatchesFilter(tag, filter) {
466
- // Case-insensitive name comparison
467
- if (tag.name.toLowerCase() !== filter.name.toLowerCase()) {
468
- return false;
469
- }
470
- // If filter has no value requirement, any value matches (including no value)
471
- if (filter.value === undefined) {
472
- return true;
473
- }
474
- // If filter has value requirement, tag must have matching value
475
- if (tag.value === undefined) {
476
- return false;
477
- }
478
- return tag.value.toLowerCase() === filter.value.toLowerCase();
479
- }
480
- /**
481
- * Checks if a sequence matches the tag filter options.
482
- * @param sequence The sequence to check
483
- * @param options The filter options (filters and mode)
484
- * @returns true if the sequence matches according to the filter mode
485
- */
486
- function sequenceMatchesTags(sequence, options) {
487
- if (options.filters.length === 0) {
488
- return true; // No filters = match all
489
- }
490
- if (options.mode === 'and') {
491
- // Must match ALL filters
492
- return options.filters.every(filter => sequence.tags.some(tag => tagMatchesFilter(tag, filter)));
493
- }
494
- else {
495
- // Must match ANY filter
496
- return options.filters.some(filter => sequence.tags.some(tag => tagMatchesFilter(tag, filter)));
497
- }
498
- }
499
- /**
500
- * Parses sequence parameters from a sequence declaration line.
501
- * Supports: sequence Name(param1, param2 = "default")
502
- * Returns empty array if no parameters.
503
- */
504
- function parseSequenceParameters(line) {
505
- const params = [];
506
- // Match the parentheses content
507
- const parenMatch = line.match(/\(([^)]*)\)/);
508
- if (!parenMatch) {
509
- return params;
510
- }
511
- const paramsStr = parenMatch[1].trim();
512
- if (!paramsStr) {
513
- return params;
514
- }
515
- // Split by comma, but handle quoted strings
516
- const paramParts = paramsStr.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/);
517
- let hasSeenDefault = false;
518
- for (const part of paramParts) {
519
- const trimmed = part.trim();
520
- if (!trimmed)
521
- continue;
522
- // Check for param = value pattern
523
- const defaultMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/);
524
- if (defaultMatch) {
525
- hasSeenDefault = true;
526
- let defaultValue = defaultMatch[2].trim();
527
- // Strip quotes if present
528
- if ((defaultValue.startsWith('"') && defaultValue.endsWith('"')) ||
529
- (defaultValue.startsWith("'") && defaultValue.endsWith("'"))) {
530
- defaultValue = defaultValue.slice(1, -1);
531
- }
532
- params.push({
533
- name: defaultMatch[1],
534
- defaultValue
535
- });
536
- }
537
- else {
538
- // No default value
539
- const nameMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)$/);
540
- if (nameMatch) {
541
- // Error: required param after optional param
542
- if (hasSeenDefault) {
543
- // We'll still add it, but validation should catch this
544
- console.warn(`Required parameter "${nameMatch[1]}" follows optional parameter`);
545
- }
546
- params.push({
547
- name: nameMatch[1]
548
- });
549
- }
550
- }
551
- }
552
- return params;
553
- }
554
- /**
555
- * Validates that required parameters come before optional ones.
556
- */
557
- function validateSequenceParameters(params) {
558
- let hasSeenDefault = false;
559
- for (const param of params) {
560
- if (param.defaultValue !== undefined) {
561
- hasSeenDefault = true;
562
- }
563
- else if (hasSeenDefault) {
564
- return `Required parameter "${param.name}" cannot come after optional parameter`;
565
- }
566
- }
567
- return null;
568
- }
569
- /**
570
- * Parses tags from lines immediately preceding a sequence declaration.
571
- * Supports: @tagname (simple) and @key(value) (key-value)
572
- * Multiple tags can be on the same line or separate lines.
573
- * Tag names follow the pattern: [a-zA-Z_][a-zA-Z0-9_-]*
574
- *
575
- * Also extracts @data(...) and @theory("file.json") annotations for parameterized tests.
576
- * @data values are parsed as typed: numbers, booleans, or quoted strings.
577
- */
578
- function parseSequenceTags(lines, sequenceLineIndex) {
579
- const tags = [];
580
- const dataCases = [];
581
- let theorySource;
582
- let tagStartLine = sequenceLineIndex;
583
- // Look backwards from the sequence line to collect tag lines
584
- for (let i = sequenceLineIndex - 1; i >= 0; i--) {
585
- const line = lines[i].trim();
586
- // Skip empty lines between tags and sequence
587
- if (!line) {
588
- continue;
589
- }
590
- // Check if this line contains tags (starts with @)
591
- if (!line.startsWith('@')) {
592
- // Not a tag line, stop looking
593
- break;
594
- }
595
- // Check for @data(...) - parameterized test data
596
- const dataMatch = line.match(/^@data\s*\((.+)\)\s*$/);
597
- if (dataMatch) {
598
- const values = parseDataValues(dataMatch[1]);
599
- dataCases.push(values);
600
- tagStartLine = i;
601
- continue;
602
- }
603
- // Check for @theory("file.json") - external data file
604
- const theoryMatch = line.match(/^@theory\s*\(\s*["']([^"']+)["']\s*\)\s*$/);
605
- if (theoryMatch) {
606
- theorySource = theoryMatch[1];
607
- tagStartLine = i;
608
- continue;
609
- }
610
- // Parse regular tags from this line
611
- // Pattern: @tagname or @key(value)
612
- const tagPattern = /@([a-zA-Z_][a-zA-Z0-9_-]*)(?:\(([^)]+)\))?/g;
613
- let match;
614
- while ((match = tagPattern.exec(line)) !== null) {
615
- const tagName = match[1];
616
- // Skip data and theory since we handle them separately
617
- if (tagName === 'data' || tagName === 'theory') {
618
- continue;
619
- }
620
- const tagValue = match[2]?.trim();
621
- tags.push({
622
- name: tagName,
623
- value: tagValue
624
- });
625
- }
626
- tagStartLine = i;
627
- }
628
- // Build theoryData if we have @data or @theory
629
- let theoryData;
630
- if (dataCases.length > 0 || theorySource) {
631
- theoryData = {
632
- cases: [], // Will be populated with param names after we know them
633
- source: theorySource
634
- };
635
- // Store raw data cases temporarily - will be bound to param names in extractSequences
636
- // Reverse because we collected them in backwards order (iterating up from sequence line)
637
- theoryData._rawCases = dataCases.reverse();
638
- }
639
- return { tags, tagStartLine, theoryData };
640
- }
641
- /**
642
- * Parses comma-separated values from @data(...) annotation.
643
- * Supports: numbers (1, 3.14), booleans (true, false), quoted strings ("hello", 'world')
644
- */
645
- function parseDataValues(valuesStr) {
646
- const values = [];
647
- let current = '';
648
- let inQuote = null;
649
- let i = 0;
650
- while (i < valuesStr.length) {
651
- const char = valuesStr[i];
652
- if (inQuote) {
653
- if (char === inQuote) {
654
- // End of quoted string - add without quotes
655
- values.push(current);
656
- current = '';
657
- inQuote = null;
658
- // Skip to next comma or end
659
- i++;
660
- while (i < valuesStr.length && valuesStr[i] !== ',') {
661
- i++;
662
- }
663
- i++; // Skip comma
664
- continue;
665
- }
666
- else {
667
- current += char;
668
- }
669
- }
670
- else {
671
- if (char === '"' || char === "'") {
672
- inQuote = char;
673
- current = '';
674
- }
675
- else if (char === ',') {
676
- // End of value
677
- const trimmed = current.trim();
678
- if (trimmed) {
679
- values.push(parseTypedValue(trimmed));
680
- }
681
- current = '';
682
- }
683
- else {
684
- current += char;
685
- }
686
- }
687
- i++;
688
- }
689
- // Handle last value
690
- if (inQuote) {
691
- // Unclosed quote - treat as string
692
- values.push(current);
693
- }
694
- else {
695
- const trimmed = current.trim();
696
- if (trimmed) {
697
- values.push(parseTypedValue(trimmed));
698
- }
699
- }
700
- return values;
701
- }
702
- /**
703
- * Parses a value string into its typed representation.
704
- * Numbers become numbers, booleans become booleans, everything else is a string.
705
- */
706
- function parseTypedValue(value) {
707
- // Boolean
708
- if (value === 'true')
709
- return true;
710
- if (value === 'false')
711
- return false;
712
- // Null
713
- if (value === 'null')
714
- return null;
715
- // Number (integer or float)
716
- if (/^-?\d+(\.\d+)?$/.test(value)) {
717
- return parseFloat(value);
718
- }
719
- // String (possibly quoted)
720
- if ((value.startsWith('"') && value.endsWith('"')) ||
721
- (value.startsWith("'") && value.endsWith("'"))) {
722
- return value.slice(1, -1);
723
- }
724
- return value;
725
- }
726
- /**
727
- * Extracts sequence blocks from the document.
728
- */
729
- function extractSequences(text) {
730
- const lines = text.split('\n');
731
- const sequences = [];
732
- let currentSequence = null;
733
- for (let i = 0; i < lines.length; i++) {
734
- const line = lines[i].trim();
735
- // Strip inline comment for matching
736
- const lineWithoutComment = stripInlineComment(line);
737
- // Check for sequence start - supports 'test sequence' and 'sequence', with optional parameters
738
- const testSequenceMatch = lineWithoutComment.match(/^test\s+sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?\s*$/);
739
- const sequenceMatch = lineWithoutComment.match(/^sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?\s*$/);
740
- if (testSequenceMatch || sequenceMatch) {
741
- const isTest = !!testSequenceMatch;
742
- const name = isTest ? testSequenceMatch[1] : sequenceMatch[1];
743
- const parameters = parseSequenceParameters(lines[i]);
744
- const { tags, tagStartLine, theoryData } = parseSequenceTags(lines, i);
745
- // If we have raw @data cases, bind them to parameter names
746
- let finalTheoryData = theoryData;
747
- if (theoryData && theoryData._rawCases) {
748
- const rawCases = theoryData._rawCases;
749
- // Handle single-param shorthand: @data(1, 2, 3) with single param (id)
750
- // should create 3 cases [{id:1}, {id:2}, {id:3}] not 1 case with 3 values
751
- if (parameters.length === 1 && rawCases.length === 1 && rawCases[0].length > 1) {
752
- const paramName = parameters[0].name;
753
- finalTheoryData = {
754
- cases: rawCases[0].map(value => ({ [paramName]: value })),
755
- source: theoryData.source
756
- };
757
- }
758
- else {
759
- // Normal multi-param case: each @data line is a test case
760
- finalTheoryData = {
761
- cases: rawCases.map(values => {
762
- const caseObj = {};
763
- parameters.forEach((param, idx) => {
764
- caseObj[param.name] = values[idx];
765
- });
766
- return caseObj;
767
- }),
768
- source: theoryData.source
769
- };
770
- }
771
- }
772
- currentSequence = {
773
- name,
774
- startLine: tagStartLine, // Include tag lines in the sequence range
775
- lines: [],
776
- parameters,
777
- tags,
778
- isTest,
779
- theoryData: finalTheoryData
780
- };
781
- continue;
782
- }
783
- // Check for sequence end
784
- if (line === 'end sequence' && currentSequence) {
785
- sequences.push({
786
- name: currentSequence.name,
787
- startLine: currentSequence.startLine,
788
- endLine: i,
789
- content: currentSequence.lines.join('\n'),
790
- parameters: currentSequence.parameters,
791
- tags: currentSequence.tags,
792
- isTest: currentSequence.isTest,
793
- theoryData: currentSequence.theoryData
794
- });
795
- currentSequence = null;
796
- continue;
797
- }
798
- // Add line to current sequence
799
- if (currentSequence) {
800
- currentSequence.lines.push(lines[i]);
801
- }
802
- }
803
- return sequences;
804
- }
805
- /**
806
- * Finds which sequence a line belongs to.
807
- */
808
- function getSequenceAtLine(text, lineNumber) {
809
- const sequences = extractSequences(text);
810
- return sequences.find(seq => lineNumber >= seq.startLine && lineNumber <= seq.endLine) || null;
811
- }
812
- /**
813
- * HTTP methods for regex matching
814
- */
815
- const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
816
- /**
817
- * Checks if a line is a variable request assignment: var name = GET/POST/etc url
818
- * This captures the response directly into a variable for stable referencing.
819
- */
820
- function isVarRequestCommand(line) {
821
- const trimmed = line.trim();
822
- const methodPattern = HTTP_METHODS.join('|');
823
- const regex = new RegExp(`^var\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s*=\\s*(${methodPattern})\\s+.+$`, 'i');
824
- return regex.test(trimmed);
825
- }
826
- /**
827
- * Parses a variable request command: var name = METHOD url [retry N] [backoff N ms]
828
- * Returns the variable name, method, URL, and optional retry options.
829
- */
830
- function parseVarRequestCommand(line) {
831
- const trimmed = line.trim();
832
- const methodPattern = HTTP_METHODS.join('|');
833
- const regex = new RegExp(`^var\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*=\\s*(${methodPattern})\\s+(.+)$`, 'i');
834
- const match = trimmed.match(regex);
835
- if (!match) {
836
- return null;
837
- }
838
- // Extract retry options from the URL part
839
- const { cleanedLine: url, retryCount, backoffMs } = (0, parser_1.extractRetryOptions)(match[3].trim());
840
- return {
841
- varName: match[1],
842
- method: match[2].toUpperCase(),
843
- url,
844
- retryCount,
845
- backoffMs
846
- };
847
- }
848
- /**
849
- * Checks if a line is a variable assignment that should be evaluated at runtime.
850
- * This handles: var x = someVar.path or var x = "literal string"
851
- * Does NOT handle: var x = $1.path (response capture), var x = run ... (script/json), var x = METHOD url (request)
852
- */
853
- function isVarAssignCommand(line) {
854
- const trimmed = line.trim();
855
- // Must start with 'var'
856
- if (!/^var\s+/i.test(trimmed)) {
857
- return false;
858
- }
859
- // Exclude response captures (var x = $1...)
860
- if (/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*\$\d+/.test(trimmed)) {
861
- return false;
862
- }
863
- // Exclude run commands (var x = run ...)
864
- if (/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+/i.test(trimmed)) {
865
- return false;
866
- }
867
- // Exclude request assignments (var x = GET/POST/etc ...)
868
- if (isVarRequestCommand(trimmed)) {
869
- return false;
870
- }
871
- // Must have the form: var name = something
872
- return /^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*.+$/.test(trimmed);
873
- }
874
- /**
875
- * Parses a variable assignment command.
876
- * Returns the variable name and the value expression.
877
- */
878
- function parseVarAssignCommand(line) {
879
- const trimmed = line.trim();
880
- const match = trimmed.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/i);
881
- if (!match) {
882
- return null;
883
- }
884
- return {
885
- varName: match[1],
886
- valueExpr: match[2].trim()
887
- };
888
- }
889
- /**
890
- * Evaluates a value expression in the context of runtime variables.
891
- *
892
- * Supports:
893
- * - Quoted strings: "hello" or 'hello' (with {{}} interpolation)
894
- * - Variable paths: someVar.property[0].value
895
- * - Simple literals: 123, true, false, null
896
- */
897
- function evaluateValueExpression(expr, runtimeVariables) {
898
- const trimmed = expr.trim();
899
- // Check for quoted string (literal with optional interpolation)
900
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
901
- (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
902
- // Remove quotes and substitute variables inside
903
- const inner = trimmed.slice(1, -1);
904
- const substituted = (0, parser_1.substituteVariables)(inner, runtimeVariables);
905
- return { value: substituted };
906
- }
907
- // Check for simple literals
908
- if (trimmed === 'true' || trimmed === 'false' || trimmed === 'null') {
909
- return { value: trimmed };
910
- }
911
- if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
912
- return { value: trimmed };
913
- }
914
- // It's a variable path expression: varName or varName.path or varName[0].path
915
- const pathMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
916
- if (!pathMatch) {
917
- return { value: trimmed, error: `Invalid expression: ${trimmed}` };
918
- }
919
- const varName = pathMatch[1];
920
- const pathPart = pathMatch[2] || '';
921
- if (!(varName in runtimeVariables)) {
922
- return { value: '', error: `Variable '${varName}' is not defined` };
923
- }
924
- const varValue = runtimeVariables[varName];
925
- // If there's a path, navigate into the value
926
- if (pathPart) {
927
- // Handle object values directly (from var x = GET responses or parsed JSON)
928
- let dataToNavigate;
929
- if (typeof varValue === 'object' && varValue !== null) {
930
- dataToNavigate = varValue;
931
- }
932
- else if (typeof varValue === 'string') {
933
- try {
934
- dataToNavigate = JSON.parse(varValue);
935
- }
936
- catch {
937
- return { value: varValue, error: `Cannot parse '${varName}' as JSON for path access` };
938
- }
939
- }
940
- else {
941
- return { value: String(varValue), error: `Cannot access path on non-object value` };
942
- }
943
- // Navigate the path
944
- const path = pathPart.replace(/^\./, '').replace(/\[(\d+)\]/g, '.$1');
945
- const parts = path.split('.').filter(p => p !== '');
946
- let current = dataToNavigate;
947
- for (const part of parts) {
948
- if (current === null || current === undefined) {
949
- return { value: '', error: `Cannot access '${part}' on null/undefined` };
950
- }
951
- current = current[part];
952
- }
953
- // Return the result - preserve type for objects/arrays
954
- if (current === null) {
955
- return { value: null };
956
- }
957
- if (current === undefined) {
958
- return { value: '', error: `Property path '${pathPart}' not found` };
959
- }
960
- // Return objects/arrays as-is so they can be used in further expressions
961
- return { value: current };
962
- }
963
- // No path, return the variable value as-is
964
- return { value: varValue };
965
- }
966
- /**
967
- * Strip inline comment from a line.
968
- * Finds # that is not inside quotes and removes everything after it.
969
- */
970
- function stripInlineComment(line) {
971
- let inSingleQuote = false;
972
- let inDoubleQuote = false;
973
- for (let i = 0; i < line.length; i++) {
974
- const char = line[i];
975
- const prevChar = i > 0 ? line[i - 1] : '';
976
- // Skip escaped quotes
977
- if (prevChar === '\\') {
978
- continue;
979
- }
980
- if (char === '"' && !inSingleQuote) {
981
- inDoubleQuote = !inDoubleQuote;
982
- }
983
- else if (char === "'" && !inDoubleQuote) {
984
- inSingleQuote = !inSingleQuote;
985
- }
986
- else if (char === '#' && !inSingleQuote && !inDoubleQuote) {
987
- // Found an unquoted #, strip from here
988
- return line.substring(0, i).trimEnd();
989
- }
990
- }
991
- return line;
992
- }
993
- /**
994
- * Checks if a line is a print command.
995
- */
996
- function isPrintCommand(line) {
997
- return /^print\s+/i.test(line.trim());
998
- }
999
- /**
1000
- * Parses a print command and returns title and optional body.
1001
- * Format: print Title | Optional body content
1002
- */
1003
- function parsePrintCommand(line) {
1004
- // Strip inline comments before parsing
1005
- const lineWithoutComment = stripInlineComment(line);
1006
- const match = lineWithoutComment.trim().match(/^print\s+(.+)$/i);
1007
- if (!match) {
1008
- return { title: '' };
1009
- }
1010
- const content = match[1];
1011
- const pipeIndex = content.indexOf('|');
1012
- if (pipeIndex === -1) {
1013
- // No body, just title
1014
- return { title: content.trim() };
1015
- }
1016
- // Split into title and body
1017
- const title = content.substring(0, pipeIndex).trim();
1018
- const body = content.substring(pipeIndex + 1).trim();
1019
- return { title, body: body || undefined };
1020
- }
1021
- /**
1022
- * Checks if a line is a wait command.
1023
- * Formats: wait 2s, wait 500ms, wait 1.5s
1024
- */
1025
- function isWaitCommand(line) {
1026
- return /^wait\s+\d+(\.\d+)?\s*(s|ms|seconds?|milliseconds?)?$/i.test(line.trim());
1027
- }
1028
- /**
1029
- * Parses a wait command and returns the duration in milliseconds.
1030
- * Formats: wait 2s, wait 500ms, wait 1.5s, wait 2 seconds
1031
- */
1032
- function parseWaitCommand(line) {
1033
- const match = line.trim().match(/^wait\s+(\d+(?:\.\d+)?)\s*(s|ms|seconds?|milliseconds?)?$/i);
1034
- if (!match) {
1035
- return 0;
1036
- }
1037
- const value = parseFloat(match[1]);
1038
- const unit = (match[2] || 's').toLowerCase();
1039
- // Convert to milliseconds
1040
- if (unit === 'ms' || unit.startsWith('millisecond')) {
1041
- return value;
1042
- }
1043
- // Default to seconds
1044
- return value * 1000;
1045
- }
1046
- /**
1047
- * Resolves bare variable references in text.
1048
- * A bare variable is a single identifier that matches a defined variable.
1049
- * Examples: "id" -> value of id, "user.body.name" -> nested access
1050
- * Does NOT resolve variables inside strings or expressions with operators.
1051
- * This is used for print statements where bare variable names should be resolved.
1052
- */
1053
- function resolveBareVariables(text, variables) {
1054
- const trimmed = text.trim();
1055
- // Check if the entire text is a bare variable reference (with optional path)
1056
- // Pattern: varName or varName.path.to.property
1057
- const bareVarMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
1058
- if (bareVarMatch) {
1059
- const varName = bareVarMatch[1];
1060
- const pathPart = bareVarMatch[2] || '';
1061
- if (varName in variables) {
1062
- const value = variables[varName];
1063
- if (pathPart) {
1064
- // Navigate the path
1065
- const path = pathPart.replace(/^\./, '');
1066
- const nestedValue = getNestedValueFromObject(value, path);
1067
- return valueToString(nestedValue);
1068
- }
1069
- return valueToString(value);
1070
- }
1071
- }
1072
- // For expressions with + or other operators, resolve each bare variable in parts
1073
- // Split by + and resolve each part, preserving string literals
1074
- if (text.includes('+')) {
1075
- const parts = splitExpressionParts(text);
1076
- const resolvedParts = parts.map(part => {
1077
- const partTrimmed = part.trim();
1078
- // If it's a quoted string, keep as-is but remove quotes
1079
- if ((partTrimmed.startsWith('"') && partTrimmed.endsWith('"')) ||
1080
- (partTrimmed.startsWith("'") && partTrimmed.endsWith("'"))) {
1081
- return partTrimmed.slice(1, -1);
1082
- }
1083
- // If it's a bare variable, resolve it
1084
- const varMatch = partTrimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
1085
- if (varMatch) {
1086
- const varName = varMatch[1];
1087
- const pathPart = varMatch[2] || '';
1088
- if (varName in variables) {
1089
- const value = variables[varName];
1090
- if (pathPart) {
1091
- const path = pathPart.replace(/^\./, '');
1092
- return valueToString(getNestedValueFromObject(value, path));
1093
- }
1094
- return valueToString(value);
1095
- }
1096
- }
1097
- // Otherwise, return as-is
1098
- return partTrimmed;
1099
- });
1100
- return resolvedParts.join('');
1101
- }
1102
- // No resolution needed
1103
- return text;
1104
- }
1105
- /**
1106
- * Split an expression by + operator, respecting string literals
1107
- */
1108
- function splitExpressionParts(expr) {
1109
- const parts = [];
1110
- let current = '';
1111
- let inString = false;
1112
- let stringChar = '';
1113
- for (let i = 0; i < expr.length; i++) {
1114
- const char = expr[i];
1115
- if (!inString && (char === '"' || char === "'")) {
1116
- inString = true;
1117
- stringChar = char;
1118
- current += char;
1119
- }
1120
- else if (inString && char === stringChar) {
1121
- inString = false;
1122
- current += char;
1123
- }
1124
- else if (!inString && char === '+') {
1125
- parts.push(current);
1126
- current = '';
1127
- }
1128
- else {
1129
- current += char;
1130
- }
1131
- }
1132
- if (current) {
1133
- parts.push(current);
1134
- }
1135
- return parts;
1136
- }
1137
- /**
1138
- * Get nested value from an object using a dot-notation path
1139
- */
1140
- function getNestedValueFromObject(obj, path) {
1141
- if (!path) {
1142
- return obj;
1143
- }
1144
- const parts = path.split(/\.|\[(\d+)\]/).filter(p => p !== '' && p !== undefined);
1145
- let current = obj;
1146
- for (const part of parts) {
1147
- if (current === null || current === undefined) {
1148
- return undefined;
1149
- }
1150
- current = current[part];
1151
- }
1152
- return current;
1153
- }
1154
- /**
1155
- * Convert a value to string for display
1156
- */
1157
- function valueToString(value) {
1158
- if (value === null) {
1159
- return 'null';
1160
- }
1161
- if (value === undefined) {
1162
- return 'undefined';
1163
- }
1164
- if (typeof value === 'object') {
1165
- return JSON.stringify(value);
1166
- }
1167
- return String(value);
1168
- }
1169
- /**
1170
- * Sleep helper function
1171
- */
1172
- function sleep(ms) {
1173
- return new Promise(resolve => setTimeout(resolve, ms));
1174
- }
1175
- /**
1176
- * Checks if a line is the start of an if block.
1177
- * Format: if <condition>
1178
- */
1179
- function isIfCommand(line) {
1180
- return /^if\s+.+$/i.test(line.trim());
1181
- }
1182
- /**
1183
- * Checks if a line is the end of an if block.
1184
- */
1185
- function isEndIfCommand(line) {
1186
- const trimmed = line.trim().toLowerCase();
1187
- return trimmed === 'end if' || trimmed === 'endif';
1188
- }
1189
- /**
1190
- * Parses an if condition.
1191
- * Reuses assertion parsing logic for the condition.
1192
- */
1193
- function parseIfCondition(line) {
1194
- const match = line.trim().match(/^if\s+(.+)$/i);
1195
- return match ? match[1].trim() : null;
1196
- }
1197
- /**
1198
- * Evaluates an if condition using the same logic as assertions.
1199
- * Supports: ==, !=, >, >=, <, <=, contains, exists, !exists, isType
1200
- */
1201
- function evaluateIfCondition(condition, responses, variables, getValueByPathFn) {
1202
- // Import assertion parsing and evaluation
1203
- const { parseAssertCommand, evaluateAssertion } = require('./assertionRunner');
1204
- // Wrap condition as "assert <condition>" to reuse parsing
1205
- const parsed = parseAssertCommand(`assert ${condition}`);
1206
- if (!parsed) {
1207
- // Can't parse - treat as false
1208
- return false;
1209
- }
1210
- const result = evaluateAssertion(parsed, responses, variables, getValueByPathFn);
1211
- return result.passed;
1212
- }
1213
- /**
1214
- * Counts the number of steps in a sequence (for progress reporting).
1215
- */
1216
- function countSequenceSteps(sequenceContent) {
1217
- return extractStepsFromSequence(sequenceContent).length;
1218
- }
1219
- /**
1220
- * Extracts steps (requests, run commands, and print statements) from a sequence block.
1221
- */
1222
- function extractStepsFromSequence(content) {
1223
- const lines = content.split('\n');
1224
- const steps = [];
1225
- const methodRegex = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i;
1226
- // Boundaries that end a pending request-like block and start a new step.
1227
- const stepBoundaryRegex = /^(###|(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+|(test\s+)?sequence\s+|end\s+sequence|end\s+if|endif\b|var\s+|run\s+|print\s+|assert\s+|if\s+|wait\s+|import\s+|return\s+|\[)/i;
1228
- let currentRequest = [];
1229
- let currentRequestStartLine = -1;
1230
- let inRequest = false;
1231
- // Track varRequest with body support (var x = POST Endpoint Headers + body lines)
1232
- let currentVarRequest = [];
1233
- let currentVarRequestStartLine = -1;
1234
- let inVarRequest = false;
1235
- // Helper to save pending varRequest
1236
- const savePendingVarRequest = () => {
1237
- if (currentVarRequest.length > 0) {
1238
- steps.push({
1239
- type: 'varRequest',
1240
- content: currentVarRequest.join('\n'),
1241
- lineNumber: currentVarRequestStartLine,
1242
- });
1243
- currentVarRequest = [];
1244
- inVarRequest = false;
1245
- }
1246
- };
1247
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
1248
- const line = lines[lineIdx];
1249
- const trimmed = line.trim();
1250
- // Check if this is an assert command
1251
- if ((0, assertionRunner_1.isAssertCommand)(trimmed)) {
1252
- // Save any pending request/varRequest first
1253
- if (currentRequest.length > 0) {
1254
- steps.push({
1255
- type: 'request',
1256
- content: currentRequest.join('\n'),
1257
- lineNumber: currentRequestStartLine,
1258
- });
1259
- currentRequest = [];
1260
- inRequest = false;
1261
- }
1262
- savePendingVarRequest();
1263
- // Add the assertion step
1264
- steps.push({
1265
- type: 'assertion',
1266
- content: trimmed,
1267
- lineNumber: lineIdx,
1268
- });
1269
- continue;
1270
- }
1271
- // Check if this is an if command
1272
- if (isIfCommand(trimmed)) {
1273
- // Save any pending request/varRequest first
1274
- if (currentRequest.length > 0) {
1275
- steps.push({
1276
- type: 'request',
1277
- content: currentRequest.join('\n'),
1278
- lineNumber: currentRequestStartLine,
1279
- });
1280
- currentRequest = [];
1281
- inRequest = false;
1282
- }
1283
- savePendingVarRequest();
1284
- // Add the if step with the condition
1285
- const condition = parseIfCondition(trimmed);
1286
- steps.push({
1287
- type: 'if',
1288
- content: condition || '',
1289
- lineNumber: lineIdx,
1290
- });
1291
- continue;
1292
- }
1293
- // Check if this is an end if command
1294
- if (isEndIfCommand(trimmed)) {
1295
- // Save any pending request/varRequest first
1296
- if (currentRequest.length > 0) {
1297
- steps.push({
1298
- type: 'request',
1299
- content: currentRequest.join('\n'),
1300
- lineNumber: currentRequestStartLine,
1301
- });
1302
- currentRequest = [];
1303
- inRequest = false;
1304
- }
1305
- savePendingVarRequest();
1306
- // Add the endif step
1307
- steps.push({
1308
- type: 'endif',
1309
- content: '',
1310
- lineNumber: lineIdx,
1311
- });
1312
- continue;
1313
- }
1314
- // Check if this is a print command
1315
- if (isPrintCommand(trimmed)) {
1316
- // Save any pending request/varRequest first
1317
- if (currentRequest.length > 0) {
1318
- steps.push({
1319
- type: 'request',
1320
- content: currentRequest.join('\n'),
1321
- lineNumber: currentRequestStartLine,
1322
- });
1323
- currentRequest = [];
1324
- inRequest = false;
1325
- }
1326
- savePendingVarRequest();
1327
- // Add the print step - store raw line for later parsing with variable substitution
1328
- steps.push({
1329
- type: 'print',
1330
- content: trimmed, // Store the full print command
1331
- lineNumber: lineIdx,
1332
- });
1333
- continue;
1334
- }
1335
- // Check if this is a wait command
1336
- if (isWaitCommand(trimmed)) {
1337
- // Save any pending request/varRequest first
1338
- if (currentRequest.length > 0) {
1339
- steps.push({
1340
- type: 'request',
1341
- content: currentRequest.join('\n'),
1342
- lineNumber: currentRequestStartLine,
1343
- });
1344
- currentRequest = [];
1345
- inRequest = false;
1346
- }
1347
- savePendingVarRequest();
1348
- // Add the wait step
1349
- steps.push({
1350
- type: 'wait',
1351
- content: trimmed,
1352
- lineNumber: lineIdx,
1353
- });
1354
- continue;
1355
- }
1356
- // Check if this is a JSON file load command (var x = run readJson ./path.json)
1357
- if ((0, jsonFileReader_1.isJsonCommand)(trimmed)) {
1358
- // Save any pending request/varRequest first
1359
- if (currentRequest.length > 0) {
1360
- steps.push({
1361
- type: 'request',
1362
- content: currentRequest.join('\n'),
1363
- lineNumber: currentRequestStartLine,
1364
- });
1365
- currentRequest = [];
1366
- inRequest = false;
1367
- }
1368
- savePendingVarRequest();
1369
- // Add the JSON load step
1370
- steps.push({
1371
- type: 'json',
1372
- content: trimmed,
1373
- lineNumber: lineIdx,
1374
- });
1375
- continue;
1376
- }
1377
- // Check if this is a property assignment (config.property = value)
1378
- if ((0, jsonFileReader_1.isPropertyAssignment)(trimmed)) {
1379
- // Save any pending request/varRequest first
1380
- if (currentRequest.length > 0) {
1381
- steps.push({
1382
- type: 'request',
1383
- content: currentRequest.join('\n'),
1384
- lineNumber: currentRequestStartLine,
1385
- });
1386
- currentRequest = [];
1387
- inRequest = false;
1388
- }
1389
- savePendingVarRequest();
1390
- // Add the property assignment step
1391
- steps.push({
1392
- type: 'propAssign',
1393
- content: trimmed,
1394
- lineNumber: lineIdx,
1395
- });
1396
- continue;
1397
- }
1398
- // Check for script run commands (run bash, run js, run powershell) including var x = run ...
1399
- // Must check BEFORE isVarRunSequenceCommand to avoid treating "powershell" as a sequence name
1400
- if ((0, scriptRunner_1.isRunCommand)(trimmed)) {
1401
- // Save any pending request/varRequest first
1402
- if (currentRequest.length > 0) {
1403
- steps.push({
1404
- type: 'request',
1405
- content: currentRequest.join('\n'),
1406
- lineNumber: currentRequestStartLine,
1407
- });
1408
- currentRequest = [];
1409
- inRequest = false;
1410
- }
1411
- savePendingVarRequest();
1412
- // Add the script step
1413
- steps.push({
1414
- type: 'script',
1415
- content: trimmed,
1416
- lineNumber: lineIdx,
1417
- });
1418
- continue;
1419
- }
1420
- // Check if this is a "var x = run SequenceName" command (capture sequence result)
1421
- // Must check BEFORE isRunNamedRequestCommand since it also involves "run"
1422
- if (isVarRunSequenceCommand(trimmed)) {
1423
- // Save any pending request/varRequest first
1424
- if (currentRequest.length > 0) {
1425
- steps.push({
1426
- type: 'request',
1427
- content: currentRequest.join('\n'),
1428
- lineNumber: currentRequestStartLine,
1429
- });
1430
- currentRequest = [];
1431
- inRequest = false;
1432
- }
1433
- savePendingVarRequest();
1434
- // Add the varRunSequence step
1435
- steps.push({
1436
- type: 'varRunSequence',
1437
- content: trimmed,
1438
- lineNumber: lineIdx,
1439
- });
1440
- continue;
1441
- }
1442
- // Check if this is a run command (including var x = run ...)
1443
- // First check for "run <RequestName>" (named request invocation)
1444
- if (isRunNamedRequestCommand(trimmed)) {
1445
- // Save any pending request/varRequest first
1446
- if (currentRequest.length > 0) {
1447
- steps.push({
1448
- type: 'request',
1449
- content: currentRequest.join('\n'),
1450
- lineNumber: currentRequestStartLine,
1451
- });
1452
- currentRequest = [];
1453
- inRequest = false;
1454
- }
1455
- savePendingVarRequest();
1456
- // Add the named request step - store the whole line so we can parse args later
1457
- steps.push({
1458
- type: 'namedRequest',
1459
- content: trimmed,
1460
- lineNumber: lineIdx,
1461
- });
1462
- continue;
1463
- }
1464
- // Check if this is a variable request assignment (var x = GET/POST url)
1465
- // This must come before varAssign check since it also starts with 'var'
1466
- if (isVarRequestCommand(trimmed)) {
1467
- // Save any pending request/varRequest first
1468
- if (currentRequest.length > 0) {
1469
- steps.push({
1470
- type: 'request',
1471
- content: currentRequest.join('\n'),
1472
- lineNumber: currentRequestStartLine,
1473
- });
1474
- currentRequest = [];
1475
- inRequest = false;
1476
- }
1477
- savePendingVarRequest();
1478
- // Start collecting the variable request (may have body on subsequent lines)
1479
- currentVarRequest = [trimmed];
1480
- currentVarRequestStartLine = lineIdx;
1481
- inVarRequest = true;
1482
- continue;
1483
- }
1484
- // Check if this is a variable assignment (var x = expr or var x = "string")
1485
- // This must come after run command check to not catch var x = run ...
1486
- if (isVarAssignCommand(trimmed)) {
1487
- // Save any pending request/varRequest first
1488
- if (currentRequest.length > 0) {
1489
- steps.push({
1490
- type: 'request',
1491
- content: currentRequest.join('\n'),
1492
- lineNumber: currentRequestStartLine,
1493
- });
1494
- currentRequest = [];
1495
- inRequest = false;
1496
- }
1497
- savePendingVarRequest();
1498
- // Add the variable assignment step
1499
- steps.push({
1500
- type: 'varAssign',
1501
- content: trimmed,
1502
- lineNumber: lineIdx,
1503
- });
1504
- continue;
1505
- }
1506
- // Check if this is a new HTTP request (use trimmed for detection)
1507
- if (methodRegex.test(trimmed)) {
1508
- // Save previous request if exists
1509
- if (currentRequest.length > 0) {
1510
- steps.push({
1511
- type: 'request',
1512
- content: currentRequest.join('\n'),
1513
- lineNumber: currentRequestStartLine,
1514
- });
1515
- }
1516
- savePendingVarRequest();
1517
- // Use trimmed line for the request content
1518
- currentRequest = [trimmed];
1519
- currentRequestStartLine = lineIdx;
1520
- inRequest = true;
1521
- }
1522
- else if (inVarRequest) {
1523
- // Keep collecting all request lines (headers/body/comments/blank lines)
1524
- // until we hit a new command boundary.
1525
- if (trimmed !== '' && stepBoundaryRegex.test(trimmed)) {
1526
- savePendingVarRequest();
1527
- // Re-process this line as the next command.
1528
- lineIdx--;
1529
- }
1530
- else {
1531
- currentVarRequest.push(trimmed);
1532
- }
1533
- }
1534
- else if (inRequest) {
1535
- // Check if this is a var line with $N reference (capture) - don't include in request
1536
- if (trimmed.match(/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*\$\d+/)) {
1537
- // This is a capture line, end the current request
1538
- if (currentRequest.length > 0) {
1539
- steps.push({
1540
- type: 'request',
1541
- content: currentRequest.join('\n'),
1542
- lineNumber: currentRequestStartLine,
1543
- });
1544
- currentRequest = [];
1545
- inRequest = false;
1546
- }
1547
- }
1548
- else if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('var ')) {
1549
- // Empty lines, comments, or static vars might be part of request context
1550
- if (trimmed.startsWith('var ') && !trimmed.match(/\$\d+/)) {
1551
- // Static var, keep going - use trimmed
1552
- currentRequest.push(trimmed);
1553
- }
1554
- else if (trimmed.startsWith('#')) {
1555
- // Comment, keep in request - use trimmed
1556
- currentRequest.push(trimmed);
1557
- }
1558
- else {
1559
- // Empty line - could be body separator or end
1560
- currentRequest.push(trimmed);
1561
- }
1562
- }
1563
- else {
1564
- // Use trimmed for request body lines too
1565
- currentRequest.push(trimmed);
1566
- }
1567
- }
1568
- }
1569
- // Don't forget the last request or varRequest
1570
- if (currentRequest.length > 0) {
1571
- steps.push({
1572
- type: 'request',
1573
- content: currentRequest.join('\n'),
1574
- lineNumber: currentRequestStartLine,
1575
- });
1576
- }
1577
- savePendingVarRequest();
1578
- return steps;
1579
- }
1580
- /**
1581
- * Extracts individual requests from a sequence block.
1582
- * @deprecated Use extractStepsFromSequence instead
1583
- */
1584
- function extractRequestsFromSequence(content) {
1585
- return extractStepsFromSequence(content)
1586
- .filter(s => s.type === 'request')
1587
- .map(s => s.content);
1588
- }
1589
- function extractCaptureDirectives(content) {
1590
- const captures = [];
1591
- // Match: var x = $1 OR var x = $1.path OR var x = $1[0] OR var x = $1[0].path
1592
- const captureRegex = /^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\$(\d+)([\.\[].*)?$/;
1593
- for (const line of content.split('\n')) {
1594
- const match = line.trim().match(captureRegex);
1595
- if (match) {
1596
- let path = match[3] || '';
1597
- // Remove leading dot if present (e.g., ".user.id" -> "user.id")
1598
- if (path.startsWith('.')) {
1599
- path = path.substring(1);
1600
- }
1601
- captures.push({
1602
- varName: match[1],
1603
- afterRequest: parseInt(match[2], 10),
1604
- path: path
1605
- });
1606
- }
1607
- }
1608
- return captures;
1609
- }
1610
- /**
1611
- * Gets a value from an HTTP response using a path.
1612
- *
1613
- * Supported top-level properties:
1614
- * - status: HTTP status code (e.g., 200)
1615
- * - statusText: HTTP status text (e.g., "OK")
1616
- * - headers: Response headers object
1617
- * - headers.X: Specific header value (e.g., headers.Content-Type)
1618
- * - duration: Request duration in milliseconds
1619
- * - body: Response body (required prefix for body access)
1620
- * - body.path.to.value: Access nested body properties
1621
- *
1622
- * Examples:
1623
- * - "status" → 200
1624
- * - "headers.Content-Type" → "application/json"
1625
- * - "body" → entire response body (stringified if object)
1626
- * - "body.user.id" → nested body value
1627
- * - "body[0].name" → array access in body
1628
- */
1629
- function getValueByPath(response, path) {
1630
- // Empty path is invalid - must specify what to access
1631
- if (!path) {
1632
- return undefined;
1633
- }
1634
- // Convert [0] to .0 and split by dots, filter out empty strings
1635
- const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(p => p !== '');
1636
- if (parts.length === 0) {
1637
- return undefined;
1638
- }
1639
- const topLevel = parts[0];
1640
- const remainingParts = parts.slice(1);
1641
- // Handle top-level response properties
1642
- switch (topLevel) {
1643
- case 'status':
1644
- return response.status;
1645
- case 'statusText':
1646
- return response.statusText;
1647
- case 'duration':
1648
- return response.duration;
1649
- case 'headers':
1650
- if (remainingParts.length === 0) {
1651
- return JSON.stringify(response.headers);
1652
- }
1653
- // Access specific header (case-insensitive lookup)
1654
- const headerName = remainingParts[0];
1655
- const headerKey = Object.keys(response.headers).find(k => k.toLowerCase() === headerName.toLowerCase());
1656
- if (headerKey) {
1657
- return response.headers[headerKey];
1658
- }
1659
- return undefined;
1660
- case 'body':
1661
- // Access body or nested body property
1662
- if (remainingParts.length === 0) {
1663
- // Return entire body as-is (keep objects/arrays for type checking)
1664
- return response.body;
1665
- }
1666
- // Navigate into body
1667
- let current = response.body;
1668
- for (const part of remainingParts) {
1669
- if (current === null || current === undefined) {
1670
- return undefined;
1671
- }
1672
- current = current[part];
1673
- }
1674
- // Return the value as-is (keep objects/arrays as objects for type checking)
1675
- return current;
1676
- default:
1677
- // Unknown top-level property
1678
- return undefined;
1679
- }
1680
- }
1681
- /**
1682
- * Runs all requests in a sequence, capturing variables between requests.
1683
- * Uses the shared cookie jar by default (for VS Code extension).
1684
- * @param fullDocumentText - Optional full document text for resolving named requests (run <RequestName>)
1685
- * @param onProgress - Optional callback invoked after each step completes
1686
- * @param callStack - Internal: tracks sequence call stack for circular reference detection
1687
- * @param sequenceArgs - Optional variables to inject as sequence parameters
1688
- * @param apiDefinitions - Optional API definitions (header groups and endpoints) from imported .nornapi files
1689
- * @param tagFilterOptions - Optional tag filter options for filtering nested sequence calls
1690
- * @param sequenceSources - Optional map of sequence names to source file paths for script path resolution
1691
- */
1692
- async function runSequence(sequenceContent, fileVariables, workingDir, fullDocumentText, onProgress, callStack, sequenceArgs, apiDefinitions, tagFilterOptions, sequenceSources) {
1693
- return runSequenceWithJar(sequenceContent, fileVariables, undefined, workingDir, fullDocumentText, onProgress, callStack, sequenceArgs, apiDefinitions, tagFilterOptions, sequenceSources);
1694
- }
1695
- /**
1696
- * Runs all requests in a sequence with a specific cookie jar.
1697
- * If no jar is provided, uses shared jar (VS Code) or creates new one (CLI).
1698
- * @param fullDocumentText - Optional full document text for resolving named requests (run <RequestName>)
1699
- * @param onProgress - Optional callback invoked after each step completes
1700
- * @param callStack - Internal: tracks sequence call stack for circular reference detection
1701
- * @param sequenceArgs - Optional variables to inject as sequence parameters
1702
- * @param apiDefinitions - Optional API definitions (header groups and endpoints) from imported .nornapi files
1703
- * @param tagFilterOptions - Optional tag filter options for filtering nested sequence calls
1704
- * @param sequenceSources - Optional map of sequence names to source file paths for script path resolution
1705
- */
1706
- async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, workingDir, fullDocumentText, onProgress, callStack, sequenceArgs, apiDefinitions, tagFilterOptions, sequenceSources) {
1707
- const startTime = Date.now();
1708
- const responses = [];
1709
- const scriptResults = [];
1710
- const assertionResults = [];
1711
- const orderedSteps = [];
1712
- const errors = [];
1713
- // Helper to add response and update $N references in runtimeVariables
1714
- const addResponse = (response) => {
1715
- responses.push(response);
1716
- // Store as $N for easy reference in print, etc.
1717
- runtimeVariables[`$${responses.length}`] = response;
1718
- };
1719
- // Track mapping from response index (1-based) to variable name for enhanced failure context
1720
- const responseIndexToVariable = new Map();
1721
- // Extract steps (requests and scripts) and capture directives
1722
- const steps = extractStepsFromSequence(sequenceContent);
1723
- const captures = extractCaptureDirectives(sequenceContent);
1724
- const totalSteps = steps.length;
1725
- // Calculate nesting depth from call stack
1726
- const nestingDepth = callStack ? callStack.length : 0;
1727
- // Helper to report progress
1728
- const reportProgress = (stepIdx, stepType, description, result, sequenceName) => {
1729
- if (onProgress) {
1730
- onProgress({
1731
- currentStep: stepIdx + 1,
1732
- totalSteps,
1733
- stepType,
1734
- stepDescription: description,
1735
- stepResult: result,
1736
- sequenceName,
1737
- nestingDepth
1738
- });
1739
- }
1740
- };
1741
- // Runtime variables (starts with file variables, grows with captures and script outputs)
1742
- // Inject sequence arguments (parameters) which shadow file variables
1743
- // Type is any to allow storing response objects (from var x = GET url)
1744
- const runtimeVariables = { ...fileVariables, ...sequenceArgs };
1745
- // Track request index for $N capture references
1746
- let requestIndex = 0;
1747
- // Track if-block state: stack of booleans (true = executing, false = skipping)
1748
- const ifStack = [];
1749
- // Helper to check if we should skip the current step (inside a false if-block)
1750
- const shouldSkip = () => ifStack.length > 0 && ifStack.some(v => !v);
1751
- for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) {
1752
- const step = steps[stepIdx];
1753
- // Handle if/endif first (they affect control flow)
1754
- if (step.type === 'if') {
1755
- // Always evaluate if statements, even when skipping (for nesting)
1756
- if (shouldSkip()) {
1757
- // Already skipping, push false to maintain nesting
1758
- ifStack.push(false);
1759
- }
1760
- else {
1761
- // Evaluate the condition
1762
- const conditionMet = evaluateIfCondition(step.content, responses, runtimeVariables, getValueByPath);
1763
- ifStack.push(conditionMet);
1764
- const stepResult = {
1765
- type: 'print',
1766
- stepIndex: stepIdx,
1767
- print: {
1768
- title: `if ${step.content}: ${conditionMet ? 'true' : 'false'}`,
1769
- timestamp: Date.now() - startTime
1770
- }
1771
- };
1772
- orderedSteps.push(stepResult);
1773
- reportProgress(stepIdx, 'if', `if ${step.content} → ${conditionMet}`, stepResult);
1774
- }
1775
- continue;
1776
- }
1777
- if (step.type === 'endif') {
1778
- if (ifStack.length > 0) {
1779
- ifStack.pop();
1780
- }
1781
- continue;
1782
- }
1783
- // Skip steps if inside a false if-block
1784
- if (shouldSkip()) {
1785
- continue;
1786
- }
1787
- if (step.type === 'assertion') {
1788
- // Parse and evaluate the assertion
1789
- const parsed = (0, assertionRunner_1.parseAssertCommand)((0, parser_1.substituteVariables)(step.content, runtimeVariables));
1790
- if (!parsed) {
1791
- errors.push(`Step ${stepIdx + 1}: Invalid assert command: ${step.content}`);
1792
- return {
1793
- name: '',
1794
- success: false,
1795
- responses,
1796
- scriptResults,
1797
- assertionResults,
1798
- steps: orderedSteps,
1799
- errors,
1800
- duration: Date.now() - startTime
1801
- };
1802
- }
1803
- const result = (0, assertionRunner_1.evaluateAssertion)(parsed, responses, runtimeVariables, getValueByPath, responseIndexToVariable, workingDir);
1804
- assertionResults.push(result);
1805
- const stepResult = {
1806
- type: 'assertion',
1807
- stepIndex: stepIdx,
1808
- assertion: result,
1809
- lineNumber: step.lineNumber
1810
- };
1811
- orderedSteps.push(stepResult);
1812
- const statusIcon = result.passed ? '✓' : '✗';
1813
- reportProgress(stepIdx, 'assertion', `${statusIcon} assert ${result.expression}`, stepResult);
1814
- // If assertion failed, stop the sequence
1815
- if (!result.passed) {
1816
- const failMessage = result.message
1817
- ? `Assertion failed: ${result.message}`
1818
- : `Assertion failed: ${result.expression}`;
1819
- errors.push(failMessage);
1820
- if (result.error) {
1821
- errors.push(` Error: ${result.error}`);
1822
- }
1823
- else {
1824
- errors.push(` Expected: ${result.rightExpression ?? result.operator}`);
1825
- errors.push(` Actual: ${JSON.stringify(result.leftValue)}`);
1826
- }
1827
- return {
1828
- name: '',
1829
- success: false,
1830
- responses,
1831
- scriptResults,
1832
- assertionResults,
1833
- steps: orderedSteps,
1834
- errors,
1835
- duration: Date.now() - startTime
1836
- };
1837
- }
1838
- continue;
1839
- }
1840
- if (step.type === 'print') {
1841
- // Parse and substitute variables in the print content
1842
- const parsed = parsePrintCommand(step.content);
1843
- // First resolve bare variables, then substitute {{var}} patterns
1844
- const titleResolved = resolveBareVariables(parsed.title, runtimeVariables);
1845
- const title = (0, parser_1.substituteVariables)(titleResolved, runtimeVariables);
1846
- const bodyResolved = parsed.body ? resolveBareVariables(parsed.body, runtimeVariables) : undefined;
1847
- const body = bodyResolved ? (0, parser_1.substituteVariables)(bodyResolved, runtimeVariables) : undefined;
1848
- const stepResult = {
1849
- type: 'print',
1850
- stepIndex: stepIdx,
1851
- print: {
1852
- title,
1853
- body,
1854
- timestamp: Date.now() - startTime
1855
- }
1856
- };
1857
- orderedSteps.push(stepResult);
1858
- reportProgress(stepIdx, 'print', `print: ${title}`, stepResult);
1859
- continue;
1860
- }
1861
- if (step.type === 'wait') {
1862
- // Parse the wait duration
1863
- const durationMs = parseWaitCommand(step.content);
1864
- // Report that we're starting to wait
1865
- reportProgress(stepIdx, 'wait', `wait: ${durationMs}ms`);
1866
- // Actually wait
1867
- await sleep(durationMs);
1868
- // Create a print-like result for the wait step
1869
- const stepResult = {
1870
- type: 'print',
1871
- stepIndex: stepIdx,
1872
- print: {
1873
- title: `Waited ${durationMs >= 1000 ? (durationMs / 1000) + 's' : durationMs + 'ms'}`,
1874
- timestamp: Date.now() - startTime
1875
- }
1876
- };
1877
- orderedSteps.push(stepResult);
1878
- continue;
1879
- }
1880
- if (step.type === 'json') {
1881
- // Parse the json command with variable substitution in the path
1882
- // First resolve bare variables, then {{var}} patterns
1883
- const bareResolved = resolveBareVariables(step.content, runtimeVariables);
1884
- const parsed = (0, jsonFileReader_1.parseJsonCommand)((0, parser_1.substituteVariables)(bareResolved, runtimeVariables));
1885
- if (!parsed) {
1886
- errors.push(`Step ${stepIdx + 1}: Invalid json command: ${step.content}`);
1887
- return {
1888
- name: '',
1889
- success: false,
1890
- responses,
1891
- scriptResults,
1892
- assertionResults,
1893
- steps: orderedSteps,
1894
- errors,
1895
- duration: Date.now() - startTime
1896
- };
1897
- }
1898
- // Read the JSON file
1899
- const result = (0, jsonFileReader_1.readJsonFile)(parsed.filePath, workingDir);
1900
- const stepResult = {
1901
- type: 'json',
1902
- stepIndex: stepIdx,
1903
- json: {
1904
- varName: parsed.varName,
1905
- filePath: result.filePath,
1906
- success: result.success,
1907
- error: result.error,
1908
- timestamp: Date.now() - startTime
1909
- }
1910
- };
1911
- orderedSteps.push(stepResult);
1912
- if (!result.success) {
1913
- errors.push(`Failed to load JSON file: ${result.error}`);
1914
- reportProgress(stepIdx, 'json', `json: ${parsed.varName} ✗ ${result.error}`, stepResult);
1915
- return {
1916
- name: '',
1917
- success: false,
1918
- responses,
1919
- scriptResults,
1920
- assertionResults,
1921
- steps: orderedSteps,
1922
- errors,
1923
- duration: Date.now() - startTime
1924
- };
1925
- }
1926
- // Store the JSON data as a stringified value in runtimeVariables
1927
- // This allows {{varName.property}} access via the substituteVariables function
1928
- runtimeVariables[parsed.varName] = JSON.stringify(result.data);
1929
- reportProgress(stepIdx, 'json', `json: ${parsed.varName} = ${parsed.filePath}`, stepResult);
1930
- continue;
1931
- }
1932
- if (step.type === 'propAssign') {
1933
- // Parse the property assignment first to extract the value part
1934
- // Then resolve variables in the value specifically
1935
- const parsed = (0, jsonFileReader_1.parsePropertyAssignment)(step.content);
1936
- if (!parsed) {
1937
- errors.push(`Step ${stepIdx + 1}: Invalid property assignment: ${step.content}`);
1938
- return {
1939
- name: '',
1940
- success: false,
1941
- responses,
1942
- scriptResults,
1943
- assertionResults,
1944
- steps: orderedSteps,
1945
- errors,
1946
- duration: Date.now() - startTime
1947
- };
1948
- }
1949
- // Check if the variable exists and is a JSON object
1950
- if (!(parsed.varName in runtimeVariables)) {
1951
- errors.push(`Step ${stepIdx + 1}: Variable '${parsed.varName}' is not defined`);
1952
- return {
1953
- name: '',
1954
- success: false,
1955
- responses,
1956
- scriptResults,
1957
- assertionResults,
1958
- steps: orderedSteps,
1959
- errors,
1960
- duration: Date.now() - startTime
1961
- };
1962
- }
1963
- // Parse the current JSON value
1964
- let jsonObj;
1965
- try {
1966
- jsonObj = JSON.parse(runtimeVariables[parsed.varName]);
1967
- }
1968
- catch {
1969
- errors.push(`Step ${stepIdx + 1}: Variable '${parsed.varName}' is not a valid JSON object`);
1970
- return {
1971
- name: '',
1972
- success: false,
1973
- responses,
1974
- scriptResults,
1975
- assertionResults,
1976
- steps: orderedSteps,
1977
- errors,
1978
- duration: Date.now() - startTime
1979
- };
1980
- }
1981
- // Check if the value is a bare variable reference - if so, preserve its original type
1982
- const valueTrimmed = parsed.value.trim();
1983
- const bareVarMatch = valueTrimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
1984
- let newValue;
1985
- if (bareVarMatch && bareVarMatch[1] in runtimeVariables) {
1986
- // It's a bare variable - preserve the original type
1987
- const varName = bareVarMatch[1];
1988
- const pathPart = bareVarMatch[2] || '';
1989
- let value = runtimeVariables[varName];
1990
- // If it's a JSON string, parse it first
1991
- if (typeof value === 'string') {
1992
- try {
1993
- const parsed = JSON.parse(value);
1994
- if (typeof parsed === 'object' && parsed !== null) {
1995
- value = parsed;
1996
- }
1997
- }
1998
- catch {
1999
- // Not a JSON string, use as-is
2000
- }
2001
- }
2002
- if (pathPart) {
2003
- // Navigate the path
2004
- const path = pathPart.replace(/^\./, '');
2005
- newValue = getNestedValueFromObject(value, path);
2006
- }
2007
- else {
2008
- newValue = value;
2009
- }
2010
- }
2011
- else {
2012
- // Not a bare variable - resolve variables and parse as JSON
2013
- const bareResolvedValue = resolveBareVariables(parsed.value, runtimeVariables);
2014
- const resolvedValue = (0, parser_1.substituteVariables)(bareResolvedValue, runtimeVariables);
2015
- // Parse the value - try to interpret it as JSON first, then as string
2016
- newValue = resolvedValue;
2017
- try {
2018
- newValue = JSON.parse(resolvedValue);
2019
- }
2020
- catch {
2021
- // Keep as string if not valid JSON
2022
- }
2023
- }
2024
- // Set the nested value
2025
- const success = (0, jsonFileReader_1.setNestedValue)(jsonObj, parsed.propertyPath, newValue);
2026
- if (!success) {
2027
- errors.push(`Step ${stepIdx + 1}: Failed to set property '${parsed.propertyPath}' on '${parsed.varName}'`);
2028
- return {
2029
- name: '',
2030
- success: false,
2031
- responses,
2032
- scriptResults,
2033
- assertionResults,
2034
- steps: orderedSteps,
2035
- errors,
2036
- duration: Date.now() - startTime
2037
- };
2038
- }
2039
- // Update the runtime variable with the modified JSON
2040
- runtimeVariables[parsed.varName] = JSON.stringify(jsonObj);
2041
- // No step result needed for property assignment - it's a simple operation
2042
- // But we report progress for visibility
2043
- reportProgress(stepIdx, 'propAssign', `${parsed.varName}.${parsed.propertyPath} = ${parsed.value}`, undefined);
2044
- continue;
2045
- }
2046
- if (step.type === 'varAssign') {
2047
- // Parse the variable assignment
2048
- const parsed = parseVarAssignCommand(step.content);
2049
- if (!parsed) {
2050
- errors.push(`Step ${stepIdx + 1}: Invalid variable assignment: ${step.content}`);
2051
- return {
2052
- name: '',
2053
- success: false,
2054
- responses,
2055
- scriptResults,
2056
- assertionResults,
2057
- steps: orderedSteps,
2058
- errors,
2059
- duration: Date.now() - startTime
2060
- };
2061
- }
2062
- // Evaluate the expression
2063
- const result = evaluateValueExpression(parsed.valueExpr, runtimeVariables);
2064
- if (result.error) {
2065
- errors.push(`Step ${stepIdx + 1}: ${result.error}`);
2066
- return {
2067
- name: '',
2068
- success: false,
2069
- responses,
2070
- scriptResults,
2071
- assertionResults,
2072
- steps: orderedSteps,
2073
- errors,
2074
- duration: Date.now() - startTime
2075
- };
2076
- }
2077
- // Store the result
2078
- runtimeVariables[parsed.varName] = result.value;
2079
- // Report progress
2080
- reportProgress(stepIdx, 'propAssign', `var ${parsed.varName} = ${result.value}`, undefined);
2081
- continue;
2082
- }
2083
- if (step.type === 'varRequest') {
2084
- // Parse the variable request command: var name = GET/POST url/endpoint
2085
- // Only parse the first line (command line), body is on subsequent lines
2086
- const commandLine = step.content.split('\n')[0];
2087
- const parsed = parseVarRequestCommand(commandLine);
2088
- if (!parsed) {
2089
- errors.push(`Step ${stepIdx + 1}: Invalid variable request: ${commandLine}`);
2090
- return {
2091
- name: '',
2092
- success: false,
2093
- responses,
2094
- scriptResults,
2095
- assertionResults,
2096
- steps: orderedSteps,
2097
- errors,
2098
- duration: Date.now() - startTime
2099
- };
2100
- }
2101
- requestIndex++; // Count as a request for $N references
2102
- let resolvedUrl = '';
2103
- let resolvedHeaders = {};
2104
- let requestDescription = `${parsed.method} ${parsed.url}`;
2105
- try {
2106
- // Check if the URL is actually a named endpoint (e.g., GetUser(1))
2107
- const endpointMatch = parsed.url.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\(([^)]*)\))?(\s+.*)?$/);
2108
- const potentialEndpointName = endpointMatch ? endpointMatch[1] : null;
2109
- const endpoint = apiDefinitions && potentialEndpointName
2110
- ? (0, nornapiParser_1.getEndpoint)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, potentialEndpointName)
2111
- : undefined;
2112
- if (endpoint && apiDefinitions) {
2113
- // This is a named endpoint - resolve it
2114
- const paramsStr = endpointMatch[2] || '';
2115
- const headerGroupsStr = endpointMatch[3]?.trim() || '';
2116
- // Parse parameters - supports both positional and named syntax
2117
- // Positional: GetUser("123", "456")
2118
- // Named: GetUser(id: "123", orderId: "456")
2119
- const paramTokens = paramsStr ? paramsStr.split(',').map(p => p.trim()) : [];
2120
- const params = {};
2121
- // Check if using named parameter syntax (first token contains ':')
2122
- const isNamedSyntax = paramTokens.length > 0 && paramTokens[0].includes(':');
2123
- if (isNamedSyntax) {
2124
- // Named parameter syntax: param: value
2125
- for (const token of paramTokens) {
2126
- const kvMatch = token.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.+)$/);
2127
- if (kvMatch) {
2128
- const paramName = kvMatch[1];
2129
- let paramValue = kvMatch[2].trim().replace(/^["']|["']$/g, '');
2130
- // Resolve variables
2131
- const bareResolved = resolveBareVariables(paramValue, runtimeVariables);
2132
- params[paramName] = (0, parser_1.substituteVariables)(bareResolved, runtimeVariables);
2133
- }
2134
- }
2135
- }
2136
- else {
2137
- // Positional parameter syntax
2138
- endpoint.parameters.forEach((paramName, idx) => {
2139
- if (paramTokens[idx] !== undefined) {
2140
- let paramValue = paramTokens[idx].replace(/^["']|["']$/g, '');
2141
- // Resolve variables
2142
- const bareResolved = resolveBareVariables(paramValue, runtimeVariables);
2143
- params[paramName] = (0, parser_1.substituteVariables)(bareResolved, runtimeVariables);
2144
- }
2145
- });
2146
- }
2147
- // Resolve endpoint path with variables and parameters
2148
- let pathWithVars = (0, parser_1.substituteVariables)(endpoint.path, runtimeVariables);
2149
- for (const paramName of endpoint.parameters) {
2150
- const value = params[paramName];
2151
- if (value !== undefined) {
2152
- pathWithVars = pathWithVars.replace(`{${paramName}}`, value);
2153
- }
2154
- }
2155
- resolvedUrl = pathWithVars;
2156
- // Collect headers from header groups if specified
2157
- if (headerGroupsStr) {
2158
- const headerGroupNames = headerGroupsStr.split(/\s+/).filter(n => n);
2159
- for (const groupName of headerGroupNames) {
2160
- const group = (0, nornapiParser_1.getHeaderGroup)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, groupName);
2161
- if (group) {
2162
- const groupHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, runtimeVariables);
2163
- Object.assign(resolvedHeaders, groupHeaders);
2164
- }
2165
- }
2166
- }
2167
- requestDescription = `${potentialEndpointName}(${paramTokens.join(', ')}) → ${parsed.method} ${resolvedUrl}`;
2168
- }
2169
- else {
2170
- // Regular URL - substitute variables
2171
- resolvedUrl = (0, parser_1.substituteVariables)(parsed.url, runtimeVariables);
2172
- requestDescription = `${parsed.method} ${resolvedUrl}`;
2173
- }
2174
- // Build request with resolved URL and any headers
2175
- let requestText = `${parsed.method} ${resolvedUrl}`;
2176
- if (Object.keys(resolvedHeaders).length > 0) {
2177
- const headerLines = Object.entries(resolvedHeaders).map(([k, v]) => `${k}: ${v}`);
2178
- requestText += '\n' + headerLines.join('\n');
2179
- }
2180
- // Extract and add body lines from step.content (lines after the command line)
2181
- const contentLines = step.content.split('\n');
2182
- if (contentLines.length > 1) {
2183
- // Additional lines may contain headers + optional blank separator + body,
2184
- // or body-only content. Preserve explicit structure when present.
2185
- const extraLines = contentLines.slice(1).map(line => (0, parser_1.substituteVariables)(line, runtimeVariables));
2186
- const hasExplicitBlank = extraLines.some(line => line.trim() === '');
2187
- const firstNonEmpty = extraLines.find(line => line.trim() !== '');
2188
- const startsWithHeader = firstNonEmpty ? /^[A-Za-z0-9\-_]+\s*:/.test(firstNonEmpty.trim()) : false;
2189
- if (hasExplicitBlank || startsWithHeader) {
2190
- requestText += '\n' + extraLines.join('\n');
2191
- }
2192
- else {
2193
- // Body-only shorthand (no headers/blank separator) should still parse as body.
2194
- requestText += '\n\n' + extraLines.join('\n');
2195
- }
2196
- }
2197
- const requestParsed = (0, parser_1.parserHttpRequest)(requestText, runtimeVariables);
2198
- // Build retry options if specified
2199
- const retryOpts = parsed.retryCount
2200
- ? {
2201
- retryCount: parsed.retryCount,
2202
- backoffMs: parsed.backoffMs || 1000,
2203
- onRetry: (attempt, total, error, waitMs) => {
2204
- reportProgress(stepIdx, 'request', `[Retry ${attempt}/${total}] ${requestDescription} - waiting ${waitMs}ms`, undefined);
2205
- }
2206
- }
2207
- : undefined;
2208
- const response = cookieJar
2209
- ? await (0, httpClient_1.sendRequestWithJar)(requestParsed, cookieJar, retryOpts)
2210
- : await (0, httpClient_1.sendRequest)(requestParsed, retryOpts);
2211
- addResponse(response);
2212
- // Track variable name → response index mapping for enhanced failure context
2213
- responseIndexToVariable.set(responses.length, parsed.varName);
2214
- // Store the FULL response object in the variable (not stringified)
2215
- // This allows {{varName.body.path}}, {{varName.status}}, etc.
2216
- runtimeVariables[parsed.varName] = response;
2217
- const stepResult = {
2218
- type: 'request',
2219
- stepIndex: stepIdx,
2220
- response: response,
2221
- requestMethod: parsed.method,
2222
- requestUrl: resolvedUrl,
2223
- variableName: parsed.varName
2224
- };
2225
- orderedSteps.push(stepResult);
2226
- reportProgress(stepIdx, 'request', `var ${parsed.varName} = ${requestDescription}`, stepResult);
2227
- // Also process captures that reference this response by $N
2228
- const relevantCaptures = captures.filter(c => c.afterRequest === requestIndex);
2229
- for (const capture of relevantCaptures) {
2230
- const value = getValueByPath(response, capture.path);
2231
- if (value !== undefined) {
2232
- // Preserve the value as-is (objects, arrays, primitives) for type checking
2233
- runtimeVariables[capture.varName] = value;
2234
- }
2235
- else {
2236
- errors.push(`Warning: Could not capture ${capture.varName} from $${requestIndex}.${capture.path}`);
2237
- }
2238
- }
2239
- }
2240
- catch (error) {
2241
- // Build a more informative error message
2242
- let errorDetails = `Request failed for var ${parsed.varName}: ${error.message}`;
2243
- if (requestDescription) {
2244
- errorDetails += `\n Request: ${requestDescription}`;
2245
- }
2246
- errors.push(errorDetails);
2247
- return {
2248
- name: '',
2249
- success: false,
2250
- responses,
2251
- scriptResults,
2252
- assertionResults,
2253
- steps: orderedSteps,
2254
- errors,
2255
- duration: Date.now() - startTime
2256
- };
2257
- }
2258
- continue;
2259
- }
2260
- if (step.type === 'script') {
2261
- // Parse and execute the script
2262
- const parsed = (0, scriptRunner_1.parseRunCommand)((0, parser_1.substituteVariables)(step.content, runtimeVariables));
2263
- if (!parsed) {
2264
- errors.push(`Step ${stepIdx + 1}: Invalid run command: ${step.content}`);
2265
- return {
2266
- name: '',
2267
- success: false,
2268
- responses,
2269
- scriptResults,
2270
- assertionResults,
2271
- steps: orderedSteps,
2272
- errors,
2273
- duration: Date.now() - startTime
2274
- };
2275
- }
2276
- // Substitute variables in args - first resolve bare variables, then {{var}} patterns
2277
- const substitutedArgs = parsed.args.map(arg => {
2278
- const bareResolved = resolveBareVariables(arg, runtimeVariables);
2279
- return (0, parser_1.substituteVariables)(bareResolved, runtimeVariables);
2280
- });
2281
- const substitutedPath = (0, parser_1.substituteVariables)(parsed.scriptPath, runtimeVariables);
2282
- try {
2283
- const result = await (0, scriptRunner_1.runScript)(parsed.type, substitutedPath, substitutedArgs, workingDir || process.cwd(), runtimeVariables, parsed.captureAs);
2284
- scriptResults.push(result);
2285
- const stepResult = {
2286
- type: 'script',
2287
- stepIndex: stepIdx,
2288
- script: result
2289
- };
2290
- orderedSteps.push(stepResult);
2291
- reportProgress(stepIdx, 'script', `run ${parsed.type} ${substitutedPath}`, stepResult);
2292
- if (!result.success) {
2293
- errors.push(`Script failed (exit ${result.exitCode}): ${result.error || result.output}`);
2294
- return {
2295
- name: '',
2296
- success: false,
2297
- responses,
2298
- scriptResults,
2299
- assertionResults,
2300
- steps: orderedSteps,
2301
- errors,
2302
- duration: Date.now() - startTime
2303
- };
2304
- }
2305
- // If this was a capture command (var x = run ...), store the output
2306
- if (parsed.captureAs) {
2307
- // Try to parse as JSON for object/array/number/boolean
2308
- let outputValue = result.output;
2309
- try {
2310
- const jsonParsed = JSON.parse(result.output);
2311
- // Accept objects, arrays, numbers, and booleans from JSON parse
2312
- // This means "1234" becomes 1234 (number), "true" becomes true (boolean)
2313
- // Strings that don't parse as JSON stay as strings
2314
- outputValue = jsonParsed;
2315
- }
2316
- catch {
2317
- // Not valid JSON, keep as string
2318
- }
2319
- runtimeVariables[parsed.captureAs] = outputValue;
2320
- }
2321
- }
2322
- catch (error) {
2323
- errors.push(`Script execution error: ${error.message}`);
2324
- return {
2325
- name: '',
2326
- success: false,
2327
- responses,
2328
- scriptResults,
2329
- assertionResults,
2330
- steps: orderedSteps,
2331
- errors,
2332
- duration: Date.now() - startTime
2333
- };
2334
- }
2335
- }
2336
- else if (step.type === 'namedRequest') {
2337
- // Named request OR sequence invocation: run <Name> or run <Name>(args)
2338
- const parsed = parseRunNamedRequestCommand(step.content);
2339
- if (!parsed) {
2340
- errors.push(`Invalid run command: ${step.content}`);
2341
- return {
2342
- name: '',
2343
- success: false,
2344
- responses,
2345
- scriptResults,
2346
- assertionResults,
2347
- steps: orderedSteps,
2348
- errors,
2349
- duration: Date.now() - startTime
2350
- };
2351
- }
2352
- const { name: targetName, args: runArgs } = parsed;
2353
- if (!fullDocumentText) {
2354
- errors.push(`Cannot run "${targetName}": full document text not provided`);
2355
- return {
2356
- name: '',
2357
- success: false,
2358
- responses,
2359
- scriptResults,
2360
- assertionResults,
2361
- steps: orderedSteps,
2362
- errors,
2363
- duration: Date.now() - startTime
2364
- };
2365
- }
2366
- // First, check if this is a sequence name
2367
- const allSequences = extractSequences(fullDocumentText);
2368
- const targetSequence = allSequences.find(s => s.name === targetName);
2369
- if (targetSequence) {
2370
- // This is a sequence call - run the sequence recursively
2371
- // Initialize call stack if not provided
2372
- const currentCallStack = callStack || [];
2373
- // Check for circular reference
2374
- if (currentCallStack.includes(targetName)) {
2375
- errors.push(`Circular reference detected: ${[...currentCallStack, targetName].join(' → ')}`);
2376
- return {
2377
- name: '',
2378
- success: false,
2379
- responses,
2380
- scriptResults,
2381
- assertionResults,
2382
- steps: orderedSteps,
2383
- errors,
2384
- duration: Date.now() - startTime
2385
- };
2386
- }
2387
- // Bind arguments to parameters
2388
- let sequenceArgs = {};
2389
- if (targetSequence.parameters.length > 0 || runArgs.length > 0) {
2390
- const bindResult = bindSequenceArguments(targetSequence.parameters, runArgs, runtimeVariables);
2391
- if ('error' in bindResult) {
2392
- errors.push(`Error calling sequence "${targetName}": ${bindResult.error}`);
2393
- return {
2394
- name: '',
2395
- success: false,
2396
- responses,
2397
- scriptResults,
2398
- assertionResults,
2399
- steps: orderedSteps,
2400
- errors,
2401
- duration: Date.now() - startTime
2402
- };
2403
- }
2404
- sequenceArgs = bindResult.variables;
2405
- }
2406
- // Check if tag filtering is active and if the target sequence matches
2407
- if (tagFilterOptions && tagFilterOptions.filters.length > 0) {
2408
- if (!sequenceMatchesTags(targetSequence, tagFilterOptions)) {
2409
- // Silently skip this sequence - it doesn't match the tag filter
2410
- continue;
2411
- }
2412
- }
2413
- // Report that we're starting a sub-sequence
2414
- reportProgress(stepIdx, 'sequenceStart', `Starting sequence ${targetName}`, undefined, targetName);
2415
- // Determine the working directory for this sub-sequence
2416
- // If we have source info for this sequence (from imports), use that file's directory
2417
- // This ensures script paths resolve relative to where the sequence was defined
2418
- let subWorkingDir = workingDir;
2419
- if (sequenceSources) {
2420
- const sourceFile = sequenceSources.get(targetName.toLowerCase());
2421
- if (sourceFile) {
2422
- const path = await import('path');
2423
- subWorkingDir = path.dirname(sourceFile);
2424
- }
2425
- }
2426
- // Run the sub-sequence with current runtime variables
2427
- // Variables set in the sub-sequence will be available after it completes
2428
- const subResult = await runSequenceWithJar(targetSequence.content, runtimeVariables, // Pass current runtime variables (includes file vars + any captured)
2429
- cookieJar, subWorkingDir, fullDocumentText, onProgress, // Pass through progress callback
2430
- [...currentCallStack, targetName], // Add to call stack for circular detection
2431
- sequenceArgs, // Pass bound arguments as initial scope variables
2432
- apiDefinitions, // Pass API definitions for endpoint resolution
2433
- tagFilterOptions, // Pass tag filter options for nested sequence filtering
2434
- sequenceSources // Pass sequence sources for nested sequences
2435
- );
2436
- // Report that sub-sequence ended
2437
- reportProgress(stepIdx, 'sequenceEnd', `Finished sequence ${targetName}`, undefined, targetName);
2438
- // Merge results from sub-sequence and update $N references
2439
- for (const subResponse of subResult.responses) {
2440
- addResponse(subResponse);
2441
- }
2442
- scriptResults.push(...subResult.scriptResults);
2443
- assertionResults.push(...subResult.assertionResults);
2444
- // Merge step results (offset step indices)
2445
- for (const subStep of subResult.steps) {
2446
- orderedSteps.push({
2447
- ...subStep,
2448
- stepIndex: orderedSteps.length
2449
- });
2450
- }
2451
- // No variable merging for plain "run Sequence"
2452
- // Variables are only accessible via "var x = run Sequence" capture syntax
2453
- // Update request index to account for sub-sequence requests
2454
- requestIndex += subResult.responses.length;
2455
- // If sub-sequence failed, propagate the failure
2456
- if (!subResult.success) {
2457
- errors.push(`Sequence "${targetName}" failed`);
2458
- errors.push(...subResult.errors);
2459
- return {
2460
- name: '',
2461
- success: false,
2462
- responses,
2463
- scriptResults,
2464
- assertionResults,
2465
- steps: orderedSteps,
2466
- errors,
2467
- duration: Date.now() - startTime,
2468
- variables: runtimeVariables
2469
- };
2470
- }
2471
- // Sub-sequence succeeded - continue
2472
- continue;
2473
- }
2474
- // Not a sequence - check for named request
2475
- const namedRequest = (0, parser_1.getNamedRequest)(fullDocumentText, targetName);
2476
- if (!namedRequest) {
2477
- errors.push(`"${targetName}" is not a named request or sequence`);
2478
- return {
2479
- name: '',
2480
- success: false,
2481
- responses,
2482
- scriptResults,
2483
- assertionResults,
2484
- steps: orderedSteps,
2485
- errors,
2486
- duration: Date.now() - startTime
2487
- };
2488
- }
2489
- requestIndex++; // 1-based for user reference
2490
- let requestUrl = '';
2491
- let requestMethod = '';
2492
- try {
2493
- let requestParsed;
2494
- // Check if the named request uses API endpoint syntax (e.g., "POST HttpBinPost Form")
2495
- if (apiDefinitions && apiDefinitions.endpoints.length > 0 &&
2496
- (0, nornapiParser_1.isApiRequestLine)(namedRequest.content, apiDefinitions.endpoints)) {
2497
- const apiRequest = (0, nornapiParser_1.parseApiRequest)(namedRequest.content, apiDefinitions.endpoints, apiDefinitions.headerGroups);
2498
- if (apiRequest) {
2499
- const endpoint = (0, nornapiParser_1.getEndpoint)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, apiRequest.endpointName);
2500
- if (endpoint) {
2501
- // Resolve the endpoint path with variables and parameters
2502
- let resolvedPath = endpoint.path;
2503
- // Substitute {{variable}} references
2504
- resolvedPath = resolvedPath.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
2505
- return runtimeVariables[varName] !== undefined ? String(runtimeVariables[varName]) : `{{${varName}}}`;
2506
- });
2507
- for (const [paramName, paramValue] of Object.entries(apiRequest.params)) {
2508
- let resolvedValue = paramValue;
2509
- if (runtimeVariables[paramValue] !== undefined) {
2510
- resolvedValue = String(runtimeVariables[paramValue]);
2511
- }
2512
- else if (paramValue.startsWith('{{') && paramValue.endsWith('}}')) {
2513
- const varName = paramValue.slice(2, -2);
2514
- if (runtimeVariables[varName] !== undefined) {
2515
- resolvedValue = String(runtimeVariables[varName]);
2516
- }
2517
- }
2518
- resolvedPath = resolvedPath.replace(`{${paramName}}`, resolvedValue);
2519
- }
2520
- // Collect headers from header groups
2521
- const combinedHeaders = {};
2522
- for (const groupName of apiRequest.headerGroupNames) {
2523
- const group = (0, nornapiParser_1.getHeaderGroup)(apiDefinitions, groupName);
2524
- if (group) {
2525
- const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, runtimeVariables);
2526
- Object.assign(combinedHeaders, resolvedHeaders);
2527
- }
2528
- }
2529
- // Add inline headers
2530
- for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
2531
- let resolved = headerValue;
2532
- resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
2533
- return runtimeVariables[varName] !== undefined ? String(runtimeVariables[varName]) : `{{${varName}}}`;
2534
- });
2535
- combinedHeaders[headerName] = resolved;
2536
- }
2537
- // Extract body from the named request content (lines after the request line)
2538
- const bodyParsed = (0, parser_1.parserHttpRequest)(namedRequest.content, runtimeVariables);
2539
- requestParsed = {
2540
- method: apiRequest.method,
2541
- url: resolvedPath,
2542
- headers: combinedHeaders,
2543
- body: bodyParsed.body
2544
- };
2545
- }
2546
- else {
2547
- throw new Error(`Unknown endpoint: ${apiRequest.endpointName}`);
2548
- }
2549
- }
2550
- else {
2551
- // Fallback to regular parsing
2552
- requestParsed = (0, parser_1.parserHttpRequest)(namedRequest.content, runtimeVariables);
2553
- }
2554
- }
2555
- else {
2556
- // Parse as regular HTTP request
2557
- requestParsed = (0, parser_1.parserHttpRequest)(namedRequest.content, runtimeVariables);
2558
- // Apply header groups if available (handles "GET url Json" syntax)
2559
- if (apiDefinitions && apiDefinitions.headerGroups.length > 0) {
2560
- requestParsed = applyHeaderGroupsToRequest(requestParsed, namedRequest.content, apiDefinitions.headerGroups, runtimeVariables);
2561
- }
2562
- }
2563
- requestUrl = requestParsed.url;
2564
- requestMethod = requestParsed.method;
2565
- // Build retry options - prefer options from the run command, fallback to options in request content
2566
- const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
2567
- const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1000;
2568
- const retryOpts = effectiveRetryCount
2569
- ? {
2570
- retryCount: effectiveRetryCount,
2571
- backoffMs: effectiveBackoffMs,
2572
- onRetry: (attempt, total, error, waitMs) => {
2573
- reportProgress(stepIdx, 'request', `[Retry ${attempt}/${total}] run ${targetName} - waiting ${waitMs}ms`, undefined);
2574
- }
2575
- }
2576
- : undefined;
2577
- const response = cookieJar
2578
- ? await (0, httpClient_1.sendRequestWithJar)(requestParsed, cookieJar, retryOpts)
2579
- : await (0, httpClient_1.sendRequest)(requestParsed, retryOpts);
2580
- addResponse(response);
2581
- const stepResult = {
2582
- type: 'request',
2583
- stepIndex: stepIdx,
2584
- response: response,
2585
- requestMethod: requestParsed.method,
2586
- requestUrl: requestParsed.url
2587
- };
2588
- orderedSteps.push(stepResult);
2589
- reportProgress(stepIdx, 'namedRequest', `run ${targetName} → ${requestParsed.method} ${requestParsed.url}`, stepResult);
2590
- // Process captures that reference this response
2591
- const relevantCaptures = captures.filter(c => c.afterRequest === requestIndex);
2592
- for (const capture of relevantCaptures) {
2593
- const value = getValueByPath(response, capture.path);
2594
- if (value !== undefined) {
2595
- // Preserve the value as-is (objects, arrays, primitives) for type checking
2596
- runtimeVariables[capture.varName] = value;
2597
- }
2598
- else {
2599
- errors.push(`Warning: Could not capture ${capture.varName} from $${requestIndex}.${capture.path}`);
2600
- }
2601
- }
2602
- }
2603
- catch (error) {
2604
- // Build a more informative error message
2605
- let errorDetails = `Named request "${targetName}" failed: ${error.message}`;
2606
- if (requestUrl) {
2607
- errorDetails += `\n Request: ${requestMethod} ${requestUrl}`;
2608
- }
2609
- errors.push(errorDetails);
2610
- return {
2611
- name: '',
2612
- success: false,
2613
- responses,
2614
- scriptResults,
2615
- assertionResults,
2616
- steps: orderedSteps,
2617
- errors,
2618
- duration: Date.now() - startTime
2619
- };
2620
- }
2621
- }
2622
- else if (step.type === 'varRunSequence') {
2623
- // var x = run SequenceName or var x = run SequenceName(args) - capture sequence result into a variable
2624
- const parsed = parseVarRunSequenceCommand(step.content);
2625
- if (!parsed) {
2626
- errors.push(`Invalid var run sequence command: ${step.content}`);
2627
- return {
2628
- name: '',
2629
- success: false,
2630
- responses,
2631
- scriptResults,
2632
- assertionResults,
2633
- steps: orderedSteps,
2634
- errors,
2635
- duration: Date.now() - startTime
2636
- };
2637
- }
2638
- const { varName, sequenceName, args: runArgs } = parsed;
2639
- if (!fullDocumentText) {
2640
- errors.push(`Cannot run sequence "${sequenceName}": full document text not provided`);
2641
- return {
2642
- name: '',
2643
- success: false,
2644
- responses,
2645
- scriptResults,
2646
- assertionResults,
2647
- steps: orderedSteps,
2648
- errors,
2649
- duration: Date.now() - startTime
2650
- };
2651
- }
2652
- // Find the target sequence
2653
- const allSequences = extractSequences(fullDocumentText);
2654
- const targetSequence = allSequences.find(s => s.name === sequenceName);
2655
- if (!targetSequence) {
2656
- // Not a sequence - check for named request
2657
- const namedRequest = (0, parser_1.getNamedRequest)(fullDocumentText, sequenceName);
2658
- if (!namedRequest) {
2659
- errors.push(`"${sequenceName}" is not a named request or sequence`);
2660
- return {
2661
- name: '',
2662
- success: false,
2663
- responses,
2664
- scriptResults,
2665
- assertionResults,
2666
- steps: orderedSteps,
2667
- errors,
2668
- duration: Date.now() - startTime
2669
- };
2670
- }
2671
- // Execute the named request and capture response to variable
2672
- requestIndex++; // 1-based for user reference
2673
- let requestUrl = '';
2674
- let requestMethod = '';
2675
- try {
2676
- // Parse and send the named request with current variables
2677
- const requestParsed = (0, parser_1.parserHttpRequest)(namedRequest.content, runtimeVariables);
2678
- requestUrl = requestParsed.url;
2679
- requestMethod = requestParsed.method;
2680
- // Build retry options - prefer options from the command, fallback to options in request content
2681
- const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
2682
- const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1000;
2683
- const retryOpts = effectiveRetryCount
2684
- ? {
2685
- retryCount: effectiveRetryCount,
2686
- backoffMs: effectiveBackoffMs,
2687
- onRetry: (attempt, total, error, waitMs) => {
2688
- reportProgress(stepIdx, 'request', `[Retry ${attempt}/${total}] var ${varName} = run ${sequenceName} - waiting ${waitMs}ms`, undefined);
2689
- }
2690
- }
2691
- : undefined;
2692
- const response = cookieJar
2693
- ? await (0, httpClient_1.sendRequestWithJar)(requestParsed, cookieJar, retryOpts)
2694
- : await (0, httpClient_1.sendRequest)(requestParsed, retryOpts);
2695
- addResponse(response);
2696
- const stepResult = {
2697
- type: 'request',
2698
- stepIndex: stepIdx,
2699
- response: response,
2700
- requestMethod: requestParsed.method,
2701
- requestUrl: requestParsed.url
2702
- };
2703
- orderedSteps.push(stepResult);
2704
- reportProgress(stepIdx, 'namedRequest', `var ${varName} = run ${sequenceName} → ${requestParsed.method} ${requestParsed.url}`, stepResult);
2705
- // Store the response in the variable as a JSON object
2706
- // Include status, body, and headers for full access
2707
- const capturedResponse = {
2708
- status: response.status,
2709
- statusText: response.statusText,
2710
- body: response.body,
2711
- headers: response.headers
2712
- };
2713
- runtimeVariables[varName] = JSON.stringify(capturedResponse);
2714
- // Process captures that reference this response
2715
- const relevantCaptures = captures.filter(c => c.afterRequest === requestIndex);
2716
- for (const capture of relevantCaptures) {
2717
- const value = getValueByPath(response, capture.path);
2718
- if (value !== undefined) {
2719
- runtimeVariables[capture.varName] = value;
2720
- }
2721
- else {
2722
- errors.push(`Warning: Could not capture ${capture.varName} from $${requestIndex}.${capture.path}`);
2723
- }
2724
- }
2725
- }
2726
- catch (error) {
2727
- let errorDetails = `Named request "${sequenceName}" failed: ${error.message}`;
2728
- if (requestUrl) {
2729
- errorDetails += `\n Request: ${requestMethod} ${requestUrl}`;
2730
- }
2731
- errors.push(errorDetails);
2732
- return {
2733
- name: '',
2734
- success: false,
2735
- responses,
2736
- scriptResults,
2737
- assertionResults,
2738
- steps: orderedSteps,
2739
- errors,
2740
- duration: Date.now() - startTime
2741
- };
2742
- }
2743
- continue;
2744
- }
2745
- // Initialize call stack if not provided
2746
- const currentCallStack = callStack || [];
2747
- // Check for circular reference
2748
- if (currentCallStack.includes(sequenceName)) {
2749
- errors.push(`Circular reference detected: ${[...currentCallStack, sequenceName].join(' → ')}`);
2750
- return {
2751
- name: '',
2752
- success: false,
2753
- responses,
2754
- scriptResults,
2755
- assertionResults,
2756
- steps: orderedSteps,
2757
- errors,
2758
- duration: Date.now() - startTime
2759
- };
2760
- }
2761
- // Bind arguments to parameters
2762
- let sequenceArgs = {};
2763
- if (targetSequence.parameters.length > 0 || runArgs.length > 0) {
2764
- const bindResult = bindSequenceArguments(targetSequence.parameters, runArgs, runtimeVariables);
2765
- if ('error' in bindResult) {
2766
- errors.push(`Error calling sequence "${sequenceName}": ${bindResult.error}`);
2767
- return {
2768
- name: '',
2769
- success: false,
2770
- responses,
2771
- scriptResults,
2772
- assertionResults,
2773
- steps: orderedSteps,
2774
- errors,
2775
- duration: Date.now() - startTime
2776
- };
2777
- }
2778
- sequenceArgs = bindResult.variables;
2779
- }
2780
- // Check if tag filtering is active and if the target sequence matches
2781
- if (tagFilterOptions && tagFilterOptions.filters.length > 0) {
2782
- if (!sequenceMatchesTags(targetSequence, tagFilterOptions)) {
2783
- // Silently skip this sequence - it doesn't match the tag filter
2784
- // For var x = run Sequence, the variable will not be set
2785
- continue;
2786
- }
2787
- }
2788
- // Report that we're starting a sub-sequence
2789
- reportProgress(stepIdx, 'sequenceStart', `Starting sequence ${sequenceName}`, undefined, sequenceName);
2790
- // Determine the working directory for this sub-sequence
2791
- // If we have source info for this sequence (from imports), use that file's directory
2792
- let subWorkingDir = workingDir;
2793
- if (sequenceSources) {
2794
- const sourceFile = sequenceSources.get(sequenceName.toLowerCase());
2795
- if (sourceFile) {
2796
- const path = await import('path');
2797
- subWorkingDir = path.dirname(sourceFile);
2798
- }
2799
- }
2800
- // Run the sub-sequence with current runtime variables
2801
- const subResult = await runSequenceWithJar(targetSequence.content, runtimeVariables, cookieJar, subWorkingDir, fullDocumentText, onProgress, [...currentCallStack, sequenceName], sequenceArgs, // Pass bound arguments
2802
- apiDefinitions, // Pass API definitions for endpoint resolution
2803
- tagFilterOptions, // Pass tag filter options for nested sequence filtering
2804
- sequenceSources // Pass sequence sources for nested sequences
2805
- );
2806
- // Report that sub-sequence ended
2807
- reportProgress(stepIdx, 'sequenceEnd', `Finished sequence ${sequenceName}`, undefined, sequenceName);
2808
- // Merge results from sub-sequence and update $N references
2809
- for (const subResponse of subResult.responses) {
2810
- addResponse(subResponse);
2811
- }
2812
- scriptResults.push(...subResult.scriptResults);
2813
- assertionResults.push(...subResult.assertionResults);
2814
- // Merge step results
2815
- for (const subStep of subResult.steps) {
2816
- orderedSteps.push({
2817
- ...subStep,
2818
- stepIndex: orderedSteps.length
2819
- });
2820
- }
2821
- // Extract return expressions from the sequence
2822
- const returnExpressions = extractReturnVariables(targetSequence.content);
2823
- if (returnExpressions && returnExpressions.length > 0 && subResult.variables) {
2824
- if (returnExpressions.length === 1) {
2825
- // Single return: store the raw value directly
2826
- // return foo.value → returns the value, not {value: ...}
2827
- const resolved = resolveReturnExpression(returnExpressions[0], subResult.variables);
2828
- if (resolved) {
2829
- runtimeVariables[varName] = resolved.value;
2830
- }
2831
- else {
2832
- runtimeVariables[varName] = '';
2833
- }
2834
- }
2835
- else {
2836
- // Multiple returns: create object with named values
2837
- // return a, b → returns {a: valueA, b: valueB}
2838
- const returnedObj = {};
2839
- for (const expr of returnExpressions) {
2840
- const resolved = resolveReturnExpression(expr, subResult.variables);
2841
- if (resolved) {
2842
- returnedObj[resolved.key] = resolved.value;
2843
- }
2844
- }
2845
- runtimeVariables[varName] = JSON.stringify(returnedObj);
2846
- }
2847
- }
2848
- else {
2849
- // No return statement - store empty string
2850
- runtimeVariables[varName] = '';
2851
- }
2852
- // Update request index
2853
- requestIndex += subResult.responses.length;
2854
- // If sub-sequence failed, propagate the failure
2855
- if (!subResult.success) {
2856
- errors.push(`Sequence "${sequenceName}" failed`);
2857
- errors.push(...subResult.errors);
2858
- return {
2859
- name: '',
2860
- success: false,
2861
- responses,
2862
- scriptResults,
2863
- assertionResults,
2864
- steps: orderedSteps,
2865
- errors,
2866
- duration: Date.now() - startTime,
2867
- variables: runtimeVariables
2868
- };
2869
- }
2870
- // Sub-sequence succeeded - continue
2871
- continue;
2872
- }
2873
- else {
2874
- // HTTP request (regular or API endpoint-based)
2875
- requestIndex++; // 1-based for user reference
2876
- let parsed;
2877
- let requestDescription = '';
2878
- try {
2879
- // Check if this is an API request (uses endpoint syntax)
2880
- if (apiDefinitions &&
2881
- apiDefinitions.endpoints.length > 0 &&
2882
- (0, nornapiParser_1.isApiRequestLine)(step.content, apiDefinitions.endpoints)) {
2883
- // Parse API request syntax
2884
- const apiRequest = (0, nornapiParser_1.parseApiRequest)(step.content, apiDefinitions.endpoints, apiDefinitions.headerGroups);
2885
- if (apiRequest) {
2886
- // Get the endpoint
2887
- const endpoint = (0, nornapiParser_1.getEndpoint)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, apiRequest.endpointName);
2888
- if (!endpoint) {
2889
- throw new Error(`Unknown endpoint: ${apiRequest.endpointName}`);
2890
- }
2891
- // Resolve parameters in the path (substitute values, including from variables)
2892
- // First substitute {{variable}} references in the endpoint path (e.g., {{baseUrl}})
2893
- // This must happen BEFORE {param} substitution to avoid conflicts
2894
- const pathWithVars = (0, parser_1.substituteVariables)(endpoint.path, runtimeVariables);
2895
- // Then substitute {param} placeholders with the values passed to the endpoint
2896
- const resolvedParams = {};
2897
- for (const [key, value] of Object.entries(apiRequest.params)) {
2898
- // First resolve bare variable names, then substitute {{var}} patterns
2899
- const bareResolved = resolveBareVariables(value, runtimeVariables);
2900
- resolvedParams[key] = (0, parser_1.substituteVariables)(bareResolved, runtimeVariables);
2901
- }
2902
- // Apply parameter substitutions to the path
2903
- let resolvedPath = pathWithVars;
2904
- for (const paramName of endpoint.parameters) {
2905
- const value = resolvedParams[paramName];
2906
- if (value !== undefined) {
2907
- resolvedPath = resolvedPath.replace(`{${paramName}}`, value);
2908
- }
2909
- }
2910
- // Collect headers from header groups
2911
- const combinedHeaders = {};
2912
- for (const groupName of apiRequest.headerGroupNames) {
2913
- const group = (0, nornapiParser_1.getHeaderGroup)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, groupName);
2914
- if (group) {
2915
- // Resolve variable references in header values
2916
- const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, runtimeVariables);
2917
- Object.assign(combinedHeaders, resolvedHeaders);
2918
- }
2919
- }
2920
- // Add inline headers (these take precedence over header groups)
2921
- for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
2922
- // Resolve variable references in header values
2923
- combinedHeaders[headerName] = (0, parser_1.substituteVariables)(headerValue, runtimeVariables);
2924
- }
2925
- // Resolve variables in the body if present
2926
- let resolvedBody;
2927
- if (apiRequest.body) {
2928
- resolvedBody = (0, parser_1.substituteVariables)(apiRequest.body, runtimeVariables);
2929
- }
2930
- parsed = {
2931
- method: apiRequest.method,
2932
- url: resolvedPath,
2933
- headers: combinedHeaders,
2934
- body: resolvedBody
2935
- };
2936
- requestDescription = `${apiRequest.endpointName}(${Object.values(resolvedParams).join(', ')}) → ${parsed.method} ${parsed.url}`;
2937
- }
2938
- else {
2939
- // Fallback to regular parsing if API parsing fails
2940
- parsed = (0, parser_1.parserHttpRequest)(step.content, runtimeVariables);
2941
- requestDescription = `${parsed.method} ${parsed.url}`;
2942
- // Check for header group names in the step content
2943
- if (apiDefinitions && apiDefinitions.headerGroups.length > 0) {
2944
- parsed = applyHeaderGroupsToRequest(parsed, step.content, apiDefinitions.headerGroups, runtimeVariables);
2945
- }
2946
- }
2947
- }
2948
- else {
2949
- // Regular HTTP request (URL-based)
2950
- parsed = (0, parser_1.parserHttpRequest)(step.content, runtimeVariables);
2951
- requestDescription = `${parsed.method} ${parsed.url}`;
2952
- // Check for header group names in the step content
2953
- if (apiDefinitions && apiDefinitions.headerGroups.length > 0) {
2954
- parsed = applyHeaderGroupsToRequest(parsed, step.content, apiDefinitions.headerGroups, runtimeVariables);
2955
- }
2956
- // Check if body is a bare variable name (no {{}} braces)
2957
- // This allows: POST url Json\npayload (where payload is a variable containing JSON)
2958
- if (parsed.body) {
2959
- const bodyTrimmed = parsed.body.trim();
2960
- const bareVarMatch = bodyTrimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)$/);
2961
- if (bareVarMatch && bareVarMatch[1] in runtimeVariables) {
2962
- const varValue = runtimeVariables[bareVarMatch[1]];
2963
- if (typeof varValue === 'object' && varValue !== null) {
2964
- parsed.body = JSON.stringify(varValue);
2965
- }
2966
- else if (typeof varValue === 'string') {
2967
- parsed.body = varValue;
2968
- }
2969
- else {
2970
- parsed.body = String(varValue);
2971
- }
2972
- }
2973
- }
2974
- }
2975
- // Extract retry options from step content (works for both API and regular requests)
2976
- const { retryCount, backoffMs } = (0, parser_1.extractRetryOptions)(step.content);
2977
- const retryOpts = (retryCount ?? parsed.retryCount)
2978
- ? {
2979
- retryCount: retryCount ?? parsed.retryCount ?? 0,
2980
- backoffMs: backoffMs ?? parsed.backoffMs ?? 1000,
2981
- onRetry: (attempt, total, error, waitMs) => {
2982
- reportProgress(stepIdx, 'request', `[Retry ${attempt}/${total}] ${requestDescription} - waiting ${waitMs}ms`, undefined);
2983
- }
2984
- }
2985
- : undefined;
2986
- // Send the request (with jar if provided, otherwise uses shared jar)
2987
- const response = cookieJar
2988
- ? await (0, httpClient_1.sendRequestWithJar)(parsed, cookieJar, retryOpts)
2989
- : await (0, httpClient_1.sendRequest)(parsed, retryOpts);
2990
- addResponse(response);
2991
- const stepResult = {
2992
- type: 'request',
2993
- stepIndex: stepIdx,
2994
- response: response,
2995
- requestMethod: parsed.method,
2996
- requestUrl: parsed.url
2997
- };
2998
- orderedSteps.push(stepResult);
2999
- reportProgress(stepIdx, 'request', requestDescription, stepResult);
3000
- // Process captures that reference this response ($N where N = requestIndex)
3001
- const relevantCaptures = captures.filter(c => c.afterRequest === requestIndex);
3002
- for (const capture of relevantCaptures) {
3003
- const value = getValueByPath(response, capture.path);
3004
- if (value !== undefined) {
3005
- // Preserve the value as-is (objects, arrays, primitives) for type checking
3006
- runtimeVariables[capture.varName] = value;
3007
- }
3008
- else {
3009
- errors.push(`Warning: Could not capture ${capture.varName} from $${requestIndex}.${capture.path}`);
3010
- }
3011
- }
3012
- }
3013
- catch (error) {
3014
- // Build a more informative error message
3015
- let errorDetails = `Request ${requestIndex} failed: ${error.message}`;
3016
- if (requestDescription) {
3017
- errorDetails += `\n Request: ${requestDescription}`;
3018
- }
3019
- errors.push(errorDetails);
3020
- // Stop sequence on error
3021
- return {
3022
- name: '',
3023
- success: false,
3024
- responses,
3025
- scriptResults,
3026
- assertionResults,
3027
- steps: orderedSteps,
3028
- errors,
3029
- duration: Date.now() - startTime
3030
- };
3031
- }
3032
- }
3033
- }
3034
- return {
3035
- name: '',
3036
- success: errors.filter(e => !e.startsWith('Warning')).length === 0,
3037
- responses,
3038
- scriptResults,
3039
- assertionResults,
3040
- steps: orderedSteps,
3041
- errors,
3042
- duration: Date.now() - startTime,
3043
- variables: runtimeVariables
3044
- };
3045
- }
3046
- //# sourceMappingURL=sequenceRunner.js.map