norn-cli 1.3.17 → 1.3.19

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.
@@ -0,0 +1,756 @@
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.refreshCoverage = refreshCoverage;
45
+ exports.recalculateCoverageAfterExecution = recalculateCoverageAfterExecution;
46
+ exports.clearCoverageCache = clearCoverageCache;
47
+ exports.onCoverageUpdate = onCoverageUpdate;
48
+ const vscode = __importStar(require("vscode"));
49
+ const fs = __importStar(require("fs"));
50
+ const swaggerParser_1 = require("./swaggerParser");
51
+ const nornapiParser_1 = require("./nornapiParser");
52
+ /**
53
+ * Extract the path portion from a full URL and normalize it for swagger matching.
54
+ * e.g., "https://petstore.swagger.io/v2/store/inventory" -> "/store/inventory"
55
+ *
56
+ * We need to strip the base URL (which typically includes version like /v2) to match swagger paths.
57
+ */
58
+ function extractSwaggerPath(fullUrl, swaggerBaseUrl) {
59
+ try {
60
+ // If it's already just a path (starts with /), return as-is
61
+ if (fullUrl.startsWith('/')) {
62
+ return fullUrl;
63
+ }
64
+ const url = new URL(fullUrl);
65
+ // Decode the pathname to convert %7B to { and %7D to }
66
+ let path = decodeURIComponent(url.pathname);
67
+ // If we have a swagger base URL, try to strip it from the path
68
+ if (swaggerBaseUrl) {
69
+ try {
70
+ const baseUrl = new URL(swaggerBaseUrl);
71
+ const basePath = decodeURIComponent(baseUrl.pathname);
72
+ // If the path starts with the base path (e.g., /v2), strip it
73
+ if (basePath && basePath !== '/' && path.startsWith(basePath)) {
74
+ path = path.substring(basePath.length);
75
+ // Ensure path starts with /
76
+ if (!path.startsWith('/')) {
77
+ path = '/' + path;
78
+ }
79
+ }
80
+ }
81
+ catch {
82
+ // Ignore base URL parsing errors
83
+ }
84
+ }
85
+ return path || '/';
86
+ }
87
+ catch {
88
+ // If URL parsing fails, try to extract path manually
89
+ const match = fullUrl.match(/https?:\/\/[^\/]+(\/.*)/);
90
+ return match ? match[1] : fullUrl;
91
+ }
92
+ }
93
+ /**
94
+ * Extract swagger URL from .nornapi file content
95
+ */
96
+ function extractSwaggerUrl(content) {
97
+ const match = content.match(/^swagger\s+["']?(https?:\/\/[^\s"']+)["']?\s*$/m);
98
+ return match?.[1];
99
+ }
100
+ /**
101
+ * Parse assert status lines from sequence content.
102
+ * Returns array of status codes/patterns (e.g., ["200", "2xx", "404"])
103
+ */
104
+ function extractAssertedStatusCodes(sequenceContent) {
105
+ const codes = [];
106
+ const lines = sequenceContent.split('\n');
107
+ for (const line of lines) {
108
+ const trimmed = line.trim();
109
+ // Match various assert status patterns:
110
+ // - assert status == 200
111
+ // - assert $1.status == 200
112
+ // - assert response.status == 200
113
+ // - assert inventory.status == 200
114
+ // - assert variableName.status >= 400
115
+ const statusMatch = trimmed.match(/^assert\s+(?:(?:\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)\.)?status\s*(?:==|>=|<=|>|<|!=)?\s*(\d{3}|[1-5]xx)$/i);
116
+ if (statusMatch) {
117
+ codes.push(statusMatch[1].toLowerCase());
118
+ continue;
119
+ }
120
+ // Also match simpler patterns like: assert status 200
121
+ const simpleMatch = trimmed.match(/^assert\s+status\s+(\d{3}|[1-5]xx)$/i);
122
+ if (simpleMatch) {
123
+ codes.push(simpleMatch[1].toLowerCase());
124
+ }
125
+ }
126
+ return codes;
127
+ }
128
+ /**
129
+ * Check if a wildcard pattern matches a status code
130
+ * e.g., "2xx" matches "200", "201", "204"
131
+ */
132
+ function wildcardMatches(pattern, code) {
133
+ const lowerPattern = pattern.toLowerCase();
134
+ // Exact match
135
+ if (lowerPattern === code) {
136
+ return true;
137
+ }
138
+ // Wildcard match (2xx, 4xx, 5xx)
139
+ if (/^[1-5]xx$/.test(lowerPattern)) {
140
+ const patternPrefix = lowerPattern[0];
141
+ return code.startsWith(patternPrefix);
142
+ }
143
+ return false;
144
+ }
145
+ /**
146
+ * Check if a concrete URL path matches a swagger endpoint path template
147
+ * e.g., "/pet/1" matches "/pet/{petId}"
148
+ * "/v2/pet/123" matches "/pet/{petId}" (with base path /v2)
149
+ */
150
+ function pathMatchesSwaggerEndpoint(urlPath, swaggerPath) {
151
+ // Normalize paths - remove leading/trailing slashes for comparison
152
+ const normalizedUrl = urlPath.replace(/^\/+|\/+$/g, '');
153
+ const normalizedSwagger = swaggerPath.replace(/^\/+|\/+$/g, '');
154
+ const urlParts = normalizedUrl.split('/');
155
+ const swaggerParts = normalizedSwagger.split('/');
156
+ // Try matching from different starting positions to handle base paths
157
+ // e.g., URL /v2/pet/1 should match swagger /pet/{petId} if base is /v2
158
+ for (let offset = 0; offset <= urlParts.length - swaggerParts.length; offset++) {
159
+ let matches = true;
160
+ for (let i = 0; i < swaggerParts.length; i++) {
161
+ const urlPart = urlParts[offset + i];
162
+ const swaggerPart = swaggerParts[i];
163
+ // Path parameter in swagger (e.g., {petId}) matches any value
164
+ if (swaggerPart.startsWith('{') && swaggerPart.endsWith('}')) {
165
+ // Path parameter matches any non-empty value
166
+ if (!urlPart || urlPart.length === 0) {
167
+ matches = false;
168
+ break;
169
+ }
170
+ }
171
+ else if (urlPart !== swaggerPart) {
172
+ // Static segments must match exactly
173
+ matches = false;
174
+ break;
175
+ }
176
+ }
177
+ if (matches) {
178
+ return true;
179
+ }
180
+ }
181
+ return false;
182
+ }
183
+ /**
184
+ * Extract API calls from sequence content.
185
+ * Returns array of { endpointName, assertedCodes, method?, url? }
186
+ *
187
+ * @param sequenceContent - The content of the sequence to analyze
188
+ * @param allSequences - Map of all sequences by name (for following run SequenceName calls)
189
+ * @param allNamedRequests - Map of named request blocks by name (for following run NamedRequest calls)
190
+ * @param visited - Set of already visited sequence names (to prevent infinite recursion)
191
+ */
192
+ function extractApiCalls(sequenceContent, allSequences, allNamedRequests, visited) {
193
+ const calls = [];
194
+ const lines = sequenceContent.split('\n');
195
+ const visitedSet = visited || new Set();
196
+ for (let i = 0; i < lines.length; i++) {
197
+ const line = lines[i].trim();
198
+ // Check for "run Name" or "var x = run Name" calls
199
+ 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);
200
+ if (runMatch) {
201
+ const targetName = runMatch[1];
202
+ // Avoid infinite recursion
203
+ if (!visitedSet.has(targetName)) {
204
+ visitedSet.add(targetName);
205
+ // First check if it's a sequence
206
+ if (allSequences) {
207
+ const subSequenceContent = allSequences.get(targetName);
208
+ if (subSequenceContent) {
209
+ // Recursively extract API calls from the sub-sequence
210
+ const subCalls = extractApiCalls(subSequenceContent, allSequences, allNamedRequests, visitedSet);
211
+ calls.push(...subCalls);
212
+ continue;
213
+ }
214
+ }
215
+ // Then check if it's a named request block
216
+ if (allNamedRequests) {
217
+ const namedRequest = allNamedRequests.get(targetName);
218
+ if (namedRequest) {
219
+ // Look for assert status lines following this run command
220
+ const assertedCodes = [];
221
+ for (let j = i + 1; j < lines.length; j++) {
222
+ const nextLine = lines[j].trim();
223
+ // Stop at next request, sequence end, or other control flow
224
+ if (/^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i.test(nextLine) ||
225
+ /^end\s+sequence$/i.test(nextLine) ||
226
+ /^run\s+/i.test(nextLine) ||
227
+ /^if\s+/i.test(nextLine) ||
228
+ /^foreach\s+/i.test(nextLine)) {
229
+ break;
230
+ }
231
+ // Check for assert status
232
+ const statusMatch = nextLine.match(/^assert\s+(?:(?:\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)\.)?status\s*(?:==|>=|<=|>|<|!=)?\s*(\d{3}|[1-5]xx)$/i);
233
+ if (statusMatch) {
234
+ assertedCodes.push(statusMatch[1].toLowerCase());
235
+ }
236
+ }
237
+ // Add this as a URL-based API call (will be matched by URL later)
238
+ calls.push({
239
+ endpointName: `__url__${namedRequest.method}__${namedRequest.url}`,
240
+ assertedCodes,
241
+ method: namedRequest.method,
242
+ url: namedRequest.url
243
+ });
244
+ continue;
245
+ }
246
+ }
247
+ }
248
+ continue;
249
+ }
250
+ // Match API request by name with optional variable assignment:
251
+ // - GET EndpointName
252
+ // - GET EndpointName(args)
253
+ // - GET EndpointName(args) HeaderGroup
254
+ // - var result = GET EndpointName
255
+ // - var inventory = GET GetInventory
256
+ // - var order = GET GetOrderById(1)
257
+ 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);
258
+ if (apiCallMatch) {
259
+ const endpointName = apiCallMatch[2];
260
+ // Look for assert status lines following this request (until next request or end)
261
+ const assertedCodes = [];
262
+ for (let j = i + 1; j < lines.length; j++) {
263
+ const nextLine = lines[j].trim();
264
+ // Stop at next request, sequence end, or other control flow
265
+ // Also stop if we see another var assignment with HTTP method
266
+ if (/^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i.test(nextLine) ||
267
+ /^end\s+sequence$/i.test(nextLine) ||
268
+ /^run\s+/i.test(nextLine) ||
269
+ /^if\s+/i.test(nextLine) ||
270
+ /^foreach\s+/i.test(nextLine)) {
271
+ break;
272
+ }
273
+ // Check for assert status with variable name or $N reference
274
+ const statusMatch = nextLine.match(/^assert\s+(?:(?:\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)\.)?status\s*(?:==|>=|<=|>|<|!=)?\s*(\d{3}|[1-5]xx)$/i);
275
+ if (statusMatch) {
276
+ assertedCodes.push(statusMatch[1].toLowerCase());
277
+ }
278
+ // Simpler pattern
279
+ const simpleMatch = nextLine.match(/^assert\s+status\s+(\d{3}|[1-5]xx)$/i);
280
+ if (simpleMatch) {
281
+ assertedCodes.push(simpleMatch[1].toLowerCase());
282
+ }
283
+ }
284
+ calls.push({ endpointName, assertedCodes });
285
+ }
286
+ }
287
+ return calls;
288
+ }
289
+ /**
290
+ * Extract all test sequences from a .norn file content.
291
+ * Only "test sequence" blocks count towards coverage, not regular "sequence" blocks.
292
+ */
293
+ function extractTestSequences(content) {
294
+ const sequences = [];
295
+ const lines = content.split('\n');
296
+ let currentSequence = null;
297
+ for (let i = 0; i < lines.length; i++) {
298
+ const line = lines[i].trim();
299
+ // Match only "test sequence Name" - regular sequences don't count towards coverage
300
+ const sequenceMatch = line.match(/^test\s+sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?$/);
301
+ if (sequenceMatch) {
302
+ currentSequence = { name: sequenceMatch[1], startLine: i };
303
+ continue;
304
+ }
305
+ // Match sequence end
306
+ if (/^end\s+sequence$/i.test(line) && currentSequence) {
307
+ const sequenceContent = lines.slice(currentSequence.startLine, i + 1).join('\n');
308
+ sequences.push({ name: currentSequence.name, content: sequenceContent });
309
+ currentSequence = null;
310
+ }
311
+ }
312
+ return sequences;
313
+ }
314
+ /**
315
+ * Extract ALL sequences from a .norn file content (both test and regular).
316
+ * Used to build a map for following "run SequenceName" calls.
317
+ */
318
+ function extractAllSequences(content) {
319
+ const sequences = new Map();
320
+ const lines = content.split('\n');
321
+ let currentSequence = null;
322
+ for (let i = 0; i < lines.length; i++) {
323
+ const line = lines[i].trim();
324
+ // Match both "test sequence Name" and "sequence Name"
325
+ const testSequenceMatch = line.match(/^test\s+sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?$/);
326
+ const sequenceMatch = line.match(/^sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?$/);
327
+ if (testSequenceMatch || sequenceMatch) {
328
+ const name = testSequenceMatch ? testSequenceMatch[1] : sequenceMatch[1];
329
+ currentSequence = { name, startLine: i };
330
+ continue;
331
+ }
332
+ // Match sequence end
333
+ if (/^end\s+sequence$/i.test(line) && currentSequence) {
334
+ const sequenceContent = lines.slice(currentSequence.startLine + 1, i).join('\n');
335
+ sequences.set(currentSequence.name, sequenceContent);
336
+ currentSequence = null;
337
+ }
338
+ }
339
+ return sequences;
340
+ }
341
+ /**
342
+ * Extract named request blocks from a .norn file content.
343
+ * Named requests are blocks like [RequestName] followed by HTTP method + URL.
344
+ */
345
+ function extractNamedRequests(content) {
346
+ const requests = new Map();
347
+ const lines = content.split('\n');
348
+ let currentRequest = null;
349
+ for (let i = 0; i < lines.length; i++) {
350
+ const line = lines[i].trim();
351
+ // Match named request start: [RequestName]
352
+ const nameMatch = line.match(/^\[([a-zA-Z_][a-zA-Z0-9_-]*)\]$/);
353
+ if (nameMatch) {
354
+ // Save previous request if exists
355
+ if (currentRequest) {
356
+ const requestContent = lines.slice(currentRequest.startLine + 1, i).join('\n').trim();
357
+ const parsed = parseRequestContent(requestContent);
358
+ if (parsed) {
359
+ requests.set(currentRequest.name, { ...parsed, content: requestContent });
360
+ }
361
+ }
362
+ currentRequest = { name: nameMatch[1], startLine: i };
363
+ continue;
364
+ }
365
+ // Check for sequence start or end - terminates named request block
366
+ if (/^(?:test\s+)?sequence\s+/i.test(line) || /^end\s+sequence$/i.test(line)) {
367
+ if (currentRequest) {
368
+ const requestContent = lines.slice(currentRequest.startLine + 1, i).join('\n').trim();
369
+ const parsed = parseRequestContent(requestContent);
370
+ if (parsed) {
371
+ requests.set(currentRequest.name, { ...parsed, content: requestContent });
372
+ }
373
+ currentRequest = null;
374
+ }
375
+ }
376
+ }
377
+ // Don't forget the last request
378
+ if (currentRequest) {
379
+ const requestContent = lines.slice(currentRequest.startLine + 1).join('\n').trim();
380
+ const parsed = parseRequestContent(requestContent);
381
+ if (parsed) {
382
+ requests.set(currentRequest.name, { ...parsed, content: requestContent });
383
+ }
384
+ }
385
+ return requests;
386
+ }
387
+ /**
388
+ * Parse the content of a named request block to extract method and URL.
389
+ */
390
+ function parseRequestContent(content) {
391
+ const lines = content.split('\n');
392
+ for (const line of lines) {
393
+ const trimmed = line.trim();
394
+ // Match: GET "url" or GET url or POST "url" etc.
395
+ const match = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+["']?([^"'\s]+)["']?/i);
396
+ if (match) {
397
+ return { method: match[1].toUpperCase(), url: match[2] };
398
+ }
399
+ }
400
+ return null;
401
+ }
402
+ /**
403
+ * Calculate coverage for the entire workspace
404
+ * @param useCachedSpecs If true, only fetch swagger specs for new URLs (used after execution)
405
+ */
406
+ async function calculateWorkspaceCoverage(useCachedSpecs = false) {
407
+ const workspaceFolders = vscode.workspace.workspaceFolders;
408
+ if (!workspaceFolders) {
409
+ return {
410
+ total: 0,
411
+ covered: 0,
412
+ percentage: 0,
413
+ specs: [],
414
+ hasSwagger: false
415
+ };
416
+ }
417
+ // Step 1: Find all .nornapi files and extract swagger URLs + endpoint mappings
418
+ const nornapiFiles = await vscode.workspace.findFiles('**/*.nornapi', '**/node_modules/**');
419
+ const swaggerUrls = new Map(); // swagger URL -> list of nornapi files
420
+ // Store endpoint info temporarily - we'll create mappings after fetching swagger specs
421
+ const pendingEndpoints = [];
422
+ for (const file of nornapiFiles) {
423
+ try {
424
+ const content = fs.readFileSync(file.fsPath, 'utf-8');
425
+ const swaggerUrl = extractSwaggerUrl(content);
426
+ if (swaggerUrl) {
427
+ // Track this nornapi file for this swagger URL
428
+ if (!swaggerUrls.has(swaggerUrl)) {
429
+ swaggerUrls.set(swaggerUrl, []);
430
+ }
431
+ swaggerUrls.get(swaggerUrl).push({ url: swaggerUrl, filePath: file.fsPath });
432
+ // Parse endpoints from this nornapi file
433
+ const nornApiDef = (0, nornapiParser_1.parseNornApiFile)(content);
434
+ for (const endpoint of nornApiDef.endpoints) {
435
+ // Store for later processing after we have swagger specs
436
+ pendingEndpoints.push({
437
+ endpoint: { name: endpoint.name, method: endpoint.method, path: endpoint.path },
438
+ swaggerUrl,
439
+ filePath: file.fsPath
440
+ });
441
+ }
442
+ }
443
+ }
444
+ catch (error) {
445
+ console.error(`Error reading .nornapi file ${file.fsPath}:`, error);
446
+ }
447
+ }
448
+ if (swaggerUrls.size === 0) {
449
+ return {
450
+ total: 0,
451
+ covered: 0,
452
+ percentage: 0,
453
+ specs: [],
454
+ hasSwagger: false
455
+ };
456
+ }
457
+ // Detect new swagger URLs (not seen before)
458
+ const currentUrls = new Set(swaggerUrls.keys());
459
+ const newUrls = new Set();
460
+ for (const url of currentUrls) {
461
+ if (!knownSwaggerUrls.has(url)) {
462
+ newUrls.add(url);
463
+ }
464
+ }
465
+ // Update known URLs
466
+ knownSwaggerUrls = currentUrls;
467
+ // Step 2: Find all .norn files and extract API calls with assert status
468
+ const nornFiles = await vscode.workspace.findFiles('**/*.norn', '**/node_modules/**');
469
+ // Map: endpoint name -> list of asserted status codes (from all sequences)
470
+ const coverageByName = new Map();
471
+ // First pass: build a combined map of all sequences across all files
472
+ // This allows test sequences to call sub-sequences defined in other files
473
+ const allSequencesMap = new Map();
474
+ const allNamedRequestsMap = new Map();
475
+ for (const file of nornFiles) {
476
+ try {
477
+ const content = fs.readFileSync(file.fsPath, 'utf-8');
478
+ const fileSequences = extractAllSequences(content);
479
+ for (const [name, seqContent] of fileSequences) {
480
+ allSequencesMap.set(name, seqContent);
481
+ }
482
+ const fileNamedRequests = extractNamedRequests(content);
483
+ for (const [name, reqInfo] of fileNamedRequests) {
484
+ allNamedRequestsMap.set(name, reqInfo);
485
+ }
486
+ }
487
+ catch (error) {
488
+ console.error(`Error reading .norn file ${file.fsPath}:`, error);
489
+ }
490
+ }
491
+ // Track URL-based API calls separately (for matching by URL instead of endpoint name)
492
+ const coverageByUrl = new Map();
493
+ // Second pass: extract API calls from test sequences, following run commands
494
+ for (const file of nornFiles) {
495
+ try {
496
+ const content = fs.readFileSync(file.fsPath, 'utf-8');
497
+ const testSequences = extractTestSequences(content);
498
+ for (const sequence of testSequences) {
499
+ // Pass the map of all sequences and named requests so we can follow "run" calls
500
+ const apiCalls = extractApiCalls(sequence.content, allSequencesMap, allNamedRequestsMap);
501
+ for (const call of apiCalls) {
502
+ // Check if this is a URL-based call (from a named request block)
503
+ if (call.url && call.method) {
504
+ const key = `${call.method} ${call.url}`;
505
+ if (!coverageByUrl.has(key)) {
506
+ coverageByUrl.set(key, { method: call.method, url: call.url, assertedCodes: new Set() });
507
+ }
508
+ for (const code of call.assertedCodes) {
509
+ coverageByUrl.get(key).assertedCodes.add(code);
510
+ }
511
+ }
512
+ else {
513
+ // Regular endpoint name-based call
514
+ if (!coverageByName.has(call.endpointName)) {
515
+ coverageByName.set(call.endpointName, new Set());
516
+ }
517
+ for (const code of call.assertedCodes) {
518
+ coverageByName.get(call.endpointName).add(code);
519
+ }
520
+ }
521
+ }
522
+ }
523
+ }
524
+ catch (error) {
525
+ console.error(`Error reading .norn file ${file.fsPath}:`, error);
526
+ }
527
+ }
528
+ // Step 3: For each swagger spec, calculate coverage
529
+ // First fetch all specs so we have baseUrl for path normalization
530
+ const swaggerSpecs = new Map();
531
+ for (const [swaggerUrl] of swaggerUrls) {
532
+ try {
533
+ const swaggerSpec = await (0, swaggerParser_1.getCachedSwaggerSpec)(swaggerUrl);
534
+ swaggerSpecs.set(swaggerUrl, swaggerSpec);
535
+ }
536
+ catch (error) {
537
+ console.error(`Error fetching swagger spec ${swaggerUrl}:`, error);
538
+ }
539
+ }
540
+ // Now create endpoint mappings with proper path normalization
541
+ const endpointMappings = [];
542
+ for (const pending of pendingEndpoints) {
543
+ const spec = swaggerSpecs.get(pending.swaggerUrl);
544
+ if (spec) {
545
+ // Extract the swagger-style path from the full URL
546
+ const swaggerPath = extractSwaggerPath(pending.endpoint.path, spec.baseUrl);
547
+ const swaggerKey = `${pending.endpoint.method} ${swaggerPath}`;
548
+ endpointMappings.push({
549
+ name: pending.endpoint.name,
550
+ swaggerKey,
551
+ swaggerUrl: pending.swaggerUrl,
552
+ filePath: pending.filePath
553
+ });
554
+ }
555
+ }
556
+ const specs = [];
557
+ for (const [swaggerUrl, swaggerSpec] of swaggerSpecs) {
558
+ const coverageResult = calculateCoverageForSpec(swaggerUrl, swaggerSpec, endpointMappings.filter(m => m.swaggerUrl === swaggerUrl), coverageByName, coverageByUrl);
559
+ specs.push(coverageResult);
560
+ }
561
+ // Step 4: Aggregate results
562
+ let totalCodes = 0;
563
+ let coveredCodes = 0;
564
+ for (const spec of specs) {
565
+ totalCodes += spec.total;
566
+ coveredCodes += spec.covered;
567
+ }
568
+ return {
569
+ total: totalCodes,
570
+ covered: coveredCodes,
571
+ percentage: totalCodes > 0 ? Math.round((coveredCodes / totalCodes) * 100) : 0,
572
+ specs,
573
+ hasSwagger: true
574
+ };
575
+ }
576
+ /**
577
+ * Calculate coverage for a single swagger spec
578
+ */
579
+ function calculateCoverageForSpec(swaggerUrl, spec, endpointMappings, coverageByName, coverageByUrl) {
580
+ const endpoints = [];
581
+ let total = 0;
582
+ let covered = 0;
583
+ // Build a lookup from swagger key to endpoint names
584
+ const keyToNames = new Map();
585
+ for (const mapping of endpointMappings) {
586
+ if (!keyToNames.has(mapping.swaggerKey)) {
587
+ keyToNames.set(mapping.swaggerKey, []);
588
+ }
589
+ keyToNames.get(mapping.swaggerKey).push(mapping.name);
590
+ }
591
+ // Process each endpoint in the swagger spec
592
+ for (const section of spec.sections) {
593
+ for (const endpoint of section.endpoints) {
594
+ const swaggerKey = `${endpoint.method} ${endpoint.path}`;
595
+ const names = keyToNames.get(swaggerKey) || [];
596
+ // Get all asserted codes for any name that maps to this endpoint
597
+ const allAssertedCodes = new Set();
598
+ const coveredByNames = {}; // code -> names that cover it
599
+ for (const name of names) {
600
+ const codes = coverageByName.get(name);
601
+ if (codes) {
602
+ for (const code of codes) {
603
+ allAssertedCodes.add(code);
604
+ // Track which names cover which codes
605
+ for (const responseCode of endpoint.responseCodes) {
606
+ if (wildcardMatches(code, responseCode)) {
607
+ if (!coveredByNames[responseCode]) {
608
+ coveredByNames[responseCode] = [];
609
+ }
610
+ if (!coveredByNames[responseCode].includes(name)) {
611
+ coveredByNames[responseCode].push(name);
612
+ }
613
+ }
614
+ }
615
+ }
616
+ }
617
+ }
618
+ // Also check URL-based coverage (from named request blocks)
619
+ // Match URLs to swagger endpoints by comparing the path portion
620
+ for (const [, urlCoverage] of coverageByUrl) {
621
+ // Check if method matches
622
+ if (urlCoverage.method.toUpperCase() !== endpoint.method.toUpperCase()) {
623
+ continue;
624
+ }
625
+ // Try to match URL to swagger endpoint path
626
+ // Extract the path from the full URL
627
+ try {
628
+ const url = new URL(urlCoverage.url);
629
+ const urlPath = url.pathname;
630
+ // Check if URL path matches the swagger endpoint path
631
+ // Handle path parameters like /pet/{petId} matching /pet/1
632
+ if (pathMatchesSwaggerEndpoint(urlPath, endpoint.path)) {
633
+ for (const code of urlCoverage.assertedCodes) {
634
+ allAssertedCodes.add(code);
635
+ }
636
+ }
637
+ }
638
+ catch {
639
+ // Invalid URL, skip
640
+ }
641
+ }
642
+ // Calculate coverage for each response code
643
+ const responseCodes = [];
644
+ let endpointCovered = 0;
645
+ for (const code of endpoint.responseCodes) {
646
+ let isCovered = false;
647
+ const coveredBy = coveredByNames[code] || [];
648
+ // Check if any asserted code matches this response code
649
+ for (const assertedCode of allAssertedCodes) {
650
+ if (wildcardMatches(assertedCode, code)) {
651
+ isCovered = true;
652
+ break;
653
+ }
654
+ }
655
+ responseCodes.push({
656
+ code,
657
+ covered: isCovered,
658
+ coveredBy
659
+ });
660
+ total++;
661
+ if (isCovered) {
662
+ covered++;
663
+ endpointCovered++;
664
+ }
665
+ }
666
+ endpoints.push({
667
+ method: endpoint.method,
668
+ path: endpoint.path,
669
+ tag: endpoint.tag,
670
+ summary: endpoint.summary,
671
+ responseCodes,
672
+ totalCodes: endpoint.responseCodes.length,
673
+ coveredCodes: endpointCovered
674
+ });
675
+ }
676
+ }
677
+ // Sort endpoints by tag, then by path
678
+ endpoints.sort((a, b) => {
679
+ if (a.tag !== b.tag) {
680
+ return a.tag.localeCompare(b.tag);
681
+ }
682
+ return a.path.localeCompare(b.path);
683
+ });
684
+ return {
685
+ total,
686
+ covered,
687
+ percentage: total > 0 ? Math.round((covered / total) * 100) : 0,
688
+ endpoints,
689
+ swaggerUrl,
690
+ swaggerTitle: spec.title,
691
+ lastUpdated: new Date()
692
+ };
693
+ }
694
+ // Cached coverage result for status bar updates
695
+ let cachedCoverage = null;
696
+ let coverageUpdateListeners = [];
697
+ // Track known swagger URLs to detect new ones
698
+ let knownSwaggerUrls = new Set();
699
+ /**
700
+ * Get cached coverage or calculate if not available
701
+ */
702
+ async function getCoverage() {
703
+ if (!cachedCoverage) {
704
+ cachedCoverage = await calculateWorkspaceCoverage();
705
+ notifyCoverageListeners();
706
+ }
707
+ return cachedCoverage;
708
+ }
709
+ /**
710
+ * Force recalculate coverage - re-fetches swagger specs
711
+ */
712
+ async function refreshCoverage() {
713
+ cachedCoverage = await calculateWorkspaceCoverage();
714
+ notifyCoverageListeners();
715
+ return cachedCoverage;
716
+ }
717
+ /**
718
+ * Recalculate coverage after execution - only re-scans .norn files,
719
+ * uses cached swagger specs unless new URLs are detected
720
+ */
721
+ async function recalculateCoverageAfterExecution() {
722
+ cachedCoverage = await calculateWorkspaceCoverage(true);
723
+ notifyCoverageListeners();
724
+ return cachedCoverage;
725
+ }
726
+ /**
727
+ * Clear cached coverage
728
+ */
729
+ function clearCoverageCache() {
730
+ cachedCoverage = null;
731
+ }
732
+ /**
733
+ * Register a listener for coverage updates
734
+ */
735
+ function onCoverageUpdate(listener) {
736
+ coverageUpdateListeners.push(listener);
737
+ return {
738
+ dispose: () => {
739
+ const index = coverageUpdateListeners.indexOf(listener);
740
+ if (index !== -1) {
741
+ coverageUpdateListeners.splice(index, 1);
742
+ }
743
+ }
744
+ };
745
+ }
746
+ /**
747
+ * Notify all listeners of coverage update
748
+ */
749
+ function notifyCoverageListeners() {
750
+ if (cachedCoverage) {
751
+ for (const listener of coverageUpdateListeners) {
752
+ listener(cachedCoverage);
753
+ }
754
+ }
755
+ }
756
+ //# sourceMappingURL=coverageCalculator.js.map