norn-cli 2.2.2 → 2.4.0

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