norn-cli 1.6.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +9 -1
  2. package/CHANGELOG.md +12 -0
  3. package/dist/cli.js +161 -59
  4. package/package.json +1 -1
  5. package/out/assertionRunner.js +0 -537
  6. package/out/chatParticipant.js +0 -722
  7. package/out/cli/colors.js +0 -129
  8. package/out/cli/formatters/assertion.js +0 -75
  9. package/out/cli/formatters/index.js +0 -23
  10. package/out/cli/formatters/response.js +0 -106
  11. package/out/cli/formatters/summary.js +0 -187
  12. package/out/cli/redaction.js +0 -237
  13. package/out/cli/reporters/html.js +0 -634
  14. package/out/cli/reporters/index.js +0 -22
  15. package/out/cli/reporters/junit.js +0 -211
  16. package/out/cli.js +0 -989
  17. package/out/codeLensProvider.js +0 -248
  18. package/out/compareContentProvider.js +0 -85
  19. package/out/completionProvider.js +0 -2404
  20. package/out/contractDecorationProvider.js +0 -243
  21. package/out/coverageCalculator.js +0 -837
  22. package/out/coveragePanel.js +0 -545
  23. package/out/diagnosticProvider.js +0 -1113
  24. package/out/environmentProvider.js +0 -442
  25. package/out/extension.js +0 -1114
  26. package/out/httpClient.js +0 -269
  27. package/out/jsonFileReader.js +0 -320
  28. package/out/nornPrompt.js +0 -580
  29. package/out/nornapiParser.js +0 -326
  30. package/out/parser.js +0 -725
  31. package/out/responsePanel.js +0 -4674
  32. package/out/schemaGenerator.js +0 -393
  33. package/out/scriptRunner.js +0 -419
  34. package/out/sequenceRunner.js +0 -3046
  35. package/out/swaggerBodyIntellisenseCache.js +0 -147
  36. package/out/swaggerParser.js +0 -419
  37. package/out/test/coverageCalculator.test.js +0 -100
  38. package/out/test/extension.test.js +0 -48
  39. package/out/testProvider.js +0 -658
  40. package/out/validationCache.js +0 -245
