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.
- package/.claude/settings.local.json +18 -0
- package/.claude/skills/norn-social-campaign/SKILL.md +70 -0
- package/CHANGELOG.md +22 -1
- package/LICENSE +20 -29
- package/README.md +32 -1
- package/demos/nornenv-region-refactor/README.md +64 -0
- package/demos/nornenv-showcase/README.md +62 -0
- package/demos/nornenv-showcase/norn.config.json +16 -0
- package/demos/nornenv-showcase/showcase.norn +70 -0
- package/demos/nornenv-showcase/showcase.nornapi +26 -0
- package/demos/nornenv-showcase/showcase.nornsql +20 -0
- package/dist/cli.js +564 -54
- package/out/apiResponseIntellisenseCache.js +394 -0
- package/out/assertionRunner.js +567 -0
- package/out/cacheDir.js +136 -0
- package/out/chatParticipant.js +763 -0
- package/out/cli/colors.js +127 -0
- package/out/cli/formatters/assertion.js +102 -0
- package/out/cli/formatters/index.js +23 -0
- package/out/cli/formatters/response.js +106 -0
- package/out/cli/formatters/summary.js +246 -0
- package/out/cli/redaction.js +237 -0
- package/out/cli/reporters/html.js +689 -0
- package/out/cli/reporters/index.js +22 -0
- package/out/cli/reporters/junit.js +226 -0
- package/out/codeLensProvider.js +351 -0
- package/out/compareContentProvider.js +85 -0
- package/out/completionProvider.js +3739 -0
- package/out/contractAssertionSummary.js +225 -0
- package/out/contractDecorationProvider.js +243 -0
- package/out/coverageCalculator.js +879 -0
- package/out/coveragePanel.js +597 -0
- package/out/debug/breakpointResolver.js +84 -0
- package/out/debug/breakpoints.js +52 -0
- package/out/debug/nornDebugAdapter.js +166 -0
- package/out/debug/nornDebugSession.js +613 -0
- package/out/debug/sequenceLocationIndex.js +77 -0
- package/out/debug/types.js +3 -0
- package/out/deepClone.js +21 -0
- package/out/diagnosticProvider.js +2554 -0
- package/out/environmentParser.js +736 -0
- package/out/environmentProvider.js +544 -0
- package/out/environmentTemplates.js +146 -0
- package/out/errors/formatError.js +113 -0
- package/out/errors/nornError.js +29 -0
- package/out/formUrlEncoded.js +89 -0
- package/out/httpClient.js +348 -0
- package/out/httpRuntimeOptions.js +16 -0
- package/out/importErrors.js +31 -0
- package/out/inlayHintResolver.js +70 -0
- package/out/jsonFileReader.js +323 -0
- package/out/mcpClient.js +193 -0
- package/out/mcpConfig.js +184 -0
- package/out/mcpToolIntellisenseCache.js +96 -0
- package/out/mcpToolSchema.js +50 -0
- package/out/nornConfig.js +132 -0
- package/out/nornHoverProvider.js +124 -0
- package/out/nornInlayHintsProvider.js +191 -0
- package/out/nornPrompt.js +755 -0
- package/out/nornSqlParser.js +286 -0
- package/out/nornapiHoverProvider.js +135 -0
- package/out/nornapiInlayHintsProvider.js +94 -0
- package/out/nornapiParser.js +324 -0
- package/out/nornenvCodeActionProvider.js +101 -0
- package/out/nornenvDecorationProvider.js +239 -0
- package/out/nornenvFoldingProvider.js +63 -0
- package/out/nornenvHoverProvider.js +114 -0
- package/out/nornenvInlayHintsProvider.js +99 -0
- package/out/nornenvLanguageModel.js +187 -0
- package/out/nornenvRegionRefactor.js +267 -0
- package/out/nornsqlHoverProvider.js +95 -0
- package/out/nornsqlInlayHintsProvider.js +114 -0
- package/out/parser.js +839 -0
- package/out/pathAccess.js +28 -0
- package/out/postmanImportPanel.js +732 -0
- package/out/postmanImportPlanner.js +1155 -0
- package/out/postmanImportSidebarView.js +532 -0
- package/out/quotedString.js +35 -0
- package/out/requestPreparation.js +179 -0
- package/out/requestValidation.js +146 -0
- package/out/responsePanel.js +7754 -0
- package/out/schemaGenerator.js +562 -0
- package/out/scriptRunner.js +419 -0
- package/out/secrets/cliSecrets.js +415 -0
- package/out/secrets/crypto.js +105 -0
- package/out/secrets/envFileSecrets.js +177 -0
- package/out/secrets/keyStore.js +259 -0
- package/out/sequenceDeclaration.js +15 -0
- package/out/sequenceRunner.js +3590 -0
- package/out/sqlAdapterRunner.js +122 -0
- package/out/sqlBuiltInAdapters.js +604 -0
- package/out/sqlConfig.js +184 -0
- package/out/starterCatalog.js +554 -0
- package/out/stringUtils.js +25 -0
- package/out/swaggerBodyIntellisenseCache.js +114 -0
- package/out/swaggerParser.js +464 -0
- package/out/testProvider.js +767 -0
- package/out/theoryCaseLoader.js +113 -0
- package/out/validationCache.js +211 -0
- package/package.json +38 -11
- package/.kanbn/index.md +0 -31
- package/.kanbn/tasks/book-first-mentor-session.md +0 -13
- package/.kanbn/tasks/decide-what-success-in-a-pilot-looks-like.md +0 -9
- package/.kanbn/tasks/do-5-customer-conversations.md +0 -9
- package/.kanbn/tasks/finalise-the-one-line-pitch.md +0 -11
- package/.kanbn/tasks/interview-script.md +0 -49
- package/.kanbn/tasks/make-a-list-of-10-people-to-speak-to.md +0 -11
- package/.kanbn/tasks/prepare-your-customer-interview-questions.md +0 -11
- package/.kanbn/tasks/recruit-2/342/200/2233-pilot-users.md +0 -9
- package/.kanbn/tasks/refine-your-pitch.md +0 -9
- package/.kanbn/tasks/use-the-shiplight-website-as-a-template-to-improve-norn-website.md +0 -9
- package/.kanbn/tasks/write-down-repeated-wording.md +0 -9
- 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
|