norn-cli 1.3.16 → 1.3.18
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.
- package/AGENTS.md +72 -0
- package/CHANGELOG.md +34 -1
- package/README.md +4 -2
- package/dist/cli.js +135 -63
- package/out/assertionRunner.js +537 -0
- package/out/cli/colors.js +129 -0
- package/out/cli/formatters/assertion.js +75 -0
- package/out/cli/formatters/index.js +23 -0
- package/out/cli/formatters/response.js +106 -0
- package/out/cli/formatters/summary.js +187 -0
- package/out/cli/redaction.js +237 -0
- package/out/cli/reporters/html.js +634 -0
- package/out/cli/reporters/index.js +22 -0
- package/out/cli/reporters/junit.js +211 -0
- package/out/cli.js +926 -0
- package/out/codeLensProvider.js +254 -0
- package/out/compareContentProvider.js +85 -0
- package/out/completionProvider.js +1886 -0
- package/out/contractDecorationProvider.js +243 -0
- package/out/coverageCalculator.js +756 -0
- package/out/coveragePanel.js +542 -0
- package/out/diagnosticProvider.js +980 -0
- package/out/environmentProvider.js +373 -0
- package/out/extension.js +1025 -0
- package/out/httpClient.js +269 -0
- package/out/jsonFileReader.js +320 -0
- package/out/nornapiParser.js +326 -0
- package/out/parser.js +725 -0
- package/out/responsePanel.js +4674 -0
- package/out/schemaGenerator.js +393 -0
- package/out/scriptRunner.js +419 -0
- package/out/sequenceRunner.js +3046 -0
- package/out/swaggerParser.js +339 -0
- package/out/test/extension.test.js +48 -0
- package/out/testProvider.js +658 -0
- package/out/validationCache.js +245 -0
- package/package.json +1 -1
package/out/extension.js
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.activate = activate;
|
|
37
|
+
exports.saveSchemaValidationResults = saveSchemaValidationResults;
|
|
38
|
+
exports.deactivate = deactivate;
|
|
39
|
+
const vscode = __importStar(require("vscode"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const fs = __importStar(require("fs/promises"));
|
|
42
|
+
const fsSync = __importStar(require("fs"));
|
|
43
|
+
const parser_1 = require("./parser");
|
|
44
|
+
const httpClient_1 = require("./httpClient");
|
|
45
|
+
const codeLensProvider_1 = require("./codeLensProvider");
|
|
46
|
+
const completionProvider_1 = require("./completionProvider");
|
|
47
|
+
const responsePanel_1 = require("./responsePanel");
|
|
48
|
+
const diagnosticProvider_1 = require("./diagnosticProvider");
|
|
49
|
+
const sequenceRunner_1 = require("./sequenceRunner");
|
|
50
|
+
const environmentProvider_1 = require("./environmentProvider");
|
|
51
|
+
const swaggerParser_1 = require("./swaggerParser");
|
|
52
|
+
const nornapiParser_1 = require("./nornapiParser");
|
|
53
|
+
const testProvider_1 = require("./testProvider");
|
|
54
|
+
const coverageCalculator_1 = require("./coverageCalculator");
|
|
55
|
+
const coveragePanel_1 = require("./coveragePanel");
|
|
56
|
+
const compareContentProvider_1 = require("./compareContentProvider");
|
|
57
|
+
const contractDecorationProvider_1 = require("./contractDecorationProvider");
|
|
58
|
+
const validationCache_1 = require("./validationCache");
|
|
59
|
+
const schemaGenerator_1 = require("./schemaGenerator");
|
|
60
|
+
// Module-level reference to contract decoration provider for refreshing after sequence runs
|
|
61
|
+
let contractDecorationProviderInstance;
|
|
62
|
+
function activate(context) {
|
|
63
|
+
console.log('Norn extension activated');
|
|
64
|
+
// Register content provider for response comparison diff
|
|
65
|
+
(0, compareContentProvider_1.registerCompareContentProvider)(context);
|
|
66
|
+
// Register Test Explorer integration
|
|
67
|
+
const testController = new testProvider_1.NornTestController();
|
|
68
|
+
context.subscriptions.push(testController);
|
|
69
|
+
const sendRequestCommand = vscode.commands.registerCommand('norn.sendRequest', (lineFromCodeLens) => processEditorsInput(context.extensionUri, lineFromCodeLens));
|
|
70
|
+
const runSequenceCommand = vscode.commands.registerCommand('norn.runSequence', (lineFromCodeLens) => processSequence(context.extensionUri, lineFromCodeLens));
|
|
71
|
+
const clearCookiesCommand = vscode.commands.registerCommand('norn.clearCookies', async () => {
|
|
72
|
+
(0, httpClient_1.clearCookies)();
|
|
73
|
+
vscode.window.showInformationMessage('Norn: All cookies cleared');
|
|
74
|
+
});
|
|
75
|
+
const showCookiesCommand = vscode.commands.registerCommand('norn.showCookies', async () => {
|
|
76
|
+
const cookies = await (0, httpClient_1.getAllCookies)();
|
|
77
|
+
if (cookies.length === 0) {
|
|
78
|
+
vscode.window.showInformationMessage('Norn: No cookies stored');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const items = cookies.map(c => `${c.domain}: ${c.name}=${c.value.substring(0, 30)}${c.value.length > 30 ? '...' : ''}`);
|
|
82
|
+
vscode.window.showQuickPick(items, { title: 'Stored Cookies', canPickMany: false });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// Register environment selector command
|
|
86
|
+
const selectEnvironmentCommand = vscode.commands.registerCommand('norn.selectEnvironment', async () => {
|
|
87
|
+
await (0, environmentProvider_1.showEnvironmentPicker)();
|
|
88
|
+
vscode.commands.executeCommand('norn.refreshDiagnostics');
|
|
89
|
+
});
|
|
90
|
+
// Register environment selector with refresh (for CodeLens)
|
|
91
|
+
const selectEnvironmentAndRefreshCommand = vscode.commands.registerCommand('norn.selectEnvironmentAndRefresh', async () => {
|
|
92
|
+
await (0, environmentProvider_1.showEnvironmentPicker)();
|
|
93
|
+
vscode.commands.executeCommand('norn.refreshCodeLenses');
|
|
94
|
+
vscode.commands.executeCommand('norn.refreshDiagnostics');
|
|
95
|
+
});
|
|
96
|
+
// Register command to create .nornenv file
|
|
97
|
+
const createEnvFileCommand = vscode.commands.registerCommand('norn.createEnvFile', async () => {
|
|
98
|
+
const workspaceFolders = vscode.workspace.workspaceFolders;
|
|
99
|
+
if (!workspaceFolders) {
|
|
100
|
+
vscode.window.showErrorMessage('No workspace folder open');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// If multiple folders, ask which one
|
|
104
|
+
let targetFolder = workspaceFolders[0];
|
|
105
|
+
if (workspaceFolders.length > 1) {
|
|
106
|
+
const picked = await vscode.window.showWorkspaceFolderPick({
|
|
107
|
+
placeHolder: 'Select workspace folder for .nornenv file'
|
|
108
|
+
});
|
|
109
|
+
if (!picked) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
targetFolder = picked;
|
|
113
|
+
}
|
|
114
|
+
const envFilePath = vscode.Uri.joinPath(targetFolder.uri, '.nornenv');
|
|
115
|
+
const template = `# Norn Environment Configuration
|
|
116
|
+
# Variables defined here are available in your .norn files as {{variableName}}
|
|
117
|
+
|
|
118
|
+
# Common variables (available in all environments)
|
|
119
|
+
var baseUrl = https://api.example.com
|
|
120
|
+
|
|
121
|
+
# Development environment
|
|
122
|
+
[env:dev]
|
|
123
|
+
var apiKey = dev-api-key-here
|
|
124
|
+
var debug = true
|
|
125
|
+
|
|
126
|
+
# Production environment
|
|
127
|
+
[env:prod]
|
|
128
|
+
var apiKey = prod-api-key-here
|
|
129
|
+
var debug = false
|
|
130
|
+
`;
|
|
131
|
+
try {
|
|
132
|
+
await vscode.workspace.fs.writeFile(envFilePath, Buffer.from(template, 'utf8'));
|
|
133
|
+
const doc = await vscode.workspace.openTextDocument(envFilePath);
|
|
134
|
+
await vscode.window.showTextDocument(doc);
|
|
135
|
+
vscode.commands.executeCommand('norn.refreshCodeLenses');
|
|
136
|
+
vscode.window.showInformationMessage('Created .nornenv file. Select an environment from the status bar.');
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
vscode.window.showErrorMessage(`Failed to create .nornenv: ${error}`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// Register command to import endpoints from Swagger/OpenAPI spec
|
|
143
|
+
const importSwaggerCommand = vscode.commands.registerCommand('norn.importSwagger', async (swaggerUrl, lineNumber) => {
|
|
144
|
+
const editor = vscode.window.activeTextEditor;
|
|
145
|
+
if (!editor) {
|
|
146
|
+
vscode.window.showErrorMessage('No active editor');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
// Show progress while fetching
|
|
151
|
+
await vscode.window.withProgress({
|
|
152
|
+
location: vscode.ProgressLocation.Notification,
|
|
153
|
+
title: 'Parsing OpenAPI spec...',
|
|
154
|
+
cancellable: false
|
|
155
|
+
}, async (progress) => {
|
|
156
|
+
progress.report({ message: 'Fetching specification...' });
|
|
157
|
+
// Parse the swagger spec
|
|
158
|
+
const spec = await (0, swaggerParser_1.parseSwaggerSpec)(swaggerUrl);
|
|
159
|
+
progress.report({ message: 'Spec loaded!' });
|
|
160
|
+
// Build QuickPick items for sections
|
|
161
|
+
const sectionItems = [
|
|
162
|
+
{ label: 'All', description: `Import all ${spec.sections.reduce((acc, s) => acc + s.endpoints.length, 0)} endpoints`, picked: true }
|
|
163
|
+
];
|
|
164
|
+
for (const section of spec.sections) {
|
|
165
|
+
sectionItems.push({
|
|
166
|
+
label: section.name,
|
|
167
|
+
description: section.description || `${section.endpoints.length} endpoints`
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Show section picker
|
|
171
|
+
const selectedSections = await vscode.window.showQuickPick(sectionItems, {
|
|
172
|
+
canPickMany: true,
|
|
173
|
+
placeHolder: `Select sections to import from ${spec.title}`,
|
|
174
|
+
title: 'Import OpenAPI Endpoints'
|
|
175
|
+
});
|
|
176
|
+
if (!selectedSections || selectedSections.length === 0) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const sectionNames = selectedSections.map(s => s.label);
|
|
180
|
+
// Ask for custom baseUrl
|
|
181
|
+
const baseUrlInput = await vscode.window.showInputBox({
|
|
182
|
+
prompt: 'Enter custom base URL (or leave empty to use spec default)',
|
|
183
|
+
placeHolder: spec.baseUrl || 'https://api.example.com',
|
|
184
|
+
value: spec.baseUrl
|
|
185
|
+
});
|
|
186
|
+
// User cancelled
|
|
187
|
+
if (baseUrlInput === undefined) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Generate the content
|
|
191
|
+
const content = (0, swaggerParser_1.generateNornapiContent)(spec, sectionNames, baseUrlInput || undefined);
|
|
192
|
+
// Find position to insert - after the swagger line
|
|
193
|
+
const document = editor.document;
|
|
194
|
+
const swaggerLine = document.lineAt(lineNumber);
|
|
195
|
+
const insertPosition = new vscode.Position(lineNumber + 1, 0);
|
|
196
|
+
// Insert the generated content
|
|
197
|
+
await editor.edit(editBuilder => {
|
|
198
|
+
editBuilder.insert(insertPosition, '\n' + content + '\n');
|
|
199
|
+
});
|
|
200
|
+
vscode.window.showInformationMessage(`Imported ${sectionNames.includes('All') ? 'all sections' : sectionNames.join(', ')} from ${spec.title}`);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
205
|
+
vscode.window.showErrorMessage(`Failed to parse OpenAPI spec: ${errorMessage}`);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// Register command to generate JSON Schemas from Swagger/OpenAPI spec
|
|
209
|
+
const generateSchemasFromSwaggerCommand = vscode.commands.registerCommand('norn.generateSchemasFromSwagger', async (swaggerUrl, sourceFile) => {
|
|
210
|
+
try {
|
|
211
|
+
const schemas = await vscode.window.withProgress({
|
|
212
|
+
location: vscode.ProgressLocation.Notification,
|
|
213
|
+
title: 'Extracting schemas from OpenAPI spec...',
|
|
214
|
+
cancellable: false
|
|
215
|
+
}, async (progress) => {
|
|
216
|
+
progress.report({ message: 'Fetching specification...' });
|
|
217
|
+
return await (0, swaggerParser_1.extractResponseSchemas)(swaggerUrl);
|
|
218
|
+
});
|
|
219
|
+
if (schemas.length === 0) {
|
|
220
|
+
vscode.window.showWarningMessage('No response schemas found in the OpenAPI spec');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Ask user for output folder
|
|
224
|
+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
225
|
+
const defaultFolder = workspaceRoot ? path.join(workspaceRoot, 'contracts') : path.dirname(sourceFile);
|
|
226
|
+
const folderItems = [
|
|
227
|
+
{ label: 'contracts/', description: `${defaultFolder}`, picked: true },
|
|
228
|
+
{ label: 'schemas/', description: path.join(path.dirname(sourceFile), 'schemas') },
|
|
229
|
+
{ label: 'Choose folder...', description: 'Select a custom folder' }
|
|
230
|
+
];
|
|
231
|
+
const selectedFolder = await vscode.window.showQuickPick(folderItems, {
|
|
232
|
+
placeHolder: 'Select output folder for schema files',
|
|
233
|
+
title: `Generate ${schemas.length} Schemas`
|
|
234
|
+
});
|
|
235
|
+
if (!selectedFolder) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
let outputFolder;
|
|
239
|
+
if (selectedFolder.label === 'Choose folder...') {
|
|
240
|
+
const folderUri = await vscode.window.showOpenDialog({
|
|
241
|
+
canSelectFiles: false,
|
|
242
|
+
canSelectFolders: true,
|
|
243
|
+
canSelectMany: false,
|
|
244
|
+
title: 'Select output folder for schemas'
|
|
245
|
+
});
|
|
246
|
+
if (!folderUri || folderUri.length === 0) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
outputFolder = folderUri[0].fsPath;
|
|
250
|
+
}
|
|
251
|
+
else if (selectedFolder.label === 'contracts/') {
|
|
252
|
+
outputFolder = defaultFolder;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
outputFolder = path.join(path.dirname(sourceFile), 'schemas');
|
|
256
|
+
}
|
|
257
|
+
// Create folder if it doesn't exist
|
|
258
|
+
if (!fsSync.existsSync(outputFolder)) {
|
|
259
|
+
fsSync.mkdirSync(outputFolder, { recursive: true });
|
|
260
|
+
}
|
|
261
|
+
// Filter schemas to generate (allow user to select)
|
|
262
|
+
const schemaItems = schemas.map((s) => ({
|
|
263
|
+
label: (0, swaggerParser_1.generateSchemaFilename)(s.operationId, s.statusCode),
|
|
264
|
+
description: `${s.method} ${s.path} → ${s.statusCode}`,
|
|
265
|
+
picked: s.statusCode === '200' // Default select 200 responses
|
|
266
|
+
}));
|
|
267
|
+
const selectedSchemas = await vscode.window.showQuickPick(schemaItems, {
|
|
268
|
+
canPickMany: true,
|
|
269
|
+
placeHolder: 'Select schemas to generate',
|
|
270
|
+
title: `${schemas.length} response schemas available`
|
|
271
|
+
});
|
|
272
|
+
if (!selectedSchemas || selectedSchemas.length === 0) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const selectedFilenames = new Set(selectedSchemas.map(s => s.label));
|
|
276
|
+
// Write schema files
|
|
277
|
+
let createdCount = 0;
|
|
278
|
+
for (const schema of schemas) {
|
|
279
|
+
const filename = (0, swaggerParser_1.generateSchemaFilename)(schema.operationId, schema.statusCode);
|
|
280
|
+
if (!selectedFilenames.has(filename)) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const filePath = path.join(outputFolder, filename);
|
|
284
|
+
const content = JSON.stringify(schema.schema, null, 2);
|
|
285
|
+
fsSync.writeFileSync(filePath, content, 'utf-8');
|
|
286
|
+
createdCount++;
|
|
287
|
+
}
|
|
288
|
+
vscode.window.showInformationMessage(`Generated ${createdCount} schema files in ${path.basename(outputFolder)}/`);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
292
|
+
vscode.window.showErrorMessage(`Failed to generate schemas: ${errorMessage}`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
// === Coverage Feature Setup ===
|
|
296
|
+
// Create coverage status bar item
|
|
297
|
+
const coverageStatusBarItem = (0, environmentProvider_1.createCoverageStatusBarItem)();
|
|
298
|
+
// Register command to show coverage panel
|
|
299
|
+
const showCoverageCommand = vscode.commands.registerCommand('norn.showCoverage', async () => {
|
|
300
|
+
try {
|
|
301
|
+
const coverage = await (0, coverageCalculator_1.getCoverage)();
|
|
302
|
+
coveragePanel_1.CoveragePanel.show(coverage);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
306
|
+
vscode.window.showErrorMessage(`Failed to calculate coverage: ${errorMessage}`);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
// Register command to refresh coverage
|
|
310
|
+
const refreshCoverageCommand = vscode.commands.registerCommand('norn.refreshCoverage', async () => {
|
|
311
|
+
try {
|
|
312
|
+
(0, swaggerParser_1.invalidateSwaggerCache)();
|
|
313
|
+
const coverage = await (0, coverageCalculator_1.refreshCoverage)();
|
|
314
|
+
coveragePanel_1.CoveragePanel.updateContent(coverage);
|
|
315
|
+
vscode.window.showInformationMessage(`Coverage refreshed: ${coverage.percentage}%`);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
319
|
+
vscode.window.showErrorMessage(`Failed to refresh coverage: ${errorMessage}`);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
// Listen for coverage updates to update status bar and CodeLens
|
|
323
|
+
const coverageUpdateListener = (0, coverageCalculator_1.onCoverageUpdate)((coverage) => {
|
|
324
|
+
(0, environmentProvider_1.updateCoverageStatusBar)(coverage.hasSwagger, coverage.percentage, coverage.total, coverage.covered);
|
|
325
|
+
(0, codeLensProvider_1.updateCodeLensCoverage)(coverage.percentage, coverage.total, coverage.covered);
|
|
326
|
+
vscode.commands.executeCommand('norn.refreshCodeLenses');
|
|
327
|
+
});
|
|
328
|
+
// File watcher for .nornapi files - only recalculate when files are created/deleted
|
|
329
|
+
// (might add new swagger URLs that need fetching)
|
|
330
|
+
const nornapiFileWatcher = vscode.workspace.createFileSystemWatcher('**/*.nornapi');
|
|
331
|
+
const scheduleNornapiRefresh = debounce(async () => {
|
|
332
|
+
(0, coverageCalculator_1.clearCoverageCache)();
|
|
333
|
+
// Recalculate - this will fetch new swagger URLs if detected
|
|
334
|
+
try {
|
|
335
|
+
await (0, coverageCalculator_1.getCoverage)();
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
// Ignore errors in background refresh
|
|
339
|
+
}
|
|
340
|
+
}, 1000);
|
|
341
|
+
// Only watch for new/deleted .nornapi files (new swagger URLs)
|
|
342
|
+
nornapiFileWatcher.onDidCreate(() => scheduleNornapiRefresh());
|
|
343
|
+
nornapiFileWatcher.onDidDelete(() => scheduleNornapiRefresh());
|
|
344
|
+
// Watch for .norn and .nornapi file saves to update coverage
|
|
345
|
+
const scheduleCoverageUpdate = debounce(async () => {
|
|
346
|
+
(0, coverageCalculator_1.clearCoverageCache)();
|
|
347
|
+
try {
|
|
348
|
+
await (0, coverageCalculator_1.getCoverage)();
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// Ignore errors in background refresh
|
|
352
|
+
}
|
|
353
|
+
}, 500);
|
|
354
|
+
const documentSaveWatcher = vscode.workspace.onDidSaveTextDocument((document) => {
|
|
355
|
+
if (document.languageId === 'norn' || document.languageId === 'nornapi') {
|
|
356
|
+
scheduleCoverageUpdate();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
// Initial coverage calculation (delayed to not slow down activation)
|
|
360
|
+
setTimeout(async () => {
|
|
361
|
+
try {
|
|
362
|
+
await (0, coverageCalculator_1.getCoverage)();
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Ignore errors in initial coverage calculation
|
|
366
|
+
}
|
|
367
|
+
}, 2000);
|
|
368
|
+
// Create status bar item for environment
|
|
369
|
+
const statusBarItem = (0, environmentProvider_1.createStatusBarItem)();
|
|
370
|
+
// Register the CodeLens provider for .norn and .nornapi files
|
|
371
|
+
const codeLensProvider = vscode.languages.registerCodeLensProvider([
|
|
372
|
+
{ language: 'norn', scheme: 'file' },
|
|
373
|
+
{ language: 'nornapi', scheme: 'file' }
|
|
374
|
+
], new codeLensProvider_1.HttpCodeLensProvider());
|
|
375
|
+
// Register the Completion provider for IntelliSense
|
|
376
|
+
const completionProvider = vscode.languages.registerCompletionItemProvider([
|
|
377
|
+
{ language: 'norn', scheme: 'file' },
|
|
378
|
+
{ language: 'nornapi', scheme: 'file' }
|
|
379
|
+
], new completionProvider_1.HttpCompletionProvider(),
|
|
380
|
+
// Trigger characters:
|
|
381
|
+
// - '{' for variable references {{
|
|
382
|
+
// - '.' for property access ($N.body, user.body)
|
|
383
|
+
// - ' ' for after keywords (print, run, etc.)
|
|
384
|
+
// - '(' and ',' for parameter suggestions
|
|
385
|
+
// - '@' for sequence tags
|
|
386
|
+
// - ':' for header values (Content-Type: ...)
|
|
387
|
+
// - Letters for typing variable names inside () or after print
|
|
388
|
+
'{', '.', ' ', '(', ',', '@', ':', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_');
|
|
389
|
+
// Register the Diagnostic provider for undefined variables
|
|
390
|
+
const diagnosticProvider = new diagnosticProvider_1.DiagnosticProvider();
|
|
391
|
+
// Register command to refresh diagnostics (called when environment changes)
|
|
392
|
+
const refreshDiagnosticsCommand = vscode.commands.registerCommand('norn.refreshDiagnostics', () => {
|
|
393
|
+
vscode.workspace.textDocuments.forEach((document) => {
|
|
394
|
+
if (document.languageId === 'norn' || document.languageId === 'nornenv' || document.languageId === 'nornapi') {
|
|
395
|
+
diagnosticProvider.updateDiagnostics(document);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
// Register command to open Contract View for schema validation errors
|
|
400
|
+
const openContractViewCommand = vscode.commands.registerCommand('norn.openContractView', (data) => {
|
|
401
|
+
responsePanel_1.ResponsePanel.showContractView(context.extensionUri, data);
|
|
402
|
+
});
|
|
403
|
+
// Register command to open a schema file from CodeLens
|
|
404
|
+
const openSchemaFileCommand = vscode.commands.registerCommand('norn.openSchemaFile', async (schemaPath, sourceFile) => {
|
|
405
|
+
try {
|
|
406
|
+
// Get workspace root for @/ alias resolution
|
|
407
|
+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
408
|
+
const sourceDir = path.dirname(sourceFile);
|
|
409
|
+
// Resolve the schema path (handles @/ alias for contracts folder)
|
|
410
|
+
const resolvedPath = (0, schemaGenerator_1.resolveSchemaPath)(schemaPath, sourceDir, workspaceRoot);
|
|
411
|
+
// Open the schema file
|
|
412
|
+
const uri = vscode.Uri.file(resolvedPath);
|
|
413
|
+
await vscode.commands.executeCommand('vscode.open', uri);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
vscode.window.showErrorMessage(`Could not open schema file: ${schemaPath}`);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
// Register command to validate a schema assertion immediately
|
|
420
|
+
const validateSchemaAssertionNowCommand = vscode.commands.registerCommand('norn.validateSchemaAssertionNow', async (line, sourceFile) => {
|
|
421
|
+
try {
|
|
422
|
+
// Find the enclosing sequence and run it
|
|
423
|
+
const editor = vscode.window.activeTextEditor;
|
|
424
|
+
if (!editor || editor.document.uri.fsPath !== sourceFile) {
|
|
425
|
+
// Open the file if not already open
|
|
426
|
+
const doc = await vscode.workspace.openTextDocument(sourceFile);
|
|
427
|
+
await vscode.window.showTextDocument(doc);
|
|
428
|
+
}
|
|
429
|
+
// Run the sequence at this line
|
|
430
|
+
await vscode.commands.executeCommand('norn.runSequence', line);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
vscode.window.showErrorMessage(`Validation failed: ${error}`);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
// Register command to view contract validation report
|
|
437
|
+
const viewContractReportCommand = vscode.commands.registerCommand('norn.viewContractReport', async (line, sourceFile, schemaPath) => {
|
|
438
|
+
try {
|
|
439
|
+
const editor = vscode.window.activeTextEditor;
|
|
440
|
+
if (!editor || editor.document.uri.fsPath !== sourceFile) {
|
|
441
|
+
const doc = await vscode.workspace.openTextDocument(sourceFile);
|
|
442
|
+
await vscode.window.showTextDocument(doc);
|
|
443
|
+
}
|
|
444
|
+
const activeEditor = vscode.window.activeTextEditor;
|
|
445
|
+
if (!activeEditor) {
|
|
446
|
+
vscode.window.showErrorMessage('No active editor');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// Run the sequence and capture the result
|
|
450
|
+
const fullText = activeEditor.document.getText();
|
|
451
|
+
const workingDir = path.dirname(activeEditor.document.uri.fsPath);
|
|
452
|
+
const sequence = (0, sequenceRunner_1.getSequenceAtLine)(fullText, line);
|
|
453
|
+
if (!sequence) {
|
|
454
|
+
vscode.window.showErrorMessage('No sequence found at this line');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// Get variables and run imports
|
|
458
|
+
const fileLevelVariables = (0, parser_1.extractFileLevelVariables)(fullText);
|
|
459
|
+
const envVars = (0, environmentProvider_1.getEnvironmentVariables)();
|
|
460
|
+
const allVariables = { ...envVars, ...fileLevelVariables };
|
|
461
|
+
const importResult = await (0, parser_1.resolveImports)(fullText, workingDir, async (filePath) => {
|
|
462
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
463
|
+
return content;
|
|
464
|
+
});
|
|
465
|
+
const fullTextWithImports = importResult.importedContent
|
|
466
|
+
? `${importResult.importedContent}\n\n${fullText}`
|
|
467
|
+
: fullText;
|
|
468
|
+
const apiDefinitions = (importResult.headerGroups && importResult.headerGroups.length > 0) ||
|
|
469
|
+
(importResult.endpoints && importResult.endpoints.length > 0)
|
|
470
|
+
? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
|
|
471
|
+
: undefined;
|
|
472
|
+
// Run the sequence
|
|
473
|
+
const result = await (0, sequenceRunner_1.runSequence)(sequence.content, allVariables, workingDir, fullTextWithImports, undefined, // no progress callback
|
|
474
|
+
undefined, // callStack
|
|
475
|
+
undefined, // defaultArgs
|
|
476
|
+
apiDefinitions, undefined, // tagFilterOptions
|
|
477
|
+
importResult.sequenceSources // track source files for script path resolution
|
|
478
|
+
);
|
|
479
|
+
// Find the matchesSchema assertion step that corresponds to this line
|
|
480
|
+
const assertionLineRelative = line - sequence.startLine - 1;
|
|
481
|
+
let matchingStep = result.steps.find(s => s.type === 'assertion' &&
|
|
482
|
+
s.assertion?.operator === 'matchesSchema' &&
|
|
483
|
+
s.lineNumber !== undefined &&
|
|
484
|
+
Math.abs(s.lineNumber - assertionLineRelative) <= 1);
|
|
485
|
+
// If not found by line, try to find any matchesSchema assertion with matching schema path
|
|
486
|
+
if (!matchingStep) {
|
|
487
|
+
matchingStep = result.steps.find(s => s.type === 'assertion' &&
|
|
488
|
+
s.assertion?.operator === 'matchesSchema' &&
|
|
489
|
+
(s.assertion?.schemaPath?.includes(schemaPath) || s.assertion?.rightValue?.includes(schemaPath)));
|
|
490
|
+
}
|
|
491
|
+
if (!matchingStep || !matchingStep.assertion) {
|
|
492
|
+
vscode.window.showErrorMessage('Could not find the schema assertion result. Try running the full sequence first.');
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// Get the response body used in the assertion
|
|
496
|
+
const responseBody = matchingStep.assertion.leftValue;
|
|
497
|
+
const schema = matchingStep.assertion.schema;
|
|
498
|
+
const errors = matchingStep.assertion.schemaErrors || [];
|
|
499
|
+
const resolvedSchemaPath = matchingStep.assertion.schemaPath || schemaPath;
|
|
500
|
+
// Show the Contract Report
|
|
501
|
+
responsePanel_1.ResponsePanel.showContractReport(context.extensionUri, {
|
|
502
|
+
schemaPath: resolvedSchemaPath,
|
|
503
|
+
errors,
|
|
504
|
+
responseBody,
|
|
505
|
+
schema,
|
|
506
|
+
assertionLine: line,
|
|
507
|
+
sourceFile
|
|
508
|
+
});
|
|
509
|
+
// Save validation results and refresh decorations
|
|
510
|
+
saveSchemaValidationResults(result, activeEditor.document.uri.fsPath, sequence.startLine + 1);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
vscode.window.showErrorMessage(`Failed to generate contract report: ${error}`);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
// Register the Contract Decoration provider for schema assertion status indicators
|
|
517
|
+
const contractDecorationProvider = new contractDecorationProvider_1.ContractDecorationProvider();
|
|
518
|
+
contractDecorationProviderInstance = contractDecorationProvider;
|
|
519
|
+
// Register hover provider for schema assertion lines
|
|
520
|
+
const contractHoverProvider = vscode.languages.registerHoverProvider({ language: 'norn' }, (0, contractDecorationProvider_1.createContractHoverProvider)());
|
|
521
|
+
// Update diagnostics when document changes
|
|
522
|
+
const onDidChangeDocument = vscode.workspace.onDidChangeTextDocument((event) => {
|
|
523
|
+
if (event.document.languageId === 'norn' || event.document.languageId === 'nornenv' || event.document.languageId === 'nornapi') {
|
|
524
|
+
diagnosticProvider.updateDiagnostics(event.document);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
// Update diagnostics when document opens
|
|
528
|
+
const onDidOpenDocument = vscode.workspace.onDidOpenTextDocument((document) => {
|
|
529
|
+
if (document.languageId === 'norn' || document.languageId === 'nornenv' || document.languageId === 'nornapi') {
|
|
530
|
+
diagnosticProvider.updateDiagnostics(document);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
// Clear diagnostics when document closes
|
|
534
|
+
const onDidCloseDocument = vscode.workspace.onDidCloseTextDocument((document) => {
|
|
535
|
+
diagnosticProvider.clearDiagnostics(document);
|
|
536
|
+
});
|
|
537
|
+
// Update diagnostics for already open documents
|
|
538
|
+
vscode.workspace.textDocuments.forEach((document) => {
|
|
539
|
+
if (document.languageId === 'norn' || document.languageId === 'nornenv' || document.languageId === 'nornapi') {
|
|
540
|
+
diagnosticProvider.updateDiagnostics(document);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
context.subscriptions.push(sendRequestCommand, runSequenceCommand, clearCookiesCommand, showCookiesCommand, selectEnvironmentCommand, selectEnvironmentAndRefreshCommand, createEnvFileCommand, importSwaggerCommand, generateSchemasFromSwaggerCommand, refreshDiagnosticsCommand, openContractViewCommand, openSchemaFileCommand, validateSchemaAssertionNowCommand, viewContractReportCommand, contractDecorationProvider, contractHoverProvider, showCoverageCommand, refreshCoverageCommand, coverageUpdateListener, statusBarItem, coverageStatusBarItem, codeLensProvider, completionProvider, diagnosticProvider, onDidChangeDocument, onDidOpenDocument, onDidCloseDocument, nornapiFileWatcher, documentSaveWatcher, { dispose: environmentProvider_1.disposeStatusBar });
|
|
544
|
+
}
|
|
545
|
+
function formatSequenceCaseLabel(caseParams, index) {
|
|
546
|
+
const entries = Object.entries(caseParams);
|
|
547
|
+
if (entries.length === 0) {
|
|
548
|
+
return `Case ${index + 1}`;
|
|
549
|
+
}
|
|
550
|
+
const formatted = entries.map(([key, value]) => {
|
|
551
|
+
if (typeof value === 'string') {
|
|
552
|
+
return `${key}="${value}"`;
|
|
553
|
+
}
|
|
554
|
+
return `${key}=${value}`;
|
|
555
|
+
});
|
|
556
|
+
return `[${formatted.join(', ')}]`;
|
|
557
|
+
}
|
|
558
|
+
function normalizeTheoryCase(caseValue, sequenceParamNames) {
|
|
559
|
+
if (caseValue && typeof caseValue === 'object' && !Array.isArray(caseValue)) {
|
|
560
|
+
return caseValue;
|
|
561
|
+
}
|
|
562
|
+
if (sequenceParamNames.length === 1) {
|
|
563
|
+
return { [sequenceParamNames[0]]: caseValue };
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
async function loadTheoryCasesFromSource(sourcePath, workingDir, sequence) {
|
|
568
|
+
const resolvedPath = path.resolve(workingDir, sourcePath);
|
|
569
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
570
|
+
const parsed = JSON.parse(content);
|
|
571
|
+
if (!Array.isArray(parsed)) {
|
|
572
|
+
throw new Error(`Theory file "${sourcePath}" must contain a JSON array of test cases`);
|
|
573
|
+
}
|
|
574
|
+
const paramNames = sequence.parameters.map(p => p.name);
|
|
575
|
+
const cases = [];
|
|
576
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
577
|
+
const normalized = normalizeTheoryCase(parsed[i], paramNames);
|
|
578
|
+
if (!normalized) {
|
|
579
|
+
throw new Error(`Theory file "${sourcePath}" has invalid case at index ${i}. ` +
|
|
580
|
+
`Use objects for multi-parameter sequences.`);
|
|
581
|
+
}
|
|
582
|
+
cases.push(normalized);
|
|
583
|
+
}
|
|
584
|
+
return cases;
|
|
585
|
+
}
|
|
586
|
+
async function resolveSequenceRunCases(sequence, workingDir, defaultArgs) {
|
|
587
|
+
const inlineCases = sequence.theoryData?.cases || [];
|
|
588
|
+
let finalCases = [];
|
|
589
|
+
if (inlineCases.length > 0) {
|
|
590
|
+
finalCases = inlineCases;
|
|
591
|
+
}
|
|
592
|
+
else if (sequence.theoryData?.source) {
|
|
593
|
+
finalCases = await loadTheoryCasesFromSource(sequence.theoryData.source, workingDir, sequence);
|
|
594
|
+
}
|
|
595
|
+
if (finalCases.length === 0) {
|
|
596
|
+
return [{ args: { ...defaultArgs } }];
|
|
597
|
+
}
|
|
598
|
+
return finalCases.map((caseParams, index) => {
|
|
599
|
+
const args = { ...defaultArgs };
|
|
600
|
+
for (const [key, value] of Object.entries(caseParams)) {
|
|
601
|
+
if (value !== undefined) {
|
|
602
|
+
args[key] = String(value);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
label: formatSequenceCaseLabel(caseParams, index),
|
|
607
|
+
args
|
|
608
|
+
};
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async function processSequence(extensionUri, lineFromCodeLens) {
|
|
612
|
+
const editor = vscode.window.activeTextEditor;
|
|
613
|
+
if (!editor) {
|
|
614
|
+
vscode.window.showErrorMessage("No active editor");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const fullText = editor.document.getText();
|
|
618
|
+
const targetLine = lineFromCodeLens ?? editor.selection.active.line;
|
|
619
|
+
const sequence = (0, sequenceRunner_1.getSequenceAtLine)(fullText, targetLine);
|
|
620
|
+
if (!sequence) {
|
|
621
|
+
vscode.window.showErrorMessage("No sequence found at cursor position");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
// Merge environment variables with file-level variables (outside sequences)
|
|
625
|
+
// Variables inside sequences are local to that sequence
|
|
626
|
+
const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
|
|
627
|
+
const fileVariables = (0, parser_1.extractFileLevelVariables)(fullText);
|
|
628
|
+
const allVariables = { ...envVariables, ...fileVariables };
|
|
629
|
+
// Apply default parameter values for sequences with all-optional parameters
|
|
630
|
+
const defaultArgs = {};
|
|
631
|
+
if (sequence.parameters) {
|
|
632
|
+
for (const param of sequence.parameters) {
|
|
633
|
+
if (param.defaultValue !== undefined) {
|
|
634
|
+
defaultArgs[param.name] = param.defaultValue;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Get working directory from the file's location
|
|
639
|
+
const workingDir = path.dirname(editor.document.uri.fsPath);
|
|
640
|
+
// Resolve imports to get the full document text with imported requests/sequences
|
|
641
|
+
const importResult = await (0, parser_1.resolveImports)(fullText, workingDir, async (filePath) => {
|
|
642
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
643
|
+
return content;
|
|
644
|
+
});
|
|
645
|
+
// Show import errors - duplicate errors block execution, others are warnings
|
|
646
|
+
const duplicateErrors = importResult.errors.filter(err => err.error.includes('Duplicate header group') ||
|
|
647
|
+
err.error.includes('Duplicate endpoint') ||
|
|
648
|
+
err.error.includes('Duplicate named request') ||
|
|
649
|
+
err.error.includes('Duplicate sequence'));
|
|
650
|
+
const otherErrors = importResult.errors.filter(err => !err.error.includes('Duplicate header group') &&
|
|
651
|
+
!err.error.includes('Duplicate endpoint') &&
|
|
652
|
+
!err.error.includes('Duplicate named request') &&
|
|
653
|
+
!err.error.includes('Duplicate sequence'));
|
|
654
|
+
// Show non-duplicate errors as warnings
|
|
655
|
+
for (const err of otherErrors) {
|
|
656
|
+
vscode.window.showWarningMessage(`Import error: ${err.path} - ${err.error}`);
|
|
657
|
+
}
|
|
658
|
+
// Duplicate errors block execution
|
|
659
|
+
if (duplicateErrors.length > 0) {
|
|
660
|
+
vscode.window.showErrorMessage(`Cannot run sequence - duplicate definitions found:\n${duplicateErrors[0].error}`);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
// Combine original text with imported content
|
|
664
|
+
const fullTextWithImports = importResult.importedContent
|
|
665
|
+
? `${importResult.importedContent}\n\n${fullText}`
|
|
666
|
+
: fullText;
|
|
667
|
+
// Build API definitions from imports (header groups and endpoints from .nornapi files)
|
|
668
|
+
const apiDefinitions = (importResult.headerGroups && importResult.headerGroups.length > 0) ||
|
|
669
|
+
(importResult.endpoints && importResult.endpoints.length > 0)
|
|
670
|
+
? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
|
|
671
|
+
: undefined;
|
|
672
|
+
let executedRuns = false;
|
|
673
|
+
try {
|
|
674
|
+
const runCases = await resolveSequenceRunCases(sequence, workingDir, defaultArgs);
|
|
675
|
+
if (runCases.length <= 1) {
|
|
676
|
+
// Single run: keep streaming behavior.
|
|
677
|
+
const totalSteps = (0, sequenceRunner_1.countSequenceSteps)(sequence.content);
|
|
678
|
+
responsePanel_1.ResponsePanel.startSequenceRun(extensionUri, sequence.name, totalSteps, editor.document.uri.fsPath);
|
|
679
|
+
try {
|
|
680
|
+
executedRuns = true;
|
|
681
|
+
const runArgs = runCases[0]?.args ?? defaultArgs;
|
|
682
|
+
const result = await (0, sequenceRunner_1.runSequence)(sequence.content, allVariables, workingDir, fullTextWithImports, (progressInfo) => {
|
|
683
|
+
// Handle sequence start/end markers for grouping
|
|
684
|
+
if (progressInfo.stepType === 'sequenceStart' || progressInfo.stepType === 'sequenceEnd') {
|
|
685
|
+
responsePanel_1.ResponsePanel.addSequenceStep(undefined, progressInfo);
|
|
686
|
+
}
|
|
687
|
+
// Stream each step result to the panel as it completes
|
|
688
|
+
else if (progressInfo.stepResult) {
|
|
689
|
+
responsePanel_1.ResponsePanel.addSequenceStep(progressInfo.stepResult, progressInfo);
|
|
690
|
+
}
|
|
691
|
+
}, undefined, // callStack
|
|
692
|
+
runArgs, apiDefinitions, // header groups and endpoints from .nornapi imports
|
|
693
|
+
undefined, // tagFilterOptions
|
|
694
|
+
importResult.sequenceSources // track source files for script path resolution
|
|
695
|
+
);
|
|
696
|
+
result.name = sequence.name;
|
|
697
|
+
responsePanel_1.ResponsePanel.finalizeSequence(result);
|
|
698
|
+
// Save validation results for schema assertions and refresh decorations
|
|
699
|
+
// Add sequence.startLine + 1 to convert relative line numbers to absolute file line numbers
|
|
700
|
+
// (+1 because the sequence content starts after the "sequence ..." declaration line)
|
|
701
|
+
saveSchemaValidationResults(result, editor.document.uri.fsPath, sequence.startLine + 1);
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
705
|
+
vscode.window.showErrorMessage(`Sequence failed: ${errorMessage}`);
|
|
706
|
+
// Finalize with error state
|
|
707
|
+
responsePanel_1.ResponsePanel.finalizeSequence({
|
|
708
|
+
name: sequence.name,
|
|
709
|
+
success: false,
|
|
710
|
+
responses: [],
|
|
711
|
+
scriptResults: [],
|
|
712
|
+
assertionResults: [],
|
|
713
|
+
steps: [],
|
|
714
|
+
errors: [errorMessage],
|
|
715
|
+
duration: 0
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
// Parameterized runs: execute all cases and show combined report in one panel.
|
|
721
|
+
const caseResults = [];
|
|
722
|
+
await vscode.window.withProgress({
|
|
723
|
+
location: vscode.ProgressLocation.Notification,
|
|
724
|
+
title: `Running ${sequence.name} (${runCases.length} cases)`,
|
|
725
|
+
cancellable: false
|
|
726
|
+
}, async (progress) => {
|
|
727
|
+
for (let i = 0; i < runCases.length; i++) {
|
|
728
|
+
const runCase = runCases[i];
|
|
729
|
+
const runLabel = runCase.label || `Case ${i + 1}`;
|
|
730
|
+
progress.report({
|
|
731
|
+
message: runLabel,
|
|
732
|
+
increment: 100 / runCases.length
|
|
733
|
+
});
|
|
734
|
+
try {
|
|
735
|
+
executedRuns = true;
|
|
736
|
+
const result = await (0, sequenceRunner_1.runSequence)(sequence.content, allVariables, workingDir, fullTextWithImports, undefined, // no per-step streaming in multi-run mode
|
|
737
|
+
undefined, // callStack
|
|
738
|
+
runCase.args, apiDefinitions, undefined, importResult.sequenceSources);
|
|
739
|
+
result.name = sequence.name;
|
|
740
|
+
saveSchemaValidationResults(result, editor.document.uri.fsPath, sequence.startLine + 1);
|
|
741
|
+
caseResults.push({ label: runLabel, result });
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
745
|
+
caseResults.push({
|
|
746
|
+
label: runLabel,
|
|
747
|
+
result: {
|
|
748
|
+
name: sequence.name,
|
|
749
|
+
success: false,
|
|
750
|
+
responses: [],
|
|
751
|
+
scriptResults: [],
|
|
752
|
+
assertionResults: [],
|
|
753
|
+
steps: [],
|
|
754
|
+
errors: [errorMessage],
|
|
755
|
+
duration: 0
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
responsePanel_1.ResponsePanel.showMultiSequenceResult(extensionUri, sequence.name, caseResults);
|
|
762
|
+
}
|
|
763
|
+
if (executedRuns) {
|
|
764
|
+
// Recalculate coverage after sequence execution (uses cached swagger specs)
|
|
765
|
+
(0, coverageCalculator_1.recalculateCoverageAfterExecution)().catch(() => {
|
|
766
|
+
// Silently ignore coverage calculation errors
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
772
|
+
vscode.window.showErrorMessage(`Sequence failed: ${errorMessage}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Save schema validation results to cache and refresh decorations
|
|
777
|
+
*/
|
|
778
|
+
function saveSchemaValidationResults(result, sourceFile, lineOffset = 0) {
|
|
779
|
+
try {
|
|
780
|
+
// Find all matchesSchema assertion steps
|
|
781
|
+
for (const step of result.steps) {
|
|
782
|
+
if (step.type === 'assertion' && step.assertion?.operator === 'matchesSchema') {
|
|
783
|
+
const assertion = step.assertion;
|
|
784
|
+
const schemaPath = assertion.schemaPath || assertion.rightValue || '';
|
|
785
|
+
const status = assertion.passed ? 'pass' : 'fail';
|
|
786
|
+
const errorCount = assertion.schemaErrors?.length || 0;
|
|
787
|
+
const errorSummary = assertion.schemaErrors?.[0]?.message || assertion.error?.substring(0, 100);
|
|
788
|
+
// Only cache if we have a line number
|
|
789
|
+
if (step.lineNumber !== undefined) {
|
|
790
|
+
// Convert relative line number to absolute file line number using lineOffset
|
|
791
|
+
const absoluteLine = step.lineNumber + lineOffset;
|
|
792
|
+
(0, validationCache_1.saveValidationResult)({
|
|
793
|
+
schemaPath,
|
|
794
|
+
sourceFile,
|
|
795
|
+
assertionLine: absoluteLine,
|
|
796
|
+
status,
|
|
797
|
+
lastRunTime: new Date().toISOString(),
|
|
798
|
+
errorCount: status === 'fail' ? errorCount : undefined,
|
|
799
|
+
errorSummary: status === 'fail' ? errorSummary : undefined
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// Refresh decorations
|
|
805
|
+
contractDecorationProviderInstance?.refreshAllDecorations();
|
|
806
|
+
}
|
|
807
|
+
catch (e) {
|
|
808
|
+
// Silently ignore cache errors
|
|
809
|
+
console.error('Failed to save validation results:', e);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
async function processEditorsInput(extensionUri, lineFromCodeLens) {
|
|
813
|
+
const editor = vscode.window.activeTextEditor;
|
|
814
|
+
if (!editor) {
|
|
815
|
+
vscode.window.showErrorMessage("No active editor");
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const fullText = editor.document.getText();
|
|
819
|
+
const selection = editor.selection;
|
|
820
|
+
let requestText;
|
|
821
|
+
if (!selection.isEmpty) {
|
|
822
|
+
// User selected specific text
|
|
823
|
+
requestText = editor.document.getText(selection);
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
// Use line from CodeLens if provided, otherwise use cursor position
|
|
827
|
+
const targetLine = lineFromCodeLens ?? selection.active.line;
|
|
828
|
+
requestText = (0, parser_1.getRequestBlockAtLine)(fullText, targetLine);
|
|
829
|
+
}
|
|
830
|
+
// Get working directory from the file's location
|
|
831
|
+
const workingDir = path.dirname(editor.document.uri.fsPath);
|
|
832
|
+
try {
|
|
833
|
+
// Extract variables: environment + file (file takes precedence)
|
|
834
|
+
const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
|
|
835
|
+
const fileVariables = (0, parser_1.extractVariables)(fullText);
|
|
836
|
+
const variables = { ...envVariables, ...fileVariables };
|
|
837
|
+
// Resolve imports to get API definitions (endpoints and header groups)
|
|
838
|
+
const importResult = await (0, parser_1.resolveImports)(fullText, workingDir, async (filePath) => {
|
|
839
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
840
|
+
return content;
|
|
841
|
+
});
|
|
842
|
+
// Check for duplicate definition errors which should block execution
|
|
843
|
+
const duplicateErrors = importResult.errors.filter(err => err.error.includes('Duplicate header group') ||
|
|
844
|
+
err.error.includes('Duplicate endpoint') ||
|
|
845
|
+
err.error.includes('Duplicate named request') ||
|
|
846
|
+
err.error.includes('Duplicate sequence'));
|
|
847
|
+
if (duplicateErrors.length > 0) {
|
|
848
|
+
vscode.window.showErrorMessage(`Cannot run request - duplicate definitions found:\n${duplicateErrors[0].error}`);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
// Build API definitions from imports
|
|
852
|
+
const apiDefinitions = (importResult.headerGroups && importResult.headerGroups.length > 0) ||
|
|
853
|
+
(importResult.endpoints && importResult.endpoints.length > 0)
|
|
854
|
+
? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
|
|
855
|
+
: undefined;
|
|
856
|
+
let parsed;
|
|
857
|
+
let requestDescription;
|
|
858
|
+
// Check if this is an API request (uses endpoint syntax like GET GetPetById(1))
|
|
859
|
+
if (apiDefinitions &&
|
|
860
|
+
apiDefinitions.endpoints.length > 0 &&
|
|
861
|
+
(0, nornapiParser_1.isApiRequestLine)(requestText, apiDefinitions.endpoints)) {
|
|
862
|
+
const apiRequest = (0, nornapiParser_1.parseApiRequest)(requestText, apiDefinitions.endpoints, apiDefinitions.headerGroups);
|
|
863
|
+
if (apiRequest) {
|
|
864
|
+
const endpoint = (0, nornapiParser_1.getEndpoint)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, apiRequest.endpointName);
|
|
865
|
+
if (endpoint) {
|
|
866
|
+
// Substitute {{variable}} references in the endpoint path
|
|
867
|
+
let resolvedPath = endpoint.path;
|
|
868
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
869
|
+
resolvedPath = resolvedPath.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value));
|
|
870
|
+
}
|
|
871
|
+
// Substitute {param} placeholders with the values passed to the endpoint
|
|
872
|
+
for (const [paramName, paramValue] of Object.entries(apiRequest.params)) {
|
|
873
|
+
// Resolve variable references in param values
|
|
874
|
+
let resolvedValue = paramValue;
|
|
875
|
+
if (variables[paramValue] !== undefined) {
|
|
876
|
+
resolvedValue = String(variables[paramValue]);
|
|
877
|
+
}
|
|
878
|
+
else if (paramValue.startsWith('{{') && paramValue.endsWith('}}')) {
|
|
879
|
+
const varName = paramValue.slice(2, -2);
|
|
880
|
+
if (variables[varName] !== undefined) {
|
|
881
|
+
resolvedValue = String(variables[varName]);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
resolvedPath = resolvedPath.replace(`{${paramName}}`, resolvedValue);
|
|
885
|
+
}
|
|
886
|
+
// Collect headers from header groups
|
|
887
|
+
const combinedHeaders = {};
|
|
888
|
+
for (const groupName of apiRequest.headerGroupNames) {
|
|
889
|
+
const group = apiDefinitions.headerGroups.find(hg => hg.name === groupName);
|
|
890
|
+
if (group) {
|
|
891
|
+
const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, variables);
|
|
892
|
+
Object.assign(combinedHeaders, resolvedHeaders);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// Add inline headers (these take precedence over header groups)
|
|
896
|
+
for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
|
|
897
|
+
// Resolve variable references in header values
|
|
898
|
+
let resolved = headerValue;
|
|
899
|
+
resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
|
|
900
|
+
return variables[varName] !== undefined ? String(variables[varName]) : `{{${varName}}}`;
|
|
901
|
+
});
|
|
902
|
+
combinedHeaders[headerName] = resolved;
|
|
903
|
+
}
|
|
904
|
+
// Parse the request text to extract body (if any)
|
|
905
|
+
const parsedForBody = (0, parser_1.parserHttpRequest)(requestText, variables);
|
|
906
|
+
parsed = {
|
|
907
|
+
method: apiRequest.method,
|
|
908
|
+
url: resolvedPath,
|
|
909
|
+
headers: combinedHeaders,
|
|
910
|
+
body: parsedForBody.body
|
|
911
|
+
};
|
|
912
|
+
requestDescription = `${apiRequest.endpointName}(${Object.values(apiRequest.params).join(', ')}) → ${parsed.method} ${parsed.url}`;
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
throw new Error(`Unknown endpoint: ${apiRequest.endpointName}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
// Fallback to regular parsing
|
|
920
|
+
parsed = (0, parser_1.parserHttpRequest)(requestText, variables);
|
|
921
|
+
requestDescription = `${parsed.method} ${parsed.url}`;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
// Regular HTTP request
|
|
926
|
+
parsed = (0, parser_1.parserHttpRequest)(requestText, variables);
|
|
927
|
+
requestDescription = `${parsed.method} ${parsed.url}`;
|
|
928
|
+
// Check for header group names in the request text
|
|
929
|
+
// This allows: GET https://example.com Json Auth
|
|
930
|
+
// Or: GET https://example.com
|
|
931
|
+
// Json
|
|
932
|
+
// Auth
|
|
933
|
+
if (apiDefinitions && apiDefinitions.headerGroups.length > 0) {
|
|
934
|
+
const requestLines = requestText.split('\n');
|
|
935
|
+
const headerGroupNames = apiDefinitions.headerGroups.map(hg => hg.name);
|
|
936
|
+
const foundGroups = [];
|
|
937
|
+
for (const line of requestLines) {
|
|
938
|
+
const trimmed = line.trim();
|
|
939
|
+
// Skip empty lines, comments, variable lines
|
|
940
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('var ')) {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
// Check if this is the HTTP method line - look for header groups at the end
|
|
944
|
+
const methodMatch = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
|
|
945
|
+
if (methodMatch) {
|
|
946
|
+
const afterMethod = methodMatch[2];
|
|
947
|
+
const tokens = afterMethod.split(/\s+/);
|
|
948
|
+
// Scan from the end to find header groups
|
|
949
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
950
|
+
if (headerGroupNames.includes(tokens[i])) {
|
|
951
|
+
foundGroups.push(tokens[i]);
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
// Skip lines that look like headers (Name: Value)
|
|
960
|
+
if (/^[A-Za-z0-9\-_]+\s*:\s*.+$/.test(trimmed)) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
// Check if this line contains header group names (can be multiple space-separated)
|
|
964
|
+
const potentialGroups = trimmed.split(/\s+/);
|
|
965
|
+
for (const groupName of potentialGroups) {
|
|
966
|
+
if (headerGroupNames.includes(groupName)) {
|
|
967
|
+
foundGroups.push(groupName);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// Collect headers from all found groups and strip from URL
|
|
972
|
+
for (const groupName of foundGroups) {
|
|
973
|
+
const group = apiDefinitions.headerGroups.find(hg => hg.name === groupName);
|
|
974
|
+
if (group) {
|
|
975
|
+
const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, variables);
|
|
976
|
+
parsed.headers = { ...resolvedHeaders, ...parsed.headers };
|
|
977
|
+
}
|
|
978
|
+
// Strip from URL
|
|
979
|
+
const endPattern = new RegExp(`\\s+${groupName}$`);
|
|
980
|
+
parsed.url = parsed.url.replace(endPattern, '');
|
|
981
|
+
}
|
|
982
|
+
parsed.url = parsed.url.trim();
|
|
983
|
+
requestDescription = `${parsed.method} ${parsed.url}`;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
// Show loading indicator
|
|
987
|
+
await vscode.window.withProgress({
|
|
988
|
+
location: vscode.ProgressLocation.Notification,
|
|
989
|
+
title: `Sending ${parsed.method} request...`,
|
|
990
|
+
cancellable: false
|
|
991
|
+
}, async () => {
|
|
992
|
+
try {
|
|
993
|
+
const response = await (0, httpClient_1.sendRequest)(parsed);
|
|
994
|
+
responsePanel_1.ResponsePanel.show(extensionUri, parsed.method, parsed.url, response);
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
responsePanel_1.ResponsePanel.showError(extensionUri, parsed.method, parsed.url, error.message);
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
catch (error) {
|
|
1002
|
+
vscode.window.showErrorMessage(error.message);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Simple debounce helper
|
|
1007
|
+
*/
|
|
1008
|
+
function debounce(fn, delay) {
|
|
1009
|
+
let timeoutId;
|
|
1010
|
+
return ((...args) => {
|
|
1011
|
+
if (timeoutId) {
|
|
1012
|
+
clearTimeout(timeoutId);
|
|
1013
|
+
}
|
|
1014
|
+
timeoutId = setTimeout(() => {
|
|
1015
|
+
fn(...args);
|
|
1016
|
+
timeoutId = undefined;
|
|
1017
|
+
}, delay);
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
function deactivate() {
|
|
1021
|
+
// Clear caches on deactivation
|
|
1022
|
+
(0, swaggerParser_1.clearSwaggerCache)();
|
|
1023
|
+
(0, coverageCalculator_1.clearCoverageCache)();
|
|
1024
|
+
}
|
|
1025
|
+
//# sourceMappingURL=extension.js.map
|