norn-cli 1.6.0 → 1.6.2

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