norn-cli 2.3.0 → 2.4.0

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