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,879 @@
1
+ "use strict";
2
+ /**
3
+ * Coverage Calculator for Swagger/OpenAPI specs
4
+ *
5
+ * Calculates what percentage of swagger-defined endpoints and their
6
+ * response codes are covered by test sequences.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.calculateWorkspaceCoverage = calculateWorkspaceCoverage;
43
+ exports.getCoverage = getCoverage;
44
+ exports.getCoverageForNornapiFile = getCoverageForNornapiFile;
45
+ exports.refreshCoverage = refreshCoverage;
46
+ exports.refreshCoverageForNornapiFile = refreshCoverageForNornapiFile;
47
+ exports.recalculateCoverageAfterExecution = recalculateCoverageAfterExecution;
48
+ exports.clearCoverageCache = clearCoverageCache;
49
+ exports.onCoverageUpdate = onCoverageUpdate;
50
+ const vscode = __importStar(require("vscode"));
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const swaggerParser_1 = require("./swaggerParser");
54
+ const nornapiParser_1 = require("./nornapiParser");
55
+ /**
56
+ * Extract the path portion from a full URL and normalize it for swagger matching.
57
+ * e.g., "https://petstore.swagger.io/v2/store/inventory" -> "/store/inventory"
58
+ *
59
+ * We need to strip the base URL (which typically includes version like /v2) to match swagger paths.
60
+ */
61
+ function extractSwaggerPath(fullUrl, swaggerBaseUrl) {
62
+ const normalizeWithBasePath = (rawPath) => {
63
+ let normalizedPath = rawPath || '/';
64
+ try {
65
+ normalizedPath = decodeURIComponent(normalizedPath);
66
+ }
67
+ catch {
68
+ // Keep original path if decoding fails
69
+ }
70
+ if (!normalizedPath.startsWith('/')) {
71
+ normalizedPath = `/${normalizedPath}`;
72
+ }
73
+ if (swaggerBaseUrl) {
74
+ try {
75
+ const baseUrl = new URL(swaggerBaseUrl);
76
+ const basePath = decodeURIComponent(baseUrl.pathname);
77
+ if (basePath && basePath !== '/' && normalizedPath.startsWith(basePath)) {
78
+ normalizedPath = normalizedPath.substring(basePath.length);
79
+ if (!normalizedPath.startsWith('/')) {
80
+ normalizedPath = '/' + normalizedPath;
81
+ }
82
+ }
83
+ }
84
+ catch {
85
+ // Ignore base URL parsing errors
86
+ }
87
+ }
88
+ return normalizedPath || '/';
89
+ };
90
+ const normalizeTemplatePath = (rawPath) => {
91
+ let candidate = rawPath.trim();
92
+ if (!candidate.startsWith('/')) {
93
+ candidate = `/${candidate}`;
94
+ }
95
+ // Remove one or more leading template segments such as
96
+ // /{{baseUrl}}/{{version}}/pet/{petId} -> /pet/{petId}
97
+ candidate = candidate.replace(/^(?:\/\{\{[^}]+\}\})+/, '');
98
+ if (!candidate.startsWith('/')) {
99
+ candidate = `/${candidate}`;
100
+ }
101
+ return normalizeWithBasePath(candidate);
102
+ };
103
+ try {
104
+ const trimmed = fullUrl.trim();
105
+ // Handle template variables such as {{baseUrl}}/pet/{petId}
106
+ const variablePrefixMatch = trimmed.match(/^\{\{[^}]+\}\}(.*)$/);
107
+ if (variablePrefixMatch) {
108
+ const remainder = variablePrefixMatch[1] || '/';
109
+ return normalizeTemplatePath(remainder);
110
+ }
111
+ // Handle path-form template segments such as /{{version}}/pet/{petId}
112
+ if (/^\/(?:\{\{[^}]+\}\}\/)+/.test(trimmed) || /^\/\{\{[^}]+\}\}$/.test(trimmed)) {
113
+ return normalizeTemplatePath(trimmed);
114
+ }
115
+ // If it's already just a path (starts with /), return as-is
116
+ if (trimmed.startsWith('/')) {
117
+ return normalizeWithBasePath(trimmed);
118
+ }
119
+ const url = new URL(trimmed);
120
+ return normalizeWithBasePath(url.pathname);
121
+ }
122
+ catch {
123
+ // If URL parsing fails, try to extract path manually
124
+ const match = fullUrl.match(/https?:\/\/[^\/]+(\/.*)/);
125
+ if (match) {
126
+ return normalizeWithBasePath(match[1]);
127
+ }
128
+ return normalizeWithBasePath(fullUrl);
129
+ }
130
+ }
131
+ /**
132
+ * Extract swagger URL from .nornapi file content
133
+ */
134
+ function extractSwaggerUrl(content) {
135
+ const match = content.match(/^swagger\s+["']?(https?:\/\/[^\s"']+)["']?\s*$/m);
136
+ return match?.[1];
137
+ }
138
+ /**
139
+ * Parse assert status lines from sequence content.
140
+ * Returns array of status codes/patterns (e.g., ["200", "2xx", "404"])
141
+ */
142
+ function extractAssertedStatusCodes(sequenceContent) {
143
+ const codes = [];
144
+ const lines = sequenceContent.split('\n');
145
+ for (const line of lines) {
146
+ const assertion = extractStatusAssertInfo(line);
147
+ if (assertion) {
148
+ codes.push(assertion.code);
149
+ }
150
+ }
151
+ return codes;
152
+ }
153
+ function extractStatusAssertInfo(line) {
154
+ const trimmed = line.trim();
155
+ // Match various assert status patterns:
156
+ // - assert status == 200
157
+ // - assert $1.status == 200
158
+ // - assert response.status == 200
159
+ // - assert inventory.status == 200
160
+ // - assert variableName.status >= 400
161
+ const statusMatch = trimmed.match(/^assert\s+(?:(\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)\.)?status\s*(?:==|>=|<=|>|<|!=)?\s*(\d{3}|[1-5]xx)$/i);
162
+ if (statusMatch) {
163
+ return {
164
+ target: statusMatch[1],
165
+ code: statusMatch[2].toLowerCase()
166
+ };
167
+ }
168
+ // Also match simpler patterns like: assert status 200
169
+ const simpleMatch = trimmed.match(/^assert\s+status\s+(\d{3}|[1-5]xx)$/i);
170
+ if (simpleMatch) {
171
+ return { code: simpleMatch[1].toLowerCase() };
172
+ }
173
+ return null;
174
+ }
175
+ function extractStatusCodeFromAssert(line) {
176
+ return extractStatusAssertInfo(line)?.code ?? null;
177
+ }
178
+ function escapeRegex(value) {
179
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
180
+ }
181
+ function isVariableRedeclaration(line, varName) {
182
+ const trimmed = line.trim();
183
+ const pattern = new RegExp(`^var\\s+${escapeRegex(varName)}\\s*=`, 'i');
184
+ return pattern.test(trimmed);
185
+ }
186
+ function isCoverageScanBoundary(line) {
187
+ return /^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i.test(line) ||
188
+ /^end\s+sequence$/i.test(line) ||
189
+ /^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?run\s+/i.test(line) ||
190
+ /^if\s+/i.test(line) ||
191
+ /^foreach\s+/i.test(line);
192
+ }
193
+ function collectStatusAssertionsFollowingLine(lines, startIndex, assignedVar) {
194
+ const assertedCodes = [];
195
+ for (let j = startIndex + 1; j < lines.length; j++) {
196
+ const nextLine = lines[j].trim();
197
+ if (assignedVar) {
198
+ if (isVariableRedeclaration(nextLine, assignedVar)) {
199
+ break;
200
+ }
201
+ const assertion = extractStatusAssertInfo(nextLine);
202
+ if (assertion?.target?.toLowerCase() === assignedVar.toLowerCase()) {
203
+ assertedCodes.push(assertion.code);
204
+ }
205
+ continue;
206
+ }
207
+ // Stop at next request, sequence end, or other control flow
208
+ if (isCoverageScanBoundary(nextLine)) {
209
+ break;
210
+ }
211
+ const assertion = extractStatusAssertInfo(nextLine);
212
+ if (assertion) {
213
+ assertedCodes.push(assertion.code);
214
+ }
215
+ }
216
+ return assertedCodes;
217
+ }
218
+ function extractReturnedVariable(sequenceContent) {
219
+ let returnedVariable;
220
+ for (const line of sequenceContent.split('\n')) {
221
+ const trimmed = line.trim();
222
+ const returnMatch = trimmed.match(/^return\s+([a-zA-Z_][a-zA-Z0-9_]*)$/i);
223
+ if (returnMatch) {
224
+ returnedVariable = returnMatch[1];
225
+ }
226
+ }
227
+ return returnedVariable;
228
+ }
229
+ /**
230
+ * Check if a wildcard pattern matches a status code
231
+ * e.g., "2xx" matches "200", "201", "204"
232
+ */
233
+ function wildcardMatches(pattern, code) {
234
+ const lowerPattern = pattern.toLowerCase();
235
+ // Exact match
236
+ if (lowerPattern === code) {
237
+ return true;
238
+ }
239
+ // Wildcard match (2xx, 4xx, 5xx)
240
+ if (/^[1-5]xx$/.test(lowerPattern)) {
241
+ const patternPrefix = lowerPattern[0];
242
+ return code.startsWith(patternPrefix);
243
+ }
244
+ return false;
245
+ }
246
+ /**
247
+ * Check if a concrete URL path matches a swagger endpoint path template
248
+ * e.g., "/pet/1" matches "/pet/{petId}"
249
+ * "/v2/pet/123" matches "/pet/{petId}" (with base path /v2)
250
+ */
251
+ function pathMatchesSwaggerEndpoint(urlPath, swaggerPath) {
252
+ // Normalize paths - remove leading/trailing slashes for comparison
253
+ const normalizedUrl = urlPath.replace(/^\/+|\/+$/g, '');
254
+ const normalizedSwagger = swaggerPath.replace(/^\/+|\/+$/g, '');
255
+ const urlParts = normalizedUrl.split('/');
256
+ const swaggerParts = normalizedSwagger.split('/');
257
+ // Try matching from different starting positions to handle base paths
258
+ // e.g., URL /v2/pet/1 should match swagger /pet/{petId} if base is /v2
259
+ for (let offset = 0; offset <= urlParts.length - swaggerParts.length; offset++) {
260
+ let matches = true;
261
+ for (let i = 0; i < swaggerParts.length; i++) {
262
+ const urlPart = urlParts[offset + i];
263
+ const swaggerPart = swaggerParts[i];
264
+ // Path parameter in swagger (e.g., {petId}) matches any value
265
+ if (swaggerPart.startsWith('{') && swaggerPart.endsWith('}')) {
266
+ // Path parameter matches any non-empty value
267
+ if (!urlPart || urlPart.length === 0) {
268
+ matches = false;
269
+ break;
270
+ }
271
+ }
272
+ else if (urlPart !== swaggerPart) {
273
+ // Static segments must match exactly
274
+ matches = false;
275
+ break;
276
+ }
277
+ }
278
+ if (matches) {
279
+ return true;
280
+ }
281
+ }
282
+ return false;
283
+ }
284
+ /**
285
+ * Extract API calls from sequence content.
286
+ * Returns array of { endpointName, assertedCodes, method?, url? }
287
+ *
288
+ * @param sequenceContent - The content of the sequence to analyze
289
+ * @param allSequences - Map of all sequences by name (for following run SequenceName calls)
290
+ * @param allNamedRequests - Map of named request blocks by name (for following run NamedRequest calls)
291
+ * @param visited - Set of already visited sequence names (to prevent infinite recursion)
292
+ */
293
+ function extractApiCalls(sequenceContent, allSequences, allNamedRequests, visited) {
294
+ const calls = [];
295
+ const lines = sequenceContent.split('\n');
296
+ const visitedSet = visited || new Set();
297
+ for (let i = 0; i < lines.length; i++) {
298
+ const line = lines[i].trim();
299
+ // Check for "run Name" or "var x = run Name" calls
300
+ const runMatch = line.match(/^(?:var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*)?run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(.*?\))?$/i);
301
+ if (runMatch) {
302
+ const assignedVar = runMatch[1];
303
+ const targetName = runMatch[2];
304
+ const assertedCodesAfterRun = collectStatusAssertionsFollowingLine(lines, i, assignedVar);
305
+ // Avoid infinite recursion
306
+ if (!visitedSet.has(targetName)) {
307
+ visitedSet.add(targetName);
308
+ // First check if it's a sequence
309
+ if (allSequences) {
310
+ const subSequenceContent = allSequences.get(targetName);
311
+ if (subSequenceContent) {
312
+ // Recursively extract API calls from the sub-sequence
313
+ const subCalls = extractApiCalls(subSequenceContent, allSequences, allNamedRequests, visitedSet);
314
+ // If caller asserts status after "run Sequence", apply those assertions
315
+ // to the response returned by that sequence (fallback: last API call).
316
+ if (assertedCodesAfterRun.length > 0 && subCalls.length > 0) {
317
+ const returnedVar = extractReturnedVariable(subSequenceContent);
318
+ const returnedCall = returnedVar
319
+ ? [...subCalls].reverse().find(call => call.assignedVar === returnedVar) || subCalls[subCalls.length - 1]
320
+ : subCalls[subCalls.length - 1];
321
+ returnedCall.assertedCodes.push(...assertedCodesAfterRun);
322
+ }
323
+ calls.push(...subCalls);
324
+ continue;
325
+ }
326
+ }
327
+ // Then check if it's a named request block
328
+ if (allNamedRequests) {
329
+ const namedRequest = allNamedRequests.get(targetName);
330
+ if (namedRequest) {
331
+ if (namedRequest.endpointName) {
332
+ calls.push({
333
+ endpointName: namedRequest.endpointName,
334
+ assertedCodes: assertedCodesAfterRun,
335
+ assignedVar
336
+ });
337
+ }
338
+ else if (namedRequest.url) {
339
+ // Add this as a URL-based API call (will be matched by URL later)
340
+ calls.push({
341
+ endpointName: `__url__${namedRequest.method}__${namedRequest.url}`,
342
+ assertedCodes: assertedCodesAfterRun,
343
+ method: namedRequest.method,
344
+ url: namedRequest.url,
345
+ assignedVar
346
+ });
347
+ }
348
+ continue;
349
+ }
350
+ }
351
+ }
352
+ continue;
353
+ }
354
+ // Match API request by name with optional variable assignment:
355
+ // - GET EndpointName
356
+ // - GET EndpointName(args)
357
+ // - GET EndpointName(args) HeaderGroup
358
+ // - var result = GET EndpointName
359
+ // - var inventory = GET GetInventory
360
+ // - var order = GET GetOrderById(1)
361
+ const apiCallMatch = line.match(/^(?:var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*\(.*?\))?(?:\s+[a-zA-Z_][a-zA-Z0-9_]*)?$/i);
362
+ if (apiCallMatch) {
363
+ const assignedVar = apiCallMatch[1];
364
+ const endpointName = apiCallMatch[3];
365
+ const assertedCodes = collectStatusAssertionsFollowingLine(lines, i, assignedVar);
366
+ calls.push({ endpointName, assertedCodes, assignedVar });
367
+ }
368
+ }
369
+ return calls;
370
+ }
371
+ /**
372
+ * Extract all test sequences from a .norn file content.
373
+ * Only "test sequence" blocks count towards coverage, not regular "sequence" blocks.
374
+ */
375
+ function extractTestSequences(content) {
376
+ const sequences = [];
377
+ const lines = content.split('\n');
378
+ let currentSequence = null;
379
+ for (let i = 0; i < lines.length; i++) {
380
+ const line = lines[i].trim();
381
+ // Match only "test sequence Name" - regular sequences don't count towards coverage
382
+ const sequenceMatch = line.match(/^test\s+sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?$/);
383
+ if (sequenceMatch) {
384
+ currentSequence = { name: sequenceMatch[1], startLine: i };
385
+ continue;
386
+ }
387
+ // Match sequence end
388
+ if (/^end\s+sequence$/i.test(line) && currentSequence) {
389
+ const sequenceContent = lines.slice(currentSequence.startLine, i + 1).join('\n');
390
+ sequences.push({ name: currentSequence.name, content: sequenceContent });
391
+ currentSequence = null;
392
+ }
393
+ }
394
+ return sequences;
395
+ }
396
+ /**
397
+ * Extract ALL sequences from a .norn file content (both test and regular).
398
+ * Used to build a map for following "run SequenceName" calls.
399
+ */
400
+ function extractAllSequences(content) {
401
+ const sequences = new Map();
402
+ const lines = content.split('\n');
403
+ let currentSequence = null;
404
+ for (let i = 0; i < lines.length; i++) {
405
+ const line = lines[i].trim();
406
+ // Match both "test sequence Name" and "sequence Name"
407
+ const testSequenceMatch = line.match(/^test\s+sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?$/);
408
+ const sequenceMatch = line.match(/^sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?$/);
409
+ if (testSequenceMatch || sequenceMatch) {
410
+ const name = testSequenceMatch ? testSequenceMatch[1] : sequenceMatch[1];
411
+ currentSequence = { name, startLine: i };
412
+ continue;
413
+ }
414
+ // Match sequence end
415
+ if (/^end\s+sequence$/i.test(line) && currentSequence) {
416
+ const sequenceContent = lines.slice(currentSequence.startLine + 1, i).join('\n');
417
+ sequences.set(currentSequence.name, sequenceContent);
418
+ currentSequence = null;
419
+ }
420
+ }
421
+ return sequences;
422
+ }
423
+ /**
424
+ * Extract named request blocks from a .norn file content.
425
+ * Named requests are blocks like [RequestName] followed by HTTP method + URL.
426
+ */
427
+ function extractNamedRequests(content) {
428
+ const requests = new Map();
429
+ const lines = content.split('\n');
430
+ let currentRequest = null;
431
+ for (let i = 0; i < lines.length; i++) {
432
+ const line = lines[i].trim();
433
+ // Match named request start: [RequestName]
434
+ const nameMatch = line.match(/^\[([a-zA-Z_][a-zA-Z0-9_-]*)\]$/);
435
+ if (nameMatch) {
436
+ // Save previous request if exists
437
+ if (currentRequest) {
438
+ const requestContent = lines.slice(currentRequest.startLine + 1, i).join('\n').trim();
439
+ const parsed = parseRequestContent(requestContent);
440
+ if (parsed) {
441
+ requests.set(currentRequest.name, { ...parsed, content: requestContent });
442
+ }
443
+ }
444
+ currentRequest = { name: nameMatch[1], startLine: i };
445
+ continue;
446
+ }
447
+ // Check for sequence start or end - terminates named request block
448
+ if (/^(?:test\s+)?sequence\s+/i.test(line) || /^end\s+sequence$/i.test(line)) {
449
+ if (currentRequest) {
450
+ const requestContent = lines.slice(currentRequest.startLine + 1, i).join('\n').trim();
451
+ const parsed = parseRequestContent(requestContent);
452
+ if (parsed) {
453
+ requests.set(currentRequest.name, { ...parsed, content: requestContent });
454
+ }
455
+ currentRequest = null;
456
+ }
457
+ }
458
+ }
459
+ // Don't forget the last request
460
+ if (currentRequest) {
461
+ const requestContent = lines.slice(currentRequest.startLine + 1).join('\n').trim();
462
+ const parsed = parseRequestContent(requestContent);
463
+ if (parsed) {
464
+ requests.set(currentRequest.name, { ...parsed, content: requestContent });
465
+ }
466
+ }
467
+ return requests;
468
+ }
469
+ /**
470
+ * Parse the content of a named request block to extract either:
471
+ * - a raw URL/path/template target
472
+ * - a .nornapi endpoint name call such as GetPetById(1)
473
+ */
474
+ function parseRequestContent(content) {
475
+ const lines = content.split('\n');
476
+ for (const line of lines) {
477
+ const trimmed = line.trim();
478
+ const match = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([^\s]+)/i);
479
+ if (match) {
480
+ const method = match[1].toUpperCase();
481
+ let target = match[2];
482
+ if ((target.startsWith('"') && target.endsWith('"')) ||
483
+ (target.startsWith("'") && target.endsWith("'"))) {
484
+ target = target.slice(1, -1);
485
+ }
486
+ const endpointMatch = target.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\(.*\))?$/);
487
+ if (endpointMatch) {
488
+ return { method, endpointName: endpointMatch[1] };
489
+ }
490
+ return { method, url: target };
491
+ }
492
+ }
493
+ return null;
494
+ }
495
+ function isPathAtOrBelowRoot(filePath, rootPath) {
496
+ const relative = path.relative(rootPath, filePath);
497
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
498
+ }
499
+ function buildScopedCoverage(nornFiles) {
500
+ const byName = new Map();
501
+ const byUrl = new Map();
502
+ const allSequencesMap = new Map();
503
+ const allNamedRequestsMap = new Map();
504
+ for (const file of nornFiles) {
505
+ try {
506
+ const content = fs.readFileSync(file.fsPath, 'utf-8');
507
+ const fileSequences = extractAllSequences(content);
508
+ for (const [name, seqContent] of fileSequences) {
509
+ allSequencesMap.set(name, seqContent);
510
+ }
511
+ const fileNamedRequests = extractNamedRequests(content);
512
+ for (const [name, reqInfo] of fileNamedRequests) {
513
+ allNamedRequestsMap.set(name, reqInfo);
514
+ }
515
+ }
516
+ catch (error) {
517
+ console.error(`Error reading .norn file ${file.fsPath}:`, error);
518
+ }
519
+ }
520
+ for (const file of nornFiles) {
521
+ try {
522
+ const content = fs.readFileSync(file.fsPath, 'utf-8');
523
+ const testSequences = extractTestSequences(content);
524
+ for (const sequence of testSequences) {
525
+ const apiCalls = extractApiCalls(sequence.content, allSequencesMap, allNamedRequestsMap);
526
+ for (const call of apiCalls) {
527
+ if (call.url && call.method) {
528
+ const key = `${call.method} ${call.url}`;
529
+ if (!byUrl.has(key)) {
530
+ byUrl.set(key, { method: call.method, url: call.url, assertedCodes: new Set() });
531
+ }
532
+ for (const code of call.assertedCodes) {
533
+ byUrl.get(key).assertedCodes.add(code);
534
+ }
535
+ }
536
+ else {
537
+ if (!byName.has(call.endpointName)) {
538
+ byName.set(call.endpointName, new Set());
539
+ }
540
+ for (const code of call.assertedCodes) {
541
+ byName.get(call.endpointName).add(code);
542
+ }
543
+ }
544
+ }
545
+ }
546
+ }
547
+ catch (error) {
548
+ console.error(`Error reading .norn file ${file.fsPath}:`, error);
549
+ }
550
+ }
551
+ return { byName, byUrl };
552
+ }
553
+ /**
554
+ * Calculate coverage for the entire workspace
555
+ * @param useCachedSpecs If true, only fetch swagger specs for new URLs (used after execution)
556
+ */
557
+ async function calculateWorkspaceCoverage(useCachedSpecs = false, scopedNornapiFilePath) {
558
+ const workspaceFolders = vscode.workspace.workspaceFolders;
559
+ if (!workspaceFolders) {
560
+ return {
561
+ total: 0,
562
+ covered: 0,
563
+ percentage: 0,
564
+ specs: [],
565
+ hasSwagger: false
566
+ };
567
+ }
568
+ // Step 1: Find all .nornapi files and extract swagger URLs + endpoint mappings
569
+ const allNornapiFiles = await vscode.workspace.findFiles('**/*.nornapi', '**/node_modules/**');
570
+ const normalizedScopedPath = scopedNornapiFilePath ? path.resolve(scopedNornapiFilePath) : undefined;
571
+ const nornapiFiles = normalizedScopedPath
572
+ ? allNornapiFiles.filter(file => path.resolve(file.fsPath) === normalizedScopedPath)
573
+ : allNornapiFiles;
574
+ const swaggerUrls = new Map(); // swagger URL -> list of nornapi files
575
+ // Store endpoint info temporarily - we'll create mappings after fetching swagger specs
576
+ const pendingEndpoints = [];
577
+ for (const file of nornapiFiles) {
578
+ try {
579
+ const content = fs.readFileSync(file.fsPath, 'utf-8');
580
+ const swaggerUrl = extractSwaggerUrl(content);
581
+ if (swaggerUrl) {
582
+ // Track this nornapi file for this swagger URL
583
+ if (!swaggerUrls.has(swaggerUrl)) {
584
+ swaggerUrls.set(swaggerUrl, []);
585
+ }
586
+ swaggerUrls.get(swaggerUrl).push({ url: swaggerUrl, filePath: file.fsPath });
587
+ // Parse endpoints from this nornapi file
588
+ const nornApiDef = (0, nornapiParser_1.parseNornApiFile)(content);
589
+ for (const endpoint of nornApiDef.endpoints) {
590
+ // Store for later processing after we have swagger specs
591
+ pendingEndpoints.push({
592
+ endpoint: { name: endpoint.name, method: endpoint.method, path: endpoint.path },
593
+ swaggerUrl,
594
+ filePath: file.fsPath
595
+ });
596
+ }
597
+ }
598
+ }
599
+ catch (error) {
600
+ console.error(`Error reading .nornapi file ${file.fsPath}:`, error);
601
+ }
602
+ }
603
+ if (swaggerUrls.size === 0) {
604
+ return {
605
+ total: 0,
606
+ covered: 0,
607
+ percentage: 0,
608
+ specs: [],
609
+ hasSwagger: false
610
+ };
611
+ }
612
+ // Detect new swagger URLs (not seen before)
613
+ const currentUrls = new Set(swaggerUrls.keys());
614
+ const newUrls = new Set();
615
+ for (const url of currentUrls) {
616
+ if (!knownSwaggerUrls.has(url)) {
617
+ newUrls.add(url);
618
+ }
619
+ }
620
+ // Update known URLs
621
+ knownSwaggerUrls = currentUrls;
622
+ // Step 2: Find all .norn files and extract API calls with assert status
623
+ const nornFiles = await vscode.workspace.findFiles('**/*.norn', '**/node_modules/**');
624
+ const scopedCoverageByRoot = new Map();
625
+ const uniqueNornapiRoots = Array.from(new Set(nornapiFiles.map(file => path.dirname(file.fsPath))));
626
+ for (const root of uniqueNornapiRoots) {
627
+ const scopedFiles = nornFiles.filter(file => isPathAtOrBelowRoot(file.fsPath, root));
628
+ scopedCoverageByRoot.set(root, buildScopedCoverage(scopedFiles));
629
+ }
630
+ // Step 3: For each swagger spec, calculate coverage
631
+ // First fetch all specs so we have baseUrl for path normalization
632
+ const swaggerSpecs = new Map();
633
+ for (const [swaggerUrl] of swaggerUrls) {
634
+ try {
635
+ const swaggerSpec = await (0, swaggerParser_1.getCachedSwaggerSpec)(swaggerUrl);
636
+ swaggerSpecs.set(swaggerUrl, swaggerSpec);
637
+ }
638
+ catch (error) {
639
+ console.error(`Error fetching swagger spec ${swaggerUrl}:`, error);
640
+ }
641
+ }
642
+ // Now create endpoint mappings with proper path normalization
643
+ const endpointMappings = [];
644
+ for (const pending of pendingEndpoints) {
645
+ const spec = swaggerSpecs.get(pending.swaggerUrl);
646
+ if (spec) {
647
+ // Extract the swagger-style path from the full URL
648
+ const swaggerPath = extractSwaggerPath(pending.endpoint.path, spec.baseUrl);
649
+ const swaggerKey = `${pending.endpoint.method} ${swaggerPath}`;
650
+ endpointMappings.push({
651
+ name: pending.endpoint.name,
652
+ swaggerKey,
653
+ swaggerUrl: pending.swaggerUrl,
654
+ filePath: pending.filePath
655
+ });
656
+ }
657
+ }
658
+ const specs = [];
659
+ for (const [swaggerUrl, swaggerSpec] of swaggerSpecs) {
660
+ const coverageResult = calculateCoverageForSpec(swaggerUrl, swaggerSpec, endpointMappings.filter(m => m.swaggerUrl === swaggerUrl), scopedCoverageByRoot);
661
+ specs.push(coverageResult);
662
+ }
663
+ // Step 4: Aggregate results
664
+ let totalCodes = 0;
665
+ let coveredCodes = 0;
666
+ for (const spec of specs) {
667
+ totalCodes += spec.total;
668
+ coveredCodes += spec.covered;
669
+ }
670
+ return {
671
+ total: totalCodes,
672
+ covered: coveredCodes,
673
+ percentage: totalCodes > 0 ? Math.round((coveredCodes / totalCodes) * 100) : 0,
674
+ specs,
675
+ hasSwagger: true
676
+ };
677
+ }
678
+ /**
679
+ * Calculate coverage for a single swagger spec
680
+ */
681
+ function calculateCoverageForSpec(swaggerUrl, spec, endpointMappings, scopedCoverageByRoot) {
682
+ const endpoints = [];
683
+ let total = 0;
684
+ let covered = 0;
685
+ // Build a lookup from swagger key to mapped .nornapi endpoints (with source file context)
686
+ const keyToMappings = new Map();
687
+ for (const mapping of endpointMappings) {
688
+ if (!keyToMappings.has(mapping.swaggerKey)) {
689
+ keyToMappings.set(mapping.swaggerKey, []);
690
+ }
691
+ keyToMappings.get(mapping.swaggerKey).push(mapping);
692
+ }
693
+ // Process each endpoint in the swagger spec
694
+ for (const section of spec.sections) {
695
+ for (const endpoint of section.endpoints) {
696
+ const swaggerKey = `${endpoint.method} ${endpoint.path}`;
697
+ const mappingsForEndpoint = keyToMappings.get(swaggerKey) || [];
698
+ // Get all asserted codes for any name that maps to this endpoint
699
+ const allAssertedCodes = new Set();
700
+ const coveredByNames = {}; // code -> names that cover it
701
+ const endpointRootPaths = new Set();
702
+ for (const mapping of mappingsForEndpoint) {
703
+ const rootPath = path.dirname(mapping.filePath);
704
+ endpointRootPaths.add(rootPath);
705
+ const scopedCoverage = scopedCoverageByRoot.get(rootPath);
706
+ const codes = scopedCoverage?.byName.get(mapping.name);
707
+ if (codes) {
708
+ for (const code of codes) {
709
+ allAssertedCodes.add(code);
710
+ // Track which names cover which codes
711
+ for (const responseCode of endpoint.responseCodes) {
712
+ if (wildcardMatches(code, responseCode)) {
713
+ if (!coveredByNames[responseCode]) {
714
+ coveredByNames[responseCode] = [];
715
+ }
716
+ if (!coveredByNames[responseCode].includes(mapping.name)) {
717
+ coveredByNames[responseCode].push(mapping.name);
718
+ }
719
+ }
720
+ }
721
+ }
722
+ }
723
+ }
724
+ // Also check URL-based coverage (from named request blocks)
725
+ // Match URLs to swagger endpoints by comparing the path portion
726
+ for (const rootPath of endpointRootPaths) {
727
+ const scopedCoverage = scopedCoverageByRoot.get(rootPath);
728
+ if (!scopedCoverage) {
729
+ continue;
730
+ }
731
+ for (const [, urlCoverage] of scopedCoverage.byUrl) {
732
+ // Check if method matches
733
+ if (urlCoverage.method.toUpperCase() !== endpoint.method.toUpperCase()) {
734
+ continue;
735
+ }
736
+ // Try to match URL/path/template targets to the swagger endpoint path.
737
+ try {
738
+ const urlPath = extractSwaggerPath(urlCoverage.url, spec.baseUrl);
739
+ // Check if URL path matches the swagger endpoint path
740
+ // Handle path parameters like /pet/{petId} matching /pet/1
741
+ if (pathMatchesSwaggerEndpoint(urlPath, endpoint.path)) {
742
+ for (const code of urlCoverage.assertedCodes) {
743
+ allAssertedCodes.add(code);
744
+ }
745
+ }
746
+ }
747
+ catch {
748
+ // Ignore malformed URLs/paths during coverage scan
749
+ }
750
+ }
751
+ }
752
+ // Calculate coverage for each response code
753
+ const responseCodes = [];
754
+ let endpointCovered = 0;
755
+ for (const code of endpoint.responseCodes) {
756
+ let isCovered = false;
757
+ const coveredBy = coveredByNames[code] || [];
758
+ // Check if any asserted code matches this response code
759
+ for (const assertedCode of allAssertedCodes) {
760
+ if (wildcardMatches(assertedCode, code)) {
761
+ isCovered = true;
762
+ break;
763
+ }
764
+ }
765
+ responseCodes.push({
766
+ code,
767
+ covered: isCovered,
768
+ coveredBy
769
+ });
770
+ total++;
771
+ if (isCovered) {
772
+ covered++;
773
+ endpointCovered++;
774
+ }
775
+ }
776
+ endpoints.push({
777
+ method: endpoint.method,
778
+ path: endpoint.path,
779
+ tag: endpoint.tag,
780
+ summary: endpoint.summary,
781
+ responseCodes,
782
+ totalCodes: endpoint.responseCodes.length,
783
+ coveredCodes: endpointCovered
784
+ });
785
+ }
786
+ }
787
+ // Sort endpoints by tag, then by path
788
+ endpoints.sort((a, b) => {
789
+ if (a.tag !== b.tag) {
790
+ return a.tag.localeCompare(b.tag);
791
+ }
792
+ return a.path.localeCompare(b.path);
793
+ });
794
+ return {
795
+ total,
796
+ covered,
797
+ percentage: total > 0 ? Math.round((covered / total) * 100) : 0,
798
+ endpoints,
799
+ swaggerUrl,
800
+ swaggerTitle: spec.title,
801
+ lastUpdated: new Date()
802
+ };
803
+ }
804
+ // Cached coverage result for status bar updates
805
+ let cachedCoverage = null;
806
+ let coverageUpdateListeners = [];
807
+ // Track known swagger URLs to detect new ones
808
+ let knownSwaggerUrls = new Set();
809
+ /**
810
+ * Get cached coverage or calculate if not available
811
+ */
812
+ async function getCoverage() {
813
+ if (!cachedCoverage) {
814
+ cachedCoverage = await calculateWorkspaceCoverage();
815
+ notifyCoverageListeners();
816
+ }
817
+ return cachedCoverage;
818
+ }
819
+ /**
820
+ * Calculate coverage scoped to a specific .nornapi file.
821
+ * Includes tests from that file's directory and descendants only.
822
+ */
823
+ async function getCoverageForNornapiFile(nornapiFilePath) {
824
+ return calculateWorkspaceCoverage(false, nornapiFilePath);
825
+ }
826
+ /**
827
+ * Force recalculate coverage - re-fetches swagger specs
828
+ */
829
+ async function refreshCoverage() {
830
+ cachedCoverage = await calculateWorkspaceCoverage();
831
+ notifyCoverageListeners();
832
+ return cachedCoverage;
833
+ }
834
+ /**
835
+ * Refresh coverage scoped to a specific .nornapi file.
836
+ */
837
+ async function refreshCoverageForNornapiFile(nornapiFilePath) {
838
+ return calculateWorkspaceCoverage(false, nornapiFilePath);
839
+ }
840
+ /**
841
+ * Recalculate coverage after execution - only re-scans .norn files,
842
+ * uses cached swagger specs unless new URLs are detected
843
+ */
844
+ async function recalculateCoverageAfterExecution() {
845
+ cachedCoverage = await calculateWorkspaceCoverage(true);
846
+ notifyCoverageListeners();
847
+ return cachedCoverage;
848
+ }
849
+ /**
850
+ * Clear cached coverage
851
+ */
852
+ function clearCoverageCache() {
853
+ cachedCoverage = null;
854
+ }
855
+ /**
856
+ * Register a listener for coverage updates
857
+ */
858
+ function onCoverageUpdate(listener) {
859
+ coverageUpdateListeners.push(listener);
860
+ return {
861
+ dispose: () => {
862
+ const index = coverageUpdateListeners.indexOf(listener);
863
+ if (index !== -1) {
864
+ coverageUpdateListeners.splice(index, 1);
865
+ }
866
+ }
867
+ };
868
+ }
869
+ /**
870
+ * Notify all listeners of coverage update
871
+ */
872
+ function notifyCoverageListeners() {
873
+ if (cachedCoverage) {
874
+ for (const listener of coverageUpdateListeners) {
875
+ listener(cachedCoverage);
876
+ }
877
+ }
878
+ }
879
+ //# sourceMappingURL=coverageCalculator.js.map