@@ -1,537 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isAssertCommand = isAssertCommand;
4
- exports.parseAssertCommand = parseAssertCommand;
5
- exports.resolveValue = resolveValue;
6
- exports.evaluateAssertion = evaluateAssertion;
7
- const schemaGenerator_1 = require("./schemaGenerator");
8
- /**
9
- * Checks if a line is an assert command
10
- */
11
- function isAssertCommand(line) {
12
- return /^assert\s+/i.test(line.trim());
13
- }
14
- /**
15
- * Parses an assert command line.
16
- *
17
- * Supported formats:
18
- * - assert $1.status == 200
19
- * - assert $1.body.name == "John"
20
- * - assert $1.body.email contains "@"
21
- * - assert $1.body.token exists
22
- * - assert $1.status >= 200 | "Should return success status"
23
- *
24
- * @param line The assert command line
25
- * @returns Parsed assertion or null if invalid
26
- */
27
- function parseAssertCommand(line) {
28
- const trimmed = line.trim();
29
- const match = trimmed.match(/^assert\s+(.+)$/i);
30
- if (!match) {
31
- return null;
32
- }
33
- let content = match[1].trim();
34
- let message;
35
- // Check for optional message after |
36
- const pipeIndex = findUnquotedPipe(content);
37
- if (pipeIndex !== -1) {
38
- message = content.substring(pipeIndex + 1).trim();
39
- // Remove surrounding quotes from message if present
40
- if ((message.startsWith('"') && message.endsWith('"')) ||
41
- (message.startsWith("'") && message.endsWith("'"))) {
42
- message = message.slice(1, -1);
43
- }
44
- content = content.substring(0, pipeIndex).trim();
45
- }
46
- // Try to parse unary operators first (exists, !exists)
47
- const existsMatch = content.match(/^(.+?)\s+(exists|!exists)$/i);
48
- if (existsMatch) {
49
- return {
50
- leftExpr: existsMatch[1].trim(),
51
- operator: existsMatch[2].toLowerCase(),
52
- message
53
- };
54
- }
55
- // Parse binary operators
56
- // Order matters: check multi-char operators first
57
- const binaryOperators = [
58
- { pattern: /^(.+?)\s*>=\s*(.+)$/, op: '>=' },
59
- { pattern: /^(.+?)\s*<=\s*(.+)$/, op: '<=' },
60
- { pattern: /^(.+?)\s*==\s*(.+)$/, op: '==' },
61
- { pattern: /^(.+?)\s*!=\s*(.+)$/, op: '!=' },
62
- { pattern: /^(.+?)\s*>\s*(.+)$/, op: '>' },
63
- { pattern: /^(.+?)\s*<\s*(.+)$/, op: '<' },
64
- { pattern: /^(.+?)\s+contains\s+(.+)$/i, op: 'contains' },
65
- { pattern: /^(.+?)\s+startsWith\s+(.+)$/i, op: 'startsWith' },
66
- { pattern: /^(.+?)\s+endsWith\s+(.+)$/i, op: 'endsWith' },
67
- { pattern: /^(.+?)\s+matches\s+(.+)$/i, op: 'matches' },
68
- { pattern: /^(.+?)\s+matchesSchema\s+(.+)$/i, op: 'matchesSchema' },
69
- { pattern: /^(.+?)\s+isType\s+(.+)$/i, op: 'isType' },
70
- ];
71
- for (const { pattern, op } of binaryOperators) {
72
- const binaryMatch = content.match(pattern);
73
- if (binaryMatch) {
74
- return {
75
- leftExpr: binaryMatch[1].trim(),
76
- operator: op,
77
- rightExpr: binaryMatch[2].trim(),
78
- message
79
- };
80
- }
81
- }
82
- return null;
83
- }
84
- /**
85
- * Find the index of an unquoted pipe character
86
- */
87
- function findUnquotedPipe(str) {
88
- let inQuote = false;
89
- let quoteChar = '';
90
- for (let i = 0; i < str.length; i++) {
91
- const char = str[i];
92
- if ((char === '"' || char === "'") && !inQuote) {
93
- inQuote = true;
94
- quoteChar = char;
95
- }
96
- else if (char === quoteChar && inQuote) {
97
- inQuote = false;
98
- quoteChar = '';
99
- }
100
- else if (char === '|' && !inQuote) {
101
- return i;
102
- }
103
- }
104
- return -1;
105
- }
106
- /**
107
- * Resolves a value expression to its actual value.
108
- *
109
- * Supports:
110
- * - $N.path references (e.g., $1.status, $1.body.user.id)
111
- * - String literals ("hello" or 'hello')
112
- * - Number literals (200, 3.14)
113
- * - Boolean literals (true, false)
114
- * - null
115
- * - Variable references {{varName}}
116
- *
117
- * @param expr The expression to resolve
118
- * @param responses Array of responses from the sequence (indexed by $N where N is 1-based)
119
- * @param variables Current runtime variables
120
- * @param getValueByPath Function to extract value from response
121
- * @param responseIndexToVariable Optional mapping from response index to variable name
122
- */
123
- function resolveValue(expr, responses, variables, getValueByPath, responseIndexToVariable) {
124
- const trimmed = expr.trim();
125
- // Check for $N.path reference
126
- const refMatch = trimmed.match(/^\$(\d+)\.(.+)$/);
127
- if (refMatch) {
128
- const responseIdx = parseInt(refMatch[1], 10); // Keep 1-based for tracking
129
- const responseIndex = responseIdx - 1; // Convert to 0-based for array access
130
- const path = refMatch[2];
131
- if (responseIndex < 0 || responseIndex >= responses.length) {
132
- return {
133
- value: undefined,
134
- error: `Response $${refMatch[1]} does not exist (only ${responses.length} responses so far)`
135
- };
136
- }
137
- const response = responses[responseIndex];
138
- const value = getValueByPath(response, path);
139
- return {
140
- value,
141
- responseIndex: responseIdx,
142
- response,
143
- jsonPath: path,
144
- variableName: responseIndexToVariable?.get(responseIdx)
145
- };
146
- }
147
- // Check for $N without path (invalid in new syntax)
148
- if (/^\$\d+$/.test(trimmed)) {
149
- return {
150
- value: undefined,
151
- error: `Invalid reference: ${trimmed}. Use $N.body for full body or $N.status, $N.headers, etc.`
152
- };
153
- }
154
- // Check for variable.path reference (e.g., user.body.username where user is a captured response)
155
- const varPathMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])+)$/);
156
- if (varPathMatch) {
157
- const varName = varPathMatch[1];
158
- const pathPart = varPathMatch[2];
159
- if (varName in variables) {
160
- const varValue = variables[varName];
161
- // If the variable is an object (like HttpResponse), navigate the path
162
- if (typeof varValue === 'object' && varValue !== null) {
163
- const path = pathPart.replace(/^\./, ''); // Remove leading dot
164
- const value = getNestedValue(varValue, path);
165
- // Check if this looks like an HttpResponse (has status and body)
166
- const isHttpResponse = 'status' in varValue && 'body' in varValue;
167
- return {
168
- value,
169
- variableName: varName,
170
- jsonPath: path,
171
- response: isHttpResponse ? varValue : undefined
172
- };
173
- }
174
- // If it's a string, try to parse as JSON
175
- if (typeof varValue === 'string') {
176
- try {
177
- const parsed = JSON.parse(varValue);
178
- const path = pathPart.replace(/^\./, '');
179
- const value = getNestedValue(parsed, path);
180
- return { value, variableName: varName, jsonPath: path };
181
- }
182
- catch {
183
- return { value: undefined, error: `Cannot access path on non-object variable: ${varName}` };
184
- }
185
- }
186
- }
187
- return { value: undefined, error: `Variable '${varName}' is not defined` };
188
- }
189
- // Check for simple variable reference (no path) - e.g., just "user" or "token"
190
- const simpleVarMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)$/);
191
- if (simpleVarMatch) {
192
- const varName = simpleVarMatch[1];
193
- // Don't treat type keywords as variables
194
- if (!['true', 'false', 'null', 'string', 'number', 'boolean', 'object', 'array', 'undefined'].includes(varName.toLowerCase())) {
195
- if (varName in variables) {
196
- const varValue = variables[varName];
197
- // Try to parse numbers
198
- if (typeof varValue === 'string' && /^-?\d+(\.\d+)?$/.test(varValue)) {
199
- return { value: parseFloat(varValue) };
200
- }
201
- return { value: varValue };
202
- }
203
- // Variable not found - but could also be a type name for isType, so don't error here
204
- }
205
- }
206
- // Check for variable reference {{varName}}
207
- const varMatch = trimmed.match(/^\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}$/);
208
- if (varMatch) {
209
- const varName = varMatch[1];
210
- if (varName in variables) {
211
- // Try to parse as number if it looks like one
212
- const varValue = variables[varName];
213
- if (typeof varValue === 'string' && /^-?\d+(\.\d+)?$/.test(varValue)) {
214
- return { value: parseFloat(varValue) };
215
- }
216
- return { value: varValue };
217
- }
218
- return { value: undefined, error: `Variable {{${varName}}} is not defined` };
219
- }
220
- // Check for string literal
221
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
222
- (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
223
- return { value: trimmed.slice(1, -1) };
224
- }
225
- // Check for number literal
226
- if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
227
- return { value: parseFloat(trimmed) };
228
- }
229
- // Check for boolean literals
230
- if (trimmed.toLowerCase() === 'true') {
231
- return { value: true };
232
- }
233
- if (trimmed.toLowerCase() === 'false') {
234
- return { value: false };
235
- }
236
- // Check for null
237
- if (trimmed.toLowerCase() === 'null') {
238
- return { value: null };
239
- }
240
- // Check for empty array literal
241
- if (trimmed === '[]') {
242
- return { value: [] };
243
- }
244
- // Check for empty object literal
245
- if (trimmed === '{}') {
246
- return { value: {} };
247
- }
248
- // Unknown expression type
249
- return { value: undefined, error: `Cannot resolve expression: ${trimmed}` };
250
- }
251
- /**
252
- * Helper function to get nested value from an object using dot notation path
253
- */
254
- function getNestedValue(obj, path) {
255
- if (!path || obj === null || obj === undefined) {
256
- return obj;
257
- }
258
- // Convert [0] to .0 and split by dots
259
- const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(p => p !== '');
260
- let current = obj;
261
- for (const part of parts) {
262
- if (current === null || current === undefined) {
263
- return undefined;
264
- }
265
- current = current[part];
266
- }
267
- return current;
268
- }
269
- function areValuesEqual(leftValue, rightValue) {
270
- if (leftValue === rightValue) {
271
- return true;
272
- }
273
- if (leftValue === null || rightValue === null || leftValue === undefined || rightValue === undefined) {
274
- return leftValue === rightValue;
275
- }
276
- const leftIsArray = Array.isArray(leftValue);
277
- const rightIsArray = Array.isArray(rightValue);
278
- if (leftIsArray || rightIsArray) {
279
- if (!leftIsArray || !rightIsArray) {
280
- return false;
281
- }
282
- if (leftValue.length !== rightValue.length) {
283
- return false;
284
- }
285
- for (let i = 0; i < leftValue.length; i++) {
286
- if (!areValuesEqual(leftValue[i], rightValue[i])) {
287
- return false;
288
- }
289
- }
290
- return true;
291
- }
292
- const leftIsObject = typeof leftValue === 'object';
293
- const rightIsObject = typeof rightValue === 'object';
294
- if (leftIsObject || rightIsObject) {
295
- if (!leftIsObject || !rightIsObject) {
296
- return false;
297
- }
298
- const leftKeys = Object.keys(leftValue);
299
- const rightKeys = Object.keys(rightValue);
300
- if (leftKeys.length !== rightKeys.length) {
301
- return false;
302
- }
303
- for (const key of leftKeys) {
304
- if (!Object.prototype.hasOwnProperty.call(rightValue, key)) {
305
- return false;
306
- }
307
- if (!areValuesEqual(leftValue[key], rightValue[key])) {
308
- return false;
309
- }
310
- }
311
- return true;
312
- }
313
- return leftValue == rightValue;
314
- }
315
- /**
316
- * Evaluates an assertion.
317
- *
318
- * @param assertion Parsed assertion
319
- * @param responses Array of responses from the sequence
320
- * @param variables Current runtime variables (can contain objects like HttpResponse)
321
- * @param getValueByPath Function to extract value from response
322
- * @param responseIndexToVariable Optional mapping from response index (1-based) to variable name
323
- * @param basePath Optional base path for resolving relative schema file paths
324
- */
325
- function evaluateAssertion(assertion, responses, variables, getValueByPath, responseIndexToVariable, basePath) {
326
- const leftResult = resolveValue(assertion.leftExpr, responses, variables, getValueByPath, responseIndexToVariable);
327
- // Helper to build failure context from left result
328
- const buildFailureContext = () => ({
329
- responseIndex: leftResult.responseIndex,
330
- friendlyName: leftResult.variableName,
331
- relatedResponse: leftResult.response,
332
- jsonPath: leftResult.jsonPath
333
- });
334
- // Handle resolution errors
335
- if (leftResult.error) {
336
- return {
337
- passed: false,
338
- expression: formatExpression(assertion),
339
- message: assertion.message,
340
- operator: assertion.operator,
341
- leftValue: undefined,
342
- leftExpression: assertion.leftExpr,
343
- rightExpression: assertion.rightExpr,
344
- error: leftResult.error,
345
- ...buildFailureContext()
346
- };
347
- }
348
- const leftValue = leftResult.value;
349
- // Handle unary operators
350
- if (assertion.operator === 'exists') {
351
- const passed = leftValue !== undefined && leftValue !== null;
352
- return {
353
- passed,
354
- expression: formatExpression(assertion),
355
- message: assertion.message,
356
- operator: assertion.operator,
357
- leftValue,
358
- leftExpression: assertion.leftExpr,
359
- ...(!passed ? buildFailureContext() : {})
360
- };
361
- }
362
- if (assertion.operator === '!exists') {
363
- const passed = leftValue === undefined || leftValue === null;
364
- return {
365
- passed,
366
- expression: formatExpression(assertion),
367
- message: assertion.message,
368
- operator: assertion.operator,
369
- leftValue,
370
- leftExpression: assertion.leftExpr,
371
- ...(!passed ? buildFailureContext() : {})
372
- };
373
- }
374
- // Binary operators require right expression
375
- if (!assertion.rightExpr) {
376
- return {
377
- passed: false,
378
- expression: formatExpression(assertion),
379
- message: assertion.message,
380
- operator: assertion.operator,
381
- leftValue,
382
- leftExpression: assertion.leftExpr,
383
- error: `Operator ${assertion.operator} requires a right-hand value`,
384
- ...buildFailureContext()
385
- };
386
- }
387
- // For isType operator, the right side is a literal type name, not a value to resolve
388
- if (assertion.operator === 'isType') {
389
- const expectedType = assertion.rightExpr.toLowerCase();
390
- let actualType;
391
- if (leftValue === null) {
392
- actualType = 'null';
393
- }
394
- else if (Array.isArray(leftValue)) {
395
- actualType = 'array';
396
- }
397
- else {
398
- actualType = typeof leftValue;
399
- }
400
- const passed = actualType === expectedType;
401
- return {
402
- passed,
403
- expression: formatExpression(assertion),
404
- message: assertion.message,
405
- operator: assertion.operator,
406
- leftValue,
407
- rightValue: expectedType,
408
- leftExpression: assertion.leftExpr,
409
- rightExpression: assertion.rightExpr,
410
- ...(!passed ? buildFailureContext() : {})
411
- };
412
- }
413
- // For matchesSchema operator, validate against a JSON Schema file
414
- if (assertion.operator === 'matchesSchema') {
415
- // Right side is a file path (with or without quotes)
416
- let schemaPath = assertion.rightExpr;
417
- // Remove surrounding quotes if present
418
- if ((schemaPath.startsWith('"') && schemaPath.endsWith('"')) ||
419
- (schemaPath.startsWith("'") && schemaPath.endsWith("'"))) {
420
- schemaPath = schemaPath.slice(1, -1);
421
- }
422
- const validationResult = (0, schemaGenerator_1.validateAgainstSchemaDetailed)(leftValue, schemaPath, basePath);
423
- const passed = validationResult.valid;
424
- return {
425
- passed,
426
- expression: formatExpression(assertion),
427
- message: assertion.message,
428
- operator: assertion.operator,
429
- leftValue,
430
- rightValue: schemaPath,
431
- leftExpression: assertion.leftExpr,
432
- rightExpression: assertion.rightExpr,
433
- error: !passed && validationResult.errorStrings ? validationResult.errorStrings.join('; ') : undefined,
434
- schemaErrors: validationResult.errors,
435
- schemaPath: validationResult.resolvedSchemaPath,
436
- schema: validationResult.schema,
437
- ...(!passed ? buildFailureContext() : {})
438
- };
439
- }
440
- const rightResult = resolveValue(assertion.rightExpr, responses, variables, getValueByPath, responseIndexToVariable);
441
- if (rightResult.error) {
442
- return {
443
- passed: false,
444
- expression: formatExpression(assertion),
445
- message: assertion.message,
446
- operator: assertion.operator,
447
- leftValue,
448
- rightValue: undefined,
449
- leftExpression: assertion.leftExpr,
450
- rightExpression: assertion.rightExpr,
451
- error: rightResult.error,
452
- ...buildFailureContext()
453
- };
454
- }
455
- const rightValue = rightResult.value;
456
- let passed = false;
457
- switch (assertion.operator) {
458
- case '==':
459
- passed = areValuesEqual(leftValue, rightValue);
460
- break;
461
- case '!=':
462
- passed = !areValuesEqual(leftValue, rightValue);
463
- break;
464
- case '>':
465
- passed = Number(leftValue) > Number(rightValue);
466
- break;
467
- case '>=':
468
- passed = Number(leftValue) >= Number(rightValue);
469
- break;
470
- case '<':
471
- passed = Number(leftValue) < Number(rightValue);
472
- break;
473
- case '<=':
474
- passed = Number(leftValue) <= Number(rightValue);
475
- break;
476
- case 'contains':
477
- passed = String(leftValue).includes(String(rightValue));
478
- break;
479
- case 'startsWith':
480
- passed = String(leftValue).startsWith(String(rightValue));
481
- break;
482
- case 'endsWith':
483
- passed = String(leftValue).endsWith(String(rightValue));
484
- break;
485
- case 'matches':
486
- try {
487
- // Remove regex delimiters if present
488
- let pattern = String(rightValue);
489
- if (pattern.startsWith('/') && pattern.lastIndexOf('/') > 0) {
490
- const lastSlash = pattern.lastIndexOf('/');
491
- const flags = pattern.substring(lastSlash + 1);
492
- pattern = pattern.substring(1, lastSlash);
493
- passed = new RegExp(pattern, flags).test(String(leftValue));
494
- }
495
- else {
496
- passed = new RegExp(pattern).test(String(leftValue));
497
- }
498
- }
499
- catch (e) {
500
- return {
501
- passed: false,
502
- expression: formatExpression(assertion),
503
- message: assertion.message,
504
- operator: assertion.operator,
505
- leftValue,
506
- rightValue,
507
- leftExpression: assertion.leftExpr,
508
- rightExpression: assertion.rightExpr,
509
- error: `Invalid regex pattern: ${rightValue}`,
510
- ...buildFailureContext()
511
- };
512
- }
513
- break;
514
- // Note: isType is handled earlier before resolveValue is called
515
- }
516
- return {
517
- passed,
518
- expression: formatExpression(assertion),
519
- message: assertion.message,
520
- operator: assertion.operator,
521
- leftValue,
522
- rightValue,
523
- leftExpression: assertion.leftExpr,
524
- rightExpression: assertion.rightExpr,
525
- ...(!passed ? buildFailureContext() : {})
526
- };
527
- }
528
- /**
529
- * Format the assertion expression for display
530
- */
531
- function formatExpression(assertion) {
532
- if (assertion.rightExpr) {
533
- return `${assertion.leftExpr} ${assertion.operator} ${assertion.rightExpr}`;
534
- }
535
- return `${assertion.leftExpr} ${assertion.operator}`;
536
- }
537
- //# sourceMappingURL=assertionRunner.js.map